/** * Copyright (c) 2014-2020 The xterm.js authors. All rights reserved. * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) * @license MIT * * Originally forked from (with the author's permission): * Fabrice Bellard's javascript vt100 for jslinux: * http://bellard.org/jslinux/ * Copyright (c) 2011 Fabrice Bellard * The original design remains. The terminal itself * has been extended to include xterm CSI codes, among * other features. * * Terminal Emulation References: * http://vt100.net/ * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html * http://invisible-island.net/vttest/ * http://www.inwap.com/pdp10/ansicode.txt * http://linux.die.net/man/4/console_codes * http://linux.die.net/man/7/urxvt */ import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, LogLevelEnum, ITerminalOptions, IOscLinkService, ICollapseService } from 'common/services/Services'; import { InstantiationService } from 'common/services/InstantiationService'; import { LogService } from 'common/services/LogService'; import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; import { OptionsService } from 'common/services/OptionsService'; import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent } from 'common/Types'; import { CoreService } from 'common/services/CoreService'; import { CoreMouseService } from 'common/services/CoreMouseService'; import { UnicodeService } from 'common/services/UnicodeService'; import { CharsetService } from 'common/services/CharsetService'; import { updateWindowsModeWrappedState } from 'common/WindowsMode'; import { IFunctionIdentifier, IParams } from 'common/parser/Types'; import { IBufferSet } from 'common/buffer/Types'; import { InputHandler } from 'common/InputHandler'; import { WriteBuffer } from 'common/input/WriteBuffer'; import { OscLinkService } from 'common/services/OscLinkService'; import { CollapseService } from 'common/services/CollapseService'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; // Only trigger this warning a single time per session let hasWriteSyncWarnHappened = false; export abstract class CoreTerminal extends Disposable implements ICoreTerminal { protected readonly _instantiationService: IInstantiationService; protected readonly _bufferService: IBufferService; protected readonly _logService: ILogService; protected readonly _charsetService: ICharsetService; protected readonly _oscLinkService: IOscLinkService; protected readonly _collapseService: ICollapseService; public readonly coreMouseService: ICoreMouseService; public readonly coreService: ICoreService; public readonly unicodeService: IUnicodeService; public readonly optionsService: IOptionsService; protected _inputHandler: InputHandler; private _writeBuffer: WriteBuffer; private _windowsWrappingHeuristics = this._register(new MutableDisposable()); private readonly _onBinary = this._register(new Emitter()); public readonly onBinary = this._onBinary.event; private readonly _onData = this._register(new Emitter()); public readonly onData = this._onData.event; protected _onLineFeed = this._register(new Emitter()); public readonly onLineFeed = this._onLineFeed.event; protected readonly _onRender = this._register(new Emitter<{ start: number, end: number }>()); public readonly onRender = this._onRender.event; private readonly _onResize = this._register(new Emitter<{ cols: number, rows: number }>()); public readonly onResize = this._onResize.event; protected readonly _onWriteParsed = this._register(new Emitter()); public readonly onWriteParsed = this._onWriteParsed.event; // Shell readiness tracking (OSC 133) private _shellReady = false; private readonly _onShellReady = this._register(new Emitter()); public readonly onShellReady = this._onShellReady.event; // OSC 133 shell integration events private readonly _onOsc133 = this._register(new Emitter<{ type: 'prompt' | 'commandStart' | 'commandExecute' | 'commandEnd'; data?: string; exitCode?: number }>()); public readonly onOsc133 = this._onOsc133.event; // OSC 7 current working directory tracking private _cwd: string | undefined; private readonly _onOsc7 = this._register(new Emitter<{ hostname: string; path: string }>()); public readonly onOsc7 = this._onOsc7.event; /** * Internally we track the source of the scroll but this is meaningless outside the library so * it's filtered out. */ protected _onScrollApi?: Emitter; protected _onScroll = this._register(new Emitter()); public get onScroll(): Event { if (!this._onScrollApi) { this._onScrollApi = this._register(new Emitter()); this._onScroll.event(ev => { this._onScrollApi?.fire(ev.position); }); } return this._onScrollApi.event; } public get cols(): number { return this._bufferService.cols; } public get rows(): number { return this._bufferService.rows; } public get buffers(): IBufferSet { return this._bufferService.buffers; } public get options(): Required { return this.optionsService.options; } public get shellReady(): boolean { return this._shellReady; } public get cwd(): string | undefined { return this._cwd; } public set options(options: ITerminalOptions) { for (const key in options) { this.optionsService.options[key] = options[key]; } } constructor( options: Partial ) { super(); // Setup and initialize services this._instantiationService = new InstantiationService(); this.optionsService = this._register(new OptionsService(options)); this._instantiationService.setService(IOptionsService, this.optionsService); this._bufferService = this._register(this._instantiationService.createInstance(BufferService)); this._instantiationService.setService(IBufferService, this._bufferService); this._logService = this._register(this._instantiationService.createInstance(LogService)); this._instantiationService.setService(ILogService, this._logService); this.coreService = this._register(this._instantiationService.createInstance(CoreService)); this._instantiationService.setService(ICoreService, this.coreService); this.coreMouseService = this._register(this._instantiationService.createInstance(CoreMouseService)); this._instantiationService.setService(ICoreMouseService, this.coreMouseService); this.unicodeService = this._register(this._instantiationService.createInstance(UnicodeService)); this._instantiationService.setService(IUnicodeService, this.unicodeService); this._charsetService = this._instantiationService.createInstance(CharsetService); this._instantiationService.setService(ICharsetService, this._charsetService); this._oscLinkService = this._instantiationService.createInstance(OscLinkService); this._instantiationService.setService(IOscLinkService, this._oscLinkService); this._collapseService = this._register(this._instantiationService.createInstance(CollapseService)); this._instantiationService.setService(ICollapseService, this._collapseService); // Register input handler and handle/forward events this._inputHandler = this._register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService, undefined, this._collapseService)); this._register(Event.forward(this._inputHandler.onLineFeed, this._onLineFeed)); // Listen for OSC 133 shell integration events this._register(this._inputHandler.onOsc133((event) => { // Mark shell as ready on first prompt if (event.type === 'prompt') { this.markShellReady(); } // Forward the event this._onOsc133.fire(event); })); // Listen for OSC 7 directory change events this._register(this._inputHandler.onOsc7((event) => { this.updateCwd(event.hostname, event.path); })); // Setup listeners this._register(Event.forward(this._bufferService.onResize, this._onResize)); this._register(Event.forward(this.coreService.onData, this._onData)); this._register(Event.forward(this.coreService.onBinary, this._onBinary)); this._register(this.coreService.onRequestScrollToBottom(() => this.scrollToBottom(true))); this._register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput())); this._register(this.optionsService.onMultipleOptionChange(['windowsPty'], () => this._handleWindowsPtyOptionChange())); this._register(this._bufferService.onScroll(() => { this._onScroll.fire({ position: this._bufferService.buffer.ydisp }); this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); })); // Setup WriteBuffer this._writeBuffer = this._register(new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult))); this._register(Event.forward(this._writeBuffer.onWriteParsed, this._onWriteParsed)); } public write(data: string | Uint8Array, callback?: () => void): void { this._writeBuffer.write(data, callback); } /** * Write data to terminal synchonously. * * This method is unreliable with async parser handlers, thus should not * be used anymore. If you need blocking semantics on data input consider * `write` with a callback instead. * * @deprecated Unreliable, will be removed soon. */ public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void { if (this._logService.logLevel <= LogLevelEnum.WARN && !hasWriteSyncWarnHappened) { this._logService.warn('writeSync is unreliable and will be removed soon.'); hasWriteSyncWarnHappened = true; } this._writeBuffer.writeSync(data, maxSubsequentCalls); } public input(data: string, wasUserInput: boolean = true): void { this.coreService.triggerDataEvent(data, wasUserInput); } public resize(x: number, y: number): void { if (isNaN(x) || isNaN(y)) { return; } x = Math.max(x, MINIMUM_COLS); y = Math.max(y, MINIMUM_ROWS); // Flush pending writes before resize to avoid race conditions where async // writes are processed with incorrect dimensions this._writeBuffer.flushSync(); this._bufferService.resize(x, y); } /** * Scroll the terminal down 1 row, creating a blank line. * @param eraseAttr The attribute data to use the for blank line. * @param isWrapped Whether the new line is wrapped from the previous line. */ public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void { this._bufferService.scroll(eraseAttr, isWrapped); } /** * Scroll the display of the terminal * @param disp The number of lines to scroll down (negative scroll up). * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used to avoid * unwanted events being handled by the viewport when the event was triggered from the viewport * originally. */ public scrollLines(disp: number, suppressScrollEvent?: boolean): void { this._bufferService.scrollLines(disp, suppressScrollEvent); } public scrollPages(pageCount: number): void { this.scrollLines(pageCount * (this.rows - 1)); } public scrollToTop(): void { this.scrollLines(-this._bufferService.buffer.ydisp); } public scrollToBottom(disableSmoothScroll?: boolean): void { this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp); } public scrollToLine(line: number): void { const scrollAmount = line - this._bufferService.buffer.ydisp; if (scrollAmount !== 0) { this.scrollLines(scrollAmount); } } /** Add handler for ESC escape sequence. See xterm.d.ts for details. */ public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable { return this._inputHandler.registerEscHandler(id, callback); } /** Add handler for DCS escape sequence. See xterm.d.ts for details. */ public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable { return this._inputHandler.registerDcsHandler(id, callback); } /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable { return this._inputHandler.registerCsiHandler(id, callback); } /** Add handler for OSC escape sequence. See xterm.d.ts for details. */ public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { return this._inputHandler.registerOscHandler(ident, callback); } protected _setup(): void { this._handleWindowsPtyOptionChange(); } public reset(): void { this._inputHandler.reset(); this._bufferService.reset(); this._charsetService.reset(); this.coreService.reset(); this.coreMouseService.reset(); } /** * Mark the shell as ready and fire the onShellReady event. * This is called when OSC 133;A (prompt ready) is first received. */ public markShellReady(): void { if (!this._shellReady) { this._shellReady = true; this._onShellReady.fire(); } } /** * Fire an OSC 133 shell integration event. * @param event The OSC 133 event data */ public fireOsc133(event: { type: 'prompt' | 'commandStart' | 'commandExecute' | 'commandEnd'; data?: string; exitCode?: number }): void { this._onOsc133.fire(event); } /** * Update the current working directory and fire the onOsc7 event. * @param hostname The hostname from the OSC 7 sequence * @param path The path from the OSC 7 sequence */ public updateCwd(hostname: string, path: string): void { this._cwd = path; this._onOsc7.fire({ hostname, path }); } private _handleWindowsPtyOptionChange(): void { let value = false; const windowsPty = this.optionsService.rawOptions.windowsPty; if (windowsPty && windowsPty.buildNumber !== undefined && windowsPty.buildNumber !== undefined) { value = !!(windowsPty.backend === 'conpty' && windowsPty.buildNumber < 21376); } if (value) { this._enableWindowsWrappingHeuristics(); } else { this._windowsWrappingHeuristics.clear(); } } protected _enableWindowsWrappingHeuristics(): void { if (!this._windowsWrappingHeuristics.value) { const disposables: IDisposable[] = []; disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService))); disposables.push(this.registerCsiHandler({ final: 'H' }, () => { updateWindowsModeWrappedState(this._bufferService); return false; })); this._windowsWrappingHeuristics.value = toDisposable(() => { for (const d of disposables) { d.dispose(); } }); } } }