Skip to content

Commit 207e43e

Browse files
committed
refactor: unify CLI and TUI bean row rendering
- Refactor renderNode to use shared RenderBeanRow function - Remove duplicate column rendering logic from tree.go - Remove unused truncateString and runeWidth functions - Remove TAGS header column from beans list output - Both CLI (beans list) and TUI now share the same row rendering code
1 parent 0c5357f commit 207e43e

File tree

1 file changed

+31
-160
lines changed

1 file changed

+31
-160
lines changed

internal/ui/tree.go

Lines changed: 31 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -184,34 +184,14 @@ func RenderTree(nodes []*TreeNode, cfg *config.Config, maxIDWidth int, hasTags b
184184
treeColWidth = maxIDWidth + maxDepth*treeIndent
185185
}
186186

187-
// Column styles with widths for alignment
188-
idStyle := lipgloss.NewStyle().Width(treeColWidth)
189-
typeStyle := lipgloss.NewStyle().Width(12)
190-
statusStyle := lipgloss.NewStyle().Width(14)
191-
titleStyle := lipgloss.NewStyle().Width(50)
187+
// Header with manual padding (lipgloss Width doesn't handle styled strings well)
192188
headerCol := lipgloss.NewStyle().Foreground(ColorMuted)
189+
idHeader := headerCol.Render("ID") + strings.Repeat(" ", treeColWidth-2)
190+
typeHeader := headerCol.Render("TYPE") + strings.Repeat(" ", 12-4)
191+
statusHeader := headerCol.Render("STATUS") + strings.Repeat(" ", 14-6)
193192

