summaryrefslogtreecommitdiff
path: root/internal/tui/common
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-08 22:35:34 +0200
committerPaul Buetow <paul@buetow.org>2026-03-08 22:35:34 +0200
commitd8bf918e83515f48564e0d0b98d30907944a1e0d (patch)
treeed155b0c365b0d322494a96ff799c3a59a1d9ec8 /internal/tui/common
parent9f21f1004beeac10be9223bc8c5514261e397b6e (diff)
tui: unify table navigation and rendering
Diffstat (limited to 'internal/tui/common')
-rw-r--r--internal/tui/common/keys.go26
-rw-r--r--internal/tui/common/styles.go21
-rw-r--r--internal/tui/common/table.go157
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
+}