diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-08 22:35:34 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-08 22:35:34 +0200 |
| commit | d8bf918e83515f48564e0d0b98d30907944a1e0d (patch) | |
| tree | ed155b0c365b0d322494a96ff799c3a59a1d9ec8 /internal/tui/common | |
| parent | 9f21f1004beeac10be9223bc8c5514261e397b6e (diff) | |
tui: unify table navigation and rendering
Diffstat (limited to 'internal/tui/common')
| -rw-r--r-- | internal/tui/common/keys.go | 26 | ||||
| -rw-r--r-- | internal/tui/common/styles.go | 21 | ||||
| -rw-r--r-- | internal/tui/common/table.go | 157 |
3 files changed, 192 insertions, 12 deletions
diff --git a/internal/tui/common/keys.go b/internal/tui/common/keys.go index f2e1271..6289997 100644 --- a/internal/tui/common/keys.go +++ b/internal/tui/common/keys.go @@ -110,13 +110,14 @@ func (k KeyMap) DashboardStatusHelpSections() []HelpSection { k.Visualize, k.Metric, helpTextBinding("space", "stream pause"), - helpTextBinding("enter", "stream push filter"), + helpTextBinding("enter", "selected filter"), helpTextBinding("esc", "stream undo filter"), - helpTextBinding("g/G", "stream top/tail"), - helpTextBinding("left/right", "stream col"), - helpTextBinding("h/l", "stream col"), - helpTextBinding("j/k", "scroll"), - helpTextBinding("up/down", "scroll"), + helpTextBinding("g/G", "table top/bottom"), + helpTextBinding("left/right", "table col"), + helpTextBinding("h/l", "table col"), + helpTextBinding("j/k", "table row"), + helpTextBinding("up/down", "table row"), + helpTextBinding("pgup/down", "table page"), helpTextBinding("/,?", "stream regex search"), helpTextBinding("n/N", "stream search next/prev"), helpTextBinding("x", "stream export"), @@ -144,13 +145,14 @@ func (k KeyMap) DashboardFullHelp() [][]key.Binding { controls, { helpTextBinding("space", "stream pause"), - helpTextBinding("enter", "stream push filter"), + helpTextBinding("enter", "selected filter"), helpTextBinding("esc", "stream undo filter"), - helpTextBinding("g/G", "stream top/tail"), - helpTextBinding("left/right", "stream col"), - helpTextBinding("h/l", "stream col"), - helpTextBinding("j/k", "scroll"), - helpTextBinding("up/down", "scroll"), + helpTextBinding("g/G", "table top/bottom"), + helpTextBinding("left/right", "table col"), + helpTextBinding("h/l", "table col"), + helpTextBinding("j/k", "table row"), + helpTextBinding("up/down", "table row"), + helpTextBinding("pgup/down", "table page"), helpTextBinding("x", "stream export"), helpTextBinding("X", "stream export as"), helpTextBinding("E", "stream open last"), diff --git a/internal/tui/common/styles.go b/internal/tui/common/styles.go index a71ef81..111927e 100644 --- a/internal/tui/common/styles.go +++ b/internal/tui/common/styles.go @@ -77,6 +77,15 @@ var ( // ErrorStyle is used for fatal or warning messages. ErrorStyle lipgloss.Style + + // TableHeaderStyle is used by shared table headers. + TableHeaderStyle lipgloss.Style + + // TableSelectedRowStyle highlights the selected row in shared tables. + TableSelectedRowStyle lipgloss.Style + + // TableSelectedCellStyle highlights the selected cell in shared tables. + TableSelectedCellStyle lipgloss.Style ) // ApplyPalette updates shared colors and styles to match the provided theme. @@ -110,6 +119,18 @@ func ApplyPalette(isDark bool) { BorderForeground(ColorPanel) HighlightStyle = lipgloss.NewStyle().Bold(true).Foreground(ColorAccent) ErrorStyle = lipgloss.NewStyle().Bold(true).Foreground(ColorDanger) + TableHeaderStyle = lipgloss.NewStyle(). + Foreground(ColorMuted). + BorderTop(true). + BorderForeground(ColorPanel) + TableSelectedRowStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(ColorBackground). + Background(ColorPrimary) + TableSelectedCellStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(ColorBackground). + Background(ColorAccent) } func init() { diff --git a/internal/tui/common/table.go b/internal/tui/common/table.go new file mode 100644 index 0000000..f4c94e8 --- /dev/null +++ b/internal/tui/common/table.go @@ -0,0 +1,157 @@ +package common + +import ( + "strings" + "unicode/utf8" + + "charm.land/lipgloss/v2" +) + +// TableColumn defines one shared TUI table column. +type TableColumn struct { + Title string + Width int +} + +// RenderTableHeader renders a shared table header row. +func RenderTableHeader(columns []TableColumn) string { + cells := make([]string, 0, len(columns)) + for _, col := range columns { + cells = append(cells, renderTableCell(col.Title, col.Width)) + } + return TableHeaderStyle.Render(strings.Join(cells, " ")) +} + +// RenderTableRow renders one shared TUI table row. +func RenderTableRow(columns []TableColumn, cells []string, selected bool, selectedCol int, baseStyle lipgloss.Style) string { + rendered := make([]string, 0, len(columns)) + for idx, col := range columns { + value := "" + if idx < len(cells) { + value = cells[idx] + } + cell := renderTableCell(value, col.Width) + switch { + case selected && idx == selectedCol: + cell = TableSelectedCellStyle.Render(cell) + case selected: + cell = TableSelectedRowStyle.Render(cell) + } + rendered = append(rendered, cell) + } + row := strings.Join(rendered, " ") + if selected { + return row + } + return baseStyle.Render(row) +} + +// HandleTableNavigationKey applies shared row/column movement for table views. +func HandleTableNavigationKey(keyStr string, row, col *int, rowCount, colCount, pageStep int) bool { + switch keyStr { + case "down", "j": + *row = clampIndex(*row+1, rowCount) + return true + case "up", "k": + *row = clampIndex(*row-1, rowCount) + return true + case "left", "h": + *col = clampIndex(*col-1, colCount) + return true + case "right", "l": + *col = clampIndex(*col+1, colCount) + return true + case "g": + *row = clampIndex(0, rowCount) + return true + case "G": + *row = clampIndex(rowCount-1, rowCount) + return true + case "pgup", "pageup": + if pageStep < 1 { + pageStep = 1 + } + *row = clampIndex(*row-pageStep, rowCount) + return true + case "pgdown", "pgdn", "pagedown": + if pageStep < 1 { + pageStep = 1 + } + *row = clampIndex(*row+pageStep, rowCount) + return true + default: + return false + } +} + +// VisibleTableWindow returns a centered visible row range for the selection. +func VisibleTableWindow(selectedRow, rowCount, visibleRows int) (int, int) { + if rowCount <= 0 { + return 0, 0 + } + if visibleRows <= 0 || visibleRows >= rowCount { + return 0, rowCount + } + selectedRow = clampIndex(selectedRow, rowCount) + start := selectedRow - visibleRows/2 + if start < 0 { + start = 0 + } + maxStart := rowCount - visibleRows + if start > maxStart { + start = maxStart + } + return start, start + visibleRows +} + +// ClampTableCol clamps a selected column index for the current table shape. +func ClampTableCol(col, colCount int) int { + return clampIndex(col, colCount) +} + +func renderTableCell(value string, width int) string { + if width <= 0 { + return "" + } + value = sanitizeTableCell(value) + value = truncateTableCell(value, width) + cellWidth := utf8.RuneCountInString(value) + if cellWidth >= width { + return value + } + return value + strings.Repeat(" ", width-cellWidth) +} + +func sanitizeTableCell(value string) string { + value = strings.ReplaceAll(value, "\n", " ") + value = strings.ReplaceAll(value, "\r", " ") + value = strings.ReplaceAll(value, "\t", " ") + return value +} + +func truncateTableCell(value string, width int) string { + if width <= 0 { + return "" + } + if utf8.RuneCountInString(value) <= width { + return value + } + if width <= 3 { + return string([]rune(value)[:width]) + } + runes := []rune(value) + return string(runes[:width-3]) + "..." +} + +func clampIndex(value, count int) int { + if count <= 0 { + return 0 + } + if value < 0 { + return 0 + } + if value >= count { + return count - 1 + } + return value +} |
