detached 5be2475

feat(vt): expose live grid line bounds

commit
5be247520ca69342fa64de1b3d5c9066301b963e
author
brice · · 2026-07-25 06:11 UTC
parent
ff84b77
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +34 -0
M pkg/vt/screen.go +9 -0
@@ -111,6 +111,15 @@ func NewScreenWithHistory(width, height int, config HistoryConfig) *Screen {
111111 // configured with NewScreenWithHistory.
112112 func (s *Screen) History() *History { return s.history }
113113
114+// LineBounds returns an owned copy of the live grid's per-row logical extents,
115+// indexed like Frame rows. It returns nil when the screen has no buffer.
116+func (s *Screen) LineBounds() []LineBound {
117+ if s == nil || s.buffer == nil {
118+ return nil
119+ }
120+ return append([]LineBound(nil), s.buffer.boundaries...)
121+}
122+
114123 func (s *Screen) Write(data []byte) {
115124 if len(s.escapeBuf) > 0 {
116125 combined := make([]byte, 0, len(s.escapeBuf)+len(data))
@@ -111,6 +111,15 @@ func NewScreenWithHistory(width, height int, config HistoryConfig) *Screen {
111 // configured with NewScreenWithHistory.111 // configured with NewScreenWithHistory.
112 func (s *Screen) History() *History { return s.history }112 func (s *Screen) History() *History { return s.history }
113 113
114+// LineBounds returns an owned copy of the live grid's per-row logical extents,
115+// indexed like Frame rows. It returns nil when the screen has no buffer.
116+func (s *Screen) LineBounds() []LineBound {
117+ if s == nil || s.buffer == nil {
118+ return nil
119+ }
120+ return append([]LineBound(nil), s.buffer.boundaries...)
121+}
122+
114 func (s *Screen) Write(data []byte) {123 func (s *Screen) Write(data []byte) {
115 if len(s.escapeBuf) > 0 {124 if len(s.escapeBuf) > 0 {
116 combined := make([]byte, 0, len(s.escapeBuf)+len(data))125 combined := make([]byte, 0, len(s.escapeBuf)+len(data))
M pkg/vt/screen_write_test.go +25 -0
@@ -922,3 +922,28 @@ func TestC1Controls(t *testing.T) {
922922 t.Run(tt.name, tt.run)
923923 }
924924 }
925+
926+func TestScreenLineBoundsDescribesTheLiveGrid(t *testing.T) {
927+ s := NewScreen(4, 3)
928+ s.Write([]byte("abcdef")) // wraps after column 4
929+
930+ bounds := s.LineBounds()
931+ if len(bounds) != 3 {
932+ t.Fatalf("len(LineBounds()) = %d, want 3", len(bounds))
933+ }
934+ if !bounds[0].Soft {
935+ t.Errorf("bounds[0].Soft = false, want true (row wrapped)")
936+ }
937+ if bounds[1].Soft {
938+ t.Errorf("bounds[1].Soft = true, want false (row did not wrap)")
939+ }
940+ if bounds[1].End != 2 {
941+ t.Errorf("bounds[1].End = %d, want 2", bounds[1].End)
942+ }
943+
944+ // The result is owned by the caller.
945+ bounds[0] = LineBound{}
946+ if !s.LineBounds()[0].Soft {
947+ t.Error("mutating the result changed the screen's own boundaries")
948+ }
949+}
@@ -922,3 +922,28 @@ func TestC1Controls(t *testing.T) {
922 t.Run(tt.name, tt.run)922 t.Run(tt.name, tt.run)
923 }923 }
924 }924 }
925+
926+func TestScreenLineBoundsDescribesTheLiveGrid(t *testing.T) {
927+ s := NewScreen(4, 3)
928+ s.Write([]byte("abcdef")) // wraps after column 4
929+
930+ bounds := s.LineBounds()
931+ if len(bounds) != 3 {
932+ t.Fatalf("len(LineBounds()) = %d, want 3", len(bounds))
933+ }
934+ if !bounds[0].Soft {
935+ t.Errorf("bounds[0].Soft = false, want true (row wrapped)")
936+ }
937+ if bounds[1].Soft {
938+ t.Errorf("bounds[1].Soft = true, want false (row did not wrap)")
939+ }
940+ if bounds[1].End != 2 {
941+ t.Errorf("bounds[1].End = %d, want 2", bounds[1].End)
942+ }
943+
944+ // The result is owned by the caller.
945+ bounds[0] = LineBound{}
946+ if !s.LineBounds()[0].Soft {
947+ t.Error("mutating the result changed the screen's own boundaries")
948+ }
949+}

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