Files
xterm.js/demo/client/components/window/optionsWindow.ts
T

305 lines
11 KiB
TypeScript

/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { BaseWindow } from './baseWindow';
import type { Terminal, ITheme } from '@xterm/xterm';
import type { IControlWindow } from '../controlBar';
import type { AddonCollection } from 'types';
const xtermjsTheme: ITheme = Object.freeze({
foreground: '#F8F8F8',
background: '#2D2E2C',
selectionBackground: '#5DA5D533',
selectionInactiveBackground: '#555555AA',
black: '#1E1E1D',
brightBlack: '#262625',
red: '#CE5C5C',
brightRed: '#FF7272',
green: '#5BCC5B',
brightGreen: '#72FF72',
yellow: '#CCCC5B',
brightYellow: '#FFFF72',
blue: '#5D5DD3',
brightBlue: '#7279FF',
magenta: '#BC5ED1',
brightMagenta: '#E572FF',
cyan: '#5DA5D5',
brightCyan: '#72F0FF',
white: '#F8F8F8',
brightWhite: '#FFFFFF'
});
const sapphireTheme: ITheme = Object.freeze({
background: '#1c2431',
foreground: '#cccccc',
selectionBackground: '#399ef440',
black: '#666666',
blue: '#399ef4',
brightBlack: '#666666',
brightBlue: '#399ef4',
brightCyan: '#21c5c7',
brightGreen: '#4eb071',
brightMagenta: '#b168df',
brightRed: '#da6771',
brightWhite: '#efefef',
brightYellow: '#fff099',
cyan: '#21c5c7',
green: '#4eb071',
magenta: '#b168df',
red: '#da6771',
white: '#efefef',
yellow: '#fff099'
});
const lightTheme: ITheme = Object.freeze({
background: '#ffffff',
foreground: '#333333',
cursor: '#333333',
cursorAccent: '#ffffff',
selectionBackground: '#add6ff',
overviewRulerBorder: '#aaaaaa',
black: '#000000',
blue: '#0451a5',
brightBlack: '#666666',
brightBlue: '#0451a5',
brightCyan: '#0598bc',
brightGreen: '#14ce14',
brightMagenta: '#bc05bc',
brightRed: '#cd3131',
brightWhite: '#a5a5a5',
brightYellow: '#b5ba00',
cyan: '#0598bc',
green: '#00bc00',
magenta: '#bc05bc',
red: '#cd3131',
white: '#555555',
yellow: '#949800'
});
export class OptionsWindow extends BaseWindow implements IControlWindow {
public readonly id = 'options';
public readonly label = 'Options';
private _container!: HTMLElement;
private _optionsContainer!: HTMLElement;
private _autoResize: boolean = true;
constructor(
terminal: Terminal,
addons: AddonCollection,
private readonly _handlers: {
updateTerminalSize: () => void;
updateTerminalContainerBackground: () => void;
},
) {
super(terminal, addons);
}
public build(container: HTMLElement): void {
this._container = container;
const description = document.createElement('p');
description.innerHTML = 'These options can be set in the <code>Terminal</code> constructor or by using the <code>Terminal.options</code> property.';
container.appendChild(description);
this._optionsContainer = document.createElement('div');
this._optionsContainer.id = 'options-container';
container.appendChild(this._optionsContainer);
}
public initOptions(addDomListener: (el: HTMLElement, type: string, handler: (...args: any[]) => any) => void): void {
const blacklistedOptions = [
'convertEol',
'termName',
'cols', 'rows',
'documentOverride',
'linkHandler',
'logger',
'quirks',
'scrollbar',
'theme',
'vtExtensions',
'windowOptions',
'windowsPty',
];
const nestedBooleanOptions: { label: string, path: string[], prop: string }[] = [
{ label: 'scrollbar.showScrollbar', path: ['scrollbar'], prop: 'showScrollbar' },
{ label: 'scrollbar.showArrows', path: ['scrollbar'], prop: 'showArrows' },
{ label: 'scrollbar.overviewRuler.showTopBorder', path: ['scrollbar', 'overviewRuler'], prop: 'showTopBorder' },
{ label: 'scrollbar.overviewRuler.showBottomBorder', path: ['scrollbar', 'overviewRuler'], prop: 'showBottomBorder' },
{ label: 'vtExtensions.kittyKeyboard', path: ['vtExtensions'], prop: 'kittyKeyboard' },
{ label: 'vtExtensions.kittySgrBoldFaintControl', path: ['vtExtensions'], prop: 'kittySgrBoldFaintControl' },
{ label: 'vtExtensions.win32InputMode', path: ['vtExtensions'], prop: 'win32InputMode' }
];
const stringOptions: { [key: string]: string[] | null } = {
cursorStyle: ['block', 'underline', 'bar'],
cursorInactiveStyle: ['outline', 'block', 'bar', 'underline', 'none'],
fontFamily: null,
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
logLevel: ['trace', 'debug', 'info', 'warn', 'error', 'off'],
theme: ['default', 'xtermjs', 'sapphire', 'light'],
wordSeparator: null,
colsRows: null
};
const options = Object.getOwnPropertyNames(this._terminal.options);
const booleanOptions: string[] = [];
const numberOptions: string[] = [];
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
switch (typeof (this._terminal.options as Record<string, unknown>)[o]) {
case 'boolean':
booleanOptions.push(o);
break;
case 'number':
numberOptions.push(o);
break;
default:
if (Object.keys(stringOptions).indexOf(o) === -1 && numberOptions.indexOf(o) === -1 && booleanOptions.indexOf(o) === -1) {
console.warn(`Unrecognized option: "${o}"`);
}
}
});
let html = '';
html += '<div class="option-group">';
booleanOptions.forEach(o => {
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${(this._terminal.options as Record<string, unknown>)[o] ? 'checked' : ''}/> ${o}</label></div>`;
});
nestedBooleanOptions.forEach(({ label, path, prop }) => {
const options = this._terminal.options as Record<string, unknown>;
const parent = path.reduce<Record<string, unknown> | undefined>((acc, key) => (acc as Record<string, unknown> | undefined)?.[key] as Record<string, unknown> | undefined, options);
const checked = (parent as Record<string, unknown> | undefined)?.[prop] ?? false;
html += `<div class="option"><label><input id="opt-${label.replace('.', '-')}" type="checkbox" ${checked ? 'checked' : ''}/> ${label}</label></div>`;
});
html += '</div><div class="option-group">';
numberOptions.forEach(o => {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${(this._terminal.options as Record<string, unknown>)[o] ?? ''}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
});
html += '</div><div class="option-group">';
Object.keys(stringOptions).forEach(o => {
if (o === 'colsRows') {
html += `<div class="option"><label>size (<var>cols</var><code>x</code><var>rows</var> or <code>auto</code>) <input id="opt-${o}" type="text" value="auto"/></label></div>`;
} else if (stringOptions[o]) {
const selectedOption = o === 'theme' ? 'xtermjs' : (this._terminal.options as Record<string, unknown>)[o];
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o]!.map(v => `<option ${v === selectedOption ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
} else {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${(this._terminal.options as Record<string, unknown>)[o]}"/></label></div>`;
}
});
html += '</div>';
this._optionsContainer.innerHTML = html;
// Attach listeners
booleanOptions.forEach(o => {
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.checked);
(this._terminal.options as Record<string, unknown>)[o] = input.checked;
if (o ==='allowTransparency') {
this._terminal.options.theme = this._getTheme();
this._handlers.updateTerminalContainerBackground();
}
});
});
nestedBooleanOptions.forEach(({ label, path, prop }) => {
const input = document.getElementById(`opt-${label.replace('.', '-')}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', label, input.checked);
const options = this._terminal.options as Record<string, unknown>;
if (path.length === 1) {
const parentKey = path[0];
options[parentKey] = { ...(options[parentKey] as Record<string, unknown> | undefined), [prop]: input.checked };
return;
}
if (path.length === 2) {
const [parentKey, childKey] = path;
const parent = (options[parentKey] as Record<string, unknown> | undefined) ?? {};
const child = (parent[childKey] as Record<string, unknown> | undefined) ?? {};
options[parentKey] = { ...parent, [childKey]: { ...child, [prop]: input.checked } };
}
});
});
numberOptions.forEach(o => {
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
if (o === 'lineHeight') {
this._terminal.options.lineHeight = parseFloat(input.value);
} else if (o === 'scrollSensitivity') {
this._terminal.options.scrollSensitivity = parseFloat(input.value);
} else if (o === 'scrollback') {
this._terminal.options.scrollback = parseInt(input.value);
setTimeout(() => this._handlers.updateTerminalSize(), 5);
} else {
(this._terminal.options as Record<string, unknown>)[o] = parseInt(input.value);
}
this._handlers.updateTerminalSize();
});
});
Object.keys(stringOptions).forEach(o => {
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
let value: unknown = input.value;
if (o === 'colsRows') {
const m = input.value.match(/^([0-9]+)x([0-9]+)$/);
if (m) {
this._autoResize = false;
this._terminal.resize(parseInt(m[1]), parseInt(m[2]));
} else {
this._autoResize = true;
input.value = 'auto';
this._handlers.updateTerminalSize();
}
const screenElement = document.querySelector('.xterm-screen');
if (screenElement instanceof HTMLElement) {
screenElement.style.outline = this._autoResize ? ''
: 'solid thin red';
}
} else if (o === 'theme') {
value = this._getTheme();
}
(this._terminal.options as Record<string, unknown>)[o] = value;
if (o === 'theme') {
this._handlers.updateTerminalContainerBackground();
}
if (o === 'fontFamily') {
this._handlers.updateTerminalSize();
}
});
});
}
public get autoResize(): boolean {
return this._autoResize;
}
public set autoResize(value: boolean) {
this._autoResize = value;
}
private _getTheme(): ITheme {
const input = document.querySelector<HTMLInputElement>('#opt-theme')!;
let theme: ITheme = {};
switch (input.value) {
case 'default':
theme = {};
break;
case 'xtermjs':
theme = { ...xtermjsTheme };
break;
case 'sapphire':
theme = { ...sapphireTheme };
break;
case 'light':
theme = { ...lightTheme };
break;
}
if (this._terminal.options.allowTransparency) {
theme.background = 'transparent';
}
return theme;
}
}