mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-05-20 09:42:34 +02:00
Init
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user