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,175 @@
|
||||
package replay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"charm.land/bubbles/v2/help"
|
||||
"charm.land/bubbles/v2/key"
|
||||
"charm.land/bubbles/v2/paginator"
|
||||
"charm.land/bubbles/v2/textarea"
|
||||
"charm.land/bubbles/v2/viewport"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/db"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
)
|
||||
|
||||
type SendToReplayMsg struct {
|
||||
Scheme string
|
||||
Host string
|
||||
RequestRaw string
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
DBID int64
|
||||
Scheme string
|
||||
Host string
|
||||
Path string
|
||||
Method string
|
||||
OriginalRaw string
|
||||
RequestRaw string // current (possibly edited) request
|
||||
ResponseRaw string // filled after send
|
||||
StatusCode int // 0 = not sent yet
|
||||
Sending bool
|
||||
Err error
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
entries []Entry
|
||||
cursor int
|
||||
editing bool
|
||||
database *db.DB
|
||||
|
||||
listViewport viewport.Model
|
||||
requestViewport viewport.Model
|
||||
responseViewport viewport.Model
|
||||
textarea textarea.Model
|
||||
pager paginator.Model
|
||||
help help.Model
|
||||
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
func New() Model {
|
||||
ta := style.NewTextarea(false)
|
||||
ta.Blur()
|
||||
return Model{
|
||||
listViewport: style.NewViewport(),
|
||||
requestViewport: style.NewViewport(),
|
||||
responseViewport: style.NewViewport(),
|
||||
textarea: ta,
|
||||
pager: style.NewPaginator(),
|
||||
help: style.NewHelp(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd { return nil }
|
||||
|
||||
func (m Model) IsEditing() bool { return m.editing }
|
||||
|
||||
func (m *Model) SetDB(d *db.DB) {
|
||||
m.database = d
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
entries, err := d.ListReplayEntries()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, dbe := range entries {
|
||||
m.entries = append(m.entries, entryFromDB(dbe))
|
||||
}
|
||||
m.pager.SetTotalPages(len(m.entries))
|
||||
if len(m.entries) > 0 {
|
||||
m.cursor = len(m.entries) - 1
|
||||
}
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
}
|
||||
|
||||
func entryFromDB(dbe db.ReplayEntry) Entry {
|
||||
var err error
|
||||
if dbe.ErrorMsg != "" {
|
||||
err = fmt.Errorf("%s", dbe.ErrorMsg)
|
||||
}
|
||||
return Entry{
|
||||
DBID: dbe.ID,
|
||||
Scheme: dbe.Scheme,
|
||||
Host: dbe.Host,
|
||||
Path: dbe.Path,
|
||||
Method: dbe.Method,
|
||||
OriginalRaw: dbe.OriginalRaw,
|
||||
RequestRaw: dbe.RequestRaw,
|
||||
ResponseRaw: dbe.ResponseRaw,
|
||||
StatusCode: dbe.StatusCode,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) SetSize(w, h int) {
|
||||
m.width = w
|
||||
m.height = h
|
||||
m.recalcSizes()
|
||||
}
|
||||
|
||||
func (m *Model) recalcSizes() {
|
||||
m.help.SetWidth(m.width - 2)
|
||||
|
||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
|
||||
listInner := m.width - 2
|
||||
if listInner < 0 {
|
||||
listInner = 0
|
||||
}
|
||||
listVH := style.PanelContentH(listH) - 1 // -1 for the pager dots row
|
||||
if listVH < 0 {
|
||||
listVH = 0
|
||||
}
|
||||
m.listViewport.SetWidth(listInner)
|
||||
m.listViewport.SetHeight(listVH)
|
||||
m.pager.PerPage = listVH
|
||||
if m.pager.PerPage < 1 {
|
||||
m.pager.PerPage = 1
|
||||
}
|
||||
|
||||
leftW, rightW := m.bodyHalfWidths()
|
||||
leftInner := leftW - 2
|
||||
rightInner := rightW - 2
|
||||
if leftInner < 0 {
|
||||
leftInner = 0
|
||||
}
|
||||
if rightInner < 0 {
|
||||
rightInner = 0
|
||||
}
|
||||
bodyVH := style.PanelContentH(bodyH)
|
||||
|
||||
m.requestViewport.SetWidth(leftInner)
|
||||
m.requestViewport.SetHeight(bodyVH)
|
||||
m.responseViewport.SetWidth(rightInner)
|
||||
m.responseViewport.SetHeight(bodyVH)
|
||||
m.textarea.SetWidth(leftInner)
|
||||
m.textarea.SetHeight(bodyVH)
|
||||
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
}
|
||||
|
||||
func (m *Model) bodyHalfWidths() (left, right int) {
|
||||
left = m.width / 2
|
||||
right = m.width - left
|
||||
return
|
||||
}
|
||||
|
||||
type replayKeyMap struct{ width int }
|
||||
|
||||
func (replayKeyMap) ShortHelp() []key.Binding {
|
||||
g := keys.Keys.Global
|
||||
r := keys.Keys.Replay
|
||||
return []key.Binding{g.Up, g.Down, r.Send, r.Edit, g.Help}
|
||||
}
|
||||
|
||||
func (m replayKeyMap) FullHelp() [][]key.Binding {
|
||||
all := append(keys.Keys.Replay.Bindings(), keys.Keys.Global.Bindings()...)
|
||||
return keys.ChunkByWidth(all, m.width)
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
package replay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"charm.land/bubbles/v2/key"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/db"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
"github.com/anotherhadi/spilltea/internal/util"
|
||||
)
|
||||
|
||||
type sentMsg struct {
|
||||
index int
|
||||
responseRaw string
|
||||
statusCode int
|
||||
err error
|
||||
}
|
||||
|
||||
func sendCmd(entry Entry, index int) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
raw, code, err := doSend(entry)
|
||||
return sentMsg{index: index, responseRaw: raw, statusCode: code, err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case SendToReplayMsg:
|
||||
entry := entryFromMsg(msg)
|
||||
if m.database != nil {
|
||||
id, err := m.database.InsertReplayEntry(entryToDB(entry))
|
||||
if err == nil {
|
||||
entry.DBID = id
|
||||
}
|
||||
}
|
||||
m.entries = append(m.entries, entry)
|
||||
m.cursor = len(m.entries) - 1
|
||||
m.pager.SetTotalPages(len(m.entries))
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
|
||||
case sentMsg:
|
||||
if msg.index >= 0 && msg.index < len(m.entries) {
|
||||
e := &m.entries[msg.index]
|
||||
e.Sending = false
|
||||
e.StatusCode = msg.statusCode
|
||||
e.ResponseRaw = msg.responseRaw
|
||||
if msg.err != nil {
|
||||
e.Err = msg.err
|
||||
e.ResponseRaw = "Error: " + msg.err.Error()
|
||||
}
|
||||
if m.database != nil && e.DBID != 0 {
|
||||
m.database.UpdateReplayEntry(entryToDB(*e))
|
||||
}
|
||||
}
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
|
||||
case util.EditorFinishedMsg:
|
||||
if msg.Err == nil && msg.Content != "" && len(m.entries) > 0 {
|
||||
m.entries[m.cursor].RequestRaw = msg.Content
|
||||
m.refreshBody()
|
||||
}
|
||||
|
||||
case tea.MouseWheelMsg:
|
||||
if !m.editing {
|
||||
switch msg.Button {
|
||||
case tea.MouseWheelUp:
|
||||
if msg.Mod.Contains(tea.ModShift) {
|
||||
m.requestViewport.ScrollLeft(6)
|
||||
m.responseViewport.ScrollLeft(6)
|
||||
} else {
|
||||
m.responseViewport.SetYOffset(m.responseViewport.YOffset() - 1)
|
||||
}
|
||||
case tea.MouseWheelDown:
|
||||
if msg.Mod.Contains(tea.ModShift) {
|
||||
m.requestViewport.ScrollRight(6)
|
||||
m.responseViewport.ScrollRight(6)
|
||||
} else {
|
||||
m.responseViewport.SetYOffset(m.responseViewport.YOffset() + 1)
|
||||
}
|
||||
case tea.MouseWheelLeft:
|
||||
m.requestViewport.ScrollLeft(6)
|
||||
m.responseViewport.ScrollLeft(6)
|
||||
case tea.MouseWheelRight:
|
||||
m.requestViewport.ScrollRight(6)
|
||||
m.responseViewport.ScrollRight(6)
|
||||
}
|
||||
}
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
if m.editing {
|
||||
return m.updateEditMode(msg)
|
||||
}
|
||||
return m.updateNormalMode(msg)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
g := keys.Keys.Global
|
||||
r := keys.Keys.Replay
|
||||
switch {
|
||||
case key.Matches(msg, g.Up):
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
}
|
||||
|
||||
case key.Matches(msg, g.Down):
|
||||
if m.cursor < len(m.entries)-1 {
|
||||
m.cursor++
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
}
|
||||
|
||||
case key.Matches(msg, r.Send):
|
||||
if len(m.entries) > 0 && !m.entries[m.cursor].Sending {
|
||||
m.entries[m.cursor].Sending = true
|
||||
m.entries[m.cursor].ResponseRaw = ""
|
||||
m.entries[m.cursor].Err = nil
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
return m, sendCmd(m.entries[m.cursor], m.cursor)
|
||||
}
|
||||
|
||||
case key.Matches(msg, r.Edit):
|
||||
if len(m.entries) > 0 {
|
||||
m.textarea.SetValue(m.entries[m.cursor].RequestRaw)
|
||||
m.editing = true
|
||||
m.textarea.Focus()
|
||||
}
|
||||
|
||||
case key.Matches(msg, r.EditExt):
|
||||
if len(m.entries) > 0 {
|
||||
return m, util.OpenExternalEditor(m.entries[m.cursor].RequestRaw)
|
||||
}
|
||||
|
||||
case key.Matches(msg, r.UndoEdits):
|
||||
if len(m.entries) > 0 {
|
||||
m.entries[m.cursor].RequestRaw = m.entries[m.cursor].OriginalRaw
|
||||
m.refreshBody()
|
||||
}
|
||||
|
||||
case key.Matches(msg, g.ScrollUp):
|
||||
step := m.responseViewport.Height() / 2
|
||||
if step < 1 {
|
||||
step = 1
|
||||
}
|
||||
m.responseViewport.SetYOffset(m.responseViewport.YOffset() - step)
|
||||
|
||||
case key.Matches(msg, g.ScrollDown):
|
||||
step := m.responseViewport.Height() / 2
|
||||
if step < 1 {
|
||||
step = 1
|
||||
}
|
||||
m.responseViewport.SetYOffset(m.responseViewport.YOffset() + step)
|
||||
|
||||
case key.Matches(msg, g.Left):
|
||||
m.requestViewport.ScrollLeft(6)
|
||||
m.responseViewport.ScrollLeft(6)
|
||||
|
||||
case key.Matches(msg, g.Right):
|
||||
m.requestViewport.ScrollRight(6)
|
||||
m.responseViewport.ScrollRight(6)
|
||||
|
||||
case key.Matches(msg, r.Delete):
|
||||
if len(m.entries) > 0 {
|
||||
e := m.entries[m.cursor]
|
||||
if m.database != nil && e.DBID != 0 {
|
||||
m.database.DeleteReplayEntry(e.DBID)
|
||||
}
|
||||
m.entries = append(m.entries[:m.cursor], m.entries[m.cursor+1:]...)
|
||||
if m.cursor >= len(m.entries) && m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
m.pager.SetTotalPages(len(m.entries))
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
}
|
||||
|
||||
case key.Matches(msg, r.DeleteAll):
|
||||
if m.database != nil {
|
||||
m.database.DeleteAllReplayEntries()
|
||||
}
|
||||
m.entries = nil
|
||||
m.cursor = 0
|
||||
m.pager.SetTotalPages(0)
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
|
||||
case key.Matches(msg, g.Help):
|
||||
m.help.ShowAll = !m.help.ShowAll
|
||||
m.recalcSizes()
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateEditMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
switch {
|
||||
case key.Matches(msg, keys.Keys.Global.Escape):
|
||||
if len(m.entries) > 0 {
|
||||
m.entries[m.cursor].RequestRaw = m.textarea.Value()
|
||||
}
|
||||
m.editing = false
|
||||
m.textarea.Blur()
|
||||
m.refreshBody()
|
||||
|
||||
default:
|
||||
var cmd tea.Cmd
|
||||
m.textarea, cmd = m.textarea.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) refreshListViewport() {
|
||||
if m.pager.PerPage > 0 {
|
||||
m.pager.Page = m.cursor / m.pager.PerPage
|
||||
m.pager.SetTotalPages(len(m.entries))
|
||||
}
|
||||
m.listViewport.SetContent(m.renderList())
|
||||
}
|
||||
|
||||
func (m *Model) refreshBody() {
|
||||
if len(m.entries) == 0 {
|
||||
m.requestViewport.SetContent("")
|
||||
m.responseViewport.SetContent("")
|
||||
return
|
||||
}
|
||||
e := m.entries[m.cursor]
|
||||
m.requestViewport.SetContent(style.HighlightHTTP(e.RequestRaw))
|
||||
m.requestViewport.SetYOffset(0)
|
||||
m.requestViewport.SetXOffset(0)
|
||||
|
||||
if e.Sending {
|
||||
m.responseViewport.SetContent(style.HighlightHTTP("Sending..."))
|
||||
} else if e.ResponseRaw != "" {
|
||||
m.responseViewport.SetContent(style.HighlightHTTP(e.ResponseRaw))
|
||||
} else {
|
||||
m.responseViewport.SetContent("")
|
||||
}
|
||||
m.responseViewport.SetYOffset(0)
|
||||
m.responseViewport.SetXOffset(0)
|
||||
}
|
||||
|
||||
func doSend(entry Entry) (responseRaw string, statusCode int, err error) {
|
||||
lines := strings.Split(strings.ReplaceAll(entry.RequestRaw, "\r\n", "\n"), "\n")
|
||||
if len(lines) == 0 {
|
||||
return "", 0, fmt.Errorf("empty request")
|
||||
}
|
||||
|
||||
parts := strings.SplitN(lines[0], " ", 3)
|
||||
if len(parts) < 2 {
|
||||
return "", 0, fmt.Errorf("invalid request line")
|
||||
}
|
||||
method := strings.TrimSpace(parts[0])
|
||||
path := strings.TrimSpace(parts[1])
|
||||
|
||||
headers := make(http.Header)
|
||||
host := entry.Host
|
||||
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])
|
||||
if strings.ToLower(k) == "host" {
|
||||
host = v
|
||||
} else {
|
||||
headers.Add(k, v)
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
var bodyBytes []byte
|
||||
if i < len(lines) {
|
||||
b := strings.Join(lines[i:], "\n")
|
||||
b = strings.TrimRight(b, "\n")
|
||||
bodyBytes = []byte(b)
|
||||
}
|
||||
|
||||
scheme := entry.Scheme
|
||||
if scheme == "" {
|
||||
scheme = "https"
|
||||
}
|
||||
urlStr := scheme + "://" + host + path
|
||||
|
||||
var bodyReader io.Reader
|
||||
if len(bodyBytes) > 0 {
|
||||
bodyReader = bytes.NewReader(bodyBytes)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, urlStr, bodyReader)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
req.Header = headers
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
|
||||
},
|
||||
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "%s %d %s\n", resp.Proto, resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
sortedKeys := make([]string, 0, len(resp.Header))
|
||||
for k := range resp.Header {
|
||||
sortedKeys = append(sortedKeys, k)
|
||||
}
|
||||
sort.Strings(sortedKeys)
|
||||
for _, k := range sortedKeys {
|
||||
for _, v := range resp.Header[k] {
|
||||
fmt.Fprintf(&sb, "%s: %s\n", k, v)
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
sb.Write(respBody)
|
||||
|
||||
return sb.String(), resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func entryToDB(e Entry) db.ReplayEntry {
|
||||
errMsg := ""
|
||||
if e.Err != nil {
|
||||
errMsg = e.Err.Error()
|
||||
}
|
||||
return db.ReplayEntry{
|
||||
ID: e.DBID,
|
||||
Timestamp: time.Now(),
|
||||
Scheme: e.Scheme,
|
||||
Host: e.Host,
|
||||
Path: e.Path,
|
||||
Method: e.Method,
|
||||
OriginalRaw: e.OriginalRaw,
|
||||
RequestRaw: e.RequestRaw,
|
||||
ResponseRaw: e.ResponseRaw,
|
||||
StatusCode: e.StatusCode,
|
||||
ErrorMsg: errMsg,
|
||||
}
|
||||
}
|
||||
|
||||
func entryFromMsg(msg SendToReplayMsg) Entry {
|
||||
method, host, path := parseFirstLine(msg.RequestRaw, msg.Host)
|
||||
scheme := msg.Scheme
|
||||
if scheme == "" {
|
||||
scheme = util.InferScheme(host)
|
||||
}
|
||||
return Entry{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
Path: path,
|
||||
Method: method,
|
||||
OriginalRaw: msg.RequestRaw,
|
||||
RequestRaw: msg.RequestRaw,
|
||||
}
|
||||
}
|
||||
|
||||
func parseFirstLine(raw, fallbackHost string) (method, host, path string) {
|
||||
host = fallbackHost
|
||||
path = "/"
|
||||
lines := strings.SplitN(raw, "\n", 2)
|
||||
if len(lines) == 0 {
|
||||
return
|
||||
}
|
||||
parts := strings.Fields(lines[0])
|
||||
if len(parts) >= 1 {
|
||||
method = parts[0]
|
||||
}
|
||||
if len(parts) >= 2 {
|
||||
path = parts[1]
|
||||
}
|
||||
if len(lines) > 1 {
|
||||
for _, line := range strings.Split(lines[1], "\n") {
|
||||
if strings.HasPrefix(strings.ToLower(line), "host:") {
|
||||
host = strings.TrimSpace(line[5:])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package replay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/icons"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
)
|
||||
|
||||
func (m Model) View() tea.View {
|
||||
if m.width == 0 {
|
||||
return tea.NewView("Loading...")
|
||||
}
|
||||
|
||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
leftW, rightW := m.bodyHalfWidths()
|
||||
|
||||
bodyRow := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
m.renderRequestPanel(leftW, bodyH),
|
||||
m.renderResponsePanel(rightW, bodyH),
|
||||
)
|
||||
|
||||
content := lipgloss.JoinVertical(lipgloss.Left,
|
||||
m.renderListPanel(m.width, listH),
|
||||
bodyRow,
|
||||
m.renderStatusBar(),
|
||||
)
|
||||
return tea.NewView(content)
|
||||
}
|
||||
|
||||
func (m *Model) renderListPanel(w, h int) string {
|
||||
s := style.S
|
||||
dots := s.Faint.Render(m.pager.View())
|
||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||
m.listViewport.View(),
|
||||
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
||||
)
|
||||
return style.RenderWithTitle(s.PanelFocused, icons.I.Replay+"Replay", inner, w, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderRequestPanel(w, h int) string {
|
||||
s := style.S
|
||||
var body string
|
||||
border := s.Panel
|
||||
if m.editing {
|
||||
body = m.textarea.View()
|
||||
border = s.PanelFocused
|
||||
} else {
|
||||
body = m.requestViewport.View()
|
||||
}
|
||||
return style.RenderWithTitle(border, icons.I.Request+"Request", body, w, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderResponsePanel(w, h int) string {
|
||||
s := style.S
|
||||
return style.RenderWithTitle(s.Panel, icons.I.Response+"Response", m.responseViewport.View(), w, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderStatusBar() string {
|
||||
return lipgloss.NewStyle().Padding(0, 1).Render(m.help.View(replayKeyMap{width: m.width}))
|
||||
}
|
||||
|
||||
func (m *Model) renderList() string {
|
||||
if len(m.entries) == 0 {
|
||||
return lipgloss.Place(
|
||||
m.listViewport.Width(), m.listViewport.Height(),
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
style.S.Faint.Render(" (╥﹏╥)\nsend a request from History or Intercept"),
|
||||
)
|
||||
}
|
||||
|
||||
s := style.S
|
||||
start, end := m.pager.GetSliceBounds(len(m.entries))
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
if end < start {
|
||||
end = start
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for i, e := range m.entries[start:end] {
|
||||
globalIdx := start + i
|
||||
selected := globalIdx == m.cursor
|
||||
selBg := s.Selection
|
||||
|
||||
w := m.listViewport.Width()
|
||||
const fixedW = 2 + 7 + 1 + 3 + 1
|
||||
hostPathW := w - fixedW
|
||||
if hostPathW < 0 {
|
||||
hostPathW = 0
|
||||
}
|
||||
|
||||
statusStr, statusSt := entryStatus(e)
|
||||
|
||||
var line string
|
||||
if selected {
|
||||
bg := lipgloss.NewStyle().Background(selBg)
|
||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
bg.Bold(true).Foreground(s.Primary).Width(2).Render(">"),
|
||||
s.Method(e.Method).Background(selBg).Render(e.Method),
|
||||
bg.Width(1).Render(""),
|
||||
statusSt.Background(selBg).Render(statusStr),
|
||||
bg.Width(1).Render(""),
|
||||
bg.Bold(true).Width(hostPathW).Render(e.Host+e.Path),
|
||||
)
|
||||
} else {
|
||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
" ",
|
||||
s.Method(e.Method).Render(e.Method),
|
||||
" ",
|
||||
statusSt.Render(statusStr),
|
||||
" ",
|
||||
s.Bold.Render(e.Host),
|
||||
s.Faint.Render(e.Path),
|
||||
)
|
||||
}
|
||||
sb.WriteString(line + "\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func entryStatus(e Entry) (string, lipgloss.Style) {
|
||||
base := lipgloss.NewStyle().Bold(true).Width(3)
|
||||
switch {
|
||||
case e.Sending:
|
||||
return "···", base.Foreground(style.S.Subtle)
|
||||
case e.Err != nil:
|
||||
return "ERR", base.Foreground(style.S.Error)
|
||||
case e.StatusCode == 0:
|
||||
return "---", base.Foreground(style.S.Subtle)
|
||||
}
|
||||
return fmt.Sprintf("%3d", e.StatusCode), style.StatusStyle(e.StatusCode, 3)
|
||||
}
|
||||
Reference in New Issue
Block a user