detached 467a998

refactor(vt): track OSC 9;4 state without callbacks; document status bar review nits

commit
467a998f90a75b5cf4d59477ff5159afa07174d9
author
brice · · 2026-07-05 08:03 UTC
parent
652325c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +21 -4
M internal/usecase/daemon/status.go +4 -1
@@ -124,7 +124,8 @@ type mruSession struct {
124124 name string
125125 ephemeral bool
126126 attention bool
127- mruAt uint64
127+ // mruAt orders entries in barStateFor (freshest first); drawing ignores it.
128+ mruAt uint64
128129 }
129130
130131 type statusSnapshot struct {
@@ -197,6 +198,8 @@ func (d *Daemon) barStateFor(cur *session, copyFeedback string) barState {
197198 }
198199
199200 func fitMRU(entries []mruSession, rowLen, leftUsed int, feedback string) []mruSession {
201+ // With no feedback, keep one blank trailing cell; with feedback, reserve
202+ // its " text" width plus a one-cell gap so drawRightPlainText always fits.
200203 copyReserve := 1
201204 if feedback != "" {
202205 copyReserve = len([]rune(feedback)) + 2
@@ -124,7 +124,8 @@ type mruSession struct {
124 name string124 name string
125 ephemeral bool125 ephemeral bool
126 attention bool126 attention bool
127- mruAt uint64127+ // mruAt orders entries in barStateFor (freshest first); drawing ignores it.
128+ mruAt uint64
128 }129 }
129 130
130 type statusSnapshot struct {131 type statusSnapshot struct {
@@ -197,6 +198,8 @@ func (d *Daemon) barStateFor(cur *session, copyFeedback string) barState {
197 }198 }
198 199
199 func fitMRU(entries []mruSession, rowLen, leftUsed int, feedback string) []mruSession {200 func fitMRU(entries []mruSession, rowLen, leftUsed int, feedback string) []mruSession {
201+ // With no feedback, keep one blank trailing cell; with feedback, reserve
202+ // its " text" width plus a one-cell gap so drawRightPlainText always fits.
200 copyReserve := 1203 copyReserve := 1
201 if feedback != "" {204 if feedback != "" {
202 copyReserve = len([]rune(feedback)) + 2205 copyReserve = len([]rune(feedback)) + 2
M pkg/vt/screen.go +0 -3
@@ -656,9 +656,6 @@ func (s *Screen) handleOSC(payload []byte) {
656656 s.handleOSC52(string(payload[len("52;"):]))
657657 return
658658 }
659- if s.OnNotify == nil && s.OnProgress == nil {
660- return
661- }
662659 p := string(payload)
663660 if p == "9;4" || strings.HasPrefix(p, "9;4;") {
664661 s.handleProgress(p[len("9;4"):])
@@ -656,9 +656,6 @@ func (s *Screen) handleOSC(payload []byte) {
656 s.handleOSC52(string(payload[len("52;"):]))656 s.handleOSC52(string(payload[len("52;"):]))
657 return657 return
658 }658 }
659- if s.OnNotify == nil && s.OnProgress == nil {
660- return
661- }
662 p := string(payload)659 p := string(payload)
663 if p == "9;4" || strings.HasPrefix(p, "9;4;") {660 if p == "9;4" || strings.HasPrefix(p, "9;4;") {
664 s.handleProgress(p[len("9;4"):])661 s.handleProgress(p[len("9;4"):])
M pkg/vt/screen_test.go +17 -0
@@ -206,6 +206,23 @@ func TestOSC9ProgressNilCallbackDoesNotPanic(t *testing.T) {
206206 })
207207 }
208208
209+func TestOSC9ProgressSTTerminated(t *testing.T) {
210+ s := NewScreen(10, 2)
211+ var got []bool
212+ s.OnProgress = func(errored bool) { got = append(got, errored) }
213+ s.Write([]byte("\x1b]9;4;1;50\x1b\\\x1b]9;4;0;100\x1b\\"))
214+ require.Equal(t, []bool{false}, got)
215+}
216+
217+func TestOSC9ProgressStateTrackedBeforeCallbackSet(t *testing.T) {
218+ s := NewScreen(10, 2)
219+ s.Write([]byte("\x1b]9;4;1;50\x07"))
220+ var got []bool
221+ s.OnProgress = func(errored bool) { got = append(got, errored) }
222+ s.Write([]byte("\x1b]9;4;0;100\x07"))
223+ require.Equal(t, []bool{false}, got)
224+}
225+
209226 func TestOnNotifyIgnoresOSC9Progress(t *testing.T) {
210227 s := NewScreen(10, 2)
211228 calls := 0
@@ -206,6 +206,23 @@ func TestOSC9ProgressNilCallbackDoesNotPanic(t *testing.T) {
206 })206 })
207 }207 }
208 208
209+func TestOSC9ProgressSTTerminated(t *testing.T) {
210+ s := NewScreen(10, 2)
211+ var got []bool
212+ s.OnProgress = func(errored bool) { got = append(got, errored) }
213+ s.Write([]byte("\x1b]9;4;1;50\x1b\\\x1b]9;4;0;100\x1b\\"))
214+ require.Equal(t, []bool{false}, got)
215+}
216+
217+func TestOSC9ProgressStateTrackedBeforeCallbackSet(t *testing.T) {
218+ s := NewScreen(10, 2)
219+ s.Write([]byte("\x1b]9;4;1;50\x07"))
220+ var got []bool
221+ s.OnProgress = func(errored bool) { got = append(got, errored) }
222+ s.Write([]byte("\x1b]9;4;0;100\x07"))
223+ require.Equal(t, []bool{false}, got)
224+}
225+
209 func TestOnNotifyIgnoresOSC9Progress(t *testing.T) {226 func TestOnNotifyIgnoresOSC9Progress(t *testing.T) {
210 s := NewScreen(10, 2)227 s := NewScreen(10, 2)
211 calls := 0228 calls := 0

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