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