summaryrefslogtreecommitdiff
path: root/internal/tui/common/table.go
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/table.go
parent9f21f1004beeac10be9223bc8c5514261e397b6e (diff)
tui: unify table navigation and rendering
Diffstat (limited to 'internal/tui/common/table.go')
-rw-r--r--internal/tui/common/table.go157
1 files changed, 157 insertions, 0 deletions
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
+}