feat(term): query terminal palette alongside OSC 10/11
- commit
- 456f79fcfeb8ee395b2a87e6156fdaf7a9ac771b
- author
- brice · · 2026-07-17 08:11 UTC
- parent
- 7855202
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +11 -6
M internal/adapters/term/terminal.go +5 -2
| @@ -31,7 +31,9 @@ const ( | |||
| 31 | 31 | bracketedPasteDisable = "\x1b[?2004l" | |
| 32 | 32 | colorSchemeEnable = "\x1b[?2031h" | |
| 33 | 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 | 39 | // bufSize is the batched writer's buffer capacity. | |
| @@ -235,7 +237,8 @@ func (t *Terminal) stopResizeLocked() { | |||
| 235 | 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 | 242 | func (t *Terminal) QueryColors() error { | |
| 240 | 243 | t.mu.Lock() | |
| 241 | 244 | 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 ( | |||
| 10 | 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 | 15 | // pipePair returns an os.Pipe and a goroutine that continuously copies | |
| 14 | 16 | // everything written to the write end into captured, closing done once | |
| 15 | 17 | // the write end is closed (EOF). | |
| @@ -60,7 +62,7 @@ func TestTerminal_EnterRaw_NonTTY_EmitsAltScreenAndCursorEscapes(t *testing.T) { | |||
| 60 | 62 | _ = outW.Close() | |
| 61 | 63 | <-done | |
| 62 | 64 | ||
| 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 | |
| 64 | 66 | if got := captured.String(); got != want { | |
| 65 | 67 | t.Fatalf("captured escapes = %q, want %q", got, want) | |
| 66 | 68 | } | |
| @@ -101,7 +103,7 @@ func TestTerminal_EnterRaw_IsIdempotentAcrossCalls(t *testing.T) { | |||
| 101 | 103 | // Alt-screen/cursor/theme-query escapes must appear exactly once for enter and | |
| 102 | 104 | // cursor/alt-screen exits exactly once, regardless of how many times | |
| 103 | 105 | // 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 | |
| 105 | 107 | if got := captured.String(); got != want { | |
| 106 | 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 | 127 | _ = outW.Close() | |
| 126 | 128 | <-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) | |
| 129 | 131 | } | |
| 130 | 132 | } | |
| 131 | 133 | ||
| @@ -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 copies | 15 | // pipePair returns an os.Pipe and a goroutine that continuously copies | ||
| 14 | // everything written to the write end into captured, closing done once | 16 | // 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 | <-done | 63 | <-done | ||
| 62 | 64 | ||||
| 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 |
| 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 and | 103 | // Alt-screen/cursor/theme-query escapes must appear exactly once for enter and | ||
| 102 | // cursor/alt-screen exits exactly once, regardless of how many times | 104 | // 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 + altScreenExit | 106 | + | 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 | <-done | 128 | <-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 | ||||