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}