Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2026-05-12 19:12:29 +02:00
commit e8e64eff12
101 changed files with 10081 additions and 0 deletions
+200
View File
@@ -0,0 +1,200 @@
package copyas
import (
"fmt"
"strings"
)
type header struct{ key, value string }
type parsedRequest struct {
method string
path string
host string
scheme string
headers []header
body string
}
func parseRaw(raw, scheme string) parsedRequest {
lines := strings.Split(strings.ReplaceAll(raw, "\r\n", "\n"), "\n")
pr := parsedRequest{scheme: scheme}
if len(lines) == 0 {
return pr
}
parts := strings.SplitN(lines[0], " ", 3)
if len(parts) >= 1 {
pr.method = strings.TrimSpace(parts[0])
}
if len(parts) >= 2 {
pr.path = strings.TrimSpace(parts[1])
}
i := 1
for i < len(lines) {
line := strings.TrimRight(lines[i], "\r")
if line == "" {
i++
break
}
if kv := strings.SplitN(line, ": ", 2); len(kv) == 2 {
k := strings.TrimSpace(kv[0])
v := strings.TrimSpace(kv[1])
pr.headers = append(pr.headers, header{k, v})
if strings.EqualFold(k, "host") {
pr.host = v
}
}
i++
}
if i < len(lines) {
pr.body = strings.TrimRight(strings.Join(lines[i:], "\n"), "\n")
}
return pr
}
func (pr parsedRequest) fullURL() string {
scheme := pr.scheme
if scheme == "" {
scheme = "https"
}
return scheme + "://" + pr.host + pr.path
}
func formatAs(id, raw, scheme string) string {
pr := parseRaw(raw, scheme)
switch id {
case "curl":
return toCurl(pr)
case "python":
return toPython(pr)
case "go":
return toGo(pr)
case "ffuf":
return toFFUF(pr)
case "markdown":
return toMarkdown(pr)
}
return raw
}
func toMarkdown(pr parsedRequest) string {
var sb strings.Builder
fmt.Fprintf(&sb, "### %s %s\n\n", pr.method, pr.fullURL())
sb.WriteString("```\n")
sb.WriteString(pr.method + " " + pr.path + " HTTP/1.1\n")
for _, h := range pr.headers {
sb.WriteString(fmt.Sprintf("%s: %s\n", h.key, h.value))
}
if pr.body != "" {
sb.WriteString("\n" + pr.body)
}
sb.WriteString("\n```")
return sb.String()
}
func toCurl(pr parsedRequest) string {
var sb strings.Builder
fmt.Fprintf(&sb, "curl -X %s '%s'", pr.method, pr.fullURL())
for _, h := range pr.headers {
if strings.EqualFold(h.key, "content-length") {
continue
}
fmt.Fprintf(&sb, " \\\n -H '%s: %s'", h.key, h.value)
}
if pr.body != "" {
body := strings.ReplaceAll(pr.body, "'", "'\\''")
fmt.Fprintf(&sb, " \\\n --data '%s'", body)
}
return sb.String()
}
func toPython(pr parsedRequest) string {
var sb strings.Builder
sb.WriteString("import requests\n\n")
fmt.Fprintf(&sb, "url = %q\n", pr.fullURL())
sb.WriteString("headers = {\n")
for _, h := range pr.headers {
if strings.EqualFold(h.key, "content-length") {
continue
}
fmt.Fprintf(&sb, " %q: %q,\n", h.key, h.value)
}
sb.WriteString("}\n")
method := strings.ToLower(pr.method)
if pr.body != "" {
fmt.Fprintf(&sb, "data = %q\n\n", pr.body)
fmt.Fprintf(&sb, "response = requests.%s(url, headers=headers, data=data)\n", method)
} else {
fmt.Fprintf(&sb, "\nresponse = requests.%s(url, headers=headers)\n", method)
}
sb.WriteString("print(response.status_code)\n")
sb.WriteString("print(response.text)\n")
return sb.String()
}
func toGo(pr parsedRequest) string {
var sb strings.Builder
sb.WriteString("package main\n\nimport (\n")
if pr.body != "" {
sb.WriteString("\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n")
} else {
sb.WriteString("\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\n")
}
sb.WriteString("func main() {\n")
if pr.body != "" {
fmt.Fprintf(&sb, "\tbody := strings.NewReader(%q)\n", pr.body)
fmt.Fprintf(&sb, "\treq, err := http.NewRequest(%q, %q, body)\n", pr.method, pr.fullURL())
} else {
fmt.Fprintf(&sb, "\treq, err := http.NewRequest(%q, %q, nil)\n", pr.method, pr.fullURL())
}
sb.WriteString("\tif err != nil { panic(err) }\n")
for _, h := range pr.headers {
if strings.EqualFold(h.key, "host") || strings.EqualFold(h.key, "content-length") {
continue
}
fmt.Fprintf(&sb, "\treq.Header.Set(%q, %q)\n", h.key, h.value)
}
sb.WriteString("\n\tclient := &http.Client{}\n")
sb.WriteString("\tresp, err := client.Do(req)\n")
sb.WriteString("\tif err != nil { panic(err) }\n")
sb.WriteString("\tdefer resp.Body.Close()\n")
sb.WriteString("\tbody2, _ := io.ReadAll(resp.Body)\n")
sb.WriteString("\tfmt.Printf(\"Status: %d\\n\", resp.StatusCode)\n")
sb.WriteString("\tfmt.Println(string(body2))\n")
sb.WriteString("}\n")
return sb.String()
}
func toFFUF(pr parsedRequest) string {
// Place FUZZ in the path: replace query string or append ?FUZZ
fuzzURL := pr.scheme + "://" + pr.host
if idx := strings.Index(pr.path, "?"); idx != -1 {
fuzzURL += pr.path[:idx] + "?FUZZ"
} else {
fuzzURL += pr.path + "?FUZZ"
}
var sb strings.Builder
fmt.Fprintf(&sb, "ffuf -u '%s'", fuzzURL)
sb.WriteString(" \\\n -w wordlist.txt")
fmt.Fprintf(&sb, " \\\n -X %s", pr.method)
for _, h := range pr.headers {
if strings.EqualFold(h.key, "content-length") {
continue
}
fmt.Fprintf(&sb, " \\\n -H '%s: %s'", h.key, h.value)
}
if pr.body != "" {
body := strings.ReplaceAll(pr.body, "'", "'\\''")
fmt.Fprintf(&sb, " \\\n -d '%s'", body)
}
return sb.String()
}
+117
View File
@@ -0,0 +1,117 @@
package copyas
import (
"encoding/base64"
"fmt"
"os"
"charm.land/bubbles/v2/list"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/spilltea/internal/style"
)
const popupInnerW = 46
// writeClipboard uses the OSC 52 terminal escape sequence to set the clipboard.
// Supported by most modern terminals (foot, kitty, wezterm, alacritty, xterm…).
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 formatItem struct {
id string
title string
desc string
}
func (f formatItem) Title() string { return f.title }
func (f formatItem) Description() string { return f.desc }
func (f formatItem) FilterValue() string { return f.title }
var allFormats = []list.Item{
formatItem{"curl", "cURL", "command line HTTP request"},
formatItem{"python", "Python", "requests library"},
formatItem{"go", "Go", "net/http package"},
formatItem{"ffuf", "FFUF", "web fuzzer: FUZZ in query string"},
formatItem{"markdown", "Markdown", "formatted for documentation"},
}
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(allFormats, 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 := 14
if m.height > 0 && m.height-4 < h {
h = m.height - 4
}
if h < 6 {
h = 6
}
return h
}
// listHeight = panel content area - hint line (1)
func (m Model) listHeight() int {
return style.PanelContentH(m.popupHeight()) - 1
}
+30
View File
@@ -0,0 +1,30 @@
package copyas
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().(formatItem); ok {
writeClipboard(formatAs(item.id, m.rawRequest, m.scheme))
}
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
}
+93
View File
@@ -0,0 +1,93 @@
package copyas
import (
"strings"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/spilltea/internal/style"
"github.com/charmbracelet/x/ansi"
)
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 as", inner, popupInnerW+2, popupH)
return overlayCenter(background, popup, m.width, m.height)
}
func overlayCenter(bg, popup string, w, h int) string {
s := style.S
stripped := ansi.Strip(bg)
rawLines := strings.Split(stripped, "\n")
bgRunes := make([][]rune, h)
for y := 0; y < h; y++ {
var line []rune
if y < len(rawLines) {
line = []rune(rawLines[y])
}
if len(line) > w {
line = line[:w]
}
for len(line) < w {
line = append(line, ' ')
}
bgRunes[y] = line
}
popupLines := strings.Split(popup, "\n")
popupH := len(popupLines)
popupW := 0
for _, l := range popupLines {
if vw := lipgloss.Width(l); vw > popupW {
popupW = vw
}
}
startY := (h - popupH) / 2
startX := (w - popupW) / 2
if startY < 0 {
startY = 0
}
if startX < 0 {
startX = 0
}
dim := lipgloss.NewStyle().Foreground(s.Subtle).Faint(true)
result := make([]string, h)
for y := 0; y < h; y++ {
popupY := y - startY
if popupY >= 0 && popupY < popupH {
leftEnd := startX
if leftEnd > len(bgRunes[y]) {
leftEnd = len(bgRunes[y])
}
prefix := dim.Render(string(bgRunes[y][:leftEnd]))
rightStart := startX + popupW
suffix := ""
if rightStart < len(bgRunes[y]) {
suffix = dim.Render(string(bgRunes[y][rightStart:]))
}
result[y] = prefix + popupLines[popupY] + suffix
} else {
result[y] = dim.Render(string(bgRunes[y]))
}
}
return strings.Join(result, "\n")
}