summaryrefslogtreecommitdiff
path: root/internal/tui/dashboard/tabregistry.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tui/dashboard/tabregistry.go')
-rw-r--r--internal/tui/dashboard/tabregistry.go257
1 files changed, 257 insertions, 0 deletions
diff --git a/internal/tui/dashboard/tabregistry.go b/internal/tui/dashboard/tabregistry.go
new file mode 100644
index 0000000..d16a363
--- /dev/null
+++ b/internal/tui/dashboard/tabregistry.go
@@ -0,0 +1,257 @@
+package dashboard
+
+import (
+ "sort"
+
+ "ior/internal/statsengine"
+ common "ior/internal/tui/common"
+ "ior/internal/tui/eventstream"
+ flamegraphtui "ior/internal/tui/flamegraph"
+ "ior/internal/tui/messages"
+
+ tea "charm.land/bubbletea/v2"
+)
+
+// tabRenderFn is a function that renders a tab's content area given the
+// current model state and viewport dimensions. It returns the rendered string.
+type tabRenderFn func(m *Model, snap *statsengine.Snapshot, stream *eventstream.Model, flame *flamegraphtui.Model, width, height int) string
+
+// tabScrollFn handles scroll key presses for a specific tab. Returns whether
+// the key was handled and an optional tea.Cmd. It is called only when bubbles
+// are not active (bubble scroll is handled before tab dispatch).
+type tabScrollFn func(m *Model, msg tea.KeyPressMsg) (bool, tea.Cmd)
+
+// tabDescriptor captures all per-tab metadata and behaviour so that adding a
+// new tab only requires registering a new entry — no switch statements need to
+// be modified.
+type tabDescriptor struct {
+ // Name is the full display label shown in the tab bar (e.g. "Overview").
+ Name string
+ // ShortName is the abbreviated label used when the tab bar is narrow.
+ ShortName string
+ // Position controls the left-to-right order of tabs in the tab bar.
+ // Lower values appear first. The existing tabs use positions 10–70 in
+ // steps of 10 so new tabs can be inserted without renumbering.
+ Position int
+ // AllowedVizModes lists the visualization modes available for this tab.
+ // Tabs that only support the plain table view contain a single entry.
+ AllowedVizModes []tabVizMode
+ // InitCmd is an optional extra Bubble Tea command to start alongside the
+ // global refresh tick when this tab is the active tab on Init. Tabs that
+ // need their own high-frequency tick (stream, flame) set this; others leave
+ // it nil.
+ InitCmd func() tea.Cmd
+ // Render draws the tab body. Nil means the tab has no registered renderer
+ // (used for tabs that handle rendering via other paths).
+ Render tabRenderFn
+ // HandleScroll handles direction keys for this tab when bubbles are off.
+ // Nil means the tab does not process scroll/navigation keys.
+ HandleScroll tabScrollFn
+}
+
+// tabDescriptors is the central registry mapping every known Tab to its
+// descriptor. Registering a new tab here is all that is required to make it
+// participate in the tab bar, keyboard navigation, and rendering dispatch.
+var tabDescriptors = map[Tab]tabDescriptor{
+ TabFlame: {
+ Name: "Flame",
+ ShortName: "Flm",
+ Position: 10,
+ AllowedVizModes: []tabVizMode{tabVizModeTable},
+ InitCmd: flameTickCmd,
+ Render: tabRenderFlame,
+ HandleScroll: nil,
+ },
+ TabOverview: {
+ Name: "Overview",
+ ShortName: "Ovr",
+ Position: 20,
+ AllowedVizModes: []tabVizMode{tabVizModeTable},
+ Render: tabRenderOverview,
+ HandleScroll: nil,
+ },
+ TabSyscalls: {
+ Name: "Syscalls",
+ ShortName: "Sys",
+ Position: 30,
+ AllowedVizModes: []tabVizMode{tabVizModeTable, tabVizModeBubbles, tabVizModeTreemap},
+ Render: tabRenderSyscalls,
+ HandleScroll: tabScrollSyscalls,
+ },
+ TabFiles: {
+ Name: "Files",
+ ShortName: "Fil",
+ Position: 40,
+ AllowedVizModes: []tabVizMode{tabVizModeTable},
+ Render: tabRenderFiles,
+ HandleScroll: tabScrollFiles,
+ },
+ TabProcesses: {
+ Name: "Processes",
+ ShortName: "Pro",
+ Position: 50,
+ AllowedVizModes: []tabVizMode{tabVizModeTable, tabVizModeBubbles, tabVizModeTreemap},
+ Render: tabRenderProcesses,
+ HandleScroll: tabScrollProcesses,
+ },
+ TabLatency: {
+ Name: "Latency+Gaps",
+ ShortName: "Lat",
+ Position: 60,
+ AllowedVizModes: []tabVizMode{tabVizModeTable},
+ Render: tabRenderLatency,
+ HandleScroll: nil,
+ },
+ TabStream: {
+ Name: "Stream",
+ ShortName: "Str",
+ Position: 70,
+ AllowedVizModes: []tabVizMode{tabVizModeTable},
+ InitCmd: streamTickCmd,
+ Render: tabRenderStream,
+ HandleScroll: tabScrollStream,
+ },
+}
+
+// orderedTabs returns all registered tabs sorted by their Position field.
+// This is the canonical tab order used for tab bar rendering and navigation.
+// It replaces the hardcoded allTabs slice so new tabs registered in
+// tabDescriptors automatically appear in the correct position.
+func orderedTabs() []Tab {
+ tabs := make([]Tab, 0, len(tabDescriptors))
+ for tab := range tabDescriptors {
+ tabs = append(tabs, tab)
+ }
+ sort.Slice(tabs, func(i, j int) bool {
+ return tabDescriptors[tabs[i]].Position < tabDescriptors[tabs[j]].Position
+ })
+ return tabs
+}
+
+// lookupTab returns the descriptor for the given tab, falling back to a
+// sensible default when the tab is not in the registry.
+func lookupTab(tab Tab) tabDescriptor {
+ if d, ok := tabDescriptors[tab]; ok {
+ return d
+ }
+ return tabDescriptor{Name: "Unknown", ShortName: "Unk", AllowedVizModes: []tabVizMode{tabVizModeTable}}
+}
+
+// tabAllowedVizModes returns the visualisation modes allowed for tab,
+// respecting any runtime conditions (e.g. Files requires dir-grouped mode for
+// non-table views). This replaces the former allowedVizModes switch statement.
+func tabAllowedVizModes(tab Tab, filesDirGrouped bool) []tabVizMode {
+ if tab == TabFiles && filesDirGrouped {
+ return []tabVizMode{tabVizModeTable, tabVizModeBubbles, tabVizModeTreemap, tabVizModeIcicle}
+ }
+ return lookupTab(tab).AllowedVizModes
+}
+
+// tabRenderFlame adapts the flame model's View to the tabRenderFn signature.
+func tabRenderFlame(_ *Model, _ *statsengine.Snapshot, _ *eventstream.Model, flame *flamegraphtui.Model, _, _ int) string {
+ if flame == nil {
+ return common.PanelStyle.Render("Flame: waiting for model...")
+ }
+ return flame.View().Content
+}
+
+// tabRenderOverview adapts renderOverview to the tabRenderFn signature.
+func tabRenderOverview(_ *Model, snap *statsengine.Snapshot, _ *eventstream.Model, _ *flamegraphtui.Model, width, height int) string {
+ return renderOverview(snap, width, height)
+}
+
+// tabRenderSyscalls adapts renderSyscalls to the tabRenderFn signature.
+// Sort-state rendering is handled by renderActiveContentTable before this path.
+func tabRenderSyscalls(_ *Model, snap *statsengine.Snapshot, _ *eventstream.Model, _ *flamegraphtui.Model, width, height int) string {
+ return renderSyscalls(snap, width, height)
+}
+
+// tabRenderFiles adapts renderFiles to the tabRenderFn signature, choosing
+// between the dir-grouped and plain view based on model state.
+// Sort-state rendering is handled by renderActiveContentTable before this path.
+func tabRenderFiles(m *Model, snap *statsengine.Snapshot, _ *eventstream.Model, _ *flamegraphtui.Model, width, height int) string {
+ if m.filesDirGrouped {
+ return renderFilesDirGrouped(snap, width, height, m.filesDirOffset, m.filesDirCol)
+ }
+ return renderFilesWithOffset(snap, width, height, m.filesOffset, m.filesCol)
+}
+
+// tabRenderProcesses adapts renderProcessesWithOffset to the tabRenderFn signature.
+func tabRenderProcesses(m *Model, snap *statsengine.Snapshot, _ *eventstream.Model, _ *flamegraphtui.Model, width, height int) string {
+ return renderProcessesWithOffset(snap, width, height, m.processesOffset, m.processesCol, m.pidFilter)
+}
+
+// tabRenderLatency adapts renderLatencyGapsTab to the tabRenderFn signature.
+func tabRenderLatency(_ *Model, snap *statsengine.Snapshot, _ *eventstream.Model, _ *flamegraphtui.Model, width, height int) string {
+ return renderLatencyGapsTab(snap, width, height)
+}
+
+// tabRenderStream adapts the stream model's View to the tabRenderFn signature.
+func tabRenderStream(_ *Model, _ *statsengine.Snapshot, stream *eventstream.Model, _ *flamegraphtui.Model, width, height int) string {
+ if stream == nil {
+ return common.PanelStyle.Render("Stream: waiting for source...")
+ }
+ return stream.View(width, height)
+}
+
+// tabScrollSyscalls handles navigation keys for the syscalls tab. When the
+// treemap viz is active it uses offset-based navigation; otherwise table nav.
+func tabScrollSyscalls(m *Model, msg tea.KeyPressMsg) (bool, tea.Cmd) {
+ keyStr := msg.String()
+ if m.syscallsVizMode == tabVizModeTreemap {
+ return scrollOffset(keyStr, &m.syscallsTreemapSelection, m.maxSyscallsRows()), nil
+ }
+ return common.HandleTableNavigationKey(keyStr, &m.syscallsOffset, &m.syscallsCol,
+ m.maxSyscallsRows(), len(syscallColumns(m.width)), tablePageStep(m.activeTableHeight())), nil
+}
+
+// tabScrollFiles handles navigation keys for the files tab, selecting between
+// the dir-grouped and plain navigation paths based on model state.
+func tabScrollFiles(m *Model, msg tea.KeyPressMsg) (bool, tea.Cmd) {
+ keyStr := msg.String()
+ if m.filesDirGrouped {
+ return common.HandleTableNavigationKey(keyStr, &m.filesDirOffset, &m.filesDirCol,
+ m.maxFilesDirRowsForMode(), len(fileDirColumns(m.width)), tablePageStep(m.activeTableHeight())), nil
+ }
+ return common.HandleTableNavigationKey(keyStr, &m.filesOffset, &m.filesCol,
+ m.maxFilesRows(), len(fileColumns(m.width)), tablePageStep(m.activeTableHeight())), nil
+}
+
+// tabScrollProcesses handles navigation keys for the processes tab.
+func tabScrollProcesses(m *Model, msg tea.KeyPressMsg) (bool, tea.Cmd) {
+ keyStr := msg.String()
+ return common.HandleTableNavigationKey(keyStr, &m.processesOffset, &m.processesCol,
+ m.maxProcessesRows(), len(processColumns()), tablePageStep(m.activeTableHeight())), nil
+}
+
+// tabScrollStream handles navigation, filter, and editor-open keys for the
+// stream tab. It delegates to streamModel and then emits the appropriate
+// Bubble Tea messages for any filter or editor requests.
+func tabScrollStream(m *Model, msg tea.KeyPressMsg) (bool, tea.Cmd) {
+ streamWidth, streamHeight := streamViewport(m.width, m.height)
+ m.streamModel.SetViewport(streamWidth, streamHeight)
+ handled := m.streamModel.HandleTeaKey(msg)
+ if m.streamModel.ConsumeGlobalFilterUndoRequest() {
+ return true, func() tea.Msg { return messages.GlobalFilterUndoRequestedMsg{} }
+ }
+ if filter, action, ok := m.streamModel.ConsumeGlobalFilterRequest(); ok {
+ return true, func() tea.Msg { return messages.GlobalFilterRequestedMsg{Filter: filter, Action: action} }
+ }
+ if path, ok := m.streamModel.ConsumeOpenEditorRequest(); ok {
+ return openStreamEditor(m, path)
+ }
+ return handled, nil
+}
+
+// openStreamEditor opens an external editor for the given path, recording any
+// open error into the stream model's status message so the user sees feedback.
+func openStreamEditor(m *Model, path string) (bool, tea.Cmd) {
+ editorCmd, err := eventstream.EditorCommandForPath(path)
+ if err != nil {
+ m.streamModel.SetStatusMessage("Open failed: " + err.Error())
+ return true, nil
+ }
+ return true, tea.ExecProcess(editorCmd, func(err error) tea.Msg {
+ return streamEditorDoneMsg{err: err}
+ })
+}