@@ -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
248228func 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.
403274type FlatItem struct {
0 commit comments