internal/tui/view/list/item.go

package list

import (
	"fmt"
	"strings"

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

// CompileMatcher parses a filter expression. It is a thin shim so that
// the list package can be tested in isolation; the real parser lives in
// internal/filter/expr.
type compiler interface {
	Parse(expr string) Matcher
}

// defaultCompiler implements a minimal k=v expression language used by
// tests. The real App wires in internal/filter.
type defaultCompiler struct{}

func (defaultCompiler) Parse(expr string) Matcher {
	expr = strings.TrimSpace(expr)
	parts := strings.SplitN(expr, "=", 2)
	if len(parts) != 2 {
		return func(parser.Message) bool { return true }
	}
	key := strings.ToLower(parts[0])
	val := parts[1]
	return func(m parser.Message) bool {
		switch key {
		case "path":
			return strings.Contains(startPath(m.StartLine), val)
		case "method":
			return strings.HasPrefix(m.StartLine, val+" ")
		case "host":
			return strings.EqualFold(parser.Header(m, "Host"), val)
		}
		return true
	}
}

// CompileMatcher is resolved at package init so tests can override it.
var CompileMatcher = defaultCompiler{}.Parse

// startPath extracts the request-target from a start line.
func startPath(line string) string {
	parts := strings.SplitN(line, " ", 3)
	if len(parts) < 2 {
		return ""
	}
	return parts[1]
}

// renderRows produces the slice of rows currently visible.
func (m Model) renderRows() string {
	var b strings.Builder
	end := m.top + m.height
	if end > len(m.visible) {
		end = len(m.visible)
	}
	for i := m.top; i < end; i++ {
		row := m.renderItem(m.items[m.visible[i]])
		if i == m.cursor {
			row = m.pal.ListSelected.Render(row)
		}
		b.WriteString(theme.Truncate(row, m.width-2))
		b.WriteByte('\n')
	}
	return strings.TrimRight(b.String(), "\n")
}

// renderItem formats a single row: "METHOD /path  host  status size".
func (m Model) renderItem(msg parser.Message) string {
	method, target, _ := splitStartLine(msg.StartLine)
	status := parser.StatusCode(msg)
	size := len(msg.Body)
	methodCell := m.pal.MethodStyle(method).Render(fmt.Sprintf("%-6s", method))
	statusCell := m.pal.StatusStyle(status).Render(fmt.Sprintf("%d", status))
	return fmt.Sprintf("%s %-40s %s %6dB",
		methodCell, theme.Truncate(target, 40), statusCell, size)
}

// splitStartLine returns (method, target, version). Responses return an
// empty method and an empty target; the caller treats them specially.
func splitStartLine(line string) (method, target, version string) {
	parts := strings.SplitN(line, " ", 3)
	if len(parts) == 3 {
		if strings.HasPrefix(parts[0], "HTTP/") {
			return "", "", parts[0]
		}
		return parts[0], parts[1], parts[2]
	}
	return "", "", ""
}