internal/tui/view/request/headers.go

// Package request contains renderers shared by the detail pane's
// Headers and Body tabs. They are kept separate from detail.Model to
// make them easy to snapshot-test.
//
// mercemay.top/src/httptap/
package request

import (
	"sort"
	"strings"

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

// RenderHeaders formats the header block as a two-column aligned list.
// Canonical casing is assumed to have already been applied; values are
// wrapped if they exceed a sensible width.
func RenderHeaders(pal theme.Palette, headers [][2]string) string {
	if len(headers) == 0 {
		return pal.Detail.Render("(no headers)")
	}
	// Compute key column width for alignment.
	maxKey := 0
	for _, kv := range headers {
		if n := len(kv[0]); n > maxKey {
			maxKey = n
		}
	}
	if maxKey > 32 {
		maxKey = 32
	}
	var b strings.Builder
	for _, kv := range headers {
		k := theme.Truncate(kv[0], maxKey)
		pad := strings.Repeat(" ", maxKey-len(k))
		b.WriteString(pal.HeaderKey().Render(k + pad))
		b.WriteString("  ")
		b.WriteString(pal.HeaderVal().Render(kv[1]))
		b.WriteByte('\n')
	}
	return strings.TrimRight(b.String(), "\n")
}

// SortedCopy returns a copy of headers sorted by name. It is used by the
// "sort headers" keybinding so that diffs between requests are easier to
// eyeball.
func SortedCopy(headers [][2]string) [][2]string {
	out := make([][2]string, len(headers))
	copy(out, headers)
	sort.SliceStable(out, func(i, j int) bool {
		if out[i][0] == out[j][0] {
			return out[i][1] < out[j][1]
		}
		return out[i][0] < out[j][0]
	})
	return out
}