From 343d4c3348111349e35950d8d20f5058841a3bd9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 1 Jun 2019 08:37:37 -0700 Subject: [PATCH 1/4] Remove path mapping to self modules This would cause imports in the common module to reference out/ instead of src/, this leads to weird behavior when errors are introduced. --- src/common/tsconfig.json | 5 +---- src/core/tsconfig.json | 3 +-- src/ui/tsconfig.json | 3 +-- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json index 82113a9e..dca04f9a 100644 --- a/src/common/tsconfig.json +++ b/src/common/tsconfig.json @@ -5,10 +5,7 @@ "types": [ "../../node_modules/@types/mocha" ], - "baseUrl": "..", - "paths": { - "common/*": [ "./common/*" ] - } + "baseUrl": ".." }, "include": [ "./**/*" ] } diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index f0ee3fbb..5eeac7bc 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -7,8 +7,7 @@ ], "baseUrl": "..", "paths": { - "common/*": [ "./common/*" ], - "core/*": [ "./core/*" ] + "common/*": [ "./common/*" ] } }, "include": [ "./**/*" ], diff --git a/src/ui/tsconfig.json b/src/ui/tsconfig.json index 4e7a2a94..06818413 100644 --- a/src/ui/tsconfig.json +++ b/src/ui/tsconfig.json @@ -11,8 +11,7 @@ ], "baseUrl": "..", "paths": { - "common/*": [ "./common/*" ], - "ui/*": [ "./ui/*" ] + "common/*": [ "./common/*" ] } }, "include": [ "./**/*" ], From 3ba706978774aef77415d6866fcf2c56e142c0b6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 1 Jun 2019 09:29:11 -0700 Subject: [PATCH 2/4] Introduce options service --- src/Buffer.test.ts | 2 +- src/InputHandler.ts | 8 +- src/SoundManager.ts | 6 - src/Terminal.test.ts | 56 +++--- src/Terminal.ts | 274 +++++++++------------------ src/TestUtils.test.ts | 5 +- src/Types.ts | 8 +- src/common/Clone.test.ts | 8 - src/common/Clone.ts | 9 +- src/common/options/OptionsService.ts | 128 +++++++++++++ src/common/options/Types.ts | 105 ++++++++++ src/public/Terminal.ts | 4 +- 12 files changed, 361 insertions(+), 252 deletions(-) create mode 100644 src/common/options/OptionsService.ts create mode 100644 src/common/options/Types.ts diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 205e2a43..e1572889 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -1366,7 +1366,7 @@ describe('Buffer', () => { const input = '\thttps://google.de'; terminal.writeSync(input); const s = terminal.buffer.iterator(true).next().content; - assert.equal(s, Array(terminal.getOption('tabStopWidth') + 1).join(' ') + 'https://google.de'); + assert.equal(s, Array(terminal.options.tabStopWidth + 1).join(' ') + 'https://google.de'); }); }); describe('BufferStringIterator', function(): void { diff --git a/src/InputHandler.ts b/src/InputHandler.ts index e8f47670..7295b3da 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1841,19 +1841,19 @@ export class InputHandler extends Disposable implements IInputHandler { switch (param) { case 1: case 2: - this._terminal.setOption('cursorStyle', 'block'); + this._terminal.options.cursorStyle = 'block'; break; case 3: case 4: - this._terminal.setOption('cursorStyle', 'underline'); + this._terminal.options.cursorStyle = 'underline'; break; case 5: case 6: - this._terminal.setOption('cursorStyle', 'bar'); + this._terminal.options.cursorStyle = 'bar'; break; } const isBlinking = param % 2 === 1; - this._terminal.setOption('cursorBlink', isBlinking); + this._terminal.options.cursorBlink = isBlinking; } } diff --git a/src/SoundManager.ts b/src/SoundManager.ts index 6084edcb..6bff444f 100644 --- a/src/SoundManager.ts +++ b/src/SoundManager.ts @@ -5,12 +5,6 @@ import { ITerminal, ISoundManager } from './Types'; -// Source: https://freesound.org/people/altemark/sounds/45759/ -// This sound is released under the Creative Commons Attribution 3.0 Unported -// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been -// made, apart from the conversion to base64. -export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg=='; - export class SoundManager implements ISoundManager { private static _audioContext: AudioContext; diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 56e84da7..6ef2da51 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -43,7 +43,7 @@ describe('Terminal', () => { }); it('should not mutate the options parameter', () => { - term.setOption('cols', 1000); + term.options.cols = 1000; assert.deepEqual(termOptions, { cols: INIT_COLS, @@ -51,21 +51,21 @@ describe('Terminal', () => { }); }); - describe('getOption', () => { - it('should retrieve the option correctly', () => { - // In the `options` namespace. - term.options.cursorBlink = true; - assert.equal(term.getOption('cursorBlink'), true); + // describe('getOption', () => { + // it('should retrieve the option correctly', () => { + // // In the `options` namespace. + // term.options.cursorBlink = true; + // assert.equal(term.getOption('cursorBlink'), true); - // On the Terminal instance - delete term.options.cursorBlink; - term.options.cursorBlink = false; - assert.equal(term.getOption('cursorBlink'), false); - }); - it('should throw when retrieving a non-existant option', () => { - assert.throws(term.getOption.bind(term, 'fake', true)); - }); - }); + // // On the Terminal instance + // delete term.options.cursorBlink; + // term.options.cursorBlink = false; + // assert.equal(term.getOption('cursorBlink'), false); + // }); + // it('should throw when retrieving a non-existant option', () => { + // assert.throws(term.getOption.bind(term, 'fake', true)); + // }); + // }); describe('events', () => { it('should fire the onData evnet', (done) => { @@ -335,17 +335,17 @@ describe('Terminal', () => { }); }); - describe('setOption', () => { - it('should set option correctly', () => { - term.setOption('cursorBlink', true); - assert.equal(term.options.cursorBlink, true); - term.setOption('cursorBlink', false); - assert.equal(term.options.cursorBlink, false); - }); - it('should throw when setting a non-existant option', () => { - assert.throws(term.setOption.bind(term, 'fake', true)); - }); - }); + // describe('setOption', () => { + // it('should set option correctly', () => { + // term.setOption('cursorBlink', true); + // assert.equal(term.options.cursorBlink, true); + // term.setOption('cursorBlink', false); + // assert.equal(term.options.cursorBlink, false); + // }); + // it('should throw when setting a non-existant option', () => { + // assert.throws(term.setOption.bind(term, 'fake', true)); + // }); + // }); describe('reset', () => { it('should not affect cursorState', () => { @@ -625,7 +625,7 @@ describe('Terminal', () => { describe('when scrollback === 0', () => { beforeEach(() => { - term.setOption('scrollback', 0); + term.optionsService.setOption('scrollback', 0); assert.equal(term.buffer.lines.maxLength, INIT_ROWS); }); @@ -730,7 +730,7 @@ describe('Terminal', () => { describe('with macOptionIsMeta', () => { beforeEach(() => { term.browser.isMac = true; - term.setOption('macOptionIsMeta', true); + term.options.macOptionIsMeta = true; }); it('should interfere with the alt key on keyDown', () => { diff --git a/src/Terminal.ts b/src/Terminal.ts index eac6aca4..6349e94f 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -24,7 +24,7 @@ import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharacterJoinerHandler, IMouseZoneManager } from './Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; -import { Buffer, MAX_BUFFER_SIZE } from './Buffer'; +import { Buffer } from './Buffer'; import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from 'common/EventEmitter'; import { Viewport } from './Viewport'; @@ -39,7 +39,7 @@ import * as Browser from 'common/Platform'; import { addDisposableDomListener } from 'ui/Lifecycle'; import * as Strings from './Strings'; import { MouseHelper } from './MouseHelper'; -import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; +import { SoundManager } from './SoundManager'; import { MouseZoneManager } from './MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; @@ -48,12 +48,13 @@ import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent } from 'common/Types'; import { evaluateKeyboardEvent } from 'core/input/Keyboard'; import { KeyboardResultType, ICharset, IBufferLine, IAttributeData } from 'core/Types'; -import { clone } from 'common/Clone'; import { EventEmitter2, IEvent } from 'common/EventEmitter2'; import { Attributes, DEFAULT_ATTR_DATA } from 'core/buffer/BufferLine'; import { applyWindowsMode } from './WindowsMode'; import { ColorManager } from 'ui/ColorManager'; import { RenderCoordinator } from './renderer/RenderCoordinator'; +import { IOptionsService } from '../out/common/options/Types'; +import { OptionsService } from 'common/options/OptionsService'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -77,44 +78,6 @@ const WRITE_BUFFER_LENGTH_THRESHOLD = 50; const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars const MINIMUM_ROWS = 1; -/** - * The set of options that only have an effect when set in the Terminal constructor. - */ -const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows']; - -const DEFAULT_OPTIONS: ITerminalOptions = { - cols: 80, - rows: 24, - convertEol: false, - termName: 'xterm', - cursorBlink: false, - cursorStyle: 'block', - bellSound: DEFAULT_BELL_SOUND, - bellStyle: 'none', - drawBoldTextInBrightColors: true, - fontFamily: 'courier-new, courier, monospace', - fontSize: 15, - fontWeight: 'normal', - fontWeightBold: 'bold', - lineHeight: 1.0, - letterSpacing: 0, - scrollback: 1000, - screenKeys: false, - screenReaderMode: false, - debug: false, - macOptionIsMeta: false, - macOptionClickForcesSelection: false, - cancelEvents: false, - disableStdin: false, - useFlowControl: false, - allowTransparency: false, - tabStopWidth: 8, - theme: undefined, - rightClickSelectsWord: Browser.isMac, - rendererType: 'canvas', - windowsMode: false -}; - export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { public textarea: HTMLTextAreaElement; public element: HTMLElement; @@ -135,7 +98,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public browser: IBrowser = Browser; - public options: ITerminalOptions; + // 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; @@ -143,6 +107,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _customKeyEventHandler: CustomKeyEventHandler; + // services + public optionsService: IOptionsService; + // modes public applicationKeypad: boolean; public applicationCursor: boolean; @@ -257,7 +224,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II options: ITerminalOptions = {} ) { super(); - this.options = clone(options); + this.optionsService = new OptionsService(options); + this._setupOptionsListeners(); + + // this.options = clone(options); this._setup(); // TODO: Remove these in v4 @@ -289,11 +259,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } private _setup(): void { - Object.keys(DEFAULT_OPTIONS).forEach((key) => { - if (this.options[key] === null || this.options[key] === undefined) { - this.options[key] = DEFAULT_OPTIONS[key]; - } - }); + // Object.keys(DEFAULT_OPTIONS).forEach((key) => { + // if (this.options[key] === null || this.options[key] === undefined) { + // this.options[key] = DEFAULT_OPTIONS[key]; + // } + // }); // this.context = options.context || window; // this.document = options.document || document; @@ -303,10 +273,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.cols = Math.max(this.options.cols, MINIMUM_COLS); this.rows = Math.max(this.options.rows, MINIMUM_ROWS); - if (this.options.handler) { - this.onData(this.options.handler); - } - this.cursorState = 0; this.cursorHidden = false; this._customKeyEventHandler = null; @@ -394,82 +360,59 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II return document.activeElement === this.textarea && document.hasFocus(); } - /** - * Retrieves an option's value from the terminal. - * @param key The option key. - */ - public getOption(key: string): any { - if (!(key in DEFAULT_OPTIONS)) { - throw new Error('No option with key "' + key + '"'); - } - - return this.options[key]; - } - - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - */ - public setOption(key: string, value: any): void { - if (!(key in DEFAULT_OPTIONS)) { - throw new Error('No option with key "' + key + '"'); - } - if (CONSTRUCTOR_ONLY_OPTIONS.indexOf(key) !== -1) { - console.error(`Option "${key}" can only be set in the constructor`); - } - if (this.options[key] === value) { - return; - } - switch (key) { - case 'bellStyle': - if (!value) { - value = 'none'; - } - break; - case 'cursorStyle': - if (!value) { - value = 'block'; - } - break; - case 'fontWeight': - if (!value) { - value = 'normal'; - } - break; - case 'fontWeightBold': - if (!value) { - value = 'bold'; - } - break; - case 'lineHeight': - if (value < 1) { - console.warn(`${key} cannot be less than 1, value: ${value}`); - return; - } - case 'rendererType': - if (!value) { - value = 'canvas'; - } - break; - case 'tabStopWidth': - if (value < 1) { - console.warn(`${key} cannot be less than 1, value: ${value}`); - return; - } - break; - case 'theme': - this._setTheme(value); - break; - case 'scrollback': - value = Math.min(value, MAX_BUFFER_SIZE); - - if (value < 0) { - console.warn(`${key} cannot be less than 0, value: ${value}`); - return; - } - if (this.options[key] !== value) { - const newBufferLength = this.rows + value; + 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._renderCoordinator) { + this._renderCoordinator.clear(); + this.charMeasure.measure(this.options); + } + 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._renderCoordinator) { + this._renderCoordinator.clear(); + this._renderCoordinator.onResize(this.cols, this.rows); + this.refresh(0, this.rows - 1); + } + break; + case 'rendererType': + if (this._renderCoordinator) { + this._renderCoordinator.setRenderer(this._createRenderer()); + } + 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._renderCoordinator) { + this._accessibilityManager = new AccessibilityManager(this, this._renderCoordinator.dimensions); + } + } 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 'scrollback': + const newBufferLength = this.rows + this.optionsService.options.scrollback; if (this.buffer.lines.length > newBufferLength) { const amountToTrim = this.buffer.lines.length - newBufferLength; const needsRefresh = (this.buffer.ydisp - amountToTrim < 0); @@ -480,68 +423,21 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.refresh(0, this.rows - 1); } } - } - break; - } - this.options[key] = value; - switch (key) { - case 'fontFamily': - case 'fontSize': - // When the font changes the size of the cells may change which requires a renderer clear - if (this._renderCoordinator) { - this._renderCoordinator.clear(); - this.charMeasure.measure(this.options); - } - 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._renderCoordinator) { - this._renderCoordinator.clear(); - this._renderCoordinator.onResize(this.cols, this.rows); - this.refresh(0, this.rows - 1); - } - break; - case 'rendererType': - if (this._renderCoordinator) { - this._renderCoordinator.setRenderer(this._createRenderer()); - } - break; - case 'scrollback': - this.buffers.resize(this.cols, this.rows); - if (this.viewport) { - this.viewport.syncScrollArea(); - } - break; - case 'screenReaderMode': - if (value) { - if (!this._accessibilityManager && this._renderCoordinator) { - this._accessibilityManager = new AccessibilityManager(this, this._renderCoordinator.dimensions); + case 'windowsMode': + if (this.optionsService.options.windowsMode) { + if (!this._windowsMode) { + this._windowsMode = applyWindowsMode(this); + } + } else { + if (this._windowsMode) { + this._windowsMode.dispose(); + this._windowsMode = undefined; + } } - } else { - if (this._accessibilityManager) { - this._accessibilityManager.dispose(); - this._accessibilityManager = null; - } - } - break; - case 'tabStopWidth': this.buffers.setupTabStops(); break; - case 'windowsMode': - if (value) { - if (!this._windowsMode) { - this._windowsMode = applyWindowsMode(this); - } - } else { - if (this._windowsMode) { - this._windowsMode.dispose(); - this._windowsMode = undefined; - } - } - break; - } + break; + } + }); + // TODO: Move into rendercoordinator // Inform renderer of changes if (this._renderCoordinator) { this._renderCoordinator.onOptionsChanged(); diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index ea7980e6..ddc0e825 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -13,6 +13,7 @@ import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm'; import { Terminal } from './Terminal'; import { AttributeData } from 'core/buffer/BufferLine'; import { IColorManager, IColorSet } from 'ui/Types'; +import { IOptionsService } from '../out/common/options/Types'; export class TestTerminal extends Terminal { writeSync(data: string): void { @@ -32,6 +33,7 @@ export class MockTerminal implements ITerminal { onRender: IEvent<{ start: number; end: number; }>; onResize: IEvent<{ cols: number; rows: number; }>; markers: IMarker[]; + optionsService: IOptionsService; addMarker(cursorYOffset: number): IMarker { throw new Error('Method not implemented.'); } @@ -42,9 +44,6 @@ export class MockTerminal implements ITerminal { throw new Error('Method not implemented.'); } static string: any; - getOption(key: any): any { - throw new Error('Method not implemented.'); - } setOption(key: any, value: any): void { throw new Error('Method not implemented.'); } diff --git a/src/Types.ts b/src/Types.ts index 7e01b99f..64b35de8 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -8,6 +8,7 @@ import { ICharset, IAttributeData, ICellData, IBufferLine, CharData } from 'core import { ICircularList } from 'common/Types'; import { IEvent } from 'common/EventEmitter2'; import { IColorSet } from 'ui/Types'; +import { IOptionsService } from '../out/common/options/Types'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -72,7 +73,6 @@ export interface IInputHandlingTerminal extends IEventEmitter { showCursor(): void; refresh(start: number, end: number): void; error(text: string, data?: any): void; - setOption(key: string, value: any): void; tabSet(): void; handler(data: string): void; handleTitle(title: string): void; @@ -204,7 +204,6 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc writeBuffer: string[]; cursorHidden: boolean; cursorState: number; - options: ITerminalOptions; buffer: IBuffer; buffers: IBufferSet; isFocused: boolean; @@ -212,6 +211,9 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc viewport: IViewport; bracketedPasteMode: boolean; applicationCursor: boolean; + optionsService: IOptionsService; + // TODO: We should remove options once components adopt optionsService + options: ITerminalOptions; handler(data: string): void; scrollLines(disp: number, suppressScrollEvent?: boolean): void; @@ -265,8 +267,6 @@ export interface IPublicTerminal extends IDisposable, IEventEmitter { clear(): void; write(data: string): void; writeUtf8(data: Uint8Array): void; - getOption(key: string): any; - setOption(key: string, value: any): void; refresh(start: number, end: number): void; reset(): void; } diff --git a/src/common/Clone.test.ts b/src/common/Clone.test.ts index fc95e196..370538ea 100644 --- a/src/common/Clone.test.ts +++ b/src/common/Clone.test.ts @@ -38,14 +38,6 @@ describe('clone', () => { }); }); - it('should clone null values', () => { - const test: any = { - a: null - }; - - assert.deepEqual(clone(test), { a: null }); - }); - it('should clone array values', () => { const test = { a: [1, 2, 3], diff --git a/src/common/Clone.ts b/src/common/Clone.ts index 78bacbb5..51c5abaa 100644 --- a/src/common/Clone.ts +++ b/src/common/Clone.ts @@ -6,22 +6,17 @@ /* * A simple utility for cloning values */ -export function clone(val: T, depth: number = 5): T | null { +export function clone(val: T, depth: number = 5): T { if (typeof val !== 'object') { return val; } - // cloning null always returns null - if (val === null) { - return null; - } - // If we're cloning an array, use an array as the base, otherwise use an object const clonedObject: any = Array.isArray(val) ? [] : {}; for (const key in val) { // Recursively clone eack item unless we're at the maximum depth - clonedObject[key] = depth <= 1 ? val[key] : clone(val[key], depth - 1); + clonedObject[key] = depth <= 1 ? val[key] : (val[key] ? clone(val[key], depth - 1) : val[key]); } return clonedObject as T; diff --git a/src/common/options/OptionsService.ts b/src/common/options/OptionsService.ts new file mode 100644 index 00000000..3523198c --- /dev/null +++ b/src/common/options/OptionsService.ts @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IOptionsService, ITerminalOptions, IPartialTerminalOptions } from 'common/options/Types'; +import { EventEmitter2, IEvent } from 'common/EventEmitter2'; +import { isMac } from 'common/Platform'; +import { clone } from 'common/Clone'; + +// Source: https://freesound.org/people/altemark/sounds/45759/ +// This sound is released under the Creative Commons Attribution 3.0 Unported +// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been +// made, apart from the conversion to base64. +export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg=='; + +// TODO: Freeze? +const DEFAULT_OPTIONS: ITerminalOptions = { + cols: 80, + rows: 24, + cursorBlink: false, + cursorStyle: 'block', + bellSound: DEFAULT_BELL_SOUND, + bellStyle: 'none', + drawBoldTextInBrightColors: true, + fontFamily: 'courier-new, courier, monospace', + fontSize: 15, + fontWeight: 'normal', + fontWeightBold: 'bold', + lineHeight: 1.0, + letterSpacing: 0, + scrollback: 1000, + screenReaderMode: false, + macOptionIsMeta: false, + macOptionClickForcesSelection: false, + disableStdin: false, + allowTransparency: false, + tabStopWidth: 8, + theme: {}, + rightClickSelectsWord: isMac, + rendererType: 'canvas', + windowsMode: false, + + convertEol: false, + termName: 'xterm', + screenKeys: false, + debug: false, + cancelEvents: false, + useFlowControl: false +}; + +/** + * The set of options that only have an effect when set in the Terminal constructor. + */ +const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows']; + +export class OptionsService implements IOptionsService { + public options: ITerminalOptions; + + private _onOptionChange = new EventEmitter2(); + public get onOptionChange(): IEvent { return this._onOptionChange.event; } + + constructor(options: IPartialTerminalOptions) { + this.options = clone(DEFAULT_OPTIONS); + Object.keys(options).forEach(k => { + if (k in this.options) { + const newValue = options[k as keyof IPartialTerminalOptions] as any; + this.options[k] = newValue; + } + }); + } + + public setOption(key: string, value: any): void { + if (!(key in DEFAULT_OPTIONS)) { + throw new Error('No option with key "' + key + '"'); + } + if (CONSTRUCTOR_ONLY_OPTIONS.indexOf(key) !== -1) { + throw new Error(`Option "${key}" can only be set in the constructor`); + } + if (this.options[key] === value) { + return; + } + + value = this._sanitizeAndValidateOption(key, value); + + // Don't fire an option change event if they didn't change + if (this.options[key] === value) { + return; + } + + this.options[key] = value; + this._onOptionChange.fire(key); + } + + private _sanitizeAndValidateOption(key: string, value: any): any { + switch (key) { + case 'bellStyle': + case 'cursorStyle': + case 'fontWeight': + case 'fontWeightBold': + case 'rendererType': + if (!value) { + value = DEFAULT_OPTIONS[key]; + } + break; + case 'lineHeight': + case 'tabStopWidth': + if (value < 1) { + throw new Error(`${key} cannot be less than 1, value: ${value}`); + } + break; + case 'scrollback': + value = Math.min(value, 4294967295); + if (value < 0) { + throw new Error(`${key} cannot be less than 0, value: ${value}`); + } + break; + } + return value; + } + + public getOption(key: string): any { + if (!(key in DEFAULT_OPTIONS)) { + throw new Error(`No option with key "${key}"`); + } + return this.options[key]; + } +} diff --git a/src/common/options/Types.ts b/src/common/options/Types.ts new file mode 100644 index 00000000..5c5c3323 --- /dev/null +++ b/src/common/options/Types.ts @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IEvent } from 'common/EventEmitter2'; + +export interface IOptionsService { + readonly onOptionChange: IEvent; + // TODO: as const? + readonly options: ITerminalOptions; + + setOption(key: string, value: T): void; + getOption(key: string): T | undefined; +} + +export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; + +export type RendererType = 'dom' | 'canvas'; + +export interface IPartialTerminalOptions { + allowTransparency?: boolean; + bellSound?: string; + bellStyle?: 'none' /*| 'visual'*/ | 'sound' /*| 'both'*/; + cols?: number; + cursorBlink?: boolean; + cursorStyle?: 'block' | 'underline' | 'bar'; + disableStdin?: boolean; + drawBoldTextInBrightColors?: boolean; + fontSize?: number; + fontFamily?: string; + fontWeight?: FontWeight; + fontWeightBold?: FontWeight; + letterSpacing?: number; + lineHeight?: number; + macOptionIsMeta?: boolean; + macOptionClickForcesSelection?: boolean; + rendererType?: RendererType; + rightClickSelectsWord?: boolean; + rows?: number; + screenReaderMode?: boolean; + scrollback?: number; + tabStopWidth?: number; + theme?: ITheme; + windowsMode?: boolean; +} + +export interface ITerminalOptions { + allowTransparency: boolean; + bellSound: string; + bellStyle: 'none' /*| 'visual'*/ | 'sound' /*| 'both'*/; + cols: number; + cursorBlink: boolean; + cursorStyle: 'block' | 'underline' | 'bar'; + disableStdin: boolean; + drawBoldTextInBrightColors: boolean; + fontSize: number; + fontFamily: string; + fontWeight: FontWeight; + fontWeightBold: FontWeight; + letterSpacing: number; + lineHeight: number; + macOptionIsMeta: boolean; + macOptionClickForcesSelection: boolean; + rendererType: RendererType; + rightClickSelectsWord: boolean; + rows: number; + screenReaderMode: boolean; + scrollback: number; + tabStopWidth: number; + theme: ITheme; + windowsMode: boolean; + + [key: string]: any; + cancelEvents: boolean; + convertEol: boolean; + debug: boolean; + screenKeys: boolean; + termName: string; + useFlowControl: boolean; +} + +export interface ITheme { + foreground?: string; + background?: string; + cursor?: string; + cursorAccent?: string; + selection?: string; + black?: string; + red?: string; + green?: string; + yellow?: string; + blue?: string; + magenta?: string; + cyan?: string; + white?: string; + brightBlack?: string; + brightRed?: string; + brightGreen?: string; + brightYellow?: string; + brightBlue?: string; + brightMagenta?: string; + brightCyan?: string; + brightWhite?: string; +} diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index b6f6ba67..e6bd5923 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -152,7 +152,7 @@ export class Terminal implements ITerminalApi { public getOption(key: 'handler'): (data: string) => void; public getOption(key: string): any; public getOption(key: any): any { - return this._core.getOption(key); + return this._core.optionsService.getOption(key); } public setOption(key: 'bellSound' | 'fontFamily' | 'termName', value: string): void; public setOption(key: 'fontWeight' | 'fontWeightBold', value: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'): void; @@ -166,7 +166,7 @@ export class Terminal implements ITerminalApi { public setOption(key: 'cols' | 'rows', value: number): void; public setOption(key: string, value: any): void; public setOption(key: any, value: any): void { - this._core.setOption(key, value); + this._core.optionsService.setOption(key, value); } public refresh(start: number, end: number): void { this._core.refresh(start, end); From 041bd7a5d1dd3f8d6b6d5105885da16d45bca7f2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 1 Jun 2019 09:34:13 -0700 Subject: [PATCH 3/4] Move RenderCoordinator options listener out of Terminal --- src/Terminal.ts | 8 +------- src/renderer/RenderCoordinator.ts | 10 +++++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 6349e94f..a13d47da 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -437,11 +437,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II break; } }); - // TODO: Move into rendercoordinator - // Inform renderer of changes - if (this._renderCoordinator) { - this._renderCoordinator.onOptionsChanged(); - } } /** @@ -613,7 +608,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.textarea = document.createElement('textarea'); this.textarea.classList.add('xterm-helper-textarea'); - // TODO: New API to set title? This could say "Terminal bash input", etc. this.textarea.setAttribute('aria-label', Strings.promptLabel); this.textarea.setAttribute('aria-multiline', 'false'); this.textarea.setAttribute('autocorrect', 'off'); @@ -640,7 +634,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._colorManager.setTheme(this._theme); const renderer = this._createRenderer(); - this._renderCoordinator = new RenderCoordinator(renderer, this.rows, this.screenElement); + this._renderCoordinator = new RenderCoordinator(renderer, this.rows, this.screenElement, this.optionsService); this._renderCoordinator.onRender(e => this._onRender.fire(e)); this.onResize(e => this._renderCoordinator.resize(e.cols, e.rows)); diff --git a/src/renderer/RenderCoordinator.ts b/src/renderer/RenderCoordinator.ts index b4bc673c..98c9ae86 100644 --- a/src/renderer/RenderCoordinator.ts +++ b/src/renderer/RenderCoordinator.ts @@ -11,6 +11,7 @@ import { ScreenDprMonitor } from 'ui/ScreenDprMonitor'; import { addDisposableDomListener } from 'ui/Lifecycle'; import { IColorSet } from 'ui/Types'; import { CharacterJoinerHandler } from '../Types'; +import { IOptionsService } from 'common/options/Types'; export class RenderCoordinator extends Disposable { private _renderDebouncer: RenderDebouncer; @@ -33,7 +34,8 @@ export class RenderCoordinator extends Disposable { constructor( private _renderer: IRenderer, private _rowCount: number, - screenElement: HTMLElement + screenElement: HTMLElement, + optionsService: IOptionsService ) { super(); this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end)); @@ -43,6 +45,8 @@ export class RenderCoordinator extends Disposable { this._screenDprMonitor.setListener(() => this._renderer.onDevicePixelRatioChange()); this.register(this._screenDprMonitor); + this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged())); + // dprchange should handle this case, we need this as well for browsers that don't support the // matchMedia query. this.register(addDisposableDomListener(window, 'resize', () => this._renderer.onDevicePixelRatioChange())); @@ -144,10 +148,6 @@ export class RenderCoordinator extends Disposable { this._renderer.onCursorMove(); } - public onOptionsChanged(): void { - this._renderer.onOptionsChanged(); - } - public clear(): void { this._renderer.clear(); } From 8a3c60d495da0a92c5761ed240dc6f29bdfd396f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 1 Jun 2019 09:38:09 -0700 Subject: [PATCH 4/4] Clean up --- src/Terminal.test.ts | 28 ---------------------------- src/Terminal.ts | 11 +---------- src/TestUtils.test.ts | 5 +++-- src/Types.ts | 2 +- 4 files changed, 5 insertions(+), 41 deletions(-) diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 6ef2da51..beb9e921 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -51,22 +51,6 @@ describe('Terminal', () => { }); }); - // describe('getOption', () => { - // it('should retrieve the option correctly', () => { - // // In the `options` namespace. - // term.options.cursorBlink = true; - // assert.equal(term.getOption('cursorBlink'), true); - - // // On the Terminal instance - // delete term.options.cursorBlink; - // term.options.cursorBlink = false; - // assert.equal(term.getOption('cursorBlink'), false); - // }); - // it('should throw when retrieving a non-existant option', () => { - // assert.throws(term.getOption.bind(term, 'fake', true)); - // }); - // }); - describe('events', () => { it('should fire the onData evnet', (done) => { term.onData(() => done()); @@ -335,18 +319,6 @@ describe('Terminal', () => { }); }); - // describe('setOption', () => { - // it('should set option correctly', () => { - // term.setOption('cursorBlink', true); - // assert.equal(term.options.cursorBlink, true); - // term.setOption('cursorBlink', false); - // assert.equal(term.options.cursorBlink, false); - // }); - // it('should throw when setting a non-existant option', () => { - // assert.throws(term.setOption.bind(term, 'fake', true)); - // }); - // }); - describe('reset', () => { it('should not affect cursorState', () => { term.cursorState = 1; diff --git a/src/Terminal.ts b/src/Terminal.ts index a13d47da..6feedcbb 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -53,7 +53,7 @@ import { Attributes, DEFAULT_ATTR_DATA } from 'core/buffer/BufferLine'; import { applyWindowsMode } from './WindowsMode'; import { ColorManager } from 'ui/ColorManager'; import { RenderCoordinator } from './renderer/RenderCoordinator'; -import { IOptionsService } from '../out/common/options/Types'; +import { IOptionsService } from 'common/options/Types'; import { OptionsService } from 'common/options/OptionsService'; // Let it work inside Node.js for automated testing purposes. @@ -259,15 +259,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } private _setup(): void { - // Object.keys(DEFAULT_OPTIONS).forEach((key) => { - // if (this.options[key] === null || this.options[key] === undefined) { - // this.options[key] = DEFAULT_OPTIONS[key]; - // } - // }); - - // this.context = options.context || window; - // this.document = options.document || document; - // TODO: WHy not document.body? this._parent = document ? document.body : null; this.cols = Math.max(this.options.cols, MINIMUM_COLS); diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index ddc0e825..e44489ea 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -13,7 +13,7 @@ import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm'; import { Terminal } from './Terminal'; import { AttributeData } from 'core/buffer/BufferLine'; import { IColorManager, IColorSet } from 'ui/Types'; -import { IOptionsService } from '../out/common/options/Types'; +import { IOptionsService } from 'common/options/Types'; export class TestTerminal extends Terminal { writeSync(data: string): void { @@ -30,7 +30,8 @@ export class MockTerminal implements ITerminal { onTitleChange: IEvent; onScroll: IEvent; onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>; - onRender: IEvent<{ start: number; end: number; }>; + onRender: IEvent<{ start: number + ; end: number; }>; onResize: IEvent<{ cols: number; rows: number; }>; markers: IMarker[]; optionsService: IOptionsService; diff --git a/src/Types.ts b/src/Types.ts index 64b35de8..c5b15bb6 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -8,7 +8,7 @@ import { ICharset, IAttributeData, ICellData, IBufferLine, CharData } from 'core import { ICircularList } from 'common/Types'; import { IEvent } from 'common/EventEmitter2'; import { IColorSet } from 'ui/Types'; -import { IOptionsService } from '../out/common/options/Types'; +import { IOptionsService } from 'common/options/Types'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;