305 lines
11 KiB
TypeScript
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;
|
|
}
|
|
}
|