xterm
Version:
Full xterm terminal, in your browser
1,120 lines (1,033 loc) • 146 kB
text/typescript
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* @license MIT
*/
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType, SpecialColorIndex } from 'common/Types';
import { C0, C1 } from 'common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets';
import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
import { Disposable } from 'common/Lifecycle';
import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { EventEmitter } from 'common/EventEmitter';
import { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { AttributeData } from 'common/buffer/AttributeData';
import { ICoreService, IBufferService, IOptionsService, ILogService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services';
import { OscHandler } from 'common/parser/OscParser';
import { DcsHandler } from 'common/parser/DcsParser';
import { IBuffer } from 'common/buffer/Types';
import { parseColor } from 'common/input/XParseColor';
/**
* Map collect to glevel. Used in `selectCharset`.
*/
const GLEVEL: { [key: string]: number } = { '(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2 };
/**
* VT commands done by the parser - FIXME: move this to the parser?
*/
// @vt: #Y ESC CSI "Control Sequence Introducer" "ESC [" "Start of a CSI sequence."
// @vt: #Y ESC OSC "Operating System Command" "ESC ]" "Start of an OSC sequence."
// @vt: #Y ESC DCS "Device Control String" "ESC P" "Start of a DCS sequence."
// @vt: #Y ESC ST "String Terminator" "ESC \" "Terminator used for string type sequences."
// @vt: #Y ESC PM "Privacy Message" "ESC ^" "Start of a privacy message."
// @vt: #Y ESC APC "Application Program Command" "ESC _" "Start of an APC sequence."
// @vt: #Y C1 CSI "Control Sequence Introducer" "\x9B" "Start of a CSI sequence."
// @vt: #Y C1 OSC "Operating System Command" "\x9D" "Start of an OSC sequence."
// @vt: #Y C1 DCS "Device Control String" "\x90" "Start of a DCS sequence."
// @vt: #Y C1 ST "String Terminator" "\x9C" "Terminator used for string type sequences."
// @vt: #Y C1 PM "Privacy Message" "\x9E" "Start of a privacy message."
// @vt: #Y C1 APC "Application Program Command" "\x9F" "Start of an APC sequence."
// @vt: #Y C0 NUL "Null" "\0, \x00" "NUL is ignored."
// @vt: #Y C0 ESC "Escape" "\e, \x1B" "Start of a sequence. Cancels any other sequence."
/**
* Document xterm VT features here that are currently unsupported
*/
// @vt: #E[Supported via xterm-addon-image.] DCS SIXEL "SIXEL Graphics" "DCS Ps ; Ps ; Ps ; q Pt ST" "Draw SIXEL image."
// @vt: #N DCS DECUDK "User Defined Keys" "DCS Ps ; Ps \| Pt ST" "Definitions for user-defined keys."
// @vt: #N DCS XTGETTCAP "Request Terminfo String" "DCS + q Pt ST" "Request Terminfo String."
// @vt: #N DCS XTSETTCAP "Set Terminfo Data" "DCS + p Pt ST" "Set Terminfo Data."
// @vt: #N OSC 1 "Set Icon Name" "OSC 1 ; Pt BEL" "Set icon name."
/**
* Max length of the UTF32 input buffer. Real memory consumption is 4 times higher.
*/
const MAX_PARSEBUFFER_LENGTH = 131072;
/**
* Limit length of title and icon name stacks.
*/
const STACK_LIMIT = 10;
// map params to window option
function paramToWindowOption(n: number, opts: IWindowOptions): boolean {
if (n > 24) {
return opts.setWinLines || false;
}
switch (n) {
case 1: return !!opts.restoreWin;
case 2: return !!opts.minimizeWin;
case 3: return !!opts.setWinPosition;
case 4: return !!opts.setWinSizePixels;
case 5: return !!opts.raiseWin;
case 6: return !!opts.lowerWin;
case 7: return !!opts.refreshWin;
case 8: return !!opts.setWinSizeChars;
case 9: return !!opts.maximizeWin;
case 10: return !!opts.fullscreenWin;
case 11: return !!opts.getWinState;
case 13: return !!opts.getWinPosition;
case 14: return !!opts.getWinSizePixels;
case 15: return !!opts.getScreenSizePixels;
case 16: return !!opts.getCellSizePixels;
case 18: return !!opts.getWinSizeChars;
case 19: return !!opts.getScreenSizeChars;
case 20: return !!opts.getIconTitle;
case 21: return !!opts.getWinTitle;
case 22: return !!opts.pushTitle;
case 23: return !!opts.popTitle;
case 24: return !!opts.setWinLines;
}
return false;
}
export enum WindowsOptionsReportType {
GET_WIN_SIZE_PIXELS = 0,
GET_CELL_SIZE_PIXELS = 1
}
// create a warning log if an async handler takes longer than the limit (in ms)
const SLOW_ASYNC_LIMIT = 5000;
// Work variables to avoid garbage collection
let $temp = 0;
/**
* The terminal's standard implementation of IInputHandler, this handles all
* input from the Parser.
*
* Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand
* each function's header comment.
*/
export class InputHandler extends Disposable implements IInputHandler {
private _parseBuffer: Uint32Array = new Uint32Array(4096);
private _stringDecoder: StringToUtf32 = new StringToUtf32();
private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32();
private _workCell: CellData = new CellData();
private _windowTitle = '';
private _iconName = '';
private _dirtyRowTracker: IDirtyRowTracker;
protected _windowTitleStack: string[] = [];
protected _iconNameStack: string[] = [];
private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone();
public getAttrData(): IAttributeData { return this._curAttrData; }
private _eraseAttrDataInternal: IAttributeData = DEFAULT_ATTR_DATA.clone();
private _activeBuffer: IBuffer;
private readonly _onRequestBell = this.register(new EventEmitter<void>());
public readonly onRequestBell = this._onRequestBell.event;
private readonly _onRequestRefreshRows = this.register(new EventEmitter<number, number>());
public readonly onRequestRefreshRows = this._onRequestRefreshRows.event;
private readonly _onRequestReset = this.register(new EventEmitter<void>());
public readonly onRequestReset = this._onRequestReset.event;
private readonly _onRequestSendFocus = this.register(new EventEmitter<void>());
public readonly onRequestSendFocus = this._onRequestSendFocus.event;
private readonly _onRequestSyncScrollBar = this.register(new EventEmitter<void>());
public readonly onRequestSyncScrollBar = this._onRequestSyncScrollBar.event;
private readonly _onRequestWindowsOptionsReport = this.register(new EventEmitter<WindowsOptionsReportType>());
public readonly onRequestWindowsOptionsReport = this._onRequestWindowsOptionsReport.event;
private readonly _onA11yChar = this.register(new EventEmitter<string>());
public readonly onA11yChar = this._onA11yChar.event;
private readonly _onA11yTab = this.register(new EventEmitter<number>());
public readonly onA11yTab = this._onA11yTab.event;
private readonly _onCursorMove = this.register(new EventEmitter<void>());
public readonly onCursorMove = this._onCursorMove.event;
private readonly _onLineFeed = this.register(new EventEmitter<void>());
public readonly onLineFeed = this._onLineFeed.event;
private readonly _onScroll = this.register(new EventEmitter<number>());
public readonly onScroll = this._onScroll.event;
private readonly _onTitleChange = this.register(new EventEmitter<string>());
public readonly onTitleChange = this._onTitleChange.event;
private readonly _onColor = this.register(new EventEmitter<IColorEvent>());
public readonly onColor = this._onColor.event;
private _parseStack: IParseStack = {
paused: false,
cursorStartX: 0,
cursorStartY: 0,
decodedLength: 0,
position: 0
};
constructor(
private readonly _bufferService: IBufferService,
private readonly _charsetService: ICharsetService,
private readonly _coreService: ICoreService,
private readonly _logService: ILogService,
private readonly _optionsService: IOptionsService,
private readonly _oscLinkService: IOscLinkService,
private readonly _coreMouseService: ICoreMouseService,
private readonly _unicodeService: IUnicodeService,
private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser()
) {
super();
this.register(this._parser);
this._dirtyRowTracker = new DirtyRowTracker(this._bufferService);
// Track properties used in performance critical code manually to avoid using slow getters
this._activeBuffer = this._bufferService.buffer;
this.register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer));
/**
* custom fallback handlers
*/
this._parser.setCsiHandlerFallback((ident, params) => {
this._logService.debug('Unknown CSI code: ', { identifier: this._parser.identToString(ident), params: params.toArray() });
});
this._parser.setEscHandlerFallback(ident => {
this._logService.debug('Unknown ESC code: ', { identifier: this._parser.identToString(ident) });
});
this._parser.setExecuteHandlerFallback(code => {
this._logService.debug('Unknown EXECUTE code: ', { code });
});
this._parser.setOscHandlerFallback((identifier, action, data) => {
this._logService.debug('Unknown OSC code: ', { identifier, action, data });
});
this._parser.setDcsHandlerFallback((ident, action, payload) => {
if (action === 'HOOK') {
payload = payload.toArray();
}
this._logService.debug('Unknown DCS code: ', { identifier: this._parser.identToString(ident), action, payload });
});
/**
* print handler
*/
this._parser.setPrintHandler((data, start, end) => this.print(data, start, end));
/**
* CSI handler
*/
this._parser.registerCsiHandler({ final: '@' }, params => this.insertChars(params));
this._parser.registerCsiHandler({ intermediates: ' ', final: '@' }, params => this.scrollLeft(params));
this._parser.registerCsiHandler({ final: 'A' }, params => this.cursorUp(params));
this._parser.registerCsiHandler({ intermediates: ' ', final: 'A' }, params => this.scrollRight(params));
this._parser.registerCsiHandler({ final: 'B' }, params => this.cursorDown(params));
this._parser.registerCsiHandler({ final: 'C' }, params => this.cursorForward(params));
this._parser.registerCsiHandler({ final: 'D' }, params => this.cursorBackward(params));
this._parser.registerCsiHandler({ final: 'E' }, params => this.cursorNextLine(params));
this._parser.registerCsiHandler({ final: 'F' }, params => this.cursorPrecedingLine(params));
this._parser.registerCsiHandler({ final: 'G' }, params => this.cursorCharAbsolute(params));
this._parser.registerCsiHandler({ final: 'H' }, params => this.cursorPosition(params));
this._parser.registerCsiHandler({ final: 'I' }, params => this.cursorForwardTab(params));
this._parser.registerCsiHandler({ final: 'J' }, params => this.eraseInDisplay(params, false));
this._parser.registerCsiHandler({ prefix: '?', final: 'J' }, params => this.eraseInDisplay(params, true));
this._parser.registerCsiHandler({ final: 'K' }, params => this.eraseInLine(params, false));
this._parser.registerCsiHandler({ prefix: '?', final: 'K' }, params => this.eraseInLine(params, true));
this._parser.registerCsiHandler({ final: 'L' }, params => this.insertLines(params));
this._parser.registerCsiHandler({ final: 'M' }, params => this.deleteLines(params));
this._parser.registerCsiHandler({ final: 'P' }, params => this.deleteChars(params));
this._parser.registerCsiHandler({ final: 'S' }, params => this.scrollUp(params));
this._parser.registerCsiHandler({ final: 'T' }, params => this.scrollDown(params));
this._parser.registerCsiHandler({ final: 'X' }, params => this.eraseChars(params));
this._parser.registerCsiHandler({ final: 'Z' }, params => this.cursorBackwardTab(params));
this._parser.registerCsiHandler({ final: '`' }, params => this.charPosAbsolute(params));
this._parser.registerCsiHandler({ final: 'a' }, params => this.hPositionRelative(params));
this._parser.registerCsiHandler({ final: 'b' }, params => this.repeatPrecedingCharacter(params));
this._parser.registerCsiHandler({ final: 'c' }, params => this.sendDeviceAttributesPrimary(params));
this._parser.registerCsiHandler({ prefix: '>', final: 'c' }, params => this.sendDeviceAttributesSecondary(params));
this._parser.registerCsiHandler({ final: 'd' }, params => this.linePosAbsolute(params));
this._parser.registerCsiHandler({ final: 'e' }, params => this.vPositionRelative(params));
this._parser.registerCsiHandler({ final: 'f' }, params => this.hVPosition(params));
this._parser.registerCsiHandler({ final: 'g' }, params => this.tabClear(params));
this._parser.registerCsiHandler({ final: 'h' }, params => this.setMode(params));
this._parser.registerCsiHandler({ prefix: '?', final: 'h' }, params => this.setModePrivate(params));
this._parser.registerCsiHandler({ final: 'l' }, params => this.resetMode(params));
this._parser.registerCsiHandler({ prefix: '?', final: 'l' }, params => this.resetModePrivate(params));
this._parser.registerCsiHandler({ final: 'm' }, params => this.charAttributes(params));
this._parser.registerCsiHandler({ final: 'n' }, params => this.deviceStatus(params));
this._parser.registerCsiHandler({ prefix: '?', final: 'n' }, params => this.deviceStatusPrivate(params));
this._parser.registerCsiHandler({ intermediates: '!', final: 'p' }, params => this.softReset(params));
this._parser.registerCsiHandler({ intermediates: ' ', final: 'q' }, params => this.setCursorStyle(params));
this._parser.registerCsiHandler({ final: 'r' }, params => this.setScrollRegion(params));
this._parser.registerCsiHandler({ final: 's' }, params => this.saveCursor(params));
this._parser.registerCsiHandler({ final: 't' }, params => this.windowOptions(params));
this._parser.registerCsiHandler({ final: 'u' }, params => this.restoreCursor(params));
this._parser.registerCsiHandler({ intermediates: '\'', final: '}' }, params => this.insertColumns(params));
this._parser.registerCsiHandler({ intermediates: '\'', final: '~' }, params => this.deleteColumns(params));
this._parser.registerCsiHandler({ intermediates: '"', final: 'q' }, params => this.selectProtected(params));
this._parser.registerCsiHandler({ intermediates: '$', final: 'p' }, params => this.requestMode(params, true));
this._parser.registerCsiHandler({ prefix: '?', intermediates: '$', final: 'p' }, params => this.requestMode(params, false));
/**
* execute handler
*/
this._parser.setExecuteHandler(C0.BEL, () => this.bell());
this._parser.setExecuteHandler(C0.LF, () => this.lineFeed());
this._parser.setExecuteHandler(C0.VT, () => this.lineFeed());
this._parser.setExecuteHandler(C0.FF, () => this.lineFeed());
this._parser.setExecuteHandler(C0.CR, () => this.carriageReturn());
this._parser.setExecuteHandler(C0.BS, () => this.backspace());
this._parser.setExecuteHandler(C0.HT, () => this.tab());
this._parser.setExecuteHandler(C0.SO, () => this.shiftOut());
this._parser.setExecuteHandler(C0.SI, () => this.shiftIn());
// FIXME: What do to with missing? Old code just added those to print.
this._parser.setExecuteHandler(C1.IND, () => this.index());
this._parser.setExecuteHandler(C1.NEL, () => this.nextLine());
this._parser.setExecuteHandler(C1.HTS, () => this.tabSet());
/**
* OSC handler
*/
// 0 - icon name + title
this._parser.registerOscHandler(0, new OscHandler(data => { this.setTitle(data); this.setIconName(data); return true; }));
// 1 - icon name
this._parser.registerOscHandler(1, new OscHandler(data => this.setIconName(data)));
// 2 - title
this._parser.registerOscHandler(2, new OscHandler(data => this.setTitle(data)));
// 3 - set property X in the form "prop=value"
// 4 - Change Color Number
this._parser.registerOscHandler(4, new OscHandler(data => this.setOrReportIndexedColor(data)));
// 5 - Change Special Color Number
// 6 - Enable/disable Special Color Number c
// 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939)
// 8 - create hyperlink (not in xterm spec, see https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)
this._parser.registerOscHandler(8, new OscHandler(data => this.setHyperlink(data)));
// 10 - Change VT100 text foreground color to Pt.
this._parser.registerOscHandler(10, new OscHandler(data => this.setOrReportFgColor(data)));
// 11 - Change VT100 text background color to Pt.
this._parser.registerOscHandler(11, new OscHandler(data => this.setOrReportBgColor(data)));
// 12 - Change text cursor color to Pt.
this._parser.registerOscHandler(12, new OscHandler(data => this.setOrReportCursorColor(data)));
// 13 - Change mouse foreground color to Pt.
// 14 - Change mouse background color to Pt.
// 15 - Change Tektronix foreground color to Pt.
// 16 - Change Tektronix background color to Pt.
// 17 - Change highlight background color to Pt.
// 18 - Change Tektronix cursor color to Pt.
// 19 - Change highlight foreground color to Pt.
// 46 - Change Log File to Pt.
// 50 - Set Font to Pt.
// 51 - reserved for Emacs shell.
// 52 - Manipulate Selection Data.
// 104 ; c - Reset Color Number c.
this._parser.registerOscHandler(104, new OscHandler(data => this.restoreIndexedColor(data)));
// 105 ; c - Reset Special Color Number c.
// 106 ; c; f - Enable/disable Special Color Number c.
// 110 - Reset VT100 text foreground color.
this._parser.registerOscHandler(110, new OscHandler(data => this.restoreFgColor(data)));
// 111 - Reset VT100 text background color.
this._parser.registerOscHandler(111, new OscHandler(data => this.restoreBgColor(data)));
// 112 - Reset text cursor color.
this._parser.registerOscHandler(112, new OscHandler(data => this.restoreCursorColor(data)));
// 113 - Reset mouse foreground color.
// 114 - Reset mouse background color.
// 115 - Reset Tektronix foreground color.
// 116 - Reset Tektronix background color.
// 117 - Reset highlight color.
// 118 - Reset Tektronix cursor color.
// 119 - Reset highlight foreground color.
/**
* ESC handlers
*/
this._parser.registerEscHandler({ final: '7' }, () => this.saveCursor());
this._parser.registerEscHandler({ final: '8' }, () => this.restoreCursor());
this._parser.registerEscHandler({ final: 'D' }, () => this.index());
this._parser.registerEscHandler({ final: 'E' }, () => this.nextLine());
this._parser.registerEscHandler({ final: 'H' }, () => this.tabSet());
this._parser.registerEscHandler({ final: 'M' }, () => this.reverseIndex());
this._parser.registerEscHandler({ final: '=' }, () => this.keypadApplicationMode());
this._parser.registerEscHandler({ final: '>' }, () => this.keypadNumericMode());
this._parser.registerEscHandler({ final: 'c' }, () => this.fullReset());
this._parser.registerEscHandler({ final: 'n' }, () => this.setgLevel(2));
this._parser.registerEscHandler({ final: 'o' }, () => this.setgLevel(3));
this._parser.registerEscHandler({ final: '|' }, () => this.setgLevel(3));
this._parser.registerEscHandler({ final: '}' }, () => this.setgLevel(2));
this._parser.registerEscHandler({ final: '~' }, () => this.setgLevel(1));
this._parser.registerEscHandler({ intermediates: '%', final: '@' }, () => this.selectDefaultCharset());
this._parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => this.selectDefaultCharset());
for (const flag in CHARSETS) {
this._parser.registerEscHandler({ intermediates: '(', final: flag }, () => this.selectCharset('(' + flag));
this._parser.registerEscHandler({ intermediates: ')', final: flag }, () => this.selectCharset(')' + flag));
this._parser.registerEscHandler({ intermediates: '*', final: flag }, () => this.selectCharset('*' + flag));
this._parser.registerEscHandler({ intermediates: '+', final: flag }, () => this.selectCharset('+' + flag));
this._parser.registerEscHandler({ intermediates: '-', final: flag }, () => this.selectCharset('-' + flag));
this._parser.registerEscHandler({ intermediates: '.', final: flag }, () => this.selectCharset('.' + flag));
this._parser.registerEscHandler({ intermediates: '/', final: flag }, () => this.selectCharset('/' + flag)); // TODO: supported?
}
this._parser.registerEscHandler({ intermediates: '#', final: '8' }, () => this.screenAlignmentPattern());
/**
* error handler
*/
this._parser.setErrorHandler((state: IParsingState) => {
this._logService.error('Parsing error: ', state);
return state;
});
/**
* DCS handler
*/
this._parser.registerDcsHandler({ intermediates: '$', final: 'q' }, new DcsHandler((data, params) => this.requestStatusString(data, params)));
}
/**
* Async parse support.
*/
private _preserveStack(cursorStartX: number, cursorStartY: number, decodedLength: number, position: number): void {
this._parseStack.paused = true;
this._parseStack.cursorStartX = cursorStartX;
this._parseStack.cursorStartY = cursorStartY;
this._parseStack.decodedLength = decodedLength;
this._parseStack.position = position;
}
private _logSlowResolvingAsync(p: Promise<boolean>): void {
// log a limited warning about an async handler taking too long
if (this._logService.logLevel <= LogLevelEnum.WARN) {
Promise.race([p, new Promise((res, rej) => setTimeout(() => rej('#SLOW_TIMEOUT'), SLOW_ASYNC_LIMIT))])
.catch(err => {
if (err !== '#SLOW_TIMEOUT') {
throw err;
}
console.warn(`async parser handler taking longer than ${SLOW_ASYNC_LIMIT} ms`);
});
}
}
private _getCurrentLinkId(): number {
return this._curAttrData.extended.urlId;
}
/**
* Parse call with async handler support.
*
* Whether the stack state got preserved for the next call, is indicated by the return value:
* - undefined (void):
* all handlers were sync, no stack save, continue normally with next chunk
* - Promise\<boolean\>:
* execution stopped at async handler, stack saved, continue with same chunk and the promise
* resolve value as `promiseResult` until the method returns `undefined`
*
* Note: This method should only be called by `Terminal.write` to ensure correct execution order
* and proper continuation of async parser handlers.
*/
public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise<boolean> {
let result: void | Promise<boolean>;
let cursorStartX = this._activeBuffer.x;
let cursorStartY = this._activeBuffer.y;
let start = 0;
const wasPaused = this._parseStack.paused;
if (wasPaused) {
// assumption: _parseBuffer never mutates between async calls
if (result = this._parser.parse(this._parseBuffer, this._parseStack.decodedLength, promiseResult)) {
this._logSlowResolvingAsync(result);
return result;
}
cursorStartX = this._parseStack.cursorStartX;
cursorStartY = this._parseStack.cursorStartY;
this._parseStack.paused = false;
if (data.length > MAX_PARSEBUFFER_LENGTH) {
start = this._parseStack.position + MAX_PARSEBUFFER_LENGTH;
}
}
// Log debug data, the log level gate is to prevent extra work in this hot path
if (this._logService.logLevel <= LogLevelEnum.DEBUG) {
this._logService.debug(`parsing data${typeof data === 'string' ? ` "${data}"` : ` "${Array.prototype.map.call(data, e => String.fromCharCode(e)).join('')}"`}`, typeof data === 'string'
? data.split('').map(e => e.charCodeAt(0))
: data
);
}
// resize input buffer if needed
if (this._parseBuffer.length < data.length) {
if (this._parseBuffer.length < MAX_PARSEBUFFER_LENGTH) {
this._parseBuffer = new Uint32Array(Math.min(data.length, MAX_PARSEBUFFER_LENGTH));
}
}
// Clear the dirty row service so we know which lines changed as a result of parsing
// Important: do not clear between async calls, otherwise we lost pending update information.
if (!wasPaused) {
this._dirtyRowTracker.clearRange();
}
// process big data in smaller chunks
if (data.length > MAX_PARSEBUFFER_LENGTH) {
for (let i = start; i < data.length; i += MAX_PARSEBUFFER_LENGTH) {
const end = i + MAX_PARSEBUFFER_LENGTH < data.length ? i + MAX_PARSEBUFFER_LENGTH : data.length;
const len = (typeof data === 'string')
? this._stringDecoder.decode(data.substring(i, end), this._parseBuffer)
: this._utf8Decoder.decode(data.subarray(i, end), this._parseBuffer);
if (result = this._parser.parse(this._parseBuffer, len)) {
this._preserveStack(cursorStartX, cursorStartY, len, i);
this._logSlowResolvingAsync(result);
return result;
}
}
} else {
if (!wasPaused) {
const len = (typeof data === 'string')
? this._stringDecoder.decode(data, this._parseBuffer)
: this._utf8Decoder.decode(data, this._parseBuffer);
if (result = this._parser.parse(this._parseBuffer, len)) {
this._preserveStack(cursorStartX, cursorStartY, len, 0);
this._logSlowResolvingAsync(result);
return result;
}
}
}
if (this._activeBuffer.x !== cursorStartX || this._activeBuffer.y !== cursorStartY) {
this._onCursorMove.fire();
}
// Refresh any dirty rows accumulated as part of parsing
this._onRequestRefreshRows.fire(this._dirtyRowTracker.start, this._dirtyRowTracker.end);
}
public print(data: Uint32Array, start: number, end: number): void {
let code: number;
let chWidth: number;
const charset = this._charsetService.charset;
const screenReaderMode = this._optionsService.rawOptions.screenReaderMode;
const cols = this._bufferService.cols;
const wraparoundMode = this._coreService.decPrivateModes.wraparound;
const insertMode = this._coreService.modes.insertMode;
const curAttr = this._curAttrData;
let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
// handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char
if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) {
bufferRow.setCellFromCodePoint(this._activeBuffer.x - 1, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended);
}
for (let pos = start; pos < end; ++pos) {
code = data[pos];
// calculate print space
// expensive call, therefore we save width in line buffer
chWidth = this._unicodeService.wcwidth(code);
// get charset replacement character
// charset is only defined for ASCII, therefore we only
// search for an replacement char if code < 127
if (code < 127 && charset) {
const ch = charset[String.fromCharCode(code)];
if (ch) {
code = ch.charCodeAt(0);
}
}
if (screenReaderMode) {
this._onA11yChar.fire(stringFromCodePoint(code));
}
if (this._getCurrentLinkId()) {
this._oscLinkService.addLineToLink(this._getCurrentLinkId(), this._activeBuffer.ybase + this._activeBuffer.y);
}
// insert combining char at last cursor position
// this._activeBuffer.x should never be 0 for a combining char
// since they always follow a cell consuming char
// therefore we can test for this._activeBuffer.x to avoid overflow left
if (!chWidth && this._activeBuffer.x) {
if (!bufferRow.getWidth(this._activeBuffer.x - 1)) {
// found empty cell after fullwidth, need to go 2 cells back
// it is save to step 2 cells back here
// since an empty cell is only set by fullwidth chars
bufferRow.addCodepointToCell(this._activeBuffer.x - 2, code);
} else {
bufferRow.addCodepointToCell(this._activeBuffer.x - 1, code);
}
continue;
}
// goto next line if ch would overflow
// NOTE: To avoid costly width checks here,
// the terminal does not allow a cols < 2.
if (this._activeBuffer.x + chWidth - 1 >= cols) {
// autowrap - DECAWM
// automatically wraps to the beginning of the next line
if (wraparoundMode) {
// clear left over cells to the right
while (this._activeBuffer.x < cols) {
bufferRow.setCellFromCodePoint(this._activeBuffer.x++, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended);
}
this._activeBuffer.x = 0;
this._activeBuffer.y++;
if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) {
this._activeBuffer.y--;
this._bufferService.scroll(this._eraseAttrData(), true);
} else {
if (this._activeBuffer.y >= this._bufferService.rows) {
this._activeBuffer.y = this._bufferService.rows - 1;
}
// The line already exists (eg. the initial viewport), mark it as a
// wrapped line
this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = true;
}
// row changed, get it again
bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;
} else {
this._activeBuffer.x = cols - 1;
if (chWidth === 2) {
// FIXME: check for xterm behavior
// What to do here? We got a wide char that does not fit into last cell
continue;
}
}
}
// insert mode: move characters to right
if (insertMode) {
// right shift cells according to the width
bufferRow.insertCells(this._activeBuffer.x, chWidth, this._activeBuffer.getNullCell(curAttr), curAttr);
// test last cell - since the last cell has only room for
// a halfwidth char any fullwidth shifted there is lost
// and will be set to empty cell
if (bufferRow.getWidth(cols - 1) === 2) {
bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr.fg, curAttr.bg, curAttr.extended);
}
}
// write current char to buffer and advance cursor
bufferRow.setCellFromCodePoint(this._activeBuffer.x++, code, chWidth, curAttr.fg, curAttr.bg, curAttr.extended);
// fullwidth char - also set next cell to placeholder stub and advance cursor
// for graphemes bigger than fullwidth we can simply loop to zero
// we already made sure above, that this._activeBuffer.x + chWidth will not overflow right
if (chWidth > 0) {
while (--chWidth) {
// other than a regular empty cell a cell following a wide char has no width
bufferRow.setCellFromCodePoint(this._activeBuffer.x++, 0, 0, curAttr.fg, curAttr.bg, curAttr.extended);
}
}
}
// store last char in Parser.precedingCodepoint for REP to work correctly
// This needs to check whether:
// - fullwidth + surrogates: reset
// - combining: only base char gets carried on (bug in xterm?)
if (end - start > 0) {
bufferRow.loadCell(this._activeBuffer.x - 1, this._workCell);
if (this._workCell.getWidth() === 2 || this._workCell.getCode() > 0xFFFF) {
this._parser.precedingCodepoint = 0;
} else if (this._workCell.isCombined()) {
this._parser.precedingCodepoint = this._workCell.getChars().charCodeAt(0);
} else {
this._parser.precedingCodepoint = this._workCell.content;
}
}
// handle wide chars: reset cell to the right if it is second cell of a wide char
if (this._activeBuffer.x < cols && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x) === 0 && !bufferRow.hasContent(this._activeBuffer.x)) {
bufferRow.setCellFromCodePoint(this._activeBuffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended);
}
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
/**
* Forward registerCsiHandler from parser.
*/
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable {
if (id.final === 't' && !id.prefix && !id.intermediates) {
// security: always check whether window option is allowed
return this._parser.registerCsiHandler(id, params => {
if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) {
return true;
}
return callback(params);
});
}
return this._parser.registerCsiHandler(id, callback);
}
/**
* Forward registerDcsHandler from parser.
*/
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable {
return this._parser.registerDcsHandler(id, new DcsHandler(callback));
}
/**
* Forward registerEscHandler from parser.
*/
public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable {
return this._parser.registerEscHandler(id, callback);
}
/**
* Forward registerOscHandler from parser.
*/
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this._parser.registerOscHandler(ident, new OscHandler(callback));
}
/**
* BEL
* Bell (Ctrl-G).
*
* @vt: #Y C0 BEL "Bell" "\a, \x07" "Ring the bell."
* The behavior of the bell is further customizable with `ITerminalOptions.bellStyle`
* and `ITerminalOptions.bellSound`.
*/
public bell(): boolean {
this._onRequestBell.fire();
return true;
}
/**
* LF
* Line Feed or New Line (NL). (LF is Ctrl-J).
*
* @vt: #Y C0 LF "Line Feed" "\n, \x0A" "Move the cursor one row down, scrolling if needed."
* Scrolling is restricted to scroll margins and will only happen on the bottom line.
*
* @vt: #Y C0 VT "Vertical Tabulation" "\v, \x0B" "Treated as LF."
* @vt: #Y C0 FF "Form Feed" "\f, \x0C" "Treated as LF."
*/
public lineFeed(): boolean {
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
if (this._optionsService.rawOptions.convertEol) {
this._activeBuffer.x = 0;
}
this._activeBuffer.y++;
if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) {
this._activeBuffer.y--;
this._bufferService.scroll(this._eraseAttrData());
} else if (this._activeBuffer.y >= this._bufferService.rows) {
this._activeBuffer.y = this._bufferService.rows - 1;
} else {
// There was an explicit line feed (not just a carriage return), so clear the wrapped state of
// the line. This is particularly important on conpty/Windows where revisiting lines to
// reprint is common, especially on resize. Note that the windowsMode wrapped line heuristics
// can mess with this so windowsMode should be disabled, which is recommended on Windows build
// 21376 and above.
this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;
}
// If the end of the line is hit, prevent this action from wrapping around to the next line.
if (this._activeBuffer.x >= this._bufferService.cols) {
this._activeBuffer.x--;
}
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
this._onLineFeed.fire();
return true;
}
/**
* CR
* Carriage Return (Ctrl-M).
*
* @vt: #Y C0 CR "Carriage Return" "\r, \x0D" "Move the cursor to the beginning of the row."
*/
public carriageReturn(): boolean {
this._activeBuffer.x = 0;
return true;
}
/**
* BS
* Backspace (Ctrl-H).
*
* @vt: #Y C0 BS "Backspace" "\b, \x08" "Move the cursor one position to the left."
* By default it is not possible to move the cursor past the leftmost position.
* If `reverse wrap-around` (`CSI ? 45 h`) is set, a previous soft line wrap (DECAWM)
* can be undone with BS within the scroll margins. In that case the cursor will wrap back
* to the end of the previous row. Note that it is not possible to peek back into the scrollbuffer
* with the cursor, thus at the home position (top-leftmost cell) this has no effect.
*/
public backspace(): boolean {
// reverse wrap-around is disabled
if (!this._coreService.decPrivateModes.reverseWraparound) {
this._restrictCursor();
if (this._activeBuffer.x > 0) {
this._activeBuffer.x--;
}
return true;
}
// reverse wrap-around is enabled
// other than for normal operation mode, reverse wrap-around allows the cursor
// to be at x=cols to be able to address the last cell of a row by BS
this._restrictCursor(this._bufferService.cols);
if (this._activeBuffer.x > 0) {
this._activeBuffer.x--;
} else {
/**
* reverse wrap-around handling:
* Our implementation deviates from xterm on purpose. Details:
* - only previous soft NLs can be reversed (isWrapped=true)
* - only works within scrollborders (top/bottom, left/right not yet supported)
* - cannot peek into scrollbuffer
* - any cursor movement sequence keeps working as expected
*/
if (this._activeBuffer.x === 0
&& this._activeBuffer.y > this._activeBuffer.scrollTop
&& this._activeBuffer.y <= this._activeBuffer.scrollBottom
&& this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)?.isWrapped) {
this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;
this._activeBuffer.y--;
this._activeBuffer.x = this._bufferService.cols - 1;
// find last taken cell - last cell can have 3 different states:
// - hasContent(true) + hasWidth(1): narrow char - we are done
// - hasWidth(0): second part of wide char - we are done
// - hasContent(false) + hasWidth(1): empty cell due to early wrapping wide char, go one
// cell further back
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;
if (line.hasWidth(this._activeBuffer.x) && !line.hasContent(this._activeBuffer.x)) {
this._activeBuffer.x--;
// We do this only once, since width=1 + hasContent=false currently happens only once
// before early wrapping of a wide char.
// This needs to be fixed once we support graphemes taking more than 2 cells.
}
}
}
this._restrictCursor();
return true;
}
/**
* TAB
* Horizontal Tab (HT) (Ctrl-I).
*
* @vt: #Y C0 HT "Horizontal Tabulation" "\t, \x09" "Move the cursor to the next character tab stop."
*/
public tab(): boolean {
if (this._activeBuffer.x >= this._bufferService.cols) {
return true;
}
const originalX = this._activeBuffer.x;
this._activeBuffer.x = this._activeBuffer.nextStop();
if (this._optionsService.rawOptions.screenReaderMode) {
this._onA11yTab.fire(this._activeBuffer.x - originalX);
}
return true;
}
/**
* SO
* Shift Out (Ctrl-N) -> Switch to Alternate Character Set. This invokes the
* G1 character set.
*
* @vt: #P[Only limited ISO-2022 charset support.] C0 SO "Shift Out" "\x0E" "Switch to an alternative character set."
*/
public shiftOut(): boolean {
this._charsetService.setgLevel(1);
return true;
}
/**
* SI
* Shift In (Ctrl-O) -> Switch to Standard Character Set. This invokes the G0
* character set (the default).
*
* @vt: #Y C0 SI "Shift In" "\x0F" "Return to regular character set after Shift Out."
*/
public shiftIn(): boolean {
this._charsetService.setgLevel(0);
return true;
}
/**
* Restrict cursor to viewport size / scroll margin (origin mode).
*/
private _restrictCursor(maxCol: number = this._bufferService.cols - 1): void {
this._activeBuffer.x = Math.min(maxCol, Math.max(0, this._activeBuffer.x));
this._activeBuffer.y = this._coreService.decPrivateModes.origin
? Math.min(this._activeBuffer.scrollBottom, Math.max(this._activeBuffer.scrollTop, this._activeBuffer.y))
: Math.min(this._bufferService.rows - 1, Math.max(0, this._activeBuffer.y));
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
/**
* Set absolute cursor position.
*/
private _setCursor(x: number, y: number): void {
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
if (this._coreService.decPrivateModes.origin) {
this._activeBuffer.x = x;
this._activeBuffer.y = this._activeBuffer.scrollTop + y;
} else {
this._activeBuffer.x = x;
this._activeBuffer.y = y;
}
this._restrictCursor();
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
/**
* Set relative cursor position.
*/
private _moveCursor(x: number, y: number): void {
// for relative changes we have to make sure we are within 0 .. cols/rows - 1
// before calculating the new position
this._restrictCursor();
this._setCursor(this._activeBuffer.x + x, this._activeBuffer.y + y);
}
/**
* CSI Ps A
* Cursor Up Ps Times (default = 1) (CUU).
*
* @vt: #Y CSI CUU "Cursor Up" "CSI Ps A" "Move cursor `Ps` times up (default=1)."
* If the cursor would pass the top scroll margin, it will stop there.
*/
public cursorUp(params: IParams): boolean {
// stop at scrollTop
const diffToTop = this._activeBuffer.y - this._activeBuffer.scrollTop;
if (diffToTop >= 0) {
this._moveCursor(0, -Math.min(diffToTop, params.params[0] || 1));
} else {
this._moveCursor(0, -(params.params[0] || 1));
}
return true;
}
/**
* CSI Ps B
* Cursor Down Ps Times (default = 1) (CUD).
*
* @vt: #Y CSI CUD "Cursor Down" "CSI Ps B" "Move cursor `Ps` times down (default=1)."
* If the cursor would pass the bottom scroll margin, it will stop there.
*/
public cursorDown(params: IParams): boolean {
// stop at scrollBottom
const diffToBottom = this._activeBuffer.scrollBottom - this._activeBuffer.y;
if (diffToBottom >= 0) {
this._moveCursor(0, Math.min(diffToBottom, params.params[0] || 1));
} else {
this._moveCursor(0, params.params[0] || 1);
}
return true;
}
/**
* CSI Ps C
* Cursor Forward Ps Times (default = 1) (CUF).
*
* @vt: #Y CSI CUF "Cursor Forward" "CSI Ps C" "Move cursor `Ps` times forward (default=1)."
*/
public cursorForward(params: IParams): boolean {
this._moveCursor(params.params[0] || 1, 0);
return true;
}
/**
* CSI Ps D
* Cursor Backward Ps Times (default = 1) (CUB).
*
* @vt: #Y CSI CUB "Cursor Backward" "CSI Ps D" "Move cursor `Ps` times backward (default=1)."
*/
public cursorBackward(params: IParams): boolean {
this._moveCursor(-(params.params[0] || 1), 0);
return true;
}
/**
* CSI Ps E
* Cursor Next Line Ps Times (default = 1) (CNL).
* Other than cursorDown (CUD) also set the cursor to first column.
*
* @vt: #Y CSI CNL "Cursor Next Line" "CSI Ps E" "Move cursor `Ps` times down (default=1) and to the first column."
* Same as CUD, additionally places the cursor at the first column.
*/
public cursorNextLine(params: IParams): boolean {
this.cursorDown(params);
this._activeBuffer.x = 0;
return true;
}
/**
* CSI Ps F
* Cursor Previous Line Ps Times (default = 1) (CPL).
* Other than cursorUp (CUU) also set the cursor to first column.
*
* @vt: #Y CSI CPL "Cursor Backward" "CSI Ps F" "Move cursor `Ps` times up (default=1) and to the first column."
* Same as CUU, additionally places the cursor at the first column.
*/
public cursorPrecedingLine(params: IParams): boolean {
this.cursorUp(params);
this._activeBuffer.x = 0;
return true;
}
/**
* CSI Ps G
* Cursor Character Absolute [column] (default = [row,1]) (CHA).
*
* @vt: #Y CSI CHA "Cursor Horizontal Absolute" "CSI Ps G" "Move cursor to `Ps`-th column of the active row (default=1)."
*/
public cursorCharAbsolute(params: IParams): boolean {
this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y);
return true;
}
/**
* CSI Ps ; Ps H
* Cursor Position [row;column] (default = [1,1]) (CUP).
*
* @vt: #Y CSI CUP "Cursor Position" "CSI Ps ; Ps H" "Set cursor to position [`Ps`, `Ps`] (default = [1, 1])."
* If ORIGIN mode is set, places the cursor to the absolute position within the scroll margins.
* If ORIGIN mode is not set, places the cursor to the absolute position within the viewport.
* Note that the coordinates are 1-based, thus the top left position starts at `1 ; 1`.
*/
public cursorPosition(params: IParams): boolean {
this._setCursor(
// col
(params.length >= 2) ? (params.params[1] || 1) - 1 : 0,
// row
(params.params[0] || 1) - 1
);
return true;
}
/**
* CSI Pm ` Character Position Absolute
* [column] (default = [row,1]) (HPA).
* Currently same functionality as CHA.
*
* @vt: #Y CSI HPA "Horizontal Position Absolute" "CSI Ps ` " "Same as CHA."
*/
public charPosAbsolute(params: IParams): boolean {
this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y);
return true;
}
/**
* CSI Pm a Character Position Relative
* [columns] (default = [row,col+1]) (HPR)
*
* @vt: #Y CSI HPR "Horizontal Position Relative" "CSI Ps a" "Same as CUF."
*/
public hPositionRelative(params: IParams): boolean {
this._moveCursor(params.params[0] || 1, 0);
return true;
}
/**
* CSI Pm d Vertical Position Absolute (VPA)
* [row] (default = [1,column])
*
* @vt: #Y CSI VPA "Vertical Position Absolute" "CSI Ps d" "Move cursor to `Ps`-th row (default=1)."
*/
public linePosAbsolute(params: IParams): boolean {
this._setCursor(this._activeBuffer.x, (params.params[0] || 1) - 1);
return true;
}
/**
* CSI Pm e Vertical Position Relative (VPR)
* [rows] (default = [row+1,column])
* reuse CSI Ps B ?
*
* @vt: #Y CSI VPR "Vertical Position Relative" "CSI Ps e" "Move cursor `Ps` times down (default=1)."
*/
public vPositionRelative(params: IParams): boolean {
this._moveCursor(0, params.params[0] || 1);
return true;
}
/**
* CSI Ps ; Ps f
* Horizontal and Vertical Position [row;column] (default =
* [1,1]) (HVP).
* Same as CUP.
*
* @vt: #Y CSI HVP "Horizontal and Vertical Position" "CSI Ps ; Ps f" "Same as CUP."
*/
public hVPosition(params: IParams): boolean {
this.cursorPosition(params);
return true;
}
/**
* CSI Ps g Tab Clear (TBC).
* Ps = 0 -> Clear Current Column (default).
* Ps = 3 -> Clear All.
* Potentially:
* Ps = 2 -> Clear Stops on Line.
* http://vt100.net/annarbor/aaa-ug/section6.html
*
* @vt: #Y CSI TBC "Tab Clear" "CSI Ps g" "Clear tab stops at current position (0) or all (3) (default=0)."
* Clearing tabstops off the active row (Ps = 2, VT100) is currently not supported.
*/
public tabClear(params: IParams): boolean {
const param = params.params[0];
if (param === 0) {
delete this._activeBuffer.tabs[this._activeBuffer.x];
} else if (param === 3) {
this._activeBuffer.tabs = {};
}
return true;
}
/**
* CSI Ps I
* Cursor Forward Tabulation Ps tab stops (default = 1) (CHT).
*
* @vt: #Y CSI CHT "Cursor Horizontal Tabulation" "CSI Ps I" "Move cursor `Ps` times tabs forward (default=1)."
*/
public cursorForwardTab(params: IParams): boolean {
if (this._activeBuffer.x >= this._bufferService.cols) {
return true;
}
let param = params.params[0] || 1;
while (param--) {
this._activeBuffer.x = this._activeBuffer.nextStop();
}
return true;
}
/**
* CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
*
* @vt: #Y CSI CBT "Cursor Backward Tabulation" "CSI Ps Z" "Move cursor `Ps` tabs backward (default=1)."
*/
public cursorBackwardTab(params: IParams): boolean {
if (this._activeBuffer.x >= this._bufferService.cols) {
return true;
}
let param = params.params[0] || 1;
while (param--) {
this._activeBuffer.x = this._activeBuffer.prevStop();
}
return true;
}
/**
* CSI Ps " q Select Character Protection Attribute (DECSCA).
*
* @vt: #Y CSI DECSCA "Select Character Protection Attribute" "CSI Ps " q" "Whether DECSED and DECSEL can erase (0=default, 2) or not (1)."
*/
public selectProtected(params: IParams): boolean {
const p = params.params[0];