internal/tui/view/response/timing.go

// Package response renders response-specific views. Right now that is
// just the Timing tab; in principle a cookies inspector belongs here too.
//
// mercemay.top/src/httptap/
package response

import (
	"fmt"
	"strings"
	"time"

	"mercemay.top/httptap/internal/parser"
	"mercemay.top/httptap/internal/tui/theme"
)

// RenderTiming draws a small waterfall chart of the message lifecycle.
// Durations are taken from parser.Timings when present.
func RenderTiming(pal theme.Palette, msg parser.Message) string {
	ts := parser.Timings(msg)
	if ts == nil {
		return pal.Detail.Render("(no timing info)")
	}
	total := ts.Total()
	if total == 0 {
		return pal.Detail.Render("(zero-length capture)")
	}
	rows := []struct {
		label string
		dur   time.Duration
	}{
		{"DNS", ts.DNS},
		{"Connect", ts.Connect},
		{"TLS", ts.TLS},
		{"Wait", ts.Wait},
		{"Receive", ts.Receive},
	}
	const barWidth = 40
	var b strings.Builder
	for _, r := range rows {
		frac := float64(r.dur) / float64(total)
		filled := int(frac*float64(barWidth) + 0.5)
		if filled > barWidth {
			filled = barWidth
		}
		bar := strings.Repeat("█", filled) + strings.Repeat("·", barWidth-filled)
		fmt.Fprintf(&b, "%-8s %s  %v\n", r.label, bar, r.dur)
	}
	fmt.Fprintf(&b, "%-8s %s  %v\n", "Total", strings.Repeat("█", barWidth), total)
	return strings.TrimRight(b.String(), "\n")
}

// FormatDuration formats d with units that vary smoothly across scales.
func FormatDuration(d time.Duration) string {
	switch {
	case d >= time.Second:
		return fmt.Sprintf("%.2fs", d.Seconds())
	case d >= time.Millisecond:
		return fmt.Sprintf("%dms", d.Milliseconds())
	case d >= time.Microsecond:
		return fmt.Sprintf("%dµs", d/time.Microsecond)
	default:
		return fmt.Sprintf("%dns", d.Nanoseconds())
	}
}

// Sparkline returns a compact horizontal bar proportional to d/max.
// Used by the list view's optional timing column.
func Sparkline(d, max time.Duration, width int) string {
	if max <= 0 || width <= 0 {
		return strings.Repeat(" ", width)
	}
	frac := float64(d) / float64(max)
	if frac > 1 {
		frac = 1
	}
	n := int(frac*float64(width) + 0.5)
	if n < 1 && d > 0 {
		n = 1
	}
	return strings.Repeat("▮", n) + strings.Repeat(" ", width-n)
}