Skip to content

Commit 839e942

Browse files
fix: gate vtExtensions features behind VtExtensions option
Add VtExtensions struct to TerminalOptions to gate non-standard terminal extensions, matching upstream xterm.js behavior. Previously these features were always enabled; now they respect per-feature opt-in flags: - KittyKeyboard: gates CSI = u, CSI ? u, CSI > u, CSI < u, and kitty keyboard flag swap on alt screen switch (modes 47/1047/1049) - ColorSchemeQuery: gates mode 2031 set/reset/DECRPM (default true) - Win32InputMode: gates mode 9001 set/reset/DECRPM - KittySgrBoldFaintControl: gates SGR 221/222 (default true) Fixes #35 Co-authored-by: Ona <no-reply@ona.com>
1 parent 581857e commit 839e942

7 files changed

Lines changed: 340 additions & 20 deletions

inputhandler_csi.go

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -667,10 +667,12 @@ func (h *InputHandler) setModePrivate(params *Params) bool {
667667
fallthrough
668668
case 47, 1047:
669669
// Swap kitty keyboard flags: save main, restore alt
670-
kk := &h.coreService.KittyKeyboard
671-
kk.MainFlags = kk.Flags
672-
kk.Flags = kk.AltFlags
673-
kk.MainStack, kk.AltStack = kk.AltStack, kk.MainStack
670+
if h.optionsService.Options.VtExtensions.KittyKeyboard {
671+
kk := &h.coreService.KittyKeyboard
672+
kk.MainFlags = kk.Flags
673+
kk.Flags = kk.AltFlags
674+
kk.MainStack, kk.AltStack = kk.AltStack, kk.MainStack
675+
}
674676
h.bufferService.Buffers.ActivateAltBuffer(h.eraseAttrData())
675677
h.coreService.IsCursorInitialized = true
676678
h.OnRequestRefreshRowsEmitter.Fire(RowRange{})
@@ -680,9 +682,13 @@ func (h *InputHandler) setModePrivate(params *Params) bool {
680682
case 2026:
681683
h.coreService.DecPrivateModes.SynchronizedOutput = true
682684
case 2031:
683-
h.coreService.DecPrivateModes.ColorSchemeUpdates = true
685+
if h.optionsService.Options.VtExtensions.colorSchemeQueryEnabled() {
686+
h.coreService.DecPrivateModes.ColorSchemeUpdates = true
687+
}
684688
case 9001:
685-
h.coreService.DecPrivateModes.Win32InputMode = true
689+
if h.optionsService.Options.VtExtensions.Win32InputMode {
690+
h.coreService.DecPrivateModes.Win32InputMode = true
691+
}
686692
}
687693
}
688694
return true
@@ -717,21 +723,25 @@ func (h *InputHandler) resetModePrivate(params *Params) bool {
717723
h.RestoreCursor()
718724
case 1049:
719725
// Swap kitty keyboard flags: save alt, restore main
720-
kk := &h.coreService.KittyKeyboard
721-
kk.AltFlags = kk.Flags
722-
kk.Flags = kk.MainFlags
723-
kk.MainStack, kk.AltStack = kk.AltStack, kk.MainStack
726+
if h.optionsService.Options.VtExtensions.KittyKeyboard {
727+
kk := &h.coreService.KittyKeyboard
728+
kk.AltFlags = kk.Flags
729+
kk.Flags = kk.MainFlags
730+
kk.MainStack, kk.AltStack = kk.AltStack, kk.MainStack
731+
}
724732
h.bufferService.Buffers.ActivateNormalBuffer()
725733
h.RestoreCursor()
726734
h.coreService.IsCursorInitialized = true
727735
h.OnRequestRefreshRowsEmitter.Fire(RowRange{})
728736
h.OnRequestSyncScrollBarEmitter.Fire(struct{}{})
729737
case 47, 1047:
730738
// Swap kitty keyboard flags: save alt, restore main
731-
kk := &h.coreService.KittyKeyboard
732-
kk.AltFlags = kk.Flags
733-
kk.Flags = kk.MainFlags
734-
kk.MainStack, kk.AltStack = kk.AltStack, kk.MainStack
739+
if h.optionsService.Options.VtExtensions.KittyKeyboard {
740+
kk := &h.coreService.KittyKeyboard
741+
kk.AltFlags = kk.Flags
742+
kk.Flags = kk.MainFlags
743+
kk.MainStack, kk.AltStack = kk.AltStack, kk.MainStack
744+
}
735745
h.bufferService.Buffers.ActivateNormalBuffer()
736746
h.coreService.IsCursorInitialized = true
737747
h.OnRequestRefreshRowsEmitter.Fire(RowRange{})
@@ -742,9 +752,13 @@ func (h *InputHandler) resetModePrivate(params *Params) bool {
742752
h.coreService.DecPrivateModes.SynchronizedOutput = false
743753
h.OnRequestRefreshRowsEmitter.Fire(RowRange{})
744754
case 2031:
745-
h.coreService.DecPrivateModes.ColorSchemeUpdates = false
755+
if h.optionsService.Options.VtExtensions.colorSchemeQueryEnabled() {
756+
h.coreService.DecPrivateModes.ColorSchemeUpdates = false
757+
}
746758
case 9001:
747-
h.coreService.DecPrivateModes.Win32InputMode = false
759+
if h.optionsService.Options.VtExtensions.Win32InputMode {
760+
h.coreService.DecPrivateModes.Win32InputMode = false
761+
}
748762
}
749763
}
750764
return true
@@ -824,8 +838,14 @@ func (h *InputHandler) privateModeSetting(mode int) int {
824838
case 2026:
825839
return boolToPm(dm.SynchronizedOutput)
826840
case 2031:
841+
if !h.optionsService.Options.VtExtensions.colorSchemeQueryEnabled() {
842+
return 0
843+
}
827844
return boolToPm(dm.ColorSchemeUpdates)
828845
case 9001:
846+
if !h.optionsService.Options.VtExtensions.Win32InputMode {
847+
return 0
848+
}
829849
return boolToPm(dm.Win32InputMode)
830850
default:
831851
return 0 // not recognized
@@ -862,6 +882,9 @@ const kittyKeyboardMaxStackSize = 10
862882
// kittyKeyboardSet handles CSI = Ps ; Pm u — set kitty keyboard flags.
863883
// Ps = flags value (default 0), Pm = mode: 1=set (OR), 2=clear (AND NOT), 3=assign (default 1).
864884
func (h *InputHandler) kittyKeyboardSet(params *Params) bool {
885+
if !h.optionsService.Options.VtExtensions.KittyKeyboard {
886+
return true
887+
}
865888
kk := &h.coreService.KittyKeyboard
866889
flags := 0
867890
if params.Params[0] > 0 {
@@ -885,6 +908,9 @@ func (h *InputHandler) kittyKeyboardSet(params *Params) bool {
885908
// kittyKeyboardQuery handles CSI ? u — query current kitty keyboard flags.
886909
// Responds with CSI ? <flags> u.
887910
func (h *InputHandler) kittyKeyboardQuery(params *Params) bool {
911+
if !h.optionsService.Options.VtExtensions.KittyKeyboard {
912+
return true
913+
}
888914
buf := h.activeBuffer()
889915
shouldScroll := buf.YBase != buf.YDisp
890916
h.coreService.TriggerDataEvent(fmt.Sprintf("\x1b[?%du", h.coreService.KittyKeyboard.Flags), false, shouldScroll)
@@ -894,6 +920,9 @@ func (h *InputHandler) kittyKeyboardQuery(params *Params) bool {
894920
// kittyKeyboardPush handles CSI > Ps u — push flags onto stack.
895921
// Pushes current flags, then sets flags = Ps (default 0).
896922
func (h *InputHandler) kittyKeyboardPush(params *Params) bool {
923+
if !h.optionsService.Options.VtExtensions.KittyKeyboard {
924+
return true
925+
}
897926
kk := &h.coreService.KittyKeyboard
898927
if len(kk.MainStack) < kittyKeyboardMaxStackSize {
899928
kk.MainStack = append(kk.MainStack, kk.Flags)
@@ -910,6 +939,9 @@ func (h *InputHandler) kittyKeyboardPush(params *Params) bool {
910939
// Ps = number of entries to pop (default 1). Restores flags from the last popped entry.
911940
// If stack is empty, sets flags = 0.
912941
func (h *InputHandler) kittyKeyboardPop(params *Params) bool {
942+
if !h.optionsService.Options.VtExtensions.KittyKeyboard {
943+
return true
944+
}
913945
kk := &h.coreService.KittyKeyboard
914946
n := 1
915947
if params.Params[0] > 0 {

inputhandler_csi_test.go

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,29 @@ import (
66
"github.com/google/go-cmp/cmp"
77
)
88

9-
// newTestInputHandler creates an InputHandler with default services for testing.
9+
// allVtExtensions returns a VtExtensions with every extension enabled.
10+
func allVtExtensions() VtExtensions {
11+
t := true
12+
return VtExtensions{
13+
KittyKeyboard: true,
14+
ColorSchemeQuery: &t,
15+
Win32InputMode: true,
16+
KittySgrBoldFaintControl: &t,
17+
}
18+
}
19+
20+
// newTestInputHandler creates an InputHandler with all VtExtensions enabled.
1021
func newTestInputHandler(cols, rows int) *InputHandler {
22+
return newTestInputHandlerWithVtExt(cols, rows, allVtExtensions())
23+
}
24+
25+
// newTestInputHandlerWithVtExt creates an InputHandler with the given VtExtensions.
26+
func newTestInputHandlerWithVtExt(cols, rows int, ext VtExtensions) *InputHandler {
1127
opts := DefaultOptions()
1228
opts.Cols = cols
1329
opts.Rows = rows
1430
opts.Scrollback = 1000
31+
opts.VtExtensions = ext
1532
optsSvc := NewOptionsService(&opts)
1633
bufSvc := NewBufferService(optsSvc)
1734
charSvc := NewCharsetService()
@@ -577,6 +594,7 @@ func newTestInputHandlerWithTermName(cols, rows int, termName string) *InputHand
577594
opts.Rows = rows
578595
opts.Scrollback = 1000
579596
opts.TermName = termName
597+
opts.VtExtensions = allVtExtensions()
580598
optsSvc := NewOptionsService(&opts)
581599
bufSvc := NewBufferService(optsSvc)
582600
charSvc := NewCharsetService()
@@ -1838,3 +1856,167 @@ func TestWindowOptionsReportTerminalLevel(t *testing.T) {
18381856
})
18391857
}
18401858

1859+
// --- VtExtensions gating tests ---
1860+
1861+
func TestVtExtensions_Win32InputModeGating(t *testing.T) {
1862+
t.Parallel()
1863+
1864+
t.Run("DECRPM_reports_not_recognized_when_disabled", func(t *testing.T) {
1865+
t.Parallel()
1866+
h := newTestInputHandlerWithVtExt(80, 24, VtExtensions{})
1867+
var response string
1868+
h.coreService.OnDataEmitter.Event(func(data string) {
1869+
response = data
1870+
})
1871+
h.ParseString("\x1b[?9001$p")
1872+
expected := "\x1b[?9001;0$y"
1873+
if response != expected {
1874+
t.Errorf("expected %q, got %q", expected, response)
1875+
}
1876+
})
1877+
1878+
t.Run("DECSET_ignored_when_disabled", func(t *testing.T) {
1879+
t.Parallel()
1880+
h := newTestInputHandlerWithVtExt(80, 24, VtExtensions{})
1881+
h.ParseString("\x1b[?9001h")
1882+
if h.coreService.DecPrivateModes.Win32InputMode {
1883+
t.Error("Win32InputMode should remain false when vtExtensions.Win32InputMode is disabled")
1884+
}
1885+
})
1886+
1887+
t.Run("DECRST_ignored_when_disabled", func(t *testing.T) {
1888+
t.Parallel()
1889+
ext := allVtExtensions()
1890+
h := newTestInputHandlerWithVtExt(80, 24, ext)
1891+
h.ParseString("\x1b[?9001h")
1892+
if !h.coreService.DecPrivateModes.Win32InputMode {
1893+
t.Fatal("precondition: Win32InputMode should be true")
1894+
}
1895+
h.optionsService.Options.VtExtensions.Win32InputMode = false
1896+
h.ParseString("\x1b[?9001l")
1897+
if !h.coreService.DecPrivateModes.Win32InputMode {
1898+
t.Error("Win32InputMode should remain true when vtExtensions.Win32InputMode is disabled")
1899+
}
1900+
})
1901+
1902+
t.Run("DECRPM_reports_set_when_enabled", func(t *testing.T) {
1903+
t.Parallel()
1904+
h := newTestInputHandlerWithVtExt(80, 24, allVtExtensions())
1905+
var response string
1906+
h.coreService.OnDataEmitter.Event(func(data string) {
1907+
response = data
1908+
})
1909+
h.ParseString("\x1b[?9001h")
1910+
h.ParseString("\x1b[?9001$p")
1911+
expected := "\x1b[?9001;1$y"
1912+
if response != expected {
1913+
t.Errorf("expected %q, got %q", expected, response)
1914+
}
1915+
})
1916+
}
1917+
1918+
func TestVtExtensions_ColorSchemeQueryGating(t *testing.T) {
1919+
t.Parallel()
1920+
1921+
t.Run("DECRPM_reports_not_recognized_when_disabled", func(t *testing.T) {
1922+
t.Parallel()
1923+
f := false
1924+
h := newTestInputHandlerWithVtExt(80, 24, VtExtensions{ColorSchemeQuery: &f})
1925+
var response string
1926+
h.coreService.OnDataEmitter.Event(func(data string) {
1927+
response = data
1928+
})
1929+
h.ParseString("\x1b[?2031$p")
1930+
expected := "\x1b[?2031;0$y"
1931+
if response != expected {
1932+
t.Errorf("expected %q, got %q", expected, response)
1933+
}
1934+
})
1935+
1936+
t.Run("DECSET_ignored_when_disabled", func(t *testing.T) {
1937+
t.Parallel()
1938+
f := false
1939+
h := newTestInputHandlerWithVtExt(80, 24, VtExtensions{ColorSchemeQuery: &f})
1940+
h.ParseString("\x1b[?2031h")
1941+
if h.coreService.DecPrivateModes.ColorSchemeUpdates {
1942+
t.Error("ColorSchemeUpdates should remain false when vtExtensions.ColorSchemeQuery is false")
1943+
}
1944+
})
1945+
1946+
t.Run("enabled_by_default_when_nil", func(t *testing.T) {
1947+
t.Parallel()
1948+
h := newTestInputHandlerWithVtExt(80, 24, VtExtensions{})
1949+
h.ParseString("\x1b[?2031h")
1950+
if !h.coreService.DecPrivateModes.ColorSchemeUpdates {
1951+
t.Error("ColorSchemeUpdates should be settable when ColorSchemeQuery is nil (default true)")
1952+
}
1953+
})
1954+
}
1955+
1956+
func TestVtExtensions_KittyKeyboardGating(t *testing.T) {
1957+
t.Parallel()
1958+
1959+
t.Run("set_ignored_when_disabled", func(t *testing.T) {
1960+
t.Parallel()
1961+
h := newTestInputHandlerWithVtExt(80, 24, VtExtensions{})
1962+
h.ParseString("\x1b[=3;3u")
1963+
if h.coreService.KittyKeyboard.Flags != 0 {
1964+
t.Errorf("expected flags 0, got %d", h.coreService.KittyKeyboard.Flags)
1965+
}
1966+
})
1967+
1968+
t.Run("query_no_response_when_disabled", func(t *testing.T) {
1969+
t.Parallel()
1970+
h := newTestInputHandlerWithVtExt(80, 24, VtExtensions{})
1971+
var response string
1972+
h.coreService.OnDataEmitter.Event(func(data string) {
1973+
response = data
1974+
})
1975+
h.ParseString("\x1b[?u")
1976+
if response != "" {
1977+
t.Errorf("expected no response, got %q", response)
1978+
}
1979+
})
1980+
1981+
t.Run("push_ignored_when_disabled", func(t *testing.T) {
1982+
t.Parallel()
1983+
h := newTestInputHandlerWithVtExt(80, 24, VtExtensions{})
1984+
h.coreService.KittyKeyboard.Flags = 5
1985+
h.ParseString("\x1b[>3u")
1986+
if len(h.coreService.KittyKeyboard.MainStack) != 0 {
1987+
t.Errorf("expected empty stack, got %v", h.coreService.KittyKeyboard.MainStack)
1988+
}
1989+
if h.coreService.KittyKeyboard.Flags != 5 {
1990+
t.Errorf("expected flags unchanged at 5, got %d", h.coreService.KittyKeyboard.Flags)
1991+
}
1992+
})
1993+
1994+
t.Run("pop_ignored_when_disabled", func(t *testing.T) {
1995+
t.Parallel()
1996+
h := newTestInputHandlerWithVtExt(80, 24, VtExtensions{})
1997+
h.coreService.KittyKeyboard.Flags = 5
1998+
h.coreService.KittyKeyboard.MainStack = []int{3}
1999+
h.ParseString("\x1b[<u")
2000+
if h.coreService.KittyKeyboard.Flags != 5 {
2001+
t.Errorf("expected flags unchanged at 5, got %d", h.coreService.KittyKeyboard.Flags)
2002+
}
2003+
if len(h.coreService.KittyKeyboard.MainStack) != 1 {
2004+
t.Errorf("expected stack unchanged, got %v", h.coreService.KittyKeyboard.MainStack)
2005+
}
2006+
})
2007+
2008+
t.Run("alt_buffer_swap_skipped_when_disabled", func(t *testing.T) {
2009+
t.Parallel()
2010+
h := newTestInputHandlerWithVtExt(80, 24, VtExtensions{})
2011+
h.coreService.KittyKeyboard.Flags = 5
2012+
h.coreService.KittyKeyboard.AltFlags = 2
2013+
h.ParseString("\x1b[?1049h")
2014+
if h.coreService.KittyKeyboard.Flags != 5 {
2015+
t.Errorf("expected flags unchanged at 5, got %d", h.coreService.KittyKeyboard.Flags)
2016+
}
2017+
if h.coreService.KittyKeyboard.MainFlags != 0 {
2018+
t.Errorf("expected MainFlags unchanged at 0, got %d", h.coreService.KittyKeyboard.MainFlags)
2019+
}
2020+
})
2021+
}
2022+

inputhandler_sgr.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,15 @@ func (h *InputHandler) charAttributes(params *Params) bool {
148148

149149
case p == 221:
150150
// kitty extension: reset bold only (leaves faint intact)
151-
attr.Fg &^= FgFlagBold
151+
if h.optionsService.Options.VtExtensions.kittySgrBoldFaintControlEnabled() {
152+
attr.Fg &^= FgFlagBold
153+
}
152154

153155
case p == 222:
154156
// kitty extension: reset faint/dim only (leaves bold intact)
155-
attr.Bg &^= BgFlagDim
157+
if h.optionsService.Options.VtExtensions.kittySgrBoldFaintControlEnabled() {
158+
attr.Bg &^= BgFlagDim
159+
}
156160
}
157161
}
158162
return true

0 commit comments

Comments
 (0)