v3.0.0 f19f453

table.go RawHistoryFolder

1package components
2
3import (
4	"strings"
5
6	"github.com/bnema/gordon/internal/adapters/in/cli/ui/styles"
7
8	"github.com/charmbracelet/lipgloss"
9	"github.com/charmbracelet/lipgloss/table"
10	"github.com/mattn/go-runewidth"
11	"github.com/rivo/uniseg"
12)
13
14// TableColumn defines a table column.
15type TableColumn struct {
16	Title string
17	Width int
18}
19
20// TableModel is a styled table component.
21type TableModel struct {
22	columns     []TableColumn
23	rows        [][]string
24	border      lipgloss.Border
25	borderStyle lipgloss.Style
26	headerStyle lipgloss.Style
27	cellStyle   lipgloss.Style
28	oddStyle    lipgloss.Style
29	evenStyle   lipgloss.Style
30	width       int
31}
32
33// TableOption configures a TableModel.
34type TableOption func(*TableModel)
35
36// NewTable creates a new styled table.
37func NewTable(opts ...TableOption) *TableModel {
38	t := &TableModel{
39		border:      lipgloss.RoundedBorder(),
40		borderStyle: lipgloss.NewStyle().Foreground(styles.ColorBorder),
41		headerStyle: lipgloss.NewStyle().
42			Bold(true).
43			Foreground(styles.ColorPrimary).
44			Padding(0, 1),
45		cellStyle: lipgloss.NewStyle().
46			Foreground(styles.ColorText).
47			Padding(0, 1),
48		oddStyle: lipgloss.NewStyle().
49			Foreground(styles.ColorText).
50			Padding(0, 1),
51		evenStyle: lipgloss.NewStyle().
52			Foreground(styles.ColorText).
53			Padding(0, 1),
54	}
55
56	for _, opt := range opts {
57		opt(t)
58	}
59
60	return t
61}
62
63// WithColumns sets the table columns.
64func WithColumns(cols []TableColumn) TableOption {
65	return func(t *TableModel) {
66		t.columns = cols
67	}
68}
69
70// WithRows sets the table rows.
71func WithRows(rows [][]string) TableOption {
72	return func(t *TableModel) {
73		t.rows = rows
74	}
75}
76
77// WithBorder sets the table border style.
78func WithBorder(b lipgloss.Border) TableOption {
79	return func(t *TableModel) {
80		t.border = b
81	}
82}
83
84// WithTableWidth sets the table width.
85func WithTableWidth(w int) TableOption {
86	return func(t *TableModel) {
87		t.width = w
88	}
89}
90
91// WithHeaderStyle sets the header style.
92func WithHeaderStyle(s lipgloss.Style) TableOption {
93	return func(t *TableModel) {
94		t.headerStyle = s
95	}
96}
97
98// WithCellStyle sets the cell style.
99func WithCellStyle(s lipgloss.Style) TableOption {
100	return func(t *TableModel) {
101		t.cellStyle = s
102		t.oddStyle = s
103		t.evenStyle = s
104	}
105}
106
107// WithAlternateRows enables alternating row colors.
108func WithAlternateRows(odd, even lipgloss.Style) TableOption {
109	return func(t *TableModel) {
110		t.oddStyle = odd
111		t.evenStyle = even
112	}
113}
114
115// SetColumns sets the table columns.
116func (t *TableModel) SetColumns(cols []TableColumn) {
117	t.columns = cols
118}
119
120// SetRows sets the table rows.
121func (t *TableModel) SetRows(rows [][]string) {
122	t.rows = rows
123}
124
125// AddRow adds a row to the table.
126func (t *TableModel) AddRow(row []string) {
127	t.rows = append(t.rows, row)
128}
129
130// ClearRows removes all rows from the table.
131func (t *TableModel) ClearRows() {
132	t.rows = nil
133}
134
135// Render renders the table as a string.
136func (t *TableModel) Render() string {
137	if len(t.columns) == 0 {
138		return ""
139	}
140
141	// Extract headers
142	headers := make([]string, len(t.columns))
143	for i, col := range t.columns {
144		headers[i] = truncateCell(col.Title, col.Width)
145	}
146
147	rows := make([][]string, len(t.rows))
148	for rowIdx, row := range t.rows {
149		rows[rowIdx] = make([]string, len(row))
150		for colIdx, cell := range row {
151			width := 0
152			if colIdx < len(t.columns) {
153				width = t.columns[colIdx].Width
154			}
155			rows[rowIdx][colIdx] = truncateCell(cell, width)
156		}
157	}
158
159	// Create table
160	tbl := table.New().
161		Border(t.border).
162		BorderStyle(t.borderStyle).
163		Headers(headers...).
164		Rows(rows...).
165		StyleFunc(func(row, col int) lipgloss.Style {
166			width := 0
167			if col >= 0 && col < len(t.columns) {
168				width = t.columns[col].Width
169			}
170
171			applyWidth := func(s lipgloss.Style) lipgloss.Style {
172				if width > 0 {
173					return s.Width(width).MaxWidth(width)
174				}
175				return s
176			}
177
178			if row == table.HeaderRow {
179				return applyWidth(t.headerStyle)
180			}
181			if row%2 == 0 {
182				return applyWidth(t.evenStyle)
183			}
184			return applyWidth(t.oddStyle)
185		})
186
187	// Apply width if set
188	if t.width > 0 {
189		tbl = tbl.Width(t.width)
190	}
191
192	return tbl.String()
193}
194
195func truncateCell(value string, maxWidth int) string {
196	if strings.Contains(value, "\x1b[") {
197		return value
198	}
199
200	if maxWidth <= 0 || runewidth.StringWidth(value) <= maxWidth {
201		return value
202	}
203
204	if maxWidth <= 3 {
205		return strings.Repeat(".", maxWidth)
206	}
207
208	targetWidth := maxWidth - 3
209	b := strings.Builder{}
210	currentWidth := 0
211	g := uniseg.NewGraphemes(value)
212	for g.Next() {
213		grapheme := g.Str()
214		graphemeWidth := runewidth.StringWidth(grapheme)
215		if currentWidth+graphemeWidth > targetWidth {
216			break
217		}
218		b.WriteString(grapheme)
219		currentWidth += graphemeWidth
220	}
221
222	if b.Len() == 0 {
223		return strings.Repeat(".", maxWidth)
224	}
225
226	return b.String() + "..."
227}
228
229// View returns the table view (alias for Render).
230func (t *TableModel) View() string {
231	return t.Render()
232}
233
234// SimpleTable creates a simple table with headers and rows.
235// This is a convenience function for quick table rendering.
236func SimpleTable(headers []string, rows [][]string) string {
237	cols := make([]TableColumn, len(headers))
238	for i, h := range headers {
239		cols[i] = TableColumn{Title: h}
240	}
241
242	t := NewTable(
243		WithColumns(cols),
244		WithRows(rows),
245	)
246
247	return t.Render()
248}
249
250// RouteTable creates a table specifically for displaying routes.
251func RouteTable(routes [][]string) string {
252	return SimpleTable(
253		[]string{"Domain", "Image", "Status"},
254		routes,
255	)
256}
257
258// TokenTable creates a table specifically for displaying tokens.
259func TokenTable(tokens [][]string) string {
260	return SimpleTable(
261		[]string{"ID", "Subject", "Scopes", "Expires", "Revoked"},
262		tokens,
263	)
264}
265
266// SecretTable creates a table specifically for displaying secrets.
267func SecretTable(secrets [][]string) string {
268	return SimpleTable(
269		[]string{"Key", "Value"},
270		secrets,
271	)
272}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close