mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-05-20 01:32:33 +02:00
CopyRequest -> Copy & CopyAs
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -51,7 +51,8 @@ keybindings:
|
||||
left: "left,h"
|
||||
right: "right,l"
|
||||
cycle_focus: "tab"
|
||||
copy_request: "ctrl+y"
|
||||
copy_as: "ctrl+y"
|
||||
copy: "y"
|
||||
send_to_replay: "ctrl+r"
|
||||
scroll_up: "pgup"
|
||||
scroll_down: "pgdown"
|
||||
|
||||
@@ -10,7 +10,8 @@ type GlobalKeys struct {
|
||||
Left string `mapstructure:"left"`
|
||||
Right string `mapstructure:"right"`
|
||||
CycleFocus string `mapstructure:"cycle_focus"`
|
||||
CopyRequest string `mapstructure:"copy_request"`
|
||||
CopyAs string `mapstructure:"copy_as"`
|
||||
Copy string `mapstructure:"copy"`
|
||||
SendToReplay string `mapstructure:"send_to_replay"`
|
||||
ScrollUp string `mapstructure:"scroll_up"`
|
||||
ScrollDown string `mapstructure:"scroll_down"`
|
||||
|
||||
@@ -15,7 +15,8 @@ type GlobalKeyMap struct {
|
||||
Left key.Binding
|
||||
Right key.Binding
|
||||
CycleFocus key.Binding
|
||||
CopyRequest key.Binding
|
||||
CopyAs key.Binding
|
||||
Copy key.Binding
|
||||
Escape key.Binding
|
||||
SendToReplay key.Binding
|
||||
ScrollUp key.Binding
|
||||
@@ -34,7 +35,8 @@ func newGlobalKeyMap(cfg config.GlobalKeys) GlobalKeyMap {
|
||||
Left: binding(cfg.Left, "scroll left"),
|
||||
Right: binding(cfg.Right, "scroll right"),
|
||||
CycleFocus: binding(cfg.CycleFocus, "cycle focus"),
|
||||
CopyRequest: binding(cfg.CopyRequest, "copy as..."),
|
||||
CopyAs: binding(cfg.CopyAs, "copy as..."),
|
||||
Copy: binding(cfg.Copy, "copy..."),
|
||||
Escape: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "cancel")),
|
||||
SendToReplay: binding(cfg.SendToReplay, "send to replay"),
|
||||
ScrollUp: binding(cfg.ScrollUp, "scroll up"),
|
||||
@@ -47,7 +49,7 @@ func (g GlobalKeyMap) Bindings() []key.Binding {
|
||||
return []key.Binding{
|
||||
g.Up, g.Down, g.Left, g.Right, g.CycleFocus,
|
||||
g.Quit, g.Escape, g.Help,
|
||||
g.OpenLogs, g.ToggleSidebar, g.CopyRequest,
|
||||
g.OpenLogs, g.ToggleSidebar, g.CopyAs, g.Copy,
|
||||
g.SendToReplay, g.SendToDiff,
|
||||
g.ScrollUp, g.ScrollDown,
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/anotherhadi/spilltea/internal/intercept"
|
||||
"github.com/anotherhadi/spilltea/internal/plugins"
|
||||
proxyPkg "github.com/anotherhadi/spilltea/internal/proxy"
|
||||
copyUI "github.com/anotherhadi/spilltea/internal/ui/components/copy"
|
||||
copyasUI "github.com/anotherhadi/spilltea/internal/ui/components/copyas"
|
||||
notificationsUI "github.com/anotherhadi/spilltea/internal/ui/components/notifications"
|
||||
diffUI "github.com/anotherhadi/spilltea/internal/ui/diff"
|
||||
@@ -66,6 +67,7 @@ type Model struct {
|
||||
pluginsPage pluginsUI.Model
|
||||
findingsPage findingsUI.Model
|
||||
copyAs copyasUI.Model
|
||||
copy copyUI.Model
|
||||
notifications notificationsUI.Model
|
||||
}
|
||||
|
||||
@@ -87,6 +89,7 @@ func New(broker *intercept.Broker, name, path string) Model {
|
||||
pluginsPage: pluginsUI.New(mgr),
|
||||
findingsPage: findingsUI.New(),
|
||||
copyAs: copyasUI.New(),
|
||||
copy: copyUI.New(),
|
||||
notifications: notificationsUI.New(),
|
||||
sidebarState: sidebarState(cfg.TUI.DefaultSidebarState),
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"github.com/anotherhadi/spilltea/internal/plugins"
|
||||
proxyPkg "github.com/anotherhadi/spilltea/internal/proxy"
|
||||
copyUI "github.com/anotherhadi/spilltea/internal/ui/components/copy"
|
||||
copyasUI "github.com/anotherhadi/spilltea/internal/ui/components/copyas"
|
||||
notificationsUI "github.com/anotherhadi/spilltea/internal/ui/components/notifications"
|
||||
diffUI "github.com/anotherhadi/spilltea/internal/ui/diff"
|
||||
@@ -81,6 +82,19 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
if m.copy.IsOpen() {
|
||||
if ws, ok := msg.(tea.WindowSizeMsg); ok {
|
||||
m.width = ws.Width
|
||||
m.height = ws.Height
|
||||
m.copy.SetSize(ws.Width, ws.Height)
|
||||
m.resizeChildren()
|
||||
return m, nil
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.copy, cmd = m.copy.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
@@ -161,7 +175,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
if !m.activeIsEditing() {
|
||||
switch {
|
||||
case key.Matches(msg, keys.Keys.Global.CopyRequest):
|
||||
case key.Matches(msg, keys.Keys.Global.CopyAs):
|
||||
if m.page == pageDiff {
|
||||
if raw := m.diff.CurrentRaw(); raw != "" {
|
||||
m.copyAs.SetSize(m.width, m.height)
|
||||
@@ -181,6 +195,28 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case key.Matches(msg, keys.Keys.Global.Copy):
|
||||
var raw, scheme string
|
||||
switch m.page {
|
||||
case pageIntercept:
|
||||
raw = m.intercept.CurrentRaw()
|
||||
scheme = m.intercept.CurrentScheme()
|
||||
case pageDiff:
|
||||
raw = m.diff.CurrentRaw()
|
||||
scheme = "https"
|
||||
case pageHistory:
|
||||
raw = m.history.CurrentRaw()
|
||||
scheme = m.history.CurrentScheme()
|
||||
case pageReplay:
|
||||
raw = m.replay.CurrentRaw()
|
||||
scheme = m.replay.CurrentScheme()
|
||||
}
|
||||
if raw != "" {
|
||||
m.copy.SetSize(m.width, m.height)
|
||||
m.copy.Open(copyUI.OpenMsg{RawRequest: raw, Scheme: scheme})
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case key.Matches(msg, keys.Keys.Global.ToggleSidebar):
|
||||
m.cycleSidebarState()
|
||||
m.resizeChildren()
|
||||
|
||||
@@ -22,6 +22,13 @@ func (m Model) View() tea.View {
|
||||
return v
|
||||
}
|
||||
|
||||
if m.copy.IsOpen() {
|
||||
v := tea.NewView(m.copy.View(normal))
|
||||
v.AltScreen = true
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
return v
|
||||
}
|
||||
|
||||
rendered := normal
|
||||
if m.notifications.HasNotifications() {
|
||||
rendered = m.notifications.View(normal)
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
package copy
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"charm.land/bubbles/v2/list"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
)
|
||||
|
||||
const popupInnerW = 40
|
||||
|
||||
func writeClipboard(text string) {
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte(text))
|
||||
fmt.Fprintf(os.Stderr, "\033]52;c;%s\a", encoded)
|
||||
}
|
||||
|
||||
type OpenMsg struct {
|
||||
RawRequest string
|
||||
Scheme string
|
||||
}
|
||||
|
||||
type copyItem struct {
|
||||
id string
|
||||
title string
|
||||
desc string
|
||||
}
|
||||
|
||||
func (c copyItem) Title() string { return c.title }
|
||||
func (c copyItem) Description() string { return c.desc }
|
||||
func (c copyItem) FilterValue() string { return c.title }
|
||||
|
||||
var allItems = []list.Item{
|
||||
copyItem{"raw", "Raw", "full HTTP request"},
|
||||
copyItem{"headers", "Headers", "request headers only"},
|
||||
copyItem{"body", "Body", "request body only"},
|
||||
copyItem{"url", "URL", "request URL"},
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
open bool
|
||||
list list.Model
|
||||
rawRequest string
|
||||
scheme string
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
func New() Model {
|
||||
s := style.S
|
||||
|
||||
delegate := list.NewDefaultDelegate()
|
||||
delegate.SetSpacing(0)
|
||||
delegate.Styles.NormalTitle = lipgloss.NewStyle().Foreground(s.Text).PaddingLeft(2)
|
||||
delegate.Styles.NormalDesc = lipgloss.NewStyle().Foreground(s.Subtle).PaddingLeft(2)
|
||||
delegate.Styles.SelectedTitle = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||
BorderForeground(s.Primary).
|
||||
Foreground(s.Primary).Bold(true).PaddingLeft(1)
|
||||
delegate.Styles.SelectedDesc = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||
BorderForeground(s.Primary).
|
||||
Foreground(s.MutedFg).PaddingLeft(1)
|
||||
|
||||
l := list.New(allItems, delegate, popupInnerW, 8)
|
||||
l.SetShowTitle(false)
|
||||
l.SetShowStatusBar(false)
|
||||
l.SetShowHelp(false)
|
||||
l.SetFilteringEnabled(true)
|
||||
l.KeyMap.Quit.SetEnabled(false)
|
||||
l.KeyMap.ForceQuit.SetEnabled(false)
|
||||
l.KeyMap.ShowFullHelp.SetEnabled(false)
|
||||
l.KeyMap.CloseFullHelp.SetEnabled(false)
|
||||
|
||||
return Model{list: l}
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd { return nil }
|
||||
|
||||
func (m Model) IsOpen() bool { return m.open }
|
||||
|
||||
func (m *Model) Open(msg OpenMsg) {
|
||||
m.rawRequest = msg.RawRequest
|
||||
m.scheme = msg.Scheme
|
||||
m.open = true
|
||||
m.list.ResetFilter()
|
||||
m.list.Select(0)
|
||||
m.list.SetSize(popupInnerW, m.listHeight())
|
||||
}
|
||||
|
||||
func (m *Model) SetSize(w, h int) {
|
||||
m.width = w
|
||||
m.height = h
|
||||
m.list.SetSize(popupInnerW, m.listHeight())
|
||||
}
|
||||
|
||||
func (m Model) popupHeight() int {
|
||||
h := 12
|
||||
if m.height > 0 && m.height-4 < h {
|
||||
h = m.height - 4
|
||||
}
|
||||
if h < 6 {
|
||||
h = 6
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (m Model) listHeight() int {
|
||||
return style.PanelContentH(m.popupHeight()) - 1
|
||||
}
|
||||
|
||||
func (m Model) extract(id string) string {
|
||||
raw := m.rawRequest
|
||||
lines := strings.Split(strings.ReplaceAll(raw, "\r\n", "\n"), "\n")
|
||||
|
||||
switch id {
|
||||
case "raw":
|
||||
return raw
|
||||
|
||||
case "headers":
|
||||
var sb strings.Builder
|
||||
for _, l := range lines[1:] {
|
||||
if l == "" {
|
||||
break
|
||||
}
|
||||
sb.WriteString(l + "\n")
|
||||
}
|
||||
return strings.TrimRight(sb.String(), "\n")
|
||||
|
||||
case "body":
|
||||
for i, l := range lines {
|
||||
if l == "" && i > 0 {
|
||||
return strings.TrimRight(strings.Join(lines[i+1:], "\n"), "\n")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
||||
case "url":
|
||||
scheme := m.scheme
|
||||
if scheme == "" {
|
||||
scheme = "https"
|
||||
}
|
||||
var host, path string
|
||||
if len(lines) > 0 {
|
||||
parts := strings.SplitN(lines[0], " ", 3)
|
||||
if len(parts) >= 2 {
|
||||
path = parts[1]
|
||||
}
|
||||
}
|
||||
for _, l := range lines[1:] {
|
||||
if l == "" {
|
||||
break
|
||||
}
|
||||
if kv := strings.SplitN(l, ": ", 2); len(kv) == 2 && strings.EqualFold(kv[0], "host") {
|
||||
host = strings.TrimSpace(kv[1])
|
||||
}
|
||||
}
|
||||
return scheme + "://" + host + path
|
||||
}
|
||||
return raw
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package copy
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/key"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
)
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
if kp, ok := msg.(tea.KeyPressMsg); ok {
|
||||
switch {
|
||||
case kp.String() == "enter":
|
||||
if item, ok := m.list.SelectedItem().(copyItem); ok {
|
||||
writeClipboard(m.extract(item.id))
|
||||
}
|
||||
m.open = false
|
||||
return m, nil
|
||||
case key.Matches(kp, keys.Keys.Global.Escape):
|
||||
if m.list.SettingFilter() {
|
||||
break
|
||||
}
|
||||
m.open = false
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.list, cmd = m.list.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package copy
|
||||
|
||||
import (
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
copyasUI "github.com/anotherhadi/spilltea/internal/ui/components/copyas"
|
||||
)
|
||||
|
||||
func (m *Model) View(background string) string {
|
||||
s := style.S
|
||||
|
||||
hint := lipgloss.NewStyle().Foreground(s.Subtle).
|
||||
Render(" enter: copy • /: filter • esc: cancel")
|
||||
|
||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||
m.list.View(),
|
||||
hint,
|
||||
)
|
||||
|
||||
border := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(s.Primary)
|
||||
|
||||
popupH := m.popupHeight()
|
||||
popup := style.RenderWithTitle(border, "Copy", inner, popupInnerW+2, popupH)
|
||||
|
||||
return copyasUI.OverlayCenter(background, popup, m.width, m.height)
|
||||
}
|
||||
@@ -66,6 +66,8 @@ func (pr parsedRequest) fullURL() string {
|
||||
func formatAs(id, raw, scheme string) string {
|
||||
pr := parseRaw(raw, scheme)
|
||||
switch id {
|
||||
case "raw":
|
||||
return raw
|
||||
case "curl":
|
||||
return toCurl(pr)
|
||||
case "python":
|
||||
|
||||
@@ -36,6 +36,7 @@ func (f formatItem) Description() string { return f.desc }
|
||||
func (f formatItem) FilterValue() string { return f.title }
|
||||
|
||||
var allFormats = []list.Item{
|
||||
formatItem{"raw", "Raw", "raw HTTP request"},
|
||||
formatItem{"curl", "cURL", "command line HTTP request"},
|
||||
formatItem{"python", "Python", "requests library"},
|
||||
formatItem{"go", "Go", "net/http package"},
|
||||
|
||||
@@ -26,10 +26,10 @@ func (m *Model) View(background string) string {
|
||||
popupH := m.popupHeight()
|
||||
popup := style.RenderWithTitle(border, "Copy as", inner, popupInnerW+2, popupH)
|
||||
|
||||
return overlayCenter(background, popup, m.width, m.height)
|
||||
return OverlayCenter(background, popup, m.width, m.height)
|
||||
}
|
||||
|
||||
func overlayCenter(bg, popup string, w, h int) string {
|
||||
func OverlayCenter(bg, popup string, w, h int) string {
|
||||
s := style.S
|
||||
|
||||
stripped := ansi.Strip(bg)
|
||||
|
||||
@@ -55,6 +55,15 @@ func (m Model) IsEditing() bool {
|
||||
return m.searchKind != searchKindOff && !m.searchAccepted
|
||||
}
|
||||
|
||||
func (m Model) CurrentRaw() string {
|
||||
if len(m.entries) == 0 || m.cursor >= len(m.entries) {
|
||||
return ""
|
||||
}
|
||||
return m.entries[m.cursor].RequestRaw
|
||||
}
|
||||
|
||||
func (m Model) CurrentScheme() string { return "https" }
|
||||
|
||||
// RefreshCmd returns the appropriate load command given the current search state.
|
||||
// The app model should call this instead of LoadEntriesCmd directly so that
|
||||
// background refreshes re-run the active search rather than resetting it.
|
||||
|
||||
@@ -50,7 +50,7 @@ func (m *Model) renderDetailPanel(h int) string {
|
||||
}
|
||||
info, ok := m.selected()
|
||||
if !ok {
|
||||
return style.RenderWithTitle(panelStyle, "Detail", "", m.width, h)
|
||||
return style.RenderWithTitle(panelStyle, icons.I.Detail+"Detail", "", m.width, h)
|
||||
}
|
||||
|
||||
statusSt := lipgloss.NewStyle().Foreground(s.Error)
|
||||
@@ -84,7 +84,7 @@ func (m *Model) renderDetailPanel(h int) string {
|
||||
}
|
||||
|
||||
inner := lipgloss.JoinVertical(lipgloss.Left, parts...)
|
||||
return style.RenderWithTitle(panelStyle, "Detail", inner, m.width, h)
|
||||
return style.RenderWithTitle(panelStyle, icons.I.Detail+"Detail", inner, m.width, h)
|
||||
}
|
||||
|
||||
func renderPluginDescription(desc string, width int) string {
|
||||
|
||||
@@ -68,6 +68,23 @@ func (m Model) Init() tea.Cmd { return nil }
|
||||
|
||||
func (m Model) IsEditing() bool { return m.editing }
|
||||
|
||||
func (m Model) CurrentRaw() string {
|
||||
if len(m.entries) == 0 || m.cursor >= len(m.entries) {
|
||||
return ""
|
||||
}
|
||||
return m.entries[m.cursor].RequestRaw
|
||||
}
|
||||
|
||||
func (m Model) CurrentScheme() string {
|
||||
if len(m.entries) == 0 || m.cursor >= len(m.entries) {
|
||||
return "https"
|
||||
}
|
||||
if s := m.entries[m.cursor].Scheme; s != "" {
|
||||
return s
|
||||
}
|
||||
return "https"
|
||||
}
|
||||
|
||||
func (m *Model) SetDB(d *db.DB) {
|
||||
m.database = d
|
||||
if d == nil {
|
||||
|
||||
Reference in New Issue
Block a user