194-
// Header
195-
var header string
196-
var dividerWidth int
197-
if hasTags {
198-
header = lipgloss.JoinHorizontal(lipgloss.Top,
199-
idStyle.Render(headerCol.Render("ID")),
200-
typeStyle.Render(headerCol.Render("TYPE")),
201-
statusStyle.Render(headerCol.Render("STATUS")),
202-
titleStyle.Render(headerCol.Render("TITLE")),
203-
headerCol.Render("TAGS"),
204-
)
205-
dividerWidth = treeColWidth + 12 + 14 + 50 + 24
206-
} else {
207-
header = lipgloss.JoinHorizontal(lipgloss.Top,
208-
idStyle.Render(headerCol.Render("ID")),
209-
typeStyle.Render(headerCol.Render("TYPE")),
210-
statusStyle.Render(headerCol.Render("STATUS")),
211-
headerCol.Render("TITLE"),
212-
)
213-
dividerWidth = treeColWidth + 12 + 14 + 50
214-
}
193+
header := idHeader + typeHeader + statusHeader + headerCol.Render("TITLE")
194+
dividerWidth := treeColWidth + 12 + 14 + 50
215195
sb.WriteString(header)
216196
sb.WriteString("\n")
217197
sb.WriteString(Muted.Render(strings.Repeat("─", dividerWidth)))
@@ -248,156 +228,47 @@ func renderNodes(sb *strings.Builder, nodes []*TreeNode, depth int, ancestry []b
248228
func renderNode(sb *strings.Builder, node *TreeNode, depth int, isLast bool, ancestry []bool, cfg *config.Config, treeColWidth int, hasTags bool) {
249229
b := node.Bean
250230

251-
// Get status color from config
252-
statusCfg := cfg.GetStatus(b.Status)
253-
statusColor := "gray"
254-
if statusCfg != nil {
255-
statusColor = statusCfg.Color
256-
}
257-
isArchive := cfg.IsArchiveStatus(b.Status)
258-
259-
// Get type color from config
260-
typeColor := ""
261-
if typeCfg := cfg.GetType(b.Type); typeCfg != nil {
262-
typeColor = typeCfg.Color
263-
}
264-
265-
// Get priority color and render symbol
266-
priorityColor := ""
267-
if priorityCfg := cfg.GetPriority(b.Priority); priorityCfg != nil {
268-
priorityColor = priorityCfg.Color
269-
}
270-
prioritySymbol := RenderPrioritySymbol(b.Priority, priorityColor)
271-
if prioritySymbol != "" {
272-
prioritySymbol += " "
273-
}
274-
275-
// Column styles - fixed widths for alignment
276-
typeStyle := lipgloss.NewStyle().Width(12)
277-
statusStyle := lipgloss.NewStyle().Width(14)
278-
titleStyle := lipgloss.NewStyle().Width(50)
279-
280-
// Build indentation and connector
281-
// depth 0: no indent, no connector
282-
// depth 1: connector only (├─ or └─)
283-
// depth 2+: indent + connector
284-
var indent string
285-
var connector string
231+
// Build tree prefix from ancestry
232+
var prefix string
286233
if depth > 0 {
287-
// Build indent from ancestry - each level adds either │ or space
288234
for _, wasLast := range ancestry {
289235
if wasLast {
290-
indent += treeSpace // parent was last child, no continuation line
236+
prefix += treeSpace
291237
} else {
292-
indent += treePipe // parent has more siblings, show continuation line
238+
prefix += treePipe
293239
}
294240
}
295241
if isLast {
296-
connector = treeLastBranch
242+
prefix += treeLastBranch
297243
} else {
298-
connector = treeBranch
244+
prefix += treeBranch
299245
}
300246
}
301247

302-
// Build the ID cell content: indent + connector + ID
303-
var idText string
304-
if node.Matched {
305-
idText = ID.Render(b.ID)
306-
} else {
307-
// Dim unmatched (ancestor) beans
308-
idText = Muted.Render(b.ID)
309-
}
310-
311-
// Style the tree connector with subtle color
312-
styledConnector := TreeLine.Render(indent + connector)
313-
314-
// Calculate visual width of indent + connector + ID (without ANSI codes)
315-
visualWidth := len(indent) + runeWidth(connector) + len(b.ID)
316-
// Pad to fixed width
317-
padding := ""
318-
if treeColWidth > visualWidth {
319-
padding = strings.Repeat(" ", treeColWidth-visualWidth)
320-
}
321-
idCell := styledConnector + idText + padding
322-
323-
typeText := ""
324-
if b.Type != "" {
325-
if node.Matched {
326-
typeText = RenderTypeText(b.Type, typeColor)
327-
} else {
328-
typeText = Muted.Render(b.Type)
329-
}
330-
}
331-
332-
var statusText string
333-
if node.Matched {
334-
statusText = RenderStatusTextWithColor(b.Status, statusColor, isArchive)
335-
} else {
336-
statusText = Muted.Render(b.Status)
337-
}
338-
339-
var titleText string
340-
// Account for priority symbol width when truncating (symbol + space = 2 chars)
341-
maxTitleWidth := 50
342-
if prioritySymbol != "" {
343-
maxTitleWidth -= 2
344-
}
345-
title := truncateString(b.Title, maxTitleWidth)
346-
if node.Matched {
347-
titleText = prioritySymbol + title
348-
} else {
349-
titleText = Muted.Render(title)
350-
}
351-
352-
var row string
353-
if hasTags {
354-
var tagsStr string
355-
if node.Matched {
356-
tagsStr = RenderTagsCompact(b.Tags, 1)
357-
} else {
358-
// For unmatched, just show muted tags
359-
if len(b.Tags) > 0 {
360-
tagsStr = Muted.Render(b.Tags[0])
361-
if len(b.Tags) > 1 {
362-
tagsStr += Muted.Render(" +" + string(rune('0'+len(b.Tags)-1)))
363-
}
364-
}
365-
}
366-
367-
row = lipgloss.JoinHorizontal(lipgloss.Top,
368-
idCell,
369-
typeStyle.Render(typeText),
370-
statusStyle.Render(statusText),
371-
titleStyle.Render(titleText),
372-
tagsStr,
373-
)
374-
} else {
375-
row = lipgloss.JoinHorizontal(lipgloss.Top,
376-
idCell,
377-
typeStyle.Render(typeText),
378-
statusStyle.Render(statusText),
379-
titleText,
380-
)
381-
}
248+
// Get colors from config
249+
colors := cfg.GetBeanColors(b.Status, b.Type, b.Priority)
250+
251+
// Use shared RenderBeanRow function
252+
row := RenderBeanRow(b.ID, b.Status, b.Type, b.Title, BeanRowConfig{
253+
StatusColor: colors.StatusColor,
254+
TypeColor: colors.TypeColor,
255+
PriorityColor: colors.PriorityColor,
256+
Priority: b.Priority,
257+
IsArchive: colors.IsArchive,
258+
MaxTitleWidth: 50,
259+
ShowCursor: false,
260+
Tags: b.Tags,
261+
ShowTags: hasTags,
262+
MaxTags: 1,
263+
TreePrefix: prefix,
264+
Dimmed: !node.Matched,
265+
IDColWidth: treeColWidth,
266+
})
382267

383268
sb.WriteString(row)
384269
sb.WriteString("\n")
385270
}
386271

387-
// truncateString truncates a string to maxLen, adding "..." if truncated.
388-
func truncateString(s string, maxLen int) string {
389-
if len(s) <= maxLen {
390-
return s
391-
}
392-
return s[:maxLen-3] + "..."
393-
}
394-
395-
// runeWidth returns the visual width of a string (counting runes, not bytes).
396-
// This assumes all runes are single-width (which works for our tree connectors).
397-
func runeWidth(s string) int {
398-
return len([]rune(s))
399-
}
400-
401272
// FlatItem represents a flattened tree node with rendering context.
402273
// Used by TUI to render tree structure in a flat list.
403274
type FlatItem struct {

0 commit comments

Comments
 (0)