detached 456f79f

feat(term): query terminal palette alongside OSC 10/11

commit
456f79fcfeb8ee395b2a87e6156fdaf7a9ac771b
author
brice · · 2026-07-17 08:11 UTC
parent
7855202
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +11 -6
M internal/adapters/term/terminal.go +5 -2
@@ -31,7 +31,9 @@ const (
3131 bracketedPasteDisable = "\x1b[?2004l"
3232 colorSchemeEnable = "\x1b[?2031h"
3333 colorSchemeDisable = "\x1b[?2031l"
34- oscColorQuery = "\x1b]10;?\x07\x1b]11;?\x07"
34+ oscDefaultColorQuery = "\x1b]10;?\x07\x1b]11;?\x07"
35+ oscPaletteQuery = "\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07"
36+ oscColorQuery = oscDefaultColorQuery + oscPaletteQuery
3537 )
3638
3739 // bufSize is the batched writer's buffer capacity.
@@ -235,7 +237,8 @@ func (t *Terminal) stopResizeLocked() {
235237 t.resizeWG.Wait()
236238 }
237239
238-// QueryColors re-emits OSC 10/11 queries for default foreground/background.
240+// QueryColors re-emits OSC 10/11 queries for default foreground/background
241+// followed by one batched OSC 4 query for palette slots 0 through 15.
239242 func (t *Terminal) QueryColors() error {
240243 t.mu.Lock()
241244 defer t.mu.Unlock()
@@ -31,7 +31,9 @@ const (
31 bracketedPasteDisable = "\x1b[?2004l"31 bracketedPasteDisable = "\x1b[?2004l"
32 colorSchemeEnable = "\x1b[?2031h"32 colorSchemeEnable = "\x1b[?2031h"
33 colorSchemeDisable = "\x1b[?2031l"33 colorSchemeDisable = "\x1b[?2031l"
34- oscColorQuery = "\x1b]10;?\x07\x1b]11;?\x07"34+ oscDefaultColorQuery = "\x1b]10;?\x07\x1b]11;?\x07"
35+ oscPaletteQuery = "\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07"
36+ oscColorQuery = oscDefaultColorQuery + oscPaletteQuery
35 )37 )
36 38
37 // bufSize is the batched writer's buffer capacity.39 // bufSize is the batched writer's buffer capacity.
@@ -235,7 +237,8 @@ func (t *Terminal) stopResizeLocked() {
235 t.resizeWG.Wait()237 t.resizeWG.Wait()
236 }238 }
237 239
238-// QueryColors re-emits OSC 10/11 queries for default foreground/background.240+// QueryColors re-emits OSC 10/11 queries for default foreground/background
241+// followed by one batched OSC 4 query for palette slots 0 through 15.
239 func (t *Terminal) QueryColors() error {242 func (t *Terminal) QueryColors() error {
240 t.mu.Lock()243 t.mu.Lock()
241 defer t.mu.Unlock()244 defer t.mu.Unlock()
M internal/adapters/term/terminal_test.go +6 -4
@@ -10,6 +10,8 @@ import (
1010 "github.com/bnema/vev/internal/domain"
1111 )
1212
13+const wantColorQueries = "\x1b]10;?\x07\x1b]11;?\x07\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07"
14+
1315 // pipePair returns an os.Pipe and a goroutine that continuously copies
1416 // everything written to the write end into captured, closing done once
1517 // the write end is closed (EOF).
@@ -60,7 +62,7 @@ func TestTerminal_EnterRaw_NonTTY_EmitsAltScreenAndCursorEscapes(t *testing.T) {
6062 _ = outW.Close()
6163 <-done
6264
63- want := altScreenEnter + cursorHide + mouseEnable + bracketedPasteEnable + colorSchemeEnable + oscColorQuery + cursorShow + cursorStyleDefault + mouseDisable + bracketedPasteDisable + colorSchemeDisable + altScreenExit
65+ want := altScreenEnter + cursorHide + mouseEnable + bracketedPasteEnable + colorSchemeEnable + wantColorQueries + cursorShow + cursorStyleDefault + mouseDisable + bracketedPasteDisable + colorSchemeDisable + altScreenExit
6466 if got := captured.String(); got != want {
6567 t.Fatalf("captured escapes = %q, want %q", got, want)
6668 }
@@ -101,7 +103,7 @@ func TestTerminal_EnterRaw_IsIdempotentAcrossCalls(t *testing.T) {
101103 // Alt-screen/cursor/theme-query escapes must appear exactly once for enter and
102104 // cursor/alt-screen exits exactly once, regardless of how many times
103105 // EnterRaw/restore were called.
104- want := altScreenEnter + cursorHide + mouseEnable + bracketedPasteEnable + colorSchemeEnable + oscColorQuery + cursorShow + cursorStyleDefault + mouseDisable + bracketedPasteDisable + colorSchemeDisable + altScreenExit
106+ want := altScreenEnter + cursorHide + mouseEnable + bracketedPasteEnable + colorSchemeEnable + wantColorQueries + cursorShow + cursorStyleDefault + mouseDisable + bracketedPasteDisable + colorSchemeDisable + altScreenExit
105107 if got := captured.String(); got != want {
106108 t.Fatalf("captured escapes = %q, want %q", got, want)
107109 }
@@ -124,8 +126,8 @@ func TestTerminal_QueryColors(t *testing.T) {
124126 }
125127 _ = outW.Close()
126128 <-done
127- if got := captured.String(); got != oscColorQuery {
128- t.Fatalf("captured = %q, want %q", got, oscColorQuery)
129+ if got := captured.String(); got != wantColorQueries {
130+ t.Fatalf("captured = %q, want %q", got, wantColorQueries)
129131 }
130132 }
131133
@@ -10,6 +10,8 @@ import (
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11 )11 )
12 12
13+const wantColorQueries = "\x1b]10;?\x07\x1b]11;?\x07\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07"
14+
13 // pipePair returns an os.Pipe and a goroutine that continuously copies15 // pipePair returns an os.Pipe and a goroutine that continuously copies
14 // everything written to the write end into captured, closing done once16 // everything written to the write end into captured, closing done once
15 // the write end is closed (EOF).17 // the write end is closed (EOF).
@@ -60,7 +62,7 @@ func TestTerminal_EnterRaw_NonTTY_EmitsAltScreenAndCursorEscapes(t *testing.T) {
60 _ = outW.Close()62 _ = outW.Close()
61 <-done63 <-done
62 64
63- want := altScreenEnter + cursorHide + mouseEnable + bracketedPasteEnable + colorSchemeEnable + oscColorQuery + cursorShow + cursorStyleDefault + mouseDisable + bracketedPasteDisable + colorSchemeDisable + altScreenExit65+ want := altScreenEnter + cursorHide + mouseEnable + bracketedPasteEnable + colorSchemeEnable + wantColorQueries + cursorShow + cursorStyleDefault + mouseDisable + bracketedPasteDisable + colorSchemeDisable + altScreenExit
64 if got := captured.String(); got != want {66 if got := captured.String(); got != want {
65 t.Fatalf("captured escapes = %q, want %q", got, want)67 t.Fatalf("captured escapes = %q, want %q", got, want)
66 }68 }
@@ -101,7 +103,7 @@ func TestTerminal_EnterRaw_IsIdempotentAcrossCalls(t *testing.T) {
101 // Alt-screen/cursor/theme-query escapes must appear exactly once for enter and103 // Alt-screen/cursor/theme-query escapes must appear exactly once for enter and
102 // cursor/alt-screen exits exactly once, regardless of how many times104 // cursor/alt-screen exits exactly once, regardless of how many times
103 // EnterRaw/restore were called.105 // EnterRaw/restore were called.
104- want := altScreenEnter + cursorHide + mouseEnable + bracketedPasteEnable + colorSchemeEnable + oscColorQuery + cursorShow + cursorStyleDefault + mouseDisable + bracketedPasteDisable + colorSchemeDisable + altScreenExit106+ want := altScreenEnter + cursorHide + mouseEnable + bracketedPasteEnable + colorSchemeEnable + wantColorQueries + cursorShow + cursorStyleDefault + mouseDisable + bracketedPasteDisable + colorSchemeDisable + altScreenExit
105 if got := captured.String(); got != want {107 if got := captured.String(); got != want {
106 t.Fatalf("captured escapes = %q, want %q", got, want)108 t.Fatalf("captured escapes = %q, want %q", got, want)
107 }109 }
@@ -124,8 +126,8 @@ func TestTerminal_QueryColors(t *testing.T) {
124 }126 }
125 _ = outW.Close()127 _ = outW.Close()
126 <-done128 <-done
127- if got := captured.String(); got != oscColorQuery {129+ if got := captured.String(); got != wantColorQueries {
128- t.Fatalf("captured = %q, want %q", got, oscColorQuery)130+ t.Fatalf("captured = %q, want %q", got, wantColorQueries)
129 }131 }
130 }132 }
131 133

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