Files
xterm.js/src/common/InputHandler.test.ts
T
Daniel ImmsandCursor 564113e053 Add unit tests for parser reset, buffer scroll, ConPTY, and search (#5988)
* Add InputHandler unit tests for ISO-2022 charset mapping

InputHandler unit tests always used MockCharsetService, so regressions in
print()'s ASCII remapping, G0/G1 designation (ESC ( ) * +), and SO/SI
level switching were never caught at unit level.

Add an ISO-2022 character sets suite wired to real CharsetService and
BufferService that verifies DEC line drawing on G0 (ESC ( 0), G1 after
ESC ) 0 + SO, and charset/glevel restoration via ESC 7 / ESC 8 before
printing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add BufferService.scroll unit tests for isUserScrolling

BufferService.scroll() adjusts ybase and ydisp differently depending on
whether the circular buffer is full and whether isUserScrolling is set,
but no unit test asserted those branches. InputHandler and other callers
use MockBufferService.scroll, which throws, so real scroll viewport logic
was only exercised indirectly.

Add BufferService.test.ts covering full-buffer scroll with the user
scrolled up (ydisp decrements), partial-buffer scroll while scrolled up
(ydisp unchanged), and full-buffer scroll at the viewport bottom
(ydisp tracks ybase).

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add InputHandler unit tests for DECOM cursor positioning

CUP with DEC origin mode (DECOM) offsets rows by the scroll region top
and clamps within DECSTBM margins, but unit tests only toggled the mode
for save/restore. Playwright covered the combination; headless unit tests
did not.

Add a cursor positioning test matching the Playwright DECOM case: with
margins and origin on, CUP places the cursor relative to scrollTop, clamps
to the margin, and uses absolute rows when origin is off.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add WindowsMode unit tests for wrap heuristic edges

ConPTY does not set isWrapped on soft-wrapped lines; updateWindowsModeWrappedState
infers wrapping from the last cell of the previous row. Browser Terminal tests
exercise this only through full writes, with no direct coverage of the helper
or negative cases (trailing space or null).

Add WindowsMode.test.ts asserting isWrapped is set for a full non-space line,
cleared for trailing whitespace, and cleared when the previous line ends empty.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add OscParser.reset unit test for mid-payload abort

OscParser.reset() must call end(false) on active handlers when aborted
during PAYLOAD, but no test invoked reset() directly. EscapeSequenceParser
tests mock OSC, so production reset/cleanup was unverified at unit level.

Add a reset describe block asserting handlers receive END false after a
partial payload, then a subsequent OSC sequence completes normally.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add DcsParser.reset unit test for mid-payload abort

DcsParser.reset() must call unhook(false) on hooked handlers when aborted
mid-DCS, matching OscParser's end(false) cleanup. No DcsParser test called
reset() directly, leaving the abort path unverified at unit level.

Add a reset describe block asserting UNHOOK false after partial put, then a
subsequent DCS sequence completes with UNHOOK true.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add InputHandler print tests for grapheme joining

print() uses UnicodeService.shouldJoin to merge combining marks, but
InputHandler tests never exercised that path. The U+200B infinite-loop
regression test used mocks and had no assertions, so it would pass even
if print() misbehaved.

Add tests for e + combining acute in one parse and across two parses.
Strengthen the U+200B regression test to use BufferService and assert the
buffer does not grow spurious rows.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add CharsetService unit tests for glevel and designation

CharsetService only updates the active charset when setgCharset targets the
current glevel; most InputHandler tests use MockCharsetService instead. A
regression in setgLevel or inactive-slot designation would not be caught
without direct service tests.

Add CharsetService.test.ts covering inactive designation, setgLevel
activation, active-slot updates, and reset clearing state.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add reflowCursorLine unit tests for ConPTY resize behavior

reflowCursorLine lets resize reflow wrapped lines that contain the cursor
(needed when ConPTY does not). No test referenced the option or
reflowLargerGetLinesToRemove's cursor skip branch.

Add BufferReflow tests for skip vs reflow when the cursor is inside a
wrapped block, and Buffer.resize tests showing merge on widen only when
reflowCursorLine is enabled.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add EscapeSequenceParser reset test for async pause recovery

reset() sets ParserStackType.RESET so the next parse continues in GROUND
at chunkPos + 1 without resuming a paused async handler. Existing async
tests only verified reset lifts FAIL after improper continuation, not
mid-chunk recovery with trailing printable data.

Add a test that pauses on async CSI, calls reset(), then continues parsing
and asserts XY is printed once and the stack returns to NONE.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add Buffer.resize unit test for ConPTY row growth

When rows increase on Windows ConPTY, Buffer.resize appends lines without
decrementing ybase or ydisp so the viewport stays stable while ConPTY
repaints. Buffer tests covered the non-Windows scroll-up path but never
passed windowsPty into options.

Add a ConPTY row-growth test asserting ybase, ydisp, and line count when
windowsPty is configured.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add Buffer reflow test for ConPTY buildNumber threshold

Buffer disables reflow on ConPTY when buildNumber is below 21376, but no
unit test asserted that gate. Column shrink without reflow leaves wrapped
content on a single line instead of splitting across rows.

Add a test comparing shrink behavior at build 21375 versus 21376, plus
retain unwrap-on-widen coverage for supported ConPTY builds.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add ApcParser.reset unit test for mid-payload abort

ApcParser.reset() calls end(false) on active handlers when aborted mid-APC,
matching OscParser and DcsParser, but no ApcParser test invoked reset()
directly.

Add a reset describe block asserting END false after partial put and a
clean subsequent APC sequence.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add InputHandler unit test for REP with grapheme clusters

repeatPrecedingCharacter uses parser.precedingJoinState and reprints the
full cluster from the buffer, but only Playwright covered e + combining
acute with CSI b. A regression could break REP for joined sequences without
failing unit tests.

Add a print-suite test asserting CSI 2 b triples e + acute in one parse.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add InputHandler unit tests for REP no-op cases

repeatPrecedingCharacter returns early when parser.precedingJoinState is
zero, including after another escape sequence clears join state. Only the
positive grapheme REP case was unit tested.

Add tests for bare CSI b and for REP following SGR so regressions cannot
spuriously duplicate buffer content.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add DecorationManager unit test for wrapped search matches

DecorationManager splits matches across wrapped rows with per-segment x
and width, but only SearchEngine had unit coverage; decoration geometry
was left to Playwright.

Add DecorationManager.test.ts using a real Terminal, a wrapped-buffer
match from SearchEngine, and assertions on registerDecoration options
for both row segments.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add BufferService.scroll unit test for DECSTBM margins

When scrollTop is non-zero, scroll() shifts lines inside the margin with
shiftElements instead of pushing scrollback. Existing BufferService tests
only covered scrollTop === 0.

Add a test with margins on rows 1–3 that verifies rows outside the region
stay fixed and lines inside the region scroll correctly.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add BufferService.scrollLines unit tests

scrollLines adjusts ydisp and toggles isUserScrolling for viewport
scrolling, but only scroll() had unit coverage. Regressions in the
user-scroll flag or ydisp clamping would affect the whole viewport.

Add tests for scrolling up, clamping at ydisp 0, and clearing
isUserScrolling when reaching the buffer bottom.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add DecorationManager overview ruler dedup unit test

Multiple highlight matches on the same buffer line should only register
one overview ruler marker; later matches on that line omit ruler options.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Assert overview ruler on each line of wrapped search match

Wrapped highlight decorations span two buffer lines; each segment
should register overview ruler options unlike same-line dedup.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 12:41:54 +00:00

2637 lines
124 KiB
TypeScript

/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { InputHandler } from './InputHandler';
import { IColorEvent, ColorRequestType, SpecialColorIndex } from './Types';
import { IAttributeData, IBufferLine } from './buffer/Types';
import { DEFAULT_ATTR_DATA } from './buffer/BufferLine';
import { CellData } from './buffer/CellData';
import { Attributes, BgFlags, UnderlineStyle } from './buffer/Constants';
import { AttributeData, ExtendedAttrs } from './buffer/AttributeData';
import { Params } from './parser/Params';
import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockMouseStateService, MockCharsetService, MockUnicodeService, MockOscLinkService, extendedAttributes } from './TestUtils.test';
import { IBufferService, ICoreService, type IOscLinkService } from './services/Services';
import { DEFAULT_OPTIONS } from './services/OptionsService';
import { BufferService } from './services/BufferService';
import { CharsetService } from './services/CharsetService';
import { CoreService } from './services/CoreService';
import { OscLinkService } from './services/OscLinkService';
import { CHARSETS } from './data/Charsets';
function getCursor(bufferService: IBufferService): number[] {
return [
bufferService.buffer.x,
bufferService.buffer.y
];
}
function getLines(bufferService: IBufferService, limit: number = bufferService.rows): string[] {
const res: string[] = [];
for (let i = 0; i < limit; ++i) {
const line = bufferService.buffer.lines.get(i);
if (line) {
res.push(line.translateToString(true));
}
}
return res;
}
class TestInputHandler extends InputHandler {
public get curAttrData(): IAttributeData { return (this as any)._curAttrData; }
public get windowTitleStack(): string[] { return this._windowTitleStack; }
public get iconNameStack(): string[] { return this._iconNameStack; }
/**
* Promise based parse call to await the full resolve of given input data.
* This is useful to test async handlers in inputhandler directly.
*/
public async parseP(data: string | Uint8Array): Promise<void> {
let result: Promise<boolean> | void;
let prev: boolean | undefined;
while (result = this.parse(data, prev)) {
prev = await result;
}
}
}
describe('InputHandler', () => {
let bufferService: IBufferService;
let coreService: ICoreService;
let optionsService: MockOptionsService;
let oscLinkService: IOscLinkService;
let inputHandler: TestInputHandler;
beforeEach(() => {
optionsService = new MockOptionsService();
bufferService = new BufferService(optionsService, new MockLogService());
bufferService.resize(80, 30);
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
oscLinkService = new OscLinkService(bufferService);
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, oscLinkService, new MockMouseStateService(), new MockUnicodeService());
});
describe('SL/SR/DECIC/DECDC', () => {
beforeEach(() => {
bufferService.resize(5, 5);
optionsService.options.scrollback = 1;
bufferService.reset();
});
it('SL (scrollLeft)', async () => {
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[ @');
assert.deepEqual(getLines(bufferService, 6), ['12345', '2345', '2345', '2345', '2345', '2345']);
await inputHandler.parseP('\x1b[0 @');
assert.deepEqual(getLines(bufferService, 6), ['12345', '345', '345', '345', '345', '345']);
await inputHandler.parseP('\x1b[2 @');
assert.deepEqual(getLines(bufferService, 6), ['12345', '5', '5', '5', '5', '5']);
});
it('SR (scrollRight)', async () => {
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[ A');
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']);
await inputHandler.parseP('\x1b[0 A');
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']);
await inputHandler.parseP('\x1b[2 A');
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']);
});
it('insertColumns (DECIC)', async () => {
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[3;3H');
await inputHandler.parseP('\x1b[\'}');
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
bufferService.reset();
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[3;3H');
await inputHandler.parseP('\x1b[1\'}');
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
bufferService.reset();
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[3;3H');
await inputHandler.parseP('\x1b[2\'}');
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']);
});
it('deleteColumns (DECDC)', async () => {
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[3;3H');
await inputHandler.parseP('\x1b[\'~');
assert.deepEqual(getLines(bufferService, 6), ['12345', '1245', '1245', '1245', '1245', '1245']);
bufferService.reset();
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[3;3H');
await inputHandler.parseP('\x1b[1\'~');
assert.deepEqual(getLines(bufferService, 6), ['12345', '1245', '1245', '1245', '1245', '1245']);
bufferService.reset();
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[3;3H');
await inputHandler.parseP('\x1b[2\'~');
assert.deepEqual(getLines(bufferService, 6), ['12345', '125', '125', '125', '125', '125']);
});
});
describe('BS with reverseWraparound set/unset', () => {
const ttyBS = '\x08 \x08'; // tty ICANON sends <BS SP BS> on pressing BS
beforeEach(() => {
bufferService.resize(5, 5);
optionsService.options.scrollback = 1;
bufferService.reset();
});
describe('reverseWraparound set', () => {
it('should not reverse outside of scroll margins', async () => {
// prepare buffer content
await inputHandler.parseP('#####abcdefghijklmnopqrstuvwxy');
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
assert.equal(bufferService.buffers.active.ydisp, 1);
assert.equal(bufferService.buffers.active.x, 5);
assert.equal(bufferService.buffers.active.y, 4);
await inputHandler.parseP(ttyBS.repeat(100));
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']);
await inputHandler.parseP('\x1b[?45h');
await inputHandler.parseP('uvwxy');
// set top/bottom to 1/3 (0-based)
await inputHandler.parseP('\x1b[2;4r');
// place cursor below scroll bottom
bufferService.buffers.active.x = 5;
bufferService.buffers.active.y = 4;
await inputHandler.parseP(ttyBS.repeat(100));
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']);
await inputHandler.parseP('uvwxy');
// place cursor within scroll margins
bufferService.buffers.active.x = 5;
bufferService.buffers.active.y = 3;
await inputHandler.parseP(ttyBS.repeat(100));
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']);
assert.equal(bufferService.buffers.active.x, 0);
assert.equal(bufferService.buffers.active.y, bufferService.buffers.active.scrollTop); // stops at 0, scrollTop
await inputHandler.parseP('fghijklmnopqrst');
// place cursor above scroll top
bufferService.buffers.active.x = 5;
bufferService.buffers.active.y = 0;
await inputHandler.parseP(ttyBS.repeat(100));
assert.deepEqual(getLines(bufferService, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
});
});
});
it('save and restore cursor', () => {
bufferService.buffer.x = 1;
bufferService.buffer.y = 2;
bufferService.buffer.ybase = 0;
inputHandler.curAttrData.fg = 3;
// Save cursor position
inputHandler.saveCursor();
assert.equal(bufferService.buffer.x, 1);
assert.equal(bufferService.buffer.y, 2);
assert.equal(inputHandler.curAttrData.fg, 3);
// Change cursor position
bufferService.buffer.x = 10;
bufferService.buffer.y = 20;
inputHandler.curAttrData.fg = 30;
// Restore cursor position
inputHandler.restoreCursor();
assert.equal(bufferService.buffer.x, 1);
assert.equal(bufferService.buffer.y, 2);
assert.equal(inputHandler.curAttrData.fg, 3);
});
describe('DECSC/DECRC - save and restore cursor', () => {
it('should save and restore origin mode', async () => {
assert.equal(coreService.decPrivateModes.origin, false);
await inputHandler.parseP('\x1b[?6h');
assert.equal(coreService.decPrivateModes.origin, true);
await inputHandler.parseP('\x1b7');
await inputHandler.parseP('\x1b[?6l');
assert.equal(coreService.decPrivateModes.origin, false);
await inputHandler.parseP('\x1b8');
assert.equal(coreService.decPrivateModes.origin, true);
});
it('should save and restore wraparound mode', async () => {
assert.equal(coreService.decPrivateModes.wraparound, true);
await inputHandler.parseP('\x1b[?7l');
assert.equal(coreService.decPrivateModes.wraparound, false);
await inputHandler.parseP('\x1b7');
await inputHandler.parseP('\x1b[?7h');
assert.equal(coreService.decPrivateModes.wraparound, true);
await inputHandler.parseP('\x1b8');
assert.equal(coreService.decPrivateModes.wraparound, false);
});
});
describe('setCursorStyle', () => {
it('should call Terminal.setOption with correct params', () => {
inputHandler.setCursorStyle(Params.fromArray([0]));
assert.equal(coreService.decPrivateModes.cursorStyle, undefined);
assert.equal(coreService.decPrivateModes.cursorBlink, undefined);
optionsService.options = structuredClone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([1]));
assert.equal(coreService.decPrivateModes.cursorStyle, 'block');
assert.equal(coreService.decPrivateModes.cursorBlink, true);
optionsService.options = structuredClone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([2]));
assert.equal(coreService.decPrivateModes.cursorStyle, 'block');
assert.equal(coreService.decPrivateModes.cursorBlink, false);
optionsService.options = structuredClone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([3]));
assert.equal(coreService.decPrivateModes.cursorStyle, 'underline');
assert.equal(coreService.decPrivateModes.cursorBlink, true);
optionsService.options = structuredClone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([4]));
assert.equal(coreService.decPrivateModes.cursorStyle, 'underline');
assert.equal(coreService.decPrivateModes.cursorBlink, false);
optionsService.options = structuredClone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([5]));
assert.equal(coreService.decPrivateModes.cursorStyle, 'bar');
assert.equal(coreService.decPrivateModes.cursorBlink, true);
optionsService.options = structuredClone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([6]));
assert.equal(coreService.decPrivateModes.cursorStyle, 'bar');
assert.equal(coreService.decPrivateModes.cursorBlink, false);
});
});
describe('setMode', () => {
it('should toggle bracketedPasteMode', () => {
const coreService = new MockCoreService();
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
// Set bracketed paste mode
inputHandler.setModePrivate(Params.fromArray([2004]));
assert.equal(coreService.decPrivateModes.bracketedPasteMode, true);
// Reset bracketed paste mode
inputHandler.resetModePrivate(Params.fromArray([2004]));
assert.equal(coreService.decPrivateModes.bracketedPasteMode, false);
});
it('should toggle colorSchemeUpdates (DECSET 2031)', () => {
const coreService = new MockCoreService();
const optionsService = new MockOptionsService();
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
// Set color scheme updates mode (default colorSchemeQuery=true)
inputHandler.setModePrivate(Params.fromArray([2031]));
assert.equal(coreService.decPrivateModes.colorSchemeUpdates, true);
// Reset color scheme updates mode
inputHandler.resetModePrivate(Params.fromArray([2031]));
assert.equal(coreService.decPrivateModes.colorSchemeUpdates, false);
});
it('should not toggle colorSchemeUpdates when colorSchemeQuery is disabled', () => {
const coreService = new MockCoreService();
const optionsService = new MockOptionsService();
optionsService.rawOptions.vtExtensions = { colorSchemeQuery: false };
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
// Attempt to set color scheme updates mode
inputHandler.setModePrivate(Params.fromArray([2031]));
assert.equal(coreService.decPrivateModes.colorSchemeUpdates, false);
});
});
describe('regression tests', function (): void {
function termContent(bufferService: IBufferService, trim: boolean): string[] {
const result = [];
for (let i = 0; i < bufferService.rows; ++i) result.push(bufferService.buffer.lines.get(i)!.translateToString(trim));
return result;
}
it('insertChars', async () => {
const bufferService = new MockBufferService(80, 30);
const inputHandler = new TestInputHandler(
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockMouseStateService(),
new MockUnicodeService()
);
// insert some data in first and second line
await inputHandler.parseP('a'.repeat(bufferService.cols - 10));
await inputHandler.parseP('1234567890');
await inputHandler.parseP('a'.repeat(bufferService.cols - 10));
await inputHandler.parseP('1234567890');
const line1: IBufferLine = bufferService.buffer.lines.get(0)!;
assert.equal(line1.translateToString(false), 'a'.repeat(bufferService.cols - 10) + '1234567890');
// insert one char from params = [0]
bufferService.buffer.y = 0;
bufferService.buffer.x = 70;
inputHandler.insertChars(Params.fromArray([0]));
assert.equal(line1.translateToString(false), 'a'.repeat(bufferService.cols - 10) + ' 123456789');
// insert one char from params = [1]
bufferService.buffer.y = 0;
bufferService.buffer.x = 70;
inputHandler.insertChars(Params.fromArray([1]));
assert.equal(line1.translateToString(false), 'a'.repeat(bufferService.cols - 10) + ' 12345678');
// insert two chars from params = [2]
bufferService.buffer.y = 0;
bufferService.buffer.x = 70;
inputHandler.insertChars(Params.fromArray([2]));
assert.equal(line1.translateToString(false), 'a'.repeat(bufferService.cols - 10) + ' 123456');
// insert 10 chars from params = [10]
bufferService.buffer.y = 0;
bufferService.buffer.x = 70;
inputHandler.insertChars(Params.fromArray([10]));
assert.equal(line1.translateToString(false), 'a'.repeat(bufferService.cols - 10) + ' ');
assert.equal(line1.translateToString(true), 'a'.repeat(bufferService.cols - 10));
});
it('deleteChars', async () => {
const bufferService = new MockBufferService(80, 30);
const inputHandler = new TestInputHandler(
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockMouseStateService(),
new MockUnicodeService()
);
// insert some data in first and second line
await inputHandler.parseP('a'.repeat(bufferService.cols - 10));
await inputHandler.parseP('1234567890');
await inputHandler.parseP('a'.repeat(bufferService.cols - 10));
await inputHandler.parseP('1234567890');
const line1: IBufferLine = bufferService.buffer.lines.get(0)!;
assert.equal(line1.translateToString(false), 'a'.repeat(bufferService.cols - 10) + '1234567890');
// delete one char from params = [0]
bufferService.buffer.y = 0;
bufferService.buffer.x = 70;
inputHandler.deleteChars(Params.fromArray([0]));
assert.equal(line1.translateToString(false), 'a'.repeat(bufferService.cols - 10) + '234567890 ');
assert.equal(line1.translateToString(true), 'a'.repeat(bufferService.cols - 10) + '234567890');
// insert one char from params = [1]
bufferService.buffer.y = 0;
bufferService.buffer.x = 70;
inputHandler.deleteChars(Params.fromArray([1]));
assert.equal(line1.translateToString(false), 'a'.repeat(bufferService.cols - 10) + '34567890 ');
assert.equal(line1.translateToString(true), 'a'.repeat(bufferService.cols - 10) + '34567890');
// insert two chars from params = [2]
bufferService.buffer.y = 0;
bufferService.buffer.x = 70;
inputHandler.deleteChars(Params.fromArray([2]));
assert.equal(line1.translateToString(false), 'a'.repeat(bufferService.cols - 10) + '567890 ');
assert.equal(line1.translateToString(true), 'a'.repeat(bufferService.cols - 10) + '567890');
// insert 10 chars from params = [10]
bufferService.buffer.y = 0;
bufferService.buffer.x = 70;
inputHandler.deleteChars(Params.fromArray([10]));
assert.equal(line1.translateToString(false), 'a'.repeat(bufferService.cols - 10) + ' ');
assert.equal(line1.translateToString(true), 'a'.repeat(bufferService.cols - 10));
});
it('eraseInLine', async () => {
const bufferService = new MockBufferService(80, 30);
const inputHandler = new TestInputHandler(
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockMouseStateService(),
new MockUnicodeService()
);
// fill 6 lines to test 3 different states
await inputHandler.parseP('a'.repeat(bufferService.cols));
await inputHandler.parseP('a'.repeat(bufferService.cols));
await inputHandler.parseP('a'.repeat(bufferService.cols));
// params[0] - right erase
bufferService.buffer.y = 0;
bufferService.buffer.x = 70;
inputHandler.eraseInLine(Params.fromArray([0]));
assert.equal(bufferService.buffer.lines.get(0)!.translateToString(false), 'a'.repeat(70) + ' ');
// params[1] - left erase
bufferService.buffer.y = 1;
bufferService.buffer.x = 70;
inputHandler.eraseInLine(Params.fromArray([1]));
assert.equal(bufferService.buffer.lines.get(1)!.translateToString(false), ' '.repeat(70) + ' aaaaaaaaa');
// params[1] - left erase
bufferService.buffer.y = 2;
bufferService.buffer.x = 70;
inputHandler.eraseInLine(Params.fromArray([2]));
assert.equal(bufferService.buffer.lines.get(2)!.translateToString(false), ' '.repeat(bufferService.cols));
});
it('eraseInLine reflow', async () => {
const bufferService = new MockBufferService(80, 30);
const inputHandler = new TestInputHandler(
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockMouseStateService(),
new MockUnicodeService()
);
const resetToBaseState = async (): Promise<void> => {
// reset and add a wrapped line
bufferService.buffer.y = 0;
bufferService.buffer.x = 0;
await inputHandler.parseP('a'.repeat(bufferService.cols)); // line 0
await inputHandler.parseP('a'.repeat(bufferService.cols + 9)); // line 1 and 2
for (let i = 3; i < bufferService.rows; ++i) await inputHandler.parseP('a'.repeat(bufferService.cols));
// confirm precondition that line 2 is wrapped
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, true);
};
// params[0] - erase from the cursor through the end of the row.
await resetToBaseState();
bufferService.buffer.y = 2;
bufferService.buffer.x = 40;
inputHandler.eraseInLine(Params.fromArray([0]));
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, true);
bufferService.buffer.y = 2;
bufferService.buffer.x = 0;
inputHandler.eraseInLine(Params.fromArray([0]));
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, false);
// params[1] - erase from the beginning of the line through the cursor
await resetToBaseState();
bufferService.buffer.y = 2;
bufferService.buffer.x = 40;
inputHandler.eraseInLine(Params.fromArray([1]));
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, true);
// params[2] - erase complete line
await resetToBaseState();
bufferService.buffer.y = 2;
bufferService.buffer.x = 40;
inputHandler.eraseInLine(Params.fromArray([2]));
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, false);
});
it('ED2 with scrollOnEraseInDisplay turned on', async () => {
const inputHandler = new TestInputHandler(
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockLogService(),
new MockOptionsService({ scrollOnEraseInDisplay: true }),
new MockOscLinkService(),
new MockMouseStateService(),
new MockUnicodeService()
);
const aLine = 'a'.repeat(bufferService.cols);
// add 2 full lines of text.
await inputHandler.parseP(aLine);
await inputHandler.parseP(aLine);
inputHandler.eraseInDisplay(Params.fromArray([2]));
// those 2 lines should have been pushed to scrollback.
assert.equal(bufferService.rows + 2, bufferService.buffer.lines.length);
assert.equal(bufferService.buffer.ybase, 2);
assert.equal(bufferService.buffer.lines.get(0)?.translateToString(), aLine);
assert.equal(bufferService.buffer.lines.get(1)?.translateToString(), aLine);
// Move to last line and add more text.
bufferService.buffer.y = bufferService.rows - 1;
bufferService.buffer.x = 0;
await inputHandler.parseP(aLine);
inputHandler.eraseInDisplay(Params.fromArray([2]));
// Screen should have been scrolled by a full screen size.
assert.equal(bufferService.rows * 2 + 2, bufferService.buffer.lines.length);
});
it('eraseInDisplay', async () => {
const bufferService = new MockBufferService(80, 7);
const inputHandler = new TestInputHandler(
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockMouseStateService(),
new MockUnicodeService()
);
// fill display with a's
for (let i = 0; i < bufferService.rows; ++i) await inputHandler.parseP('a'.repeat(bufferService.cols));
// params [0] - right and below erase
bufferService.buffer.y = 5;
bufferService.buffer.x = 40;
inputHandler.eraseInDisplay(Params.fromArray([0]));
assert.deepEqual(termContent(bufferService, false), [
'a'.repeat(bufferService.cols),
'a'.repeat(bufferService.cols),
'a'.repeat(bufferService.cols),
'a'.repeat(bufferService.cols),
'a'.repeat(bufferService.cols),
'a'.repeat(40) + ' '.repeat(bufferService.cols - 40),
' '.repeat(bufferService.cols)
]);
assert.deepEqual(termContent(bufferService, true), [
'a'.repeat(bufferService.cols),
'a'.repeat(bufferService.cols),
'a'.repeat(bufferService.cols),
'a'.repeat(bufferService.cols),
'a'.repeat(bufferService.cols),
'a'.repeat(40),
''
]);
// reset
bufferService.buffer.y = 0;
bufferService.buffer.x = 0;
for (let i = 0; i < bufferService.rows; ++i) await inputHandler.parseP('a'.repeat(bufferService.cols));
// params [1] - left and above
bufferService.buffer.y = 5;
bufferService.buffer.x = 40;
inputHandler.eraseInDisplay(Params.fromArray([1]));
assert.deepEqual(termContent(bufferService, false), [
' '.repeat(bufferService.cols),
' '.repeat(bufferService.cols),
' '.repeat(bufferService.cols),
' '.repeat(bufferService.cols),
' '.repeat(bufferService.cols),
' '.repeat(41) + 'a'.repeat(bufferService.cols - 41),
'a'.repeat(bufferService.cols)
]);
assert.deepEqual(termContent(bufferService, true), [
'',
'',
'',
'',
'',
' '.repeat(41) + 'a'.repeat(bufferService.cols - 41),
'a'.repeat(bufferService.cols)
]);
// reset
bufferService.buffer.y = 0;
bufferService.buffer.x = 0;
for (let i = 0; i < bufferService.rows; ++i) await inputHandler.parseP('a'.repeat(bufferService.cols));
// params [2] - whole screen
bufferService.buffer.y = 5;
bufferService.buffer.x = 40;
inputHandler.eraseInDisplay(Params.fromArray([2]));
assert.deepEqual(termContent(bufferService, false), [
' '.repeat(bufferService.cols),
' '.repeat(bufferService.cols),
' '.repeat(bufferService.cols),
' '.repeat(bufferService.cols),
' '.repeat(bufferService.cols),
' '.repeat(bufferService.cols),
' '.repeat(bufferService.cols)
]);
assert.deepEqual(termContent(bufferService, true), [
'',
'',
'',
'',
'',
'',
''
]);
// reset and add a wrapped line
bufferService.buffer.y = 0;
bufferService.buffer.x = 0;
await inputHandler.parseP('a'.repeat(bufferService.cols)); // line 0
await inputHandler.parseP('a'.repeat(bufferService.cols + 9)); // line 1 and 2
for (let i = 3; i < bufferService.rows; ++i) await inputHandler.parseP('a'.repeat(bufferService.cols));
// params[1] left and above with wrap
// confirm precondition that line 2 is wrapped
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, true);
bufferService.buffer.y = 2;
bufferService.buffer.x = 40;
inputHandler.eraseInDisplay(Params.fromArray([1]));
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, false);
// reset and add a wrapped line
bufferService.buffer.y = 0;
bufferService.buffer.x = 0;
await inputHandler.parseP('a'.repeat(bufferService.cols)); // line 0
await inputHandler.parseP('a'.repeat(bufferService.cols + 9)); // line 1 and 2
for (let i = 3; i < bufferService.rows; ++i) await inputHandler.parseP('a'.repeat(bufferService.cols));
// params[1] left and above with wrap
// confirm precondition that line 2 is wrapped
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, true);
bufferService.buffer.y = 1;
bufferService.buffer.x = 90; // Cursor is beyond last column
inputHandler.eraseInDisplay(Params.fromArray([1]));
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, false);
});
});
describe('print', () => {
it('should not cause an infinite loop (regression test)', () => {
const container = new Uint32Array(10);
container[0] = 0x200B;
const lineCountBefore = bufferService.buffer.lines.length;
inputHandler.print(container, 0, 1);
assert.strictEqual(bufferService.buffer.y, 0);
assert.strictEqual(bufferService.buffer.lines.length, lineCountBefore);
});
it('should join combining characters in a single print', async () => {
await inputHandler.parseP('e\u0301');
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), 'e\u0301');
assert.strictEqual(bufferService.buffer.x, 1);
});
it('should join combining characters split across parse calls', async () => {
await inputHandler.parseP('e');
await inputHandler.parseP('\u0301');
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), 'e\u0301');
assert.strictEqual(bufferService.buffer.x, 1);
});
it('should repeat preceding grapheme cluster via REP', async () => {
await inputHandler.parseP('e\u0301\x1b[2b');
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), 'e\u0301e\u0301e\u0301');
assert.strictEqual(bufferService.buffer.x, 3);
});
it('should not repeat when REP has no preceding join state', async () => {
await inputHandler.parseP('\x1b[2b');
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), '');
assert.strictEqual(bufferService.buffer.x, 0);
});
it('should not repeat after an intervening escape sequence', async () => {
await inputHandler.parseP('a\x1b[0m\x1b[2b');
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), 'a');
assert.strictEqual(bufferService.buffer.x, 1);
});
it('should clear cells to the right on early wrap-around', async () => {
bufferService.resize(5, 5);
optionsService.options.scrollback = 1;
await inputHandler.parseP('12345');
bufferService.buffer.x = 0;
await inputHandler.parseP('¥¥¥');
assert.deepEqual(getLines(bufferService, 2), ['¥¥', '¥']);
});
it('should strip soft hyphens (U+00AD)', async () => {
await inputHandler.parseP('Soft\xadhy\xadphen');
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), 'Softhyphen');
assert.strictEqual(bufferService.buffer.x, 10);
});
});
describe('ISO-2022 character sets', () => {
let charsetService: CharsetService;
beforeEach(() => {
charsetService = new CharsetService();
inputHandler = new TestInputHandler(
bufferService,
charsetService,
coreService,
new MockLogService(),
optionsService,
oscLinkService,
new MockMouseStateService(),
new MockUnicodeService()
);
});
it('should map G0 line drawing via ESC ( 0', async () => {
await inputHandler.parseP('\x1b(0q\x1b(Bq');
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), '\u2500q');
});
it('should map G1 line drawing after ESC ) 0 and SO', async () => {
await inputHandler.parseP('\x1b)0\x0eq\x0f\x1b(Bq');
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), '\u2500q');
});
it('should restore charset and glevel on ESC 7 / ESC 8', async () => {
await inputHandler.parseP('\x1b)0\x0e');
assert.strictEqual(charsetService.glevel, 1);
assert.strictEqual(charsetService.charset, CHARSETS['0']);
await inputHandler.parseP('\x1b7');
await inputHandler.parseP('\x0f\x1b(B');
assert.strictEqual(charsetService.glevel, 0);
assert.ok(charsetService.charset === undefined);
await inputHandler.parseP('\x1b8');
assert.strictEqual(charsetService.glevel, 1);
assert.strictEqual(charsetService.charset, CHARSETS['0']);
await inputHandler.parseP('q');
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), '\u2500');
});
});
describe('alt screen', () => {
let bufferService: IBufferService;
let handler: TestInputHandler;
beforeEach(() => {
bufferService = new MockBufferService(80, 30);
handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
});
it('should handle DECSET/DECRST 47 (alt screen buffer)', async () => {
await handler.parseP('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST');
assert.equal(bufferService.buffer.translateBufferLineToString(0, true), '');
assert.equal(bufferService.buffer.translateBufferLineToString(1, true), ' TEST');
// Text color of 'TEST' should be red
assert.equal((bufferService.buffer.lines.get(1)!.loadCell(4, new CellData()).getFgColor()), 1);
});
it('should handle DECSET/DECRST 1047 (alt screen buffer)', async () => {
await handler.parseP('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST');
assert.equal(bufferService.buffer.translateBufferLineToString(0, true), '');
assert.equal(bufferService.buffer.translateBufferLineToString(1, true), ' TEST');
// Text color of 'TEST' should be red
assert.equal((bufferService.buffer.lines.get(1)!.loadCell(4, new CellData()).getFgColor()), 1);
});
it('should handle DECSET/DECRST 1048 (alt screen cursor)', async () => {
await handler.parseP('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST');
assert.equal(bufferService.buffer.translateBufferLineToString(0, true), 'TEST');
assert.equal(bufferService.buffer.translateBufferLineToString(1, true), 'JUNK');
// Text color of 'TEST' should be default
assert.equal(bufferService.buffer.lines.get(0)!.loadCell(0, new CellData()).fg, DEFAULT_ATTR_DATA.fg);
// Text color of 'JUNK' should be red
assert.equal((bufferService.buffer.lines.get(1)!.loadCell(0, new CellData()).getFgColor()), 1);
});
it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', async () => {
await handler.parseP('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST');
assert.equal(bufferService.buffer.translateBufferLineToString(0, true), 'TEST');
assert.equal(bufferService.buffer.translateBufferLineToString(1, true), '');
// Text color of 'TEST' should be default
assert.equal(bufferService.buffer.lines.get(0)!.loadCell(0, new CellData()).fg, DEFAULT_ATTR_DATA.fg);
});
it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', async () => {
await handler.parseP('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST');
assert.equal(bufferService.buffer.translateBufferLineToString(0, true), 'TEST');
// Text color of 'TEST' should be default
assert.equal(bufferService.buffer.lines.get(0)!.loadCell(0, new CellData()).fg, DEFAULT_ATTR_DATA.fg);
await handler.parseP('\x1b[?1049h\x1b[uTEST');
assert.equal(bufferService.buffer.translateBufferLineToString(1, true), 'TEST');
// Text color of 'TEST' should be red
assert.equal((bufferService.buffer.lines.get(1)!.loadCell(0, new CellData()).getFgColor()), 1);
});
it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', async () => {
await handler.parseP('\x1b[42m\x1b[?1049h');
// Buffer should be filled with green background
assert.equal(bufferService.buffer.lines.get(20)!.loadCell(10, new CellData()).getBgColor(), 2);
});
});
describe('text attributes', () => {
it('bold', async () => {
await inputHandler.parseP('\x1b[1m');
assert.equal(!!inputHandler.curAttrData.isBold(), true);
await inputHandler.parseP('\x1b[22m');
assert.equal(!!inputHandler.curAttrData.isBold(), false);
});
it('dim', async () => {
await inputHandler.parseP('\x1b[2m');
assert.equal(!!inputHandler.curAttrData.isDim(), true);
await inputHandler.parseP('\x1b[22m');
assert.equal(!!inputHandler.curAttrData.isDim(), false);
});
it('SGR 221 resets bold only (kitty)', async () => {
await inputHandler.parseP('\x1b[1;2m');
assert.equal(!!inputHandler.curAttrData.isBold(), true);
assert.equal(!!inputHandler.curAttrData.isDim(), true);
await inputHandler.parseP('\x1b[221m');
assert.equal(!!inputHandler.curAttrData.isBold(), false);
assert.equal(!!inputHandler.curAttrData.isDim(), true);
});
it('SGR 222 resets faint only (kitty)', async () => {
await inputHandler.parseP('\x1b[1;2m');
assert.equal(!!inputHandler.curAttrData.isBold(), true);
assert.equal(!!inputHandler.curAttrData.isDim(), true);
await inputHandler.parseP('\x1b[222m');
assert.equal(!!inputHandler.curAttrData.isBold(), true);
assert.equal(!!inputHandler.curAttrData.isDim(), false);
});
it('italic', async () => {
await inputHandler.parseP('\x1b[3m');
assert.equal(!!inputHandler.curAttrData.isItalic(), true);
await inputHandler.parseP('\x1b[23m');
assert.equal(!!inputHandler.curAttrData.isItalic(), false);
});
it('underline', async () => {
await inputHandler.parseP('\x1b[4m');
assert.equal(!!inputHandler.curAttrData.isUnderline(), true);
await inputHandler.parseP('\x1b[24m');
assert.equal(!!inputHandler.curAttrData.isUnderline(), false);
});
it('blink', async () => {
await inputHandler.parseP('\x1b[5m');
assert.equal(!!inputHandler.curAttrData.isBlink(), true);
await inputHandler.parseP('\x1b[25m');
assert.equal(!!inputHandler.curAttrData.isBlink(), false);
});
it('inverse', async () => {
await inputHandler.parseP('\x1b[7m');
assert.equal(!!inputHandler.curAttrData.isInverse(), true);
await inputHandler.parseP('\x1b[27m');
assert.equal(!!inputHandler.curAttrData.isInverse(), false);
});
it('invisible', async () => {
await inputHandler.parseP('\x1b[8m');
assert.equal(!!inputHandler.curAttrData.isInvisible(), true);
await inputHandler.parseP('\x1b[28m');
assert.equal(!!inputHandler.curAttrData.isInvisible(), false);
});
it('strikethrough', async () => {
await inputHandler.parseP('\x1b[9m');
assert.equal(!!inputHandler.curAttrData.isStrikethrough(), true);
await inputHandler.parseP('\x1b[29m');
assert.equal(!!inputHandler.curAttrData.isStrikethrough(), false);
});
it('colormode palette 16', async () => {
assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); // DEFAULT
assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); // DEFAULT
// lower 8 colors
for (let i = 0; i < 8; ++i) {
await inputHandler.parseP(`\x1b[${i + 30};${i + 40}m`);
assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P16);
assert.equal(inputHandler.curAttrData.getFgColor(), i);
assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P16);
assert.equal(inputHandler.curAttrData.getBgColor(), i);
}
// reset to DEFAULT
await inputHandler.parseP(`\x1b[39;49m`);
assert.equal(inputHandler.curAttrData.getFgColorMode(), 0);
assert.equal(inputHandler.curAttrData.getBgColorMode(), 0);
});
it('colormode palette 256', async () => {
assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); // DEFAULT
assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); // DEFAULT
// lower 8 colors
for (let i = 0; i < 256; ++i) {
await inputHandler.parseP(`\x1b[38;5;${i};48;5;${i}m`);
assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P256);
assert.equal(inputHandler.curAttrData.getFgColor(), i);
assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P256);
assert.equal(inputHandler.curAttrData.getBgColor(), i);
}
// reset to DEFAULT
await inputHandler.parseP(`\x1b[39;49m`);
assert.equal(inputHandler.curAttrData.getFgColorMode(), 0);
assert.equal(inputHandler.curAttrData.getFgColor(), -1);
assert.equal(inputHandler.curAttrData.getBgColorMode(), 0);
assert.equal(inputHandler.curAttrData.getBgColor(), -1);
});
it('colormode RGB', async () => {
assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); // DEFAULT
assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); // DEFAULT
await inputHandler.parseP(`\x1b[38;2;1;2;3;48;2;4;5;6m`);
assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_RGB);
assert.equal(inputHandler.curAttrData.getFgColor(), 1 << 16 | 2 << 8 | 3);
assert.deepEqual(AttributeData.toColorRGB(inputHandler.curAttrData.getFgColor()), [1, 2, 3]);
assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_RGB);
assert.deepEqual(AttributeData.toColorRGB(inputHandler.curAttrData.getBgColor()), [4, 5, 6]);
// reset to DEFAULT
await inputHandler.parseP(`\x1b[39;49m`);
assert.equal(inputHandler.curAttrData.getFgColorMode(), 0);
assert.equal(inputHandler.curAttrData.getFgColor(), -1);
assert.equal(inputHandler.curAttrData.getBgColorMode(), 0);
assert.equal(inputHandler.curAttrData.getBgColor(), -1);
});
it('colormode transition RGB to 256', async () => {
// enter RGB for FG and BG
await inputHandler.parseP(`\x1b[38;2;1;2;3;48;2;4;5;6m`);
// enter 256 for FG and BG
await inputHandler.parseP(`\x1b[38;5;255;48;5;255m`);
assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P256);
assert.equal(inputHandler.curAttrData.getFgColor(), 255);
assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P256);
assert.equal(inputHandler.curAttrData.getBgColor(), 255);
});
it('colormode transition RGB to 16', async () => {
// enter RGB for FG and BG
await inputHandler.parseP(`\x1b[38;2;1;2;3;48;2;4;5;6m`);
// enter 16 for FG and BG
await inputHandler.parseP(`\x1b[37;47m`);
assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P16);
assert.equal(inputHandler.curAttrData.getFgColor(), 7);
assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P16);
assert.equal(inputHandler.curAttrData.getBgColor(), 7);
});
it('colormode transition 16 to 256', async () => {
// enter 16 for FG and BG
await inputHandler.parseP(`\x1b[37;47m`);
// enter 256 for FG and BG
await inputHandler.parseP(`\x1b[38;5;255;48;5;255m`);
assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P256);
assert.equal(inputHandler.curAttrData.getFgColor(), 255);
assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P256);
assert.equal(inputHandler.curAttrData.getBgColor(), 255);
});
it('colormode transition 256 to 16', async () => {
// enter 256 for FG and BG
await inputHandler.parseP(`\x1b[38;5;255;48;5;255m`);
// enter 16 for FG and BG
await inputHandler.parseP(`\x1b[37;47m`);
assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P16);
assert.equal(inputHandler.curAttrData.getFgColor(), 7);
assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P16);
assert.equal(inputHandler.curAttrData.getBgColor(), 7);
});
it('should zero missing RGB values', async () => {
await inputHandler.parseP(`\x1b[38;2;1;2;3m`);
await inputHandler.parseP(`\x1b[38;2;5m`);
assert.deepEqual(AttributeData.toColorRGB(inputHandler.curAttrData.getFgColor()), [5, 0, 0]);
});
});
describe('colon notation', () => {
let inputHandler2: TestInputHandler;
beforeEach(() => {
inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
});
describe('should equal to semicolon', () => {
it('CSI 38:2::50:100:150 m', async () => {
inputHandler.curAttrData.fg = 0xFFFFFFFF;
inputHandler2.curAttrData.fg = 0xFFFFFFFF;
await inputHandler2.parseP('\x1b[38;2;50;100;150m');
await inputHandler.parseP('\x1b[38:2::50:100:150m');
assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
it('CSI 38:2::50:100: m', async () => {
inputHandler.curAttrData.fg = 0xFFFFFFFF;
inputHandler2.curAttrData.fg = 0xFFFFFFFF;
await inputHandler2.parseP('\x1b[38;2;50;100;m');
await inputHandler.parseP('\x1b[38:2::50:100:m');
assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
it('CSI 38:2::50:: m', async () => {
inputHandler.curAttrData.fg = 0xFFFFFFFF;
inputHandler2.curAttrData.fg = 0xFFFFFFFF;
await inputHandler2.parseP('\x1b[38;2;50;;m');
await inputHandler.parseP('\x1b[38:2::50::m');
assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 0 << 8 | 0);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
it('CSI 38:2:::: m', async () => {
inputHandler.curAttrData.fg = 0xFFFFFFFF;
inputHandler2.curAttrData.fg = 0xFFFFFFFF;
await inputHandler2.parseP('\x1b[38;2;;;m');
await inputHandler.parseP('\x1b[38:2::::m');
assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0 << 16 | 0 << 8 | 0);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
it('CSI 38;2::50:100:150 m', async () => {
inputHandler.curAttrData.fg = 0xFFFFFFFF;
inputHandler2.curAttrData.fg = 0xFFFFFFFF;
await inputHandler2.parseP('\x1b[38;2;50;100;150m');
await inputHandler.parseP('\x1b[38;2::50:100:150m');
assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
it('CSI 38;2;50:100:150 m', async () => {
inputHandler.curAttrData.fg = 0xFFFFFFFF;
inputHandler2.curAttrData.fg = 0xFFFFFFFF;
await inputHandler2.parseP('\x1b[38;2;50;100;150m');
await inputHandler.parseP('\x1b[38;2;50:100:150m');
assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
it('CSI 38;2;50;100:150 m', async () => {
inputHandler.curAttrData.fg = 0xFFFFFFFF;
inputHandler2.curAttrData.fg = 0xFFFFFFFF;
await inputHandler2.parseP('\x1b[38;2;50;100;150m');
await inputHandler.parseP('\x1b[38;2;50;100:150m');
assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
it('CSI 38:5:50 m', async () => {
inputHandler.curAttrData.fg = 0xFFFFFFFF;
inputHandler2.curAttrData.fg = 0xFFFFFFFF;
await inputHandler2.parseP('\x1b[38;5;50m');
await inputHandler.parseP('\x1b[38:5:50m');
assert.equal(inputHandler2.curAttrData.fg & 0xFF, 50);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
it('CSI 38:5: m', async () => {
inputHandler.curAttrData.fg = 0xFFFFFFFF;
inputHandler2.curAttrData.fg = 0xFFFFFFFF;
await inputHandler2.parseP('\x1b[38;5;m');
await inputHandler.parseP('\x1b[38:5:m');
assert.equal(inputHandler2.curAttrData.fg & 0xFF, 0);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
it('CSI 38;5:50 m', async () => {
inputHandler.curAttrData.fg = 0xFFFFFFFF;
inputHandler2.curAttrData.fg = 0xFFFFFFFF;
await inputHandler2.parseP('\x1b[38;5;50m');
await inputHandler.parseP('\x1b[38;5:50m');
assert.equal(inputHandler2.curAttrData.fg & 0xFF, 50);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
});
describe('should fill early sequence end with default of 0', () => {
it('CSI 38:2 m', async () => {
inputHandler.curAttrData.fg = 0xFFFFFFFF;
inputHandler2.curAttrData.fg = 0xFFFFFFFF;
await inputHandler2.parseP('\x1b[38;2m');
await inputHandler.parseP('\x1b[38:2m');
assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0 << 16 | 0 << 8 | 0);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
it('CSI 38:5 m', async () => {
inputHandler.curAttrData.fg = 0xFFFFFFFF;
inputHandler2.curAttrData.fg = 0xFFFFFFFF;
await inputHandler2.parseP('\x1b[38;5m');
await inputHandler.parseP('\x1b[38:5m');
assert.equal(inputHandler2.curAttrData.fg & 0xFF, 0);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
});
describe('should not interfere with leading/following SGR attrs', () => {
it('CSI 1 ; 38:2::50:100:150 ; 4 m', async () => {
await inputHandler2.parseP('\x1b[1;38;2;50;100;150;4m');
await inputHandler.parseP('\x1b[1;38:2::50:100:150;4m');
assert.equal(!!inputHandler2.curAttrData.isBold(), true);
assert.equal(!!inputHandler2.curAttrData.isUnderline(), true);
assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
it('CSI 1 ; 38:2::50:100: ; 4 m', async () => {
await inputHandler2.parseP('\x1b[1;38;2;50;100;;4m');
await inputHandler.parseP('\x1b[1;38:2::50:100:;4m');
assert.equal(!!inputHandler2.curAttrData.isBold(), true);
assert.equal(!!inputHandler2.curAttrData.isUnderline(), true);
assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
it('CSI 1 ; 38:2::50:100 ; 4 m', async () => {
await inputHandler2.parseP('\x1b[1;38;2;50;100;;4m');
await inputHandler.parseP('\x1b[1;38:2::50:100;4m');
assert.equal(!!inputHandler2.curAttrData.isBold(), true);
assert.equal(!!inputHandler2.curAttrData.isUnderline(), true);
assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
it('CSI 1 ; 38:2:: ; 4 m', async () => {
await inputHandler2.parseP('\x1b[1;38;2;;;;4m');
await inputHandler.parseP('\x1b[1;38:2::;4m');
assert.equal(!!inputHandler2.curAttrData.isBold(), true);
assert.equal(!!inputHandler2.curAttrData.isUnderline(), true);
assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
it('CSI 1 ; 38;2:: ; 4 m', async () => {
await inputHandler2.parseP('\x1b[1;38;2;;;;4m');
await inputHandler.parseP('\x1b[1;38;2::;4m');
assert.equal(!!inputHandler2.curAttrData.isBold(), true);
assert.equal(!!inputHandler2.curAttrData.isUnderline(), true);
assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0);
assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg);
});
});
});
describe('cursor positioning', () => {
beforeEach(() => {
bufferService.resize(10, 10);
});
it('cursor forward (CUF)', async () => {
await inputHandler.parseP('\x1b[C');
assert.deepEqual(getCursor(bufferService), [1, 0]);
await inputHandler.parseP('\x1b[1C');
assert.deepEqual(getCursor(bufferService), [2, 0]);
await inputHandler.parseP('\x1b[4C');
assert.deepEqual(getCursor(bufferService), [6, 0]);
await inputHandler.parseP('\x1b[100C');
assert.deepEqual(getCursor(bufferService), [9, 0]);
// should not change y
bufferService.buffer.x = 8;
bufferService.buffer.y = 4;
await inputHandler.parseP('\x1b[C');
assert.deepEqual(getCursor(bufferService), [9, 4]);
});
it('cursor backward (CUB)', async () => {
await inputHandler.parseP('\x1b[D');
assert.deepEqual(getCursor(bufferService), [0, 0]);
await inputHandler.parseP('\x1b[1D');
assert.deepEqual(getCursor(bufferService), [0, 0]);
// place cursor at end of first line
await inputHandler.parseP('\x1b[100C');
await inputHandler.parseP('\x1b[D');
assert.deepEqual(getCursor(bufferService), [8, 0]);
await inputHandler.parseP('\x1b[1D');
assert.deepEqual(getCursor(bufferService), [7, 0]);
await inputHandler.parseP('\x1b[4D');
assert.deepEqual(getCursor(bufferService), [3, 0]);
await inputHandler.parseP('\x1b[100D');
assert.deepEqual(getCursor(bufferService), [0, 0]);
// should not change y
bufferService.buffer.x = 4;
bufferService.buffer.y = 4;
await inputHandler.parseP('\x1b[D');
assert.deepEqual(getCursor(bufferService), [3, 4]);
});
it('cursor down (CUD)', async () => {
await inputHandler.parseP('\x1b[B');
assert.deepEqual(getCursor(bufferService), [0, 1]);
await inputHandler.parseP('\x1b[1B');
assert.deepEqual(getCursor(bufferService), [0, 2]);
await inputHandler.parseP('\x1b[4B');
assert.deepEqual(getCursor(bufferService), [0, 6]);
await inputHandler.parseP('\x1b[100B');
assert.deepEqual(getCursor(bufferService), [0, 9]);
// should not change x
bufferService.buffer.x = 8;
bufferService.buffer.y = 0;
await inputHandler.parseP('\x1b[B');
assert.deepEqual(getCursor(bufferService), [8, 1]);
});
it('cursor up (CUU)', async () => {
await inputHandler.parseP('\x1b[A');
assert.deepEqual(getCursor(bufferService), [0, 0]);
await inputHandler.parseP('\x1b[1A');
assert.deepEqual(getCursor(bufferService), [0, 0]);
// place cursor at beginning of last row
await inputHandler.parseP('\x1b[100B');
await inputHandler.parseP('\x1b[A');
assert.deepEqual(getCursor(bufferService), [0, 8]);
await inputHandler.parseP('\x1b[1A');
assert.deepEqual(getCursor(bufferService), [0, 7]);
await inputHandler.parseP('\x1b[4A');
assert.deepEqual(getCursor(bufferService), [0, 3]);
await inputHandler.parseP('\x1b[100A');
assert.deepEqual(getCursor(bufferService), [0, 0]);
// should not change x
bufferService.buffer.x = 8;
bufferService.buffer.y = 9;
await inputHandler.parseP('\x1b[A');
assert.deepEqual(getCursor(bufferService), [8, 8]);
});
it('cursor next line (CNL)', async () => {
await inputHandler.parseP('\x1b[E');
assert.deepEqual(getCursor(bufferService), [0, 1]);
await inputHandler.parseP('\x1b[1E');
assert.deepEqual(getCursor(bufferService), [0, 2]);
await inputHandler.parseP('\x1b[4E');
assert.deepEqual(getCursor(bufferService), [0, 6]);
await inputHandler.parseP('\x1b[100E');
assert.deepEqual(getCursor(bufferService), [0, 9]);
// should reset x to zero
bufferService.buffer.x = 8;
bufferService.buffer.y = 0;
await inputHandler.parseP('\x1b[E');
assert.deepEqual(getCursor(bufferService), [0, 1]);
});
it('cursor previous line (CPL)', async () => {
await inputHandler.parseP('\x1b[F');
assert.deepEqual(getCursor(bufferService), [0, 0]);
await inputHandler.parseP('\x1b[1F');
assert.deepEqual(getCursor(bufferService), [0, 0]);
// place cursor at beginning of last row
await inputHandler.parseP('\x1b[100E');
await inputHandler.parseP('\x1b[F');
assert.deepEqual(getCursor(bufferService), [0, 8]);
await inputHandler.parseP('\x1b[1F');
assert.deepEqual(getCursor(bufferService), [0, 7]);
await inputHandler.parseP('\x1b[4F');
assert.deepEqual(getCursor(bufferService), [0, 3]);
await inputHandler.parseP('\x1b[100F');
assert.deepEqual(getCursor(bufferService), [0, 0]);
// should reset x to zero
bufferService.buffer.x = 8;
bufferService.buffer.y = 9;
await inputHandler.parseP('\x1b[F');
assert.deepEqual(getCursor(bufferService), [0, 8]);
});
it('cursor character absolute (CHA)', async () => {
await inputHandler.parseP('\x1b[G');
assert.deepEqual(getCursor(bufferService), [0, 0]);
await inputHandler.parseP('\x1b[1G');
assert.deepEqual(getCursor(bufferService), [0, 0]);
await inputHandler.parseP('\x1b[2G');
assert.deepEqual(getCursor(bufferService), [1, 0]);
await inputHandler.parseP('\x1b[5G');
assert.deepEqual(getCursor(bufferService), [4, 0]);
await inputHandler.parseP('\x1b[100G');
assert.deepEqual(getCursor(bufferService), [9, 0]);
});
it('cursor position (CUP)', async () => {
bufferService.buffer.x = 5;
bufferService.buffer.y = 5;
await inputHandler.parseP('\x1b[H');
assert.deepEqual(getCursor(bufferService), [0, 0]);
bufferService.buffer.x = 5;
bufferService.buffer.y = 5;
await inputHandler.parseP('\x1b[1H');
assert.deepEqual(getCursor(bufferService), [0, 0]);
bufferService.buffer.x = 5;
bufferService.buffer.y = 5;
await inputHandler.parseP('\x1b[1;1H');
assert.deepEqual(getCursor(bufferService), [0, 0]);
bufferService.buffer.x = 5;
bufferService.buffer.y = 5;
await inputHandler.parseP('\x1b[8H');
assert.deepEqual(getCursor(bufferService), [0, 7]);
bufferService.buffer.x = 5;
bufferService.buffer.y = 5;
await inputHandler.parseP('\x1b[;8H');
assert.deepEqual(getCursor(bufferService), [7, 0]);
bufferService.buffer.x = 5;
bufferService.buffer.y = 5;
await inputHandler.parseP('\x1b[100;100H');
assert.deepEqual(getCursor(bufferService), [9, 9]);
});
it('cursor position (CUP) with DECOM and scroll margins', async () => {
await inputHandler.parseP('\x1b[?6h\x1b[2;3r\x1b[1;1H');
assert.deepEqual(getCursor(bufferService), [0, 1]);
await inputHandler.parseP('X');
assert.strictEqual(getLines(bufferService, 3)[1], 'X');
await inputHandler.parseP('\x1b[2;1H');
assert.deepEqual(getCursor(bufferService), [0, 2]);
await inputHandler.parseP('\x1b[10;10H');
assert.deepEqual(getCursor(bufferService), [9, 2]);
await inputHandler.parseP('\x1b[?6l');
await inputHandler.parseP('\x1b[2;1H');
assert.deepEqual(getCursor(bufferService), [0, 1]);
});
it('horizontal position absolute (HPA)', async () => {
await inputHandler.parseP('\x1b[`');
assert.deepEqual(getCursor(bufferService), [0, 0]);
await inputHandler.parseP('\x1b[1`');
assert.deepEqual(getCursor(bufferService), [0, 0]);
await inputHandler.parseP('\x1b[2`');
assert.deepEqual(getCursor(bufferService), [1, 0]);
await inputHandler.parseP('\x1b[5`');
assert.deepEqual(getCursor(bufferService), [4, 0]);
await inputHandler.parseP('\x1b[100`');
assert.deepEqual(getCursor(bufferService), [9, 0]);
});
it('horizontal position relative (HPR)', async () => {
await inputHandler.parseP('\x1b[a');
assert.deepEqual(getCursor(bufferService), [1, 0]);
await inputHandler.parseP('\x1b[1a');
assert.deepEqual(getCursor(bufferService), [2, 0]);
await inputHandler.parseP('\x1b[4a');
assert.deepEqual(getCursor(bufferService), [6, 0]);
await inputHandler.parseP('\x1b[100a');
assert.deepEqual(getCursor(bufferService), [9, 0]);
// should not change y
bufferService.buffer.x = 8;
bufferService.buffer.y = 4;
await inputHandler.parseP('\x1b[a');
assert.deepEqual(getCursor(bufferService), [9, 4]);
});
it('vertical position absolute (VPA)', async () => {
await inputHandler.parseP('\x1b[d');
assert.deepEqual(getCursor(bufferService), [0, 0]);
await inputHandler.parseP('\x1b[1d');
assert.deepEqual(getCursor(bufferService), [0, 0]);
await inputHandler.parseP('\x1b[2d');
assert.deepEqual(getCursor(bufferService), [0, 1]);
await inputHandler.parseP('\x1b[5d');
assert.deepEqual(getCursor(bufferService), [0, 4]);
await inputHandler.parseP('\x1b[100d');
assert.deepEqual(getCursor(bufferService), [0, 9]);
// should not change x
bufferService.buffer.x = 8;
bufferService.buffer.y = 4;
await inputHandler.parseP('\x1b[d');
assert.deepEqual(getCursor(bufferService), [8, 0]);
});
it('vertical position relative (VPR)', async () => {
await inputHandler.parseP('\x1b[e');
assert.deepEqual(getCursor(bufferService), [0, 1]);
await inputHandler.parseP('\x1b[1e');
assert.deepEqual(getCursor(bufferService), [0, 2]);
await inputHandler.parseP('\x1b[4e');
assert.deepEqual(getCursor(bufferService), [0, 6]);
await inputHandler.parseP('\x1b[100e');
assert.deepEqual(getCursor(bufferService), [0, 9]);
// should not change x
bufferService.buffer.x = 8;
bufferService.buffer.y = 4;
await inputHandler.parseP('\x1b[e');
assert.deepEqual(getCursor(bufferService), [8, 5]);
});
describe('should clamp cursor into addressable range', () => {
it('CUF', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[C');
assert.deepEqual(getCursor(bufferService), [9, 9]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[C');
assert.deepEqual(getCursor(bufferService), [1, 0]);
});
it('CUB', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[D');
assert.deepEqual(getCursor(bufferService), [8, 9]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[D');
assert.deepEqual(getCursor(bufferService), [0, 0]);
});
it('CUD', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[B');
assert.deepEqual(getCursor(bufferService), [9, 9]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[B');
assert.deepEqual(getCursor(bufferService), [0, 1]);
});
it('CUU', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[A');
assert.deepEqual(getCursor(bufferService), [9, 8]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[A');
assert.deepEqual(getCursor(bufferService), [0, 0]);
});
it('CNL', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[E');
assert.deepEqual(getCursor(bufferService), [0, 9]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[E');
assert.deepEqual(getCursor(bufferService), [0, 1]);
});
it('CPL', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[F');
assert.deepEqual(getCursor(bufferService), [0, 8]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[F');
assert.deepEqual(getCursor(bufferService), [0, 0]);
});
it('CHA', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[5G');
assert.deepEqual(getCursor(bufferService), [4, 9]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[5G');
assert.deepEqual(getCursor(bufferService), [4, 0]);
});
it('CUP', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[5;5H');
assert.deepEqual(getCursor(bufferService), [4, 4]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[5;5H');
assert.deepEqual(getCursor(bufferService), [4, 4]);
});
it('HPA', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[5`');
assert.deepEqual(getCursor(bufferService), [4, 9]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[5`');
assert.deepEqual(getCursor(bufferService), [4, 0]);
});
it('HPR', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[a');
assert.deepEqual(getCursor(bufferService), [9, 9]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[a');
assert.deepEqual(getCursor(bufferService), [1, 0]);
});
it('VPA', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[5d');
assert.deepEqual(getCursor(bufferService), [9, 4]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[5d');
assert.deepEqual(getCursor(bufferService), [0, 4]);
});
it('VPR', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[e');
assert.deepEqual(getCursor(bufferService), [9, 9]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[e');
assert.deepEqual(getCursor(bufferService), [0, 1]);
});
it('DCH', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[P');
assert.deepEqual(getCursor(bufferService), [9, 9]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[P');
assert.deepEqual(getCursor(bufferService), [0, 0]);
});
it('DCH - should delete last cell', async () => {
await inputHandler.parseP('0123456789\x1b[P');
assert.equal(bufferService.buffer.lines.get(0)!.translateToString(false), '012345678 ');
});
it('ECH', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[X');
assert.deepEqual(getCursor(bufferService), [9, 9]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[X');
assert.deepEqual(getCursor(bufferService), [0, 0]);
});
it('ECH - should delete last cell', async () => {
await inputHandler.parseP('0123456789\x1b[X');
assert.equal(bufferService.buffer.lines.get(0)!.translateToString(false), '012345678 ');
});
it('ICH', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[@');
assert.deepEqual(getCursor(bufferService), [9, 9]);
bufferService.buffer.x = -10000;
bufferService.buffer.y = -10000;
await inputHandler.parseP('\x1b[@');
assert.deepEqual(getCursor(bufferService), [0, 0]);
});
it('ICH - should delete last cell', async () => {
await inputHandler.parseP('0123456789\x1b[@');
assert.equal(bufferService.buffer.lines.get(0)!.translateToString(false), '012345678 ');
});
});
});
describe('DECSTBM - scroll margins', () => {
beforeEach(() => {
bufferService.resize(10, 10);
});
it('should default to whole viewport', async () => {
await inputHandler.parseP('\x1b[r');
assert.equal(bufferService.buffer.scrollTop, 0);
assert.equal(bufferService.buffer.scrollBottom, 9);
await inputHandler.parseP('\x1b[3;7r');
assert.equal(bufferService.buffer.scrollTop, 2);
assert.equal(bufferService.buffer.scrollBottom, 6);
await inputHandler.parseP('\x1b[0;0r');
assert.equal(bufferService.buffer.scrollTop, 0);
assert.equal(bufferService.buffer.scrollBottom, 9);
});
it('should clamp bottom', async () => {
await inputHandler.parseP('\x1b[3;1000r');
assert.equal(bufferService.buffer.scrollTop, 2);
assert.equal(bufferService.buffer.scrollBottom, 9);
});
it('should only apply for top < bottom', async () => {
await inputHandler.parseP('\x1b[7;2r');
assert.equal(bufferService.buffer.scrollTop, 0);
assert.equal(bufferService.buffer.scrollBottom, 9);
});
it('should home cursor', async () => {
bufferService.buffer.x = 10000;
bufferService.buffer.y = 10000;
await inputHandler.parseP('\x1b[2;7r');
assert.deepEqual(getCursor(bufferService), [0, 0]);
});
});
describe('scroll margins', () => {
beforeEach(() => {
bufferService.resize(10, 10);
});
it('scrollUp', async () => {
await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Sm');
assert.deepEqual(getLines(bufferService), ['m', '3', '', '', '4', '5', '6', '7', '8', '9']);
});
it('scrollDown', async () => {
await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Tm');
assert.deepEqual(getLines(bufferService), ['m', '', '', '1', '4', '5', '6', '7', '8', '9']);
});
it('insertLines - out of margins', async () => {
await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r');
assert.equal(bufferService.buffer.scrollTop, 2);
assert.equal(bufferService.buffer.scrollBottom, 5);
await inputHandler.parseP('\x1b[2Lm');
assert.deepEqual(getLines(bufferService), ['m', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
await inputHandler.parseP('\x1b[2H\x1b[2Ln');
assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', '6', '7', '8', '9']);
// skip below scrollbottom
await inputHandler.parseP('\x1b[7H\x1b[2Lo');
assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', '7', '8', '9']);
await inputHandler.parseP('\x1b[8H\x1b[2Lp');
assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', '9']);
await inputHandler.parseP('\x1b[100H\x1b[2Lq');
assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', 'q']);
});
it('insertLines - within margins', async () => {
await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r');
assert.equal(bufferService.buffer.scrollTop, 2);
assert.equal(bufferService.buffer.scrollBottom, 5);
await inputHandler.parseP('\x1b[3H\x1b[2Lm');
assert.deepEqual(getLines(bufferService), ['0', '1', 'm', '', '2', '3', '6', '7', '8', '9']);
await inputHandler.parseP('\x1b[6H\x1b[2Ln');
assert.deepEqual(getLines(bufferService), ['0', '1', 'm', '', '2', 'n', '6', '7', '8', '9']);
});
it('deleteLines - out of margins', async () => {
await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r');
assert.equal(bufferService.buffer.scrollTop, 2);
assert.equal(bufferService.buffer.scrollBottom, 5);
await inputHandler.parseP('\x1b[2Mm');
assert.deepEqual(getLines(bufferService), ['m', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
await inputHandler.parseP('\x1b[2H\x1b[2Mn');
assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', '6', '7', '8', '9']);
// skip below scrollbottom
await inputHandler.parseP('\x1b[7H\x1b[2Mo');
assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', '7', '8', '9']);
await inputHandler.parseP('\x1b[8H\x1b[2Mp');
assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', '9']);
await inputHandler.parseP('\x1b[100H\x1b[2Mq');
assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', 'q']);
});
it('deleteLines - within margins', async () => {
await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r');
assert.equal(bufferService.buffer.scrollTop, 2);
assert.equal(bufferService.buffer.scrollBottom, 5);
await inputHandler.parseP('\x1b[6H\x1b[2Mm');
assert.deepEqual(getLines(bufferService), ['0', '1', '2', '3', '4', 'm', '6', '7', '8', '9']);
await inputHandler.parseP('\x1b[3H\x1b[2Mn');
assert.deepEqual(getLines(bufferService), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']);
});
});
it('should parse big chunks in smaller subchunks', async () => {
// max single chunk size is hardcoded as 131072
const calls: any[] = [];
bufferService.resize(10, 10);
(inputHandler as any)._parser.parse = (data: Uint32Array, length: number) => {
calls.push([data.length, length]);
};
await inputHandler.parseP('12345');
await inputHandler.parseP('a'.repeat(10000));
await inputHandler.parseP('a'.repeat(200000));
await inputHandler.parseP('a'.repeat(300000));
assert.deepEqual(calls, [
[4096, 5],
[10000, 10000],
[131072, 131072], [131072, 200000 - 131072],
[131072, 131072], [131072, 131072], [131072, 300000 - 131072 - 131072]
]);
});
describe('windowOptions', () => {
it('all should be disabled by default and not report', async () => {
bufferService.resize(10, 10);
const stack: string[] = [];
coreService.onData(data => stack.push(data));
await inputHandler.parseP('\x1b[14t');
await inputHandler.parseP('\x1b[16t');
await inputHandler.parseP('\x1b[18t');
await inputHandler.parseP('\x1b[20t');
await inputHandler.parseP('\x1b[21t');
assert.deepEqual(stack, []);
});
it('14 - GetWinSizePixels', async () => {
bufferService.resize(10, 10);
optionsService.options.windowOptions.getWinSizePixels = true;
const stack: string[] = [];
coreService.onData(data => stack.push(data));
await inputHandler.parseP('\x1b[14t');
// does not report in test terminal due to missing renderer
assert.deepEqual(stack, []);
});
it('16 - GetCellSizePixels', async () => {
bufferService.resize(10, 10);
optionsService.options.windowOptions.getCellSizePixels = true;
const stack: string[] = [];
coreService.onData(data => stack.push(data));
await inputHandler.parseP('\x1b[16t');
// does not report in test terminal due to missing renderer
assert.deepEqual(stack, []);
});
it('18 - GetWinSizeChars', async () => {
bufferService.resize(10, 10);
optionsService.options.windowOptions.getWinSizeChars = true;
const stack: string[] = [];
coreService.onData(data => stack.push(data));
await inputHandler.parseP('\x1b[18t');
assert.deepEqual(stack, ['\x1b[8;10;10t']);
bufferService.resize(50, 20);
await inputHandler.parseP('\x1b[18t');
assert.deepEqual(stack, ['\x1b[8;10;10t', '\x1b[8;20;50t']);
});
it('22/23 - PushTitle/PopTitle', async () => {
bufferService.resize(10, 10);
optionsService.options.windowOptions.pushTitle = true;
optionsService.options.windowOptions.popTitle = true;
const stack: string[] = [];
inputHandler.onTitleChange(data => stack.push(data));
await inputHandler.parseP('\x1b]0;1\x07');
await inputHandler.parseP('\x1b[22t');
await inputHandler.parseP('\x1b]0;2\x07');
await inputHandler.parseP('\x1b[22t');
await inputHandler.parseP('\x1b]0;3\x07');
await inputHandler.parseP('\x1b[22t');
assert.deepEqual(inputHandler.windowTitleStack, ['1', '2', '3']);
assert.deepEqual(inputHandler.iconNameStack, ['1', '2', '3']);
assert.deepEqual(stack, ['1', '2', '3']);
await inputHandler.parseP('\x1b[23t');
await inputHandler.parseP('\x1b[23t');
await inputHandler.parseP('\x1b[23t');
await inputHandler.parseP('\x1b[23t'); // one more to test "overflow"
assert.deepEqual(inputHandler.windowTitleStack, []);
assert.deepEqual(inputHandler.iconNameStack, []);
assert.deepEqual(stack, ['1', '2', '3', '3', '2', '1']);
});
it('22/23 - PushTitle/PopTitle with ;1', async () => {
bufferService.resize(10, 10);
optionsService.options.windowOptions.pushTitle = true;
optionsService.options.windowOptions.popTitle = true;
const stack: string[] = [];
inputHandler.onTitleChange(data => stack.push(data));
await inputHandler.parseP('\x1b]0;1\x07');
await inputHandler.parseP('\x1b[22;1t');
await inputHandler.parseP('\x1b]0;2\x07');
await inputHandler.parseP('\x1b[22;1t');
await inputHandler.parseP('\x1b]0;3\x07');
await inputHandler.parseP('\x1b[22;1t');
assert.deepEqual(inputHandler.windowTitleStack, []);
assert.deepEqual(inputHandler.iconNameStack, ['1', '2', '3']);
assert.deepEqual(stack, ['1', '2', '3']);
await inputHandler.parseP('\x1b[23;1t');
await inputHandler.parseP('\x1b[23;1t');
await inputHandler.parseP('\x1b[23;1t');
await inputHandler.parseP('\x1b[23;1t'); // one more to test "overflow"
assert.deepEqual(inputHandler.windowTitleStack, []);
assert.deepEqual(inputHandler.iconNameStack, []);
assert.deepEqual(stack, ['1', '2', '3']);
});
it('22/23 - PushTitle/PopTitle with ;2', async () => {
bufferService.resize(10, 10);
optionsService.options.windowOptions.pushTitle = true;
optionsService.options.windowOptions.popTitle = true;
const stack: string[] = [];
inputHandler.onTitleChange(data => stack.push(data));
await inputHandler.parseP('\x1b]0;1\x07');
await inputHandler.parseP('\x1b[22;2t');
await inputHandler.parseP('\x1b]0;2\x07');
await inputHandler.parseP('\x1b[22;2t');
await inputHandler.parseP('\x1b]0;3\x07');
await inputHandler.parseP('\x1b[22;2t');
assert.deepEqual(inputHandler.windowTitleStack, ['1', '2', '3']);
assert.deepEqual(inputHandler.iconNameStack, []);
assert.deepEqual(stack, ['1', '2', '3']);
await inputHandler.parseP('\x1b[23;2t');
await inputHandler.parseP('\x1b[23;2t');
await inputHandler.parseP('\x1b[23;2t');
await inputHandler.parseP('\x1b[23;2t'); // one more to test "overflow"
assert.deepEqual(inputHandler.windowTitleStack, []);
assert.deepEqual(inputHandler.iconNameStack, []);
assert.deepEqual(stack, ['1', '2', '3', '3', '2', '1']);
});
it('DECCOLM - should only work with "SetWinLines" (24) enabled', async () => {
// disabled
bufferService.resize(10, 10);
await inputHandler.parseP('\x1b[?3l');
assert.equal(bufferService.cols, 10);
await inputHandler.parseP('\x1b[?3h');
assert.equal(bufferService.cols, 10);
// enabled
inputHandler.reset();
optionsService.options.windowOptions.setWinLines = true;
await inputHandler.parseP('\x1b[?3l');
assert.equal(bufferService.cols, 80);
await inputHandler.parseP('\x1b[?3h');
assert.equal(bufferService.cols, 132);
});
});
describe('XTVERSION (CSI > q, CSI > 0 q)', () => {
it('should report xterm.js version', async () => {
const stack: string[] = [];
coreService.onData(data => stack.push(data));
await inputHandler.parseP('\x1b[>q');
assert.strictEqual(stack.length, 1);
assert.ok(stack[0].match(/^\x1bP>\|xterm\.js\(\d+\.\d+\.\d+(-beta\.\d+)?\)\x1b\\/));
});
it('should report xterm.js version for CSI > 0 q', async () => {
const stack: string[] = [];
coreService.onData(data => stack.push(data));
await inputHandler.parseP('\x1b[>0q');
assert.strictEqual(stack.length, 1);
assert.ok(stack[0].match(/^\x1bP>\|xterm\.js\(\d+\.\d+\.\d+(-beta\.\d+)?\)\x1b\\/));
});
it('should not report for CSI > 1 q', async () => {
const stack: string[] = [];
coreService.onData(data => stack.push(data));
await inputHandler.parseP('\x1b[>1q');
assert.strictEqual(stack.length, 0);
});
});
describe('should correctly reset cells taken by wide chars', () => {
beforeEach(async () => {
bufferService.resize(10, 5);
optionsService.options.scrollback = 1;
await inputHandler.parseP('¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥');
});
it('print', async () => {
await inputHandler.parseP('\x1b[H#');
assert.deepEqual(getLines(bufferService), ['# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
await inputHandler.parseP('\x1b[1;6H######');
assert.deepEqual(getLines(bufferService), ['# ¥ #####', '# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
await inputHandler.parseP('#');
assert.deepEqual(getLines(bufferService), ['# ¥ #####', '##¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
await inputHandler.parseP('#');
assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
await inputHandler.parseP('\x1b[3;9H#');
assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥#', '¥¥¥¥¥', '']);
await inputHandler.parseP('#');
assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '¥¥¥¥¥', '']);
await inputHandler.parseP('#');
assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥¥', '']);
await inputHandler.parseP('\x1b[4;10H#');
assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥ #', '']);
});
it('EL', async () => {
await inputHandler.parseP('\x1b[1;6H\x1b[K#');
assert.deepEqual(getLines(bufferService), ['¥¥ #', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
await inputHandler.parseP('\x1b[2;5H\x1b[1K');
assert.deepEqual(getLines(bufferService), ['¥¥ #', ' ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
await inputHandler.parseP('\x1b[3;6H\x1b[1K');
assert.deepEqual(getLines(bufferService), ['¥¥ #', ' ¥¥', ' ¥¥', '¥¥¥¥¥', '']);
});
it('ICH', async () => {
await inputHandler.parseP('\x1b[1;6H\x1b[@');
assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
await inputHandler.parseP('\x1b[2;4H\x1b[2@');
assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
await inputHandler.parseP('\x1b[3;4H\x1b[3@');
assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥¥¥¥¥', '']);
await inputHandler.parseP('\x1b[4;4H\x1b[4@');
assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥ ¥', '']);
});
it('DCH', async () => {
await inputHandler.parseP('\x1b[1;6H\x1b[P');
assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
await inputHandler.parseP('\x1b[2;6H\x1b[2P');
assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
await inputHandler.parseP('\x1b[3;6H\x1b[3P');
assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']);
});
it('ECH', async () => {
await inputHandler.parseP('\x1b[1;6H\x1b[X');
assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
await inputHandler.parseP('\x1b[2;6H\x1b[2X');
assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
await inputHandler.parseP('\x1b[3;6H\x1b[3X');
assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']);
});
});
describe('BS with reverseWraparound set/unset', () => {
const ttyBS = '\x08 \x08'; // tty ICANON sends <BS SP BS> on pressing BS
beforeEach(() => {
bufferService.resize(5, 5);
optionsService.options.scrollback = 1;
});
describe('reverseWraparound unset (default)', () => {
it('cannot delete last cell', async () => {
await inputHandler.parseP('12345');
await inputHandler.parseP(ttyBS);
assert.deepEqual(getLines(bufferService, 1), ['123 5']);
await inputHandler.parseP(ttyBS.repeat(10));
assert.deepEqual(getLines(bufferService, 1), [' 5']);
});
it('cannot access prev line', async () => {
await inputHandler.parseP('12345'.repeat(2));
await inputHandler.parseP(ttyBS);
assert.deepEqual(getLines(bufferService, 2), ['12345', '123 5']);
await inputHandler.parseP(ttyBS.repeat(10));
assert.deepEqual(getLines(bufferService, 2), ['12345', ' 5']);
});
});
describe('reverseWraparound set', () => {
it('can delete last cell', async () => {
await inputHandler.parseP('\x1b[?45h');
await inputHandler.parseP('12345');
await inputHandler.parseP(ttyBS);
assert.deepEqual(getLines(bufferService, 1), ['1234 ']);
await inputHandler.parseP(ttyBS.repeat(7));
assert.deepEqual(getLines(bufferService, 1), [' ']);
});
it('can access prev line if wrapped', async () => {
await inputHandler.parseP('\x1b[?45h');
await inputHandler.parseP('12345'.repeat(2));
await inputHandler.parseP(ttyBS);
assert.deepEqual(getLines(bufferService, 2), ['12345', '1234 ']);
await inputHandler.parseP(ttyBS.repeat(7));
assert.deepEqual(getLines(bufferService, 2), ['12 ', ' ']);
});
it('should lift isWrapped', async () => {
await inputHandler.parseP('\x1b[?45h');
await inputHandler.parseP('12345'.repeat(2));
assert.equal(bufferService.buffer.lines.get(1)?.isWrapped, true);
await inputHandler.parseP(ttyBS.repeat(7));
assert.equal(bufferService.buffer.lines.get(1)?.isWrapped, false);
});
it('stops at hard NLs', async () => {
await inputHandler.parseP('\x1b[?45h');
await inputHandler.parseP('12345\r\n');
await inputHandler.parseP('12345'.repeat(2));
await inputHandler.parseP(ttyBS.repeat(50));
assert.deepEqual(getLines(bufferService, 3), ['12345', ' ', ' ']);
assert.equal(bufferService.buffer.x, 0);
assert.equal(bufferService.buffer.y, 1);
});
it('handles wide chars correctly', async () => {
await inputHandler.parseP('\x1b[?45h');
await inputHandler.parseP('¥¥¥');
assert.deepEqual(getLines(bufferService, 2), ['¥¥', '¥']);
await inputHandler.parseP(ttyBS);
assert.deepEqual(getLines(bufferService, 2), ['¥¥', ' ']);
assert.equal(bufferService.buffer.x, 1);
await inputHandler.parseP(ttyBS);
assert.deepEqual(getLines(bufferService, 2), ['¥¥', ' ']);
assert.equal(bufferService.buffer.x, 0);
await inputHandler.parseP(ttyBS);
assert.deepEqual(getLines(bufferService, 2), ['¥ ', ' ']);
assert.equal(bufferService.buffer.x, 3); // x=4 skipped due to early wrap-around
await inputHandler.parseP(ttyBS);
assert.deepEqual(getLines(bufferService, 2), ['¥ ', ' ']);
assert.equal(bufferService.buffer.x, 2);
await inputHandler.parseP(ttyBS);
assert.deepEqual(getLines(bufferService, 2), [' ', ' ']);
assert.equal(bufferService.buffer.x, 1);
await inputHandler.parseP(ttyBS);
assert.deepEqual(getLines(bufferService, 2), [' ', ' ']);
assert.equal(bufferService.buffer.x, 0);
});
});
});
describe('reset text attributes (SGR 0)', () => {
it('resets all attributes if there is no url', async () => {
await inputHandler.parseP('\x1b[30m\x1b[40m\x1b[4m');
assert.notEqual(inputHandler.curAttrData.fg, 0);
assert.notEqual(inputHandler.curAttrData.bg, 0);
assert.isFalse(inputHandler.curAttrData.extended.isEmpty());
await inputHandler.parseP('\x1b[m');
assert.equal(inputHandler.curAttrData.fg, 0);
assert.equal(inputHandler.curAttrData.bg, 0);
assert.isTrue(inputHandler.curAttrData.extended.isEmpty());
});
it('resets all attributes except for the url', async () => {
await inputHandler.parseP('\x1b[30m\x1b[40m\x1b[4m');
await inputHandler.parseP('\x1b]8;;http://example.com\x1b\\');
assert.notEqual(inputHandler.curAttrData.fg, 0);
assert.notEqual(inputHandler.curAttrData.bg, 0);
assert.notEqual(inputHandler.curAttrData.extended.ext, 0);
const urlId = inputHandler.curAttrData.extended.urlId;
assert.notEqual(urlId, 0);
await inputHandler.parseP('\x1b[m');
assert.equal(inputHandler.curAttrData.fg, 0);
assert.equal(inputHandler.curAttrData.bg, BgFlags.HAS_EXTENDED);
const expectedExtended = new ExtendedAttrs();
expectedExtended.urlId = urlId;
assert.deepEqual(inputHandler.curAttrData.extended, expectedExtended);
});
});
describe('extended underline style support (SGR 4)', () => {
beforeEach(() => {
bufferService.resize(10, 5);
});
it('4 | 24', async () => {
await inputHandler.parseP('\x1b[4m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE);
await inputHandler.parseP('\x1b[24m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE);
});
it('21 | 24', async () => {
await inputHandler.parseP('\x1b[21m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOUBLE);
await inputHandler.parseP('\x1b[24m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE);
});
it('4:1 | 4:0', async () => {
await inputHandler.parseP('\x1b[4:1m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE);
await inputHandler.parseP('\x1b[4:0m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE);
await inputHandler.parseP('\x1b[4:1m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE);
await inputHandler.parseP('\x1b[24m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE);
});
it('4:2 | 4:0', async () => {
await inputHandler.parseP('\x1b[4:2m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOUBLE);
await inputHandler.parseP('\x1b[4:0m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE);
await inputHandler.parseP('\x1b[4:2m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOUBLE);
await inputHandler.parseP('\x1b[24m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE);
});
it('4:3 | 4:0', async () => {
await inputHandler.parseP('\x1b[4:3m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.CURLY);
await inputHandler.parseP('\x1b[4:0m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE);
await inputHandler.parseP('\x1b[4:3m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.CURLY);
await inputHandler.parseP('\x1b[24m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE);
});
it('4:4 | 4:0', async () => {
await inputHandler.parseP('\x1b[4:4m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOTTED);
await inputHandler.parseP('\x1b[4:0m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE);
await inputHandler.parseP('\x1b[4:4m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOTTED);
await inputHandler.parseP('\x1b[24m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE);
});
it('4:5 | 4:0', async () => {
await inputHandler.parseP('\x1b[4:5m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DASHED);
await inputHandler.parseP('\x1b[4:0m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE);
await inputHandler.parseP('\x1b[4:5m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DASHED);
await inputHandler.parseP('\x1b[24m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE);
});
it('4:x --> 4 should revert to single underline', async () => {
await inputHandler.parseP('\x1b[4:5m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DASHED);
await inputHandler.parseP('\x1b[4m');
assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE);
});
});
describe('underline colors (SGR 58 & SGR 59)', () => {
beforeEach(() => {
bufferService.resize(10, 5);
});
it('defaults to FG color', async () => {
for (const s of ['', '\x1b[30m', '\x1b[38;510m', '\x1b[38;2;1;2;3m']) {
await inputHandler.parseP(s);
assert.equal(inputHandler.curAttrData.getUnderlineColor(), inputHandler.curAttrData.getFgColor());
assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), inputHandler.curAttrData.getFgColorMode());
assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), inputHandler.curAttrData.isFgRGB());
assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), inputHandler.curAttrData.isFgPalette());
assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), inputHandler.curAttrData.isFgDefault());
}
});
it('correctly sets P256/RGB colors', async () => {
await inputHandler.parseP('\x1b[4m');
await inputHandler.parseP('\x1b[58;5;123m');
assert.equal(inputHandler.curAttrData.getUnderlineColor(), 123);
assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_P256);
assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), false);
assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), true);
assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false);
await inputHandler.parseP('\x1b[58;2::1:2:3m');
assert.equal(inputHandler.curAttrData.getUnderlineColor(), (1 << 16) | (2 << 8) | 3);
assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_RGB);
assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), true);
assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), false);
assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false);
});
it('P256/RGB persistence', async () => {
const cell = new CellData();
await inputHandler.parseP('\x1b[4m');
await inputHandler.parseP('\x1b[58;5;123m');
assert.equal(inputHandler.curAttrData.getUnderlineColor(), 123);
assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_P256);
assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), false);
assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), true);
assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false);
await inputHandler.parseP('ab');
bufferService.buffer!.lines.get(0)!.loadCell(1, cell);
assert.equal(cell.getUnderlineColor(), 123);
assert.equal(cell.getUnderlineColorMode(), Attributes.CM_P256);
assert.equal(cell.isUnderlineColorRGB(), false);
assert.equal(cell.isUnderlineColorPalette(), true);
assert.equal(cell.isUnderlineColorDefault(), false);
await inputHandler.parseP('\x1b[4:0m');
assert.equal(inputHandler.curAttrData.getUnderlineColor(), inputHandler.curAttrData.getFgColor());
assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), inputHandler.curAttrData.getFgColorMode());
assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), inputHandler.curAttrData.isFgRGB());
assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), inputHandler.curAttrData.isFgPalette());
assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), inputHandler.curAttrData.isFgDefault());
await inputHandler.parseP('a');
bufferService.buffer!.lines.get(0)!.loadCell(1, cell);
assert.equal(cell.getUnderlineColor(), 123);
assert.equal(cell.getUnderlineColorMode(), Attributes.CM_P256);
assert.equal(cell.isUnderlineColorRGB(), false);
assert.equal(cell.isUnderlineColorPalette(), true);
assert.equal(cell.isUnderlineColorDefault(), false);
bufferService.buffer!.lines.get(0)!.loadCell(2, cell);
assert.equal(cell.getUnderlineColor(), inputHandler.curAttrData.getFgColor());
assert.equal(cell.getUnderlineColorMode(), inputHandler.curAttrData.getFgColorMode());
assert.equal(cell.isUnderlineColorRGB(), inputHandler.curAttrData.isFgRGB());
assert.equal(cell.isUnderlineColorPalette(), inputHandler.curAttrData.isFgPalette());
assert.equal(cell.isUnderlineColorDefault(), inputHandler.curAttrData.isFgDefault());
await inputHandler.parseP('\x1b[4m');
await inputHandler.parseP('\x1b[58;2::1:2:3m');
assert.equal(inputHandler.curAttrData.getUnderlineColor(), (1 << 16) | (2 << 8) | 3);
assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_RGB);
assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), true);
assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), false);
assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false);
await inputHandler.parseP('a');
await inputHandler.parseP('\x1b[24m');
bufferService.buffer!.lines.get(0)!.loadCell(1, cell);
assert.equal(cell.getUnderlineColor(), 123);
assert.equal(cell.getUnderlineColorMode(), Attributes.CM_P256);
assert.equal(cell.isUnderlineColorRGB(), false);
assert.equal(cell.isUnderlineColorPalette(), true);
assert.equal(cell.isUnderlineColorDefault(), false);
bufferService.buffer!.lines.get(0)!.loadCell(3, cell);
assert.equal(cell.getUnderlineColor(), (1 << 16) | (2 << 8) | 3);
assert.equal(cell.getUnderlineColorMode(), Attributes.CM_RGB);
assert.equal(cell.isUnderlineColorRGB(), true);
assert.equal(cell.isUnderlineColorPalette(), false);
assert.equal(cell.isUnderlineColorDefault(), false);
// eAttrs in buffer pos 0 and 1 should be the same object
assert.equal(
extendedAttributes(bufferService.buffer!.lines.get(0)!, 0),
extendedAttributes(bufferService.buffer!.lines.get(0)!, 1)
);
// should not have written eAttr for pos 2 in the buffer
assert.equal(extendedAttributes(bufferService.buffer!.lines.get(0)!, 2), undefined);
// eAttrs in buffer pos 1 and pos 3 must be different objs
assert.notEqual(
extendedAttributes(bufferService.buffer!.lines.get(0)!, 1),
extendedAttributes(bufferService.buffer!.lines.get(0)!, 3)
);
});
});
describe('DECSTR', () => {
beforeEach(async () => {
bufferService.resize(10, 5);
optionsService.options.scrollback = 1;
await inputHandler.parseP('01234567890123');
});
it('should reset IRM', async () => {
await inputHandler.parseP('\x1b[4h');
assert.equal(coreService.modes.insertMode, true);
await inputHandler.parseP('\x1b[!p');
assert.equal(coreService.modes.insertMode, false);
});
it('should reset cursor visibility', async () => {
await inputHandler.parseP('\x1b[?25l');
assert.equal(coreService.isCursorHidden, true);
await inputHandler.parseP('\x1b[!p');
assert.equal(coreService.isCursorHidden, false);
});
it('should reset scroll margins', async () => {
await inputHandler.parseP('\x1b[2;4r');
assert.equal(bufferService.buffer.scrollTop, 1);
assert.equal(bufferService.buffer.scrollBottom, 3);
await inputHandler.parseP('\x1b[!p');
assert.equal(bufferService.buffer.scrollTop, 0);
assert.equal(bufferService.buffer.scrollBottom, bufferService.rows - 1);
});
it('should reset text attributes', async () => {
await inputHandler.parseP('\x1b[1;2;32;43m');
assert.equal(!!inputHandler.curAttrData.isBold(), true);
await inputHandler.parseP('\x1b[!p');
assert.equal(!!inputHandler.curAttrData.isBold(), false);
assert.equal(inputHandler.curAttrData.fg, 0);
assert.equal(inputHandler.curAttrData.bg, 0);
});
it('should reset DECSC data', async () => {
await inputHandler.parseP('\x1b7');
assert.equal(bufferService.buffer.savedX, 4);
assert.equal(bufferService.buffer.savedY, 1);
await inputHandler.parseP('\x1b[!p');
assert.equal(bufferService.buffer.savedX, 0);
assert.equal(bufferService.buffer.savedY, 0);
});
it('should reset DECOM', async () => {
await inputHandler.parseP('\x1b[?6h');
assert.equal(coreService.decPrivateModes.origin, true);
await inputHandler.parseP('\x1b[!p');
assert.equal(coreService.decPrivateModes.origin, false);
});
});
describe('OSC', () => {
it('4: query color events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single color query
await inputHandler.parseP('\x1b]4;0;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: 0 }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]4;123;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: 123 }]]);
stack.length = 0;
// multiple queries
await inputHandler.parseP('\x1b]4;0;?;123;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: 0 }, { type: ColorRequestType.REPORT, index: 123 }]]);
stack.length = 0;
});
it('4: set color events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single color query
await inputHandler.parseP('\x1b]4;0;rgb:01/02/03\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 0, color: [1, 2, 3] }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]4;123;#aabbcc\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 123, color: [170, 187, 204] }]]);
stack.length = 0;
// multiple queries
await inputHandler.parseP('\x1b]4;0;rgb:aa/bb/cc;123;#001122\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 0, color: [170, 187, 204] }, { type: ColorRequestType.SET, index: 123, color: [0, 17, 34] }]]);
stack.length = 0;
});
it('4: should ignore invalid values', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]4;0;rgb:aa/bb/cc;45;rgb:1/22/333;123;#001122\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 0, color: [170, 187, 204] }, { type: ColorRequestType.SET, index: 123, color: [0, 17, 34] }]]);
stack.length = 0;
});
it('8: hyperlink with id', async () => {
await inputHandler.parseP('\x1b]8;id=100;http://localhost:3000\x07');
assert.notStrictEqual(inputHandler.curAttrData.extended.urlId, 0);
assert.deepStrictEqual(
oscLinkService.getLinkData(inputHandler.curAttrData.extended.urlId),
{
id: '100',
uri: 'http://localhost:3000'
}
);
await inputHandler.parseP('\x1b]8;;\x07');
assert.strictEqual(inputHandler.curAttrData.extended.urlId, 0);
});
it('8: hyperlink with semi-colon', async () => {
await inputHandler.parseP('\x1b]8;;http://localhost:3000;abc=def\x07');
assert.notStrictEqual(inputHandler.curAttrData.extended.urlId, 0);
assert.deepStrictEqual(
oscLinkService.getLinkData(inputHandler.curAttrData.extended.urlId),
{
id: undefined,
uri: 'http://localhost:3000;abc=def'
}
);
await inputHandler.parseP('\x1b]8;;\x07');
assert.strictEqual(inputHandler.curAttrData.extended.urlId, 0);
});
it('104: restore events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]104;0\x07\x1b]104;43\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: 0 }], [{ type: ColorRequestType.RESTORE, index: 43 }]]);
stack.length = 0;
// multiple in one command
await inputHandler.parseP('\x1b]104;0;43\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: 0 }, { type: ColorRequestType.RESTORE, index: 43 }]]);
stack.length = 0;
// full ANSI table restore
await inputHandler.parseP('\x1b]104\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE}]]);
});
it('10: FG set & query events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single foreground query --> color undefined
await inputHandler.parseP('\x1b]10;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: SpecialColorIndex.FOREGROUND }]]);
stack.length = 0;
// OSC with multiple values maps to OSC 10 & OSC 11 & OSC 12
await inputHandler.parseP('\x1b]10;?;?;?;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: SpecialColorIndex.FOREGROUND }], [{ type: ColorRequestType.REPORT, index: SpecialColorIndex.BACKGROUND }], [{ type: ColorRequestType.REPORT, index: SpecialColorIndex.CURSOR }]]);
stack.length = 0;
// set foreground color events
await inputHandler.parseP('\x1b]10;rgb:01/02/03\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: SpecialColorIndex.FOREGROUND, color: [1, 2, 3] }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]10;#aabbcc\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: SpecialColorIndex.FOREGROUND, color: [170, 187, 204] }]]);
stack.length = 0;
// set FG, BG and cursor color at once
await inputHandler.parseP('\x1b]10;rgb:aa/bb/cc;#001122;rgb:12/34/56\x07');
assert.deepEqual(stack, [
[{ type: ColorRequestType.SET, index: SpecialColorIndex.FOREGROUND, color: [170, 187, 204] }],
[{ type: ColorRequestType.SET, index: SpecialColorIndex.BACKGROUND, color: [0, 17, 34] }],
[{ type: ColorRequestType.SET, index: SpecialColorIndex.CURSOR, color: [18, 52, 86] }]
]);
});
it('110: restore FG color', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]110\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.FOREGROUND }]]);
});
it('11: BG set & query events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single background query --> color undefined
await inputHandler.parseP('\x1b]11;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: SpecialColorIndex.BACKGROUND }]]);
stack.length = 0;
// OSC 11 with multiple values creates only BG and cursor event
await inputHandler.parseP('\x1b]11;?;?;?;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: SpecialColorIndex.BACKGROUND }], [{ type: ColorRequestType.REPORT, index: SpecialColorIndex.CURSOR }]]);
stack.length = 0;
// set background color events
await inputHandler.parseP('\x1b]11;rgb:01/02/03\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: SpecialColorIndex.BACKGROUND, color: [1, 2, 3] }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]11;#aabbcc\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: SpecialColorIndex.BACKGROUND, color: [170, 187, 204] }]]);
stack.length = 0;
// set BG and cursor color at once
await inputHandler.parseP('\x1b]11;#001122;rgb:12/34/56\x07');
assert.deepEqual(stack, [
[{ type: ColorRequestType.SET, index: SpecialColorIndex.BACKGROUND, color: [0, 17, 34] }],
[{ type: ColorRequestType.SET, index: SpecialColorIndex.CURSOR, color: [18, 52, 86] }]
]);
});
it('111: restore BG color', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]111\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.BACKGROUND }]]);
});
it('12: cursor color set & query events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single cursor query --> color undefined
await inputHandler.parseP('\x1b]12;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: SpecialColorIndex.CURSOR }]]);
stack.length = 0;
// OSC 12 with multiple values creates only cursor event
await inputHandler.parseP('\x1b]12;?;?;?;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: SpecialColorIndex.CURSOR }]]);
stack.length = 0;
// set cursor color events
await inputHandler.parseP('\x1b]12;rgb:01/02/03\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: SpecialColorIndex.CURSOR, color: [1, 2, 3] }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]12;#aabbcc\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: SpecialColorIndex.CURSOR, color: [170, 187, 204] }]]);
});
it('112: restore cursor color', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]112\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: SpecialColorIndex.CURSOR }]]);
});
});
// issue #3362 and #2979
describe('EL/ED cursor at buffer.cols', () => {
beforeEach(() => {
bufferService.resize(10, 5);
});
describe('cursor should stay at cols / does not overflow', () => {
it('EL0', async () => {
await inputHandler.parseP('##########\x1b[0K');
assert.equal(bufferService.buffer.x, 10);
assert.deepEqual(getLines(bufferService), ['#'.repeat(10), '', '', '', '']);
});
it('EL1', async () => {
await inputHandler.parseP('##########\x1b[1K');
assert.equal(bufferService.buffer.x, 10);
assert.deepEqual(getLines(bufferService), ['', '', '', '', '']);
});
it('EL2', async () => {
await inputHandler.parseP('##########\x1b[2K');
assert.equal(bufferService.buffer.x, 10);
assert.deepEqual(getLines(bufferService), ['', '', '', '', '']);
});
it('ED0', async () => {
await inputHandler.parseP('##########\x1b[0J');
assert.equal(bufferService.buffer.x, 10);
assert.deepEqual(getLines(bufferService), ['#'.repeat(10), '', '', '', '']);
});
it('ED1', async () => {
await inputHandler.parseP('##########\x1b[1J');
assert.equal(bufferService.buffer.x, 10);
assert.deepEqual(getLines(bufferService), ['', '', '', '', '']);
});
it('ED2', async () => {
await inputHandler.parseP('##########\x1b[2J');
assert.equal(bufferService.buffer.x, 10);
assert.deepEqual(getLines(bufferService), ['', '', '', '', '']);
});
it('ED3', async () => {
await inputHandler.parseP('##########\x1b[3J');
assert.equal(bufferService.buffer.x, 10);
assert.deepEqual(getLines(bufferService), ['#'.repeat(10), '', '', '', '']);
});
});
describe('following sequence keeps working', () => {
// sequences to test (cursor related ones)
const SEQ = [
/* ICH */ '\x1b[10@',
/* SL */ '\x1b[10 @',
/* CUU */ '\x1b[10A',
/* SR */ '\x1b[10 A',
/* CUD */ '\x1b[10B',
/* CUF */ '\x1b[10C',
/* CUB */ '\x1b[10D',
/* CNL */ '\x1b[10E',
/* CPL */ '\x1b[10F',
/* CHA */ '\x1b[10G',
/* CUP */ '\x1b[10;10H',
/* CHT */ '\x1b[10I',
/* IL */ '\x1b[10L',
/* DL */ '\x1b[10M',
/* DCH */ '\x1b[10P',
/* SU */ '\x1b[10S',
/* SD */ '\x1b[10T',
/* ECH */ '\x1b[10X',
/* CBT */ '\x1b[10Z',
/* HPA */ '\x1b[10`',
/* HPR */ '\x1b[10a',
/* REP */ '\x1b[10b',
/* VPA */ '\x1b[10d',
/* VPR */ '\x1b[10e',
/* HVP */ '\x1b[10;10f',
/* TBC */ '\x1b[0g',
/* SCOSC */ '\x1b[s',
/* DECIC */ '\x1b[10\'}',
/* DECDC */ '\x1b[10\'~'
];
it('cursor never advances beyond cols', async () => {
for (const seq of SEQ) {
await inputHandler.parseP('##########\x1b[2J' + seq);
assert.equal(bufferService.buffer.x <= bufferService.cols, true);
inputHandler.reset();
bufferService.reset();
}
});
});
});
describe('DECSCA and DECSED/DECSEL', () => {
it('default is unprotected', async () => {
await inputHandler.parseP('some text');
await inputHandler.parseP('\x1b[?2K');
assert.deepEqual(getLines(bufferService, 2), ['', '']);
await inputHandler.parseP('some text');
await inputHandler.parseP('\x1b[?2J');
assert.deepEqual(getLines(bufferService, 2), ['', '']);
});
it('DECSCA 1 with DECSEL', async () => {
await inputHandler.parseP('###\x1b[1"qlineerase\x1b[0"q***');
await inputHandler.parseP('\x1b[?2K');
assert.deepEqual(getLines(bufferService, 2), [' lineerase', '']);
// normal EL works as before
await inputHandler.parseP('\x1b[2K');
assert.deepEqual(getLines(bufferService, 2), ['', '']);
});
it('DECSCA 1 with DECSED', async () => {
await inputHandler.parseP('###\x1b[1"qdisplayerase\x1b[0"q***');
await inputHandler.parseP('\x1b[?2J');
assert.deepEqual(getLines(bufferService, 2), [' displayerase', '']);
// normal ED works as before
await inputHandler.parseP('\x1b[2J');
assert.deepEqual(getLines(bufferService, 2), ['', '']);
});
it('DECRQSS reports correct DECSCA state', async () => {
const sendStack: string[] = [];
coreService.onData(d => sendStack.push(d));
// DCS $ q " q ST
await inputHandler.parseP('\x1bP$q"q\x1b\\');
// default - DECSCA unset (0 or 2)
assert.deepEqual(sendStack.pop(), '\x1bP1$r0"q\x1b\\');
// DECSCA 1 - protected set
await inputHandler.parseP('###\x1b[1"q');
await inputHandler.parseP('\x1bP$q"q\x1b\\');
assert.deepEqual(sendStack.pop(), '\x1bP1$r1"q\x1b\\');
// DECSCA 2 - protected reset (same as 0)
await inputHandler.parseP('###\x1b[2"q');
await inputHandler.parseP('\x1bP$q"q\x1b\\');
assert.deepEqual(sendStack.pop(), '\x1bP1$r0"q\x1b\\'); // reported as DECSCA 0
});
});
describe('DECRQM', () => {
const reportStack: string[] = [];
beforeEach(() => {
reportStack.length = 0;
coreService.onData(data => reportStack.push(data));
});
it('ANSI 2 (keyboard action mode)', async () => {
await inputHandler.parseP('\x1b[2$p');
assert.deepEqual(reportStack.pop(), '\x1b[2;4$y'); // always reset
});
it('ANSI 4 (insert mode)', async () => {
await inputHandler.parseP('\x1b[4$p');
assert.deepEqual(reportStack.pop(), '\x1b[4;2$y'); // reset by default
await inputHandler.parseP('\x1b[4h');
await inputHandler.parseP('\x1b[4$p');
assert.deepEqual(reportStack.pop(), '\x1b[4;1$y'); // now active
await inputHandler.parseP('\x1b[4l');
await inputHandler.parseP('\x1b[4$p');
assert.deepEqual(reportStack.pop(), '\x1b[4;2$y'); // again reset
});
it('ANSI 12 (send/receive)', async () => {
await inputHandler.parseP('\x1b[12$p');
assert.deepEqual(reportStack.pop(), '\x1b[12;3$y'); // always set
});
it('ANSI 20 (newline mode)', async () => {
await inputHandler.parseP('\x1b[20$p');
assert.deepEqual(reportStack.pop(), '\x1b[20;2$y'); // reset by default
await inputHandler.parseP('\x1b[20h');
await inputHandler.parseP('\x1b[20$p');
assert.deepEqual(reportStack.pop(), '\x1b[20;1$y'); // now active
await inputHandler.parseP('\x1b[20l');
await inputHandler.parseP('\x1b[20$p');
assert.deepEqual(reportStack.pop(), '\x1b[20;2$y'); // again reset
});
it('ANSI unknown', async () => {
await inputHandler.parseP('\x1b[1234$p');
assert.deepEqual(reportStack.pop(), '\x1b[1234;0$y'); // not recognized
});
it('DEC privates with set/reset semantic', async () => {
// initially reset
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
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`); // again reset
}
// initially set
const set = [7, 25];
for (const mode of set) {
await inputHandler.parseP(`\x1b[?${mode}$p`);
assert.deepEqual(reportStack.pop(), `\x1b[?${mode};1$y`); // initial set
await inputHandler.parseP(`\x1b[?${mode}l`);
await inputHandler.parseP(`\x1b[?${mode}$p`);
assert.deepEqual(reportStack.pop(), `\x1b[?${mode};2$y`); // now inactive
await inputHandler.parseP(`\x1b[?${mode}h`);
await inputHandler.parseP(`\x1b[?${mode}$p`);
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]];
for (const [mode, value] of perma) {
await inputHandler.parseP(`\x1b[?${mode}$p`);
assert.deepEqual(reportStack.pop(), `\x1b[?${mode};${value}$y`);
}
});
});
describe('InputHandler - kitty keyboard', () => {
let bufferService: IBufferService;
let coreService: ICoreService;
let optionsService: MockOptionsService;
let inputHandler: TestInputHandler;
beforeEach(() => {
optionsService = new MockOptionsService({ vtExtensions: { kittyKeyboard: true } });
bufferService = new BufferService(optionsService, new MockLogService());
bufferService.resize(80, 30);
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
});
describe('stack limit', () => {
it('should evict oldest entry when stack exceeds 16 entries', async () => {
for (let i = 1; i <= 20; i++) {
await inputHandler.parseP(`\x1b[>${i}u`);
}
assert.strictEqual(coreService.kittyKeyboard.mainStack.length, 16);
assert.strictEqual(coreService.kittyKeyboard.mainStack[0], 4);
});
});
describe('buffer switch', () => {
it('should maintain separate flags for main and alt screens', async () => {
await inputHandler.parseP('\x1b[>5u');
assert.strictEqual(coreService.kittyKeyboard.flags, 5);
await inputHandler.parseP('\x1b[?1049h');
assert.strictEqual(coreService.kittyKeyboard.flags, 0);
assert.strictEqual(coreService.kittyKeyboard.mainFlags, 5);
await inputHandler.parseP('\x1b[>7u');
assert.strictEqual(coreService.kittyKeyboard.flags, 7);
await inputHandler.parseP('\x1b[?1049l');
assert.strictEqual(coreService.kittyKeyboard.flags, 5);
assert.strictEqual(coreService.kittyKeyboard.altFlags, 7);
});
});
describe('pop reset', () => {
it('should reset flags to 0 when stack is emptied', async () => {
await inputHandler.parseP('\x1b[>5u');
assert.strictEqual(coreService.kittyKeyboard.flags, 5);
await inputHandler.parseP('\x1b[<10u');
assert.strictEqual(coreService.kittyKeyboard.flags, 0);
});
});
});
describe('InputHandler - async handlers', () => {
let bufferService: IBufferService;
let coreService: ICoreService;
let optionsService: MockOptionsService;
let inputHandler: TestInputHandler;
beforeEach(() => {
optionsService = new MockOptionsService();
bufferService = new BufferService(optionsService, new MockLogService());
bufferService.resize(80, 30);
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
coreService.onData(data => { console.log(data); });
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
});
it('async CUP with CPR check', async () => {
const cup: number[][] = [];
const cpr: number[][] = [];
inputHandler.registerCsiHandler({ final: 'H' }, async params => {
cup.push(params.toArray() as number[]);
await Promise.resolve();
// late call of real repositioning
return inputHandler.cursorPosition(params);
});
coreService.onData(data => {
const m = data.match(/\x1b\[(.*?);(.*?)R/);
if (m) {
cpr.push([parseInt(m[1]), parseInt(m[2])]);
}
});
await inputHandler.parseP('aaa\x1b[3;4H\x1b[6nbbb\x1b[6;8H\x1b[6n');
assert.deepEqual(cup, cpr);
});
it('async OSC between', async () => {
inputHandler.registerOscHandler(1000, async data => {
await Promise.resolve();
assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']);
assert.equal(data, 'some data');
return true;
});
await inputHandler.parseP('hello world!\r\n\x1b]1000;some data\x07second line');
assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']);
});
it('async DCS between', async () => {
inputHandler.registerDcsHandler({ final: 'a' }, async (data, params) => {
await Promise.resolve();
assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']);
assert.equal(data, 'some data');
assert.deepEqual(params.toArray(), [1, 2]);
return true;
});
await inputHandler.parseP('hello world!\r\n\x1bP1;2asome data\x1b\\second line');
assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']);
});
});
});