202 lines
6.0 KiB
TypeScript
202 lines
6.0 KiB
TypeScript
/**
|
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
|
* @license MIT
|
|
*/
|
|
|
|
import { EventEmitter } from 'common/EventEmitter';
|
|
import { Disposable } from 'common/Lifecycle';
|
|
import { isMac } from 'common/Platform';
|
|
import { CursorStyle, IDisposable } from 'common/Types';
|
|
import { FontWeight, IOptionsService, ITerminalOptions } from 'common/services/Services';
|
|
|
|
export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
|
|
cols: 80,
|
|
rows: 24,
|
|
cursorBlink: false,
|
|
cursorStyle: 'block',
|
|
cursorWidth: 1,
|
|
cursorInactiveStyle: 'outline',
|
|
customGlyphs: true,
|
|
drawBoldTextInBrightColors: true,
|
|
fastScrollModifier: 'alt',
|
|
fastScrollSensitivity: 5,
|
|
fontFamily: 'courier-new, courier, monospace',
|
|
fontSize: 15,
|
|
fontWeight: 'normal',
|
|
fontWeightBold: 'bold',
|
|
ignoreBracketedPasteMode: false,
|
|
lineHeight: 1.0,
|
|
letterSpacing: 0,
|
|
linkHandler: null,
|
|
logLevel: 'info',
|
|
logger: null,
|
|
scrollback: 1000,
|
|
scrollOnUserInput: true,
|
|
scrollSensitivity: 1,
|
|
screenReaderMode: false,
|
|
smoothScrollDuration: 0,
|
|
macOptionIsMeta: false,
|
|
macOptionClickForcesSelection: false,
|
|
minimumContrastRatio: 1,
|
|
disableStdin: false,
|
|
allowProposedApi: false,
|
|
allowTransparency: false,
|
|
tabStopWidth: 8,
|
|
theme: {},
|
|
rightClickSelectsWord: isMac,
|
|
windowOptions: {},
|
|
windowsMode: false,
|
|
windowsPty: {},
|
|
wordSeparator: ' ()[]{}\',"`',
|
|
altClickMovesCursor: true,
|
|
convertEol: false,
|
|
termName: 'xterm',
|
|
cancelEvents: false,
|
|
overviewRulerWidth: 0
|
|
};
|
|
|
|
const FONT_WEIGHT_OPTIONS: Extract<FontWeight, string>[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'];
|
|
|
|
export class OptionsService extends Disposable implements IOptionsService {
|
|
public serviceBrand: any;
|
|
|
|
public readonly rawOptions: Required<ITerminalOptions>;
|
|
public options: Required<ITerminalOptions>;
|
|
|
|
private readonly _onOptionChange = this.register(new EventEmitter<keyof ITerminalOptions>());
|
|
public readonly onOptionChange = this._onOptionChange.event;
|
|
|
|
constructor(options: Partial<ITerminalOptions>) {
|
|
super();
|
|
// set the default value of each option
|
|
const defaultOptions = { ...DEFAULT_OPTIONS };
|
|
for (const key in options) {
|
|
if (key in defaultOptions) {
|
|
try {
|
|
const newValue = options[key];
|
|
defaultOptions[key] = this._sanitizeAndValidateOption(key, newValue);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// set up getters and setters for each option
|
|
this.rawOptions = defaultOptions;
|
|
this.options = { ... defaultOptions };
|
|
this._setupOptions();
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
public onSpecificOptionChange<T extends keyof ITerminalOptions>(key: T, listener: (value: ITerminalOptions[T]) => any): IDisposable {
|
|
return this.onOptionChange(eventKey => {
|
|
if (eventKey === key) {
|
|
listener(this.rawOptions[key]);
|
|
}
|
|
});
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
public onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable {
|
|
return this.onOptionChange(eventKey => {
|
|
if (keys.indexOf(eventKey) !== -1) {
|
|
listener();
|
|
}
|
|
});
|
|
}
|
|
|
|
private _setupOptions(): void {
|
|
const getter = (propName: string): any => {
|
|
if (!(propName in DEFAULT_OPTIONS)) {
|
|
throw new Error(`No option with key "${propName}"`);
|
|
}
|
|
return this.rawOptions[propName];
|
|
};
|
|
|
|
const setter = (propName: string, value: any): void => {
|
|
if (!(propName in DEFAULT_OPTIONS)) {
|
|
throw new Error(`No option with key "${propName}"`);
|
|
}
|
|
|
|
value = this._sanitizeAndValidateOption(propName, value);
|
|
// Don't fire an option change event if they didn't change
|
|
if (this.rawOptions[propName] !== value) {
|
|
this.rawOptions[propName] = value;
|
|
this._onOptionChange.fire(propName);
|
|
}
|
|
};
|
|
|
|
for (const propName in this.rawOptions) {
|
|
const desc = {
|
|
get: getter.bind(this, propName),
|
|
set: setter.bind(this, propName)
|
|
};
|
|
Object.defineProperty(this.options, propName, desc);
|
|
}
|
|
}
|
|
|
|
private _sanitizeAndValidateOption(key: string, value: any): any {
|
|
switch (key) {
|
|
case 'cursorStyle':
|
|
if (!value) {
|
|
value = DEFAULT_OPTIONS[key];
|
|
}
|
|
if (!isCursorStyle(value)) {
|
|
throw new Error(`"${value}" is not a valid value for ${key}`);
|
|
}
|
|
break;
|
|
case 'wordSeparator':
|
|
if (!value) {
|
|
value = DEFAULT_OPTIONS[key];
|
|
}
|
|
break;
|
|
case 'fontWeight':
|
|
case 'fontWeightBold':
|
|
if (typeof value === 'number' && 1 <= value && value <= 1000) {
|
|
// already valid numeric value
|
|
break;
|
|
}
|
|
value = FONT_WEIGHT_OPTIONS.includes(value) ? value : DEFAULT_OPTIONS[key];
|
|
break;
|
|
case 'cursorWidth':
|
|
value = Math.floor(value);
|
|
// Fall through for bounds check
|
|
case 'lineHeight':
|
|
case 'tabStopWidth':
|
|
if (value < 1) {
|
|
throw new Error(`${key} cannot be less than 1, value: ${value}`);
|
|
}
|
|
break;
|
|
case 'minimumContrastRatio':
|
|
value = Math.max(1, Math.min(21, Math.round(value * 10) / 10));
|
|
break;
|
|
case 'scrollback':
|
|
value = Math.min(value, 4294967295);
|
|
if (value < 0) {
|
|
throw new Error(`${key} cannot be less than 0, value: ${value}`);
|
|
}
|
|
break;
|
|
case 'fastScrollSensitivity':
|
|
case 'scrollSensitivity':
|
|
if (value <= 0) {
|
|
throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`);
|
|
}
|
|
break;
|
|
case 'rows':
|
|
case 'cols':
|
|
if (!value && value !== 0) {
|
|
throw new Error(`${key} must be numeric, value: ${value}`);
|
|
}
|
|
break;
|
|
case 'windowsPty':
|
|
value = value ?? {};
|
|
break;
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
|
|
function isCursorStyle(value: unknown): value is CursorStyle {
|
|
return value === 'block' || value === 'underline' || value === 'bar';
|
|
}
|