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 509e3697..1a4ef187 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1842,19 +1842,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..beb9e921 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,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; @@ -625,7 +597,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 +702,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..6feedcbb 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 '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,24 +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]; - } - }); - - // 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); 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 +351,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,72 +414,20 @@ 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; - } - // Inform renderer of changes - if (this._renderCoordinator) { - this._renderCoordinator.onOptionsChanged(); - } + break; + } + }); } /** @@ -717,7 +599,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'); @@ -744,7 +625,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/TestUtils.test.ts b/src/TestUtils.test.ts index ea7980e6..e44489ea 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 'common/options/Types'; export class TestTerminal extends Terminal { writeSync(data: string): void { @@ -29,9 +30,11 @@ 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; addMarker(cursorYOffset: number): IMarker { throw new Error('Method not implemented.'); } @@ -42,9 +45,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 0ef623d3..7b9c68c5 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 '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); 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(); }