detached b40aa09

feat(ui): export TruncateText helper

Wrap the existing truncateText logic used by toast rendering so later
tab-title work can reuse the same cell-width clipping.
commit
b40aa09e134e11bbb40c1fcdff3b09d3709a3648
author
brice · · 2026-07-11 07:55 UTC
parent
d0ab56b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +27 -0
M internal/usecase/ui/toast.go +3 -0
@@ -223,6 +223,9 @@ func drawToast(frame renderer.Frame, toast Toast, styles ToastStyles) {
223223 DrawText(frame, textLeft, textY, textRight, truncateText(toast.Message, textRight-textLeft), styles.Text)
224224 }
225225
226+// TruncateText clips text to maxWidth cells, appending an ellipsis when cut.
227+func TruncateText(text string, maxWidth int) string { return truncateText(text, maxWidth) }
228+
226229 func truncateText(text string, maxWidth int) string {
227230 if maxWidth <= 0 {
228231 return ""
@@ -223,6 +223,9 @@ func drawToast(frame renderer.Frame, toast Toast, styles ToastStyles) {
223 DrawText(frame, textLeft, textY, textRight, truncateText(toast.Message, textRight-textLeft), styles.Text)223 DrawText(frame, textLeft, textY, textRight, truncateText(toast.Message, textRight-textLeft), styles.Text)
224 }224 }
225 225
226+// TruncateText clips text to maxWidth cells, appending an ellipsis when cut.
227+func TruncateText(text string, maxWidth int) string { return truncateText(text, maxWidth) }
228+
226 func truncateText(text string, maxWidth int) string {229 func truncateText(text string, maxWidth int) string {
227 if maxWidth <= 0 {230 if maxWidth <= 0 {
228 return ""231 return ""
M internal/usecase/ui/toast_test.go +24 -0
@@ -81,6 +81,30 @@ func TestToastManagerDurationDismissAndClear(t *testing.T) {
8181 })
8282 }
8383
84+func TestTruncateText(t *testing.T) {
85+ tests := []struct {
86+ name string
87+ text string
88+ maxWidth int
89+ want string
90+ }{
91+ {"no truncation needed", "hello", 10, "hello"},
92+ {"truncation with ellipsis", "hello world", 8, "hello w…"},
93+ {"maxWidth zero", "hello", 0, ""},
94+ // The ellipsis itself is one cell wide, so any non-positive maxWidth is
95+ // also "smaller than the ellipsis" and collapses to the same empty result.
96+ {"maxWidth smaller than ellipsis", "hello world", -1, ""},
97+ {"wide runes", "日本語のテキスト", 5, "日本…"},
98+ }
99+ for _, tt := range tests {
100+ t.Run(tt.name, func(t *testing.T) {
101+ if got := TruncateText(tt.text, tt.maxWidth); got != tt.want {
102+ t.Fatalf("TruncateText(%q, %d) = %q, want %q", tt.text, tt.maxWidth, got, tt.want)
103+ }
104+ })
105+ }
106+}
107+
84108 func TestToastBoundsAnchorsMarginsAndTinyFrames(t *testing.T) {
85109 message := "Reconnection attempts…"
86110 width := textWidth(message) + 4
@@ -81,6 +81,30 @@ func TestToastManagerDurationDismissAndClear(t *testing.T) {
81 })81 })
82 }82 }
83 83
84+func TestTruncateText(t *testing.T) {
85+ tests := []struct {
86+ name string
87+ text string
88+ maxWidth int
89+ want string
90+ }{
91+ {"no truncation needed", "hello", 10, "hello"},
92+ {"truncation with ellipsis", "hello world", 8, "hello w…"},
93+ {"maxWidth zero", "hello", 0, ""},
94+ // The ellipsis itself is one cell wide, so any non-positive maxWidth is
95+ // also "smaller than the ellipsis" and collapses to the same empty result.
96+ {"maxWidth smaller than ellipsis", "hello world", -1, ""},
97+ {"wide runes", "日本語のテキスト", 5, "日本…"},
98+ }
99+ for _, tt := range tests {
100+ t.Run(tt.name, func(t *testing.T) {
101+ if got := TruncateText(tt.text, tt.maxWidth); got != tt.want {
102+ t.Fatalf("TruncateText(%q, %d) = %q, want %q", tt.text, tt.maxWidth, got, tt.want)
103+ }
104+ })
105+ }
106+}
107+
84 func TestToastBoundsAnchorsMarginsAndTinyFrames(t *testing.T) {108 func TestToastBoundsAnchorsMarginsAndTinyFrames(t *testing.T) {
85 message := "Reconnection attempts…"109 message := "Reconnection attempts…"
86 width := textWidth(message) + 4110 width := textWidth(message) + 4

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close