From 6277bb363c7166e2ddf541d8dbb9b57883455cef Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 31 Dec 2025 08:12:39 -0800 Subject: [PATCH 1/2] Serialize showCursor Fixes #3364 --- .../src/SerializeAddon.test.ts | 32 +++++++++++++++++++ addons/addon-serialize/src/SerializeAddon.ts | 6 ++++ src/browser/public/Terminal.ts | 1 + src/headless/public/Terminal.ts | 1 + typings/xterm-headless.d.ts | 4 +++ typings/xterm.d.ts | 4 +++ 6 files changed, 48 insertions(+) diff --git a/addons/addon-serialize/src/SerializeAddon.test.ts b/addons/addon-serialize/src/SerializeAddon.test.ts index b0718ad3..e5307e1a 100644 --- a/addons/addon-serialize/src/SerializeAddon.test.ts +++ b/addons/addon-serialize/src/SerializeAddon.test.ts @@ -190,6 +190,38 @@ describe('SerializeAddon', () => { assert.equal(buffer.scrollBottom, 3); }); }); + + describe('cursor visibility', () => { + it('should serialize hidden cursor', async () => { + await writeP(terminal, 'hello\x1b[?25l'); + assert.equal(terminal.modes.showCursor, false); + const result = serializeAddon.serialize(); + assert.ok(result.includes('\x1b[?25l'), result); + }); + + it('should not serialize visible cursor (default state)', async () => { + await writeP(terminal, 'hello'); + assert.equal(terminal.modes.showCursor, true); + const result = serializeAddon.serialize(); + assert.ok(!result.includes('\x1b[?25l'), result); + assert.ok(!result.includes('\x1b[?25h'), result); + }); + + it('should not serialize cursor visibility when excludeModes is true', async () => { + await writeP(terminal, 'hello\x1b[?25l'); + const result = serializeAddon.serialize({ excludeModes: true }); + assert.ok(!result.includes('\x1b[?25l'), result); + }); + + it('should restore hidden cursor correctly when deserialized', async () => { + await writeP(terminal, 'hello\x1b[?25l'); + const serialized = serializeAddon.serialize(); + const terminal2 = new Terminal({ cols: 10, rows: 2, allowProposedApi: true }); + terminal2.loadAddon(new SerializeAddon()); + await writeP(terminal2, serialized); + assert.equal(terminal2.modes.showCursor, false); + }); + }); }); describe('html', () => { diff --git a/addons/addon-serialize/src/SerializeAddon.ts b/addons/addon-serialize/src/SerializeAddon.ts index 35baeab7..c9c66ce8 100644 --- a/addons/addon-serialize/src/SerializeAddon.ts +++ b/addons/addon-serialize/src/SerializeAddon.ts @@ -559,6 +559,12 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi { } } + // Cursor visibility (DECTCEM) + // Default: visible + if (!modes.showCursor) { + content += '\x1b[?25l'; + } + return content; } diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 1e16d556..c872f5e1 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -124,6 +124,7 @@ export class Terminal extends Disposable implements ITerminalApi { originMode: m.origin, reverseWraparoundMode: m.reverseWraparound, sendFocusMode: m.sendFocus, + showCursor: !this._core.coreService.isCursorHidden, synchronizedOutputMode: m.synchronizedOutput, wraparoundMode: m.wraparound }; diff --git a/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts index 00acae8d..811345b9 100644 --- a/src/headless/public/Terminal.ts +++ b/src/headless/public/Terminal.ts @@ -125,6 +125,7 @@ export class Terminal extends Disposable implements ITerminalApi { originMode: m.origin, reverseWraparoundMode: m.reverseWraparound, sendFocusMode: m.sendFocus, + showCursor: !this._core.coreService.isCursorHidden, synchronizedOutputMode: m.synchronizedOutput, wraparoundMode: m.wraparound }; diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 2f304604..3c018a3b 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -1350,6 +1350,10 @@ declare module '@xterm/headless' { * Send FocusIn/FocusOut events: `CSI ? 1 0 0 4 h` */ readonly sendFocusMode: boolean; + /** + * Show Cursor (DECTCEM): `CSI ? 2 5 h` + */ + readonly showCursor: boolean; /** * Synchronized Output Mode: `CSI ? 2 0 2 6 h` * diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 6a936213..1084cae8 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1969,6 +1969,10 @@ declare module '@xterm/xterm' { * Send FocusIn/FocusOut events: `CSI ? 1 0 0 4 h` */ readonly sendFocusMode: boolean; + /** + * Show Cursor (DECTCEM): `CSI ? 2 5 h` + */ + readonly showCursor: boolean; /** * Synchronized Output Mode: `CSI ? 2 0 2 6 h` * From 44a8c7efbcf8621657a0f7e022538fb7d8146c3b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:08:05 -0800 Subject: [PATCH 2/2] Fix test expectations --- src/headless/public/Terminal.test.ts | 1 + test/playwright/Terminal.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/headless/public/Terminal.test.ts b/src/headless/public/Terminal.test.ts index 1ddd8c05..8beffc69 100644 --- a/src/headless/public/Terminal.test.ts +++ b/src/headless/public/Terminal.test.ts @@ -409,6 +409,7 @@ describe('Headless API Tests', function (): void { originMode: false, reverseWraparoundMode: false, sendFocusMode: false, + showCursor: true, synchronizedOutputMode: false, wraparoundMode: true }); diff --git a/test/playwright/Terminal.test.ts b/test/playwright/Terminal.test.ts index f086e307..f03d687d 100644 --- a/test/playwright/Terminal.test.ts +++ b/test/playwright/Terminal.test.ts @@ -605,6 +605,7 @@ test.describe('API Integration Tests', () => { originMode: false, reverseWraparoundMode: false, sendFocusMode: false, + showCursor: true, synchronizedOutputMode: false, wraparoundMode: true });