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, ), ) }