CopyRequest -> Copy & CopyAs

Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2026-05-13 18:07:23 +02:00
parent de254b4e52
commit c7392474b7
15 changed files with 312 additions and 10 deletions
+165
View File
@@ -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
}
+30
View File
@@ -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
}
+28
View File
@@ -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)
}