mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-07-06 12:42:32 +02:00
cc0176a73d
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
292 lines
6.5 KiB
Go
292 lines
6.5 KiB
Go
package replay
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"charm.land/bubbles/v2/help"
|
|
"charm.land/bubbles/v2/key"
|
|
"charm.land/bubbles/v2/paginator"
|
|
"charm.land/bubbles/v2/textarea"
|
|
"charm.land/bubbles/v2/textinput"
|
|
"charm.land/bubbles/v2/viewport"
|
|
tea "charm.land/bubbletea/v2"
|
|
ilovetui "github.com/anotherhadi/ilovetui"
|
|
"github.com/anotherhadi/spilltea/internal/db"
|
|
"github.com/anotherhadi/spilltea/internal/keys"
|
|
)
|
|
|
|
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
|
|
Flagged bool
|
|
Err error
|
|
}
|
|
|
|
type panel int
|
|
|
|
const (
|
|
panelList panel = iota
|
|
panelRequest
|
|
panelResponse
|
|
)
|
|
|
|
type Model struct {
|
|
allEntries []Entry
|
|
entries []Entry // filtered view; equals allEntries when no filter
|
|
entryIndices []int // maps entries[i] -> allEntries[i]; nil when no filter
|
|
cursor int
|
|
editing bool
|
|
focusedPanel panel
|
|
database *db.DB
|
|
|
|
filterInput textinput.Model
|
|
filterActive bool
|
|
filterAccepted bool
|
|
|
|
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 := ilovetui.NewTextarea(false)
|
|
ta.Blur()
|
|
ti := textinput.New()
|
|
ti.Prompt = ""
|
|
return Model{
|
|
listViewport: ilovetui.NewViewport(),
|
|
requestViewport: ilovetui.NewViewport(),
|
|
responseViewport: ilovetui.NewViewport(),
|
|
textarea: ta,
|
|
filterInput: ti,
|
|
pager: ilovetui.NewPaginator(),
|
|
help: ilovetui.NewHelp(),
|
|
}
|
|
}
|
|
|
|
func (m Model) Init() tea.Cmd { return nil }
|
|
|
|
func (m Model) IsEditing() bool { return m.editing || (m.filterActive && !m.filterAccepted) }
|
|
|
|
func (m Model) IsResponseFocused() bool {
|
|
return m.focusedPanel == panelResponse
|
|
}
|
|
|
|
func (m Model) CurrentRaw() string {
|
|
if len(m.entries) == 0 || m.cursor >= len(m.entries) {
|
|
return ""
|
|
}
|
|
if m.focusedPanel == panelResponse {
|
|
return m.entries[m.cursor].ResponseRaw
|
|
}
|
|
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 {
|
|
return
|
|
}
|
|
entries, err := d.ListReplayEntries()
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, dbe := range entries {
|
|
m.allEntries = append(m.allEntries, entryFromDB(dbe))
|
|
}
|
|
m.entries = m.allEntries
|
|
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,
|
|
Flagged: dbe.Flagged,
|
|
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)
|
|
m.filterInput.SetWidth(m.width - 4)
|
|
|
|
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
|
|
|
listInner := m.width - 2
|
|
if listInner < 0 {
|
|
listInner = 0
|
|
}
|
|
listVH := ilovetui.ContentHeight(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 := ilovetui.ContentHeight(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
|
|
}
|
|
|
|
func (m *Model) currentAllIdx() int {
|
|
if m.entryIndices != nil && m.cursor < len(m.entryIndices) {
|
|
return m.entryIndices[m.cursor]
|
|
}
|
|
if m.cursor < len(m.allEntries) {
|
|
return m.cursor
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func matchesFilter(e Entry, term string) bool {
|
|
return strings.Contains(strings.ToLower(e.Host), term) ||
|
|
strings.Contains(strings.ToLower(e.Path), term) ||
|
|
strings.Contains(strings.ToLower(strings.TrimSpace(e.Method)), term)
|
|
}
|
|
|
|
func (m *Model) applyFilter(preferAllIdx int) {
|
|
term := strings.ToLower(m.filterInput.Value())
|
|
if !m.filterActive || term == "" {
|
|
m.entries = m.allEntries
|
|
m.entryIndices = nil
|
|
if preferAllIdx >= 0 && preferAllIdx < len(m.allEntries) {
|
|
m.cursor = preferAllIdx
|
|
}
|
|
} else {
|
|
m.entries = nil
|
|
m.entryIndices = nil
|
|
newCursor := 0
|
|
found := false
|
|
for i, e := range m.allEntries {
|
|
if matchesFilter(e, term) {
|
|
if i == preferAllIdx && !found {
|
|
newCursor = len(m.entries)
|
|
found = true
|
|
}
|
|
m.entries = append(m.entries, e)
|
|
m.entryIndices = append(m.entryIndices, i)
|
|
}
|
|
}
|
|
if found {
|
|
m.cursor = newCursor
|
|
} else {
|
|
m.cursor = 0
|
|
}
|
|
}
|
|
if len(m.entries) == 0 {
|
|
m.cursor = 0
|
|
} else if m.cursor >= len(m.entries) {
|
|
m.cursor = len(m.entries) - 1
|
|
}
|
|
m.pager.SetTotalPages(len(m.entries))
|
|
m.refreshListViewport()
|
|
m.refreshBody()
|
|
}
|
|
|
|
func (m *Model) clearFilter() {
|
|
prevAllIdx := m.currentAllIdx()
|
|
m.filterActive = false
|
|
m.filterAccepted = false
|
|
m.filterInput.SetValue("")
|
|
m.filterInput.Blur()
|
|
m.recalcSizes()
|
|
m.applyFilter(prevAllIdx)
|
|
}
|
|
|
|
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, g.CycleFocus, r.Send, r.Edit, r.Flag, r.Filter, g.Help}
|
|
}
|
|
|
|
func (m replayKeyMap) FullHelp() [][]key.Binding {
|
|
g := keys.Keys.Global
|
|
pageGlobals := []key.Binding{g.Up, g.Down, g.CycleFocus, g.ScrollUp, g.ScrollDown, g.Left, g.Right, g.Escape, g.Copy, g.CopyAs, g.SendToDiff}
|
|
all := append(keys.Keys.Replay.Bindings(), pageGlobals...)
|
|
all = append(all, g.CommonBindings()...)
|
|
return keys.ChunkByWidth(all, m.width)
|
|
}
|