Skip to content

Commit 9e32a11

Browse files
committed
fix: prevent TUI crash when window width is very narrow
- Fix slice bounds panic in RenderBeanRow when MaxTitleWidth < 4 - Only add ellipsis truncation when width > 3 (room for char + ...) - Hard truncate without ellipsis when width is 1-3 - Add regression tests for narrow width edge cases
1 parent cb73e3a commit 9e32a11

File tree

2 files changed

+90
-1
lines changed

2 files changed

+90
-1
lines changed

internal/ui/styles.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,8 +485,10 @@ func RenderBeanRow(id, status, typeName, title string, cfg BeanRowConfig) string
485485
if maxWidth > 0 && prioritySymbol != "" {
486486
maxWidth -= 2 // Account for symbol + space
487487
}
488-
if maxWidth > 0 && len(title) > maxWidth {
488+
if maxWidth > 3 && len(title) > maxWidth {
489489
displayTitle = title[:maxWidth-3] + "..."
490+
} else if maxWidth > 0 && maxWidth <= 3 && len(title) > maxWidth {
491+
displayTitle = title[:maxWidth]
490492
}
491493

492494
// Cursor and title styling

internal/ui/styles_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package ui
2+
3+
import "testing"
4+
5+
func TestRenderBeanRow_NarrowWidth(t *testing.T) {
6+
// Test that RenderBeanRow doesn't panic with very small MaxTitleWidth values
7+
// This was a bug where MaxTitleWidth < 4 caused a slice bounds panic
8+
9+
tests := []struct {
10+
name string
11+
maxTitleWidth int
12+
title string
13+
}{
14+
{"zero width", 0, "Test Title"},
15+
{"width 1", 1, "Test Title"},
16+
{"width 2", 2, "Test Title"},
17+
{"width 3", 3, "Test Title"},
18+
{"width 4", 4, "Test Title"},
19+
{"width 5", 5, "Test Title"},
20+
{"short title fits", 10, "Hi"},
21+
{"exact fit", 10, "0123456789"},
22+
{"needs truncation", 10, "This is a longer title"},
23+
}
24+
25+
for _, tt := range tests {
26+
t.Run(tt.name, func(t *testing.T) {
27+
// Should not panic
28+
defer func() {
29+
if r := recover(); r != nil {
30+
t.Errorf("RenderBeanRow panicked with MaxTitleWidth=%d: %v", tt.maxTitleWidth, r)
31+
}
32+
}()
33+
34+
cfg := BeanRowConfig{
35+
MaxTitleWidth: tt.maxTitleWidth,
36+
StatusColor: "green",
37+
TypeColor: "blue",
38+
}
39+
40+
result := RenderBeanRow("abc123", "todo", "task", tt.title, cfg)
41+
if result == "" {
42+
t.Error("expected non-empty result")
43+
}
44+
})
45+
}
46+
}
47+
48+
func TestRenderBeanRow_NarrowWidthWithPriority(t *testing.T) {
49+
// Priority symbol takes 2 extra chars, which reduces available title width
50+
// This tests that the adjustment doesn't cause negative slice bounds
51+
52+
tests := []struct {
53+
name string
54+
maxTitleWidth int
55+
priority string
56+
}{
57+
{"width 1 with priority", 1, "high"},
58+
{"width 2 with priority", 2, "high"},
59+
{"width 3 with priority", 3, "critical"},
60+
{"width 4 with priority", 4, "high"},
61+
{"width 5 with priority", 5, "low"},
62+
}
63+
64+
for _, tt := range tests {
65+
t.Run(tt.name, func(t *testing.T) {
66+
defer func() {
67+
if r := recover(); r != nil {
68+
t.Errorf("RenderBeanRow panicked with MaxTitleWidth=%d and priority=%s: %v",
69+
tt.maxTitleWidth, tt.priority, r)
70+
}
71+
}()
72+
73+
cfg := BeanRowConfig{
74+
MaxTitleWidth: tt.maxTitleWidth,
75+
Priority: tt.priority,
76+
PriorityColor: "red",
77+
StatusColor: "green",
78+
TypeColor: "blue",
79+
}
80+
81+
result := RenderBeanRow("abc123", "todo", "task", "Long title that needs truncation", cfg)
82+
if result == "" {
83+
t.Error("expected non-empty result")
84+
}
85+
})
86+
}
87+
}

0 commit comments

Comments
 (0)