xterm
[VSoRC/.git] / node_modules / xterm / src / Terminal.ts
1 /**
2  * Copyright (c) 2014 The xterm.js authors. All rights reserved.
3  * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
4  * @license MIT
5  *
6  * Originally forked from (with the author's permission):
7  *   Fabrice Bellard's javascript vt100 for jslinux:
8  *   http://bellard.org/jslinux/
9  *   Copyright (c) 2011 Fabrice Bellard
10  *   The original design remains. The terminal itself
11  *   has been extended to include xterm CSI codes, among
12  *   other features.
13  *
14  * Terminal Emulation References:
15  *   http://vt100.net/
16  *   http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt
17  *   http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
18  *   http://invisible-island.net/vttest/
19  *   http://www.inwap.com/pdp10/ansicode.txt
20  *   http://linux.die.net/man/4/console_codes
21  *   http://linux.die.net/man/7/urxvt
22  */
23
24 import { IInputHandlingTerminal, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, CustomKeyEventHandler } from './Types';
25 import { IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types';
26 import { CompositionHelper } from 'browser/input/CompositionHelper';
27 import { Viewport } from 'browser/Viewport';
28 import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard';
29 import { C0 } from 'common/data/EscapeSequences';
30 import { InputHandler } from './InputHandler';
31 import { Renderer } from './renderer/Renderer';
32 import { Linkifier } from 'browser/Linkifier';
33 import { SelectionService } from 'browser/services/SelectionService';
34 import * as Browser from 'common/Platform';
35 import { addDisposableDomListener } from 'browser/Lifecycle';
36 import * as Strings from 'browser/LocalizableStrings';
37 import { SoundService } from 'browser/services/SoundService';
38 import { MouseZoneManager } from 'browser/MouseZoneManager';
39 import { AccessibilityManager } from './AccessibilityManager';
40 import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm';
41 import { DomRenderer } from './renderer/dom/DomRenderer';
42 import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
43 import { evaluateKeyboardEvent } from 'common/input/Keyboard';
44 import { EventEmitter, IEvent } from 'common/EventEmitter';
45 import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
46 import { applyWindowsMode } from './WindowsMode';
47 import { ColorManager } from 'browser/ColorManager';
48 import { RenderService } from 'browser/services/RenderService';
49 import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services';
50 import { OptionsService } from 'common/services/OptionsService';
51 import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService } from 'browser/services/Services';
52 import { CharSizeService } from 'browser/services/CharSizeService';
53 import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
54 import { Disposable } from 'common/Lifecycle';
55 import { IBufferSet, IBuffer } from 'common/buffer/Types';
56 import { Attributes } from 'common/buffer/Constants';
57 import { MouseService } from 'browser/services/MouseService';
58 import { IParams, IFunctionIdentifier } from 'common/parser/Types';
59 import { CoreService } from 'common/services/CoreService';
60 import { LogService } from 'common/services/LogService';
61 import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport } from 'browser/Types';
62 import { DirtyRowService } from 'common/services/DirtyRowService';
63 import { InstantiationService } from 'common/services/InstantiationService';
64 import { CoreMouseService } from 'common/services/CoreMouseService';
65 import { WriteBuffer } from 'common/input/WriteBuffer';
66
67 // Let it work inside Node.js for automated testing purposes.
68 const document = (typeof window !== 'undefined') ? window.document : null;
69
70
71 export class Terminal extends Disposable implements ITerminal, IDisposable, IInputHandlingTerminal {
72   public textarea: HTMLTextAreaElement;
73   public element: HTMLElement;
74   public screenElement: HTMLElement;
75
76   /**
77    * The HTMLElement that the terminal is created in, set by Terminal.open.
78    */
79   private _parent: HTMLElement | null;
80   private _document: Document;
81   private _viewportScrollArea: HTMLElement;
82   private _viewportElement: HTMLElement;
83   private _helperContainer: HTMLElement;
84   private _compositionView: HTMLElement;
85
86   private _visualBellTimer: number;
87
88   public browser: IBrowser = <any>Browser;
89
90   // TODO: We should remove options once components adopt optionsService
91   public get options(): ITerminalOptions { return this.optionsService.options; }
92
93   // TODO: This can be changed to an enum or boolean, 0 and 1 seem to be the only options
94   public cursorState: number;
95   public cursorHidden: boolean;
96
97   private _customKeyEventHandler: CustomKeyEventHandler;
98
99   // common services
100   private _bufferService: IBufferService;
101   private _coreService: ICoreService;
102   private _coreMouseService: ICoreMouseService;
103   private _dirtyRowService: IDirtyRowService;
104   private _instantiationService: IInstantiationService;
105   private _logService: ILogService;
106   public optionsService: IOptionsService;
107
108   // browser services
109   private _charSizeService: ICharSizeService;
110   private _mouseService: IMouseService;
111   private _renderService: IRenderService;
112   private _selectionService: ISelectionService;
113   private _soundService: ISoundService;
114
115   // modes
116   public applicationKeypad: boolean;
117   public originMode: boolean;
118   public insertMode: boolean;
119   public wraparoundMode: boolean; // defaults: xterm - true, vt100 - false
120   public bracketedPasteMode: boolean;
121
122   // charset
123   // The current charset
124   public charset: ICharset;
125   public gcharset: number;
126   public glevel: number;
127   public charsets: ICharset[];
128
129   // mouse properties
130   public mouseEvents: CoreMouseEventType = CoreMouseEventType.NONE;
131   public sendFocus: boolean;
132
133   // misc
134   public savedCols: number;
135
136   public curAttrData: IAttributeData;
137   private _eraseAttrData: IAttributeData;
138
139   public params: (string | number)[];
140   public currentParam: string | number;
141
142   // write buffer
143   private _writeBuffer: WriteBuffer;
144
145   // Store if user went browsing history in scrollback
146   private _userScrolling: boolean;
147
148   /**
149    * Records whether the keydown event has already been handled and triggered a data event, if so
150    * the keypress event should not trigger a data event but should still print to the textarea so
151    * screen readers will announce it.
152    */
153   private _keyDownHandled: boolean = false;
154
155   private _inputHandler: InputHandler;
156   public linkifier: ILinkifier;
157   public viewport: IViewport;
158   private _compositionHelper: ICompositionHelper;
159   private _mouseZoneManager: IMouseZoneManager;
160   private _accessibilityManager: AccessibilityManager;
161   private _colorManager: ColorManager;
162   private _theme: ITheme;
163   private _windowsMode: IDisposable | undefined;
164
165   // bufferline to clone/copy from for new blank lines
166   private _blankLine: IBufferLine = null;
167
168   public get cols(): number { return this._bufferService.cols; }
169   public get rows(): number { return this._bufferService.rows; }
170
171   private _onCursorMove = new EventEmitter<void>();
172   public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
173   private _onData = new EventEmitter<string>();
174   public get onData(): IEvent<string> { return this._onData.event; }
175   private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>();
176   public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; }
177   private _onLineFeed = new EventEmitter<void>();
178   public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
179   private _onRender = new EventEmitter<{ start: number, end: number }>();
180   public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
181   private _onResize = new EventEmitter<{ cols: number, rows: number }>();
182   public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
183   private _onScroll = new EventEmitter<number>();
184   public get onScroll(): IEvent<number> { return this._onScroll.event; }
185   private _onSelectionChange = new EventEmitter<void>();
186   public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
187   private _onTitleChange = new EventEmitter<string>();
188   public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
189
190   private _onFocus = new EventEmitter<void>();
191   public get onFocus(): IEvent<void> { return this._onFocus.event; }
192   private _onBlur = new EventEmitter<void>();
193   public get onBlur(): IEvent<void> { return this._onBlur.event; }
194   public onA11yCharEmitter = new EventEmitter<string>();
195   public get onA11yChar(): IEvent<string> { return this.onA11yCharEmitter.event; }
196   public onA11yTabEmitter = new EventEmitter<number>();
197   public get onA11yTab(): IEvent<number> { return this.onA11yTabEmitter.event; }
198
199   /**
200    * Creates a new `Terminal` object.
201    *
202    * @param options An object containing a set of options, the available options are:
203    *   - `cursorBlink` (boolean): Whether the terminal cursor blinks
204    *   - `cols` (number): The number of columns of the terminal (horizontal size)
205    *   - `rows` (number): The number of rows of the terminal (vertical size)
206    *
207    * @public
208    * @class Xterm Xterm
209    * @alias module:xterm/src/xterm
210    */
211   constructor(
212     options: ITerminalOptions = {}
213   ) {
214     super();
215
216     // Setup and initialize common services
217     this._instantiationService = new InstantiationService();
218     this.optionsService = new OptionsService(options);
219     this._instantiationService.setService(IOptionsService, this.optionsService);
220     this._bufferService = this._instantiationService.createInstance(BufferService);
221     this._instantiationService.setService(IBufferService, this._bufferService);
222     this._logService = this._instantiationService.createInstance(LogService);
223     this._instantiationService.setService(ILogService, this._logService);
224     this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom());
225     this._instantiationService.setService(ICoreService, this._coreService);
226     this._coreService.onData(e => this._onData.fire(e));
227     this._coreMouseService = this._instantiationService.createInstance(CoreMouseService);
228     this._instantiationService.setService(ICoreMouseService, this._coreMouseService);
229     this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService);
230     this._instantiationService.setService(IDirtyRowService, this._dirtyRowService);
231
232     this._setupOptionsListeners();
233     this._setup();
234
235     this._writeBuffer = new WriteBuffer(data => this._inputHandler.parse(data));
236   }
237
238   public dispose(): void {
239     if (this._isDisposed) {
240       return;
241     }
242     super.dispose();
243     if (this._windowsMode) {
244       this._windowsMode.dispose();
245       this._windowsMode = undefined;
246     }
247     if (this._renderService) {
248       this._renderService.dispose();
249     }
250     this._customKeyEventHandler = null;
251     this.write = () => {};
252     if (this.element && this.element.parentNode) {
253       this.element.parentNode.removeChild(this.element);
254     }
255   }
256
257   private _setup(): void {
258     this._parent = document ? document.body : null;
259
260     this.cursorState = 0;
261     this.cursorHidden = false;
262     this._customKeyEventHandler = null;
263
264     // modes
265     this.applicationKeypad = false;
266     this.originMode = false;
267     this.insertMode = false;
268     this.wraparoundMode = true; // defaults: xterm - true, vt100 - false
269     this.bracketedPasteMode = false;
270
271     // charset
272     this.charset = null;
273     this.gcharset = null;
274     this.glevel = 0;
275     // TODO: Can this be just []?
276     this.charsets = [null];
277
278     this.curAttrData = DEFAULT_ATTR_DATA.clone();
279     this._eraseAttrData = DEFAULT_ATTR_DATA.clone();
280
281     this.params = [];
282     this.currentParam = 0;
283
284     this._userScrolling = false;
285
286     // Register input handler and refire/handle events
287     this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService);
288     this._inputHandler.onCursorMove(() => this._onCursorMove.fire());
289     this._inputHandler.onLineFeed(() => this._onLineFeed.fire());
290     this.register(this._inputHandler);
291
292     this.linkifier = this.linkifier || new Linkifier(this._bufferService, this._logService);
293
294     if (this.options.windowsMode) {
295       this._windowsMode = applyWindowsMode(this);
296     }
297   }
298
299   /**
300    * Convenience property to active buffer.
301    */
302   public get buffer(): IBuffer {
303     return this.buffers.active;
304   }
305
306   public get buffers(): IBufferSet {
307     return this._bufferService.buffers;
308   }
309
310   /**
311    * back_color_erase feature for xterm.
312    */
313   public eraseAttrData(): IAttributeData {
314     this._eraseAttrData.bg &= ~(Attributes.CM_MASK | 0xFFFFFF);
315     this._eraseAttrData.bg |= this.curAttrData.bg & ~0xFC000000;
316     return this._eraseAttrData;
317   }
318
319   /**
320    * Focus the terminal. Delegates focus handling to the terminal's DOM element.
321    */
322   public focus(): void {
323     if (this.textarea) {
324       this.textarea.focus({ preventScroll: true });
325     }
326   }
327
328   public get isFocused(): boolean {
329     return document.activeElement === this.textarea && document.hasFocus();
330   }
331
332   private _setupOptionsListeners(): void {
333     // TODO: These listeners should be owned by individual components
334     this.optionsService.onOptionChange(key => {
335       switch (key) {
336         case 'fontFamily':
337         case 'fontSize':
338           // When the font changes the size of the cells may change which requires a renderer clear
339           if (this._renderService) {
340             this._renderService.clear();
341           }
342           if (this._charSizeService) {
343             this._charSizeService.measure();
344           }
345           break;
346         case 'drawBoldTextInBrightColors':
347         case 'letterSpacing':
348         case 'lineHeight':
349         case 'fontWeight':
350         case 'fontWeightBold':
351           // When the font changes the size of the cells may change which requires a renderer clear
352           if (this._renderService) {
353             this._renderService.clear();
354             this._renderService.onResize(this.cols, this.rows);
355             this.refresh(0, this.rows - 1);
356           }
357           break;
358         case 'rendererType':
359           if (this._renderService) {
360             this._renderService.setRenderer(this._createRenderer());
361             this._renderService.onResize(this.cols, this.rows);
362           }
363           break;
364         case 'scrollback':
365           this.buffers.resize(this.cols, this.rows);
366           if (this.viewport) {
367             this.viewport.syncScrollArea();
368           }
369           break;
370         case 'screenReaderMode':
371           if (this.optionsService.options.screenReaderMode) {
372             if (!this._accessibilityManager && this._renderService) {
373               this._accessibilityManager = new AccessibilityManager(this, this._renderService);
374             }
375           } else {
376             if (this._accessibilityManager) {
377               this._accessibilityManager.dispose();
378               this._accessibilityManager = null;
379             }
380           }
381           break;
382         case 'tabStopWidth': this.buffers.setupTabStops(); break;
383         case 'theme':
384           this._setTheme(this.optionsService.options.theme);
385           break;
386         case 'windowsMode':
387           if (this.optionsService.options.windowsMode) {
388             if (!this._windowsMode) {
389               this._windowsMode = applyWindowsMode(this);
390             }
391           } else {
392             if (this._windowsMode) {
393               this._windowsMode.dispose();
394               this._windowsMode = undefined;
395             }
396           }
397           break;
398       }
399     });
400   }
401
402   /**
403    * Binds the desired focus behavior on a given terminal object.
404    */
405   private _onTextAreaFocus(ev: KeyboardEvent): void {
406     if (this.sendFocus) {
407       this._coreService.triggerDataEvent(C0.ESC + '[I');
408     }
409     this.updateCursorStyle(ev);
410     this.element.classList.add('focus');
411     this.showCursor();
412     this._onFocus.fire();
413   }
414
415   /**
416    * Blur the terminal, calling the blur function on the terminal's underlying
417    * textarea.
418    */
419   public blur(): void {
420     return this.textarea.blur();
421   }
422
423   /**
424    * Binds the desired blur behavior on a given terminal object.
425    */
426   private _onTextAreaBlur(): void {
427     // Text can safely be removed on blur. Doing it earlier could interfere with
428     // screen readers reading it out.
429     this.textarea.value = '';
430     this.refresh(this.buffer.y, this.buffer.y);
431     if (this.sendFocus) {
432       this._coreService.triggerDataEvent(C0.ESC + '[O');
433     }
434     this.element.classList.remove('focus');
435     this._onBlur.fire();
436   }
437
438   /**
439    * Initialize default behavior
440    */
441   private _initGlobal(): void {
442     this._bindKeys();
443
444     // Bind clipboard functionality
445     this.register(addDisposableDomListener(this.element, 'copy', (event: ClipboardEvent) => {
446       // If mouse events are active it means the selection manager is disabled and
447       // copy should be handled by the host program.
448       if (!this.hasSelection()) {
449         return;
450       }
451       copyHandler(event, this._selectionService);
452     }));
453     const pasteHandlerWrapper = (event: ClipboardEvent) => handlePasteEvent(event, this.textarea, this.bracketedPasteMode, this._coreService);
454     this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper));
455     this.register(addDisposableDomListener(this.element, 'paste', pasteHandlerWrapper));
456
457     // Handle right click context menus
458     if (Browser.isFirefox) {
459       // Firefox doesn't appear to fire the contextmenu event on right click
460       this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => {
461         if (event.button === 2) {
462           rightClickHandler(event, this.textarea, this.screenElement, this._selectionService, this.options.rightClickSelectsWord);
463         }
464       }));
465     } else {
466       this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => {
467         rightClickHandler(event, this.textarea, this.screenElement, this._selectionService, this.options.rightClickSelectsWord);
468       }));
469     }
470
471     // Move the textarea under the cursor when middle clicking on Linux to ensure
472     // middle click to paste selection works. This only appears to work in Chrome
473     // at the time is writing.
474     if (Browser.isLinux) {
475       // Use auxclick event over mousedown the latter doesn't seem to work. Note
476       // that the regular click event doesn't fire for the middle mouse button.
477       this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => {
478         if (event.button === 1) {
479           moveTextAreaUnderMouseCursor(event, this.textarea, this.screenElement);
480         }
481       }));
482     }
483   }
484
485   /**
486    * Apply key handling to the terminal
487    */
488   private _bindKeys(): void {
489     this.register(addDisposableDomListener(this.textarea, 'keyup', (ev: KeyboardEvent) => this._keyUp(ev), true));
490     this.register(addDisposableDomListener(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true));
491     this.register(addDisposableDomListener(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true));
492     this.register(addDisposableDomListener(this.textarea, 'compositionstart', () => this._compositionHelper.compositionstart()));
493     this.register(addDisposableDomListener(this.textarea, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper.compositionupdate(e)));
494     this.register(addDisposableDomListener(this.textarea, 'compositionend', () => this._compositionHelper.compositionend()));
495     this.register(this.onRender(() => this._compositionHelper.updateCompositionElements()));
496     this.register(this.onRender(e => this._queueLinkification(e.start, e.end)));
497   }
498
499   /**
500    * Opens the terminal within an element.
501    *
502    * @param parent The element to create the terminal within.
503    */
504   public open(parent: HTMLElement): void {
505     this._parent = parent || this._parent;
506
507     if (!this._parent) {
508       throw new Error('Terminal requires a parent element.');
509     }
510
511     if (!document.body.contains(parent)) {
512       this._logService.warn('Terminal.open was called on an element that was not attached to the DOM');
513     }
514
515     this._document = this._parent.ownerDocument;
516
517     // Create main element container
518     this.element = this._document.createElement('div');
519     this.element.dir = 'ltr';   // xterm.css assumes LTR
520     this.element.classList.add('terminal');
521     this.element.classList.add('xterm');
522     this.element.setAttribute('tabindex', '0');
523     this._parent.appendChild(this.element);
524
525     // Performance: Use a document fragment to build the terminal
526     // viewport and helper elements detached from the DOM
527     const fragment = document.createDocumentFragment();
528     this._viewportElement = document.createElement('div');
529     this._viewportElement.classList.add('xterm-viewport');
530     fragment.appendChild(this._viewportElement);
531     this._viewportScrollArea = document.createElement('div');
532     this._viewportScrollArea.classList.add('xterm-scroll-area');
533     this._viewportElement.appendChild(this._viewportScrollArea);
534
535     this.screenElement = document.createElement('div');
536     this.screenElement.classList.add('xterm-screen');
537     // Create the container that will hold helpers like the textarea for
538     // capturing DOM Events. Then produce the helpers.
539     this._helperContainer = document.createElement('div');
540     this._helperContainer.classList.add('xterm-helpers');
541     this.screenElement.appendChild(this._helperContainer);
542     fragment.appendChild(this.screenElement);
543
544     this.textarea = document.createElement('textarea');
545     this.textarea.classList.add('xterm-helper-textarea');
546     this.textarea.setAttribute('aria-label', Strings.promptLabel);
547     this.textarea.setAttribute('aria-multiline', 'false');
548     this.textarea.setAttribute('autocorrect', 'off');
549     this.textarea.setAttribute('autocapitalize', 'off');
550     this.textarea.setAttribute('spellcheck', 'false');
551     this.textarea.tabIndex = 0;
552     this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._onTextAreaFocus(ev)));
553     this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
554     this._helperContainer.appendChild(this.textarea);
555
556     this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
557     this._instantiationService.setService(ICharSizeService, this._charSizeService);
558
559     this._compositionView = document.createElement('div');
560     this._compositionView.classList.add('composition-view');
561     this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
562     this._helperContainer.appendChild(this._compositionView);
563
564     // Performance: Add viewport and helper elements from the fragment
565     this.element.appendChild(fragment);
566
567     this._theme = this.options.theme || this._theme;
568     this.options.theme = undefined;
569     this._colorManager = new ColorManager(document, this.options.allowTransparency);
570     this._colorManager.setTheme(this._theme);
571
572     const renderer = this._createRenderer();
573     this._renderService = this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement);
574     this._instantiationService.setService(IRenderService, this._renderService);
575     this._renderService.onRender(e => this._onRender.fire(e));
576     this.onResize(e => this._renderService.resize(e.cols, e.rows));
577
578     this._soundService = this._instantiationService.createInstance(SoundService);
579     this._instantiationService.setService(ISoundService, this._soundService);
580     this._mouseService = this._instantiationService.createInstance(MouseService);
581     this._instantiationService.setService(IMouseService, this._mouseService);
582
583     this.viewport = this._instantiationService.createInstance(Viewport,
584       (amount: number, suppressEvent: boolean) => this.scrollLines(amount, suppressEvent),
585       this._viewportElement,
586       this._viewportScrollArea
587     );
588     this.viewport.onThemeChange(this._colorManager.colors);
589     this.register(this.viewport);
590
591     this.register(this.onCursorMove(() => this._renderService.onCursorMove()));
592     this.register(this.onResize(() => this._renderService.onResize(this.cols, this.rows)));
593     this.register(this.onBlur(() => this._renderService.onBlur()));
594     this.register(this.onFocus(() => this._renderService.onFocus()));
595     this.register(this._renderService.onDimensionsChange(() => this.viewport.syncScrollArea()));
596
597     this._selectionService = this._instantiationService.createInstance(SelectionService,
598       (amount: number, suppressEvent: boolean) => this.scrollLines(amount, suppressEvent),
599       this.element,
600       this.screenElement);
601     this._instantiationService.setService(ISelectionService, this._selectionService);
602     this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire()));
603     this.register(this._selectionService.onRedrawRequest(e => this._renderService.onSelectionChanged(e.start, e.end, e.columnSelectMode)));
604     this.register(this._selectionService.onLinuxMouseSelection(text => {
605       // If there's a new selection, put it into the textarea, focus and select it
606       // in order to register it as a selection on the OS. This event is fired
607       // only on Linux to enable middle click to paste selection.
608       this.textarea.value = text;
609       this.textarea.focus();
610       this.textarea.select();
611     }));
612     this.register(this.onScroll(() => {
613       this.viewport.syncScrollArea();
614       this._selectionService.refresh();
615     }));
616     this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService.refresh()));
617
618     this._mouseZoneManager = this._instantiationService.createInstance(MouseZoneManager, this.element, this.screenElement);
619     this.register(this._mouseZoneManager);
620     this.register(this.onScroll(() => this._mouseZoneManager.clearAll()));
621     this.linkifier.attachToDom(this.element, this._mouseZoneManager);
622
623     // This event listener must be registered aftre MouseZoneManager is created
624     this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService.onMouseDown(e)));
625
626     // apply mouse event classes set by escape codes before terminal was attached
627     if (this.mouseEvents) {
628       this._selectionService.disable();
629       this.element.classList.add('enable-mouse-events');
630     } else {
631       this._selectionService.enable();
632     }
633
634     if (this.options.screenReaderMode) {
635       // Note that this must be done *after* the renderer is created in order to
636       // ensure the correct order of the dprchange event
637       this._accessibilityManager = new AccessibilityManager(this, this._renderService);
638     }
639
640     // Measure the character size
641     this._charSizeService.measure();
642
643     // Setup loop that draws to screen
644     this.refresh(0, this.rows - 1);
645
646     // Initialize global actions that need to be taken on the document.
647     this._initGlobal();
648
649     // Listen for mouse events and translate
650     // them into terminal mouse protocols.
651     this.bindMouse();
652   }
653
654   private _createRenderer(): IRenderer {
655     switch (this.options.rendererType) {
656       case 'canvas': return new Renderer(this._colorManager.colors, this, this._bufferService, this._charSizeService, this.optionsService);
657       case 'dom': return new DomRenderer(this, this._colorManager.colors, this._charSizeService, this.optionsService);
658       default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
659     }
660   }
661
662   /**
663    * Sets the theme on the renderer. The renderer must have been initialized.
664    * @param theme The theme to set.
665    */
666   private _setTheme(theme: ITheme): void {
667     this._theme = theme;
668     if (this._colorManager) {
669       this._colorManager.setTheme(theme);
670     }
671     if (this._renderService) {
672       this._renderService.setColors(this._colorManager.colors);
673     }
674     if (this.viewport) {
675       this.viewport.onThemeChange(this._colorManager.colors);
676     }
677   }
678
679   /**
680    * Bind certain mouse events to the terminal.
681    * By default only 3 button + wheel up/down is ativated. For higher buttons
682    * no mouse report will be created. Typically the standard actions will be active.
683    *
684    * There are several reasons not to enable support for higher buttons/wheel:
685    * - Button 4 and 5 are typically used for history back and forward navigation,
686    *   there is no straight forward way to supress/intercept those standard actions.
687    * - Support for higher buttons does not work in some platform/browser combinations.
688    * - Left/right wheel was not tested.
689    * - Emulators vary in mouse button support, typically only 3 buttons and
690    *   wheel up/down work reliable.
691    *
692    * TODO: Move mouse event code into its own file.
693    */
694   public bindMouse(): void {
695     const self = this;
696     const el = this.element;
697
698     // send event to CoreMouseService
699     function sendEvent(ev: MouseEvent | WheelEvent): boolean {
700       let pos;
701
702       // get mouse coordinates
703       pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows);
704       if (!pos) {
705         return false;
706       }
707
708       let but: CoreMouseButton;
709       let action: CoreMouseAction;
710       switch ((<any>ev).overrideType || ev.type) {
711         case 'mousemove':
712           action = CoreMouseAction.MOVE;
713           if (ev.buttons === undefined) {
714             // buttons is not supported on macOS, try to get a value from button instead
715             but = CoreMouseButton.NONE;
716             if (ev.button !== undefined) {
717               but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
718             }
719           } else {
720             // according to MDN buttons only reports up to button 5 (AUX2)
721             but = ev.buttons & 1 ? CoreMouseButton.LEFT :
722                   ev.buttons & 4 ? CoreMouseButton.MIDDLE :
723                   ev.buttons & 2 ? CoreMouseButton.RIGHT :
724                   CoreMouseButton.NONE; // fallback to NONE
725           }
726           break;
727         case 'mouseup':
728           action = CoreMouseAction.UP;
729           but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
730           break;
731         case 'mousedown':
732           action = CoreMouseAction.DOWN;
733           but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
734           break;
735         case 'wheel':
736           // only UP/DOWN wheel events are respected
737           if ((ev as WheelEvent).deltaY !== 0) {
738             action = (ev as WheelEvent).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
739           }
740           but = CoreMouseButton.WHEEL;
741           break;
742         default:
743           // dont handle other event types by accident
744           return false;
745       }
746
747       // exit if we cannot determine valid button/action values
748       // do nothing for higher buttons than wheel
749       if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) {
750         return false;
751       }
752
753       return self._coreMouseService.triggerMouseEvent({
754         col: pos.x - 33, // FIXME: why -33 here?
755         row: pos.y - 33,
756         button: but,
757         action,
758         ctrl: ev.ctrlKey,
759         alt: ev.altKey,
760         shift: ev.shiftKey
761       });
762     }
763
764     /**
765      * Event listener state handling.
766      * We listen to the onProtocolChange event of CoreMouseService and put
767      * requested listeners in `requestedEvents`. With this the listeners
768      * have all bits to do the event listener juggling.
769      * Note: 'mousedown' currently is "always on" and not managed
770      * by onProtocolChange.
771      */
772     const requestedEvents: {[key: string]: ((ev: Event) => void) | null} = {
773       mouseup: null,
774       wheel: null,
775       mousedrag: null,
776       mousemove: null
777     };
778     const eventListeners: {[key: string]: (ev: Event) => void} = {
779       mouseup: (ev: MouseEvent) => {
780         sendEvent(ev);
781         if (!ev.buttons) {
782           // if no other button is held remove global handlers
783           this._document.removeEventListener('mouseup', requestedEvents.mouseup);
784           if (requestedEvents.mousedrag) {
785             this._document.removeEventListener('mousemove', requestedEvents.mousedrag);
786           }
787         }
788         return this.cancel(ev);
789       },
790       wheel: (ev: WheelEvent) => {
791         sendEvent(ev);
792         ev.preventDefault();
793         return this.cancel(ev);
794       },
795       mousedrag: (ev: MouseEvent) => {
796         // deal only with move while a button is held
797         if (ev.buttons) {
798           sendEvent(ev);
799         }
800       },
801       mousemove: (ev: MouseEvent) => {
802         // deal only with move without any button
803         if (!ev.buttons) {
804           sendEvent(ev);
805         }
806       }
807     };
808     this._coreMouseService.onProtocolChange(events => {
809       // apply global changes on events
810       this.mouseEvents = events;
811       if (events) {
812         if (this.optionsService.options.logLevel === 'debug') {
813           this._logService.debug('Binding to mouse events:', this._coreMouseService.explainEvents(events));
814         }
815         this.element.classList.add('enable-mouse-events');
816         this._selectionService.disable();
817       } else {
818         this._logService.debug('Unbinding from mouse events.');
819         this.element.classList.remove('enable-mouse-events');
820         this._selectionService.enable();
821       }
822
823       // add/remove handlers from requestedEvents
824
825       if (!(events & CoreMouseEventType.MOVE)) {
826         el.removeEventListener('mousemove', requestedEvents.mousemove);
827         requestedEvents.mousemove = null;
828       } else if (!requestedEvents.mousemove) {
829         el.addEventListener('mousemove', eventListeners.mousemove);
830         requestedEvents.mousemove = eventListeners.mousemove;
831       }
832
833       if (!(events & CoreMouseEventType.WHEEL)) {
834         el.removeEventListener('wheel', requestedEvents.wheel);
835         requestedEvents.wheel = null;
836       } else if (!requestedEvents.wheel) {
837         el.addEventListener('wheel', eventListeners.wheel);
838         requestedEvents.wheel = eventListeners.wheel;
839       }
840
841       if (!(events & CoreMouseEventType.UP)) {
842         this._document.removeEventListener('mouseup', requestedEvents.mouseup);
843         requestedEvents.mouseup = null;
844       } else if (!requestedEvents.mouseup) {
845         requestedEvents.mouseup = eventListeners.mouseup;
846       }
847
848       if (!(events & CoreMouseEventType.DRAG)) {
849         this._document.removeEventListener('mousemove', requestedEvents.mousedrag);
850         requestedEvents.mousedrag = null;
851       } else if (!requestedEvents.mousedrag) {
852         requestedEvents.mousedrag = eventListeners.mousedrag;
853       }
854     });
855     // force initial onProtocolChange so we dont miss early mouse requests
856     this._coreMouseService.activeProtocol = this._coreMouseService.activeProtocol;
857
858     /**
859      * "Always on" event listeners.
860      */
861     this.register(addDisposableDomListener(el, 'mousedown', (ev: MouseEvent) => {
862       ev.preventDefault();
863       this.focus();
864
865       // Don't send the mouse button to the pty if mouse events are disabled or
866       // if the selection manager is having selection forced (ie. a modifier is
867       // held).
868       if (!this.mouseEvents || this._selectionService.shouldForceSelection(ev)) {
869         return;
870       }
871
872       sendEvent(ev);
873
874       // Register additional global handlers which should keep reporting outside
875       // of the terminal element.
876       // Note: Other emulators also do this for 'mousedown' while a button
877       // is held, we currently limit 'mousedown' to the terminal only.
878       if (requestedEvents.mouseup) {
879         this._document.addEventListener('mouseup', requestedEvents.mouseup);
880       }
881       if (requestedEvents.mousedrag) {
882         this._document.addEventListener('mousemove', requestedEvents.mousedrag);
883       }
884
885       return this.cancel(ev);
886     }));
887
888     this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
889       if (!requestedEvents.wheel) {
890         // Convert wheel events into up/down events when the buffer does not have scrollback, this
891         // enables scrolling in apps hosted in the alt buffer such as vim or tmux.
892         if (!this.buffer.hasScrollback) {
893           const amount = this.viewport.getLinesScrolled(ev);
894
895           // Do nothing if there's no vertical scroll
896           if (amount === 0) {
897             return;
898           }
899
900           // Construct and send sequences
901           const sequence = C0.ESC + (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + ( ev.deltaY < 0 ? 'A' : 'B');
902           let data = '';
903           for (let i = 0; i < Math.abs(amount); i++) {
904             data += sequence;
905           }
906           this._coreService.triggerDataEvent(data, true);
907         }
908         return;
909       }
910     }));
911
912     // allow wheel scrolling in
913     // the shell for example
914     this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
915       if (requestedEvents.wheel) return;
916       if (!this.viewport.onWheel(ev)) {
917         return this.cancel(ev);
918       }
919     }));
920
921     this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => {
922       if (this.mouseEvents) return;
923       this.viewport.onTouchStart(ev);
924       return this.cancel(ev);
925     }));
926
927     this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => {
928       if (this.mouseEvents) return;
929       if (!this.viewport.onTouchMove(ev)) {
930         return this.cancel(ev);
931       }
932     }));
933   }
934
935
936   /**
937    * Tells the renderer to refresh terminal content between two rows (inclusive) at the next
938    * opportunity.
939    * @param start The row to start from (between 0 and this.rows - 1).
940    * @param end The row to end at (between start and this.rows - 1).
941    */
942   public refresh(start: number, end: number): void {
943     if (this._renderService) {
944       this._renderService.refreshRows(start, end);
945     }
946   }
947
948   /**
949    * Queues linkification for the specified rows.
950    * @param start The row to start from (between 0 and this.rows - 1).
951    * @param end The row to end at (between start and this.rows - 1).
952    */
953   private _queueLinkification(start: number, end: number): void {
954     if (this.linkifier) {
955       this.linkifier.linkifyRows(start, end);
956     }
957   }
958
959   /**
960    * Change the cursor style for different selection modes
961    */
962   public updateCursorStyle(ev: KeyboardEvent): void {
963     if (this._selectionService && this._selectionService.shouldColumnSelect(ev)) {
964       this.element.classList.add('column-select');
965     } else {
966       this.element.classList.remove('column-select');
967     }
968   }
969
970   /**
971    * Display the cursor element
972    */
973   public showCursor(): void {
974     if (!this.cursorState) {
975       this.cursorState = 1;
976       this.refresh(this.buffer.y, this.buffer.y);
977     }
978   }
979
980   /**
981    * Scroll the terminal down 1 row, creating a blank line.
982    * @param isWrapped Whether the new line is wrapped from the previous line.
983    */
984   public scroll(isWrapped: boolean = false): void {
985     let newLine: IBufferLine;
986     newLine = this._blankLine;
987     const eraseAttr = this.eraseAttrData();
988     if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {
989       newLine = this.buffer.getBlankLine(eraseAttr, isWrapped);
990       this._blankLine = newLine;
991     }
992     newLine.isWrapped = isWrapped;
993
994     const topRow = this.buffer.ybase + this.buffer.scrollTop;
995     const bottomRow = this.buffer.ybase + this.buffer.scrollBottom;
996
997     if (this.buffer.scrollTop === 0) {
998       // Determine whether the buffer is going to be trimmed after insertion.
999       const willBufferBeTrimmed = this.buffer.lines.isFull;
1000
1001       // Insert the line using the fastest method
1002       if (bottomRow === this.buffer.lines.length - 1) {
1003         if (willBufferBeTrimmed) {
1004           this.buffer.lines.recycle().copyFrom(newLine);
1005         } else {
1006           this.buffer.lines.push(newLine.clone());
1007         }
1008       } else {
1009         this.buffer.lines.splice(bottomRow + 1, 0, newLine.clone());
1010       }
1011
1012       // Only adjust ybase and ydisp when the buffer is not trimmed
1013       if (!willBufferBeTrimmed) {
1014         this.buffer.ybase++;
1015         // Only scroll the ydisp with ybase if the user has not scrolled up
1016         if (!this._userScrolling) {
1017           this.buffer.ydisp++;
1018         }
1019       } else {
1020         // When the buffer is full and the user has scrolled up, keep the text
1021         // stable unless ydisp is right at the top
1022         if (this._userScrolling) {
1023           this.buffer.ydisp = Math.max(this.buffer.ydisp - 1, 0);
1024         }
1025       }
1026     } else {
1027       // scrollTop is non-zero which means no line will be going to the
1028       // scrollback, instead we can just shift them in-place.
1029       const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/;
1030       this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);
1031       this.buffer.lines.set(bottomRow, newLine.clone());
1032     }
1033
1034     // Move the viewport to the bottom of the buffer unless the user is
1035     // scrolling.
1036     if (!this._userScrolling) {
1037       this.buffer.ydisp = this.buffer.ybase;
1038     }
1039
1040     // Flag rows that need updating
1041     this._dirtyRowService.markRangeDirty(this.buffer.scrollTop, this.buffer.scrollBottom);
1042
1043     this._onScroll.fire(this.buffer.ydisp);
1044   }
1045
1046   /**
1047    * Scroll the display of the terminal
1048    * @param disp The number of lines to scroll down (negative scroll up).
1049    * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used
1050    * to avoid unwanted events being handled by the viewport when the event was triggered from the
1051    * viewport originally.
1052    */
1053   public scrollLines(disp: number, suppressScrollEvent?: boolean): void {
1054     if (disp < 0) {
1055       if (this.buffer.ydisp === 0) {
1056         return;
1057       }
1058       this._userScrolling = true;
1059     } else if (disp + this.buffer.ydisp >= this.buffer.ybase) {
1060       this._userScrolling = false;
1061     }
1062
1063     const oldYdisp = this.buffer.ydisp;
1064     this.buffer.ydisp = Math.max(Math.min(this.buffer.ydisp + disp, this.buffer.ybase), 0);
1065
1066     // No change occurred, don't trigger scroll/refresh
1067     if (oldYdisp === this.buffer.ydisp) {
1068       return;
1069     }
1070
1071     if (!suppressScrollEvent) {
1072       this._onScroll.fire(this.buffer.ydisp);
1073     }
1074
1075     this.refresh(0, this.rows - 1);
1076   }
1077
1078   /**
1079    * Scroll the display of the terminal by a number of pages.
1080    * @param pageCount The number of pages to scroll (negative scrolls up).
1081    */
1082   public scrollPages(pageCount: number): void {
1083     this.scrollLines(pageCount * (this.rows - 1));
1084   }
1085
1086   /**
1087    * Scrolls the display of the terminal to the top.
1088    */
1089   public scrollToTop(): void {
1090     this.scrollLines(-this.buffer.ydisp);
1091   }
1092
1093   /**
1094    * Scrolls the display of the terminal to the bottom.
1095    */
1096   public scrollToBottom(): void {
1097     this.scrollLines(this.buffer.ybase - this.buffer.ydisp);
1098   }
1099
1100   public scrollToLine(line: number): void {
1101     const scrollAmount = line - this.buffer.ydisp;
1102     if (scrollAmount !== 0) {
1103       this.scrollLines(scrollAmount);
1104     }
1105   }
1106
1107   public paste(data: string): void {
1108     paste(data, this.textarea, this.bracketedPasteMode, this._coreService);
1109   }
1110
1111   /**
1112    * Attaches a custom key event handler which is run before keys are processed,
1113    * giving consumers of xterm.js ultimate control as to what keys should be
1114    * processed by the terminal and what keys should not.
1115    * @param customKeyEventHandler The custom KeyboardEvent handler to attach.
1116    * This is a function that takes a KeyboardEvent, allowing consumers to stop
1117    * propagation and/or prevent the default action. The function returns whether
1118    * the event should be processed by xterm.js.
1119    */
1120   public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void {
1121     this._customKeyEventHandler = customKeyEventHandler;
1122   }
1123
1124   /** Add handler for ESC escape sequence. See xterm.d.ts for details. */
1125   public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable {
1126     return this._inputHandler.addEscHandler(id, callback);
1127   }
1128
1129   /** Add handler for DCS escape sequence. See xterm.d.ts for details. */
1130   public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
1131     return this._inputHandler.addDcsHandler(id, callback);
1132   }
1133
1134   /** Add handler for CSI escape sequence. See xterm.d.ts for details. */
1135   public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
1136     return this._inputHandler.addCsiHandler(id, callback);
1137   }
1138   /** Add handler for OSC escape sequence. See xterm.d.ts for details. */
1139   public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
1140     return this._inputHandler.addOscHandler(ident, callback);
1141   }
1142
1143   /**
1144    * Registers a link matcher, allowing custom link patterns to be matched and
1145    * handled.
1146    * @param regex The regular expression to search for, specifically
1147    * this searches the textContent of the rows. You will want to use \s to match
1148    * a space ' ' character for example.
1149    * @param handler The callback when the link is called.
1150    * @param options Options for the link matcher.
1151    * @return The ID of the new matcher, this can be used to deregister.
1152    */
1153   public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number {
1154     const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options);
1155     this.refresh(0, this.rows - 1);
1156     return matcherId;
1157   }
1158
1159   /**
1160    * Deregisters a link matcher if it has been registered.
1161    * @param matcherId The link matcher's ID (returned after register)
1162    */
1163   public deregisterLinkMatcher(matcherId: number): void {
1164     if (this.linkifier.deregisterLinkMatcher(matcherId)) {
1165       this.refresh(0, this.rows - 1);
1166     }
1167   }
1168
1169   public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
1170     const joinerId = this._renderService.registerCharacterJoiner(handler);
1171     this.refresh(0, this.rows - 1);
1172     return joinerId;
1173   }
1174
1175   public deregisterCharacterJoiner(joinerId: number): void {
1176     if (this._renderService.deregisterCharacterJoiner(joinerId)) {
1177       this.refresh(0, this.rows - 1);
1178     }
1179   }
1180
1181   public get markers(): IMarker[] {
1182     return this.buffer.markers;
1183   }
1184
1185   public addMarker(cursorYOffset: number): IMarker {
1186     // Disallow markers on the alt buffer
1187     if (this.buffer !== this.buffers.normal) {
1188       return;
1189     }
1190
1191     return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset);
1192   }
1193
1194   /**
1195    * Gets whether the terminal has an active selection.
1196    */
1197   public hasSelection(): boolean {
1198     return this._selectionService ? this._selectionService.hasSelection : false;
1199   }
1200
1201   /**
1202    * Selects text within the terminal.
1203    * @param column The column the selection starts at..
1204    * @param row The row the selection starts at.
1205    * @param length The length of the selection.
1206    */
1207   public select(column: number, row: number, length: number): void {
1208     this._selectionService.setSelection(column, row, length);
1209   }
1210
1211   /**
1212    * Gets the terminal's current selection, this is useful for implementing copy
1213    * behavior outside of xterm.js.
1214    */
1215   public getSelection(): string {
1216     return this._selectionService ? this._selectionService.selectionText : '';
1217   }
1218
1219   public getSelectionPosition(): ISelectionPosition | undefined {
1220     if (!this._selectionService.hasSelection) {
1221       return undefined;
1222     }
1223
1224     return {
1225       startColumn: this._selectionService.selectionStart[0],
1226       startRow: this._selectionService.selectionStart[1],
1227       endColumn: this._selectionService.selectionEnd[0],
1228       endRow: this._selectionService.selectionEnd[1]
1229     };
1230   }
1231
1232   /**
1233    * Clears the current terminal selection.
1234    */
1235   public clearSelection(): void {
1236     if (this._selectionService) {
1237       this._selectionService.clearSelection();
1238     }
1239   }
1240
1241   /**
1242    * Selects all text within the terminal.
1243    */
1244   public selectAll(): void {
1245     if (this._selectionService) {
1246       this._selectionService.selectAll();
1247     }
1248   }
1249
1250   public selectLines(start: number, end: number): void {
1251     if (this._selectionService) {
1252       this._selectionService.selectLines(start, end);
1253     }
1254   }
1255
1256   /**
1257    * Handle a keydown event
1258    * Key Resources:
1259    *   - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
1260    * @param ev The keydown event to be handled.
1261    */
1262   protected _keyDown(event: KeyboardEvent): boolean {
1263     this._keyDownHandled = false;
1264
1265     if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) {
1266       return false;
1267     }
1268
1269     if (!this._compositionHelper.keydown(event)) {
1270       if (this.buffer.ybase !== this.buffer.ydisp) {
1271         this.scrollToBottom();
1272       }
1273       return false;
1274     }
1275
1276     const result = evaluateKeyboardEvent(event, this._coreService.decPrivateModes.applicationCursorKeys, this.browser.isMac, this.options.macOptionIsMeta);
1277
1278     this.updateCursorStyle(event);
1279
1280     if (result.type === KeyboardResultType.PAGE_DOWN || result.type === KeyboardResultType.PAGE_UP) {
1281       const scrollCount = this.rows - 1;
1282       this.scrollLines(result.type === KeyboardResultType.PAGE_UP ? -scrollCount : scrollCount);
1283       return this.cancel(event, true);
1284     }
1285
1286     if (result.type === KeyboardResultType.SELECT_ALL) {
1287       this.selectAll();
1288     }
1289
1290     if (this._isThirdLevelShift(this.browser, event)) {
1291       return true;
1292     }
1293
1294     if (result.cancel) {
1295       // The event is canceled at the end already, is this necessary?
1296       this.cancel(event, true);
1297     }
1298
1299     if (!result.key) {
1300       return true;
1301     }
1302
1303     // If ctrl+c or enter is being sent, clear out the textarea. This is done so that screen readers
1304     // will announce deleted characters. This will not work 100% of the time but it should cover
1305     // most scenarios.
1306     if (result.key === C0.ETX || result.key === C0.CR) {
1307       this.textarea.value = '';
1308     }
1309
1310     this._onKey.fire({ key: result.key, domEvent: event });
1311     this.showCursor();
1312     this._coreService.triggerDataEvent(result.key, true);
1313
1314     // Cancel events when not in screen reader mode so events don't get bubbled up and handled by
1315     // other listeners. When screen reader mode is enabled, this could cause issues if the event
1316     // is handled at a higher level, this is a compromise in order to echo keys to the screen
1317     // reader.
1318     if (!this.optionsService.options.screenReaderMode) {
1319       return this.cancel(event, true);
1320     }
1321
1322     this._keyDownHandled = true;
1323   }
1324
1325   private _isThirdLevelShift(browser: IBrowser, ev: IKeyboardEvent): boolean {
1326     const thirdLevelKey =
1327         (browser.isMac && !this.options.macOptionIsMeta && ev.altKey && !ev.ctrlKey && !ev.metaKey) ||
1328         (browser.isWindows && ev.altKey && ev.ctrlKey && !ev.metaKey);
1329
1330     if (ev.type === 'keypress') {
1331       return thirdLevelKey;
1332     }
1333
1334     // Don't invoke for arrows, pageDown, home, backspace, etc. (on non-keypress events)
1335     return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47);
1336   }
1337
1338   /**
1339    * Set the G level of the terminal
1340    * @param g
1341    */
1342   public setgLevel(g: number): void {
1343     this.glevel = g;
1344     this.charset = this.charsets[g];
1345   }
1346
1347   /**
1348    * Set the charset for the given G level of the terminal
1349    * @param g
1350    * @param charset
1351    */
1352   public setgCharset(g: number, charset: ICharset): void {
1353     this.charsets[g] = charset;
1354     if (this.glevel === g) {
1355       this.charset = charset;
1356     }
1357   }
1358
1359   protected _keyUp(ev: KeyboardEvent): void {
1360     if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) {
1361       return;
1362     }
1363
1364     if (!wasModifierKeyOnlyEvent(ev)) {
1365       this.focus();
1366     }
1367
1368     this.updateCursorStyle(ev);
1369   }
1370
1371   /**
1372    * Handle a keypress event.
1373    * Key Resources:
1374    *   - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
1375    * @param ev The keypress event to be handled.
1376    */
1377   protected _keyPress(ev: KeyboardEvent): boolean {
1378     let key;
1379
1380     if (this._keyDownHandled) {
1381       return false;
1382     }
1383
1384     if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) {
1385       return false;
1386     }
1387
1388     this.cancel(ev);
1389
1390     if (ev.charCode) {
1391       key = ev.charCode;
1392     } else if (ev.which === null || ev.which === undefined) {
1393       key = ev.keyCode;
1394     } else if (ev.which !== 0 && ev.charCode !== 0) {
1395       key = ev.which;
1396     } else {
1397       return false;
1398     }
1399
1400     if (!key || (
1401       (ev.altKey || ev.ctrlKey || ev.metaKey) && !this._isThirdLevelShift(this.browser, ev)
1402     )) {
1403       return false;
1404     }
1405
1406     key = String.fromCharCode(key);
1407
1408     this._onKey.fire({ key, domEvent: ev });
1409     this.showCursor();
1410     this._coreService.triggerDataEvent(key, true);
1411
1412     return true;
1413   }
1414
1415   /**
1416    * Ring the bell.
1417    * Note: We could do sweet things with webaudio here
1418    */
1419   public bell(): void {
1420     if (this._soundBell()) {
1421       this._soundService.playBellSound();
1422     }
1423
1424     if (this._visualBell()) {
1425       this.element.classList.add('visual-bell-active');
1426       clearTimeout(this._visualBellTimer);
1427       this._visualBellTimer = window.setTimeout(() => {
1428         this.element.classList.remove('visual-bell-active');
1429       }, 200);
1430     }
1431   }
1432
1433   /**
1434    * Resizes the terminal.
1435    *
1436    * @param x The number of columns to resize to.
1437    * @param y The number of rows to resize to.
1438    */
1439   public resize(x: number, y: number): void {
1440     if (isNaN(x) || isNaN(y)) {
1441       return;
1442     }
1443
1444     if (x === this.cols && y === this.rows) {
1445       // Check if we still need to measure the char size (fixes #785).
1446       if (this._charSizeService && !this._charSizeService.hasValidSize) {
1447         this._charSizeService.measure();
1448       }
1449       return;
1450     }
1451
1452     if (x < MINIMUM_COLS) x = MINIMUM_COLS;
1453     if (y < MINIMUM_ROWS) y = MINIMUM_ROWS;
1454
1455     this.buffers.resize(x, y);
1456
1457     this._bufferService.resize(x, y);
1458     this.buffers.setupTabStops(this.cols);
1459
1460     if (this._charSizeService) {
1461       this._charSizeService.measure();
1462     }
1463
1464     // Sync the scroll area to make sure scroll events don't fire and scroll the viewport to an
1465     // invalid location
1466     this.viewport.syncScrollArea(true);
1467
1468     this.refresh(0, this.rows - 1);
1469     this._onResize.fire({ cols: x, rows: y });
1470   }
1471
1472   /**
1473    * Clear the entire buffer, making the prompt line the new first line.
1474    */
1475   public clear(): void {
1476     if (this.buffer.ybase === 0 && this.buffer.y === 0) {
1477       // Don't clear if it's already clear
1478       return;
1479     }
1480     this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y));
1481     this.buffer.lines.length = 1;
1482     this.buffer.ydisp = 0;
1483     this.buffer.ybase = 0;
1484     this.buffer.y = 0;
1485     for (let i = 1; i < this.rows; i++) {
1486       this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA));
1487     }
1488     this.refresh(0, this.rows - 1);
1489     this._onScroll.fire(this.buffer.ydisp);
1490   }
1491
1492   /**
1493    * Evaluate if the current terminal is the given argument.
1494    * @param term The terminal name to evaluate
1495    */
1496   public is(term: string): boolean {
1497     return (this.options.termName + '').indexOf(term) === 0;
1498   }
1499
1500   /**
1501    * Emit the data event and populate the given data.
1502    * @param data The data to populate in the event.
1503    */
1504   // public handler(data: string): void {
1505   //   // Prevents all events to pty process if stdin is disabled
1506   //   if (this.options.disableStdin) {
1507   //     return;
1508   //   }
1509
1510   //   // Clear the selection if the selection manager is available and has an active selection
1511   //   if (this.selectionService && this.selectionService.hasSelection) {
1512   //     this.selectionService.clearSelection();
1513   //   }
1514
1515   //   // Input is being sent to the terminal, the terminal should focus the prompt.
1516   //   if (this.buffer.ybase !== this.buffer.ydisp) {
1517   //     this.scrollToBottom();
1518   //   }
1519   //   this._onData.fire(data);
1520   // }
1521
1522   /**
1523    * Emit the 'title' event and populate the given title.
1524    * @param title The title to populate in the event.
1525    */
1526   public handleTitle(title: string): void {
1527     this._onTitleChange.fire(title);
1528   }
1529
1530   /**
1531    * Reset terminal.
1532    * Note: Calling this directly from JS is synchronous but does not clear
1533    * input buffers and does not reset the parser, thus the terminal will
1534    * continue to apply pending input data.
1535    * If you need in band reset (synchronous with input data) consider
1536    * using DECSTR (soft reset, CSI ! p) or RIS instead (hard reset, ESC c).
1537    */
1538   public reset(): void {
1539     /**
1540      * Since _setup handles a full terminal creation, we have to carry forward
1541      * a few things that should not reset.
1542      */
1543     this.options.rows = this.rows;
1544     this.options.cols = this.cols;
1545     const customKeyEventHandler = this._customKeyEventHandler;
1546     const inputHandler = this._inputHandler;
1547     const cursorState = this.cursorState;
1548     const userScrolling = this._userScrolling;
1549
1550     this._setup();
1551     this._bufferService.reset();
1552     this._coreService.reset();
1553     this._coreMouseService.reset();
1554     if (this._selectionService) {
1555       this._selectionService.reset();
1556     }
1557
1558     // reattach
1559     this._customKeyEventHandler = customKeyEventHandler;
1560     this._inputHandler = inputHandler;
1561     this.cursorState = cursorState;
1562     this._userScrolling = userScrolling;
1563
1564     // do a full screen refresh
1565     this.refresh(0, this.rows - 1);
1566     if (this.viewport) {
1567       this.viewport.syncScrollArea();
1568     }
1569   }
1570
1571   // TODO: Remove cancel function and cancelEvents option
1572   public cancel(ev: Event, force?: boolean): boolean {
1573     if (!this.options.cancelEvents && !force) {
1574       return;
1575     }
1576     ev.preventDefault();
1577     ev.stopPropagation();
1578     return false;
1579   }
1580
1581   private _visualBell(): boolean {
1582     return false;
1583     // return this.options.bellStyle === 'visual' ||
1584     //     this.options.bellStyle === 'both';
1585   }
1586
1587   private _soundBell(): boolean {
1588     return this.options.bellStyle === 'sound';
1589     // return this.options.bellStyle === 'sound' ||
1590     //     this.options.bellStyle === 'both';
1591   }
1592
1593   public write(data: string | Uint8Array, callback?: () => void): void {
1594     this._writeBuffer.write(data, callback);
1595   }
1596
1597   public writeSync(data: string | Uint8Array): void {
1598     this._writeBuffer.writeSync(data);
1599   }
1600 }
1601
1602 /**
1603  * Helpers
1604  */
1605
1606 function wasModifierKeyOnlyEvent(ev: KeyboardEvent): boolean {
1607   return ev.keyCode === 16 || // Shift
1608     ev.keyCode === 17 || // Ctrl
1609     ev.keyCode === 18; // Alt
1610 }