detached a75e98b

Merge pull request #62 from bnema/agent/fix-bce-erase

Fix background color erase semantics
commit
a75e98b14167475ee256146d641705cb2e69182c
author
bnema · · 2026-07-09 19:52 UTC
committer
GitHub · · 2026-07-09 19:52 UTC
merge of
f392c58 + a162043 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
2 files · +77 -2
M pkg/vt/screen.go +15 -2
@@ -394,6 +394,17 @@ func (s *Screen) clearWidePairAt(x, y int) {
394394 }
395395 }
396396
397+func (s *Screen) eraseCell() renderer.Cell {
398+ style := renderer.DefaultStyle()
399+ if s.Style.HasBackgroundRGB {
400+ style.HasBackgroundRGB = true
401+ style.BackgroundRGB = s.Style.BackgroundRGB
402+ } else {
403+ style.Background = s.Style.Background
404+ }
405+ return renderer.Cell{Rune: ' ', Style: style}
406+}
407+
397408 // clearRow blanks cells [x0,x1) on row y, extending the range to swallow either
398409 // half of a wide pair that straddles a boundary so no orphan half is left. It
399410 // returns the actual modified span [start, start+width).
@@ -419,8 +430,9 @@ func (s *Screen) clearRow(y, x0, x1 int) (start, width int) {
419430 if x1 < s.Frame.Width && s.Frame.At(x1, y).Continuation {
420431 x1++
421432 }
433+ blank := s.eraseCell()
422434 for x := x0; x < x1; x++ {
423- s.Frame.Set(x, y, renderer.BlankCell())
435+ s.Frame.Set(x, y, blank)
424436 }
425437 return x0, x1 - x0
426438 }
@@ -1448,8 +1460,9 @@ func (s *Screen) clearScreenMode(mode int) {
14481460 }
14491461 s.record(renderer.Damage{Kind: renderer.DamageClear, X: 0, Y: 0, Width: s.Frame.Width, Height: min(s.Row+1, s.Frame.Height), Count: 1})
14501462 case 2, 3:
1463+ blank := s.eraseCell()
14511464 for i := range s.Frame.Cells {
1452- s.Frame.Cells[i] = renderer.BlankCell()
1465+ s.Frame.Cells[i] = blank
14531466 }
14541467 s.record(renderer.Damage{Kind: renderer.DamageClear, X: 0, Y: 0, Width: s.Frame.Width, Height: s.Frame.Height, Count: 1})
14551468 default:
@@ -394,6 +394,17 @@ func (s *Screen) clearWidePairAt(x, y int) {
394 }394 }
395 }395 }
396 396
397+func (s *Screen) eraseCell() renderer.Cell {
398+ style := renderer.DefaultStyle()
399+ if s.Style.HasBackgroundRGB {
400+ style.HasBackgroundRGB = true
401+ style.BackgroundRGB = s.Style.BackgroundRGB
402+ } else {
403+ style.Background = s.Style.Background
404+ }
405+ return renderer.Cell{Rune: ' ', Style: style}
406+}
407+
397 // clearRow blanks cells [x0,x1) on row y, extending the range to swallow either408 // clearRow blanks cells [x0,x1) on row y, extending the range to swallow either
398 // half of a wide pair that straddles a boundary so no orphan half is left. It409 // half of a wide pair that straddles a boundary so no orphan half is left. It
399 // returns the actual modified span [start, start+width).410 // returns the actual modified span [start, start+width).
@@ -419,8 +430,9 @@ func (s *Screen) clearRow(y, x0, x1 int) (start, width int) {
419 if x1 < s.Frame.Width && s.Frame.At(x1, y).Continuation {430 if x1 < s.Frame.Width && s.Frame.At(x1, y).Continuation {
420 x1++431 x1++
421 }432 }
433+ blank := s.eraseCell()
422 for x := x0; x < x1; x++ {434 for x := x0; x < x1; x++ {
423- s.Frame.Set(x, y, renderer.BlankCell())435+ s.Frame.Set(x, y, blank)
424 }436 }
425 return x0, x1 - x0437 return x0, x1 - x0
426 }438 }
@@ -1448,8 +1460,9 @@ func (s *Screen) clearScreenMode(mode int) {
1448 }1460 }
1449 s.record(renderer.Damage{Kind: renderer.DamageClear, X: 0, Y: 0, Width: s.Frame.Width, Height: min(s.Row+1, s.Frame.Height), Count: 1})1461 s.record(renderer.Damage{Kind: renderer.DamageClear, X: 0, Y: 0, Width: s.Frame.Width, Height: min(s.Row+1, s.Frame.Height), Count: 1})
1450 case 2, 3:1462 case 2, 3:
1463+ blank := s.eraseCell()
1451 for i := range s.Frame.Cells {1464 for i := range s.Frame.Cells {
1452- s.Frame.Cells[i] = renderer.BlankCell()1465+ s.Frame.Cells[i] = blank
1453 }1466 }
1454 s.record(renderer.Damage{Kind: renderer.DamageClear, X: 0, Y: 0, Width: s.Frame.Width, Height: s.Frame.Height, Count: 1})1467 s.record(renderer.Damage{Kind: renderer.DamageClear, X: 0, Y: 0, Width: s.Frame.Width, Height: s.Frame.Height, Count: 1})
1455 default:1468 default:
M pkg/vt/screen_test.go +62 -0
@@ -1177,6 +1177,68 @@ func TestClearAndErase(t *testing.T) {
11771177 }
11781178 }
11791179
1180+func TestBackgroundColorErase(t *testing.T) {
1181+ tests := []struct {
1182+ name string
1183+ width int
1184+ height int
1185+ seq string
1186+ startX int
1187+ wantStyle renderer.Style
1188+ }{
1189+ {
1190+ name: "indexed background for EL",
1191+ width: 8,
1192+ height: 1,
1193+ seq: "\x1b[1;31;48;5;236mX\x1b[K",
1194+ startX: 1,
1195+ wantStyle: renderer.Style{Foreground: -1, Background: 236},
1196+ },
1197+ {
1198+ name: "truecolor background for full ED",
1199+ width: 3,
1200+ height: 2,
1201+ seq: "abcdef\x1b[1;31;48;2;12;34;56m\x1b[2J",
1202+ wantStyle: renderer.Style{
1203+ Foreground: -1,
1204+ Background: -1,
1205+ HasBackgroundRGB: true,
1206+ BackgroundRGB: renderer.RGB{R: 12, G: 34, B: 56},
1207+ },
1208+ },
1209+ {
1210+ name: "default background after SGR 49",
1211+ width: 2,
1212+ height: 1,
1213+ seq: "\x1b[48;2;12;34;56m\x1b[49m\x1b[K",
1214+ wantStyle: renderer.DefaultStyle(),
1215+ },
1216+ {
1217+ name: "default background after SGR 0",
1218+ width: 2,
1219+ height: 1,
1220+ seq: "\x1b[48;2;12;34;56m\x1b[0m\x1b[K",
1221+ wantStyle: renderer.DefaultStyle(),
1222+ },
1223+ }
1224+
1225+ for _, tt := range tests {
1226+ t.Run(tt.name, func(t *testing.T) {
1227+ s := NewScreen(tt.width, tt.height)
1228+ s.Write([]byte(tt.seq))
1229+
1230+ for y := range tt.height {
1231+ for x := tt.startX; x < tt.width; x++ {
1232+ cell := cellAt(s, x, y)
1233+ if cell.Rune != ' ' || cell.Style != tt.wantStyle {
1234+ t.Errorf("cell(%d,%d) = %+v, want space with style %+v", x, y, cell, tt.wantStyle)
1235+ }
1236+ }
1237+ }
1238+ })
1239+ }
1240+}
1241+
11801242 // ---------------------------------------------------------------------------
11811243 // Cursor move
11821244 // ---------------------------------------------------------------------------
@@ -1177,6 +1177,68 @@ func TestClearAndErase(t *testing.T) {
1177 }1177 }
1178 }1178 }
1179 1179
1180+func TestBackgroundColorErase(t *testing.T) {
1181+ tests := []struct {
1182+ name string
1183+ width int
1184+ height int
1185+ seq string
1186+ startX int
1187+ wantStyle renderer.Style
1188+ }{
1189+ {
1190+ name: "indexed background for EL",
1191+ width: 8,
1192+ height: 1,
1193+ seq: "\x1b[1;31;48;5;236mX\x1b[K",
1194+ startX: 1,
1195+ wantStyle: renderer.Style{Foreground: -1, Background: 236},
1196+ },
1197+ {
1198+ name: "truecolor background for full ED",
1199+ width: 3,
1200+ height: 2,
1201+ seq: "abcdef\x1b[1;31;48;2;12;34;56m\x1b[2J",
1202+ wantStyle: renderer.Style{
1203+ Foreground: -1,
1204+ Background: -1,
1205+ HasBackgroundRGB: true,
1206+ BackgroundRGB: renderer.RGB{R: 12, G: 34, B: 56},
1207+ },
1208+ },
1209+ {
1210+ name: "default background after SGR 49",
1211+ width: 2,
1212+ height: 1,
1213+ seq: "\x1b[48;2;12;34;56m\x1b[49m\x1b[K",
1214+ wantStyle: renderer.DefaultStyle(),
1215+ },
1216+ {
1217+ name: "default background after SGR 0",
1218+ width: 2,
1219+ height: 1,
1220+ seq: "\x1b[48;2;12;34;56m\x1b[0m\x1b[K",
1221+ wantStyle: renderer.DefaultStyle(),
1222+ },
1223+ }
1224+
1225+ for _, tt := range tests {
1226+ t.Run(tt.name, func(t *testing.T) {
1227+ s := NewScreen(tt.width, tt.height)
1228+ s.Write([]byte(tt.seq))
1229+
1230+ for y := range tt.height {
1231+ for x := tt.startX; x < tt.width; x++ {
1232+ cell := cellAt(s, x, y)
1233+ if cell.Rune != ' ' || cell.Style != tt.wantStyle {
1234+ t.Errorf("cell(%d,%d) = %+v, want space with style %+v", x, y, cell, tt.wantStyle)
1235+ }
1236+ }
1237+ }
1238+ })
1239+ }
1240+}
1241+
1180 // ---------------------------------------------------------------------------1242 // ---------------------------------------------------------------------------
1181 // Cursor move1243 // Cursor move
1182 // ---------------------------------------------------------------------------1244 // ---------------------------------------------------------------------------

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