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', diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 6788d3f9..d21e674f 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]]; 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 ee9a3c35..6afdf48e 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 @@ -407,6 +413,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. */