Skip to content

Commit 08e2ae0

Browse files
feat: add DSR 996 color scheme query support
Handle CSI ? 996 n (DSR 996) in deviceStatusPrivate. When color scheme updates are enabled (DECSET 2031), fires OnRequestColorSchemeQuery so the host application can respond with the current color scheme. - Add OnRequestColorSchemeQueryEmitter to InputHandler - Add case 996 to deviceStatusPrivate (gated on ColorSchemeUpdates mode) - Expose OnRequestColorSchemeQuery event on Terminal - Add tests at both InputHandler and Terminal levels Fixes #31 Co-authored-by: Ona <no-reply@ona.com>
1 parent b5b6a04 commit 08e2ae0

5 files changed

Lines changed: 145 additions & 11 deletions

File tree

inputhandler.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,9 @@ type InputHandler struct {
9898
OnRequestBellEmitter EventEmitter[struct{}]
9999
OnRequestResetEmitter EventEmitter[struct{}]
100100
OnRequestRefreshRowsEmitter EventEmitter[RowRange]
101-
OnColorEmitter EventEmitter[[]ColorEvent]
102-
OnRequestSyncScrollBarEmitter EventEmitter[struct{}]
101+
OnColorEmitter EventEmitter[[]ColorEvent]
102+
OnRequestSyncScrollBarEmitter EventEmitter[struct{}]
103+
OnRequestColorSchemeQueryEmitter EventEmitter[struct{}]
103104
}
104105

105106
// NewInputHandler creates an InputHandler and registers all parser handlers.
@@ -547,4 +548,5 @@ func (h *InputHandler) Dispose() {
547548
h.OnRequestRefreshRowsEmitter.Dispose()
548549
h.OnColorEmitter.Dispose()
549550
h.OnRequestSyncScrollBarEmitter.Dispose()
551+
h.OnRequestColorSchemeQueryEmitter.Dispose()
550552
}

