1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
|
package main
import (
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
)
// TableColumn defines a single column in a table.
// RenderCell receives the content width and returns styled text of exactly that width.
// The Table framework adds trailing space between columns and enforces width.
type TableColumn struct {
Name string // Header text (e.g., "Commit", "Author")
Width int // Content width (table adds trailing space between columns, last column gets remaining space)
Align lipgloss.Position // Text alignment (Left, Right, Center)
RenderCell func(data any, width int) string // Receives content width, returns styled text of exactly that width
}
// Table manages rendering of tabular data with fixed-width columns.
// All columns have fixed width except the last one, which gets remaining window width.
type Table struct {
columns []TableColumn
windowWidth int
borderStyle lipgloss.Style
}
// NewTable creates a new table with the given columns and window width.
// The borderStyle is used to calculate frame size for content width calculation.
func NewTable(columns []TableColumn, windowWidth int, borderStyle lipgloss.Style) *Table {
return &Table{
columns: columns,
windowWidth: windowWidth,
borderStyle: borderStyle,
}
}
// calculateLastColumnWidth computes the remaining width for the last column.
// Formula: windowWidth - sum(otherColumnWidths) - interColumnSpaces - borderFrameSize
// Enforces minimum width of 20 characters for safety.
func (t *Table) calculateLastColumnWidth() int {
if len(t.columns) == 0 {
return 20
}
// Sum up all column widths except the last
totalFixedWidth := 0
for i := 0; i < len(t.columns)-1; i++ {
totalFixedWidth += t.columns[i].Width
}
// Calculate remaining width, accounting for inter-column spaces
// We add 1 space between each column (numColumns - 1 spaces total)
numInterColumnSpaces := len(t.columns) - 1
borderFrameSize := t.borderStyle.GetHorizontalFrameSize()
remainingWidth := max(
// Enforce minimum width
t.windowWidth-totalFixedWidth-numInterColumnSpaces-borderFrameSize, 20)
return remainingWidth
}
// RenderHeader renders the table header row with column names.
// Applies bold styling and adds trailing space between columns.
func (t *Table) RenderHeader() string {
var cells []string
lastColWidth := t.calculateLastColumnWidth()
for i, col := range t.columns {
// Determine actual width for this column
width := col.Width
if i == len(t.columns)-1 {
width = lastColWidth
}
// Add trailing space to column name (except last column)
content := col.Name
renderWidth := width
if i < len(t.columns)-1 {
content = content + " "
renderWidth = width + 1 // Account for trailing space
}
// Create header style with bold and alignment
headerStyle := lipgloss.NewStyle().Bold(true)
if col.Align != 0 {
headerStyle = headerStyle.Align(col.Align)
}
// Render cell with exact width
cell := renderCell(content, renderWidth, headerStyle)
cells = append(cells, cell)
}
return lipgloss.JoinHorizontal(lipgloss.Top, cells...)
}
// RenderRow renders a single data row by calling each column's RenderCell function.
// Adds trailing space between columns and enforces exact width.
func (t *Table) RenderRow(data any) string {
var cells []string
lastColWidth := t.calculateLastColumnWidth()
for i, col := range t.columns {
// Determine actual width for this column
width := col.Width
if i == len(t.columns)-1 {
width = lastColWidth
}
// Call column's render function with width - returns content of exact width
content := col.RenderCell(data, width)
// Add trailing space to content (except last column)
renderWidth := width
if i < len(t.columns)-1 {
content = content + " "
renderWidth = width + 1 // Account for trailing space
}
// Create cell style with alignment
cellStyle := lipgloss.NewStyle()
if col.Align != 0 {
cellStyle = cellStyle.Align(col.Align)
}
// Render cell with exact width
cell := renderCell(content, renderWidth, cellStyle)
cells = append(cells, cell)
}
return lipgloss.JoinHorizontal(lipgloss.Top, cells...)
}
// RenderRowWithBackground renders a row with a background color applied to all cells.
// Used for selection highlighting.
func (t *Table) RenderRowWithBackground(data any, bgColor lipgloss.Color) string {
var cells []string
lastColWidth := t.calculateLastColumnWidth()
for i, col := range t.columns {
// Determine actual width for this column
width := col.Width
if i == len(t.columns)-1 {
width = lastColWidth
}
// Call column's render function with width - returns content of exact width
content := col.RenderCell(data, width)
// Add trailing space with background to content (except last column)
renderWidth := width
if i < len(t.columns)-1 {
styledSpace := lipgloss.NewStyle().Background(bgColor).Render(" ")
content = content + styledSpace
renderWidth = width + 1 // Account for trailing space
}
// Create cell style with alignment and background
cellStyle := lipgloss.NewStyle().
Background(bgColor)
if col.Align != 0 {
cellStyle = cellStyle.Align(col.Align)
}
// Render cell with exact width
cell := renderCell(content, renderWidth, cellStyle)
cells = append(cells, cell)
}
return lipgloss.JoinHorizontal(lipgloss.Top, cells...)
}
// GetTotalWidth returns the total width of all columns including the last column and inter-column spaces.
// Used for validation and debugging.
func (t *Table) GetTotalWidth() int {
if len(t.columns) == 0 {
return 0
}
totalWidth := 0
for i := 0; i < len(t.columns)-1; i++ {
totalWidth += t.columns[i].Width
}
totalWidth += t.calculateLastColumnWidth()
// Add inter-column spaces
numInterColumnSpaces := len(t.columns) - 1
totalWidth += numInterColumnSpaces
return totalWidth
}
// truncateContent is a helper to truncate content to fit within a column.
// Preserves ANSI color codes and adds ellipsis if truncated.
func truncateContent(content string, maxWidth int) string {
if ansi.StringWidth(content) <= maxWidth {
return content
}
return ansi.Truncate(content, maxWidth, "…")
}
|