From 15664de472b3b556e7976766b414c9d1f22b81d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 3 Oct 2024 21:50:34 +0200 Subject: [PATCH 01/19] initial --- .eslintrc.json | 2 + addons/addon-web-fonts/LICENSE | 19 ++ addons/addon-web-fonts/README.md | 137 ++++++++ addons/addon-web-fonts/package.json | 29 ++ addons/addon-web-fonts/src/WebFontsAddon.ts | 299 ++++++++++++++++++ addons/addon-web-fonts/src/tsconfig.json | 28 ++ .../test/WebLinksAddon.test.ts | 185 +++++++++++ .../addon-web-fonts/test/playwright.config.ts | 35 ++ addons/addon-web-fonts/test/tsconfig.json | 42 +++ addons/addon-web-fonts/tsconfig.json | 8 + .../typings/addon-web-fonts.d.ts | 18 ++ addons/addon-web-fonts/webpack.config.js | 33 ++ demo/client.ts | 33 +- demo/style.css | 112 +++++++ demo/tsconfig.json | 1 + tsconfig.all.json | 1 + 16 files changed, 970 insertions(+), 12 deletions(-) create mode 100644 addons/addon-web-fonts/LICENSE create mode 100644 addons/addon-web-fonts/README.md create mode 100644 addons/addon-web-fonts/package.json create mode 100644 addons/addon-web-fonts/src/WebFontsAddon.ts create mode 100644 addons/addon-web-fonts/src/tsconfig.json create mode 100644 addons/addon-web-fonts/test/WebLinksAddon.test.ts create mode 100644 addons/addon-web-fonts/test/playwright.config.ts create mode 100644 addons/addon-web-fonts/test/tsconfig.json create mode 100644 addons/addon-web-fonts/tsconfig.json create mode 100644 addons/addon-web-fonts/typings/addon-web-fonts.d.ts create mode 100644 addons/addon-web-fonts/webpack.config.js diff --git a/.eslintrc.json b/.eslintrc.json index 8c475982..3d9322d3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -33,6 +33,8 @@ "addons/addon-unicode-graphemes/src/tsconfig.json", "addons/addon-unicode-graphemes/test/tsconfig.json", "addons/addon-unicode-graphemes/benchmark/tsconfig.json", + "addons/addon-web-fonts/src/tsconfig.json", + "addons/addon-web-fonts/test/tsconfig.json", "addons/addon-web-links/src/tsconfig.json", "addons/addon-web-links/test/tsconfig.json", "addons/addon-webgl/src/tsconfig.json", diff --git a/addons/addon-web-fonts/LICENSE b/addons/addon-web-fonts/LICENSE new file mode 100644 index 00000000..447eb79f --- /dev/null +++ b/addons/addon-web-fonts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024, The xterm.js authors (https://github.com/xtermjs/xterm.js) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/addons/addon-web-fonts/README.md b/addons/addon-web-fonts/README.md new file mode 100644 index 00000000..bf506cf1 --- /dev/null +++ b/addons/addon-web-fonts/README.md @@ -0,0 +1,137 @@ +## @xterm/addon-web-fonts + +Addon to use webfonts with [xterm.js](https://github.com/xtermjs/xterm.js). This addon requires xterm.js v5+. + +### Install + +```bash +npm install --save @xterm/addon-web-fonts +``` + +### Issue with Webfonts + +Webfonts are announced by CSS `font-face` rules (or its Javascript `FontFace` counterparts). Since font files tend to be quite big assets, browser engines often postpone their loading to an actual styling request of a codepoint matching a font file's `unicode-range`. In short - font files will not be loaded until really needed. + +xterm.js on the other hand heavily relies on exact measurements of character glyphs to layout its output. This is done by determining the glyph width (DOM renderer) or by creating a glyph texture (WebGl renderer) for every output character. +For performance reasons both is done in synchronous code and cached. This logic only works properly, +if a font glyph is available on its first usage, or a wrong glyph from a fallback font chosen by the browser will be used instead. + +For webfonts and xterm.js this means, that we cannot rely on the default loading strategy of the browser, but have to preload the font files before using that font in xterm.js. + + +### Static Preloading for the Rescue? + +If you dont mind higher initial loading times of the embedding document, you can tell the browser to preload the needed font files by placing the following link elements in the document's head above any other CSS/Javascript: +```html + + + ... + +``` +Downside of this approach is the much higher initial loading time showing as a white page. Browsers also will resort to system fonts, if the preloading takes too long, so with a slow connection or a very big font this solves literally nothing. + + +### Preloading with WebFontsAddon + +The webfonts addon offers several ways to deal with the loading of font assets without leaving the terminal in an unusable state. + + +Recap - normally boostrapping of a new terminal involves these basic steps: + +```typescript +import { Terminal } from '@xterm/xterm'; +import { XYAddon } from '@xterm/addon-xy'; + +// create a `Terminal` instance with some options, e.g. a custom font family +const terminal = new Terminal({fontFamily: 'monospace'}); + +// create and load all addons you want to use, e.g. fit addon +const xyAddon = new XYAddon(); +terminal.loadAddon(xyAddon); + +// finally: call `open` of the terminal instance +terminal.open(your_terminal_div_element); // <-- critical path for webfonts +// more boostrapping goes here ... +``` + +This synchronous code is guaranteed to work in all browsers, as the font `monospace` will always be available. +It will also work that way with any installed system font, but breaks horribly for webfonts. The actual culprit here is the call to `terminal.open`, which attaches the terminal to the DOM and starts the renderer with all the glyph caching mentioned above, while the webfont is not fully available yet. + +To fix that, the webfonts addon provides a waiting condition: +```typescript +import { Terminal } from '@xterm/xterm'; +import { XYAddon } from '@xterm/addon-xy'; +import { WebFontsAddon } from '@xterm/addon-web-fonts'; + +// create a `Terminal` instance, now with webfonts +const terminal = new Terminal({fontFamily: '"Web Mono 1", "Super Powerline", monospace'}); +const xyAddon = new XYAddon(); +terminal.loadAddon(xyAddon); + +const webFontsAddon = new WebFontsAddon(); +terminal.loadAddon(webFontsAddon); + +// wait for webfonts to be fully loaded +await WebFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { + terminal.open(your_terminal_div_element); + // more boostrapping goes here ... +}); +``` +Here `loadFonts` will look up the font face objects in `document.fonts` and load them before continuing. +For this to work, you have to make sure, that the CSS `font-face` rules for these webfonts are loaded +on the initial document load (more precise - by the time this code runs). + +Please note, that this code cannot run synchronous anymore, so you will have to split your +bootstrapping code into several stages. If thats too much of a hassle, you can also move the whole +bootstrapping under that waiting condition (`loadFonts` is actually a static method): +```typescript +import { Terminal } from '@xterm/xterm'; +import { XYAddon } from '@xterm/addon-xy'; +import { WebFontsAddon } from '@xterm/addon-web-fonts'; + +WebFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { + // create a `Terminal` instance, now with webfonts + const terminal = new Terminal({fontFamily: '"Web Mono 1", "Super Powerline", monospace'}); + const xyAddon = new XYAddon(); + terminal.loadAddon(xyAddon); + + const webFontsAddon = new WebFontsAddon(); + terminal.loadAddon(webFontsAddon); + + terminal.open(your_terminal_div_element); + // more boostrapping goes here ... +}); +``` + +### Webfont Loading at Runtime + +Given you have a terminal already running and want to change the font family to a different not yet loaded webfont. +That can be achieved like this: +```typescript +// either create font face objects in javascript +const ff1 = new FontFace('New Web Mono', url1, ...); +const ff2 = new FontFace('New Web Mono', url2, ...); +// and await their loading +await WebFontsAddon.loadFonts([ff1, ff2]).then(() => { + // apply new webfont to terminal + terminal.options.fontFamily = 'New Web Mono'; + // since the new font might have slighly different metrics, + // also run the fit addon here (or any other custom resize logic) + fitAddon.fit(); +}); + +// or alternatively use CSS to add new font-face rules, e.g. +document.styleSheets[0].insertRule( + "@font-face { font-family: 'New Web Mono'; src: url(newfont.woff); }", 0); +// and await the new font family name +await WebFontsAddon.loadFonts(['New Web Mono']).then(() => { + // apply new webfont to terminal + terminal.options.fontFamily = 'New Web Mono'; + // since the new font might have slighly different metrics, + // also run the fit addon here (or any other custom resize logic) + fitAddon.fit(); +}); +``` + + +See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-web-fonts/typings/addon-web-fonts.d.ts) for more advanced usage. diff --git a/addons/addon-web-fonts/package.json b/addons/addon-web-fonts/package.json new file mode 100644 index 00000000..1daac376 --- /dev/null +++ b/addons/addon-web-fonts/package.json @@ -0,0 +1,29 @@ +{ + "name": "@xterm/addon-web-fonts", + "version": "0.1.0", + "author": { + "name": "The xterm.js authors", + "url": "https://xtermjs.org/" + }, + "main": "lib/addon-web-fonts.js", + "module": "lib/addon-web-fonts.mjs", + "types": "typings/addon-web-fonts.d.ts", + "repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-web-fonts", + "license": "MIT", + "keywords": [ + "terminal", + "xterm", + "xterm.js" + ], + "scripts": { + "build": "../../node_modules/.bin/tsc -p .", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package", + "start": "node ../../demo/start" + }, + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + }, + "dependencies": {} +} diff --git a/addons/addon-web-fonts/src/WebFontsAddon.ts b/addons/addon-web-fonts/src/WebFontsAddon.ts new file mode 100644 index 00000000..91800abd --- /dev/null +++ b/addons/addon-web-fonts/src/WebFontsAddon.ts @@ -0,0 +1,299 @@ +/** + * Copyright (c) 2024 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import type { Terminal, ITerminalAddon } from '@xterm/xterm'; +import type { WebFontsAddon as IWebFontsApi } from '@xterm/addon-web-fonts'; + + +/** + * Unquote family name. + */ +function unquote(s: string): string { + if (s[0] === '"' && s[s.length - 1] === '"') return s.slice(1, -1); + if (s[0] === "'" && s[s.length - 1] === "'") return s.slice(1, -1); + return s; +} + + +/** + * Quote family name. + * @see https://mathiasbynens.be/notes/unquoted-font-family + */ +function quote(s: string): string { + const pos = s.match(/([-_a-zA-Z0-9\xA0-\u{10FFFF}]+)/u); + const neg = s.match(/^(-?\d|--)/m); + if (!neg && pos && pos[1] === s) return s; + return `"${s.replace('"', '\\"')}"`; +} + + +function splitFamily(family: string | undefined): string[] { + if (!family) return []; + return family.split(',').map(e => unquote(e.trim())); +} + + +function createFamily(families: string[]): string { + return families.map(quote).join(', '); +} + + +function _loadFonts(fonts?: (string | FontFace)[]): Promise { + let ffs = Array.from(document.fonts); + if (!fonts || !fonts.length) { + return Promise.all(ffs.map(ff => ff.load())); + } + let toLoad: FontFace[] = []; + let ffsHashed = ffs.map(ff => WebFontsAddon.hashFontFace(ff)); + for (const font of fonts) { + if (font instanceof FontFace) { + const fontHashed = WebFontsAddon.hashFontFace(font); + const idx = ffsHashed.indexOf(fontHashed); + if (idx === -1) { + document.fonts.add(font); + ffs.push(font); + ffsHashed.push(fontHashed); + toLoad.push(font); + } else { + toLoad.push(ffs[idx]); + } + } else { + // string as font + const familyFiltered = ffs.filter(ff => font === unquote(ff.family)); + toLoad = toLoad.concat(familyFiltered); + if (!familyFiltered.length) { + console.warn(`font family "${font}" not registered in document.fonts`); + } + } + } + return Promise.all(toLoad.map(ff => ff.load())); +} + + + +export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { + constructor(public forceInitialRelayout: boolean = true) { } + public dispose(): void { } + + public activate(terminal: Terminal): void { + if (this.forceInitialRelayout) { + document.fonts.ready.then(() => this.relayout(terminal)); + } + } + + /** + * Force a terminal re-layout by altering `options.FontFamily`. + * + * Found webfonts in `fontFamily` are temporarily removed until the webfont + * resources are fully loaded. + * + * This method is meant as a fallback fix for sloppy integrations, + * that wrongly placed a webfont at the terminal contructor options. + * It is likely to lead to terminal flickering in all browsers (FOUT). + * + * To avoid triggering this fallback in your integration, make sure to have + * the needed webfonts loaded at the time `terminal.open` is called. + */ + public relayout(terminal: Terminal): void { + const family = terminal.options.fontFamily; + const families = splitFamily(family); + const webFamilies = WebFontsAddon.getFontFamilies(); + const dirty: string[] = []; + const clean: string[] = []; + for (const fam of families) + (webFamilies.indexOf(fam) !== -1 ? dirty : clean).push(fam); + if (dirty.length) { + _loadFonts(dirty).then(() => { + terminal.options.fontFamily = clean.length ? createFamily(clean) : 'monospace'; + terminal.options.fontFamily = family; + }); + } + } + + /** + * Hash a font face from it properties. + * Used in `loadFonts` to avoid bloating + * `document.fonts` from multiple calls. + */ + public static hashFontFace(ff: FontFace): string { + return JSON.stringify([ + unquote(ff.family), + ff.stretch, + ff.style, + ff.unicodeRange, + ff.weight, + ]) + } + + /** + * Return font families known in `document.fonts`. + */ + public static getFontFamilies(): string[] { + return Array.from(new Set(Array.from(document.fonts).map(e => unquote(e.family)))); + } + + /** + * Wait for webfont resources to be loaded. + * + * Without any argument, all fonts currently listed in + * `document.fonts` will be loaded. + * For a more fine-grained loading strategy you can populate + * the `fonts` argument with: + * - font families : loads all fontfaces in `document.fonts` + * matching the family names + * - fontface objects : loads given fontfaces and adds them to + * `document.fonts` + * + * The returned promise will resolve, when all loading is done. + */ + public static loadFonts(fonts?: (string | FontFace)[]): Promise { + return document.fonts.ready.then(() => _loadFonts(fonts)); + } +} + + + + + + + + +// TODO: place into test cases +/* +(window as any).__roboto = [ + // cyrillic-ext + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2) format('woff2')", + { + style: 'italic', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F' + } + ), + // cyrillic + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2) format('woff2')", + { + style: 'italic', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116' + } + ), + // greek + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2) format('woff2')", + { + style: 'italic', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF' + } + ), + // vietnamese + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2) format('woff2')", + { + style: 'italic', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB' + } + ), + // latin-ext + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2) format('woff2')", + { + style: 'italic', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF' + } + ), + // latin + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2) format('woff2')", + { + style: 'italic', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD' + } + ), + // cyrillic-ext + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2) format('woff2')", + { + style: 'normal', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F' + } + ), + // cyrillic + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2) format('woff2')", + { + style: 'normal', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116' + } + ), + // greek + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2) format('woff2')", + { + style: 'normal', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF' + } + ), + // vietnamese + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2) format('woff2')", + { + style: 'normal', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB' + } + ), + // latin-ext + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2) format('woff2')", + { + style: 'normal', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF' + } + ), + // latin + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2) format('woff2')", + { + style: 'normal', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD' + } + ), +]; +*/ \ No newline at end of file diff --git a/addons/addon-web-fonts/src/tsconfig.json b/addons/addon-web-fonts/src/tsconfig.json new file mode 100644 index 00000000..cd297308 --- /dev/null +++ b/addons/addon-web-fonts/src/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2021", + "lib": [ + "dom", + "es2015", + "dom.iterable" + ], + "rootDir": ".", + "outDir": "../out", + "sourceMap": true, + "removeComments": true, + "strict": true, + "types": [ + "../../../node_modules/@types/mocha" + ], + "paths": { + "@xterm/addon-web-fonts": [ + "../typings/addon-web-fonts.d.ts" + ] + } + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ] +} diff --git a/addons/addon-web-fonts/test/WebLinksAddon.test.ts b/addons/addon-web-fonts/test/WebLinksAddon.test.ts new file mode 100644 index 00000000..8682c533 --- /dev/null +++ b/addons/addon-web-fonts/test/WebLinksAddon.test.ts @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import test from '@playwright/test'; +import { deepStrictEqual, strictEqual } from 'assert'; +import { readFile } from 'fs'; +import { resolve } from 'path'; +import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../test/playwright/TestUtils'; + +interface ILinkStateData { + uri?: string; + range?: { + start: { + x: number; + y: number; + }; + end: { + x: number; + y: number; + }; + }; +} + + +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx, { cols: 40 }); +}); +test.afterAll(async () => await ctx.page.close()); + +test.describe('WebLinksAddon', () => { + + test.beforeEach(async () => { + await ctx.page.evaluate(` + window.term.reset() + window._linkaddon?.dispose(); + window._linkaddon = new WebLinksAddon(); + window.term.loadAddon(window._linkaddon); + `); + }); + + const countryTlds = [ + '.ac', '.ad', '.ae', '.af', '.ag', '.ai', '.al', '.am', '.ao', '.aq', '.ar', '.as', '.at', + '.au', '.aw', '.ax', '.az', '.ba', '.bb', '.bd', '.be', '.bf', '.bg', '.bh', '.bi', '.bj', + '.bm', '.bn', '.bo', '.bq', '.br', '.bs', '.bt', '.bw', '.by', '.bz', '.ca', '.cc', '.cd', + '.cf', '.cg', '.ch', '.ci', '.ck', '.cl', '.cm', '.cn', '.co', '.cr', '.cu', '.cv', '.cw', + '.cx', '.cy', '.cz', '.de', '.dj', '.dk', '.dm', '.do', '.dz', '.ec', '.ee', '.eg', '.eh', + '.er', '.es', '.et', '.eu', '.fi', '.fj', '.fk', '.fm', '.fo', '.fr', '.ga', '.gd', '.ge', + '.gf', '.gg', '.gh', '.gi', '.gl', '.gm', '.gn', '.gp', '.gq', '.gr', '.gs', '.gt', '.gu', + '.gw', '.gy', '.hk', '.hm', '.hn', '.hr', '.ht', '.hu', '.id', '.ie', '.il', '.im', '.in', + '.io', '.iq', '.ir', '.is', '.it', '.je', '.jm', '.jo', '.jp', '.ke', '.kg', '.kh', '.ki', + '.km', '.kn', '.kp', '.kr', '.kw', '.ky', '.kz', '.la', '.lb', '.lc', '.li', '.lk', '.lr', + '.ls', '.lt', '.lu', '.lv', '.ly', '.ma', '.mc', '.md', '.me', '.mg', '.mh', '.mk', '.ml', + '.mm', '.mn', '.mo', '.mp', '.mq', '.mr', '.ms', '.mt', '.mu', '.mv', '.mw', '.mx', '.my', + '.mz', '.na', '.nc', '.ne', '.nf', '.ng', '.ni', '.nl', '.no', '.np', '.nr', '.nu', '.nz', + '.om', '.pa', '.pe', '.pf', '.pg', '.ph', '.pk', '.pl', '.pm', '.pn', '.pr', '.ps', '.pt', + '.pw', '.py', '.qa', '.re', '.ro', '.rs', '.ru', '.rw', '.sa', '.sb', '.sc', '.sd', '.se', + '.sg', '.sh', '.si', '.sk', '.sl', '.sm', '.sn', '.so', '.sr', '.ss', '.st', '.su', '.sv', + '.sx', '.sy', '.sz', '.tc', '.td', '.tf', '.tg', '.th', '.tj', '.tk', '.tl', '.tm', '.tn', + '.to', '.tr', '.tt', '.tv', '.tw', '.tz', '.ua', '.ug', '.uk', '.us', '.uy', '.uz', '.va', + '.vc', '.ve', '.vg', '.vi', '.vn', '.vu', '.wf', '.ws', '.ye', '.yt', '.za', '.zm', '.zw' + ]; + for (const tld of countryTlds) { + test(tld, async () => await testHostName(`foo${tld}`)); + } + test(`.com`, async () => await testHostName(`foo.com`)); + for (const tld of countryTlds) { + test(`.com${tld}`, async () => await testHostName(`foo.com${tld}`)); + } + + test.describe('correct buffer offsets & uri', () => { + test.beforeEach(async () => { + await ctx.page.evaluate(` + window._linkStateData = {uri:''}; + window._linkaddon._options.hover = (event, uri, range) => { window._linkStateData = { uri, range }; }; + `); + }); + test('all half width', async () => { + await ctx.proxy.write('aaa http://example.com aaa http://example.com aaa'); + await resetAndHover(5, 0); + await evalLinkStateData('http://example.com', { start: { x: 5, y: 1 }, end: { x: 22, y: 1 } }); + await resetAndHover(1, 1); + await evalLinkStateData('http://example.com', { start: { x: 28, y: 1 }, end: { x: 5, y: 2 } }); + }); + test('url after full width', async () => { + await ctx.proxy.write('¥¥¥ http://example.com ¥¥¥ http://example.com aaa'); + await resetAndHover(8, 0); + await evalLinkStateData('http://example.com', { start: { x: 8, y: 1 }, end: { x: 25, y: 1 } }); + await resetAndHover(1, 1); + await evalLinkStateData('http://example.com', { start: { x: 34, y: 1 }, end: { x: 11, y: 2 } }); + }); + test('full width within url and before', async () => { + await ctx.proxy.write('¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥'); + await resetAndHover(8, 0); + await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } }); + await resetAndHover(1, 1); + await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } }); + await resetAndHover(17, 1); + await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 17, y: 2 }, end: { x: 19, y: 3 } }); + }); + test('name + password url after full width and combining', async () => { + await ctx.proxy.write('¥¥¥cafe\u0301 http://test:password@example.com/some_path'); + await resetAndHover(12, 0); + await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } }); + await resetAndHover(5, 1); + await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } }); + }); + test('url encoded params work properly', async () => { + await ctx.proxy.write('¥¥¥cafe\u0301 http://test:password@example.com/some_path?param=1%202%3'); + await resetAndHover(12, 0); + await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } }); + await resetAndHover(5, 1); + await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } }); + }); + }); + + // issue #4964 + test('uppercase in protocol and host, default ports', async () => { + await ctx.proxy.write( + ` HTTP://EXAMPLE.COM \r\n` + + ` HTTPS://Example.com \r\n` + + ` HTTP://Example.com:80 \r\n` + + ` HTTP://Example.com:80/staysUpper \r\n` + + ` HTTP://Ab:xY@abc.com:80/staysUpper \r\n` + ); + await pollForLinkAtCell(3, 0, `HTTP://EXAMPLE.COM`); + await pollForLinkAtCell(3, 1, `HTTPS://Example.com`); + await pollForLinkAtCell(3, 2, `HTTP://Example.com:80`); + await pollForLinkAtCell(3, 3, `HTTP://Example.com:80/staysUpper`); + await pollForLinkAtCell(3, 4, `HTTP://Ab:xY@abc.com:80/staysUpper`); + }); +}); + +async function testHostName(hostname: string): Promise { + await ctx.proxy.write( + ` http://${hostname} \r\n` + + ` http://${hostname}/a~b#c~d?e~f \r\n` + + ` http://${hostname}/colon:test \r\n` + + ` http://${hostname}/colon:test: \r\n` + + `"http://${hostname}/"\r\n` + + `\'http://${hostname}/\'\r\n` + + `http://${hostname}/subpath/+/id` + ); + await pollForLinkAtCell(3, 0, `http://${hostname}`); + await pollForLinkAtCell(3, 1, `http://${hostname}/a~b#c~d?e~f`); + await pollForLinkAtCell(3, 2, `http://${hostname}/colon:test`); + await pollForLinkAtCell(3, 3, `http://${hostname}/colon:test`); + await pollForLinkAtCell(2, 4, `http://${hostname}/`); + await pollForLinkAtCell(2, 5, `http://${hostname}/`); + await pollForLinkAtCell(1, 6, `http://${hostname}/subpath/+/id`); +} + +async function pollForLinkAtCell(col: number, row: number, value: string): Promise { + await ctx.page.mouse.move(...(await cellPos(col, row))); + await pollFor(ctx.page, `!!Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').length`, true); + const text = await ctx.page.evaluate(`Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').map(el => el.textContent).join('');`); + deepStrictEqual(text, value); +} + +async function resetAndHover(col: number, row: number): Promise { + await ctx.page.mouse.move(0, 0); + await ctx.page.evaluate(`window._linkStateData = {uri:''};`); + await new Promise(r => setTimeout(r, 200)); + await ctx.page.mouse.move(...(await cellPos(col, row))); + await pollFor(ctx.page, `!!window._linkStateData.uri.length`, true); +} + +async function evalLinkStateData(uri: string, range: any): Promise { + const data: ILinkStateData = await ctx.page.evaluate(`window._linkStateData`); + strictEqual(data.uri, uri); + deepStrictEqual(data.range, range); +} + +async function cellPos(col: number, row: number): Promise<[number, number]> { + const coords: any = await ctx.page.evaluate(` + (function() { + const rect = window.term.element.getBoundingClientRect(); + const dim = term._core._renderService.dimensions; + return {left: rect.left, top: rect.top, bottom: rect.bottom, right: rect.right, width: dim.css.cell.width, height: dim.css.cell.height}; + })(); + `); + return [col * coords.width + coords.left + 2, row * coords.height + coords.top + 2]; +} diff --git a/addons/addon-web-fonts/test/playwright.config.ts b/addons/addon-web-fonts/test/playwright.config.ts new file mode 100644 index 00000000..22834be1 --- /dev/null +++ b/addons/addon-web-fonts/test/playwright.config.ts @@ -0,0 +1,35 @@ +import { PlaywrightTestConfig } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + testDir: '.', + timeout: 10000, + projects: [ + { + name: 'ChromeStable', + use: { + browserName: 'chromium', + channel: 'chrome' + } + }, + { + name: 'FirefoxStable', + use: { + browserName: 'firefox' + } + }, + { + name: 'WebKit', + use: { + browserName: 'webkit' + } + } + ], + reporter: 'list', + webServer: { + command: 'npm run start', + port: 3000, + timeout: 120000, + reuseExistingServer: !process.env.CI + } +}; +export default config; diff --git a/addons/addon-web-fonts/test/tsconfig.json b/addons/addon-web-fonts/test/tsconfig.json new file mode 100644 index 00000000..120fccdc --- /dev/null +++ b/addons/addon-web-fonts/test/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2021", + "lib": [ + "es2021", + ], + // "downlevelIteration": true, + "rootDir": ".", + "outDir": "../out-test", + "sourceMap": true, + "removeComments": true, + "baseUrl": ".", + "paths": { + "common/*": [ + "../../../src/common/*" + ], + "browser/*": [ + "../../../src/browser/*" + ] + }, + "strict": true, + "types": [ + "../../../node_modules/@types/node" + ] + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../../src/common" + }, + { + "path": "../../../src/browser" + }, + { + "path": "../../../test/playwright" + } + ] +} diff --git a/addons/addon-web-fonts/tsconfig.json b/addons/addon-web-fonts/tsconfig.json new file mode 100644 index 00000000..2d820dd1 --- /dev/null +++ b/addons/addon-web-fonts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "./src" }, + { "path": "./test" } + ] +} diff --git a/addons/addon-web-fonts/typings/addon-web-fonts.d.ts b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts new file mode 100644 index 00000000..42cab9b6 --- /dev/null +++ b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + + +import { Terminal, ITerminalAddon, IViewportRange } from '@xterm/xterm'; + +declare module '@xterm/addon-web-fonts' { + /** + * An xterm.js addon that enables web links. + */ + export class WebFontsAddon implements ITerminalAddon { + constructor(); + public activate(terminal: Terminal): void; + public dispose(): void; + } +} diff --git a/addons/addon-web-fonts/webpack.config.js b/addons/addon-web-fonts/webpack.config.js new file mode 100644 index 00000000..c73a60ba --- /dev/null +++ b/addons/addon-web-fonts/webpack.config.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'WebFontsAddon'; +const mainFile = 'addon-web-fonts.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd', + // Force usage of globalThis instead of global / self. (This is cross-env compatible) + globalObject: 'globalThis', + }, + mode: 'production' +}; diff --git a/demo/client.ts b/demo/client.ts index 49ba6743..d125bfee 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -23,6 +23,7 @@ import { FitAddon } from '@xterm/addon-fit'; import { LigaturesAddon } from '@xterm/addon-ligatures'; import { SearchAddon, ISearchOptions } from '@xterm/addon-search'; import { SerializeAddon } from '@xterm/addon-serialize'; +import { WebFontsAddon } from '@xterm/addon-web-fonts'; import { WebLinksAddon } from '@xterm/addon-web-links'; import { WebglAddon } from '@xterm/addon-webgl'; import { Unicode11Addon } from '@xterm/addon-unicode11'; @@ -37,6 +38,7 @@ export interface IWindowWithTerminal extends Window { ImageAddon?: typeof ImageAddon; // 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 + WebFontsAddon?: typeof WebFontsAddon; // 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 @@ -52,7 +54,7 @@ let socket; let pid; let autoResize: boolean = true; -type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webLinks' | 'webgl' | 'ligatures'; +type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl' | 'ligatures'; interface IDemoAddon { name: T; @@ -65,11 +67,12 @@ interface IDemoAddon { T extends 'ligatures' ? typeof LigaturesAddon : T extends 'search' ? typeof SearchAddon : T extends 'serialize' ? typeof SerializeAddon : - T extends 'webLinks' ? typeof WebLinksAddon : - T extends 'unicode11' ? typeof Unicode11Addon : - T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : - T extends 'webgl' ? typeof WebglAddon : - never + T extends 'webFonts' ? typeof WebFontsAddon : + T extends 'webLinks' ? typeof WebLinksAddon : + T extends 'unicode11' ? typeof Unicode11Addon : + T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : + T extends 'webgl' ? typeof WebglAddon : + never ); instance?: ( T extends 'attach' ? AttachAddon : @@ -79,11 +82,12 @@ interface IDemoAddon { T extends 'ligatures' ? LigaturesAddon : T extends 'search' ? SearchAddon : T extends 'serialize' ? SerializeAddon : - T extends 'webLinks' ? WebLinksAddon : - T extends 'unicode11' ? Unicode11Addon : - T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : - T extends 'webgl' ? WebglAddon : - never + T extends 'webFonts' ? WebFontsAddon : + T extends 'webLinks' ? WebLinksAddon : + T extends 'unicode11' ? Unicode11Addon : + T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : + T extends 'webgl' ? WebglAddon : + never ); } @@ -94,6 +98,7 @@ const addons: { [T in AddonType]: IDemoAddon } = { image: { name: 'image', ctor: ImageAddon, canChange: true }, search: { name: 'search', ctor: SearchAddon, canChange: true }, serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true }, + webFonts: { name: 'webFonts', ctor: WebFontsAddon, canChange: true }, webLinks: { name: 'webLinks', ctor: WebLinksAddon, canChange: true }, webgl: { name: 'webgl', ctor: WebglAddon, canChange: true }, unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true }, @@ -169,6 +174,7 @@ const disposeRecreateButtonHandler: () => void = () => { addons.unicode11.instance = undefined; addons.unicodeGraphemes.instance = undefined; addons.ligatures.instance = undefined; + addons.webFonts.instance = undefined; addons.webLinks.instance = undefined; addons.webgl.instance = undefined; document.getElementById('dispose').innerHTML = 'Recreate Terminal'; @@ -218,6 +224,7 @@ if (document.location.pathname === '/test') { window.Unicode11Addon = Unicode11Addon; window.UnicodeGraphemesAddon = UnicodeGraphemesAddon; window.LigaturesAddon = LigaturesAddon; + window.WebFontsAddon = WebFontsAddon; window.WebLinksAddon = WebLinksAddon; window.WebglAddon = WebglAddon; } else { @@ -261,7 +268,7 @@ function createTerminal(): void { backend: 'conpty', buildNumber: 22621 } : undefined, - fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', + fontFamily: '"Roboto Mono", "Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', theme: xtermjsTheme } as ITerminalOptions); @@ -278,12 +285,14 @@ function createTerminal(): void { } catch (e) { console.warn(e); } + addons.webFonts.instance = new WebFontsAddon(); addons.webLinks.instance = new WebLinksAddon(); typedTerm.loadAddon(addons.fit.instance); typedTerm.loadAddon(addons.image.instance); typedTerm.loadAddon(addons.search.instance); typedTerm.loadAddon(addons.serialize.instance); typedTerm.loadAddon(addons.unicodeGraphemes.instance); + typedTerm.loadAddon(addons.webFonts.instance); typedTerm.loadAddon(addons.webLinks.instance); typedTerm.loadAddon(addons.clipboard.instance); diff --git a/demo/style.css b/demo/style.css index 1fb8f7ac..3e32b42f 100644 --- a/demo/style.css +++ b/demo/style.css @@ -1,3 +1,115 @@ +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2) format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2) format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + + + + body { font-family: helvetica, sans-serif, arial; font-size: 1em; diff --git a/demo/tsconfig.json b/demo/tsconfig.json index 5569bd1d..db8939fa 100644 --- a/demo/tsconfig.json +++ b/demo/tsconfig.json @@ -12,6 +12,7 @@ "@xterm/addon-image": ["../addons/addon-image"], "@xterm/addon-search": ["../addons/addon-search"], "@xterm/addon-serialize": ["../addons/addon-serialize"], + "@xterm/addon-web-fonts": ["../addons/addon-web-fonts"], "@xterm/addon-web-links": ["../addons/addon-web-links"], "@xterm/addon-webgl": ["../addons/addon-webgl"], "@xterm/addon-unicode11": ["../addons/addon-unicode11"], diff --git a/tsconfig.all.json b/tsconfig.all.json index 7ca3b7a7..9bc8896e 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -15,6 +15,7 @@ { "path": "./addons/addon-serialize" }, { "path": "./addons/addon-unicode11" }, { "path": "./addons/addon-unicode-graphemes" }, + { "path": "./addons/addon-web-fonts" }, { "path": "./addons/addon-web-links" }, { "path": "./addons/addon-webgl" } ] From 41640d94022b21710689de95fcce65f770884b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 3 Oct 2024 22:07:10 +0200 Subject: [PATCH 02/19] make linter happy --- addons/addon-web-fonts/src/WebFontsAddon.ts | 25 +++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/addons/addon-web-fonts/src/WebFontsAddon.ts b/addons/addon-web-fonts/src/WebFontsAddon.ts index 91800abd..3cce5092 100644 --- a/addons/addon-web-fonts/src/WebFontsAddon.ts +++ b/addons/addon-web-fonts/src/WebFontsAddon.ts @@ -12,7 +12,7 @@ import type { WebFontsAddon as IWebFontsApi } from '@xterm/addon-web-fonts'; */ function unquote(s: string): string { if (s[0] === '"' && s[s.length - 1] === '"') return s.slice(1, -1); - if (s[0] === "'" && s[s.length - 1] === "'") return s.slice(1, -1); + if (s[0] === '\'' && s[s.length - 1] === '\'') return s.slice(1, -1); return s; } @@ -41,12 +41,12 @@ function createFamily(families: string[]): string { function _loadFonts(fonts?: (string | FontFace)[]): Promise { - let ffs = Array.from(document.fonts); + const ffs = Array.from(document.fonts); if (!fonts || !fonts.length) { return Promise.all(ffs.map(ff => ff.load())); } let toLoad: FontFace[] = []; - let ffsHashed = ffs.map(ff => WebFontsAddon.hashFontFace(ff)); + const ffsHashed = ffs.map(ff => WebFontsAddon.hashFontFace(ff)); for (const font of fonts) { if (font instanceof FontFace) { const fontHashed = WebFontsAddon.hashFontFace(font); @@ -85,14 +85,14 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { /** * Force a terminal re-layout by altering `options.FontFamily`. - * + * * Found webfonts in `fontFamily` are temporarily removed until the webfont * resources are fully loaded. - * + * * This method is meant as a fallback fix for sloppy integrations, * that wrongly placed a webfont at the terminal contructor options. * It is likely to lead to terminal flickering in all browsers (FOUT). - * + * * To avoid triggering this fallback in your integration, make sure to have * the needed webfonts loaded at the time `terminal.open` is called. */ @@ -102,8 +102,9 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { const webFamilies = WebFontsAddon.getFontFamilies(); const dirty: string[] = []; const clean: string[] = []; - for (const fam of families) + for (const fam of families) { (webFamilies.indexOf(fam) !== -1 ? dirty : clean).push(fam); + } if (dirty.length) { _loadFonts(dirty).then(() => { terminal.options.fontFamily = clean.length ? createFamily(clean) : 'monospace'; @@ -123,8 +124,8 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { ff.stretch, ff.style, ff.unicodeRange, - ff.weight, - ]) + ff.weight + ]); } /** @@ -145,7 +146,7 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { * matching the family names * - fontface objects : loads given fontfaces and adds them to * `document.fonts` - * + * * The returned promise will resolve, when all loading is done. */ public static loadFonts(fonts?: (string | FontFace)[]): Promise { @@ -159,7 +160,7 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { - +/* eslint-disable */ // TODO: place into test cases /* (window as any).__roboto = [ @@ -296,4 +297,4 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { } ), ]; -*/ \ No newline at end of file +*/ From 15a63bae228eb8f17d72e5c8db43c2b819298673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 3 Oct 2024 22:10:40 +0200 Subject: [PATCH 03/19] change to global replacement --- addons/addon-web-fonts/src/WebFontsAddon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/addon-web-fonts/src/WebFontsAddon.ts b/addons/addon-web-fonts/src/WebFontsAddon.ts index 3cce5092..e3132602 100644 --- a/addons/addon-web-fonts/src/WebFontsAddon.ts +++ b/addons/addon-web-fonts/src/WebFontsAddon.ts @@ -25,7 +25,7 @@ function quote(s: string): string { const pos = s.match(/([-_a-zA-Z0-9\xA0-\u{10FFFF}]+)/u); const neg = s.match(/^(-?\d|--)/m); if (!neg && pos && pos[1] === s) return s; - return `"${s.replace('"', '\\"')}"`; + return `"${s.replace(/"/g, '\\"')}"`; } From 9a536f758d5258ac690d5f96fa35f491759c132c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 3 Oct 2024 22:23:31 +0200 Subject: [PATCH 04/19] fix integration test runner --- bin/esbuild.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/esbuild.mjs b/bin/esbuild.mjs index 9ed69eda..95b3856e 100644 --- a/bin/esbuild.mjs +++ b/bin/esbuild.mjs @@ -138,6 +138,7 @@ if (config.addon) { "@xterm/addon-image": "./addons/addon-image/lib/addon-image.mjs", "@xterm/addon-search": "./addons/addon-search/lib/addon-search.mjs", "@xterm/addon-serialize": "./addons/addon-serialize/lib/addon-serialize.mjs", + "@xterm/addon-web-fonts": "./addons/addon-web-fonts/lib/addon-web-fonts.mjs", "@xterm/addon-web-links": "./addons/addon-web-links/lib/addon-web-links.mjs", "@xterm/addon-webgl": "./addons/addon-webgl/lib/addon-webgl.mjs", "@xterm/addon-unicode11": "./addons/addon-unicode11/lib/addon-unicode11.mjs", From 809b9aa1ad0ba1083cba9935cdc0fd73fa743574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 00:15:49 +0200 Subject: [PATCH 05/19] fix test runner --- .github/workflows/ci.yml | 3 + .../test/WebFontsAddon.test.ts | 19 ++ .../test/WebLinksAddon.test.ts | 185 ------------------ bin/test_integration.js | 1 + 4 files changed, 23 insertions(+), 185 deletions(-) create mode 100644 addons/addon-web-fonts/test/WebFontsAddon.test.ts delete mode 100644 addons/addon-web-fonts/test/WebLinksAddon.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2f39e6b..eb9066d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,9 @@ jobs: ./addons/addon-web-links/lib/* \ ./addons/addon-web-links/out/* \ ./addons/addon-web-links/out-*/* \ + ./addons/addon-web-fonts/lib/* \ + ./addons/addon-web-fonts/out/* \ + ./addons/addon-web-fonts/out-*/* \ ./addons/addon-webgl/lib/* \ ./addons/addon-webgl/out/* \ ./addons/addon-webgl/out-*st/* diff --git a/addons/addon-web-fonts/test/WebFontsAddon.test.ts b/addons/addon-web-fonts/test/WebFontsAddon.test.ts new file mode 100644 index 00000000..09bb01a9 --- /dev/null +++ b/addons/addon-web-fonts/test/WebFontsAddon.test.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2024 The xterm.js authors. All rights reserved. + * @license MIT + */ +import test from '@playwright/test'; +import { deepStrictEqual, strictEqual } from 'assert'; +import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../test/playwright/TestUtils'; + + +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx, { cols: 40 }); +}); +test.afterAll(async () => await ctx.page.close()); + +test.describe('WebFontsAddon', () => { + test('nothing', () => {}); +}); diff --git a/addons/addon-web-fonts/test/WebLinksAddon.test.ts b/addons/addon-web-fonts/test/WebLinksAddon.test.ts deleted file mode 100644 index 8682c533..00000000 --- a/addons/addon-web-fonts/test/WebLinksAddon.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ -import test from '@playwright/test'; -import { deepStrictEqual, strictEqual } from 'assert'; -import { readFile } from 'fs'; -import { resolve } from 'path'; -import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../test/playwright/TestUtils'; - -interface ILinkStateData { - uri?: string; - range?: { - start: { - x: number; - y: number; - }; - end: { - x: number; - y: number; - }; - }; -} - - -let ctx: ITestContext; -test.beforeAll(async ({ browser }) => { - ctx = await createTestContext(browser); - await openTerminal(ctx, { cols: 40 }); -}); -test.afterAll(async () => await ctx.page.close()); - -test.describe('WebLinksAddon', () => { - - test.beforeEach(async () => { - await ctx.page.evaluate(` - window.term.reset() - window._linkaddon?.dispose(); - window._linkaddon = new WebLinksAddon(); - window.term.loadAddon(window._linkaddon); - `); - }); - - const countryTlds = [ - '.ac', '.ad', '.ae', '.af', '.ag', '.ai', '.al', '.am', '.ao', '.aq', '.ar', '.as', '.at', - '.au', '.aw', '.ax', '.az', '.ba', '.bb', '.bd', '.be', '.bf', '.bg', '.bh', '.bi', '.bj', - '.bm', '.bn', '.bo', '.bq', '.br', '.bs', '.bt', '.bw', '.by', '.bz', '.ca', '.cc', '.cd', - '.cf', '.cg', '.ch', '.ci', '.ck', '.cl', '.cm', '.cn', '.co', '.cr', '.cu', '.cv', '.cw', - '.cx', '.cy', '.cz', '.de', '.dj', '.dk', '.dm', '.do', '.dz', '.ec', '.ee', '.eg', '.eh', - '.er', '.es', '.et', '.eu', '.fi', '.fj', '.fk', '.fm', '.fo', '.fr', '.ga', '.gd', '.ge', - '.gf', '.gg', '.gh', '.gi', '.gl', '.gm', '.gn', '.gp', '.gq', '.gr', '.gs', '.gt', '.gu', - '.gw', '.gy', '.hk', '.hm', '.hn', '.hr', '.ht', '.hu', '.id', '.ie', '.il', '.im', '.in', - '.io', '.iq', '.ir', '.is', '.it', '.je', '.jm', '.jo', '.jp', '.ke', '.kg', '.kh', '.ki', - '.km', '.kn', '.kp', '.kr', '.kw', '.ky', '.kz', '.la', '.lb', '.lc', '.li', '.lk', '.lr', - '.ls', '.lt', '.lu', '.lv', '.ly', '.ma', '.mc', '.md', '.me', '.mg', '.mh', '.mk', '.ml', - '.mm', '.mn', '.mo', '.mp', '.mq', '.mr', '.ms', '.mt', '.mu', '.mv', '.mw', '.mx', '.my', - '.mz', '.na', '.nc', '.ne', '.nf', '.ng', '.ni', '.nl', '.no', '.np', '.nr', '.nu', '.nz', - '.om', '.pa', '.pe', '.pf', '.pg', '.ph', '.pk', '.pl', '.pm', '.pn', '.pr', '.ps', '.pt', - '.pw', '.py', '.qa', '.re', '.ro', '.rs', '.ru', '.rw', '.sa', '.sb', '.sc', '.sd', '.se', - '.sg', '.sh', '.si', '.sk', '.sl', '.sm', '.sn', '.so', '.sr', '.ss', '.st', '.su', '.sv', - '.sx', '.sy', '.sz', '.tc', '.td', '.tf', '.tg', '.th', '.tj', '.tk', '.tl', '.tm', '.tn', - '.to', '.tr', '.tt', '.tv', '.tw', '.tz', '.ua', '.ug', '.uk', '.us', '.uy', '.uz', '.va', - '.vc', '.ve', '.vg', '.vi', '.vn', '.vu', '.wf', '.ws', '.ye', '.yt', '.za', '.zm', '.zw' - ]; - for (const tld of countryTlds) { - test(tld, async () => await testHostName(`foo${tld}`)); - } - test(`.com`, async () => await testHostName(`foo.com`)); - for (const tld of countryTlds) { - test(`.com${tld}`, async () => await testHostName(`foo.com${tld}`)); - } - - test.describe('correct buffer offsets & uri', () => { - test.beforeEach(async () => { - await ctx.page.evaluate(` - window._linkStateData = {uri:''}; - window._linkaddon._options.hover = (event, uri, range) => { window._linkStateData = { uri, range }; }; - `); - }); - test('all half width', async () => { - await ctx.proxy.write('aaa http://example.com aaa http://example.com aaa'); - await resetAndHover(5, 0); - await evalLinkStateData('http://example.com', { start: { x: 5, y: 1 }, end: { x: 22, y: 1 } }); - await resetAndHover(1, 1); - await evalLinkStateData('http://example.com', { start: { x: 28, y: 1 }, end: { x: 5, y: 2 } }); - }); - test('url after full width', async () => { - await ctx.proxy.write('¥¥¥ http://example.com ¥¥¥ http://example.com aaa'); - await resetAndHover(8, 0); - await evalLinkStateData('http://example.com', { start: { x: 8, y: 1 }, end: { x: 25, y: 1 } }); - await resetAndHover(1, 1); - await evalLinkStateData('http://example.com', { start: { x: 34, y: 1 }, end: { x: 11, y: 2 } }); - }); - test('full width within url and before', async () => { - await ctx.proxy.write('¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥'); - await resetAndHover(8, 0); - await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } }); - await resetAndHover(1, 1); - await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } }); - await resetAndHover(17, 1); - await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 17, y: 2 }, end: { x: 19, y: 3 } }); - }); - test('name + password url after full width and combining', async () => { - await ctx.proxy.write('¥¥¥cafe\u0301 http://test:password@example.com/some_path'); - await resetAndHover(12, 0); - await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } }); - await resetAndHover(5, 1); - await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } }); - }); - test('url encoded params work properly', async () => { - await ctx.proxy.write('¥¥¥cafe\u0301 http://test:password@example.com/some_path?param=1%202%3'); - await resetAndHover(12, 0); - await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } }); - await resetAndHover(5, 1); - await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } }); - }); - }); - - // issue #4964 - test('uppercase in protocol and host, default ports', async () => { - await ctx.proxy.write( - ` HTTP://EXAMPLE.COM \r\n` + - ` HTTPS://Example.com \r\n` + - ` HTTP://Example.com:80 \r\n` + - ` HTTP://Example.com:80/staysUpper \r\n` + - ` HTTP://Ab:xY@abc.com:80/staysUpper \r\n` - ); - await pollForLinkAtCell(3, 0, `HTTP://EXAMPLE.COM`); - await pollForLinkAtCell(3, 1, `HTTPS://Example.com`); - await pollForLinkAtCell(3, 2, `HTTP://Example.com:80`); - await pollForLinkAtCell(3, 3, `HTTP://Example.com:80/staysUpper`); - await pollForLinkAtCell(3, 4, `HTTP://Ab:xY@abc.com:80/staysUpper`); - }); -}); - -async function testHostName(hostname: string): Promise { - await ctx.proxy.write( - ` http://${hostname} \r\n` + - ` http://${hostname}/a~b#c~d?e~f \r\n` + - ` http://${hostname}/colon:test \r\n` + - ` http://${hostname}/colon:test: \r\n` + - `"http://${hostname}/"\r\n` + - `\'http://${hostname}/\'\r\n` + - `http://${hostname}/subpath/+/id` - ); - await pollForLinkAtCell(3, 0, `http://${hostname}`); - await pollForLinkAtCell(3, 1, `http://${hostname}/a~b#c~d?e~f`); - await pollForLinkAtCell(3, 2, `http://${hostname}/colon:test`); - await pollForLinkAtCell(3, 3, `http://${hostname}/colon:test`); - await pollForLinkAtCell(2, 4, `http://${hostname}/`); - await pollForLinkAtCell(2, 5, `http://${hostname}/`); - await pollForLinkAtCell(1, 6, `http://${hostname}/subpath/+/id`); -} - -async function pollForLinkAtCell(col: number, row: number, value: string): Promise { - await ctx.page.mouse.move(...(await cellPos(col, row))); - await pollFor(ctx.page, `!!Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').length`, true); - const text = await ctx.page.evaluate(`Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').map(el => el.textContent).join('');`); - deepStrictEqual(text, value); -} - -async function resetAndHover(col: number, row: number): Promise { - await ctx.page.mouse.move(0, 0); - await ctx.page.evaluate(`window._linkStateData = {uri:''};`); - await new Promise(r => setTimeout(r, 200)); - await ctx.page.mouse.move(...(await cellPos(col, row))); - await pollFor(ctx.page, `!!window._linkStateData.uri.length`, true); -} - -async function evalLinkStateData(uri: string, range: any): Promise { - const data: ILinkStateData = await ctx.page.evaluate(`window._linkStateData`); - strictEqual(data.uri, uri); - deepStrictEqual(data.range, range); -} - -async function cellPos(col: number, row: number): Promise<[number, number]> { - const coords: any = await ctx.page.evaluate(` - (function() { - const rect = window.term.element.getBoundingClientRect(); - const dim = term._core._renderService.dimensions; - return {left: rect.left, top: rect.top, bottom: rect.bottom, right: rect.right, width: dim.css.cell.width, height: dim.css.cell.height}; - })(); - `); - return [col * coords.width + coords.left + 2, row * coords.height + coords.top + 2]; -} diff --git a/bin/test_integration.js b/bin/test_integration.js index 936467cc..b7773e57 100644 --- a/bin/test_integration.js +++ b/bin/test_integration.js @@ -29,6 +29,7 @@ const addons = [ 'serialize', 'unicode-graphemes', 'unicode11', + 'web-fonts', 'web-links', 'webgl', ]; From b104bcd144b2816fa1bc7b464a502c4713a119f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 00:42:38 +0200 Subject: [PATCH 06/19] remove await remnants --- addons/addon-web-fonts/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/addon-web-fonts/README.md b/addons/addon-web-fonts/README.md index bf506cf1..bd1dedf4 100644 --- a/addons/addon-web-fonts/README.md +++ b/addons/addon-web-fonts/README.md @@ -72,7 +72,7 @@ const webFontsAddon = new WebFontsAddon(); terminal.loadAddon(webFontsAddon); // wait for webfonts to be fully loaded -await WebFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { +WebFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { terminal.open(your_terminal_div_element); // more boostrapping goes here ... }); @@ -112,7 +112,7 @@ That can be achieved like this: const ff1 = new FontFace('New Web Mono', url1, ...); const ff2 = new FontFace('New Web Mono', url2, ...); // and await their loading -await WebFontsAddon.loadFonts([ff1, ff2]).then(() => { +WebFontsAddon.loadFonts([ff1, ff2]).then(() => { // apply new webfont to terminal terminal.options.fontFamily = 'New Web Mono'; // since the new font might have slighly different metrics, @@ -124,7 +124,7 @@ await WebFontsAddon.loadFonts([ff1, ff2]).then(() => { document.styleSheets[0].insertRule( "@font-face { font-family: 'New Web Mono'; src: url(newfont.woff); }", 0); // and await the new font family name -await WebFontsAddon.loadFonts(['New Web Mono']).then(() => { +WebFontsAddon.loadFonts(['New Web Mono']).then(() => { // apply new webfont to terminal terminal.options.fontFamily = 'New Web Mono'; // since the new font might have slighly different metrics, From 23bef9913290707e65323ad1ee620ebe0c310e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 02:06:02 +0200 Subject: [PATCH 07/19] cleanup API --- .github/workflows/ci.yml | 2 + addons/addon-web-fonts/README.md | 12 +- addons/addon-web-fonts/src/WebFontsAddon.ts | 136 +++++++++--------- .../typings/addon-web-fonts.d.ts | 55 ++++++- 4 files changed, 127 insertions(+), 78 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb9066d9..f7616b7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -223,6 +223,8 @@ jobs: run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode-graphemes - name: Integration tests (addon-unicode11) run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode11 + - name: Integration tests (addon-web-fonts) + run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-fonts - name: Integration tests (addon-web-links) run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-links - name: Integration tests (addon-webgl) diff --git a/addons/addon-web-fonts/README.md b/addons/addon-web-fonts/README.md index bd1dedf4..e910c21e 100644 --- a/addons/addon-web-fonts/README.md +++ b/addons/addon-web-fonts/README.md @@ -72,7 +72,7 @@ const webFontsAddon = new WebFontsAddon(); terminal.loadAddon(webFontsAddon); // wait for webfonts to be fully loaded -WebFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { +webFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { terminal.open(your_terminal_div_element); // more boostrapping goes here ... }); @@ -83,13 +83,13 @@ on the initial document load (more precise - by the time this code runs). Please note, that this code cannot run synchronous anymore, so you will have to split your bootstrapping code into several stages. If thats too much of a hassle, you can also move the whole -bootstrapping under that waiting condition (`loadFonts` is actually a static method): +bootstrapping under that waiting condition (import `loadFonts` for a static variant): ```typescript import { Terminal } from '@xterm/xterm'; import { XYAddon } from '@xterm/addon-xy'; -import { WebFontsAddon } from '@xterm/addon-web-fonts'; +import { WebFontsAddon, loadFonts } from '@xterm/addon-web-fonts'; -WebFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { +loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { // create a `Terminal` instance, now with webfonts const terminal = new Terminal({fontFamily: '"Web Mono 1", "Super Powerline", monospace'}); const xyAddon = new XYAddon(); @@ -112,7 +112,7 @@ That can be achieved like this: const ff1 = new FontFace('New Web Mono', url1, ...); const ff2 = new FontFace('New Web Mono', url2, ...); // and await their loading -WebFontsAddon.loadFonts([ff1, ff2]).then(() => { +loadFonts([ff1, ff2]).then(() => { // apply new webfont to terminal terminal.options.fontFamily = 'New Web Mono'; // since the new font might have slighly different metrics, @@ -124,7 +124,7 @@ WebFontsAddon.loadFonts([ff1, ff2]).then(() => { document.styleSheets[0].insertRule( "@font-face { font-family: 'New Web Mono'; src: url(newfont.woff); }", 0); // and await the new font family name -WebFontsAddon.loadFonts(['New Web Mono']).then(() => { +loadFonts(['New Web Mono']).then(() => { // apply new webfont to terminal terminal.options.fontFamily = 'New Web Mono'; // since the new font might have slighly different metrics, diff --git a/addons/addon-web-fonts/src/WebFontsAddon.ts b/addons/addon-web-fonts/src/WebFontsAddon.ts index e3132602..219a64af 100644 --- a/addons/addon-web-fonts/src/WebFontsAddon.ts +++ b/addons/addon-web-fonts/src/WebFontsAddon.ts @@ -8,7 +8,7 @@ import type { WebFontsAddon as IWebFontsApi } from '@xterm/addon-web-fonts'; /** - * Unquote family name. + * Unquote a font family name. */ function unquote(s: string): string { if (s[0] === '"' && s[s.length - 1] === '"') return s.slice(1, -1); @@ -18,7 +18,7 @@ function unquote(s: string): string { /** - * Quote family name. + * Quote a font family name conditionally. * @see https://mathiasbynens.be/notes/unquoted-font-family */ function quote(s: string): string { @@ -40,16 +40,46 @@ function createFamily(families: string[]): string { } +/** + * Hash a font face from it properties. + * Used in `loadFonts` to avoid bloating + * `document.fonts` from multiple calls. + */ +function hashFontFace(ff: FontFace): string { + return JSON.stringify([ + unquote(ff.family), + ff.stretch, + ff.style, + ff.unicodeRange, + ff.weight + ]); +} + + +/** + * Wait for webfont resources to be loaded. + * + * Without any argument, all fonts currently listed in + * `document.fonts` will be loaded. + * For a more fine-grained loading strategy you can populate + * the `fonts` argument with: + * - font families : loads all fontfaces in `document.fonts` + * matching the family names + * - fontface objects : loads given fontfaces and adds them to + * `document.fonts` + * + * The returned promise will resolve, when all loading is done. + */ function _loadFonts(fonts?: (string | FontFace)[]): Promise { const ffs = Array.from(document.fonts); if (!fonts || !fonts.length) { return Promise.all(ffs.map(ff => ff.load())); } let toLoad: FontFace[] = []; - const ffsHashed = ffs.map(ff => WebFontsAddon.hashFontFace(ff)); + const ffsHashed = ffs.map(ff => hashFontFace(ff)); for (const font of fonts) { if (font instanceof FontFace) { - const fontHashed = WebFontsAddon.hashFontFace(font); + const fontHashed = hashFontFace(font); const idx = ffsHashed.indexOf(fontHashed); if (idx === -1) { document.fonts.add(font); @@ -72,85 +102,53 @@ function _loadFonts(fonts?: (string | FontFace)[]): Promise { } +export async function loadFonts(fonts?: (string | FontFace)[]): Promise { + await document.fonts.ready; + return _loadFonts(fonts); +} + export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { - constructor(public forceInitialRelayout: boolean = true) { } - public dispose(): void { } + private _term: Terminal | undefined; - public activate(terminal: Terminal): void { + constructor(public forceInitialRelayout: boolean = true) { } + + public dispose(): void { + this._term = undefined; + } + + public activate(term: Terminal): void { + this._term = term; if (this.forceInitialRelayout) { - document.fonts.ready.then(() => this.relayout(terminal)); + document.fonts.ready.then(() => this.relayout()); } } - /** - * Force a terminal re-layout by altering `options.FontFamily`. - * - * Found webfonts in `fontFamily` are temporarily removed until the webfont - * resources are fully loaded. - * - * This method is meant as a fallback fix for sloppy integrations, - * that wrongly placed a webfont at the terminal contructor options. - * It is likely to lead to terminal flickering in all browsers (FOUT). - * - * To avoid triggering this fallback in your integration, make sure to have - * the needed webfonts loaded at the time `terminal.open` is called. - */ - public relayout(terminal: Terminal): void { - const family = terminal.options.fontFamily; + public async loadFonts(fonts?: (string | FontFace)[]): Promise { + return loadFonts(fonts); + } + + public async relayout(): Promise { + if (!this._term) { + return; + } + await document.fonts.ready; + const family = this._term.options.fontFamily; const families = splitFamily(family); - const webFamilies = WebFontsAddon.getFontFamilies(); + const webFamilies = Array.from(new Set(Array.from(document.fonts).map(e => unquote(e.family)))); const dirty: string[] = []; const clean: string[] = []; for (const fam of families) { (webFamilies.indexOf(fam) !== -1 ? dirty : clean).push(fam); } - if (dirty.length) { - _loadFonts(dirty).then(() => { - terminal.options.fontFamily = clean.length ? createFamily(clean) : 'monospace'; - terminal.options.fontFamily = family; - }); + if (!dirty.length) { + return; + } + await _loadFonts(dirty); + if (this._term) { + this._term.options.fontFamily = clean.length ? createFamily(clean) : 'monospace'; + this._term.options.fontFamily = family; } - } - - /** - * Hash a font face from it properties. - * Used in `loadFonts` to avoid bloating - * `document.fonts` from multiple calls. - */ - public static hashFontFace(ff: FontFace): string { - return JSON.stringify([ - unquote(ff.family), - ff.stretch, - ff.style, - ff.unicodeRange, - ff.weight - ]); - } - - /** - * Return font families known in `document.fonts`. - */ - public static getFontFamilies(): string[] { - return Array.from(new Set(Array.from(document.fonts).map(e => unquote(e.family)))); - } - - /** - * Wait for webfont resources to be loaded. - * - * Without any argument, all fonts currently listed in - * `document.fonts` will be loaded. - * For a more fine-grained loading strategy you can populate - * the `fonts` argument with: - * - font families : loads all fontfaces in `document.fonts` - * matching the family names - * - fontface objects : loads given fontfaces and adds them to - * `document.fonts` - * - * The returned promise will resolve, when all loading is done. - */ - public static loadFonts(fonts?: (string | FontFace)[]): Promise { - return document.fonts.ready.then(() => _loadFonts(fonts)); } } diff --git a/addons/addon-web-fonts/typings/addon-web-fonts.d.ts b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts index 42cab9b6..c353a421 100644 --- a/addons/addon-web-fonts/typings/addon-web-fonts.d.ts +++ b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts @@ -4,15 +4,64 @@ */ -import { Terminal, ITerminalAddon, IViewportRange } from '@xterm/xterm'; +import { Terminal, ITerminalAddon } from '@xterm/xterm'; declare module '@xterm/addon-web-fonts' { + /** - * An xterm.js addon that enables web links. + * Addon to use webfonts in xterm.js */ export class WebFontsAddon implements ITerminalAddon { - constructor(); + /** + * @param forceInitialRelayout Force an initial relayout, if a webfont was found. + */ + constructor(forceInitialRelayout?: boolean); public activate(terminal: Terminal): void; public dispose(): void; + + /** + * Wait for webfont resources to be loaded. + * + * Without any argument, all fonts currently listed in + * `document.fonts` will be loaded. + * For a more fine-grained loading strategy you can populate + * the `fonts` argument with: + * - font families : loads all fontfaces in `document.fonts` + * matching the family names + * - fontface objects : loads given fontfaces and adds them to + * `document.fonts` + * + * The returned promise will resolve, when all loading is done. + */ + public loadFonts(fonts?: (string | FontFace)[]): Promise; + + /** + * Force a terminal relayout by altering `options.FontFamily`. + * + * Found webfonts in `fontFamily` are temporarily removed until the webfont + * resources are fully loaded. + * + * Call this method, if a terminal with webfonts is stuck with broken + * glyph metrics. + * + * Returns a promise on completion. + */ + public relayout(): Promise; } + + /** + * Wait for webfont resources to be loaded. + * + * Without any argument, all fonts currently listed in + * `document.fonts` will be loaded. + * For a more fine-grained loading strategy you can populate + * the `fonts` argument with: + * - font families : loads all fontfaces in `document.fonts` + * matching the family names + * - fontface objects : loads given fontfaces and adds them to + * `document.fonts` + * + * The returned promise will resolve, when all loading is done. + */ + function loadFonts(fonts?: (string | FontFace)[]): Promise; } From 956c7cbcba5dd5e0300c84203e074f32c0c66abc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 04:00:20 +0200 Subject: [PATCH 08/19] demo tests --- .../typings/addon-web-fonts.d.ts | 4 +- addons/addon-web-fonts/webpack.config.js | 2 +- demo/bpdots.regular.otf | Bin 0 -> 48296 bytes demo/client.ts | 23 +++- demo/index.html | 4 + demo/kongtext.regular.ttf | Bin 0 -> 10280 bytes demo/server.js | 3 + demo/style.css | 112 ------------------ 8 files changed, 31 insertions(+), 117 deletions(-) create mode 100644 demo/bpdots.regular.otf create mode 100644 demo/kongtext.regular.ttf diff --git a/addons/addon-web-fonts/typings/addon-web-fonts.d.ts b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts index c353a421..9be3de21 100644 --- a/addons/addon-web-fonts/typings/addon-web-fonts.d.ts +++ b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * Copyright (c) 2024 The xterm.js authors. All rights reserved. * @license MIT */ @@ -13,7 +13,7 @@ declare module '@xterm/addon-web-fonts' { */ export class WebFontsAddon implements ITerminalAddon { /** - * @param forceInitialRelayout Force an initial relayout, if a webfont was found. + * @param forceInitialRelayout Force initial relayout, if a webfont was found (default true). */ constructor(forceInitialRelayout?: boolean); public activate(terminal: Terminal): void; diff --git a/addons/addon-web-fonts/webpack.config.js b/addons/addon-web-fonts/webpack.config.js index c73a60ba..f75d2d50 100644 --- a/addons/addon-web-fonts/webpack.config.js +++ b/addons/addon-web-fonts/webpack.config.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * Copyright (c) 2024 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/demo/bpdots.regular.otf b/demo/bpdots.regular.otf new file mode 100644 index 0000000000000000000000000000000000000000..e5b5d1dcb74e06e7719aefbe81fd356e6257c1a7 GIT binary patch literal 48296 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 literal 0 HcmV?d00001 diff --git a/demo/client.ts b/demo/client.ts index d125bfee..d5e9fc08 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -23,7 +23,7 @@ import { FitAddon } from '@xterm/addon-fit'; import { LigaturesAddon } from '@xterm/addon-ligatures'; import { SearchAddon, ISearchOptions } from '@xterm/addon-search'; import { SerializeAddon } from '@xterm/addon-serialize'; -import { WebFontsAddon } from '@xterm/addon-web-fonts'; +import { WebFontsAddon, loadFonts } from '@xterm/addon-web-fonts'; import { WebLinksAddon } from '@xterm/addon-web-links'; import { WebglAddon } from '@xterm/addon-webgl'; import { Unicode11Addon } from '@xterm/addon-unicode11'; @@ -252,6 +252,7 @@ if (document.location.pathname === '/test') { addVtButtons(); initImageAddonExposed(); testEvents(); + testWebfonts(); } function createTerminal(): void { @@ -268,7 +269,7 @@ function createTerminal(): void { backend: 'conpty', buildNumber: 22621 } : undefined, - fontFamily: '"Roboto Mono", "Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', + fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', theme: xtermjsTheme } as ITerminalOptions); @@ -1427,3 +1428,21 @@ function testEvents(): void { document.getElementById('event-focus').addEventListener('click', ()=> term.focus()); document.getElementById('event-blur').addEventListener('click', ()=> term.blur()); } + +function testWebfonts() { + document.getElementById('webfont-kongtime').addEventListener('click', async () => { + const ff = new FontFace('Kongtext', "url(/kongtext.regular.ttf) format('truetype')"); + await loadFonts([ff]); + term.options.fontFamily = 'Kongtext'; + term.options.lineHeight = 1.3; + addons.fit.instance?.fit(); + }); + document.getElementById('webfont-bpdots').addEventListener('click', async () => { + document.styleSheets[0].insertRule("@font-face { font-family: 'BPdots'; src: url(/bpdots.regular.otf) format('opentype'); weight: 400 }", 0); + await loadFonts(['BPdots']); + term.options.fontFamily = 'BPdots'; + term.options.lineHeight = 1.3; + term.options.fontSize = 20; + addons.fit.instance?.fit(); + }); +} diff --git a/demo/index.html b/demo/index.html index 06451847..a5673481 100644 --- a/demo/index.html +++ b/demo/index.html @@ -116,6 +116,10 @@
Events Test
+ +
Webfonts
+
+
diff --git a/demo/kongtext.regular.ttf b/demo/kongtext.regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5e4d65fd1e059a159342d62f86c374da152df7f5 GIT binary patch literal 10280 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 res.sendFile(__dirname + '/kongtext.regular.ttf')); + app.get('/bpdots.regular.otf', (req, res) => res.sendFile(__dirname + '/bpdots.regular.otf')); + app.use('/dist', express.static(__dirname + '/dist')); app.use('/src', express.static(__dirname + '/src')); diff --git a/demo/style.css b/demo/style.css index 3e32b42f..1fb8f7ac 100644 --- a/demo/style.css +++ b/demo/style.css @@ -1,115 +1,3 @@ -/* cyrillic-ext */ -@font-face { - font-family: 'Roboto Mono'; - font-style: italic; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2) format('woff2'); - unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; -} -/* cyrillic */ -@font-face { - font-family: 'Roboto Mono'; - font-style: italic; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2) format('woff2'); - unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; -} -/* greek */ -@font-face { - font-family: 'Roboto Mono'; - font-style: italic; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2) format('woff2'); - unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; -} -/* vietnamese */ -@font-face { - font-family: 'Roboto Mono'; - font-style: italic; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2) format('woff2'); - unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; -} -/* latin-ext */ -@font-face { - font-family: 'Roboto Mono'; - font-style: italic; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2) format('woff2'); - unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; -} -/* latin */ -@font-face { - font-family: 'Roboto Mono'; - font-style: italic; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} -/* cyrillic-ext */ -@font-face { - font-family: 'Roboto Mono'; - font-style: normal; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2) format('woff2'); - unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; -} -/* cyrillic */ -@font-face { - font-family: 'Roboto Mono'; - font-style: normal; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2) format('woff2'); - unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; -} -/* greek */ -@font-face { - font-family: 'Roboto Mono'; - font-style: normal; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2) format('woff2'); - unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; -} -/* vietnamese */ -@font-face { - font-family: 'Roboto Mono'; - font-style: normal; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2) format('woff2'); - unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; -} -/* latin-ext */ -@font-face { - font-family: 'Roboto Mono'; - font-style: normal; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2) format('woff2'); - unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; -} -/* latin */ -@font-face { - font-family: 'Roboto Mono'; - font-style: normal; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - - - - body { font-family: helvetica, sans-serif, arial; font-size: 1em; From 7907f13774ef6c0023f3277d5d72577d8e126717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 04:14:10 +0200 Subject: [PATCH 09/19] add license notes for fonts --- demo/font-licenses.txt | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 demo/font-licenses.txt diff --git a/demo/font-licenses.txt b/demo/font-licenses.txt new file mode 100644 index 00000000..5b1b5ba6 --- /dev/null +++ b/demo/font-licenses.txt @@ -0,0 +1,41 @@ + +BPdots is licensed under the Creative Commons Attribution-NoDerivs License (CC BY-ND). + + +kongtext license text: + +Thanks for downloading one of codeman38's retro video game fonts, +as seen on Memepool, BoingBoing, and all around the blogosphere. + +So, you're wondering what the license is for these fonts? Pretty simple; +it's based upon that used for Bitstream's Vera font set . + +Basically, here are the key points summarized, in as little legalese as possible; +I hate reading license agreements as much as you probably do: + +With one specific exception, you have full permission to bundle these fonts in +your own free or commercial projects-- and by projects, I'm referring to not +just software but also electronic documents and print publications. + +So what's the exception? Simple: you can't re-sell these fonts +in a commercial font collection. I've seen too many font CDs for sale in stores +that are just a repackaging of thousands of freeware fonts found on the internet, +and in my mind, that's quite a bit like highway robbery. Note that this *only* +applies to products that are font collections in and of themselves; +you may freely bundle these fonts with an operating system, application program, +or the like. + +Feel free to modify these fonts and even to release the modified versions, +as long as you change the original font names (to ensure consistency among +people with the font installed) and as long as you give credit somewhere +in the font file to codeman38 or zone38.net. I may even incorporate these changes +into a later version of my fonts if you wish to send me the modifed fonts via e-mail. + +Also, feel free to mirror these fonts on your own site, as long as you make it +reasonably clear that these fonts are not your own work. I'm not asking for much; +linking to zone38.net or even just mentioning the nickname codeman38 should be enough. + +Well, that pretty much sums it up... so without further ado, +install and enjoy these fonts from the golden age of video games. + +[ codeman38 | cody@zone38.net | http://www.zone38.net/ ] From b0389a2ed173bf3b9df32a1b6f5f8c461b14d887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 05:31:47 +0200 Subject: [PATCH 10/19] future proof demo --- demo/client.ts | 4 +++- demo/index.html | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index d5e9fc08..732efda5 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -1430,12 +1430,14 @@ function testEvents(): void { } function testWebfonts() { - document.getElementById('webfont-kongtime').addEventListener('click', async () => { + document.getElementById('webfont-kongtext').addEventListener('click', async () => { const ff = new FontFace('Kongtext', "url(/kongtext.regular.ttf) format('truetype')"); await loadFonts([ff]); term.options.fontFamily = 'Kongtext'; term.options.lineHeight = 1.3; addons.fit.instance?.fit(); + setTimeout(() => term.write('\x1b[?12h\x1b]12;#776CF9\x07\x1b[38;2;119;108;249;48;2;21;8;150m\x1b[2J\x1b[2;5H**** COMMODORE 64 BASIC V2 ****\r\n\r\n 64K RAM SYSTEM 38911 BASIC BYTES FREE\r\n\r\nREADY.\r\nLOAD '), 1000); + setTimeout(() => {term.write('🤣\x1b[m\x1b[99;1H'); term.input('\r');}, 5000); }); document.getElementById('webfont-bpdots').addEventListener('click', async () => { document.styleSheets[0].insertRule("@font-face { font-family: 'BPdots'; src: url(/bpdots.regular.otf) format('opentype'); weight: 400 }", 0); diff --git a/demo/index.html b/demo/index.html index a5673481..691871ee 100644 --- a/demo/index.html +++ b/demo/index.html @@ -118,8 +118,8 @@
Webfonts
-
-
+
+
From c93623cb3bcd5d53fbc5c51333b2e7cac0b0e927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 14:24:36 +0200 Subject: [PATCH 11/19] CI tests --- .../test/WebFontsAddon.test.ts | 105 +++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/addons/addon-web-fonts/test/WebFontsAddon.test.ts b/addons/addon-web-fonts/test/WebFontsAddon.test.ts index 09bb01a9..1315b1ee 100644 --- a/addons/addon-web-fonts/test/WebFontsAddon.test.ts +++ b/addons/addon-web-fonts/test/WebFontsAddon.test.ts @@ -6,7 +6,6 @@ import test from '@playwright/test'; import { deepStrictEqual, strictEqual } from 'assert'; import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../test/playwright/TestUtils'; - let ctx: ITestContext; test.beforeAll(async ({ browser }) => { ctx = await createTestContext(browser); @@ -15,5 +14,107 @@ test.beforeAll(async ({ browser }) => { test.afterAll(async () => await ctx.page.close()); test.describe('WebFontsAddon', () => { - test('nothing', () => {}); + + test.beforeEach(async () => { + // make sure that we start with no webfonts in the document + const empty = await await getDocumentFonts(); + deepStrictEqual(empty, []); + }); + test.afterEach(async () => { + // for font loading tests to work, we have to remove added rules and fonts + // to work around the quite aggressive font caching done by the browsers + await ctx.page.evaluate(` + document.styleSheets[0].deleteRule(1); + document.styleSheets[0].deleteRule(0); + document.fonts.clear(); + `); + }); + + test.describe('font loading at runtime', () => { + test('loadFonts (JS)', async () => { + await ctx.page.evaluate(` + const ff1 = new FontFace('Kongtext', "url(/kongtext.regular.ttf) format('truetype')"); + const ff2 = new FontFace('BPdots', "url(/bpdots.regular.otf) format('opentype')"); + loadFonts([ff1, ff2]); + `); + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }, { family: 'BPdots', status: 'loaded' }]); + }); + test('loadFonts (CSS, unquoted)', async () => { + await ctx.page.evaluate(` + document.styleSheets[0].insertRule("@font-face {font-family: Kongtext; src: url(/kongtext.regular.ttf) format('truetype')}", 0); + document.styleSheets[0].insertRule("@font-face {font-family: BPdots; src: url(/bpdots.regular.otf) format('opentype')}", 1); + loadFonts(['Kongtext', 'BPdots']); + `); + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }, { family: 'BPdots', status: 'loaded' }]); + }); + test('loadFonts (CSS, quoted)', async ({ browser }) => { + // NOTE: firefox preserves family quotes from CSS rules in fontface, all other browsers unquote them + await ctx.page.evaluate(` + document.styleSheets[0].insertRule("@font-face {font-family: 'Kongtext'; src: url(/kongtext.regular.ttf) format('truetype')}", 0); + document.styleSheets[0].insertRule("@font-face {font-family: 'BPdots'; src: url(/bpdots.regular.otf) format('opentype')}", 1); + loadFonts(['Kongtext', 'BPdots']); + `); + if (browser.browserType().name() === 'firefox') { + deepStrictEqual(await getDocumentFonts(), [{ family: '"Kongtext"', status: 'loaded' }, { family: '"BPdots"', status: 'loaded' }]); + } else { + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }, { family: 'BPdots', status: 'loaded' }]); + } + }); + test('FontFace hashing', async () => { + // multiple calls of `loadFonts` with the same objects shall not bloat document.fonts + await ctx.page.evaluate(` + const ff1 = new FontFace('Kongtext', "url(/kongtext.regular.ttf) format('truetype')"); + const ff2 = new FontFace('BPdots', "url(/bpdots.regular.otf) format('opentype')"); + loadFonts([ff1, ff2]); + loadFonts([ff1, ff2]); + loadFonts([ff1, ff2]).then(() => loadFonts([ff1, ff2])); + `); + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }, { family: 'BPdots', status: 'loaded' }]); + }); + + test('autoload & relayout from ctor', async ({ browser }) => { + // to make this test work, we exclude the default measurement char W (x57) by restricting unicode-range + // now the browser will postpone font loading until codepoint is hit --> wrong glyph metrics on first usage + const data = await ctx.page.evaluate(` + document.styleSheets[0].insertRule("@font-face {font-family: Kongtext; src: url(/kongtext.regular.ttf) format('truetype'); unicode-range: U+00A0-00FF}", 0); + `); + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'unloaded' }]); + + // broken case: webfont in ctor without addon usage + await ctx.page.evaluate(` + window.helperTerm = new Terminal({fontFamily: '"Kongtext", ' + term.options.fontFamily}); + window.helperTerm.open(term.element); + `); + + // safari loads the font, firefox & chrome dont + if (browser.browserType().name() === 'webkit') { + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }]); + } else { + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'unloaded' }]); + } + + // good case: addon fixes layout for webfont in ctor + // the relayout happens async, so wait a bit with a promise + await ctx.page.evaluate(` + window.helperTerm.dispose(); + window.helperTerm = new Terminal({fontFamily: '"Kongtext", ' + term.options.fontFamily}); + window._webfontsAddon = new WebFontsAddon(); + window.helperTerm.loadAddon(window._webfontsAddon); + window.helperTerm.open(term.element); + `); + await timeout(100); + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }]); + + // cleanup this messy test case + await ctx.page.evaluate(` + window.helperTerm.dispose(); + window._webfontsAddon.dispose(); + `); + }); + }); + }); + +async function getDocumentFonts(): Promise { + return ctx.page.evaluate(`Array.from(document.fonts).map(ff => ({family: ff.family, status: ff.status}))`); +} From 104c63d7e035c4297432081a36e9e3ae1af80be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 17:52:45 +0200 Subject: [PATCH 12/19] cleanup files --- addons/addon-web-fonts/src/WebFontsAddon.ts | 145 -------------------- 1 file changed, 145 deletions(-) diff --git a/addons/addon-web-fonts/src/WebFontsAddon.ts b/addons/addon-web-fonts/src/WebFontsAddon.ts index 219a64af..4a9f7e7a 100644 --- a/addons/addon-web-fonts/src/WebFontsAddon.ts +++ b/addons/addon-web-fonts/src/WebFontsAddon.ts @@ -151,148 +151,3 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { } } } - - - - - - - -/* eslint-disable */ -// TODO: place into test cases -/* -(window as any).__roboto = [ - // cyrillic-ext - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2) format('woff2')", - { - style: 'italic', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F' - } - ), - // cyrillic - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2) format('woff2')", - { - style: 'italic', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116' - } - ), - // greek - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2) format('woff2')", - { - style: 'italic', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF' - } - ), - // vietnamese - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2) format('woff2')", - { - style: 'italic', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB' - } - ), - // latin-ext - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2) format('woff2')", - { - style: 'italic', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF' - } - ), - // latin - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2) format('woff2')", - { - style: 'italic', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD' - } - ), - // cyrillic-ext - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2) format('woff2')", - { - style: 'normal', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F' - } - ), - // cyrillic - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2) format('woff2')", - { - style: 'normal', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116' - } - ), - // greek - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2) format('woff2')", - { - style: 'normal', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF' - } - ), - // vietnamese - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2) format('woff2')", - { - style: 'normal', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB' - } - ), - // latin-ext - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2) format('woff2')", - { - style: 'normal', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF' - } - ), - // latin - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2) format('woff2')", - { - style: 'normal', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD' - } - ), -]; -*/ From 085a2750387f8c446b9443d2d0a9d7187e38db9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 6 Oct 2024 23:53:30 +0200 Subject: [PATCH 13/19] fix promise return, reject on missing family name, doc polish --- addons/addon-web-fonts/README.md | 157 +++++++++++++----- addons/addon-web-fonts/src/WebFontsAddon.ts | 13 +- .../typings/addon-web-fonts.d.ts | 6 +- 3 files changed, 129 insertions(+), 47 deletions(-) diff --git a/addons/addon-web-fonts/README.md b/addons/addon-web-fonts/README.md index e910c21e..ce43cbe5 100644 --- a/addons/addon-web-fonts/README.md +++ b/addons/addon-web-fonts/README.md @@ -1,6 +1,7 @@ ## @xterm/addon-web-fonts -Addon to use webfonts with [xterm.js](https://github.com/xtermjs/xterm.js). This addon requires xterm.js v5+. +Addon to use webfonts with [xterm.js](https://github.com/xtermjs/xterm.js). +This addon requires xterm.js v5+. ### Install @@ -10,33 +11,43 @@ npm install --save @xterm/addon-web-fonts ### Issue with Webfonts -Webfonts are announced by CSS `font-face` rules (or its Javascript `FontFace` counterparts). Since font files tend to be quite big assets, browser engines often postpone their loading to an actual styling request of a codepoint matching a font file's `unicode-range`. In short - font files will not be loaded until really needed. +Webfonts are announced by CSS `font-face` rules (or its Javascript `FontFace` counterparts). +Since font files tend to be quite big assets, browser engines often postpone their loading +to an actual styling request of a codepoint matching a font file's `unicode-range`. +In short - font files will not be loaded until really needed. -xterm.js on the other hand heavily relies on exact measurements of character glyphs to layout its output. This is done by determining the glyph width (DOM renderer) or by creating a glyph texture (WebGl renderer) for every output character. -For performance reasons both is done in synchronous code and cached. This logic only works properly, -if a font glyph is available on its first usage, or a wrong glyph from a fallback font chosen by the browser will be used instead. +xterm.js on the other hand heavily relies on exact measurement of character glyphs +to layout its output. This is done by determining the glyph width (DOM renderer) or +by creating a glyph texture (WebGl renderer) for every output character. +For performance reasons both is done in synchronous code and cached. +This logic only works properly, if a font glyph is available on its first usage, +otherwise the browser will pick a glyph from a fallback font messing up the metrics. -For webfonts and xterm.js this means, that we cannot rely on the default loading strategy of the browser, but have to preload the font files before using that font in xterm.js. +For webfonts and xterm.js this means that we cannot rely on the default loading strategy +of the browser, but have to preload the font files before using that font in xterm.js. ### Static Preloading for the Rescue? -If you dont mind higher initial loading times of the embedding document, you can tell the browser to preload the needed font files by placing the following link elements in the document's head above any other CSS/Javascript: +If you dont mind higher initial loading times with a white page shown, +you can tell the browser to preload the needed font files by placing the following +link elements in the document's head above any other CSS/Javascript: ```html ... ``` -Downside of this approach is the much higher initial loading time showing as a white page. Browsers also will resort to system fonts, if the preloading takes too long, so with a slow connection or a very big font this solves literally nothing. +Browsers also will resort to system fonts, if the preloading takes too long. +So this solution has only a very limited scope. -### Preloading with WebFontsAddon +### Loading with WebFontsAddon -The webfonts addon offers several ways to deal with the loading of font assets without leaving the terminal in an unusable state. +The webfonts addon offers several ways to deal with font assets loading +without leaving the terminal in an unusable state. - -Recap - normally boostrapping of a new terminal involves these basic steps: +Recap - normally boostrapping of a new terminal involves these basic steps (Typescript): ```typescript import { Terminal } from '@xterm/xterm'; @@ -46,18 +57,20 @@ import { XYAddon } from '@xterm/addon-xy'; const terminal = new Terminal({fontFamily: 'monospace'}); // create and load all addons you want to use, e.g. fit addon -const xyAddon = new XYAddon(); -terminal.loadAddon(xyAddon); +const xyInstance = new XYAddon(); +terminal.loadAddon(xyInstance); // finally: call `open` of the terminal instance terminal.open(your_terminal_div_element); // <-- critical path for webfonts // more boostrapping goes here ... ``` +This code is guaranteed to work in all browsers synchronously, as the identifier `monospace` +will always be available. It will also work synchronously with any installed system font, +but breaks horribly with webfonts. The actual culprit here is the call to `terminal.open`, +which attaches the terminal to the DOM and starts the renderer with all the glyph caching +mentioned above, while the webfont is not yet fully available. -This synchronous code is guaranteed to work in all browsers, as the font `monospace` will always be available. -It will also work that way with any installed system font, but breaks horribly for webfonts. The actual culprit here is the call to `terminal.open`, which attaches the terminal to the DOM and starts the renderer with all the glyph caching mentioned above, while the webfont is not fully available yet. - -To fix that, the webfonts addon provides a waiting condition: +To fix that, the webfonts addon provides a waiting condition (Typescript): ```typescript import { Terminal } from '@xterm/xterm'; import { XYAddon } from '@xterm/addon-xy'; @@ -65,48 +78,55 @@ import { WebFontsAddon } from '@xterm/addon-web-fonts'; // create a `Terminal` instance, now with webfonts const terminal = new Terminal({fontFamily: '"Web Mono 1", "Super Powerline", monospace'}); -const xyAddon = new XYAddon(); -terminal.loadAddon(xyAddon); +const xyInstance = new XYAddon(); +terminal.loadAddon(xyInstance); -const webFontsAddon = new WebFontsAddon(); -terminal.loadAddon(webFontsAddon); +const webFontsInstance = new WebFontsAddon(); +terminal.loadAddon(webFontsInstance); // wait for webfonts to be fully loaded -webFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { +webFontsInstance.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { terminal.open(your_terminal_div_element); // more boostrapping goes here ... }); ``` -Here `loadFonts` will look up the font face objects in `document.fonts` and load them before continuing. -For this to work, you have to make sure, that the CSS `font-face` rules for these webfonts are loaded -on the initial document load (more precise - by the time this code runs). +Here `loadFonts` will look up the font face objects in `document.fonts` +and load them before continuing. For this to work, you have to make sure, +that the CSS `font-face` rules for these webfonts are loaded beforehand, +otherwise `loadFonts` will not find the font family names (promise will be +rejected for missing font family names). -Please note, that this code cannot run synchronous anymore, so you will have to split your -bootstrapping code into several stages. If thats too much of a hassle, you can also move the whole -bootstrapping under that waiting condition (import `loadFonts` for a static variant): +Please note, that this cannot run synchronous anymore, so you will have to split your +bootstrapping code into several stages. If that is too much of a hassle, +you can also move the whole bootstrapping under the waiting condition by using +the static loader instead (Typescript): ```typescript import { Terminal } from '@xterm/xterm'; import { XYAddon } from '@xterm/addon-xy'; -import { WebFontsAddon, loadFonts } from '@xterm/addon-web-fonts'; +// import static loader +import { loadFonts } from '@xterm/addon-web-fonts'; loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { // create a `Terminal` instance, now with webfonts const terminal = new Terminal({fontFamily: '"Web Mono 1", "Super Powerline", monospace'}); - const xyAddon = new XYAddon(); - terminal.loadAddon(xyAddon); + const xyInstance = new XYAddon(); + terminal.loadAddon(xyInstance); - const webFontsAddon = new WebFontsAddon(); - terminal.loadAddon(webFontsAddon); + // optional when using static loader + const webfontsInstance = new WebFontsAddon(); + terminal.loadAddon(webfontsInstance); terminal.open(your_terminal_div_element); // more boostrapping goes here ... }); ``` +With the static loader creating and loading of the actual addon can be omitted, +as fonts are already loaded before any terminal setup happens. ### Webfont Loading at Runtime -Given you have a terminal already running and want to change the font family to a different not yet loaded webfont. -That can be achieved like this: +Given you have a terminal already running and want to change the font family +to a different not yet loaded webfont: ```typescript // either create font face objects in javascript const ff1 = new FontFace('New Web Mono', url1, ...); @@ -133,5 +153,68 @@ loadFonts(['New Web Mono']).then(() => { }); ``` +### Forced Layout Update -See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-web-fonts/typings/addon-web-fonts.d.ts) for more advanced usage. +If you have the addon loaded into your terminal, you can force the terminal to update +the layout with the method `WebFontsAddon.relayout`. This might come handy, +if the terminal shows webfont related output issue for unknown reasons: +```typescript +... +// given - terminal shows weird font issues, run: +webFontsInstance.relayout().then(() => { + // also run resize logic here, e.g. fit addon + fitAddon.fit(); +}); +``` +Note that this method is only meant as a quickfix on a running terminal to keep it +in a working condition. A production-ready integration should never rely on it, +better fix the real root cause (most likely not properly awaiting the font loader +higher up in the code). + + +### Webfonts from Fontsource + +The addon has been tested to work with webfonts from fontsource. +Javascript example for `vite` with ESM import: +```javascript +import { Terminal } from '@xterm/xterm'; +import { FitAddon } from '@xterm/addon-fit'; +import { loadFonts } from '@xterm/addon-web-fonts'; +import '@xterm/xterm/css/xterm.css'; +import '@fontsource/roboto-mono'; +import '@fontsource/roboto-mono/400.css'; +import '@fontsource/roboto-mono/400-italic.css'; +import '@fontsource/roboto-mono/700.css'; +import '@fontsource/roboto-mono/700-italic.css'; + +async function main() { + let fontFamily = '"Roboto Mono", monospace'; + try { + await loadFonts(['Roboto Mono']); + } catch (e) { + fontFamily = 'monospace'; + } + + const terminal = new Terminal({ fontFamily }); + const fitAddon = new FitAddon(); + terminal.loadAddon(fitAddon); + terminal.open(document.getElementById('your-xterm-container-div')); + fitAddon.fit(); + + // sync writing shows up in Roboto Mono w'o FOUT + // and a fallback to monospace + terminal.write('put any unicode char here'); +} + +main(); +``` +The fontsource packages download the font files to your project folder to be delivered +from there later on. For security sensitive projects this should be the preferred way, +as it brings the font files under your control. + +The example furthermore contains proper exception handling with a fallback +(skipped in all other examples for better readability). + +--- + +Also see the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-web-fonts/typings/addon-web-fonts.d.ts). diff --git a/addons/addon-web-fonts/src/WebFontsAddon.ts b/addons/addon-web-fonts/src/WebFontsAddon.ts index 4a9f7e7a..cc0ccd1f 100644 --- a/addons/addon-web-fonts/src/WebFontsAddon.ts +++ b/addons/addon-web-fonts/src/WebFontsAddon.ts @@ -94,7 +94,7 @@ function _loadFonts(fonts?: (string | FontFace)[]): Promise { const familyFiltered = ffs.filter(ff => font === unquote(ff.family)); toLoad = toLoad.concat(familyFiltered); if (!familyFiltered.length) { - console.warn(`font family "${font}" not registered in document.fonts`); + return Promise.reject(`font family "${font}" not registered in document.fonts`); } } } @@ -102,16 +102,15 @@ function _loadFonts(fonts?: (string | FontFace)[]): Promise { } -export async function loadFonts(fonts?: (string | FontFace)[]): Promise { - await document.fonts.ready; - return _loadFonts(fonts); +export function loadFonts(fonts?: (string | FontFace)[]): Promise { + return document.fonts.ready.then(() => _loadFonts(fonts)); } export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { private _term: Terminal | undefined; - constructor(public forceInitialRelayout: boolean = true) { } + constructor(public initialRelayout: boolean = true) { } public dispose(): void { this._term = undefined; @@ -119,12 +118,12 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { public activate(term: Terminal): void { this._term = term; - if (this.forceInitialRelayout) { + if (this.initialRelayout) { document.fonts.ready.then(() => this.relayout()); } } - public async loadFonts(fonts?: (string | FontFace)[]): Promise { + public loadFonts(fonts?: (string | FontFace)[]): Promise { return loadFonts(fonts); } diff --git a/addons/addon-web-fonts/typings/addon-web-fonts.d.ts b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts index 9be3de21..c7a159ed 100644 --- a/addons/addon-web-fonts/typings/addon-web-fonts.d.ts +++ b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts @@ -13,9 +13,9 @@ declare module '@xterm/addon-web-fonts' { */ export class WebFontsAddon implements ITerminalAddon { /** - * @param forceInitialRelayout Force initial relayout, if a webfont was found (default true). + * @param initialRelayout Force initial relayout, if a webfont was found (default true). */ - constructor(forceInitialRelayout?: boolean); + constructor(initialRelayout?: boolean); public activate(terminal: Terminal): void; public dispose(): void; @@ -44,7 +44,7 @@ declare module '@xterm/addon-web-fonts' { * Call this method, if a terminal with webfonts is stuck with broken * glyph metrics. * - * Returns a promise on completion. + * The returned promise will resolve, when font loading and layouting are done. */ public relayout(): Promise; } From 5e95dc1f1014c3c56969fea4b1176acaaf604890 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:24:11 -0800 Subject: [PATCH 14/19] Sync package lock --- addons/addon-web-fonts/package.json | 3 --- package-lock.json | 20 +++++++++----------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/addons/addon-web-fonts/package.json b/addons/addon-web-fonts/package.json index 1daac376..856c7e25 100644 --- a/addons/addon-web-fonts/package.json +++ b/addons/addon-web-fonts/package.json @@ -22,8 +22,5 @@ "prepublishOnly": "npm run package", "start": "node ../../demo/start" }, - "peerDependencies": { - "@xterm/xterm": "^5.0.0" - }, "dependencies": {} } diff --git a/package-lock.json b/package-lock.json index 2e55f3de..4c218b70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -145,6 +145,11 @@ "version": "0.9.0", "license": "MIT" }, + "addons/addon-web-fonts": { + "name": "@xterm/addon-web-fonts", + "version": "0.1.0", + "license": "MIT" + }, "addons/addon-web-links": { "name": "@xterm/addon-web-links", "version": "0.12.0", @@ -239,7 +244,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -553,7 +557,6 @@ "url": "https://opencollective.com/csstools" } ], - "peer": true, "engines": { "node": ">=18" }, @@ -595,7 +598,6 @@ "url": "https://opencollective.com/csstools" } ], - "peer": true, "engines": { "node": ">=18" } @@ -1897,7 +1899,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz", "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -2297,6 +2298,10 @@ "resolved": "addons/addon-unicode11", "link": true }, + "node_modules/@xterm/addon-web-fonts": { + "resolved": "addons/addon-web-fonts", + "link": true + }, "node_modules/@xterm/addon-web-links": { "resolved": "addons/addon-web-links", "link": true @@ -2342,7 +2347,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2732,7 +2736,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3734,7 +3737,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7090,7 +7092,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7958,7 +7959,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8142,7 +8142,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -8191,7 +8190,6 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", From a35223a819eb588c3ddc01a3f239e0055d658a8b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:31:24 -0800 Subject: [PATCH 15/19] Add web fonts window to demo --- demo/client.ts | 1450 ----------------- demo/client/client.ts | 9 +- .../components/window/addonWebFontsWindow.ts | 60 + demo/client/types.ts | 5 +- 4 files changed, 72 insertions(+), 1452 deletions(-) delete mode 100644 demo/client.ts create mode 100644 demo/client/components/window/addonWebFontsWindow.ts diff --git a/demo/client.ts b/demo/client.ts deleted file mode 100644 index 732efda5..00000000 --- a/demo/client.ts +++ /dev/null @@ -1,1450 +0,0 @@ -/* eslint-disable no-restricted-syntax */ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - * - * This file is the entry point for browserify. - */ - -/// - -// HACK: Playwright/WebKit on Windows does not support WebAssembly https://stackoverflow.com/q/62311688/1156119 -import type { ImageAddon as ImageAddonType, IImageAddonOptions } from '@xterm/addon-image'; -let ImageAddon: typeof ImageAddonType | undefined; // eslint-disable-line @typescript-eslint/naming-convention -if ('WebAssembly' in window) { - const imageAddon = require('@xterm/addon-image'); - ImageAddon = imageAddon.ImageAddon; -} - -import { Terminal, ITerminalOptions, type IDisposable } from '@xterm/xterm'; -import { AttachAddon } from '@xterm/addon-attach'; -import { ClipboardAddon } from '@xterm/addon-clipboard'; -import { FitAddon } from '@xterm/addon-fit'; -import { LigaturesAddon } from '@xterm/addon-ligatures'; -import { SearchAddon, ISearchOptions } from '@xterm/addon-search'; -import { SerializeAddon } from '@xterm/addon-serialize'; -import { WebFontsAddon, loadFonts } from '@xterm/addon-web-fonts'; -import { WebLinksAddon } from '@xterm/addon-web-links'; -import { WebglAddon } from '@xterm/addon-webgl'; -import { Unicode11Addon } from '@xterm/addon-unicode11'; -import { UnicodeGraphemesAddon } from '@xterm/addon-unicode-graphemes'; - -export interface IWindowWithTerminal extends Window { - term: typeof Terminal; - Terminal: typeof Terminal; - AttachAddon?: typeof AttachAddon; // eslint-disable-line @typescript-eslint/naming-convention - ClipboardAddon?: typeof ClipboardAddon; // eslint-disable-line @typescript-eslint/naming-convention - FitAddon?: typeof FitAddon; // eslint-disable-line @typescript-eslint/naming-convention - ImageAddon?: typeof ImageAddon; // 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 - WebFontsAddon?: typeof WebFontsAddon; // 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 - UnicodeGraphemesAddon?: typeof UnicodeGraphemesAddon; // eslint-disable-line @typescript-eslint/naming-convention - LigaturesAddon?: typeof LigaturesAddon; // eslint-disable-line @typescript-eslint/naming-convention -} -declare let window: IWindowWithTerminal; - -let term; -let protocol; -let socketURL; -let socket; -let pid; -let autoResize: boolean = true; - -type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl' | 'ligatures'; - -interface IDemoAddon { - name: T; - canChange: boolean; - ctor: ( - T extends 'attach' ? typeof AttachAddon : - T extends 'clipboard' ? typeof ClipboardAddon : - T extends 'fit' ? typeof FitAddon : - T extends 'image' ? typeof ImageAddonType : - T extends 'ligatures' ? typeof LigaturesAddon : - T extends 'search' ? typeof SearchAddon : - T extends 'serialize' ? typeof SerializeAddon : - T extends 'webFonts' ? typeof WebFontsAddon : - T extends 'webLinks' ? typeof WebLinksAddon : - T extends 'unicode11' ? typeof Unicode11Addon : - T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : - T extends 'webgl' ? typeof WebglAddon : - never - ); - instance?: ( - T extends 'attach' ? AttachAddon : - T extends 'clipboard' ? ClipboardAddon : - T extends 'fit' ? FitAddon : - T extends 'image' ? ImageAddonType : - T extends 'ligatures' ? LigaturesAddon : - T extends 'search' ? SearchAddon : - T extends 'serialize' ? SerializeAddon : - T extends 'webFonts' ? WebFontsAddon : - T extends 'webLinks' ? WebLinksAddon : - T extends 'unicode11' ? Unicode11Addon : - T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : - T extends 'webgl' ? WebglAddon : - never - ); -} - -const addons: { [T in AddonType]: IDemoAddon } = { - attach: { name: 'attach', ctor: AttachAddon, canChange: false }, - clipboard: { name: 'clipboard', ctor: ClipboardAddon, canChange: true }, - fit: { name: 'fit', ctor: FitAddon, canChange: false }, - image: { name: 'image', ctor: ImageAddon, canChange: true }, - search: { name: 'search', ctor: SearchAddon, canChange: true }, - serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true }, - webFonts: { name: 'webFonts', ctor: WebFontsAddon, canChange: true }, - webLinks: { name: 'webLinks', ctor: WebLinksAddon, canChange: true }, - webgl: { name: 'webgl', ctor: WebglAddon, canChange: true }, - unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true }, - unicodeGraphemes: { name: 'unicodeGraphemes', ctor: UnicodeGraphemesAddon, canChange: true }, - ligatures: { name: 'ligatures', ctor: LigaturesAddon, canChange: true } -}; - -let terminalContainer = document.getElementById('terminal-container'); -const actionElements = { - 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 = document.getElementById('padding') as HTMLInputElement; - -const xtermjsTheme = { - 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' -}; -function setPadding(): void { - term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px'; - addons.fit.instance.fit(); -} - -function getSearchOptions(): ISearchOptions { - return { - regex: (document.getElementById('regex') as HTMLInputElement).checked, - wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, - caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked, - decorations: (document.getElementById('highlight-all-matches') as HTMLInputElement).checked ? { - matchBackground: '#232422', - matchBorder: '#555753', - matchOverviewRuler: '#555753', - activeMatchBackground: '#ef2929', - activeMatchBorder: '#ffffff', - activeMatchColorOverviewRuler: '#ef2929' - } : undefined - }; -} - -const disposeRecreateButtonHandler: () => void = () => { - // If the terminal exists dispose of it, otherwise recreate it - if (term) { - term.dispose(); - term = null; - window.term = null; - socket = null; - addons.attach.instance = undefined; - addons.clipboard.instance = undefined; - addons.fit.instance = undefined; - addons.image.instance = undefined; - addons.search.instance = undefined; - addons.serialize.instance = undefined; - addons.unicode11.instance = undefined; - addons.unicodeGraphemes.instance = undefined; - addons.ligatures.instance = undefined; - addons.webFonts.instance = undefined; - addons.webLinks.instance = undefined; - addons.webgl.instance = undefined; - document.getElementById('dispose').innerHTML = 'Recreate Terminal'; - } else { - createTerminal(); - document.getElementById('dispose').innerHTML = 'Dispose terminal'; - } -}; - -const createNewWindowButtonHandler: () => void = () => { - if (term) { - disposeRecreateButtonHandler(); - } - const win = window.open(); - terminalContainer = win.document.createElement('div'); - terminalContainer.id = 'terminal-container'; - win.document.body.appendChild(terminalContainer); - - // Stylesheets are needed to get the terminal in the popout window to render - // correctly. We also need to wait for them to load before creating the - // terminal, otherwise we will not compute the correct metrics when rendering. - let pendingStylesheets = 0; - for (const linkNode of document.querySelectorAll('head link[rel=stylesheet]')) { - const newLink = document.createElement('link'); - newLink.rel = 'stylesheet'; - newLink.href = (linkNode as HTMLLinkElement).href; - win.document.head.appendChild(newLink); - - pendingStylesheets++; - newLink.addEventListener('load', () => { - pendingStylesheets--; - if (pendingStylesheets === 0) { - createTerminal(); - } - }); - } -}; - -if (document.location.pathname === '/test') { - window.Terminal = Terminal; - window.AttachAddon = AttachAddon; - window.ClipboardAddon = ClipboardAddon; - window.FitAddon = FitAddon; - window.ImageAddon = ImageAddon; - window.SearchAddon = SearchAddon; - window.SerializeAddon = SerializeAddon; - window.Unicode11Addon = Unicode11Addon; - window.UnicodeGraphemesAddon = UnicodeGraphemesAddon; - window.LigaturesAddon = LigaturesAddon; - window.WebFontsAddon = WebFontsAddon; - window.WebLinksAddon = WebLinksAddon; - window.WebglAddon = WebglAddon; -} else { - createTerminal(); - document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); - document.getElementById('create-new-window').addEventListener('click', createNewWindowButtonHandler); - document.getElementById('serialize').addEventListener('click', serializeButtonHandler); - document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler); - document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler); - document.getElementById('load-test').addEventListener('click', loadTest); - document.getElementById('load-test-long-lines').addEventListener('click', loadTestLongLines); - document.getElementById('print-cjk').addEventListener('click', addCjk); - document.getElementById('print-cjk-sgr').addEventListener('click', addCjkRandomSgr); - document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest); - 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-grapheme-clusters').addEventListener('click', addGraphemeClusters); - document.getElementById('add-decoration').addEventListener('click', addDecoration); - document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler); - document.getElementById('decoration-stress-test').addEventListener('click', decorationStressTest); - document.getElementById('weblinks-test').addEventListener('click', testWeblinks); - document.getElementById('bce').addEventListener('click', coloredErase); - addVtButtons(); - initImageAddonExposed(); - testEvents(); - testWebfonts(); -} - -function createTerminal(): void { - // Clean terminal - while (terminalContainer.children.length) { - terminalContainer.removeChild(terminalContainer.children[0]); - } - - const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; - term = new Terminal({ - allowProposedApi: true, - windowsPty: isWindows ? { - // In a real scenario, these values should be verified on the backend - backend: 'conpty', - buildNumber: 22621 - } : undefined, - fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', - theme: xtermjsTheme - } as ITerminalOptions); - - // Load addons - const typedTerm = term as Terminal; - addons.search.instance = new SearchAddon(); - addons.serialize.instance = new SerializeAddon(); - addons.fit.instance = new FitAddon(); - addons.image.instance = new ImageAddon(); - addons.unicodeGraphemes.instance = new UnicodeGraphemesAddon(); - addons.clipboard.instance = new ClipboardAddon(); - try { // try to start with webgl renderer (might throw on older safari/webkit) - addons.webgl.instance = new WebglAddon(); - } catch (e) { - console.warn(e); - } - addons.webFonts.instance = new WebFontsAddon(); - addons.webLinks.instance = new WebLinksAddon(); - typedTerm.loadAddon(addons.fit.instance); - typedTerm.loadAddon(addons.image.instance); - typedTerm.loadAddon(addons.search.instance); - typedTerm.loadAddon(addons.serialize.instance); - typedTerm.loadAddon(addons.unicodeGraphemes.instance); - typedTerm.loadAddon(addons.webFonts.instance); - typedTerm.loadAddon(addons.webLinks.instance); - typedTerm.loadAddon(addons.clipboard.instance); - - window.term = term; // Expose `term` to window for debugging purposes - term.onResize((size: { cols: number, rows: number }) => { - if (!pid) { - return; - } - const cols = size.cols; - const rows = size.rows; - const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows; - - fetch(url, { method: 'POST' }); - }); - protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; - socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; - - addons.fit.instance!.fit(); - - if (addons.webgl.instance) { - try { - typedTerm.loadAddon(addons.webgl.instance); - term.open(terminalContainer); - setTextureAtlas(addons.webgl.instance.textureAtlas); - addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e)); - addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e)); - addons.webgl.instance.onRemoveTextureAtlasCanvas(e => removeTextureAtlas(e)); - } catch (e) { - console.warn('error during loading webgl addon:', e); - addons.webgl.instance.dispose(); - addons.webgl.instance = undefined; - } - } - if (!typedTerm.element) { - // webgl loading failed for some reason, attach with DOM renderer - term.open(terminalContainer); - } - - term.focus(); - - const resizeObserver = new ResizeObserver(entries => { - if (autoResize) { - addons.fit.instance.fit(); - } - }); - resizeObserver.observe(terminalContainer); - - addDomListener(paddingElement, 'change', setPadding); - - addDomListener(actionElements.findNext, 'keydown', (e) => { - if (e.key === 'Enter') { - addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions()); - e.preventDefault(); - } - }); - addDomListener(actionElements.findNext, 'input', (e) => { - addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions()); - }); - addDomListener(actionElements.findPrevious, 'keydown', (e) => { - if (e.key === 'Enter') { - addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions()); - e.preventDefault(); - } - }); - addDomListener(actionElements.findPrevious, 'input', (e) => { - addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions()); - }); - addDomListener(actionElements.findNext, 'blur', (e) => { - addons.search.instance.clearActiveDecoration(); - }); - addDomListener(actionElements.findPrevious, 'blur', (e) => { - addons.search.instance.clearActiveDecoration(); - }); - - // fit is called within a setTimeout, cols and rows need this. - setTimeout(async () => { - initOptions(term); - paddingElement.value = '0'; - - // Set terminal size again to set the specific dimensions on the demo - updateTerminalSize(); - - const res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' }); - const processId = await res.text(); - pid = processId; - socketURL += processId; - socket = new WebSocket(socketURL); - socket.onopen = runRealTerminal; - socket.onclose = runFakeTerminal; - socket.onerror = runFakeTerminal; - }, 0); -} - -function runRealTerminal(): void { - addons.attach.instance = new AttachAddon(socket); - term.loadAddon(addons.attach.instance); - term._initialized = true; - initAddons(term); -} - -function runFakeTerminal(): void { - if (term._initialized) { - return; - } - - term._initialized = true; - initAddons(term); - - term.prompt = () => { - term.write('\r\n$ '); - }; - - term.writeln('Welcome to xterm.js'); - term.writeln('This is a local terminal emulation, without a real terminal in the back-end.'); - term.writeln('Type some keys and commands to play around.'); - term.writeln(''); - term.prompt(); - - term.onKey((e: { key: string, domEvent: KeyboardEvent }) => { - const ev = e.domEvent; - const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey; - - if (ev.keyCode === 13) { - term.prompt(); - } else if (ev.keyCode === 8) { - // Do not delete the prompt - if (term._core.buffer.x > 2) { - term.write('\b \b'); - } - } else if (printable) { - term.write(e.key); - } - }); -} - -function initOptions(term: Terminal): void { - const blacklistedOptions = [ - // Internal only options - 'cancelEvents', - 'convertEol', - 'termName', - 'cols', 'rows', // subsumed by "size" (colsRows) option - // Complex option - 'documentOverride', - 'linkHandler', - 'logger', - 'overviewRuler', - 'theme', - 'windowOptions', - 'windowsPty', - // Deprecated - 'fastScrollModifier' - ]; - const stringOptions = { - 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(term.options); - const booleanOptions = []; - const numberOptions = []; - options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => { - switch (typeof term.options[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 += '
'; - booleanOptions.forEach(o => { - html += `
`; - }); - html += '
'; - numberOptions.forEach(o => { - html += `
`; - }); - html += '
'; - Object.keys(stringOptions).forEach(o => { - if (o === 'colsRows') { - html += `
`; - } else if (stringOptions[o]) { - const selectedOption = o === 'theme' ? 'xtermjs' : term.options[o]; - html += `
`; - } else { - html += `
`; - } - }); - html += '
'; - - const container = document.getElementById('options-container'); - container.innerHTML = html; - - // Attach listeners - booleanOptions.forEach(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 = document.getElementById(`opt-${o}`) as HTMLInputElement; - addDomListener(input, 'change', () => { - console.log('change', o, input.value); - if (o === 'lineHeight') { - term.options.lineHeight = parseFloat(input.value); - } else if (o === 'scrollSensitivity') { - term.options.scrollSensitivity = parseFloat(input.value); - } else if (o === 'scrollback') { - term.options.scrollback = parseInt(input.value); - setTimeout(() => updateTerminalSize(), 5); - } else { - term.options[o] = parseInt(input.value); - } - // Always update terminal size in case the option changes the dimensions - 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: any = input.value; - if (o === 'colsRows') { - const m = input.value.match(/^([0-9]+)x([0-9]+)$/); - if (m) { - autoResize = false; - term.resize(parseInt(m[1]), parseInt(m[2])); - } else { - autoResize = true; - input.value = 'auto'; - updateTerminalSize(); - } - } else if (o === 'theme') { - switch (input.value) { - case 'default': - value = undefined; - break; - case 'xtermjs': - // Custom theme to match style of xterm.js logo - value = xtermjsTheme; - case 'sapphire': - // Color source: https://github.com/Tyriar/vscode-theme-sapphire - value = { - 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' - }; - break; - case 'light': - // Color source: https://github.com/microsoft/vscode/blob/main/extensions/theme-defaults/themes/light_plus.json - value = { - 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' - }; - break; - } - } - term.options[o] = value; - }); - }); -} - -function initAddons(term: Terminal): void { - const fragment = document.createDocumentFragment(); - Object.keys(addons).forEach((name: AddonType) => { - const addon = addons[name]; - const checkbox = document.createElement('input') as HTMLInputElement; - checkbox.type = 'checkbox'; - checkbox.checked = !!addon.instance; - if (!addon.canChange) { - checkbox.disabled = true; - } - if (name === 'unicode11' && checkbox.checked) { - term.unicode.activeVersion = '11'; - } - if (name === 'unicodeGraphemes' && checkbox.checked) { - term.unicode.activeVersion = '15-graphemes'; - } - if (name === 'search' && checkbox.checked) { - addons[name].instance.onDidChangeResults(e => updateFindResults(e)); - } - addDomListener(checkbox, 'change', () => { - if (name === 'image') { - if (checkbox.checked) { - const ctorOptionsJson = document.querySelector('#image-options').value; - addon.instance = ctorOptionsJson - ? new addons[name].ctor(JSON.parse(ctorOptionsJson)) - : new addons[name].ctor(); - term.loadAddon(addon.instance); - } else { - addon.instance!.dispose(); - addon.instance = undefined; - } - return; - } - if (checkbox.checked) { - // HACK: Manually remove addons that cannot be changes - addon.instance = new (addon as IDemoAddon>).ctor(); - try { - term.loadAddon(addon.instance); - if (name === 'webgl') { - setTimeout(() => { - setTextureAtlas(addons.webgl.instance.textureAtlas); - addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e)); - addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e)); - }, 0); - } else if (name === 'unicode11') { - term.unicode.activeVersion = '11'; - } else if (name === 'unicodeGraphemes') { - term.unicode.activeVersion = '15-graphemes'; - } else if (name === 'search') { - addons[name].instance.onDidChangeResults(e => updateFindResults(e)); - } - } - catch { - addon.instance = undefined; - checkbox.checked = false; - checkbox.disabled = true; - } - } else { - if (name === 'webgl') { - addons.webgl.instance.textureAtlas.remove(); - } else if (name === 'unicode11' || name === 'unicodeGraphemes') { - term.unicode.activeVersion = '6'; - } - addon.instance!.dispose(); - addon.instance = undefined; - } - }); - const label = document.createElement('label'); - label.classList.add('addon'); - if (!addon.canChange) { - label.title = 'This addon is needed for the demo to operate'; - } - label.appendChild(checkbox); - label.appendChild(document.createTextNode(name)); - const wrapper = document.createElement('div'); - wrapper.classList.add('addon'); - wrapper.appendChild(label); - fragment.appendChild(wrapper); - }); - const container = document.getElementById('addons-container'); - container.innerHTML = ''; - container.appendChild(fragment); -} - -function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined): void { - let content: string; - if (e === undefined) { - content = 'undefined'; - } else { - content = `index: ${e.resultIndex}, count: ${e.resultCount}`; - } - actionElements.findResults.textContent = content; -} - -function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void { - element.addEventListener(type, handler); - term._core._register({ dispose: () => element.removeEventListener(type, handler) }); -} - -function updateTerminalSize(): void { - const width = autoResize ? '100%' - : (term._core._renderService.dimensions.css.canvas.width + term._core.viewport.scrollBarWidth).toString() + 'px'; - const height = autoResize ? '100%' - : (term._core._renderService.dimensions.css.canvas.height).toString() + 'px'; - terminalContainer.style.width = width; - terminalContainer.style.height = height; - addons.fit.instance.fit(); -} - -function serializeButtonHandler(): void { - const output = addons.serialize.instance.serialize(); - const outputString = JSON.stringify(output); - - document.getElementById('serialize-output').innerText = outputString; - if ((document.getElementById('write-to-terminal') as HTMLInputElement).checked) { - term.reset(); - term.write(output); - } -} - -function htmlSerializeButtonHandler(): void { - const output = addons.serialize.instance.serializeAsHTML(); - document.getElementById('htmlserialize-output').innerText = output; - - // Deprecated, but the most supported for now. - 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'; -} - -function setTextureAtlas(e: HTMLCanvasElement): void { - styleAtlasPage(e); - document.querySelector('#texture-atlas').replaceChildren(e); -} -function appendTextureAtlas(e: HTMLCanvasElement): void { - styleAtlasPage(e); - document.querySelector('#texture-atlas').appendChild(e); -} -function removeTextureAtlas(e: HTMLCanvasElement): void { - e.remove(); -} -function styleAtlasPage(e: HTMLCanvasElement): void { - e.style.width = `${e.width / window.devicePixelRatio}px`; - e.style.height = `${e.height / window.devicePixelRatio}px`; -} - -function writeCustomGlyphHandler(): void { - term.write('\n\r'); - term.write('\n\r'); - term.write('Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐\n\r'); - term.write('┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤\n\r'); - term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘\n\r'); - term.write('├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐\n\r'); - term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤\n\r'); - term.write('└─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘\n\r'); - term.write('\n\r'); - term.write('Other:\n\r'); - term.write('╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈\n\r'); - term.write('│ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉\n\r'); - term.write('╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋\n\r'); - term.write('\n\r'); - term.write('All box drawing characters:\n\r'); - term.write('─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏\n\r'); - term.write('┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟\n\r'); - term.write('┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯\n\r'); - term.write('┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿\n\r'); - term.write('╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏\n\r'); - term.write('═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟\n\r'); - term.write('╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯\n\r'); - term.write('╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿\n\r'); - term.write('Box drawing alignment tests:\x1b[31m █\n\r'); - term.write(' ▉\n\r'); - term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r'); - term.write(' ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n\r'); - term.write(' ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n\r'); - term.write(' ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n\r'); - term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r'); - term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r'); - term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r'); - term.write('Box drawing alignment tests:\x1b[32m █\n\r'); - term.write(' ▉\n\r'); - term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r'); - term.write(' ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n\r'); - term.write(' ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n\r'); - term.write(' ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n\r'); - term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r'); - term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r'); - term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r'); - term.write('\x1b[0m'); - window.scrollTo(0, 0); -} - -function loadTest(): void { - const rendererName = addons.webgl.instance ? 'webgl' : 'dom'; - const testData = []; - let byteCount = 0; - for (let i = 0; i < 50; i++) { - const count = 1 + Math.floor(Math.random() * 79); - byteCount += count + 2; - const data = new Uint8Array(count + 2); - data[0] = 0x0A; // \n - for (let i = 1; i < count + 1; i++) { - data[i] = 0x61 + Math.floor(Math.random() * (0x7A - 0x61)); - } - // End each line with \r so the cursor remains constant, this is what ls/tree do and improves - // performance significantly due to the cursor DOM element not needing to change - data[data.length - 1] = 0x0D; // \r - testData.push(data); - } - const start = performance.now(); - for (let i = 0; i < 1024; i++) { - for (const d of testData) { - term.write(d); - } - } - // Wait for all data to be parsed before evaluating time - term.write('', () => { - const time = Math.round(performance.now() - start); - const mbs = ((byteCount / 1024) * (1 / (time / 1000))).toFixed(2); - term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${rendererName} renderer)`); - // Send ^C to get a new prompt - term._core._onData.fire('\x03'); - }); -} - -function loadTestLongLines(): void { - const rendererName = addons.webgl.instance ? 'webgl' : 'dom'; - const testData = []; - let byteCount = 0; - for (let i = 0; i < 50; i++) { - const count = 1 + Math.floor(Math.random() * 500); - byteCount += count + 2; - const data = new Uint8Array(count + 2); - data[0] = 0x0A; // \n - for (let i = 1; i < count + 1; i++) { - data[i] = 0x61 + Math.floor(Math.random() * (0x7A - 0x61)); - } - // End each line with \r so the cursor remains constant, this is what ls/tree do and improves - // performance significantly due to the cursor DOM element not needing to change - data[data.length - 1] = 0x0D; // \r - testData.push(data); - } - const start = performance.now(); - for (let i = 0; i < 1024 * 50; i++) { - for (const d of testData) { - term.write(d); - } - } - // Wait for all data to be parsed before evaluating time - term.write('', () => { - const time = Math.round(performance.now() - start); - const mbs = ((byteCount / 1024) * (1 / (time / 1000))).toFixed(2); - term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${rendererName} renderer)`); - // Send ^C to get a new prompt - term._core._onData.fire('\x03'); - }); -} - -function powerlineSymbolTest(): void { - function s(char: string): string { - return `${char} \x1b[7m${char}\x1b[0m `; - } - term.write('\n\n\r'); - term.writeln('Standard powerline symbols:'); - term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F'); - term.writeln(`0xA_ ${s('\ue0a0')}${s('\ue0a1')}${s('\ue0a2')}`); - term.writeln(`0xB_ ${s('\ue0b0')}${s('\ue0b1')}${s('\ue0b2')}${s('\ue0b3')}`); - term.writeln(''); - term.writeln( - `\x1b[7m` + - ` inverse \ue0b1 \x1b[0;40m\ue0b0` + - ` 0 \ue0b1 \x1b[30;41m\ue0b0\x1b[39m` + - ` 1 \ue0b1 \x1b[31;42m\ue0b0\x1b[39m` + - ` 2 \ue0b1 \x1b[32;43m\ue0b0\x1b[39m` + - ` 3 \ue0b1 \x1b[33;44m\ue0b0\x1b[39m` + - ` 4 \ue0b1 \x1b[34;45m\ue0b0\x1b[39m` + - ` 5 \ue0b1 \x1b[35;46m\ue0b0\x1b[39m` + - ` 6 \ue0b1 \x1b[36;47m\ue0b0\x1b[30m` + - ` 7 \ue0b1 \x1b[37;49m\ue0b0\x1b[0m` - ); - term.writeln(''); - term.writeln( - `\x1b[7m` + - ` inverse \ue0b3 \x1b[0;7;40m\ue0b2\x1b[27m` + - ` 0 \ue0b3 \x1b[7;30;41m\ue0b2\x1b[27;39m` + - ` 1 \ue0b3 \x1b[7;31;42m\ue0b2\x1b[27;39m` + - ` 2 \ue0b3 \x1b[7;32;43m\ue0b2\x1b[27;39m` + - ` 3 \ue0b3 \x1b[7;33;44m\ue0b2\x1b[27;39m` + - ` 4 \ue0b3 \x1b[7;34;45m\ue0b2\x1b[27;39m` + - ` 5 \ue0b3 \x1b[7;35;46m\ue0b2\x1b[27;39m` + - ` 6 \ue0b3 \x1b[7;36;47m\ue0b2\x1b[27;30m` + - ` 7 \ue0b3 \x1b[7;37;49m\ue0b2\x1b[0m` - ); - term.writeln(''); - term.writeln( - `\x1b[7m` + - ` inverse \ue0b5 \x1b[0;40m\ue0b4` + - ` 0 \ue0b5 \x1b[30;41m\ue0b4\x1b[39m` + - ` 1 \ue0b5 \x1b[31;42m\ue0b4\x1b[39m` + - ` 2 \ue0b5 \x1b[32;43m\ue0b4\x1b[39m` + - ` 3 \ue0b5 \x1b[33;44m\ue0b4\x1b[39m` + - ` 4 \ue0b5 \x1b[34;45m\ue0b4\x1b[39m` + - ` 5 \ue0b5 \x1b[35;46m\ue0b4\x1b[39m` + - ` 6 \ue0b5 \x1b[36;47m\ue0b4\x1b[30m` + - ` 7 \ue0b5 \x1b[37;49m\ue0b4\x1b[0m` - ); - term.writeln(''); - term.writeln( - `\x1b[7m` + - ` inverse \ue0b7 \x1b[0;7;40m\ue0b6\x1b[27m` + - ` 0 \ue0b7 \x1b[7;30;41m\ue0b6\x1b[27;39m` + - ` 1 \ue0b7 \x1b[7;31;42m\ue0b6\x1b[27;39m` + - ` 2 \ue0b7 \x1b[7;32;43m\ue0b6\x1b[27;39m` + - ` 3 \ue0b7 \x1b[7;33;44m\ue0b6\x1b[27;39m` + - ` 4 \ue0b7 \x1b[7;34;45m\ue0b6\x1b[27;39m` + - ` 5 \ue0b7 \x1b[7;35;46m\ue0b6\x1b[27;39m` + - ` 6 \ue0b7 \x1b[7;36;47m\ue0b6\x1b[27;30m` + - ` 7 \ue0b7 \x1b[7;37;49m\ue0b6\x1b[0m` - ); - term.writeln(''); - term.writeln('Powerline extra symbols:'); - term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F'); - term.writeln(`0xA_ ${s('\ue0a3')}`); - term.writeln(`0xB_ ${s('\ue0b4')}${s('\ue0b5')}${s('\ue0b6')}${s('\ue0b7')}${s('\ue0b8')}${s('\ue0b9')}${s('\ue0ba')}${s('\ue0bb')}${s('\ue0bc')}${s('\ue0bd')}${s('\ue0be')}${s('\ue0bf')}`); - term.writeln(`0xC_ ${s('\ue0c0')}${s('\ue0c1')}${s('\ue0c2')}${s('\ue0c3')}${s('\ue0c4')}${s('\ue0c5')}${s('\ue0c6')}${s('\ue0c7')}${s('\ue0c8')}${s('\ue0c9')}${s('\ue0ca')}${s('\ue0cb')}${s('\ue0cc')}${s('\ue0cd')}${s('\ue0be')}${s('\ue0bf')}`); - term.writeln(`0xD_ ${s('\ue0d0')}${s('\ue0d1')}${s('\ue0d2')} ${s('\ue0d4')}`); - term.writeln(''); - term.writeln('Sample of nerd fonts icons:'); - term.writeln(' nf-linux-apple (\\uF302) \uf302'); - term.writeln('nf-mdi-github_face (\\uFbd9) \ufbd9'); -} - -function underlineTest(): void { - function u(style: number): string { - return `\x1b[4:${style}m`; - } - function c(color: string): string { - return `\x1b[58:${color}m`; - } - term.write('\n\n\r'); - term.writeln('Underline styles:'); - term.writeln(''); - function showSequence(id: number, name: string): 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 `${u(id)}4:${id}m - ${name}\x1b[4:0m`.padEnd(33, ' ') + `${u(id)}${alphabet} ${numbers} 汉语 한국어 👽\x1b[4:0m`; - } - term.writeln(showSequence(0, 'No underline')); - term.writeln(showSequence(1, 'Straight')); - term.writeln(showSequence(2, 'Double')); - term.writeln(showSequence(3, 'Curly')); - term.writeln(showSequence(4, 'Dotted')); - term.writeln(showSequence(5, 'Dashed')); - term.writeln(''); - term.writeln(`Underline colors (256 color mode):`); - term.writeln(''); - for (let i = 0; i < 256; i++) { - term.write((i !== 0 ? '\x1b[0m, ' : '') + u(1 + i % 5) + c('5:' + i) + i); - } - term.writeln(`\x1b[0m\n\n\rUnderline colors (true color mode):`); - term.writeln(''); - for (let i = 0; i < 80; i++) { - const v = Math.round(i / 79 * 255); - term.write(u(1) + c(`2:0:${v}:${v}:${v}`) + (i < 4 ? 'grey'[i] : ' ')); - } - term.write('\n\r'); - for (let i = 0; i < 80; i++) { - const v = Math.round(i / 79 * 255); - term.write(u(1) + c(`2:0:${v}:${0}:${0}`) + (i < 3 ? 'red'[i] : ' ')); - } - term.write('\n\r'); - for (let i = 0; i < 80; i++) { - const v = Math.round(i / 79 * 255); - term.write(u(1) + c(`2:0:${0}:${v}:${0}`) + (i < 5 ? 'green'[i] : ' ')); - } - term.write('\n\r'); - for (let i = 0; i < 80; i++) { - const v = Math.round(i / 79 * 255); - term.write(u(1) + c(`2:0:${0}:${0}:${v}`) + (i < 4 ? 'blue'[i] : ' ')); - } - term.write('\x1b[0m\n\r'); -} - -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`); - } - - term.writeln(`\x1b[0m\n\n\rColors 17-231 from 256 palette:`); - for (let i = 0; i < 6; i++) { - const startId = 16 + i * 36; - const endId = 16 + (i + 1) * 36 - 1; - term.write(`${startId.toString().padStart(3, ' ')}-${endId.toString().padStart(3, ' ')} `); - for (let j = 0; j < 36; j++) { - const id = 16 + i * 36 + j; - term.write(`\x1b[48;5;${id}m${(id % 10).toString().padStart(2, ' ')}\x1b[0m`); - } - term.write(`\r\n`); - } - - term.writeln(`\x1b[0m\n\rGreyscale from 256 palette:`); - term.write('232-255 '); - for (let i = 232; i < 256; i++) { - term.write(`\x1b[48;5;${i}m ${(i % 10)} \x1b[0m`); - } -} - -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' }, - { ps: 53, name: 'Overlined' }, - { ps: 55, name: 'Not overlined' } - ]; - const maxNameLength = entries.reduce((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`); - } - const entriesByPs: Map = new Map(); - for (const e of entries) { - entriesByPs.set(e.ps, e.name); - } - const comboEntries: { ps: number[] }[] = [ - { ps: [1, 2, 3, 4, 5, 6, 7, 9] }, - { ps: [2, 41] }, - { ps: [4, 53] } - ]; - term.write('\n\n\r'); - term.writeln(`Combinations`); - for (const e of comboEntries) { - const name = e.ps.map(e => entriesByPs.get(e)).join(', '); - term.writeln(`\x1b[0m\x1b[${e.ps.join(';')}m ${name}\n\r${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'); - term.writeln('\x1b]8;;https://xtermjs.org\x07https://xtermjs.org\x1b]8;;\x07\x1b[C<- null cell'); - term.writeln(`\nAdjacent links:`); - term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;https://xtermjs.org\x07\x1b[32mxterm.js\x1b[0m\x1b]8;;\x07'); - term.writeln(`\nShared ID link (underline should be shared):`); - term.writeln('╔════╗'); - term.writeln('║\x1b]8;id=testid;https://github.com\x07GitH\x1b]8;;\x07║'); - term.writeln('║\x1b]8;id=testid;https://github.com\x07ub\x1b]8;;\x07 ║'); - term.writeln('╚════╝'); - term.writeln(`\nWrapped link with no ID (not necessarily meant to share underline):`); - term.writeln('╔════╗'); - term.writeln('║ ║'); - term.writeln('║ ║'); - term.writeln('╚════╝'); - term.write('\x1b[3A\x1b[1C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[5D'); -} - -/** - * Prints the 20977 characters from the CJK Unified Ideographs unicode block. - */ -function addCjk(): void { - term.write('\n\n\r'); - for (let i = 0x4E00; i < 0x9FCC; i++) { - term.write(String.fromCharCode(i)); - } -} - -/** - * Prints the 20977 characters from the CJK Unified Ideographs unicode block with randomized styles. - */ -function addCjkRandomSgr(): void { - term.write('\n\n\r'); - for (let i = 0x4E00; i < 0x9FCC; i++) { - term.write(`\x1b[${getRandomSgr()}m${String.fromCharCode(i)}\x1b[0m`); - } -} -const randomSgrAttributes = [ - '1', '2', '3', '4', '5', '6', '7', '9', - '21', '22', '23', '24', '25', '26', '27', '28', '29', - '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', - '40', '41', '42', '43', '44', '45', '46', '47', '48', '49' -]; -function getRandomSgr(): string { - return randomSgrAttributes[Math.floor(Math.random() * randomSgrAttributes.length)]; -} - -function addGraphemeClusters(): void { - term.write('\n\n\r'); - term.writeln('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣 [Simple emoji v6: 10 cells, v15: 20 cells]'); - term.writeln('\u{1F476}\u{1F3FF}\u{1F476} [baby with emoji modifier fitzpatrick type-6; baby]'); - term.writeln('\u{1F469}\u200d\u{1f469}\u200d\u{1f466} [woman+zwj+woman+zwj+boy]'); - term.writeln('\u{1F64B}\u{1F64B}\u{200D}\u{2642}\u{FE0F} [person/man raising hand]'); - term.writeln('\u{1F3CB}\u{FE0F}=\u{1F3CB}\u{1F3FE}\u{200D}\u{2640}\u{FE0F} [person lifting weights emoji; woman lighting weights, medium dark]'); - term.writeln('\u{1F469}\u{1F469}\u{200D}\u{1F393}\u{1F468}\u{1F3FF}\u{200D}\u{1F393} [woman; woman student; man student dark]'); - term.writeln('\u{1f1f3}\u{1f1f4}_ [REGIONAL INDICATOR SYMBOL LETTER N and RI O]'); - term.writeln('\u{1f1f3}_\u{1f1f4} {RI N; underscore; RI O]'); - term.writeln('\u0061\u0301 [letter a with acute accent]'); - term.writeln('\u1100\u1161\u11A8=\u1100\u1161= [Korean Jamo]'); - term.writeln('\uAC00=\uD685= [Hangul syllables (pre-composed)]'); - term.writeln('(\u26b0\ufe0e) [coffin with text_presentation]'); - term.writeln('(\u26b0\ufe0f) [coffin with Emoji_presentation]'); - term.writeln(' [Égalité (using separate acute) emoij_presentation]'); -} - -function addDecoration(): void { - term.options['overviewRuler'] = { width: 14 }; - const marker = term.registerMarker(1); - const decoration = term.registerDecoration({ - marker, - backgroundColor: '#00FF00', - foregroundColor: '#00FE00', - overviewRulerOptions: { color: '#ef292980', position: 'left' } - }); - decoration.onRender((e: HTMLElement) => { - e.style.right = '100%'; - e.style.backgroundColor = '#ef292980'; - }); -} - -function addOverviewRuler(): void { - term.options['overviewRuler'] = { width: 14 }; - term.registerDecoration({ marker: term.registerMarker(1), overviewRulerOptions: { color: '#ef2929' } }); - term.registerDecoration({ marker: term.registerMarker(3), overviewRulerOptions: { color: '#8ae234' } }); - term.registerDecoration({ marker: term.registerMarker(5), overviewRulerOptions: { color: '#729fcf' } }); - term.registerDecoration({ marker: term.registerMarker(7), overviewRulerOptions: { color: '#ef2929', position: 'left' } }); - term.registerDecoration({ marker: term.registerMarker(7), overviewRulerOptions: { color: '#8ae234', position: 'center' } }); - term.registerDecoration({ marker: term.registerMarker(7), overviewRulerOptions: { color: '#729fcf', position: 'right' } }); - term.registerDecoration({ marker: term.registerMarker(10), overviewRulerOptions: { color: '#8ae234', position: 'center' } }); - term.registerDecoration({ marker: term.registerMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' } }); -} - -let decorationStressTestDecorations: IDisposable[] | undefined; -function decorationStressTest(): void { - if (decorationStressTestDecorations) { - for (const d of decorationStressTestDecorations) { - d.dispose(); - } - decorationStressTestDecorations = undefined; - } else { - const t = term as Terminal; - const buffer = t.buffer.active; - const cursorY = buffer.baseY + buffer.cursorY; - decorationStressTestDecorations = []; - for (const x of [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]) { - for (let y = 0; y < t.buffer.active.length; y++) { - const cursorOffsetY = y - cursorY; - decorationStressTestDecorations.push(t.registerDecoration({ - marker: t.registerMarker(cursorOffsetY), - x, - width: 4, - backgroundColor: '#FF0000', - overviewRulerOptions: { color: '#FF0000' } - })); - } - } - } -} - -(console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => { - function getBox(width: number, height: number): any { - return { - string: '+', - style: 'font-size: 1px; padding: ' + Math.floor(height/2) + 'px ' + Math.floor(width/2) + 'px; line-height: ' + height + 'px;' - }; - } - if (source instanceof HTMLCanvasElement) { - source = source.getContext('2d')?.getImageData(0, 0, source.width, source.height)!; - } - const canvas = document.createElement('canvas'); - canvas.width = source.width; - canvas.height = source.height; - const ctx = canvas.getContext('2d')!; - ctx.putImageData(source, 0, 0); - - const sw = source.width * scale; - const sh = source.height * scale; - const dim = getBox(sw, sh); - console.log( - `Image: ${source.width} x ${source.height}\n%c${dim.string}`, - `${dim.style}background: url(${canvas.toDataURL()}); background-size: ${sw}px ${sh}px; background-repeat: no-repeat; color: transparent;` - ); - console.groupCollapsed('Zoomed'); - console.log( - `%c${dim.string}`, - `${getBox(sw * 10, sh * 10).style}background: url(${canvas.toDataURL()}); background-size: ${sw * 10}px ${sh * 10}px; background-repeat: no-repeat; color: transparent; image-rendering: pixelated;-ms-interpolation-mode: nearest-neighbor;` - ); - console.groupEnd(); -}; - -function addVtButtons(): void { - function csi(e: string): string { - return `\x1b[${e}`; - } - - function createButton(name: string, description: string, writeCsi: string, paramCount: number = 1): HTMLElement { - const inputs: HTMLInputElement[] = []; - for (let i = 0; i < paramCount; i++) { - const input = document.createElement('input'); - input.type = 'number'; - input.title = `Input #${i + 1}`; - inputs.push(input); - } - - const element = document.createElement('button'); - element.textContent = name; - writeCsi.split(''); - const prefix = writeCsi.length === 2 ? writeCsi[0] : ''; - const suffix = writeCsi[writeCsi.length - 1]; - element.addEventListener(`click`, () => term.write(csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`))); - - const desc = document.createElement('span'); - desc.textContent = description; - - const container = document.createElement('div'); - container.classList.add('vt-button'); - container.append(element, ...inputs, desc); - return container; - } - const vtFragment = document.createDocumentFragment(); - const buttonSpecs: { [key: string]: { label: string, description: string, paramCount?: number }} = { - A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' }, - B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' }, - C: { label: 'CUF →', description: 'Cursor Forward Ps Times' }, - D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' }, - E: { label: 'CNL', description: 'Cursor Next Line Ps Times' }, - F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' }, - G: { label: 'CHA', description: 'Cursor Character Absolute' }, - H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 }, - I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' }, - J: { label: 'ED', description: 'Erase in Display' }, - '?J': { label: 'DECSED', description: 'Erase in Display' }, - K: { label: 'EL', description: 'Erase in Line' }, - '?K': { label: 'DECSEL', description: 'Erase in Line' }, - L: { label: 'IL', description: 'Insert Ps Line(s)' }, - M: { label: 'DL', description: 'Delete Ps Line(s)' }, - P: { label: 'DCH', description: 'Delete Ps Character(s)' } - }; - for (const s of Object.keys(buttonSpecs)) { - const spec = buttonSpecs[s]; - vtFragment.appendChild(createButton(spec.label, spec.description, s, spec.paramCount)); - } - - document.querySelector('#vt-container').appendChild(vtFragment); -} - -function testWeblinks(): void { - const linkExamples = ` -aaa http://example.com aaa http://example.com aaa -¥¥¥ http://example.com aaa http://example.com aaa -aaa http://example.com ¥¥¥ http://example.com aaa -¥¥¥ http://example.com ¥¥¥ http://example.com aaa -aaa https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 aaa -¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥ -aaa http://test:password@example.com/some_path aaa -brackets enclosed: -aaa [http://example.de] aaa -aaa (http://example.de) aaa -aaa aaa -aaa {http://example.de} aaa -ipv6 https://[::1]/with/some?vars=and&a#hash aaa -stop at final '.': This is a sentence with an url to http://example.com. -stop at final '?': Is this the right url http://example.com/? -stop at final '?': Maybe this one http://example.com/with?arguments=false? -`; - term.write(linkExamples.split('\n').join('\r\n')); -} - - -function coloredErase(): void { - const sp5 = ' '; - const data = ` -Test BG-colored Erase (BCE): - The color block in the following lines should look identical. - For newly created rows at the bottom the last color should be applied - for all cells to the right. - - def 41 42 43 44 45 46 47\x1b[47m -\x1b[m${sp5}\x1b[41m${sp5}\x1b[42m${sp5}\x1b[43m${sp5}\x1b[44m${sp5}\x1b[45m${sp5}\x1b[46m${sp5}\x1b[47m${sp5} -\x1b[m\x1b[5X\x1b[41m\x1b[5C\x1b[5X\x1b[42m\x1b[5C\x1b[5X\x1b[43m\x1b[5C\x1b[5X\x1b[44m\x1b[5C\x1b[5X\x1b[45m\x1b[5C\x1b[5X\x1b[46m\x1b[5C\x1b[5X\x1b[47m\x1b[5C\x1b[5X\x1b[m -`; - term.write(data.split('\n').join('\r\n')); -} - - -function initImageAddonExposed(): void { - const DEFAULT_OPTIONS: IImageAddonOptions = (addons.image.instance as any)._defaultOpts; - const limitStorageElement = document.querySelector('#image-storagelimit'); - limitStorageElement.valueAsNumber = addons.image.instance.storageLimit; - addDomListener(limitStorageElement, 'change', () => { - try { - addons.image.instance.storageLimit = limitStorageElement.valueAsNumber; - limitStorageElement.valueAsNumber = addons.image.instance.storageLimit; - console.log('changed storageLimit to', addons.image.instance.storageLimit); - } catch (e) { - limitStorageElement.valueAsNumber = addons.image.instance.storageLimit; - console.log('storageLimit at', addons.image.instance.storageLimit); - throw e; - } - }); - const showPlaceholderElement = document.querySelector('#image-showplaceholder'); - showPlaceholderElement.checked = addons.image.instance.showPlaceholder; - addDomListener(showPlaceholderElement, 'change', () => { - addons.image.instance.showPlaceholder = showPlaceholderElement.checked; - }); - const ctorOptionsElement = document.querySelector('#image-options'); - ctorOptionsElement.value = JSON.stringify(DEFAULT_OPTIONS, null, 2); - - const sixelDemo = (url: string) => () => fetch(url) - .then(resp => resp.arrayBuffer()) - .then(buffer => { - term.write('\r\n'); - term.write(new Uint8Array(buffer)); - }); - - const iipDemo = (url: string) => () => fetch(url) - .then(resp => resp.arrayBuffer()) - .then(buffer => { - const data = new Uint8Array(buffer); - let sdata = ''; - for (let i = 0; i < data.length; ++i) sdata += String.fromCharCode(data[i]); - term.write('\r\n'); - term.write(`\x1b]1337;File=inline=1;size=${data.length}:${btoa(sdata)}\x1b\\`); - }); - - document.getElementById('image-demo1').addEventListener('click', - sixelDemo('https://raw.githubusercontent.com/saitoha/libsixel/master/images/snake.six')); - document.getElementById('image-demo2').addEventListener('click', - sixelDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/testfiles/test2.sixel')); - document.getElementById('image-demo3').addEventListener('click', - iipDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png')); - - // demo for image retrieval API - term.element.addEventListener('click', (ev: MouseEvent) => { - if (!ev.ctrlKey || !addons.image.instance) return; - - // TODO... - // if (ev.altKey) { - // const sel = term.getSelectionPosition(); - // if (sel) { - // addons.image.instance - // .extractCanvasAtBufferRange(term.getSelectionPosition()) - // ?.toBlob(data => window.open(URL.createObjectURL(data), '_blank')); - // return; - // } - // } - - const pos = term._core._mouseService!.getCoords(ev, term._core.screenElement!, term.cols, term.rows); - const x = pos[0] - 1; - const y = pos[1] - 1; - const canvas = ev.shiftKey - // ctrl+shift+click: get single tile - ? addons.image.instance.extractTileAtBufferCell(x, term.buffer.active.viewportY + y) - // ctrl+click: get original image - : addons.image.instance.getImageAtBufferCell(x, term.buffer.active.viewportY + y); - canvas?.toBlob(data => window.open(URL.createObjectURL(data), '_blank')); - }); -} - -function testEvents(): void { - document.getElementById('event-focus').addEventListener('click', ()=> term.focus()); - document.getElementById('event-blur').addEventListener('click', ()=> term.blur()); -} - -function testWebfonts() { - document.getElementById('webfont-kongtext').addEventListener('click', async () => { - const ff = new FontFace('Kongtext', "url(/kongtext.regular.ttf) format('truetype')"); - await loadFonts([ff]); - term.options.fontFamily = 'Kongtext'; - term.options.lineHeight = 1.3; - addons.fit.instance?.fit(); - setTimeout(() => term.write('\x1b[?12h\x1b]12;#776CF9\x07\x1b[38;2;119;108;249;48;2;21;8;150m\x1b[2J\x1b[2;5H**** COMMODORE 64 BASIC V2 ****\r\n\r\n 64K RAM SYSTEM 38911 BASIC BYTES FREE\r\n\r\nREADY.\r\nLOAD '), 1000); - setTimeout(() => {term.write('🤣\x1b[m\x1b[99;1H'); term.input('\r');}, 5000); - }); - document.getElementById('webfont-bpdots').addEventListener('click', async () => { - document.styleSheets[0].insertRule("@font-face { font-family: 'BPdots'; src: url(/bpdots.regular.otf) format('opentype'); weight: 400 }", 0); - await loadFonts(['BPdots']); - term.options.fontFamily = 'BPdots'; - term.options.lineHeight = 1.3; - term.options.fontSize = 20; - addons.fit.instance?.fit(); - }); -} diff --git a/demo/client/client.ts b/demo/client/client.ts index 6a0cdead..68202ad7 100644 --- a/demo/client/client.ts +++ b/demo/client/client.ts @@ -18,6 +18,7 @@ import { AttachAddon } from '@xterm/addon-attach'; import { AddonImageWindow } from './components/window/addonImageWindow'; import { AddonSearchWindow } from './components/window/addonSearchWindow'; import { AddonSerializeWindow } from './components/window/addonSerializeWindow'; +import { AddonWebFontsWindow } from './components/window/addonWebFontsWindow'; import { AddonsWindow } from './components/window/addonsWindow'; import { CellInspectorWindow } from './components/window/cellInspectorWindow'; import { ControlBar } from './components/controlBar'; @@ -32,6 +33,7 @@ import { LigaturesAddon } from '@xterm/addon-ligatures'; import { ProgressAddon } from '@xterm/addon-progress'; import { SearchAddon, ISearchOptions } from '@xterm/addon-search'; import { SerializeAddon } from '@xterm/addon-serialize'; +import { WebFontsAddon } from '@xterm/addon-web-fonts'; import { WebLinksAddon } from '@xterm/addon-web-links'; import { WebglAddon } from '@xterm/addon-webgl'; import { Unicode11Addon } from '@xterm/addon-unicode11'; @@ -75,6 +77,7 @@ const addons: AddonCollection = { progress: { name: 'progress', ctor: ProgressAddon, canChange: true }, search: { name: 'search', ctor: SearchAddon, canChange: true }, serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true }, + webFonts: { name: 'webFonts', ctor: WebFontsAddon, canChange: true }, webLinks: { name: 'webLinks', ctor: WebLinksAddon, canChange: true }, webgl: { name: 'webgl', ctor: WebglAddon, canChange: true }, unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true }, @@ -213,7 +216,8 @@ if (document.location.pathname === '/test') { addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true }); controlBar.registerWindow(new AddonSerializeWindow(typedTerm, addons), { afterId: 'addon-search', hidden: true, italics: true }); controlBar.registerWindow(new AddonImageWindow(typedTerm, addons), { afterId: 'addon-serialize', hidden: true, italics: true }); - addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true }); + controlBar.registerWindow(new AddonWebFontsWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true }); + addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-web-fonts', hidden: true, italics: true }); controlBar.registerWindow(new TestWindow(typedTerm, addons, { disposeRecreateButtonHandler, createNewWindowButtonHandler }), { afterId: 'options' }); actionElements = { findNext: addonSearchWindow.findNextInput, @@ -229,6 +233,7 @@ if (document.location.pathname === '/test') { controlBar.setTabVisible('addon-search', true); controlBar.setTabVisible('addon-serialize', true); controlBar.setTabVisible('addon-image', true); + controlBar.setTabVisible('addon-web-fonts', true); addonWebglWindow.setTextureAtlas(addons.webgl.instance.textureAtlas); addons.webgl.instance.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e)); addons.webgl.instance.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e)); @@ -295,6 +300,7 @@ function createTerminal(): Terminal { console.warn(e); } addons.webLinks.instance = new WebLinksAddon(); + addons.webFonts.instance = new WebFontsAddon(); typedTerm.loadAddon(addons.fit.instance); typedTerm.loadAddon(addons.image.instance); typedTerm.loadAddon(addons.progress.instance); @@ -302,6 +308,7 @@ function createTerminal(): Terminal { typedTerm.loadAddon(addons.serialize.instance); typedTerm.loadAddon(addons.unicodeGraphemes.instance); typedTerm.loadAddon(addons.webLinks.instance); + typedTerm.loadAddon(addons.webFonts.instance); typedTerm.loadAddon(addons.clipboard.instance); window.term = term; // Expose `term` to window for debugging purposes diff --git a/demo/client/components/window/addonWebFontsWindow.ts b/demo/client/components/window/addonWebFontsWindow.ts new file mode 100644 index 00000000..a56f0a56 --- /dev/null +++ b/demo/client/components/window/addonWebFontsWindow.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2024 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { loadFonts } from '@xterm/addon-web-fonts'; +import { BaseWindow } from './baseWindow'; +import type { IControlWindow } from '../controlBar'; + +export class AddonWebFontsWindow extends BaseWindow implements IControlWindow { + public readonly id = 'addon-web-fonts'; + public readonly label = 'web-fonts'; + + public build(container: HTMLElement): void { + const dl = document.createElement('dl'); + + // Kongtext font button + const dtKongtext = document.createElement('dt'); + dtKongtext.textContent = 'Kongtext'; + dl.appendChild(dtKongtext); + + const ddKongtext = document.createElement('dd'); + const btnKongtext = document.createElement('button'); + btnKongtext.textContent = 'Load Kongtext'; + btnKongtext.title = 'Load Kongtext font and apply C64 style'; + btnKongtext.addEventListener('click', async () => { + const ff = new FontFace('Kongtext', 'url(/kongtext.regular.ttf) format(\'truetype\')'); + await loadFonts([ff]); + this._terminal.options.fontFamily = 'Kongtext'; + this._terminal.options.lineHeight = 1.3; + this._addons.fit.instance?.fit(); + setTimeout(() => this._terminal.write('\x1b[?12h\x1b]12;#776CF9\x07\x1b[38;2;119;108;249;48;2;21;8;150m\x1b[2J\x1b[2;5H**** COMMODORE 64 BASIC V2 ****\r\n\r\n 64K RAM SYSTEM 38911 BASIC BYTES FREE\r\n\r\nREADY.\r\nLOAD '), 1000); + setTimeout(() => { this._terminal.write('🤣\x1b[m\x1b[99;1H'); this._terminal.input('\r'); }, 5000); + }); + ddKongtext.appendChild(btnKongtext); + dl.appendChild(ddKongtext); + + // BPdots font button + const dtBpdots = document.createElement('dt'); + dtBpdots.textContent = 'BPdots'; + dl.appendChild(dtBpdots); + + const ddBpdots = document.createElement('dd'); + const btnBpdots = document.createElement('button'); + btnBpdots.textContent = 'Load BPdots'; + btnBpdots.title = 'Load BPdots font'; + btnBpdots.addEventListener('click', async () => { + document.styleSheets[0].insertRule('@font-face { font-family: "BPdots"; src: url(/bpdots.regular.otf) format("opentype"); weight: 400 }', 0); + await loadFonts(['BPdots']); + this._terminal.options.fontFamily = 'BPdots'; + this._terminal.options.lineHeight = 1.3; + this._terminal.options.fontSize = 20; + this._addons.fit.instance?.fit(); + }); + ddBpdots.appendChild(btnBpdots); + dl.appendChild(ddBpdots); + + container.appendChild(dl); + } +} diff --git a/demo/client/types.ts b/demo/client/types.ts index bc73ae0e..c4845257 100644 --- a/demo/client/types.ts +++ b/demo/client/types.ts @@ -15,10 +15,11 @@ import type { SearchAddon } from '@xterm/addon-search'; import type { SerializeAddon } from '@xterm/addon-serialize'; import type { UnicodeGraphemesAddon } from '@xterm/addon-unicode-graphemes'; import type { Unicode11Addon } from '@xterm/addon-unicode11'; +import type { WebFontsAddon } from '@xterm/addon-web-fonts'; import type { WebLinksAddon } from '@xterm/addon-web-links'; import type { WebglAddon } from '@xterm/addon-webgl'; -export type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webLinks' | 'webgl' | 'ligatures'; +export type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl' | 'ligatures'; export interface IDemoAddon { name: T; @@ -32,6 +33,7 @@ export interface IDemoAddon { T extends 'progress' ? typeof ProgressAddon : T extends 'search' ? typeof SearchAddon : T extends 'serialize' ? typeof SerializeAddon : + T extends 'webFonts' ? typeof WebFontsAddon : T extends 'webLinks' ? typeof WebLinksAddon : T extends 'unicode11' ? typeof Unicode11Addon : T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : @@ -47,6 +49,7 @@ export interface IDemoAddon { T extends 'progress' ? ProgressAddon : T extends 'search' ? SearchAddon : T extends 'serialize' ? SerializeAddon : + T extends 'webFonts' ? WebFontsAddon : T extends 'webLinks' ? WebLinksAddon : T extends 'unicode11' ? Unicode11Addon : T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : From 0e57643f1625b44c8e30a60808d544d09e128f1a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:33:13 -0800 Subject: [PATCH 16/19] Move fonts into folder --- .../client/components/window/addonWebFontsWindow.ts | 4 ++-- demo/{ => fonts}/bpdots.regular.otf | Bin demo/{ => fonts}/font-licenses.txt | 0 demo/{ => fonts}/kongtext.regular.ttf | Bin demo/server/server.ts | 3 +-- 5 files changed, 3 insertions(+), 4 deletions(-) rename demo/{ => fonts}/bpdots.regular.otf (100%) rename demo/{ => fonts}/font-licenses.txt (100%) rename demo/{ => fonts}/kongtext.regular.ttf (100%) diff --git a/demo/client/components/window/addonWebFontsWindow.ts b/demo/client/components/window/addonWebFontsWindow.ts index a56f0a56..c8fb288d 100644 --- a/demo/client/components/window/addonWebFontsWindow.ts +++ b/demo/client/components/window/addonWebFontsWindow.ts @@ -24,7 +24,7 @@ export class AddonWebFontsWindow extends BaseWindow implements IControlWindow { btnKongtext.textContent = 'Load Kongtext'; btnKongtext.title = 'Load Kongtext font and apply C64 style'; btnKongtext.addEventListener('click', async () => { - const ff = new FontFace('Kongtext', 'url(/kongtext.regular.ttf) format(\'truetype\')'); + const ff = new FontFace('Kongtext', 'url(/fonts/kongtext.regular.ttf) format(\'truetype\')'); await loadFonts([ff]); this._terminal.options.fontFamily = 'Kongtext'; this._terminal.options.lineHeight = 1.3; @@ -45,7 +45,7 @@ export class AddonWebFontsWindow extends BaseWindow implements IControlWindow { btnBpdots.textContent = 'Load BPdots'; btnBpdots.title = 'Load BPdots font'; btnBpdots.addEventListener('click', async () => { - document.styleSheets[0].insertRule('@font-face { font-family: "BPdots"; src: url(/bpdots.regular.otf) format("opentype"); weight: 400 }', 0); + document.styleSheets[0].insertRule('@font-face { font-family: "BPdots"; src: url(/fonts/bpdots.regular.otf) format("opentype"); weight: 400 }', 0); await loadFonts(['BPdots']); this._terminal.options.fontFamily = 'BPdots'; this._terminal.options.lineHeight = 1.3; diff --git a/demo/bpdots.regular.otf b/demo/fonts/bpdots.regular.otf similarity index 100% rename from demo/bpdots.regular.otf rename to demo/fonts/bpdots.regular.otf diff --git a/demo/font-licenses.txt b/demo/fonts/font-licenses.txt similarity index 100% rename from demo/font-licenses.txt rename to demo/fonts/font-licenses.txt diff --git a/demo/kongtext.regular.ttf b/demo/fonts/kongtext.regular.ttf similarity index 100% rename from demo/kongtext.regular.ttf rename to demo/fonts/kongtext.regular.ttf diff --git a/demo/server/server.ts b/demo/server/server.ts index 8733ff62..0f4a79b3 100644 --- a/demo/server/server.ts +++ b/demo/server/server.ts @@ -45,8 +45,7 @@ function startServer(): void { res.sendFile(demoRoot + '/index.css'); }); - app.get('/kongtext.regular.ttf', (req, res) => res.sendFile(demoRoot + '/kongtext.regular.ttf')); - app.get('/bpdots.regular.otf', (req, res) => res.sendFile(demoRoot + '/bpdots.regular.otf')); + app.use('/fonts', express.static(demoRoot + '/fonts')); app.use('/dist', express.static(demoRoot + '/dist')); app.use('/src', express.static(demoRoot + '/src')); From b0145998c59c8350fd7710bc4fbfa93e52470046 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:36:31 -0800 Subject: [PATCH 17/19] Fix lint --- demo/client/types.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/demo/client/types.ts b/demo/client/types.ts index c4845257..4790fd4f 100644 --- a/demo/client/types.ts +++ b/demo/client/types.ts @@ -33,12 +33,12 @@ export interface IDemoAddon { T extends 'progress' ? typeof ProgressAddon : T extends 'search' ? typeof SearchAddon : T extends 'serialize' ? typeof SerializeAddon : - T extends 'webFonts' ? typeof WebFontsAddon : - T extends 'webLinks' ? typeof WebLinksAddon : - T extends 'unicode11' ? typeof Unicode11Addon : - T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : - T extends 'webgl' ? typeof WebglAddon : - never + T extends 'webFonts' ? typeof WebFontsAddon : + T extends 'webLinks' ? typeof WebLinksAddon : + T extends 'unicode11' ? typeof Unicode11Addon : + T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : + T extends 'webgl' ? typeof WebglAddon : + never ); instance?: ( T extends 'attach' ? AttachAddon : @@ -49,12 +49,12 @@ export interface IDemoAddon { T extends 'progress' ? ProgressAddon : T extends 'search' ? SearchAddon : T extends 'serialize' ? SerializeAddon : - T extends 'webFonts' ? WebFontsAddon : - T extends 'webLinks' ? WebLinksAddon : - T extends 'unicode11' ? Unicode11Addon : - T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : - T extends 'webgl' ? WebglAddon : - never + T extends 'webFonts' ? WebFontsAddon : + T extends 'webLinks' ? WebLinksAddon : + T extends 'unicode11' ? Unicode11Addon : + T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : + T extends 'webgl' ? WebglAddon : + never ); } From 2cf292986926cf960f29b93a69306ac52a0e8567 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:36:36 -0800 Subject: [PATCH 18/19] Publish webfonts --- bin/publish.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/publish.js b/bin/publish.js index fd676b3a..0fa2fadd 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -51,6 +51,7 @@ const addonPackageDirs = [ path.resolve(__dirname, '../addons/addon-serialize'), path.resolve(__dirname, '../addons/addon-unicode11'), path.resolve(__dirname, '../addons/addon-unicode-graphemes'), + path.resolve(__dirname, '../addons/addon-web-fonts'), path.resolve(__dirname, '../addons/addon-web-links'), path.resolve(__dirname, '../addons/addon-webgl') ]; From c6c4a2ffbce18a022ea40c761b15d79ac9c0d0e4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:39:24 -0800 Subject: [PATCH 19/19] Delete unwanted eslintrc --- .eslintrc.json | 273 ------------------------------------------------- 1 file changed, 273 deletions(-) delete mode 100644 .eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 3d9322d3..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,273 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true, - "node": true - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": [ - "demo/tsconfig.json", - "src/browser/tsconfig.json", - "src/common/tsconfig.json", - "src/headless/tsconfig.json", - "src/vs/tsconfig.json", - "test/benchmark/tsconfig.json", - "test/playwright/tsconfig.json", - "addons/addon-attach/src/tsconfig.json", - "addons/addon-attach/test/tsconfig.json", - "addons/addon-clipboard/src/tsconfig.json", - "addons/addon-clipboard/test/tsconfig.json", - "addons/addon-fit/src/tsconfig.json", - "addons/addon-fit/test/tsconfig.json", - "addons/addon-image/src/tsconfig.json", - "addons/addon-image/test/tsconfig.json", - "addons/addon-ligatures/src/tsconfig.json", - "addons/addon-search/src/tsconfig.json", - "addons/addon-search/test/tsconfig.json", - "addons/addon-serialize/src/tsconfig.json", - "addons/addon-serialize/test/tsconfig.json", - "addons/addon-serialize/benchmark/tsconfig.json", - "addons/addon-unicode11/src/tsconfig.json", - "addons/addon-unicode11/test/tsconfig.json", - "addons/addon-unicode-graphemes/src/tsconfig.json", - "addons/addon-unicode-graphemes/test/tsconfig.json", - "addons/addon-unicode-graphemes/benchmark/tsconfig.json", - "addons/addon-web-fonts/src/tsconfig.json", - "addons/addon-web-fonts/test/tsconfig.json", - "addons/addon-web-links/src/tsconfig.json", - "addons/addon-web-links/test/tsconfig.json", - "addons/addon-webgl/src/tsconfig.json", - "addons/addon-webgl/test/tsconfig.json" - ], - "sourceType": "module" - }, - "ignorePatterns": [ - "addons/*/src/third-party/*.ts", - "src/vs/*", - "out/*", - "out-test/*", - "out-esbuild/*", - "out-esbuild-test/*", - "**/inwasm-sdks/*", - "**/typings/*.d.ts", - "**/node_modules", - "**/*.js", - "**/*.mjs" - ], - "plugins": [ - "@stylistic/ts", - "@typescript-eslint", - "jsdoc" - ], - "rules": { - "@stylistic/ts/indent": [ - "warn", - 2 - ], - "@stylistic/ts/semi": [ - "warn", - "always" - ], - "@stylistic/ts/quotes": [ - "warn", - "single", - { "allowTemplateLiterals": true } - ], - - "@typescript-eslint/array-type": [ - "warn", - { - "default": "array", - "readonly": "generic" - } - ], - "@typescript-eslint/consistent-type-assertions": "warn", - "@typescript-eslint/consistent-type-definitions": "warn", - "@typescript-eslint/explicit-function-return-type": [ - "warn", - { - "allowExpressions": true - } - ], - "@typescript-eslint/explicit-member-accessibility": [ - "warn", - { - "accessibility": "explicit", - "overrides": { - "constructors": "off" - } - } - ], - "@typescript-eslint/member-delimiter-style": [ - "warn", - { - "multiline": { - "delimiter": "semi", - "requireLast": true - }, - "singleline": { - "delimiter": "comma", - "requireLast": false - } - } - ], - "@typescript-eslint/naming-convention": [ - "warn", - { "selector": "default", "format": ["camelCase"], - "filter": { - "regex": "^[a-z]", - "match": true - } - }, - // variableLike - { "selector": "variable", "format": ["camelCase", "UPPER_CASE"] }, - { "selector": "variable", "filter": "^I.+Service$", "format": ["PascalCase"], "prefix": ["I"] }, - // memberLike - { "selector": "memberLike", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require" }, - { "selector": "memberLike", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require" }, - { "selector": "enumMember", "format": ["UPPER_CASE"] }, - // memberLike - Allow enum-like objects to use UPPER_CASE - { "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"], - "filter": { - "regex": "^[a-z]", - "match": true - } - }, - // restrict on* naming for events only - { "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"], "custom": { - "regex": "^on[A-Z].+", - "match": false - } }, - { "selector": "method", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": { - "regex": "^on[A-Z].+", - "match": false - } }, - { "selector": "method", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": { - "regex": "^on[A-Z].+", - "match": false - } }, - // typeLike - { "selector": "typeLike", "format": ["PascalCase"] }, - { "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] } - ], - "@typescript-eslint/no-confusing-void-expression": [ - "warn", - { "ignoreArrowShorthand": true } - ], - "@typescript-eslint/no-useless-constructor": "warn", - "@typescript-eslint/prefer-namespace-keyword": "warn", - "@typescript-eslint/type-annotation-spacing": "warn", - - "comma-dangle": [ - "warn", - { - "objects": "never", - "arrays": "never", - "functions": "never" - } - ], - "curly": [ - "warn", - "multi-line" - ], - "eol-last": "warn", - "eqeqeq": [ - "warn", - "always" - ], - "jsdoc/check-alignment": 1, - "jsdoc/check-param-names": 1, - "jsdoc/no-multi-asterisks": 1, - "keyword-spacing": "warn", - "max-len": [ - "warn", - { - "code": 1000, // Don't enforce for code - "comments": 100, - "ignoreTrailingComments": true, - "ignoreUrls": true, - "ignorePattern": "^ *((?(//|\\*) @vt)|(?\\* \\| )|(?// ))" - } - ], - "new-parens": "warn", - "no-duplicate-imports": "warn", - "no-else-return": [ - "warn", - { - "allowElseIf": false - } - ], - "no-eval": "warn", - "no-extra-semi": "error", - "no-irregular-whitespace": "warn", - "no-restricted-imports": [ - "warn", - { - "patterns": [ - ".*\\/out\\/.*" - ] - } - ], - "no-restricted-syntax": [ - "warn", - { - "selector": "CallExpression[callee.name='requestAnimationFrame']", - "message": "The global requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService." - }, - { - "selector": "CallExpression[callee.name='cancelAnimationFrame']", - "message": "The global cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService." - }, - { - "selector": "CallExpression > MemberExpression[object.name='window'][property.name='requestAnimationFrame']", - "message": "window.requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService." - }, - { - "selector": "CallExpression > MemberExpression[object.name='window'][property.name='cancelAnimationFrame']", - "message": "window.cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService." - }, - { - "selector": "MemberExpression[object.name='window'][property.name='devicePixelRatio']", - "message": "window.devicePixelRatio should be avoided, get it from ICoreBrowserService." - } - ], - "no-trailing-spaces": "warn", - "no-unsafe-finally": "warn", - "no-unused-vars": ["warn", { - "vars": "all", - "args": "none" - }], - "no-var": "warn", - "one-var": [ - "warn", - "never" - ], - "object-curly-spacing": [ - "warn", - "always" - ], - "prefer-const": "warn", - "spaced-comment": [ - "warn", - "always", - { - "markers": ["/"], - "exceptions": ["-"] - } - ] - }, - "overrides": [ - { - "files": [ - "**/*.api.ts", - "**/*.test.ts" - ], - "rules": { - "object-curly-spacing": "off", - "max-len": "off", - "no-unused-vars": "off" - } - } - ] -}