diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-24 09:17:20 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-24 09:17:20 +0200 |
| commit | 1cbb2430d027c9d8850bf3a2b79a05338efea3ea (patch) | |
| tree | 5572c87d76b18ec8ea2956fb9e9a20dc7e69b2ac /internal | |
| parent | ceb5c392d363f9f6afccd310b0a7a7efb14bb4e3 (diff) | |
tui: add help overlay behavior and docs update
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/tui/tui.go | 46 | ||||
| -rw-r--r-- | internal/tui/tui_test.go | 86 |
2 files changed, 132 insertions, 0 deletions
diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 90b0162..4d7a7dc 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -10,12 +10,14 @@ import ( tuiexport "ior/internal/tui/export" "ior/internal/tui/pidpicker" "os" + "strings" "sync" "time" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) // Screen identifies the currently active TUI screen. @@ -85,6 +87,7 @@ type Model struct { attaching bool spin spinner.Model lastErr error + showHelp bool startTrace TraceStarter traceStop context.CancelFunc @@ -139,6 +142,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.stopTrace() return m, tea.Quit } + if !m.exporter.Visible() && key.Matches(msg, m.keys.Help) { + m.showHelp = !m.showHelp + return m, nil + } + if !m.exporter.Visible() && m.showHelp && key.Matches(msg, m.keys.Esc) { + m.showHelp = false + return m, nil + } + if !m.exporter.Visible() && m.showHelp { + return m, nil + } if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.Export) && !m.exporter.Visible() { m.exporter = m.exporter.Open() return m, nil @@ -264,12 +278,18 @@ func (m Model) View() string { if m.exporter.Visible() { return m.exporter.View(m.width, m.height) + "\n" + base } + if m.showHelp { + return renderHelpOverlay(m.width, m.height, [][]key.Binding{m.keys.PickerShortHelp()}) + "\n" + base + } return base case ScreenDashboard: base := m.dashboard.View() if m.exporter.Visible() { return m.exporter.View(m.width, m.height) + "\n" + base } + if m.showHelp { + return renderHelpOverlay(m.width, m.height, m.keys.DashboardFullHelp()) + "\n" + base + } return base default: return "" @@ -407,3 +427,29 @@ func snapValueF(snap *statsengine.Snapshot, get func(*statsengine.Snapshot) floa func exportFlamegraph() (string, error) { return "", errors.New("flamegraph export is not yet available in TUI mode") } + +func renderHelpOverlay(width, height int, groups [][]key.Binding) string { + if width <= 0 { + width = 80 + } + if height <= 0 { + height = 24 + } + + lines := []string{"Help"} + for _, group := range groups { + parts := make([]string, 0, len(group)) + for _, binding := range group { + h := binding.Help() + parts = append(parts, fmt.Sprintf("%s %s", h.Key, h.Desc)) + } + lines = append(lines, strings.Join(parts, " • ")) + } + lines = append(lines, "", "Esc/? close") + + box := PanelStyle.Copy(). + Width(72). + Render(strings.Join(lines, "\n")) + + return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, box) +} diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index d66ec84..98b249f 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -229,3 +229,89 @@ func TestRunExportCmdCSVWritesFile(t *testing.T) { t.Fatalf("expected CSV file to exist: %v", err) } } + +func TestHelpKeyTogglesOverlay(t *testing.T) { + m := NewModel(-1, func(context.Context) error { return nil }) + if m.showHelp { + t.Fatalf("expected help hidden by default") + } + + next, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}}) + updated := next.(Model) + if !updated.showHelp { + t.Fatalf("expected help to be shown after ?") + } + + next, _ = updated.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}}) + updated = next.(Model) + if updated.showHelp { + t.Fatalf("expected help to toggle off after second ?") + } +} + +func TestViewShowsHelpOverlay(t *testing.T) { + m := NewModel(-1, func(context.Context) error { return nil }) + m.screen = ScreenDashboard + m.showHelp = true + m.width = 100 + m.height = 30 + + out := m.View() + if !strings.Contains(out, "Help") { + t.Fatalf("expected help title in overlay") + } + if !strings.Contains(out, "tab next tab") { + t.Fatalf("expected keybinding text in overlay") + } +} + +func TestHelpOverlayBlocksUnderlyingActions(t *testing.T) { + m := NewModel(-1, func(context.Context) error { return nil }) + m.screen = ScreenDashboard + m.showHelp = true + + next, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'e'}}) + updated := next.(Model) + if updated.exporter.Visible() { + t.Fatalf("expected export modal to stay closed while help overlay is active") + } +} + +func TestHelpOverlayUsesPickerBindingsOnPickerScreen(t *testing.T) { + m := NewModel(-1, func(context.Context) error { return nil }) + m.screen = ScreenPIDPicker + m.showHelp = true + m.width = 100 + m.height = 30 + + out := m.View() + if !strings.Contains(out, "enter select") || !strings.Contains(out, "r refresh") { + t.Fatalf("expected picker shortcuts in help overlay") + } + if strings.Contains(out, "e export") { + t.Fatalf("did not expect dashboard-only shortcut in picker help overlay") + } +} + +func TestHelpToggleDoesNotBreakExportModalInput(t *testing.T) { + m := NewModel(-1, func(context.Context) error { return nil }) + m.screen = ScreenDashboard + + next, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'e'}}) + updated := next.(Model) + if !updated.exporter.Visible() { + t.Fatalf("expected export modal to open") + } + + next, _ = updated.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}}) + updated = next.(Model) + if updated.showHelp { + t.Fatalf("did not expect hidden help flag while export modal is open") + } + + next, _ = updated.Update(tea.KeyMsg{Type: tea.KeyEsc}) + updated = next.(Model) + if updated.exporter.Visible() { + t.Fatalf("expected esc to close export modal") + } +} |
