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,149 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/help"
|
||||
"charm.land/bubbles/v2/key"
|
||||
"charm.land/bubbles/v2/paginator"
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
"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 panel int
|
||||
|
||||
const (
|
||||
panelRequest panel = iota
|
||||
panelResponse
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
database *db.DB
|
||||
entries []db.Entry
|
||||
cursor int
|
||||
focusedPanel panel
|
||||
|
||||
listViewport viewport.Model
|
||||
bodyViewport viewport.Model
|
||||
pager paginator.Model
|
||||
help help.Model
|
||||
|
||||
searchInput textinput.Model
|
||||
searchKind searchKind
|
||||
searchAccepted bool
|
||||
searchErr string
|
||||
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
func New() Model {
|
||||
ti := textinput.New()
|
||||
ti.Prompt = ""
|
||||
return Model{
|
||||
listViewport: style.NewViewport(),
|
||||
bodyViewport: style.NewViewport(),
|
||||
pager: style.NewPaginator(),
|
||||
help: style.NewHelp(),
|
||||
searchInput: ti,
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) IsEditing() bool {
|
||||
return m.searchKind != searchKindOff && !m.searchAccepted
|
||||
}
|
||||
|
||||
// RefreshCmd returns the appropriate load command given the current search state.
|
||||
// The app model should call this instead of LoadEntriesCmd directly so that
|
||||
// background refreshes re-run the active search rather than resetting it.
|
||||
func (m Model) RefreshCmd() tea.Cmd {
|
||||
switch m.searchKind {
|
||||
case searchKindFulltext:
|
||||
return SearchCmd(m.database, m.searchInput.Value())
|
||||
case searchKindSQL:
|
||||
return nil
|
||||
default:
|
||||
return LoadEntriesCmd(m.database)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) clearSearch() tea.Cmd {
|
||||
m.searchKind = searchKindOff
|
||||
m.searchAccepted = false
|
||||
m.searchErr = ""
|
||||
m.searchInput.SetValue("")
|
||||
m.searchInput.Blur()
|
||||
m.recalcSizes()
|
||||
return LoadEntriesCmd(m.database)
|
||||
}
|
||||
|
||||
func (m *Model) acceptSearch() {
|
||||
m.searchAccepted = true
|
||||
m.searchInput.Blur()
|
||||
m.recalcSizes()
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd { return nil }
|
||||
|
||||
func (m *Model) SetDB(d *db.DB) {
|
||||
m.database = d
|
||||
}
|
||||
|
||||
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)
|
||||
// 2 (padding) + 2 (prefix char + space)
|
||||
m.searchInput.SetWidth(m.width - 4)
|
||||
|
||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
|
||||
inner := m.width - 2
|
||||
if inner < 0 {
|
||||
inner = 0
|
||||
}
|
||||
|
||||
listVH := style.PanelContentH(listH) - 1 // -1 for the pager dots row
|
||||
if listVH < 0 {
|
||||
listVH = 0
|
||||
}
|
||||
m.listViewport.SetWidth(inner)
|
||||
m.listViewport.SetHeight(listVH)
|
||||
m.pager.PerPage = listVH
|
||||
if m.pager.PerPage < 1 {
|
||||
m.pager.PerPage = 1
|
||||
}
|
||||
|
||||
bodyVH := style.PanelContentH(bodyH)
|
||||
m.bodyViewport.SetWidth(inner)
|
||||
m.bodyViewport.SetHeight(bodyVH)
|
||||
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
}
|
||||
|
||||
type historyKeyMap struct{ width int }
|
||||
|
||||
func (historyKeyMap) ShortHelp() []key.Binding {
|
||||
h := keys.Keys.History
|
||||
g := keys.Keys.Global
|
||||
return []key.Binding{
|
||||
g.Up, g.Down, g.CycleFocus,
|
||||
h.DeleteEntry, h.DeleteAll,
|
||||
h.Filter, h.SqlQuery,
|
||||
g.Help,
|
||||
}
|
||||
}
|
||||
|
||||
func (m historyKeyMap) FullHelp() [][]key.Binding {
|
||||
h := keys.Keys.History
|
||||
all := []key.Binding{h.DeleteEntry, h.DeleteAll, h.Filter, h.SqlQuery}
|
||||
all = append(all, keys.Keys.Global.Bindings()...)
|
||||
return keys.ChunkByWidth(all, m.width)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/db"
|
||||
)
|
||||
|
||||
type searchKind int
|
||||
|
||||
const (
|
||||
searchKindOff searchKind = iota
|
||||
searchKindFulltext
|
||||
searchKindSQL
|
||||
)
|
||||
|
||||
type SearchResultMsg struct {
|
||||
Entries []db.Entry
|
||||
}
|
||||
|
||||
type SearchErrMsg struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func SearchCmd(database *db.DB, term string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if database == nil {
|
||||
return SearchResultMsg{}
|
||||
}
|
||||
entries, err := database.SearchEntries(term)
|
||||
if err != nil {
|
||||
return SearchErrMsg{Err: err}
|
||||
}
|
||||
return SearchResultMsg{Entries: entries}
|
||||
}
|
||||
}
|
||||
|
||||
func SQLCmd(database *db.DB, query string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if database == nil {
|
||||
return SearchResultMsg{}
|
||||
}
|
||||
entries, err := database.QueryEntries(query)
|
||||
if err != nil {
|
||||
return SearchErrMsg{Err: err}
|
||||
}
|
||||
return SearchResultMsg{Entries: entries}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"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"
|
||||
diffUI "github.com/anotherhadi/spilltea/internal/ui/diff"
|
||||
replayUI "github.com/anotherhadi/spilltea/internal/ui/replay"
|
||||
"github.com/anotherhadi/spilltea/internal/util"
|
||||
)
|
||||
|
||||
type EntriesLoadedMsg struct {
|
||||
Entries []db.Entry
|
||||
}
|
||||
|
||||
func LoadEntriesCmd(database *db.DB) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if database == nil {
|
||||
return EntriesLoadedMsg{}
|
||||
}
|
||||
entries, _ := database.ListEntries()
|
||||
return EntriesLoadedMsg{Entries: entries}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case EntriesLoadedMsg:
|
||||
// Ignore background reloads while a search is active (but not during a mode switch reset).
|
||||
if m.searchKind != searchKindOff && (m.searchAccepted || m.searchInput.Value() != "") {
|
||||
return m, nil
|
||||
}
|
||||
prevCursor := m.cursor
|
||||
m.entries = msg.Entries
|
||||
if m.cursor >= len(m.entries) {
|
||||
m.cursor = len(m.entries) - 1
|
||||
}
|
||||
if m.cursor < 0 {
|
||||
m.cursor = 0
|
||||
}
|
||||
m.pager.SetTotalPages(len(m.entries))
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
if m.cursor != prevCursor {
|
||||
m.bodyViewport.SetYOffset(0)
|
||||
m.bodyViewport.SetXOffset(0)
|
||||
}
|
||||
|
||||
case SearchResultMsg:
|
||||
m.entries = msg.Entries
|
||||
m.cursor = 0
|
||||
m.searchErr = ""
|
||||
m.pager.SetTotalPages(len(m.entries))
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
m.bodyViewport.SetYOffset(0)
|
||||
m.bodyViewport.SetXOffset(0)
|
||||
if m.searchKind == searchKindSQL {
|
||||
m.acceptSearch()
|
||||
}
|
||||
|
||||
case SearchErrMsg:
|
||||
m.searchErr = msg.Err.Error()
|
||||
m.entries = nil
|
||||
m.pager.SetTotalPages(0)
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
m.bodyViewport.SetYOffset(0)
|
||||
m.bodyViewport.SetXOffset(0)
|
||||
|
||||
case tea.MouseWheelMsg:
|
||||
switch msg.Button {
|
||||
case tea.MouseWheelUp:
|
||||
if msg.Mod.Contains(tea.ModShift) {
|
||||
m.bodyViewport.ScrollLeft(6)
|
||||
} else {
|
||||
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() - 1)
|
||||
}
|
||||
case tea.MouseWheelDown:
|
||||
if msg.Mod.Contains(tea.ModShift) {
|
||||
m.bodyViewport.ScrollRight(6)
|
||||
} else {
|
||||
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() + 1)
|
||||
}
|
||||
case tea.MouseWheelLeft:
|
||||
m.bodyViewport.ScrollLeft(6)
|
||||
case tea.MouseWheelRight:
|
||||
m.bodyViewport.ScrollRight(6)
|
||||
}
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
h := keys.Keys.History
|
||||
g := keys.Keys.Global
|
||||
|
||||
if m.searchKind != searchKindOff && !m.searchAccepted {
|
||||
// Actively typing: only search navigation + accept/cancel.
|
||||
switch {
|
||||
case key.Matches(msg, g.Escape):
|
||||
return m, m.clearSearch()
|
||||
|
||||
case msg.String() == "enter":
|
||||
if m.searchKind == searchKindSQL {
|
||||
return m, SQLCmd(m.database, m.searchInput.Value())
|
||||
}
|
||||
m.acceptSearch()
|
||||
|
||||
case key.Matches(msg, g.Up):
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
m.bodyViewport.SetYOffset(0)
|
||||
m.bodyViewport.SetXOffset(0)
|
||||
}
|
||||
|
||||
case key.Matches(msg, g.Down):
|
||||
if m.cursor < len(m.entries)-1 {
|
||||
m.cursor++
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
m.bodyViewport.SetYOffset(0)
|
||||
m.bodyViewport.SetXOffset(0)
|
||||
}
|
||||
|
||||
default:
|
||||
var cmd tea.Cmd
|
||||
m.searchInput, cmd = m.searchInput.Update(msg)
|
||||
if m.searchKind == searchKindFulltext {
|
||||
return m, tea.Batch(cmd, SearchCmd(m.database, m.searchInput.Value()))
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if m.searchKind != searchKindOff && m.searchAccepted {
|
||||
// Filter accepted: Escape clears, all other shortcuts fall through.
|
||||
if key.Matches(msg, g.Escape) {
|
||||
return m, m.clearSearch()
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case key.Matches(msg, keys.Keys.History.Filter):
|
||||
prev := m.searchKind
|
||||
m.searchKind = searchKindFulltext
|
||||
m.searchAccepted = false
|
||||
m.searchInput.Placeholder = "filter requests..."
|
||||
m.searchErr = ""
|
||||
m.searchInput.Focus()
|
||||
m.recalcSizes()
|
||||
if prev != searchKindFulltext {
|
||||
m.searchInput.SetValue("")
|
||||
return m, LoadEntriesCmd(m.database)
|
||||
}
|
||||
|
||||
case key.Matches(msg, keys.Keys.History.SqlQuery):
|
||||
prev := m.searchKind
|
||||
m.searchKind = searchKindSQL
|
||||
m.searchAccepted = false
|
||||
m.searchInput.Placeholder = "status_code = 200 AND host LIKE '%.api.%'"
|
||||
m.searchErr = ""
|
||||
m.searchInput.Focus()
|
||||
m.recalcSizes()
|
||||
if prev != searchKindSQL {
|
||||
m.searchInput.SetValue("")
|
||||
return m, LoadEntriesCmd(m.database)
|
||||
}
|
||||
|
||||
case key.Matches(msg, g.Up):
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
m.bodyViewport.SetYOffset(0)
|
||||
m.bodyViewport.SetXOffset(0)
|
||||
}
|
||||
|
||||
case key.Matches(msg, g.Down):
|
||||
if m.cursor < len(m.entries)-1 {
|
||||
m.cursor++
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
m.bodyViewport.SetYOffset(0)
|
||||
m.bodyViewport.SetXOffset(0)
|
||||
}
|
||||
|
||||
case key.Matches(msg, g.CycleFocus):
|
||||
if m.focusedPanel == panelRequest {
|
||||
m.focusedPanel = panelResponse
|
||||
} else {
|
||||
m.focusedPanel = panelRequest
|
||||
}
|
||||
m.refreshBody()
|
||||
m.bodyViewport.SetYOffset(0)
|
||||
m.bodyViewport.SetXOffset(0)
|
||||
|
||||
case key.Matches(msg, g.SendToReplay):
|
||||
if len(m.entries) > 0 {
|
||||
e := m.entries[m.cursor]
|
||||
scheme := util.InferScheme(e.Host)
|
||||
return m, func() tea.Msg {
|
||||
return replayUI.SendToReplayMsg{
|
||||
Scheme: scheme,
|
||||
Host: e.Host,
|
||||
RequestRaw: e.RequestRaw,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case key.Matches(msg, g.SendToDiff):
|
||||
if len(m.entries) > 0 {
|
||||
e := m.entries[m.cursor]
|
||||
var raw, label string
|
||||
if m.focusedPanel == panelResponse {
|
||||
raw = e.ResponseRaw
|
||||
label = fmt.Sprintf("%d %s", e.StatusCode, http.StatusText(e.StatusCode))
|
||||
} else {
|
||||
raw = e.RequestRaw
|
||||
label = e.Method + " " + e.Host + e.Path
|
||||
}
|
||||
return m, func() tea.Msg {
|
||||
return diffUI.SendToDiffMsg{Label: label, Raw: raw}
|
||||
}
|
||||
}
|
||||
|
||||
case key.Matches(msg, h.DeleteEntry):
|
||||
if len(m.entries) > 0 {
|
||||
id := m.entries[m.cursor].ID
|
||||
if m.database != nil {
|
||||
m.database.DeleteEntry(id)
|
||||
}
|
||||
return m, LoadEntriesCmd(m.database)
|
||||
}
|
||||
|
||||
case key.Matches(msg, h.DeleteAll):
|
||||
if m.database != nil {
|
||||
if m.searchKind != searchKindOff {
|
||||
for _, e := range m.entries {
|
||||
m.database.DeleteEntry(e.ID)
|
||||
}
|
||||
} else {
|
||||
m.database.DeleteAllEntries()
|
||||
}
|
||||
}
|
||||
return m, m.clearSearch()
|
||||
|
||||
case key.Matches(msg, g.ScrollUp):
|
||||
step := m.bodyViewport.Height() / 2
|
||||
if step < 1 {
|
||||
step = 1
|
||||
}
|
||||
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() - step)
|
||||
|
||||
case key.Matches(msg, g.ScrollDown):
|
||||
step := m.bodyViewport.Height() / 2
|
||||
if step < 1 {
|
||||
step = 1
|
||||
}
|
||||
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() + step)
|
||||
|
||||
case key.Matches(msg, g.Left):
|
||||
m.bodyViewport.ScrollLeft(6)
|
||||
|
||||
case key.Matches(msg, g.Right):
|
||||
m.bodyViewport.ScrollRight(6)
|
||||
|
||||
case key.Matches(msg, keys.Keys.Global.Help):
|
||||
m.help.ShowAll = !m.help.ShowAll
|
||||
m.recalcSizes()
|
||||
}
|
||||
}
|
||||
|
||||
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.bodyViewport.SetContent("")
|
||||
return
|
||||
}
|
||||
e := m.entries[m.cursor]
|
||||
var raw string
|
||||
if m.focusedPanel == panelResponse {
|
||||
raw = e.ResponseRaw
|
||||
} else {
|
||||
raw = e.RequestRaw
|
||||
}
|
||||
m.bodyViewport.SetContent(style.HighlightHTTP(raw))
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/icons"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"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)
|
||||
|
||||
content := lipgloss.JoinVertical(lipgloss.Left,
|
||||
m.renderListPanel(m.width, listH),
|
||||
m.renderBodyPanel(bodyH),
|
||||
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.History+"History", inner, w, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderBodyPanel(h int) string {
|
||||
s := style.S
|
||||
title := icons.I.Request + "Request"
|
||||
if m.focusedPanel == panelResponse {
|
||||
title = icons.I.Response + "Response"
|
||||
}
|
||||
return style.RenderWithTitle(s.Panel, title, m.bodyViewport.View(), m.width, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderStatusBar() string {
|
||||
s := style.S
|
||||
pad := lipgloss.NewStyle().Padding(0, 1)
|
||||
escKey := keys.Keys.Global.Escape.Help().Key
|
||||
switch m.searchKind {
|
||||
case searchKindFulltext:
|
||||
filterKey := keys.Keys.History.Filter.Help().Key
|
||||
if m.searchAccepted {
|
||||
accent := lipgloss.NewStyle().Foreground(s.Primary)
|
||||
filterLine := pad.Render(accent.Render(filterKey) + " " + s.Bold.Render(m.searchInput.Value()) + s.Faint.Render(" "+escKey+" to clear"))
|
||||
return lipgloss.JoinVertical(lipgloss.Left, filterLine, pad.Render(m.help.View(historyKeyMap{width: m.width})))
|
||||
}
|
||||
return pad.Render(s.Faint.Render(filterKey) + " " + m.searchInput.View())
|
||||
case searchKindSQL:
|
||||
sqlKey := keys.Keys.History.SqlQuery.Help().Key
|
||||
if m.searchAccepted {
|
||||
accent := lipgloss.NewStyle().Foreground(s.Primary)
|
||||
filterLine := pad.Render(accent.Render(sqlKey) + " " + s.Bold.Render(m.searchInput.Value()) + s.Faint.Render(" "+escKey+" to clear"))
|
||||
return lipgloss.JoinVertical(lipgloss.Left, filterLine, pad.Render(m.help.View(historyKeyMap{width: m.width})))
|
||||
}
|
||||
return pad.Render(s.Faint.Render(sqlKey) + " " + m.searchInput.View())
|
||||
default:
|
||||
return pad.Render(m.help.View(historyKeyMap{width: m.width}))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) renderList() string {
|
||||
s := style.S
|
||||
if m.searchErr != "" {
|
||||
return lipgloss.Place(
|
||||
m.listViewport.Width(), m.listViewport.Height(),
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
lipgloss.NewStyle().Foreground(s.Error).Render(m.searchErr),
|
||||
)
|
||||
}
|
||||
if len(m.entries) == 0 {
|
||||
msg := " (⌐■_■)\nno history yet"
|
||||
if m.searchKind != searchKindOff {
|
||||
msg = "ʕノ•ᴥ•ʔノ ︵ ┻━┻\n no results"
|
||||
}
|
||||
return lipgloss.Place(
|
||||
m.listViewport.Width(), m.listViewport.Height(),
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
s.Faint.Render(msg),
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
statusStr := fmt.Sprintf("%3d", e.StatusCode)
|
||||
const fixedW = 2 + 7 + 1 + 3 + 1 + 10 + 1
|
||||
hostPathW := w - fixedW
|
||||
if hostPathW < 0 {
|
||||
hostPathW = 0
|
||||
}
|
||||
|
||||
ts := e.Timestamp.Format("15:04:05")
|
||||
statusSt := style.StatusStyle(e.StatusCode, 3)
|
||||
|
||||
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.Foreground(s.Subtle).Width(10).Render(ts),
|
||||
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.Faint.Width(10).Render(ts),
|
||||
" ",
|
||||
s.Bold.Render(e.Host),
|
||||
s.Faint.Render(e.Path),
|
||||
)
|
||||
}
|
||||
sb.WriteString(line + "\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
Reference in New Issue
Block a user