Merge pull request #2171 from Tyriar/options_service

Introduce options service, move options handling code into common
This commit is contained in:
Daniel Imms
2019-06-01 14:36:39 -07:00
committed by GitHub
13 changed files with 338 additions and 271 deletions
+1 -1
View File
@@ -1366,7 +1366,7 @@ describe('Buffer', () => {
const input = '\thttps://google.de'; const input = '\thttps://google.de';
terminal.writeSync(input); terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content; 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 { describe('BufferStringIterator', function(): void {
+4 -4
View File
@@ -1842,19 +1842,19 @@ export class InputHandler extends Disposable implements IInputHandler {
switch (param) { switch (param) {
case 1: case 1:
case 2: case 2:
this._terminal.setOption('cursorStyle', 'block'); this._terminal.options.cursorStyle = 'block';
break; break;
case 3: case 3:
case 4: case 4:
this._terminal.setOption('cursorStyle', 'underline'); this._terminal.options.cursorStyle = 'underline';
break; break;
case 5: case 5:
case 6: case 6:
this._terminal.setOption('cursorStyle', 'bar'); this._terminal.options.cursorStyle = 'bar';
break; break;
} }
const isBlinking = param % 2 === 1; const isBlinking = param % 2 === 1;
this._terminal.setOption('cursorBlink', isBlinking); this._terminal.options.cursorBlink = isBlinking;
} }
} }
-6
View File
@@ -5,12 +5,6 @@
import { ITerminal, ISoundManager } from './Types'; 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 { export class SoundManager implements ISoundManager {
private static _audioContext: AudioContext; private static _audioContext: AudioContext;
+3 -31
View File
@@ -43,7 +43,7 @@ describe('Terminal', () => {
}); });
it('should not mutate the options parameter', () => { it('should not mutate the options parameter', () => {
term.setOption('cols', 1000); term.options.cols = 1000;
assert.deepEqual(termOptions, { assert.deepEqual(termOptions, {
cols: INIT_COLS, 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', () => { describe('events', () => {
it('should fire the onData evnet', (done) => { it('should fire the onData evnet', (done) => {
term.onData(() => 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', () => { describe('reset', () => {
it('should not affect cursorState', () => { it('should not affect cursorState', () => {
term.cursorState = 1; term.cursorState = 1;
@@ -625,7 +597,7 @@ describe('Terminal', () => {
describe('when scrollback === 0', () => { describe('when scrollback === 0', () => {
beforeEach(() => { beforeEach(() => {
term.setOption('scrollback', 0); term.optionsService.setOption('scrollback', 0);
assert.equal(term.buffer.lines.maxLength, INIT_ROWS); assert.equal(term.buffer.lines.maxLength, INIT_ROWS);
}); });
@@ -730,7 +702,7 @@ describe('Terminal', () => {
describe('with macOptionIsMeta', () => { describe('with macOptionIsMeta', () => {
beforeEach(() => { beforeEach(() => {
term.browser.isMac = true; term.browser.isMac = true;
term.setOption('macOptionIsMeta', true); term.options.macOptionIsMeta = true;
}); });
it('should interfere with the alt key on keyDown', () => { it('should interfere with the alt key on keyDown', () => {
+80 -199
View File
@@ -24,7 +24,7 @@
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharacterJoinerHandler, IMouseZoneManager } from './Types'; import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharacterJoinerHandler, IMouseZoneManager } from './Types';
import { IRenderer } from './renderer/Types'; import { IRenderer } from './renderer/Types';
import { BufferSet } from './BufferSet'; import { BufferSet } from './BufferSet';
import { Buffer, MAX_BUFFER_SIZE } from './Buffer'; import { Buffer } from './Buffer';
import { CompositionHelper } from './CompositionHelper'; import { CompositionHelper } from './CompositionHelper';
import { EventEmitter } from 'common/EventEmitter'; import { EventEmitter } from 'common/EventEmitter';
import { Viewport } from './Viewport'; import { Viewport } from './Viewport';
@@ -39,7 +39,7 @@ import * as Browser from 'common/Platform';
import { addDisposableDomListener } from 'ui/Lifecycle'; import { addDisposableDomListener } from 'ui/Lifecycle';
import * as Strings from './Strings'; import * as Strings from './Strings';
import { MouseHelper } from './MouseHelper'; import { MouseHelper } from './MouseHelper';
import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; import { SoundManager } from './SoundManager';
import { MouseZoneManager } from './MouseZoneManager'; import { MouseZoneManager } from './MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager'; import { AccessibilityManager } from './AccessibilityManager';
import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm';
@@ -48,12 +48,13 @@ import { DomRenderer } from './renderer/dom/DomRenderer';
import { IKeyboardEvent } from 'common/Types'; import { IKeyboardEvent } from 'common/Types';
import { evaluateKeyboardEvent } from 'core/input/Keyboard'; import { evaluateKeyboardEvent } from 'core/input/Keyboard';
import { KeyboardResultType, ICharset, IBufferLine, IAttributeData } from 'core/Types'; import { KeyboardResultType, ICharset, IBufferLine, IAttributeData } from 'core/Types';
import { clone } from 'common/Clone';
import { EventEmitter2, IEvent } from 'common/EventEmitter2'; import { EventEmitter2, IEvent } from 'common/EventEmitter2';
import { Attributes, DEFAULT_ATTR_DATA } from 'core/buffer/BufferLine'; import { Attributes, DEFAULT_ATTR_DATA } from 'core/buffer/BufferLine';
import { applyWindowsMode } from './WindowsMode'; import { applyWindowsMode } from './WindowsMode';
import { ColorManager } from 'ui/ColorManager'; import { ColorManager } from 'ui/ColorManager';
import { RenderCoordinator } from './renderer/RenderCoordinator'; 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. // Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null; 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_COLS = 2; // Less than 2 can mess with wide chars
const MINIMUM_ROWS = 1; 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 { export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal {
public textarea: HTMLTextAreaElement; public textarea: HTMLTextAreaElement;
public element: HTMLElement; public element: HTMLElement;
@@ -135,7 +98,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public browser: IBrowser = <any>Browser; public browser: IBrowser = <any>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 // TODO: This can be changed to an enum or boolean, 0 and 1 seem to be the only options
public cursorState: number; public cursorState: number;
@@ -143,6 +107,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
private _customKeyEventHandler: CustomKeyEventHandler; private _customKeyEventHandler: CustomKeyEventHandler;
// services
public optionsService: IOptionsService;
// modes // modes
public applicationKeypad: boolean; public applicationKeypad: boolean;
public applicationCursor: boolean; public applicationCursor: boolean;
@@ -257,7 +224,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
options: ITerminalOptions = {} options: ITerminalOptions = {}
) { ) {
super(); super();
this.options = clone(options); this.optionsService = new OptionsService(options);
this._setupOptionsListeners();
// this.options = clone(options);
this._setup(); this._setup();
// TODO: Remove these in v4 // TODO: Remove these in v4
@@ -289,24 +259,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
} }
private _setup(): void { 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._parent = document ? document.body : null;
this.cols = Math.max(this.options.cols, MINIMUM_COLS); this.cols = Math.max(this.options.cols, MINIMUM_COLS);
this.rows = Math.max(this.options.rows, MINIMUM_ROWS); this.rows = Math.max(this.options.rows, MINIMUM_ROWS);
if (this.options.handler) {
this.onData(this.options.handler);
}
this.cursorState = 0; this.cursorState = 0;
this.cursorHidden = false; this.cursorHidden = false;
this._customKeyEventHandler = null; this._customKeyEventHandler = null;
@@ -394,82 +351,59 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
return document.activeElement === this.textarea && document.hasFocus(); return document.activeElement === this.textarea && document.hasFocus();
} }
/** private _setupOptionsListeners(): void {
* Retrieves an option's value from the terminal. // TODO: These listeners should be owned by individual components
* @param key The option key. this.optionsService.onOptionChange(key => {
*/ switch (key) {
public getOption(key: string): any { case 'fontFamily':
if (!(key in DEFAULT_OPTIONS)) { case 'fontSize':
throw new Error('No option with key "' + key + '"'); // When the font changes the size of the cells may change which requires a renderer clear
} if (this._renderCoordinator) {
this._renderCoordinator.clear();
return this.options[key]; this.charMeasure.measure(this.options);
} }
break;
/** case 'drawBoldTextInBrightColors':
* Sets an option on the terminal. case 'letterSpacing':
* @param key The option key. case 'lineHeight':
* @param value The option value. case 'fontWeight':
*/ case 'fontWeightBold':
public setOption(key: string, value: any): void { // When the font changes the size of the cells may change which requires a renderer clear
if (!(key in DEFAULT_OPTIONS)) { if (this._renderCoordinator) {
throw new Error('No option with key "' + key + '"'); this._renderCoordinator.clear();
} this._renderCoordinator.onResize(this.cols, this.rows);
if (CONSTRUCTOR_ONLY_OPTIONS.indexOf(key) !== -1) { this.refresh(0, this.rows - 1);
console.error(`Option "${key}" can only be set in the constructor`); }
} break;
if (this.options[key] === value) { case 'rendererType':
return; if (this._renderCoordinator) {
} this._renderCoordinator.setRenderer(this._createRenderer());
switch (key) { }
case 'bellStyle': break;
if (!value) { case 'scrollback':
value = 'none'; this.buffers.resize(this.cols, this.rows);
} if (this.viewport) {
break; this.viewport.syncScrollArea();
case 'cursorStyle': }
if (!value) { break;
value = 'block'; case 'screenReaderMode':
} if (this.optionsService.options.screenReaderMode) {
break; if (!this._accessibilityManager && this._renderCoordinator) {
case 'fontWeight': this._accessibilityManager = new AccessibilityManager(this, this._renderCoordinator.dimensions);
if (!value) { }
value = 'normal'; } else {
} if (this._accessibilityManager) {
break; this._accessibilityManager.dispose();
case 'fontWeightBold': this._accessibilityManager = null;
if (!value) { }
value = 'bold'; }
} break;
break; case 'tabStopWidth': this.buffers.setupTabStops(); break;
case 'lineHeight': case 'theme':
if (value < 1) { this._setTheme(this.optionsService.options.theme);
console.warn(`${key} cannot be less than 1, value: ${value}`); break;
return; case 'scrollback':
} const newBufferLength = this.rows + this.optionsService.options.scrollback;
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(<ITheme>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;
if (this.buffer.lines.length > newBufferLength) { if (this.buffer.lines.length > newBufferLength) {
const amountToTrim = this.buffer.lines.length - newBufferLength; const amountToTrim = this.buffer.lines.length - newBufferLength;
const needsRefresh = (this.buffer.ydisp - amountToTrim < 0); 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); this.refresh(0, this.rows - 1);
} }
} }
} case 'windowsMode':
break; if (this.optionsService.options.windowsMode) {
} if (!this._windowsMode) {
this.options[key] = value; this._windowsMode = applyWindowsMode(this);
switch (key) { }
case 'fontFamily': } else {
case 'fontSize': if (this._windowsMode) {
// When the font changes the size of the cells may change which requires a renderer clear this._windowsMode.dispose();
if (this._renderCoordinator) { this._windowsMode = undefined;
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);
} }
} else { break;
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();
}
} }
/** /**
@@ -717,7 +599,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.textarea = document.createElement('textarea'); this.textarea = document.createElement('textarea');
this.textarea.classList.add('xterm-helper-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-label', Strings.promptLabel);
this.textarea.setAttribute('aria-multiline', 'false'); this.textarea.setAttribute('aria-multiline', 'false');
this.textarea.setAttribute('autocorrect', 'off'); this.textarea.setAttribute('autocorrect', 'off');
@@ -744,7 +625,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._colorManager.setTheme(this._theme); this._colorManager.setTheme(this._theme);
const renderer = this._createRenderer(); 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._renderCoordinator.onRender(e => this._onRender.fire(e));
this.onResize(e => this._renderCoordinator.resize(e.cols, e.rows)); this.onResize(e => this._renderCoordinator.resize(e.cols, e.rows));
+4 -4
View File
@@ -13,6 +13,7 @@ import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm';
import { Terminal } from './Terminal'; import { Terminal } from './Terminal';
import { AttributeData } from 'core/buffer/BufferLine'; import { AttributeData } from 'core/buffer/BufferLine';
import { IColorManager, IColorSet } from 'ui/Types'; import { IColorManager, IColorSet } from 'ui/Types';
import { IOptionsService } from 'common/options/Types';
export class TestTerminal extends Terminal { export class TestTerminal extends Terminal {
writeSync(data: string): void { writeSync(data: string): void {
@@ -29,9 +30,11 @@ export class MockTerminal implements ITerminal {
onTitleChange: IEvent<string>; onTitleChange: IEvent<string>;
onScroll: IEvent<number>; onScroll: IEvent<number>;
onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>; onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>;
onRender: IEvent<{ start: number; end: number; }>; onRender: IEvent<{ start: number
; end: number; }>;
onResize: IEvent<{ cols: number; rows: number; }>; onResize: IEvent<{ cols: number; rows: number; }>;
markers: IMarker[]; markers: IMarker[];
optionsService: IOptionsService;
addMarker(cursorYOffset: number): IMarker { addMarker(cursorYOffset: number): IMarker {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
@@ -42,9 +45,6 @@ export class MockTerminal implements ITerminal {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
static string: any; static string: any;
getOption(key: any): any {
throw new Error('Method not implemented.');
}
setOption(key: any, value: any): void { setOption(key: any, value: any): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
+4 -4
View File
@@ -8,6 +8,7 @@ import { ICharset, IAttributeData, ICellData, IBufferLine, CharData } from 'core
import { ICircularList } from 'common/Types'; import { ICircularList } from 'common/Types';
import { IEvent } from 'common/EventEmitter2'; import { IEvent } from 'common/EventEmitter2';
import { IColorSet } from 'ui/Types'; import { IColorSet } from 'ui/Types';
import { IOptionsService } from 'common/options/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
@@ -72,7 +73,6 @@ export interface IInputHandlingTerminal extends IEventEmitter {
showCursor(): void; showCursor(): void;
refresh(start: number, end: number): void; refresh(start: number, end: number): void;
error(text: string, data?: any): void; error(text: string, data?: any): void;
setOption(key: string, value: any): void;
tabSet(): void; tabSet(): void;
handler(data: string): void; handler(data: string): void;
handleTitle(title: string): void; handleTitle(title: string): void;
@@ -204,7 +204,6 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc
writeBuffer: string[]; writeBuffer: string[];
cursorHidden: boolean; cursorHidden: boolean;
cursorState: number; cursorState: number;
options: ITerminalOptions;
buffer: IBuffer; buffer: IBuffer;
buffers: IBufferSet; buffers: IBufferSet;
isFocused: boolean; isFocused: boolean;
@@ -212,6 +211,9 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc
viewport: IViewport; viewport: IViewport;
bracketedPasteMode: boolean; bracketedPasteMode: boolean;
applicationCursor: boolean; applicationCursor: boolean;
optionsService: IOptionsService;
// TODO: We should remove options once components adopt optionsService
options: ITerminalOptions;
handler(data: string): void; handler(data: string): void;
scrollLines(disp: number, suppressScrollEvent?: boolean): void; scrollLines(disp: number, suppressScrollEvent?: boolean): void;
@@ -265,8 +267,6 @@ export interface IPublicTerminal extends IDisposable, IEventEmitter {
clear(): void; clear(): void;
write(data: string): void; write(data: string): void;
writeUtf8(data: Uint8Array): void; writeUtf8(data: Uint8Array): void;
getOption(key: string): any;
setOption(key: string, value: any): void;
refresh(start: number, end: number): void; refresh(start: number, end: number): void;
reset(): void; reset(): void;
} }
-8
View File
@@ -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', () => { it('should clone array values', () => {
const test = { const test = {
a: [1, 2, 3], a: [1, 2, 3],
+2 -7
View File
@@ -6,22 +6,17 @@
/* /*
* A simple utility for cloning values * A simple utility for cloning values
*/ */
export function clone<T>(val: T, depth: number = 5): T | null { export function clone<T>(val: T, depth: number = 5): T {
if (typeof val !== 'object') { if (typeof val !== 'object') {
return val; 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 // If we're cloning an array, use an array as the base, otherwise use an object
const clonedObject: any = Array.isArray(val) ? [] : {}; const clonedObject: any = Array.isArray(val) ? [] : {};
for (const key in val) { for (const key in val) {
// Recursively clone eack item unless we're at the maximum depth // 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; return clonedObject as T;
+128
View File
@@ -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<string>();
public get onOptionChange(): IEvent<string> { 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];
}
}
+105
View File
@@ -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<string>;
// TODO: as const?
readonly options: ITerminalOptions;
setOption<T>(key: string, value: T): void;
getOption<T>(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;
}
+2 -2
View File
@@ -152,7 +152,7 @@ export class Terminal implements ITerminalApi {
public getOption(key: 'handler'): (data: string) => void; public getOption(key: 'handler'): (data: string) => void;
public getOption(key: string): any; public getOption(key: string): any;
public getOption(key: any): 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: 'bellSound' | 'fontFamily' | 'termName', value: string): void;
public setOption(key: 'fontWeight' | 'fontWeightBold', value: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'): 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: 'cols' | 'rows', value: number): void;
public setOption(key: string, value: any): void; public setOption(key: string, value: any): void;
public setOption(key: any, 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 { public refresh(start: number, end: number): void {
this._core.refresh(start, end); this._core.refresh(start, end);
+5 -5
View File
@@ -11,6 +11,7 @@ import { ScreenDprMonitor } from 'ui/ScreenDprMonitor';
import { addDisposableDomListener } from 'ui/Lifecycle'; import { addDisposableDomListener } from 'ui/Lifecycle';
import { IColorSet } from 'ui/Types'; import { IColorSet } from 'ui/Types';
import { CharacterJoinerHandler } from '../Types'; import { CharacterJoinerHandler } from '../Types';
import { IOptionsService } from 'common/options/Types';
export class RenderCoordinator extends Disposable { export class RenderCoordinator extends Disposable {
private _renderDebouncer: RenderDebouncer; private _renderDebouncer: RenderDebouncer;
@@ -33,7 +34,8 @@ export class RenderCoordinator extends Disposable {
constructor( constructor(
private _renderer: IRenderer, private _renderer: IRenderer,
private _rowCount: number, private _rowCount: number,
screenElement: HTMLElement screenElement: HTMLElement,
optionsService: IOptionsService
) { ) {
super(); super();
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end)); 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._screenDprMonitor.setListener(() => this._renderer.onDevicePixelRatioChange());
this.register(this._screenDprMonitor); 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 // dprchange should handle this case, we need this as well for browsers that don't support the
// matchMedia query. // matchMedia query.
this.register(addDisposableDomListener(window, 'resize', () => this._renderer.onDevicePixelRatioChange())); this.register(addDisposableDomListener(window, 'resize', () => this._renderer.onDevicePixelRatioChange()));
@@ -144,10 +148,6 @@ export class RenderCoordinator extends Disposable {
this._renderer.onCursorMove(); this._renderer.onCursorMove();
} }
public onOptionsChanged(): void {
this._renderer.onOptionsChanged();
}
public clear(): void { public clear(): void {
this._renderer.clear(); this._renderer.clear();
} }