Merge remote-tracking branch 'upstream/master' into renderer_before_open

This commit is contained in:
Daniel Imms
2022-10-07 10:10:24 -07:00
82 changed files with 1425 additions and 1615 deletions
+135 -63
View File
@@ -36,15 +36,15 @@ import { Terminal as TerminalType, ITerminalOptions } from 'xterm';
export interface IWindowWithTerminal extends Window {
term: TerminalType;
Terminal?: typeof TerminalType;
AttachAddon?: typeof AttachAddon;
FitAddon?: typeof FitAddon;
SearchAddon?: typeof SearchAddon;
SerializeAddon?: typeof SerializeAddon;
WebLinksAddon?: typeof WebLinksAddon;
WebglAddon?: typeof WebglAddon;
Unicode11Addon?: typeof Unicode11Addon;
LigaturesAddon?: typeof LigaturesAddon;
Terminal?: typeof TerminalType; // eslint-disable-line @typescript-eslint/naming-convention
AttachAddon?: typeof AttachAddon; // eslint-disable-line @typescript-eslint/naming-convention
FitAddon?: typeof FitAddon; // eslint-disable-line @typescript-eslint/naming-convention
SearchAddon?: typeof SearchAddon; // eslint-disable-line @typescript-eslint/naming-convention
SerializeAddon?: typeof SerializeAddon; // eslint-disable-line @typescript-eslint/naming-convention
WebLinksAddon?: typeof WebLinksAddon; // eslint-disable-line @typescript-eslint/naming-convention
WebglAddon?: typeof WebglAddon; // eslint-disable-line @typescript-eslint/naming-convention
Unicode11Addon?: typeof Unicode11Addon; // eslint-disable-line @typescript-eslint/naming-convention
LigaturesAddon?: typeof LigaturesAddon; // eslint-disable-line @typescript-eslint/naming-convention
}
declare let window: IWindowWithTerminal;
@@ -59,27 +59,29 @@ type AddonType = 'attach' | 'canvas' | 'fit' | 'search' | 'serialize' | 'unicode
interface IDemoAddon<T extends AddonType> {
name: T;
canChange: boolean;
ctor:
ctor: (
T extends 'attach' ? typeof AttachAddon :
T extends 'canvas' ? typeof CanvasAddon :
T extends 'fit' ? typeof FitAddon :
T extends 'search' ? typeof SearchAddon :
T extends 'serialize' ? typeof SerializeAddon :
T extends 'web-links' ? typeof WebLinksAddon :
T extends 'unicode11' ? typeof Unicode11Addon :
T extends 'ligatures' ? typeof LigaturesAddon :
typeof WebglAddon;
instance?:
T extends 'canvas' ? typeof CanvasAddon :
T extends 'fit' ? typeof FitAddon :
T extends 'search' ? typeof SearchAddon :
T extends 'serialize' ? typeof SerializeAddon :
T extends 'web-links' ? typeof WebLinksAddon :
T extends 'unicode11' ? typeof Unicode11Addon :
T extends 'ligatures' ? typeof LigaturesAddon :
typeof WebglAddon
);
instance?: (
T extends 'attach' ? AttachAddon :
T extends 'canvas' ? CanvasAddon :
T extends 'fit' ? FitAddon :
T extends 'search' ? SearchAddon :
T extends 'serialize' ? SerializeAddon :
T extends 'web-links' ? WebLinksAddon :
T extends 'webgl' ? WebglAddon :
T extends 'unicode11' ? typeof Unicode11Addon :
T extends 'ligatures' ? typeof LigaturesAddon :
never;
T extends 'canvas' ? CanvasAddon :
T extends 'fit' ? FitAddon :
T extends 'search' ? SearchAddon :
T extends 'serialize' ? SerializeAddon :
T extends 'web-links' ? WebLinksAddon :
T extends 'webgl' ? WebglAddon :
T extends 'unicode11' ? typeof Unicode11Addon :
T extends 'ligatures' ? typeof LigaturesAddon :
never
);
}
const addons: { [T in AddonType]: IDemoAddon<T> } = {
@@ -96,12 +98,12 @@ const addons: { [T in AddonType]: IDemoAddon<T> } = {
let terminalContainer = document.getElementById('terminal-container');
const actionElements = {
find: <HTMLInputElement>document.querySelector('#find'),
findNext: <HTMLInputElement>document.querySelector('#find-next'),
findPrevious: <HTMLInputElement>document.querySelector('#find-previous'),
find: document.querySelector('#find') as HTMLInputElement,
findNext: document.querySelector('#find-next') as HTMLInputElement,
findPrevious: document.querySelector('#find-previous') as HTMLInputElement,
findResults: document.querySelector('#find-results')
};
const paddingElement = <HTMLInputElement>document.getElementById('padding');
const paddingElement = document.getElementById('padding') as HTMLInputElement;
const xtermjsTheme = {
foreground: '#F8F8F8',
@@ -146,7 +148,7 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions {
};
}
const disposeRecreateButtonHandler = () => {
const disposeRecreateButtonHandler: () => void = () => {
// If the terminal exists dispose of it, otherwise recreate it
if (term) {
term.dispose();
@@ -169,7 +171,7 @@ const disposeRecreateButtonHandler = () => {
}
};
const createNewWindowButtonHandler = () => {
const createNewWindowButtonHandler: () => void = () => {
if (term) {
disposeRecreateButtonHandler();
}
@@ -196,7 +198,7 @@ const createNewWindowButtonHandler = () => {
}
});
}
}
};
if (document.location.pathname === '/test') {
window.Terminal = Terminal;
@@ -220,6 +222,7 @@ if (document.location.pathname === '/test') {
document.getElementById('underline-test').addEventListener('click', underlineTest);
document.getElementById('ansi-colors').addEventListener('click', ansiColorsTest);
document.getElementById('osc-hyperlinks').addEventListener('click', addAnsiHyperlink);
document.getElementById('sgr-test').addEventListener('click', sgrTest);
document.getElementById('add-decoration').addEventListener('click', addDecoration);
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
}
@@ -296,8 +299,8 @@ function createTerminal(): void {
setTimeout(() => {
initOptions(term);
// TODO: Clean this up, opt-cols/rows doesn't exist anymore
(<HTMLInputElement>document.getElementById(`opt-cols`)).value = term.cols;
(<HTMLInputElement>document.getElementById(`opt-rows`)).value = term.rows;
(document.getElementById(`opt-cols`) as HTMLInputElement).value = term.cols;
(document.getElementById(`opt-rows`) as HTMLInputElement).value = term.rows;
paddingElement.value = '0';
// Set terminal size again to set the specific dimensions on the demo
@@ -423,14 +426,14 @@ function initOptions(term: TerminalType): void {
// Attach listeners
booleanOptions.forEach(o => {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.checked);
term.options[o] = input.checked;
});
});
numberOptions.forEach(o => {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
if (o === 'rows') {
@@ -452,7 +455,7 @@ function initOptions(term: TerminalType): void {
});
});
Object.keys(stringOptions).forEach(o => {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
let value: any = input.value;
@@ -485,7 +488,7 @@ function initOptions(term: TerminalType): void {
magenta: '#b168df',
red: '#da6771',
white: '#efefef',
yellow: '#fff099',
yellow: '#fff099'
};
break;
case 'light':
@@ -509,7 +512,7 @@ function initOptions(term: TerminalType): void {
magenta: '#bc05bc',
red: '#cd3131',
white: '#555555',
yellow: '#949800',
yellow: '#949800'
};
break;
}
@@ -541,7 +544,15 @@ function initAddons(term: TerminalType): void {
try {
term.loadAddon(addon.instance);
if (name === 'webgl') {
(addon.instance as WebglAddon).onChangeTextureAtlas(e => addTextureAtlas(e));
setTimeout(() => {
addTextureAtlas(addons.webgl.instance.textureAtlas);
addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e));
}, 0);
} else if (name === 'canvas') {
setTimeout(() => {
addTextureAtlas(addons.canvas.instance.textureAtlas);
addons.canvas.instance.onChangeTextureAtlas(e => addTextureAtlas(e));
}, 0);
} else if (name === 'unicode11') {
term.unicode.activeVersion = '11';
} else if (name === 'search') {
@@ -555,7 +566,9 @@ function initAddons(term: TerminalType): void {
}
} else {
if (name === 'webgl') {
(addon.instance as WebglAddon).textureAtlas.remove();
addons.webgl.instance.textureAtlas.remove();
} else if (name === 'canvas') {
addons.canvas.instance.textureAtlas.remove();
} else if (name === 'unicode11') {
term.unicode.activeVersion = '6';
}
@@ -580,7 +593,7 @@ function initAddons(term: TerminalType): void {
container.appendChild(fragment);
}
function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined) {
function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined): void {
let content: string;
if (e === undefined) {
content = 'undefined';
@@ -596,8 +609,8 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a
}
function updateTerminalSize(): void {
const cols = parseInt((<HTMLInputElement>document.getElementById(`opt-cols`)).value, 10);
const rows = parseInt((<HTMLInputElement>document.getElementById(`opt-rows`)).value, 10);
const cols = parseInt((document.getElementById(`opt-cols`) as HTMLInputElement).value, 10);
const rows = parseInt((document.getElementById(`opt-rows`) as HTMLInputElement).value, 10);
const width = (cols * term._core._renderService.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
const height = (rows * term._core._renderService.dimensions.actualCellHeight).toString() + 'px';
terminalContainer.style.width = width;
@@ -621,21 +634,21 @@ function htmlSerializeButtonHandler(): void {
document.getElementById('htmlserialize-output').innerText = output;
// Deprecated, but the most supported for now.
function listener(e: any) {
e.clipboardData.setData("text/html", output);
function listener(e: any): void {
e.clipboardData.setData('text/html', output);
e.preventDefault();
}
document.addEventListener("copy", listener);
document.execCommand("copy");
document.removeEventListener("copy", listener);
document.getElementById("htmlserialize-output-result").innerText = "Copied to clipboard";
document.addEventListener('copy', listener);
document.execCommand('copy');
document.removeEventListener('copy', listener);
document.getElementById('htmlserialize-output-result').innerText = 'Copied to clipboard';
}
function addTextureAtlas(e: HTMLCanvasElement) {
function addTextureAtlas(e: HTMLCanvasElement): void {
document.querySelector('#texture-atlas').replaceChildren(e);
}
function writeCustomGlyphHandler() {
function writeCustomGlyphHandler(): void {
term.write('\n\r');
term.write('\n\r');
term.write('Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐\n\r');
@@ -680,7 +693,7 @@ function writeCustomGlyphHandler() {
window.scrollTo(0, 0);
}
function loadTest() {
function loadTest(): void {
const rendererName = addons.webgl.instance ? 'webgl' : !!addons.canvas.instance ? 'canvas' : 'dom';
const testData = [];
let byteCount = 0;
@@ -713,7 +726,7 @@ function loadTest() {
});
}
function powerlineSymbolTest() {
function powerlineSymbolTest(): void {
function s(char: string): string {
return `${char} \x1b[7m${char}\x1b[0m `;
}
@@ -787,7 +800,7 @@ function powerlineSymbolTest() {
term.writeln('nf-mdi-github_face (\\uFbd9) \ufbd9');
}
function underlineTest() {
function underlineTest(): void {
function u(style: number): string {
return `\x1b[4:${style}m`;
}
@@ -797,7 +810,7 @@ function underlineTest() {
term.write('\n\n\r');
term.writeln('Underline styles:');
term.writeln('');
function showSequence(id: number, name: string) {
function showSequence(id: number, name: string): string {
let alphabet = '';
for (let i = 97; i < 123; i++) {
alphabet += String.fromCharCode(i);
@@ -844,7 +857,7 @@ function underlineTest() {
term.write('\x1b[0m\n\r');
}
function ansiColorsTest() {
function ansiColorsTest(): void {
term.writeln(`\x1b[0m\n\n\rStandard colors: Bright colors:`);
for (let i = 0; i < 16; i++) {
term.write(`\x1b[48;5;${i}m ${i.toString().padEnd(2, ' ').padStart(3, ' ')} \x1b[0m`);
@@ -869,7 +882,66 @@ function ansiColorsTest() {
}
}
function addAnsiHyperlink() {
function writeTestString(): string {
let alphabet = '';
for (let i = 97; i < 123; i++) {
alphabet += String.fromCharCode(i);
}
let numbers = '';
for (let i = 0; i < 10; i++) {
numbers += i.toString();
}
return `${alphabet} ${numbers} 汉语 한국어 👽`;
}
const testString = writeTestString();
function sgrTest(): void {
term.write('\n\n\r');
term.writeln(`Character Attributes (SGR, Select Graphic Rendition)`);
const entries: { ps: number, name: string }[] = [
{ ps: 0, name: 'Normal' },
{ ps: 1, name: 'Bold' },
{ ps: 2, name: 'Faint/dim' },
{ ps: 3, name: 'Italicized' },
{ ps: 4, name: 'Underlined' },
{ ps: 5, name: 'Blink' },
{ ps: 7, name: 'Inverse' },
{ ps: 8, name: 'Invisible' },
{ ps: 9, name: 'Crossed-out characters' },
{ ps: 21, name: 'Doubly-underlined' },
{ ps: 22, name: 'Normal' },
{ ps: 23, name: 'Not italicized' },
{ ps: 24, name: 'Not underlined' },
{ ps: 25, name: 'Steady (not blink)' },
{ ps: 27, name: 'Positive (not inverse)' },
{ ps: 28, name: 'Visible (not hidden)' },
{ ps: 29, name: 'Not crossed-out' },
{ ps: 30, name: 'Foreground Black' },
{ ps: 31, name: 'Foreground Red' },
{ ps: 32, name: 'Foreground Green' },
{ ps: 33, name: 'Foreground Yellow' },
{ ps: 34, name: 'Foreground Blue' },
{ ps: 35, name: 'Foreground Magenta' },
{ ps: 36, name: 'Foreground Cyan' },
{ ps: 37, name: 'Foreground White' },
{ ps: 39, name: 'Foreground default' },
{ ps: 40, name: 'Background Black' },
{ ps: 41, name: 'Background Red' },
{ ps: 42, name: 'Background Green' },
{ ps: 43, name: 'Background Yellow' },
{ ps: 44, name: 'Background Blue' },
{ ps: 45, name: 'Background Magenta' },
{ ps: 46, name: 'Background Cyan' },
{ ps: 47, name: 'Background White' },
{ ps: 49, name: 'Background default' }
];
const maxNameLength = entries.reduce<number>((p, c) => Math.max(c.name.length, p), 0);
for (const e of entries) {
term.writeln(`\x1b[0m\x1b[${e.ps}m ${e.ps.toString().padEnd(2, ' ')} ${e.name.padEnd(maxNameLength, ' ')} - ${testString}\x1b[0m`);
}
}
function addAnsiHyperlink(): void {
term.write('\n\n\r');
term.writeln(`Regular link with no id:`);
term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;\x07');
@@ -889,7 +961,7 @@ function addAnsiHyperlink() {
term.write('\x1b[3A\x1b[1C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[5D');
}
function addDecoration() {
function addDecoration(): void {
term.options['overviewRulerWidth'] = 15;
const marker = term.registerMarker(1);
const decoration = term.registerDecoration({
@@ -904,7 +976,7 @@ function addDecoration() {
});
}
function addOverviewRuler() {
function addOverviewRuler(): void {
term.options['overviewRulerWidth'] = 15;
term.registerDecoration({ marker: term.registerMarker(1), overviewRulerOptions: { color: '#ef2929' } });
term.registerDecoration({ marker: term.registerMarker(3), overviewRulerOptions: { color: '#8ae234' } });
+1
View File
@@ -79,6 +79,7 @@
<dd><button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button></dd>
<dd><button id="powerline-symbol-test" title="Write powerline symbol characters to the terminal (\ue0a0+)">Powerline symbol test</button></dd>
<dd><button id="underline-test" title="Write text with Kitty's extended underline sequences">Underline test</button></dd>
<dd><button id="sgr-test" title="Write text with SGR attribute">SGR test</button></dd>
<dd><button id="ansi-colors" title="Write a wide range of ansi colors">Ansi colors test</button></dd>
<dd><button id="osc-hyperlinks" title="Write some OSC 8 hyperlinks">Ansi hyperlinks test</button></dd>
+28 -18
View File
@@ -16,8 +16,7 @@ function startServer() {
var app = express();
expressWs(app);
var terminals = {},
logs = {};
var terminals = {};
app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css'));
app.get('/logo.png', (req, res) => {
@@ -55,10 +54,6 @@ function startServer() {
console.log('Created terminal with PID: ' + term.pid);
terminals[term.pid] = term;
logs[term.pid] = '';
term.on('data', function(data) {
logs[term.pid] += data;
});
res.send(term.pid.toString());
res.end();
});
@@ -77,15 +72,25 @@ function startServer() {
app.ws('/terminals/:pid', function (ws, req) {
var term = terminals[parseInt(req.params.pid)];
console.log('Connected to terminal ' + term.pid);
ws.send(logs[term.pid]);
// unbuffered delivery after user input
let userInput = false;
// string message buffering
function buffer(socket, timeout) {
function buffer(socket, timeout, maxSize) {
let s = '';
let sender = null;
return (data) => {
s += data;
if (!sender) {
if (s.length > maxSize || userInput) {
userInput = false;
socket.send(s);
s = '';
if (sender) {
clearTimeout(sender);
sender = null;
}
} else if (!sender) {
sender = setTimeout(() => {
socket.send(s);
s = '';
@@ -95,14 +100,23 @@ function startServer() {
};
}
// binary message buffering
function bufferUtf8(socket, timeout) {
function bufferUtf8(socket, timeout, maxSize) {
let buffer = [];
let sender = null;
let length = 0;
return (data) => {
buffer.push(data);
length += data.length;
if (!sender) {
if (length > maxSize || userInput) {
userInput = false;
socket.send(Buffer.concat(buffer, length));
buffer = [];
length = 0;
if (sender) {
clearTimeout(sender);
sender = null;
}
} else if (!sender) {
sender = setTimeout(() => {
socket.send(Buffer.concat(buffer, length));
buffer = [];
@@ -112,27 +126,23 @@ function startServer() {
}
};
}
const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5);
const send = (USE_BINARY ? bufferUtf8 : buffer)(ws, 5, 262144);
// WARNING: This is a naive implementation that will not throttle the flow of data. This means
// it could flood the communication channel and make the terminal unresponsive. Learn more about
// the problem and how to implement flow control at https://xtermjs.org/docs/guides/flowcontrol/
term.on('data', function(data) {
try {
send(data);
} catch (ex) {
// The WebSocket is not open, ignore
}
send(data);
});
ws.on('message', function(msg) {
term.write(msg);
userInput = true;
});
ws.on('close', function () {
term.kill();
console.log('Closed terminal ' + term.pid);
// Clean things up
delete terminals[term.pid];
delete logs[term.pid];
});
});