From ca5a6813636ffdf3439158e08561ea842ba5c0b4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:34:55 -0800 Subject: [PATCH 1/4] Add quirks setting to control DECSET/DECRST 12 Fixes #5314 --- src/common/InputHandler.ts | 12 ++++++++---- src/common/services/OptionsService.ts | 3 ++- src/common/services/Services.ts | 5 +++++ typings/xterm.d.ts | 21 +++++++++++++++++++++ 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index e6231725..99ae6252 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -1877,7 +1877,7 @@ export class InputHandler extends Disposable implements IInputHandler { * | 7 | Auto-wrap Mode (DECAWM). | #Y | * | 8 | Auto-repeat Keys (DECARM). Always on. | #N | * | 9 | X10 xterm mouse protocol. | #Y | - * | 12 | Start Blinking Cursor. | #Y | + * | 12 | Start Blinking Cursor. | #P[Requires the allowSetCursorBlink quirk option enabled.] | * | 25 | Show Cursor (DECTCEM). | #Y | * | 45 | Reverse wrap-around. | #Y | * | 47 | Use Alternate Screen Buffer. | #Y | @@ -1930,7 +1930,9 @@ export class InputHandler extends Disposable implements IInputHandler { this._coreService.decPrivateModes.wraparound = true; break; case 12: - this._optionsService.options.cursorBlink = true; + if (this._optionsService.rawOptions.quirks?.allowSetCursorBlink) { + this._optionsService.options.cursorBlink = true; + } break; case 45: this._coreService.decPrivateModes.reverseWraparound = true; @@ -2125,7 +2127,7 @@ export class InputHandler extends Disposable implements IInputHandler { * | 7 | No Wraparound Mode (DECAWM). | #Y | * | 8 | No Auto-repeat Keys (DECARM). | #N | * | 9 | Don't send Mouse X & Y on button press. | #Y | - * | 12 | Stop Blinking Cursor. | #Y | + * | 12 | Stop Blinking Cursor. | #P[Requires the allowSetCursorBlink quirk option enabled.] | * | 25 | Hide Cursor (DECTCEM). | #Y | * | 45 | No reverse wrap-around. | #Y | * | 47 | Use Normal Screen Buffer. | #Y | @@ -2171,7 +2173,9 @@ export class InputHandler extends Disposable implements IInputHandler { this._coreService.decPrivateModes.wraparound = false; break; case 12: - this._optionsService.options.cursorBlink = false; + if (this._optionsService.rawOptions.quirks?.allowSetCursorBlink) { + this._optionsService.options.cursorBlink = false; + } break; case 45: this._coreService.decPrivateModes.reverseWraparound = false; diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index b33a0856..54c3db23 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -53,7 +53,8 @@ export const DEFAULT_OPTIONS: Readonly> = { convertEol: false, termName: 'xterm', cancelEvents: false, - overviewRuler: {} + overviewRuler: {}, + quirks: {} }; const FONT_WEIGHT_OPTIONS: Extract[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 849abcd5..a73d99f3 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -262,6 +262,7 @@ export interface ITerminalOptions { windowOptions?: IWindowOptions; wordSeparator?: string; overviewRuler?: IOverviewRulerOptions; + quirks?: ITerminalQuirks; scrollOnEraseInDisplay?: boolean; [key: string]: any; @@ -300,6 +301,10 @@ export interface ITheme { extendedAnsi?: string[]; } +export interface ITerminalQuirks { + allowSetCursorBlink?: boolean; +} + export const IOscLinkService = createDecorator('OscLinkService'); export interface IOscLinkService { serviceBrand: undefined; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 8e34a00e..a5d82601 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -197,6 +197,12 @@ declare module '@xterm/xterm' { */ minimumContrastRatio?: number; + /** + * Control various quirks features that are either non-standard or standard + * in but generally rejected in modern terminals. + */ + quirks?: ITerminalQuirks; + /** * Whether to reflow the line containing the cursor when the terminal is * resized. Defaults to false, because shells usually handle this @@ -406,6 +412,21 @@ declare module '@xterm/xterm' { extendedAnsi?: string[]; } + /** + * Control various quirks features that are either non-standard or standard + * in but generally rejected in modern terminals. + */ + export interface ITerminalQuirks { + /** + * Enables support for DECSET 12 and DECRST 12 which controls cursor blink. + * Programs such as `vim` may use this to set the cursor blink state but may + * not change it back when exiting. Generally the terminal emulator should + * be in control of whether the cursor blinks or not and the application in + * modern terminals. Note that DECRQM works regardless of this option. + */ + allowSetCursorBlink?: boolean; + } + /** * Pty information for Windows. */ From ab4524873b00023f687677d917aa41a66bc89c26 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:49:12 -0800 Subject: [PATCH 2/4] Add test for DECSET quirks --- src/common/InputHandler.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 6788d3f9..afc68c1e 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -2341,7 +2341,7 @@ describe('InputHandler', () => { }); it('DEC privates with set/reset semantic', async () => { // initially reset - const reset = [1, 6, 9, 12, 45, 66, 1000, 1002, 1003, 1004, 1006, 1016, 47, 1047, 1049, 2004, 2026]; + const reset = [1, 6, 9, 45, 66, 1000, 1002, 1003, 1004, 1006, 1016, 47, 1047, 1049, 2004, 2026]; for (const mode of reset) { await inputHandler.parseP(`\x1b[?${mode}$p`); assert.deepEqual(reportStack.pop(), `\x1b[?${mode};2$y`); // initial reset @@ -2365,6 +2365,23 @@ describe('InputHandler', () => { assert.deepEqual(reportStack.pop(), `\x1b[?${mode};1$y`); // again set } }); + it('DEC privates quirks', async () => { + // Cursor blink + const mode = 12; + await inputHandler.parseP(`\x1b[?${mode}$p`); + assert.deepEqual(reportStack.pop(), `\x1b[?${mode};2$y`); // initial reset + await inputHandler.parseP(`\x1b[?${mode}h`); + await inputHandler.parseP(`\x1b[?${mode}$p`); + assert.deepEqual(reportStack.pop(), `\x1b[?${mode};2$y`); // still reset + + optionsService.options.quirks.allowSetCursorBlink = true; + await inputHandler.parseP(`\x1b[?${mode}h`); + await inputHandler.parseP(`\x1b[?${mode}$p`); + assert.deepEqual(reportStack.pop(), `\x1b[?${mode};1$y`); // now active + await inputHandler.parseP(`\x1b[?${mode}l`); + await inputHandler.parseP(`\x1b[?${mode}$p`); + assert.deepEqual(reportStack.pop(), `\x1b[?${mode};2$y`); // now inactive + }); it('DEC privates perma modes', async () => { // [mode number, state value] const perma = [[3, 0], [8, 3], [67, 4], [1005, 4], [1015, 4], [1048, 1]]; From 2a283a307c9e60de16282d25adb017f96148f193 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:56:31 -0800 Subject: [PATCH 3/4] Fix lint --- src/common/InputHandler.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index afc68c1e..d21e674f 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -2373,7 +2373,7 @@ describe('InputHandler', () => { await inputHandler.parseP(`\x1b[?${mode}h`); await inputHandler.parseP(`\x1b[?${mode}$p`); assert.deepEqual(reportStack.pop(), `\x1b[?${mode};2$y`); // still reset - + optionsService.options.quirks.allowSetCursorBlink = true; await inputHandler.parseP(`\x1b[?${mode}h`); await inputHandler.parseP(`\x1b[?${mode}$p`); From 52b6710b19a8c7698fb20269b7470380737eadf9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 28 Dec 2025 08:01:56 -0800 Subject: [PATCH 4/4] Hide quirks from demo for now --- demo/client/components/window/optionsWindow.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/client/components/window/optionsWindow.ts b/demo/client/components/window/optionsWindow.ts index 18c31410..0fbcf339 100644 --- a/demo/client/components/window/optionsWindow.ts +++ b/demo/client/components/window/optionsWindow.ts @@ -72,6 +72,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { 'linkHandler', 'logger', 'overviewRuler', + 'quirks', 'theme', 'windowOptions', 'windowsPty',