internal/tui/view/detail/detail.go

// Package detail is the right-hand pane: tabs for Headers/Body/Timing
// plus a shared scroll viewport.
//
// mercemay.top/src/httptap/
package detail

import (
	"strings"

	tea "github.com/charmbracelet/bubbletea"

	"mercemay.top/httptap/internal/parser"
	"mercemay.top/httptap/internal/tui/theme"
	"mercemay.top/httptap/internal/tui/view/request"
	"mercemay.top/httptap/internal/tui/view/response"
	"mercemay.top/httptap/internal/tui/viewport"
)

// Model is the detail pane state.
type Model struct {
	pal    theme.Palette
	tabs   Tabs
	active int
	msg    parser.Message
	vp     viewport.Model
	width  int
	height int
}

// New returns an empty detail pane.
func New(pal theme.Palette) Model {
	return Model{
		pal:  pal,
		tabs: NewTabs(pal, []string{"Headers", "Body", "Timing"}),
		vp:   viewport.New(0, 0),
	}
}

// SetSize responds to App.relayout.
func (m *Model) SetSize(w, h int) {
	m.width, m.height = w, h
	// Reserve a row for the tab strip.
	m.vp.SetSize(w-2, h-2)
}

// Height is exposed for app tests.
func (m Model) Height() int { return m.height }

// SetMessage refreshes the message shown in the pane and re-renders the
// active tab's contents into the viewport.
func (m *Model) SetMessage(msg parser.Message) {
	m.msg = msg
	m.refresh()
}

// Update handles key input: tab switching and viewport scrolling.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	if k, ok := msg.(tea.KeyMsg); ok {
		switch k.String() {
		case "tab", "l":
			m.active = (m.active + 1) % len(m.tabs.Labels())
			m.refresh()
			return m, nil
		case "shift+tab", "h":
			m.active = (m.active - 1 + len(m.tabs.Labels())) % len(m.tabs.Labels())
			m.refresh()
			return m, nil
		}
	}
	var cmd tea.Cmd
	m.vp, cmd = m.vp.Update(msg)
	return m, cmd
}

// refresh re-paints the viewport body for the active tab.
func (m *Model) refresh() {
	switch m.active {
	case 0:
		m.vp.SetContent(request.RenderHeaders(m.pal, m.msg.Headers))
	case 1:
		m.vp.SetContent(request.RenderBody(m.pal, m.msg))
	case 2:
		m.vp.SetContent(response.RenderTiming(m.pal, m.msg))
	}
}

// View paints the tab strip above the viewport.
func (m Model) View() string {
	strip := m.tabs.Render(m.active, m.width)
	body := m.pal.Detail.Width(m.width).Height(m.height - 2).Render(m.vp.View())
	return strings.Join([]string{strip, body}, "\n")
}