X-Git-Url: https://git.josue.xyz/?p=VSoRC%2F.git;a=blobdiff_plain;f=node_modules%2Fxterm%2Fsrc%2FTerminal.ts;fp=node_modules%2Fxterm%2Fsrc%2FTerminal.ts;h=3e7e55ca99c7b03779a89ff90f391d1458e3f9d1;hp=0000000000000000000000000000000000000000;hb=4339da12467b75fb8b6ca831f4bf0081c485ed2c;hpb=af450fde25a9ccf4767b29254c463ffb8ef25237 diff --git a/node_modules/xterm/src/Terminal.ts b/node_modules/xterm/src/Terminal.ts new file mode 100644 index 0000000..3e7e55c --- /dev/null +++ b/node_modules/xterm/src/Terminal.ts @@ -0,0 +1,1610 @@ +/** + * Copyright (c) 2014 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 { IInputHandlingTerminal, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, CustomKeyEventHandler } from './Types'; +import { IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types'; +import { CompositionHelper } from 'browser/input/CompositionHelper'; +import { Viewport } from 'browser/Viewport'; +import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard'; +import { C0 } from 'common/data/EscapeSequences'; +import { InputHandler } from './InputHandler'; +import { Renderer } from './renderer/Renderer'; +import { Linkifier } from 'browser/Linkifier'; +import { SelectionService } from 'browser/services/SelectionService'; +import * as Browser from 'common/Platform'; +import { addDisposableDomListener } from 'browser/Lifecycle'; +import * as Strings from 'browser/LocalizableStrings'; +import { SoundService } from 'browser/services/SoundService'; +import { MouseZoneManager } from 'browser/MouseZoneManager'; +import { AccessibilityManager } from './AccessibilityManager'; +import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; +import { DomRenderer } from './renderer/dom/DomRenderer'; +import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; +import { evaluateKeyboardEvent } from 'common/input/Keyboard'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { applyWindowsMode } from './WindowsMode'; +import { ColorManager } from 'browser/ColorManager'; +import { RenderService } from 'browser/services/RenderService'; +import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services'; +import { OptionsService } from 'common/services/OptionsService'; +import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService } from 'browser/services/Services'; +import { CharSizeService } from 'browser/services/CharSizeService'; +import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; +import { Disposable } from 'common/Lifecycle'; +import { IBufferSet, IBuffer } from 'common/buffer/Types'; +import { Attributes } from 'common/buffer/Constants'; +import { MouseService } from 'browser/services/MouseService'; +import { IParams, IFunctionIdentifier } from 'common/parser/Types'; +import { CoreService } from 'common/services/CoreService'; +import { LogService } from 'common/services/LogService'; +import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport } from 'browser/Types'; +import { DirtyRowService } from 'common/services/DirtyRowService'; +import { InstantiationService } from 'common/services/InstantiationService'; +import { CoreMouseService } from 'common/services/CoreMouseService'; +import { WriteBuffer } from 'common/input/WriteBuffer'; + +// Let it work inside Node.js for automated testing purposes. +const document = (typeof window !== 'undefined') ? window.document : null; + + +export class Terminal extends Disposable implements ITerminal, IDisposable, IInputHandlingTerminal { + public textarea: HTMLTextAreaElement; + public element: HTMLElement; + public screenElement: HTMLElement; + + /** + * The HTMLElement that the terminal is created in, set by Terminal.open. + */ + private _parent: HTMLElement | null; + private _document: Document; + private _viewportScrollArea: HTMLElement; + private _viewportElement: HTMLElement; + private _helperContainer: HTMLElement; + private _compositionView: HTMLElement; + + private _visualBellTimer: number; + + public browser: IBrowser = Browser; + + // TODO: We should remove options once components adopt optionsService + public get options(): ITerminalOptions { return this.optionsService.options; } + + // TODO: This can be changed to an enum or boolean, 0 and 1 seem to be the only options + public cursorState: number; + public cursorHidden: boolean; + + private _customKeyEventHandler: CustomKeyEventHandler; + + // common services + private _bufferService: IBufferService; + private _coreService: ICoreService; + private _coreMouseService: ICoreMouseService; + private _dirtyRowService: IDirtyRowService; + private _instantiationService: IInstantiationService; + private _logService: ILogService; + public optionsService: IOptionsService; + + // browser services + private _charSizeService: ICharSizeService; + private _mouseService: IMouseService; + private _renderService: IRenderService; + private _selectionService: ISelectionService; + private _soundService: ISoundService; + + // modes + public applicationKeypad: boolean; + public originMode: boolean; + public insertMode: boolean; + public wraparoundMode: boolean; // defaults: xterm - true, vt100 - false + public bracketedPasteMode: boolean; + + // charset + // The current charset + public charset: ICharset; + public gcharset: number; + public glevel: number; + public charsets: ICharset[]; + + // mouse properties + public mouseEvents: CoreMouseEventType = CoreMouseEventType.NONE; + public sendFocus: boolean; + + // misc + public savedCols: number; + + public curAttrData: IAttributeData; + private _eraseAttrData: IAttributeData; + + public params: (string | number)[]; + public currentParam: string | number; + + // write buffer + private _writeBuffer: WriteBuffer; + + // Store if user went browsing history in scrollback + private _userScrolling: boolean; + + /** + * Records whether the keydown event has already been handled and triggered a data event, if so + * the keypress event should not trigger a data event but should still print to the textarea so + * screen readers will announce it. + */ + private _keyDownHandled: boolean = false; + + private _inputHandler: InputHandler; + public linkifier: ILinkifier; + public viewport: IViewport; + private _compositionHelper: ICompositionHelper; + private _mouseZoneManager: IMouseZoneManager; + private _accessibilityManager: AccessibilityManager; + private _colorManager: ColorManager; + private _theme: ITheme; + private _windowsMode: IDisposable | undefined; + + // bufferline to clone/copy from for new blank lines + private _blankLine: IBufferLine = null; + + public get cols(): number { return this._bufferService.cols; } + public get rows(): number { return this._bufferService.rows; } + + private _onCursorMove = new EventEmitter(); + public get onCursorMove(): IEvent { return this._onCursorMove.event; } + private _onData = new EventEmitter(); + public get onData(): IEvent { return this._onData.event; } + private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>(); + public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; } + private _onLineFeed = new EventEmitter(); + public get onLineFeed(): IEvent { return this._onLineFeed.event; } + private _onRender = new EventEmitter<{ start: number, end: number }>(); + public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } + private _onResize = new EventEmitter<{ cols: number, rows: number }>(); + public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } + private _onScroll = new EventEmitter(); + public get onScroll(): IEvent { return this._onScroll.event; } + private _onSelectionChange = new EventEmitter(); + public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } + private _onTitleChange = new EventEmitter(); + public get onTitleChange(): IEvent { return this._onTitleChange.event; } + + private _onFocus = new EventEmitter(); + public get onFocus(): IEvent { return this._onFocus.event; } + private _onBlur = new EventEmitter(); + public get onBlur(): IEvent { return this._onBlur.event; } + public onA11yCharEmitter = new EventEmitter(); + public get onA11yChar(): IEvent { return this.onA11yCharEmitter.event; } + public onA11yTabEmitter = new EventEmitter(); + public get onA11yTab(): IEvent { return this.onA11yTabEmitter.event; } + + /** + * Creates a new `Terminal` object. + * + * @param options An object containing a set of options, the available options are: + * - `cursorBlink` (boolean): Whether the terminal cursor blinks + * - `cols` (number): The number of columns of the terminal (horizontal size) + * - `rows` (number): The number of rows of the terminal (vertical size) + * + * @public + * @class Xterm Xterm + * @alias module:xterm/src/xterm + */ + constructor( + options: ITerminalOptions = {} + ) { + super(); + + // Setup and initialize common services + this._instantiationService = new InstantiationService(); + this.optionsService = new OptionsService(options); + this._instantiationService.setService(IOptionsService, this.optionsService); + this._bufferService = this._instantiationService.createInstance(BufferService); + this._instantiationService.setService(IBufferService, this._bufferService); + this._logService = this._instantiationService.createInstance(LogService); + this._instantiationService.setService(ILogService, this._logService); + this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom()); + this._instantiationService.setService(ICoreService, this._coreService); + this._coreService.onData(e => this._onData.fire(e)); + this._coreMouseService = this._instantiationService.createInstance(CoreMouseService); + this._instantiationService.setService(ICoreMouseService, this._coreMouseService); + this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); + this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); + + this._setupOptionsListeners(); + this._setup(); + + this._writeBuffer = new WriteBuffer(data => this._inputHandler.parse(data)); + } + + public dispose(): void { + if (this._isDisposed) { + return; + } + super.dispose(); + if (this._windowsMode) { + this._windowsMode.dispose(); + this._windowsMode = undefined; + } + if (this._renderService) { + this._renderService.dispose(); + } + this._customKeyEventHandler = null; + this.write = () => {}; + if (this.element && this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + } + + private _setup(): void { + this._parent = document ? document.body : null; + + this.cursorState = 0; + this.cursorHidden = false; + this._customKeyEventHandler = null; + + // modes + this.applicationKeypad = false; + this.originMode = false; + this.insertMode = false; + this.wraparoundMode = true; // defaults: xterm - true, vt100 - false + this.bracketedPasteMode = false; + + // charset + this.charset = null; + this.gcharset = null; + this.glevel = 0; + // TODO: Can this be just []? + this.charsets = [null]; + + this.curAttrData = DEFAULT_ATTR_DATA.clone(); + this._eraseAttrData = DEFAULT_ATTR_DATA.clone(); + + this.params = []; + this.currentParam = 0; + + this._userScrolling = false; + + // Register input handler and refire/handle events + this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService); + this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); + this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); + this.register(this._inputHandler); + + this.linkifier = this.linkifier || new Linkifier(this._bufferService, this._logService); + + if (this.options.windowsMode) { + this._windowsMode = applyWindowsMode(this); + } + } + + /** + * Convenience property to active buffer. + */ + public get buffer(): IBuffer { + return this.buffers.active; + } + + public get buffers(): IBufferSet { + return this._bufferService.buffers; + } + + /** + * back_color_erase feature for xterm. + */ + public eraseAttrData(): IAttributeData { + this._eraseAttrData.bg &= ~(Attributes.CM_MASK | 0xFFFFFF); + this._eraseAttrData.bg |= this.curAttrData.bg & ~0xFC000000; + return this._eraseAttrData; + } + + /** + * Focus the terminal. Delegates focus handling to the terminal's DOM element. + */ + public focus(): void { + if (this.textarea) { + this.textarea.focus({ preventScroll: true }); + } + } + + public get isFocused(): boolean { + return document.activeElement === this.textarea && document.hasFocus(); + } + + private _setupOptionsListeners(): void { + // TODO: These listeners should be owned by individual components + this.optionsService.onOptionChange(key => { + switch (key) { + case 'fontFamily': + case 'fontSize': + // When the font changes the size of the cells may change which requires a renderer clear + if (this._renderService) { + this._renderService.clear(); + } + if (this._charSizeService) { + this._charSizeService.measure(); + } + break; + case 'drawBoldTextInBrightColors': + case 'letterSpacing': + case 'lineHeight': + case 'fontWeight': + case 'fontWeightBold': + // When the font changes the size of the cells may change which requires a renderer clear + if (this._renderService) { + this._renderService.clear(); + this._renderService.onResize(this.cols, this.rows); + this.refresh(0, this.rows - 1); + } + break; + case 'rendererType': + if (this._renderService) { + this._renderService.setRenderer(this._createRenderer()); + this._renderService.onResize(this.cols, this.rows); + } + break; + case 'scrollback': + this.buffers.resize(this.cols, this.rows); + if (this.viewport) { + this.viewport.syncScrollArea(); + } + break; + case 'screenReaderMode': + if (this.optionsService.options.screenReaderMode) { + if (!this._accessibilityManager && this._renderService) { + this._accessibilityManager = new AccessibilityManager(this, this._renderService); + } + } else { + if (this._accessibilityManager) { + this._accessibilityManager.dispose(); + this._accessibilityManager = null; + } + } + break; + case 'tabStopWidth': this.buffers.setupTabStops(); break; + case 'theme': + this._setTheme(this.optionsService.options.theme); + break; + case 'windowsMode': + if (this.optionsService.options.windowsMode) { + if (!this._windowsMode) { + this._windowsMode = applyWindowsMode(this); + } + } else { + if (this._windowsMode) { + this._windowsMode.dispose(); + this._windowsMode = undefined; + } + } + break; + } + }); + } + + /** + * Binds the desired focus behavior on a given terminal object. + */ + private _onTextAreaFocus(ev: KeyboardEvent): void { + if (this.sendFocus) { + this._coreService.triggerDataEvent(C0.ESC + '[I'); + } + this.updateCursorStyle(ev); + this.element.classList.add('focus'); + this.showCursor(); + this._onFocus.fire(); + } + + /** + * Blur the terminal, calling the blur function on the terminal's underlying + * textarea. + */ + public blur(): void { + return this.textarea.blur(); + } + + /** + * Binds the desired blur behavior on a given terminal object. + */ + private _onTextAreaBlur(): void { + // Text can safely be removed on blur. Doing it earlier could interfere with + // screen readers reading it out. + this.textarea.value = ''; + this.refresh(this.buffer.y, this.buffer.y); + if (this.sendFocus) { + this._coreService.triggerDataEvent(C0.ESC + '[O'); + } + this.element.classList.remove('focus'); + this._onBlur.fire(); + } + + /** + * Initialize default behavior + */ + private _initGlobal(): void { + this._bindKeys(); + + // Bind clipboard functionality + this.register(addDisposableDomListener(this.element, 'copy', (event: ClipboardEvent) => { + // If mouse events are active it means the selection manager is disabled and + // copy should be handled by the host program. + if (!this.hasSelection()) { + return; + } + copyHandler(event, this._selectionService); + })); + const pasteHandlerWrapper = (event: ClipboardEvent) => handlePasteEvent(event, this.textarea, this.bracketedPasteMode, this._coreService); + this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper)); + this.register(addDisposableDomListener(this.element, 'paste', pasteHandlerWrapper)); + + // Handle right click context menus + if (Browser.isFirefox) { + // Firefox doesn't appear to fire the contextmenu event on right click + this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => { + if (event.button === 2) { + rightClickHandler(event, this.textarea, this.screenElement, this._selectionService, this.options.rightClickSelectsWord); + } + })); + } else { + this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => { + rightClickHandler(event, this.textarea, this.screenElement, this._selectionService, this.options.rightClickSelectsWord); + })); + } + + // Move the textarea under the cursor when middle clicking on Linux to ensure + // middle click to paste selection works. This only appears to work in Chrome + // at the time is writing. + if (Browser.isLinux) { + // Use auxclick event over mousedown the latter doesn't seem to work. Note + // that the regular click event doesn't fire for the middle mouse button. + this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => { + if (event.button === 1) { + moveTextAreaUnderMouseCursor(event, this.textarea, this.screenElement); + } + })); + } + } + + /** + * Apply key handling to the terminal + */ + private _bindKeys(): void { + this.register(addDisposableDomListener(this.textarea, 'keyup', (ev: KeyboardEvent) => this._keyUp(ev), true)); + this.register(addDisposableDomListener(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true)); + this.register(addDisposableDomListener(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true)); + this.register(addDisposableDomListener(this.textarea, 'compositionstart', () => this._compositionHelper.compositionstart())); + this.register(addDisposableDomListener(this.textarea, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper.compositionupdate(e))); + this.register(addDisposableDomListener(this.textarea, 'compositionend', () => this._compositionHelper.compositionend())); + this.register(this.onRender(() => this._compositionHelper.updateCompositionElements())); + this.register(this.onRender(e => this._queueLinkification(e.start, e.end))); + } + + /** + * Opens the terminal within an element. + * + * @param parent The element to create the terminal within. + */ + public open(parent: HTMLElement): void { + this._parent = parent || this._parent; + + if (!this._parent) { + throw new Error('Terminal requires a parent element.'); + } + + if (!document.body.contains(parent)) { + this._logService.warn('Terminal.open was called on an element that was not attached to the DOM'); + } + + this._document = this._parent.ownerDocument; + + // Create main element container + this.element = this._document.createElement('div'); + this.element.dir = 'ltr'; // xterm.css assumes LTR + this.element.classList.add('terminal'); + this.element.classList.add('xterm'); + this.element.setAttribute('tabindex', '0'); + this._parent.appendChild(this.element); + + // Performance: Use a document fragment to build the terminal + // viewport and helper elements detached from the DOM + const fragment = document.createDocumentFragment(); + this._viewportElement = document.createElement('div'); + this._viewportElement.classList.add('xterm-viewport'); + fragment.appendChild(this._viewportElement); + this._viewportScrollArea = document.createElement('div'); + this._viewportScrollArea.classList.add('xterm-scroll-area'); + this._viewportElement.appendChild(this._viewportScrollArea); + + this.screenElement = document.createElement('div'); + this.screenElement.classList.add('xterm-screen'); + // Create the container that will hold helpers like the textarea for + // capturing DOM Events. Then produce the helpers. + this._helperContainer = document.createElement('div'); + this._helperContainer.classList.add('xterm-helpers'); + this.screenElement.appendChild(this._helperContainer); + fragment.appendChild(this.screenElement); + + this.textarea = document.createElement('textarea'); + this.textarea.classList.add('xterm-helper-textarea'); + this.textarea.setAttribute('aria-label', Strings.promptLabel); + this.textarea.setAttribute('aria-multiline', 'false'); + this.textarea.setAttribute('autocorrect', 'off'); + this.textarea.setAttribute('autocapitalize', 'off'); + this.textarea.setAttribute('spellcheck', 'false'); + this.textarea.tabIndex = 0; + this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._onTextAreaFocus(ev))); + this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur())); + this._helperContainer.appendChild(this.textarea); + + this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer); + this._instantiationService.setService(ICharSizeService, this._charSizeService); + + this._compositionView = document.createElement('div'); + this._compositionView.classList.add('composition-view'); + this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView); + this._helperContainer.appendChild(this._compositionView); + + // Performance: Add viewport and helper elements from the fragment + this.element.appendChild(fragment); + + this._theme = this.options.theme || this._theme; + this.options.theme = undefined; + this._colorManager = new ColorManager(document, this.options.allowTransparency); + this._colorManager.setTheme(this._theme); + + const renderer = this._createRenderer(); + this._renderService = this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement); + this._instantiationService.setService(IRenderService, this._renderService); + this._renderService.onRender(e => this._onRender.fire(e)); + this.onResize(e => this._renderService.resize(e.cols, e.rows)); + + this._soundService = this._instantiationService.createInstance(SoundService); + this._instantiationService.setService(ISoundService, this._soundService); + this._mouseService = this._instantiationService.createInstance(MouseService); + this._instantiationService.setService(IMouseService, this._mouseService); + + this.viewport = this._instantiationService.createInstance(Viewport, + (amount: number, suppressEvent: boolean) => this.scrollLines(amount, suppressEvent), + this._viewportElement, + this._viewportScrollArea + ); + this.viewport.onThemeChange(this._colorManager.colors); + this.register(this.viewport); + + this.register(this.onCursorMove(() => this._renderService.onCursorMove())); + this.register(this.onResize(() => this._renderService.onResize(this.cols, this.rows))); + this.register(this.onBlur(() => this._renderService.onBlur())); + this.register(this.onFocus(() => this._renderService.onFocus())); + this.register(this._renderService.onDimensionsChange(() => this.viewport.syncScrollArea())); + + this._selectionService = this._instantiationService.createInstance(SelectionService, + (amount: number, suppressEvent: boolean) => this.scrollLines(amount, suppressEvent), + this.element, + this.screenElement); + this._instantiationService.setService(ISelectionService, this._selectionService); + this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire())); + this.register(this._selectionService.onRedrawRequest(e => this._renderService.onSelectionChanged(e.start, e.end, e.columnSelectMode))); + this.register(this._selectionService.onLinuxMouseSelection(text => { + // If there's a new selection, put it into the textarea, focus and select it + // in order to register it as a selection on the OS. This event is fired + // only on Linux to enable middle click to paste selection. + this.textarea.value = text; + this.textarea.focus(); + this.textarea.select(); + })); + this.register(this.onScroll(() => { + this.viewport.syncScrollArea(); + this._selectionService.refresh(); + })); + this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService.refresh())); + + this._mouseZoneManager = this._instantiationService.createInstance(MouseZoneManager, this.element, this.screenElement); + this.register(this._mouseZoneManager); + this.register(this.onScroll(() => this._mouseZoneManager.clearAll())); + this.linkifier.attachToDom(this.element, this._mouseZoneManager); + + // This event listener must be registered aftre MouseZoneManager is created + this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService.onMouseDown(e))); + + // apply mouse event classes set by escape codes before terminal was attached + if (this.mouseEvents) { + this._selectionService.disable(); + this.element.classList.add('enable-mouse-events'); + } else { + this._selectionService.enable(); + } + + if (this.options.screenReaderMode) { + // Note that this must be done *after* the renderer is created in order to + // ensure the correct order of the dprchange event + this._accessibilityManager = new AccessibilityManager(this, this._renderService); + } + + // Measure the character size + this._charSizeService.measure(); + + // Setup loop that draws to screen + this.refresh(0, this.rows - 1); + + // Initialize global actions that need to be taken on the document. + this._initGlobal(); + + // Listen for mouse events and translate + // them into terminal mouse protocols. + this.bindMouse(); + } + + private _createRenderer(): IRenderer { + switch (this.options.rendererType) { + case 'canvas': return new Renderer(this._colorManager.colors, this, this._bufferService, this._charSizeService, this.optionsService); + case 'dom': return new DomRenderer(this, this._colorManager.colors, this._charSizeService, this.optionsService); + default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); + } + } + + /** + * Sets the theme on the renderer. The renderer must have been initialized. + * @param theme The theme to set. + */ + private _setTheme(theme: ITheme): void { + this._theme = theme; + if (this._colorManager) { + this._colorManager.setTheme(theme); + } + if (this._renderService) { + this._renderService.setColors(this._colorManager.colors); + } + if (this.viewport) { + this.viewport.onThemeChange(this._colorManager.colors); + } + } + + /** + * Bind certain mouse events to the terminal. + * By default only 3 button + wheel up/down is ativated. For higher buttons + * no mouse report will be created. Typically the standard actions will be active. + * + * There are several reasons not to enable support for higher buttons/wheel: + * - Button 4 and 5 are typically used for history back and forward navigation, + * there is no straight forward way to supress/intercept those standard actions. + * - Support for higher buttons does not work in some platform/browser combinations. + * - Left/right wheel was not tested. + * - Emulators vary in mouse button support, typically only 3 buttons and + * wheel up/down work reliable. + * + * TODO: Move mouse event code into its own file. + */ + public bindMouse(): void { + const self = this; + const el = this.element; + + // send event to CoreMouseService + function sendEvent(ev: MouseEvent | WheelEvent): boolean { + let pos; + + // get mouse coordinates + pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); + if (!pos) { + return false; + } + + let but: CoreMouseButton; + let action: CoreMouseAction; + switch ((ev).overrideType || ev.type) { + case 'mousemove': + action = CoreMouseAction.MOVE; + if (ev.buttons === undefined) { + // buttons is not supported on macOS, try to get a value from button instead + but = CoreMouseButton.NONE; + if (ev.button !== undefined) { + but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; + } + } else { + // according to MDN buttons only reports up to button 5 (AUX2) + but = ev.buttons & 1 ? CoreMouseButton.LEFT : + ev.buttons & 4 ? CoreMouseButton.MIDDLE : + ev.buttons & 2 ? CoreMouseButton.RIGHT : + CoreMouseButton.NONE; // fallback to NONE + } + break; + case 'mouseup': + action = CoreMouseAction.UP; + but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; + break; + case 'mousedown': + action = CoreMouseAction.DOWN; + but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; + break; + case 'wheel': + // only UP/DOWN wheel events are respected + if ((ev as WheelEvent).deltaY !== 0) { + action = (ev as WheelEvent).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN; + } + but = CoreMouseButton.WHEEL; + break; + default: + // dont handle other event types by accident + return false; + } + + // exit if we cannot determine valid button/action values + // do nothing for higher buttons than wheel + if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) { + return false; + } + + return self._coreMouseService.triggerMouseEvent({ + col: pos.x - 33, // FIXME: why -33 here? + row: pos.y - 33, + button: but, + action, + ctrl: ev.ctrlKey, + alt: ev.altKey, + shift: ev.shiftKey + }); + } + + /** + * Event listener state handling. + * We listen to the onProtocolChange event of CoreMouseService and put + * requested listeners in `requestedEvents`. With this the listeners + * have all bits to do the event listener juggling. + * Note: 'mousedown' currently is "always on" and not managed + * by onProtocolChange. + */ + const requestedEvents: {[key: string]: ((ev: Event) => void) | null} = { + mouseup: null, + wheel: null, + mousedrag: null, + mousemove: null + }; + const eventListeners: {[key: string]: (ev: Event) => void} = { + mouseup: (ev: MouseEvent) => { + sendEvent(ev); + if (!ev.buttons) { + // if no other button is held remove global handlers + this._document.removeEventListener('mouseup', requestedEvents.mouseup); + if (requestedEvents.mousedrag) { + this._document.removeEventListener('mousemove', requestedEvents.mousedrag); + } + } + return this.cancel(ev); + }, + wheel: (ev: WheelEvent) => { + sendEvent(ev); + ev.preventDefault(); + return this.cancel(ev); + }, + mousedrag: (ev: MouseEvent) => { + // deal only with move while a button is held + if (ev.buttons) { + sendEvent(ev); + } + }, + mousemove: (ev: MouseEvent) => { + // deal only with move without any button + if (!ev.buttons) { + sendEvent(ev); + } + } + }; + this._coreMouseService.onProtocolChange(events => { + // apply global changes on events + this.mouseEvents = events; + if (events) { + if (this.optionsService.options.logLevel === 'debug') { + this._logService.debug('Binding to mouse events:', this._coreMouseService.explainEvents(events)); + } + this.element.classList.add('enable-mouse-events'); + this._selectionService.disable(); + } else { + this._logService.debug('Unbinding from mouse events.'); + this.element.classList.remove('enable-mouse-events'); + this._selectionService.enable(); + } + + // add/remove handlers from requestedEvents + + if (!(events & CoreMouseEventType.MOVE)) { + el.removeEventListener('mousemove', requestedEvents.mousemove); + requestedEvents.mousemove = null; + } else if (!requestedEvents.mousemove) { + el.addEventListener('mousemove', eventListeners.mousemove); + requestedEvents.mousemove = eventListeners.mousemove; + } + + if (!(events & CoreMouseEventType.WHEEL)) { + el.removeEventListener('wheel', requestedEvents.wheel); + requestedEvents.wheel = null; + } else if (!requestedEvents.wheel) { + el.addEventListener('wheel', eventListeners.wheel); + requestedEvents.wheel = eventListeners.wheel; + } + + if (!(events & CoreMouseEventType.UP)) { + this._document.removeEventListener('mouseup', requestedEvents.mouseup); + requestedEvents.mouseup = null; + } else if (!requestedEvents.mouseup) { + requestedEvents.mouseup = eventListeners.mouseup; + } + + if (!(events & CoreMouseEventType.DRAG)) { + this._document.removeEventListener('mousemove', requestedEvents.mousedrag); + requestedEvents.mousedrag = null; + } else if (!requestedEvents.mousedrag) { + requestedEvents.mousedrag = eventListeners.mousedrag; + } + }); + // force initial onProtocolChange so we dont miss early mouse requests + this._coreMouseService.activeProtocol = this._coreMouseService.activeProtocol; + + /** + * "Always on" event listeners. + */ + this.register(addDisposableDomListener(el, 'mousedown', (ev: MouseEvent) => { + ev.preventDefault(); + this.focus(); + + // Don't send the mouse button to the pty if mouse events are disabled or + // if the selection manager is having selection forced (ie. a modifier is + // held). + if (!this.mouseEvents || this._selectionService.shouldForceSelection(ev)) { + return; + } + + sendEvent(ev); + + // Register additional global handlers which should keep reporting outside + // of the terminal element. + // Note: Other emulators also do this for 'mousedown' while a button + // is held, we currently limit 'mousedown' to the terminal only. + if (requestedEvents.mouseup) { + this._document.addEventListener('mouseup', requestedEvents.mouseup); + } + if (requestedEvents.mousedrag) { + this._document.addEventListener('mousemove', requestedEvents.mousedrag); + } + + return this.cancel(ev); + })); + + this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => { + if (!requestedEvents.wheel) { + // Convert wheel events into up/down events when the buffer does not have scrollback, this + // enables scrolling in apps hosted in the alt buffer such as vim or tmux. + if (!this.buffer.hasScrollback) { + const amount = this.viewport.getLinesScrolled(ev); + + // Do nothing if there's no vertical scroll + if (amount === 0) { + return; + } + + // Construct and send sequences + const sequence = C0.ESC + (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + ( ev.deltaY < 0 ? 'A' : 'B'); + let data = ''; + for (let i = 0; i < Math.abs(amount); i++) { + data += sequence; + } + this._coreService.triggerDataEvent(data, true); + } + return; + } + })); + + // allow wheel scrolling in + // the shell for example + this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => { + if (requestedEvents.wheel) return; + if (!this.viewport.onWheel(ev)) { + return this.cancel(ev); + } + })); + + this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => { + if (this.mouseEvents) return; + this.viewport.onTouchStart(ev); + return this.cancel(ev); + })); + + this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => { + if (this.mouseEvents) return; + if (!this.viewport.onTouchMove(ev)) { + return this.cancel(ev); + } + })); + } + + + /** + * Tells the renderer to refresh terminal content between two rows (inclusive) at the next + * opportunity. + * @param start The row to start from (between 0 and this.rows - 1). + * @param end The row to end at (between start and this.rows - 1). + */ + public refresh(start: number, end: number): void { + if (this._renderService) { + this._renderService.refreshRows(start, end); + } + } + + /** + * Queues linkification for the specified rows. + * @param start The row to start from (between 0 and this.rows - 1). + * @param end The row to end at (between start and this.rows - 1). + */ + private _queueLinkification(start: number, end: number): void { + if (this.linkifier) { + this.linkifier.linkifyRows(start, end); + } + } + + /** + * Change the cursor style for different selection modes + */ + public updateCursorStyle(ev: KeyboardEvent): void { + if (this._selectionService && this._selectionService.shouldColumnSelect(ev)) { + this.element.classList.add('column-select'); + } else { + this.element.classList.remove('column-select'); + } + } + + /** + * Display the cursor element + */ + public showCursor(): void { + if (!this.cursorState) { + this.cursorState = 1; + this.refresh(this.buffer.y, this.buffer.y); + } + } + + /** + * Scroll the terminal down 1 row, creating a blank line. + * @param isWrapped Whether the new line is wrapped from the previous line. + */ + public scroll(isWrapped: boolean = false): void { + let newLine: IBufferLine; + newLine = this._blankLine; + const eraseAttr = this.eraseAttrData(); + if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) { + newLine = this.buffer.getBlankLine(eraseAttr, isWrapped); + this._blankLine = newLine; + } + newLine.isWrapped = isWrapped; + + const topRow = this.buffer.ybase + this.buffer.scrollTop; + const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; + + if (this.buffer.scrollTop === 0) { + // Determine whether the buffer is going to be trimmed after insertion. + const willBufferBeTrimmed = this.buffer.lines.isFull; + + // Insert the line using the fastest method + if (bottomRow === this.buffer.lines.length - 1) { + if (willBufferBeTrimmed) { + this.buffer.lines.recycle().copyFrom(newLine); + } else { + this.buffer.lines.push(newLine.clone()); + } + } else { + this.buffer.lines.splice(bottomRow + 1, 0, newLine.clone()); + } + + // Only adjust ybase and ydisp when the buffer is not trimmed + if (!willBufferBeTrimmed) { + this.buffer.ybase++; + // Only scroll the ydisp with ybase if the user has not scrolled up + if (!this._userScrolling) { + this.buffer.ydisp++; + } + } else { + // When the buffer is full and the user has scrolled up, keep the text + // stable unless ydisp is right at the top + if (this._userScrolling) { + this.buffer.ydisp = Math.max(this.buffer.ydisp - 1, 0); + } + } + } else { + // scrollTop is non-zero which means no line will be going to the + // scrollback, instead we can just shift them in-place. + const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/; + this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); + this.buffer.lines.set(bottomRow, newLine.clone()); + } + + // Move the viewport to the bottom of the buffer unless the user is + // scrolling. + if (!this._userScrolling) { + this.buffer.ydisp = this.buffer.ybase; + } + + // Flag rows that need updating + this._dirtyRowService.markRangeDirty(this.buffer.scrollTop, this.buffer.scrollBottom); + + this._onScroll.fire(this.buffer.ydisp); + } + + /** + * 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 { + if (disp < 0) { + if (this.buffer.ydisp === 0) { + return; + } + this._userScrolling = true; + } else if (disp + this.buffer.ydisp >= this.buffer.ybase) { + this._userScrolling = false; + } + + const oldYdisp = this.buffer.ydisp; + this.buffer.ydisp = Math.max(Math.min(this.buffer.ydisp + disp, this.buffer.ybase), 0); + + // No change occurred, don't trigger scroll/refresh + if (oldYdisp === this.buffer.ydisp) { + return; + } + + if (!suppressScrollEvent) { + this._onScroll.fire(this.buffer.ydisp); + } + + this.refresh(0, this.rows - 1); + } + + /** + * Scroll the display of the terminal by a number of pages. + * @param pageCount The number of pages to scroll (negative scrolls up). + */ + public scrollPages(pageCount: number): void { + this.scrollLines(pageCount * (this.rows - 1)); + } + + /** + * Scrolls the display of the terminal to the top. + */ + public scrollToTop(): void { + this.scrollLines(-this.buffer.ydisp); + } + + /** + * Scrolls the display of the terminal to the bottom. + */ + public scrollToBottom(): void { + this.scrollLines(this.buffer.ybase - this.buffer.ydisp); + } + + public scrollToLine(line: number): void { + const scrollAmount = line - this.buffer.ydisp; + if (scrollAmount !== 0) { + this.scrollLines(scrollAmount); + } + } + + public paste(data: string): void { + paste(data, this.textarea, this.bracketedPasteMode, this._coreService); + } + + /** + * Attaches a custom key event handler which is run before keys are processed, + * giving consumers of xterm.js ultimate control as to what keys should be + * processed by the terminal and what keys should not. + * @param customKeyEventHandler The custom KeyboardEvent handler to attach. + * This is a function that takes a KeyboardEvent, allowing consumers to stop + * propagation and/or prevent the default action. The function returns whether + * the event should be processed by xterm.js. + */ + public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void { + this._customKeyEventHandler = customKeyEventHandler; + } + + /** Add handler for ESC escape sequence. See xterm.d.ts for details. */ + public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable { + return this._inputHandler.addEscHandler(id, callback); + } + + /** Add handler for DCS escape sequence. See xterm.d.ts for details. */ + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { + return this._inputHandler.addDcsHandler(id, callback); + } + + /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ + public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + return this._inputHandler.addCsiHandler(id, callback); + } + /** Add handler for OSC escape sequence. See xterm.d.ts for details. */ + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._inputHandler.addOscHandler(ident, callback); + } + + /** + * Registers a link matcher, allowing custom link patterns to be matched and + * handled. + * @param regex The regular expression to search for, specifically + * this searches the textContent of the rows. You will want to use \s to match + * a space ' ' character for example. + * @param handler The callback when the link is called. + * @param options Options for the link matcher. + * @return The ID of the new matcher, this can be used to deregister. + */ + public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number { + const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); + this.refresh(0, this.rows - 1); + return matcherId; + } + + /** + * Deregisters a link matcher if it has been registered. + * @param matcherId The link matcher's ID (returned after register) + */ + public deregisterLinkMatcher(matcherId: number): void { + if (this.linkifier.deregisterLinkMatcher(matcherId)) { + this.refresh(0, this.rows - 1); + } + } + + public registerCharacterJoiner(handler: CharacterJoinerHandler): number { + const joinerId = this._renderService.registerCharacterJoiner(handler); + this.refresh(0, this.rows - 1); + return joinerId; + } + + public deregisterCharacterJoiner(joinerId: number): void { + if (this._renderService.deregisterCharacterJoiner(joinerId)) { + this.refresh(0, this.rows - 1); + } + } + + public get markers(): IMarker[] { + return this.buffer.markers; + } + + public addMarker(cursorYOffset: number): IMarker { + // Disallow markers on the alt buffer + if (this.buffer !== this.buffers.normal) { + return; + } + + return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset); + } + + /** + * Gets whether the terminal has an active selection. + */ + public hasSelection(): boolean { + return this._selectionService ? this._selectionService.hasSelection : false; + } + + /** + * Selects text within the terminal. + * @param column The column the selection starts at.. + * @param row The row the selection starts at. + * @param length The length of the selection. + */ + public select(column: number, row: number, length: number): void { + this._selectionService.setSelection(column, row, length); + } + + /** + * Gets the terminal's current selection, this is useful for implementing copy + * behavior outside of xterm.js. + */ + public getSelection(): string { + return this._selectionService ? this._selectionService.selectionText : ''; + } + + public getSelectionPosition(): ISelectionPosition | undefined { + if (!this._selectionService.hasSelection) { + return undefined; + } + + return { + startColumn: this._selectionService.selectionStart[0], + startRow: this._selectionService.selectionStart[1], + endColumn: this._selectionService.selectionEnd[0], + endRow: this._selectionService.selectionEnd[1] + }; + } + + /** + * Clears the current terminal selection. + */ + public clearSelection(): void { + if (this._selectionService) { + this._selectionService.clearSelection(); + } + } + + /** + * Selects all text within the terminal. + */ + public selectAll(): void { + if (this._selectionService) { + this._selectionService.selectAll(); + } + } + + public selectLines(start: number, end: number): void { + if (this._selectionService) { + this._selectionService.selectLines(start, end); + } + } + + /** + * Handle a keydown event + * Key Resources: + * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent + * @param ev The keydown event to be handled. + */ + protected _keyDown(event: KeyboardEvent): boolean { + this._keyDownHandled = false; + + if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) { + return false; + } + + if (!this._compositionHelper.keydown(event)) { + if (this.buffer.ybase !== this.buffer.ydisp) { + this.scrollToBottom(); + } + return false; + } + + const result = evaluateKeyboardEvent(event, this._coreService.decPrivateModes.applicationCursorKeys, this.browser.isMac, this.options.macOptionIsMeta); + + this.updateCursorStyle(event); + + if (result.type === KeyboardResultType.PAGE_DOWN || result.type === KeyboardResultType.PAGE_UP) { + const scrollCount = this.rows - 1; + this.scrollLines(result.type === KeyboardResultType.PAGE_UP ? -scrollCount : scrollCount); + return this.cancel(event, true); + } + + if (result.type === KeyboardResultType.SELECT_ALL) { + this.selectAll(); + } + + if (this._isThirdLevelShift(this.browser, event)) { + return true; + } + + if (result.cancel) { + // The event is canceled at the end already, is this necessary? + this.cancel(event, true); + } + + if (!result.key) { + return true; + } + + // If ctrl+c or enter is being sent, clear out the textarea. This is done so that screen readers + // will announce deleted characters. This will not work 100% of the time but it should cover + // most scenarios. + if (result.key === C0.ETX || result.key === C0.CR) { + this.textarea.value = ''; + } + + this._onKey.fire({ key: result.key, domEvent: event }); + this.showCursor(); + this._coreService.triggerDataEvent(result.key, true); + + // Cancel events when not in screen reader mode so events don't get bubbled up and handled by + // other listeners. When screen reader mode is enabled, this could cause issues if the event + // is handled at a higher level, this is a compromise in order to echo keys to the screen + // reader. + if (!this.optionsService.options.screenReaderMode) { + return this.cancel(event, true); + } + + this._keyDownHandled = true; + } + + private _isThirdLevelShift(browser: IBrowser, ev: IKeyboardEvent): boolean { + const thirdLevelKey = + (browser.isMac && !this.options.macOptionIsMeta && ev.altKey && !ev.ctrlKey && !ev.metaKey) || + (browser.isWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); + + if (ev.type === 'keypress') { + return thirdLevelKey; + } + + // Don't invoke for arrows, pageDown, home, backspace, etc. (on non-keypress events) + return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47); + } + + /** + * Set the G level of the terminal + * @param g + */ + public setgLevel(g: number): void { + this.glevel = g; + this.charset = this.charsets[g]; + } + + /** + * Set the charset for the given G level of the terminal + * @param g + * @param charset + */ + public setgCharset(g: number, charset: ICharset): void { + this.charsets[g] = charset; + if (this.glevel === g) { + this.charset = charset; + } + } + + protected _keyUp(ev: KeyboardEvent): void { + if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) { + return; + } + + if (!wasModifierKeyOnlyEvent(ev)) { + this.focus(); + } + + this.updateCursorStyle(ev); + } + + /** + * Handle a keypress event. + * Key Resources: + * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent + * @param ev The keypress event to be handled. + */ + protected _keyPress(ev: KeyboardEvent): boolean { + let key; + + if (this._keyDownHandled) { + return false; + } + + if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) { + return false; + } + + this.cancel(ev); + + if (ev.charCode) { + key = ev.charCode; + } else if (ev.which === null || ev.which === undefined) { + key = ev.keyCode; + } else if (ev.which !== 0 && ev.charCode !== 0) { + key = ev.which; + } else { + return false; + } + + if (!key || ( + (ev.altKey || ev.ctrlKey || ev.metaKey) && !this._isThirdLevelShift(this.browser, ev) + )) { + return false; + } + + key = String.fromCharCode(key); + + this._onKey.fire({ key, domEvent: ev }); + this.showCursor(); + this._coreService.triggerDataEvent(key, true); + + return true; + } + + /** + * Ring the bell. + * Note: We could do sweet things with webaudio here + */ + public bell(): void { + if (this._soundBell()) { + this._soundService.playBellSound(); + } + + if (this._visualBell()) { + this.element.classList.add('visual-bell-active'); + clearTimeout(this._visualBellTimer); + this._visualBellTimer = window.setTimeout(() => { + this.element.classList.remove('visual-bell-active'); + }, 200); + } + } + + /** + * Resizes the terminal. + * + * @param x The number of columns to resize to. + * @param y The number of rows to resize to. + */ + public resize(x: number, y: number): void { + if (isNaN(x) || isNaN(y)) { + return; + } + + if (x === this.cols && y === this.rows) { + // Check if we still need to measure the char size (fixes #785). + if (this._charSizeService && !this._charSizeService.hasValidSize) { + this._charSizeService.measure(); + } + return; + } + + if (x < MINIMUM_COLS) x = MINIMUM_COLS; + if (y < MINIMUM_ROWS) y = MINIMUM_ROWS; + + this.buffers.resize(x, y); + + this._bufferService.resize(x, y); + this.buffers.setupTabStops(this.cols); + + if (this._charSizeService) { + this._charSizeService.measure(); + } + + // Sync the scroll area to make sure scroll events don't fire and scroll the viewport to an + // invalid location + this.viewport.syncScrollArea(true); + + this.refresh(0, this.rows - 1); + this._onResize.fire({ cols: x, rows: y }); + } + + /** + * Clear the entire buffer, making the prompt line the new first line. + */ + public clear(): void { + if (this.buffer.ybase === 0 && this.buffer.y === 0) { + // Don't clear if it's already clear + return; + } + this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)); + this.buffer.lines.length = 1; + this.buffer.ydisp = 0; + this.buffer.ybase = 0; + this.buffer.y = 0; + for (let i = 1; i < this.rows; i++) { + this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + this.refresh(0, this.rows - 1); + this._onScroll.fire(this.buffer.ydisp); + } + + /** + * Evaluate if the current terminal is the given argument. + * @param term The terminal name to evaluate + */ + public is(term: string): boolean { + return (this.options.termName + '').indexOf(term) === 0; + } + + /** + * Emit the data event and populate the given data. + * @param data The data to populate in the event. + */ + // public handler(data: string): void { + // // Prevents all events to pty process if stdin is disabled + // if (this.options.disableStdin) { + // return; + // } + + // // Clear the selection if the selection manager is available and has an active selection + // if (this.selectionService && this.selectionService.hasSelection) { + // this.selectionService.clearSelection(); + // } + + // // Input is being sent to the terminal, the terminal should focus the prompt. + // if (this.buffer.ybase !== this.buffer.ydisp) { + // this.scrollToBottom(); + // } + // this._onData.fire(data); + // } + + /** + * Emit the 'title' event and populate the given title. + * @param title The title to populate in the event. + */ + public handleTitle(title: string): void { + this._onTitleChange.fire(title); + } + + /** + * Reset terminal. + * Note: Calling this directly from JS is synchronous but does not clear + * input buffers and does not reset the parser, thus the terminal will + * continue to apply pending input data. + * If you need in band reset (synchronous with input data) consider + * using DECSTR (soft reset, CSI ! p) or RIS instead (hard reset, ESC c). + */ + public reset(): void { + /** + * Since _setup handles a full terminal creation, we have to carry forward + * a few things that should not reset. + */ + this.options.rows = this.rows; + this.options.cols = this.cols; + const customKeyEventHandler = this._customKeyEventHandler; + const inputHandler = this._inputHandler; + const cursorState = this.cursorState; + const userScrolling = this._userScrolling; + + this._setup(); + this._bufferService.reset(); + this._coreService.reset(); + this._coreMouseService.reset(); + if (this._selectionService) { + this._selectionService.reset(); + } + + // reattach + this._customKeyEventHandler = customKeyEventHandler; + this._inputHandler = inputHandler; + this.cursorState = cursorState; + this._userScrolling = userScrolling; + + // do a full screen refresh + this.refresh(0, this.rows - 1); + if (this.viewport) { + this.viewport.syncScrollArea(); + } + } + + // TODO: Remove cancel function and cancelEvents option + public cancel(ev: Event, force?: boolean): boolean { + if (!this.options.cancelEvents && !force) { + return; + } + ev.preventDefault(); + ev.stopPropagation(); + return false; + } + + private _visualBell(): boolean { + return false; + // return this.options.bellStyle === 'visual' || + // this.options.bellStyle === 'both'; + } + + private _soundBell(): boolean { + return this.options.bellStyle === 'sound'; + // return this.options.bellStyle === 'sound' || + // this.options.bellStyle === 'both'; + } + + public write(data: string | Uint8Array, callback?: () => void): void { + this._writeBuffer.write(data, callback); + } + + public writeSync(data: string | Uint8Array): void { + this._writeBuffer.writeSync(data); + } +} + +/** + * Helpers + */ + +function wasModifierKeyOnlyEvent(ev: KeyboardEvent): boolean { + return ev.keyCode === 16 || // Shift + ev.keyCode === 17 || // Ctrl + ev.keyCode === 18; // Alt +}