inputhandler_csi.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,10 +497,15 @@ func (h *InputHandler) deviceStatus(params *Params) bool {
497497

498498
func (h *InputHandler) deviceStatusPrivate(params *Params) bool {
499499
buf := h.activeBuffer()
500-
if params.Params[0] == 6 {
500+
switch params.Params[0] {
501+
case 6:
501502
y := buf.Y + 1
502503
x := buf.X + 1
503504
h.coreService.TriggerDataEvent(fmt.Sprintf("\x1b[?%d;%dR", y, x), false, false)
505+
case 996:
506+
if h.coreService.DecPrivateModes.ColorSchemeUpdates {
507+
h.OnRequestColorSchemeQueryEmitter.Fire(struct{}{})
508+
}
504509
}
505510
return true
506511
}

inputhandler_csi_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,3 +1516,80 @@ func TestWindowOptionsPopIconNameEmptyStack(t *testing.T) {
15161516
}
15171517
}
15181518

1519+
func TestDSR996ColorSchemeQuery(t *testing.T) {
1520+
t.Parallel()
1521+
1522+
t.Run("fires_when_color_scheme_updates_enabled", func(t *testing.T) {
1523+
t.Parallel()
1524+
h := newTestInputHandler(80, 24)
1525+
fired := 0
1526+
h.OnRequestColorSchemeQueryEmitter.Event(func(struct{}) {
1527+
fired++
1528+
})
1529+
1530+
// Enable color scheme updates (DECSET 2031).
1531+
h.ParseString("\x1b[?2031h")
1532+
// Send DSR 996.
1533+
h.ParseString("\x1b[?996n")
1534+
1535+
if fired != 1 {
1536+
t.Errorf("expected OnRequestColorSchemeQuery to fire once, got %d", fired)
1537+
}
1538+
})
1539+
1540+
t.Run("does_not_fire_when_color_scheme_updates_disabled", func(t *testing.T) {
1541+
t.Parallel()
1542+
h := newTestInputHandler(80, 24)
1543+
fired := 0
1544+
h.OnRequestColorSchemeQueryEmitter.Event(func(struct{}) {
1545+
fired++
1546+
})
1547+
1548+
// Do NOT enable color scheme updates — send DSR 996 directly.
1549+
h.ParseString("\x1b[?996n")
1550+
1551+
if fired != 0 {
1552+
t.Errorf("expected OnRequestColorSchemeQuery not to fire, got %d", fired)
1553+
}
1554+
})
1555+
1556+
t.Run("does_not_fire_after_color_scheme_updates_reset", func(t *testing.T) {
1557+
t.Parallel()
1558+
h := newTestInputHandler(80, 24)
1559+
fired := 0
1560+
h.OnRequestColorSchemeQueryEmitter.Event(func(struct{}) {
1561+
fired++
1562+
})
1563+
1564+
// Enable then disable color scheme updates.
1565+
h.ParseString("\x1b[?2031h")
1566+
h.ParseString("\x1b[?2031l")
1567+
// Send DSR 996.
1568+
h.ParseString("\x1b[?996n")
1569+
1570+
if fired != 0 {
1571+
t.Errorf("expected OnRequestColorSchemeQuery not to fire after reset, got %d", fired)
1572+
}
1573+
})
1574+
1575+
t.Run("dsr6_still_works", func(t *testing.T) {
1576+
t.Parallel()
1577+
h := newTestInputHandler(80, 24)
1578+
var response string
1579+
h.coreService.OnDataEmitter.Event(func(s string) {
1580+
response = s
1581+
})
1582+
1583+
// Move cursor to row 5, col 10.
1584+
h.ParseString("\x1b[5;10H")
1585+
// Send DSR 6 (cursor position report).
1586+
h.ParseString("\x1b[?6n")
1587+
1588+
expected := "\x1b[?5;10R"
1589+
if response != expected {
1590+
t.Errorf("DSR 6 response = %q, want %q", response, expected)
1591+
}
1592+
})
1593+
}
1594+
1595+

terminal.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,15 @@ type Terminal struct {
4040
inputHandler *InputHandler
4141

4242
// Public event emitters (forwarded from sub-components).
43-
OnBellEmitter EventEmitter[struct{}]
44-
OnTitleChangeEmitter EventEmitter[string]
45-
OnIconNameChangeEmitter EventEmitter[string]
46-
OnLineFeedEmitter EventEmitter[struct{}]
47-
OnCursorMoveEmitter EventEmitter[struct{}]
48-
OnResizeEmitter EventEmitter[BufferResizeEvent]
49-
OnScrollEmitter EventEmitter[int]
50-
OnRenderEmitter EventEmitter[RowRange]
43+
OnBellEmitter EventEmitter[struct{}]
44+
OnTitleChangeEmitter EventEmitter[string]
45+
OnIconNameChangeEmitter EventEmitter[string]
46+
OnLineFeedEmitter EventEmitter[struct{}]
47+
OnCursorMoveEmitter EventEmitter[struct{}]
48+
OnResizeEmitter EventEmitter[BufferResizeEvent]
49+
OnScrollEmitter EventEmitter[int]
50+
OnRenderEmitter EventEmitter[RowRange]
51+
OnRequestColorSchemeQueryEmitter EventEmitter[struct{}]
5152
}
5253

5354
// New creates a new Terminal with the given options.
@@ -84,6 +85,7 @@ func New(opts ...Option) *Terminal {
8485
ih.OnLineFeedEmitter.Event(func(struct{}) { t.OnLineFeedEmitter.Fire(struct{}{}) })
8586
ih.OnCursorMoveEmitter.Event(func(struct{}) { t.OnCursorMoveEmitter.Fire(struct{}{}) })
8687
ih.OnRequestRefreshRowsEmitter.Event(func(r RowRange) { t.OnRenderEmitter.Fire(r) })
88+
ih.OnRequestColorSchemeQueryEmitter.Event(func(struct{}) { t.OnRequestColorSchemeQueryEmitter.Fire(struct{}{}) })
8789

8890
// Forward buffer service events.
8991
bufSvc.OnResizeEmitter.Event(func(e BufferResizeEvent) { t.OnResizeEmitter.Fire(e) })
@@ -252,6 +254,12 @@ func (t *Terminal) OnRender(fn func(RowRange)) Disposable {
252254
return t.OnRenderEmitter.Event(fn)
253255
}
254256

257+
// OnRequestColorSchemeQuery subscribes to DSR 996 color scheme query events.
258+
// Fired when the client sends CSI ? 996 n while color scheme updates (DECSET 2031) are enabled.
259+
func (t *Terminal) OnRequestColorSchemeQuery(fn func()) Disposable {
260+
return t.OnRequestColorSchemeQueryEmitter.Event(func(struct{}) { fn() })
261+
}
262+
255263
// OnColor subscribes to color palette query/set/restore events (OSC 4/10/11/12).
256264
func (t *Terminal) OnColor(fn func([]ColorEvent)) Disposable {
257265
return t.inputHandler.OnColorEmitter.Event(fn)
@@ -401,4 +409,5 @@ func (t *Terminal) Dispose() {
401409
t.OnResizeEmitter.Dispose()
402410
t.OnScrollEmitter.Dispose()
403411
t.OnRenderEmitter.Dispose()
412+
t.OnRequestColorSchemeQueryEmitter.Dispose()
404413
}

terminal_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,4 +1179,45 @@ func TestTerminalOnA11yChar(t *testing.T) {
11791179
d.Dispose()
11801180
}
11811181

1182+
func TestTerminalOnRequestColorSchemeQuery(t *testing.T) {
1183+
t.Parallel()
1184+
term := newTestTerminal(80, 24)
1185+
fired := 0
1186+
term.OnRequestColorSchemeQuery(func() {
1187+
fired++
1188+
})
1189+
1190+
// Enable color scheme updates and send DSR 996.
1191+
term.WriteString("\x1b[?2031h")
1192+
term.WriteString("\x1b[?996n")
1193+
1194+
if fired != 1 {
1195+
t.Errorf("expected OnRequestColorSchemeQuery to fire once via Terminal, got %d", fired)
1196+
}
1197+
}
1198+
1199+
func TestTerminalOnRequestColorSchemeQueryDispose(t *testing.T) {
1200+
t.Parallel()
1201+
term := newTestTerminal(80, 24)
1202+
fired := 0
1203+
d := term.OnRequestColorSchemeQuery(func() {
1204+
fired++
1205+
})
1206+
1207+
// Enable color scheme updates and send DSR 996.
1208+
term.WriteString("\x1b[?2031h")
1209+
term.WriteString("\x1b[?996n")
1210+
if fired != 1 {
1211+
t.Fatalf("expected 1 fire before dispose, got %d", fired)
1212+
}
1213+
1214+
// Dispose the listener and send again.
1215+
d.Dispose()
1216+
term.WriteString("\x1b[?996n")
1217+
if fired != 1 {
1218+
t.Errorf("expected no additional fires after dispose, got %d", fired)
1219+
}
1220+
}
1221+
1222+
11821223

0 commit comments

Comments
 (0)