UNPKG

xterm

Version:

Full xterm terminal, in your browser

1,120 lines (1,033 loc) • 146 kB
/** * 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];