182 lines
4.6 KiB
Go
182 lines
4.6 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
const maxHistory = 100
|
|
|
|
// HistoryEntry records one completed HTTP transaction.
|
|
type HistoryEntry struct {
|
|
At time.Time
|
|
Name string
|
|
Method string
|
|
URL string
|
|
StatusCode int
|
|
Status string
|
|
Duration time.Duration
|
|
Size int
|
|
ContentType string
|
|
IsError bool
|
|
ErrMsg string
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Formatting helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func fmtDuration(d time.Duration) string {
|
|
switch {
|
|
case d < time.Millisecond:
|
|
return "< 1ms"
|
|
case d < time.Second:
|
|
return fmt.Sprintf("%dms", d.Milliseconds())
|
|
case d < time.Minute:
|
|
return fmt.Sprintf("%.1fs", d.Seconds())
|
|
default:
|
|
m := int(d.Minutes())
|
|
s := int(d.Seconds()) % 60
|
|
return fmt.Sprintf("%dm%ds", m, s)
|
|
}
|
|
}
|
|
|
|
func fmtSize(n int) string {
|
|
switch {
|
|
case n < 1024:
|
|
return fmt.Sprintf("%d B", n)
|
|
case n < 1024*1024:
|
|
return fmt.Sprintf("%.1f KB", float64(n)/1024)
|
|
default:
|
|
return fmt.Sprintf("%.1f MB", float64(n)/1024/1024)
|
|
}
|
|
}
|
|
|
|
func durationStyle(d time.Duration) lipgloss.Style {
|
|
switch {
|
|
case d < 200*time.Millisecond:
|
|
return lipgloss.NewStyle().Foreground(colorGreen)
|
|
case d < time.Second:
|
|
return lipgloss.NewStyle().Foreground(colorYellow)
|
|
default:
|
|
return lipgloss.NewStyle().Foreground(colorRed)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Stats bar (rendered inside the response panel, below the viewport)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// renderStatsBar returns a single line summarising the last response.
|
|
func renderStatsBar(e HistoryEntry, width int) string {
|
|
if e.IsError {
|
|
return statusErrStyle.Render(" ✗ " + e.ErrMsg)
|
|
}
|
|
|
|
status := statusCodeStyle(e.StatusCode).Render(e.Status)
|
|
dur := durationStyle(e.Duration).Render("⚡ " + fmtDuration(e.Duration))
|
|
size := lipgloss.NewStyle().Foreground(colorFg).Render("◎ " + fmtSize(e.Size))
|
|
|
|
sep := dimItemStyle.Render(" │ ")
|
|
bar := " " + status + sep + dur + sep + size
|
|
|
|
// Append content-type if it fits
|
|
if e.ContentType != "" {
|
|
candidate := bar + sep + dimItemStyle.Render(e.ContentType)
|
|
if lipgloss.Width(candidate) <= width-2 {
|
|
bar = candidate
|
|
}
|
|
}
|
|
return bar
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// History overlay table
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// renderHistoryTable builds the full text content for the history viewport.
|
|
func renderHistoryTable(history []HistoryEntry, vpWidth int) string {
|
|
if len(history) == 0 {
|
|
return hintStyle.Render("\n No requests yet in this session.")
|
|
}
|
|
|
|
// Column widths
|
|
const (
|
|
colTime = 8 // HH:MM:SS
|
|
colMethod = 7 // DELETE + space
|
|
colStatus = 10 // "200 OK" etc.
|
|
colDur = 9 // "1.2s" etc.
|
|
colSize = 8 // "4.2 KB"
|
|
colSep = 2 // " "
|
|
)
|
|
fixedW := colTime + colMethod + colStatus + colDur + colSize + colSep*5
|
|
nameW := vpWidth - fixedW - 2
|
|
if nameW < 10 {
|
|
nameW = 10
|
|
}
|
|
|
|
header := buildHistoryRow(
|
|
dimItemStyle,
|
|
"Time", "Method", "Status", "Duration", "Size", "Name",
|
|
colTime, colMethod, colStatus, colDur, colSize, nameW,
|
|
)
|
|
divider := dimItemStyle.Render(strings.Repeat("─", vpWidth-2))
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString(header + "\n")
|
|
sb.WriteString(divider + "\n")
|
|
|
|
// Most recent first
|
|
for i := len(history) - 1; i >= 0; i-- {
|
|
e := history[i]
|
|
|
|
timeStr := e.At.Format("15:04:05")
|
|
method := e.Method
|
|
status := e.Status
|
|
dur := fmtDuration(e.Duration)
|
|
size := fmtSize(e.Size)
|
|
name := e.Name
|
|
if len(name) > nameW {
|
|
name = name[:nameW-1] + "…"
|
|
}
|
|
|
|
var rowStyle lipgloss.Style
|
|
if e.IsError {
|
|
rowStyle = lipgloss.NewStyle().Foreground(colorRed)
|
|
} else {
|
|
rowStyle = lipgloss.NewStyle().Foreground(colorFg)
|
|
}
|
|
|
|
methodStr := methodStyle(method).Render(fmt.Sprintf("%-*s", colMethod, method))
|
|
statusStr := statusCodeStyle(e.StatusCode).Render(fmt.Sprintf("%-*s", colStatus, status))
|
|
durStr := durationStyle(e.Duration).Render(fmt.Sprintf("%-*s", colDur, dur))
|
|
|
|
row := rowStyle.Render(timeStr+" ") +
|
|
methodStr + " " +
|
|
statusStr + " " +
|
|
durStr + " " +
|
|
rowStyle.Render(fmt.Sprintf("%-*s", colSize, size)+" "+name)
|
|
|
|
sb.WriteString(row + "\n")
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func buildHistoryRow(s lipgloss.Style, t, method, status, dur, size, name string,
|
|
wT, wM, wSt, wD, wSz, wN int) string {
|
|
return s.Render(
|
|
fmt.Sprintf("%-*s %-*s %-*s %-*s %-*s %-*s",
|
|
wT, t,
|
|
wM, method,
|
|
wSt, status,
|
|
wD, dur,
|
|
wSz, size,
|
|
wN, name,
|
|
),
|
|
)
|
|
}
|