@adecore/terminal 0.0.0-stage → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bas Milius
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,39 @@
1
- # Temporary Holding Version
1
+ # @adecore/terminal
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/@adecore/terminal)](https://www.npmjs.com/package/@adecore/terminal)
4
+ [![Docs](https://img.shields.io/badge/docs-adecore.dev-blue)](https://adecore.dev/terminal/)
5
+
6
+ A terminal pane on [xterm.js](https://xtermjs.org) that looks like the rest of an `@adecore/ui` interface in both themes. It fits the box it is given, opens links in the output and either takes typing or only shows output. The app feeds it output and reads back what is on screen through a handle, without touching xterm's addons.
7
+
8
+ **[Documentation](https://adecore.dev/terminal/)**
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ bun add @adecore/terminal @xterm/xterm @xterm/addon-fit @xterm/addon-web-links @xterm/addon-webgl
14
+ ```
15
+
16
+ xterm and its addons are peer dependencies, so the app picks the version. Import xterm's stylesheet and this package's after the theme of `@adecore/ui`:
17
+
18
+ ```css
19
+ @import "tailwindcss";
20
+ @import "@adecore/ui/theme.css";
21
+ @import "@xterm/xterm/css/xterm.css";
22
+ @import "@adecore/terminal/terminal.css";
23
+ ```
24
+
25
+ ## Use
26
+
27
+ ```tsx
28
+ import { TerminalView, type TerminalViewHandle } from '@adecore/terminal';
29
+
30
+ const view = useRef<TerminalViewHandle>(null);
31
+
32
+ useEffect(() => session.onOutput((data) => view.current?.write(data)), [session]);
33
+
34
+ <TerminalView ref={view} label="Shell" onData={(data) => session.write(data)} onResize={(cols, rows) => session.resize(cols, rows)} className="h-full" />
35
+ ```
36
+
37
+ ## License
38
+
39
+ MIT
@@ -0,0 +1,34 @@
1
+ import { type Ref } from 'react';
2
+ import { Terminal } from '@xterm/xterm';
3
+ export interface TerminalSize {
4
+ cols: number;
5
+ rows: number;
6
+ }
7
+ export interface TerminalViewHandle {
8
+ readonly terminal: Terminal | null;
9
+ write(data: string, done?: () => void): void;
10
+ reset(): void;
11
+ focus(): void;
12
+ blur(): void;
13
+ paste(text: string): void;
14
+ selectAll(): void;
15
+ selection(): string;
16
+ size(): TerminalSize;
17
+ visibleText(): string;
18
+ followGrid(size: TerminalSize): void;
19
+ }
20
+ export interface TerminalViewProps {
21
+ ref?: Ref<TerminalViewHandle>;
22
+ onData?(data: string): void;
23
+ onResize?(cols: number, rows: number): void;
24
+ onOpenLink?(uri: string): void;
25
+ fontSize?: number;
26
+ lineHeight?: number;
27
+ readOnly?: boolean;
28
+ convertEol?: boolean;
29
+ webgl?: boolean;
30
+ scaledByAncestor?: boolean;
31
+ label?: string;
32
+ className?: string;
33
+ }
34
+ export declare function TerminalView({ ref, onData, onResize, onOpenLink, fontSize, lineHeight, readOnly, convertEol, webgl, scaledByAncestor, label, className }: TerminalViewProps): import("react").JSX.Element;
@@ -0,0 +1,196 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useImperativeHandle, useRef } from 'react';
3
+ import clsx from 'clsx';
4
+ import { FitAddon } from '@xterm/addon-fit';
5
+ import { WebLinksAddon } from '@xterm/addon-web-links';
6
+ import { Terminal } from '@xterm/xterm';
7
+ import { readTerminalFont, readTerminalTheme } from './theme.js';
8
+ import { webglBudget } from './webgl-budget.js';
9
+ import { fitToHost, followAncestorScale } from './xterm-internals.js';
10
+ const RESIZE_DEBOUNCE_MS = 50;
11
+ /* What the theme and the font are read from: the theme attribute and the tokens an app sets inline on `<html>`. */
12
+ const ROOT_ATTRIBUTES = ['data-theme', 'style', 'class'];
13
+ let terminals = 0;
14
+ /* A terminal on xterm.js that fits its container and follows the theme. The app feeds it through the handle. */
15
+ export function TerminalView({ ref, onData, onResize, onOpenLink, fontSize = 13, lineHeight = 1, readOnly = false, convertEol = false, webgl = false, scaledByAncestor = false, label, className }) {
16
+ const hostRef = useRef(null);
17
+ const live = useRef(null);
18
+ // Filled with the first props, so the mount reads them before the effect below brings them up to date.
19
+ const latest = useRef({ onData, onResize, onOpenLink, fontSize, lineHeight, scaledByAncestor });
20
+ useEffect(() => {
21
+ latest.current = { onData, onResize, onOpenLink, fontSize, lineHeight, scaledByAncestor };
22
+ });
23
+ useImperativeHandle(ref, () => ({
24
+ get terminal() {
25
+ return live.current?.term ?? null;
26
+ },
27
+ write: (data, done) => {
28
+ const current = live.current;
29
+ if (!current) {
30
+ return;
31
+ }
32
+ current.term.write(data, done);
33
+ if (current.webgl) {
34
+ // A terminal that is being written to outranks an idle one when contexts are scarce.
35
+ webglBudget.touch(current.id);
36
+ }
37
+ },
38
+ // Through the parser (RIS), since `Terminal.reset()` runs at once and output still queued would land on the fresh screen.
39
+ reset: () => live.current?.term.write('\x1bc'),
40
+ focus: () => {
41
+ live.current?.term.focus();
42
+ if (live.current?.webgl) {
43
+ webglBudget.focus(live.current.id);
44
+ }
45
+ },
46
+ blur: () => {
47
+ live.current?.term.blur();
48
+ if (live.current?.webgl) {
49
+ webglBudget.blur(live.current.id);
50
+ }
51
+ },
52
+ paste: (text) => live.current?.term.paste(text),
53
+ selectAll: () => live.current?.term.selectAll(),
54
+ selection: () => live.current?.term.getSelection() ?? '',
55
+ size: () => ({ cols: live.current?.term.cols ?? 80, rows: live.current?.term.rows ?? 24 }),
56
+ visibleText: () => {
57
+ const term = live.current?.term;
58
+ if (!term) {
59
+ return '';
60
+ }
61
+ const buffer = term.buffer.active;
62
+ const lines = [];
63
+ for (let row = 0; row < term.rows; row++) {
64
+ lines.push(buffer.getLine(buffer.viewportY + row)?.translateToString(true) ?? '');
65
+ }
66
+ return lines.join('\n').replace(/\s+$/, '');
67
+ },
68
+ followGrid: (size) => live.current?.followGrid(size)
69
+ }), []);
70
+ useEffect(() => {
71
+ const host = hostRef.current;
72
+ if (!host) {
73
+ return;
74
+ }
75
+ const initial = latest.current;
76
+ const term = new Terminal({
77
+ theme: readTerminalTheme(),
78
+ fontFamily: readTerminalFont(),
79
+ fontSize: initial.fontSize,
80
+ lineHeight: initial.lineHeight,
81
+ cursorBlink: true,
82
+ scrollback: 5000,
83
+ macOptionIsMeta: true
84
+ });
85
+ const fit = new FitAddon();
86
+ term.loadAddon(fit);
87
+ term.loadAddon(new WebLinksAddon(latest.current.onOpenLink ? (_event, uri) => latest.current.onOpenLink?.(uri) : undefined));
88
+ term.open(host);
89
+ if (initial.scaledByAncestor) {
90
+ followAncestorScale(term);
91
+ }
92
+ fitToHost(term, fit);
93
+ /*
94
+ * The grid this terminal fits and tells the program, and the grid the program has. They part while
95
+ * another client sizes the same program; this one then draws that grid, clipped or with room to
96
+ * spare, until a fit here changes the grid and claims it back.
97
+ */
98
+ let claimed = { cols: term.cols, rows: term.rows };
99
+ let shared = claimed;
100
+ const drawShared = () => {
101
+ if (term.cols !== shared.cols || term.rows !== shared.rows) {
102
+ term.resize(shared.cols, shared.rows);
103
+ }
104
+ };
105
+ // A resize, a font change and a renderer swap all land here: WebGL and the DOM measure a glyph
106
+ // differently, so a swap can change how many cells fit.
107
+ const refit = () => {
108
+ fitToHost(term, fit);
109
+ if (term.cols !== claimed.cols || term.rows !== claimed.rows) {
110
+ claimed = { cols: term.cols, rows: term.rows };
111
+ shared = claimed;
112
+ latest.current.onResize?.(claimed.cols, claimed.rows);
113
+ }
114
+ drawShared();
115
+ };
116
+ let theme = JSON.stringify(term.options.theme);
117
+ const restyle = (size, height) => {
118
+ const next = readTerminalTheme();
119
+ // A fresh theme object repaints every cell, so only a different one is handed over.
120
+ if (JSON.stringify(next) !== theme) {
121
+ theme = JSON.stringify(next);
122
+ term.options.theme = next;
123
+ }
124
+ term.options.fontFamily = readTerminalFont();
125
+ term.options.fontSize = size;
126
+ term.options.lineHeight = height;
127
+ // A new glyph size changes how many cells fit; the resize observer only hears the host.
128
+ refit();
129
+ };
130
+ const id = `terminal-${++terminals}`;
131
+ let releaseWebgl = null;
132
+ const current = {
133
+ term,
134
+ refit,
135
+ restyle,
136
+ followGrid: (size) => {
137
+ shared = size;
138
+ drawShared();
139
+ },
140
+ setInput: (readOnly, convertEol) => {
141
+ term.options.disableStdin = readOnly;
142
+ term.options.cursorBlink = !readOnly;
143
+ term.options.convertEol = convertEol;
144
+ },
145
+ setWebgl: (on) => {
146
+ current.webgl = on;
147
+ if (on && releaseWebgl === null) {
148
+ releaseWebgl = webglBudget.register(id, term, refit);
149
+ }
150
+ else if (!on && releaseWebgl !== null) {
151
+ releaseWebgl();
152
+ releaseWebgl = null;
153
+ }
154
+ },
155
+ webgl: false,
156
+ id
157
+ };
158
+ live.current = current;
159
+ const data = term.onData((input) => latest.current.onData?.(input));
160
+ let timer = null;
161
+ const resizes = new ResizeObserver(() => {
162
+ if (timer !== null) {
163
+ window.clearTimeout(timer);
164
+ }
165
+ timer = window.setTimeout(() => {
166
+ timer = null;
167
+ refit();
168
+ }, RESIZE_DEBOUNCE_MS);
169
+ });
170
+ resizes.observe(host);
171
+ const themes = new MutationObserver(() => restyle(latest.current.fontSize, latest.current.lineHeight));
172
+ themes.observe(document.documentElement, { attributes: true, attributeFilter: ROOT_ATTRIBUTES });
173
+ return () => {
174
+ if (timer !== null) {
175
+ window.clearTimeout(timer);
176
+ }
177
+ resizes.disconnect();
178
+ themes.disconnect();
179
+ data.dispose();
180
+ // The context goes back before the terminal is gone, so the next one can take it.
181
+ current.setWebgl(false);
182
+ term.dispose();
183
+ live.current = null;
184
+ };
185
+ }, []);
186
+ useEffect(() => {
187
+ live.current?.restyle(fontSize, lineHeight);
188
+ }, [fontSize, lineHeight]);
189
+ useEffect(() => {
190
+ live.current?.setInput(readOnly, convertEol);
191
+ }, [readOnly, convertEol]);
192
+ useEffect(() => {
193
+ live.current?.setWebgl(webgl);
194
+ }, [webgl]);
195
+ return (_jsx("div", { className: clsx('terminal-view', className), role: label === undefined ? undefined : 'region', "aria-label": label, children: _jsx("div", { ref: hostRef, className: "terminal-view-host" }) }));
196
+ }
@@ -0,0 +1,2 @@
1
+ export { TerminalView, type TerminalSize, type TerminalViewHandle, type TerminalViewProps } from './TerminalView.tsx';
2
+ export { webglTerminals } from './webgl-budget.ts';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { TerminalView } from './TerminalView.js';
2
+ export { webglTerminals } from './webgl-budget.js';
@@ -0,0 +1,58 @@
1
+ /* The tokens and rules of @adecore/terminal. An app imports this after the theme of @adecore/ui, whose
2
+ `--accent`, `--selection` and `--font-mono` the terminal reads too. */
3
+
4
+ /* What `TerminalView` draws with, read as literal colors on every theme change. The cursor follows the
5
+ accent; the sixteen colors are the ones a program asks for by number. The selectors are the ones
6
+ the theme of @adecore/ui uses, so an app overrides both in one rule. */
7
+ :root, [data-theme="light"] {
8
+ --term-bg: #fbfbfc;
9
+ --term-fg: #27272a;
10
+ --term-cursor: var(--accent);
11
+ --term-ansi-black: #3f3f46;
12
+ --term-ansi-red: #b91c1c;
13
+ --term-ansi-green: #15803d;
14
+ --term-ansi-yellow: #a16207;
15
+ --term-ansi-blue: #1d4ed8;
16
+ --term-ansi-magenta: #a21caf;
17
+ --term-ansi-cyan: #0e7490;
18
+ --term-ansi-white: #a1a1aa;
19
+ --term-ansi-bright-black: #71717a;
20
+ --term-ansi-bright-red: #dc2626;
21
+ --term-ansi-bright-green: #16a34a;
22
+ --term-ansi-bright-yellow: #ca8a04;
23
+ --term-ansi-bright-blue: #2563eb;
24
+ --term-ansi-bright-magenta: #c026d3;
25
+ --term-ansi-bright-cyan: #0891b2;
26
+ --term-ansi-bright-white: #e4e4e7;
27
+ }
28
+
29
+ [data-theme="dark"] {
30
+ --term-bg: #08080a;
31
+ --term-fg: #d6d6de;
32
+ --term-ansi-black: #2a2a31;
33
+ --term-ansi-red: #f87171;
34
+ --term-ansi-green: #4ade80;
35
+ --term-ansi-yellow: #fbbf24;
36
+ --term-ansi-blue: #60a5fa;
37
+ --term-ansi-magenta: #c084fc;
38
+ --term-ansi-cyan: #67e8f9;
39
+ --term-ansi-white: #d6d6de;
40
+ --term-ansi-bright-black: #6b6b76;
41
+ --term-ansi-bright-red: #fca5a5;
42
+ --term-ansi-bright-green: #86efac;
43
+ --term-ansi-bright-yellow: #fde68a;
44
+ --term-ansi-bright-blue: #93c5fd;
45
+ --term-ansi-bright-magenta: #d8b4fe;
46
+ --term-ansi-bright-cyan: #a5f3fc;
47
+ --term-ansi-bright-white: #f4f4f5;
48
+ }
49
+
50
+ /* In the components layer, so a caller's utilities, such as `absolute inset-0`, outrank the box. */
51
+ @layer components {
52
+ /* The host takes no vertical padding, since the fit would count it as room for a
53
+ row that is cut off; the rows sit centered by the offset the fit sets instead. */
54
+ .terminal-view { position: relative; overflow: hidden; background: var(--term-bg); }
55
+ .terminal-view-host { position: absolute; inset: 0; overflow: hidden; padding: 0 0 0 8px; }
56
+ .terminal-view-host .xterm { height: 100%; top: var(--term-offset, 6px); }
57
+ .terminal-view-host .xterm .xterm-viewport { background-color: transparent !important; }
58
+ }
@@ -0,0 +1,3 @@
1
+ import type { ITheme } from '@xterm/xterm';
2
+ export declare const readTerminalTheme: () => ITheme;
3
+ export declare const readTerminalFont: () => string;
package/dist/theme.js ADDED
@@ -0,0 +1,20 @@
1
+ const ANSI = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'];
2
+ const token = (style, name) => style.getPropertyValue(name).trim();
3
+ const brightKey = (name) => `bright${name[0].toUpperCase()}${name.slice(1)}`;
4
+ /* xterm takes literal colors, so the tokens are resolved from the root on every theme change. */
5
+ export const readTerminalTheme = () => {
6
+ const style = getComputedStyle(document.documentElement);
7
+ const theme = {
8
+ background: token(style, '--term-bg'),
9
+ foreground: token(style, '--term-fg'),
10
+ cursor: token(style, '--term-cursor'),
11
+ cursorAccent: token(style, '--term-bg'),
12
+ selectionBackground: token(style, '--selection')
13
+ };
14
+ for (const name of ANSI) {
15
+ theme[name] = token(style, `--term-ansi-${name}`);
16
+ theme[brightKey(name)] = token(style, `--term-ansi-bright-${name}`);
17
+ }
18
+ return theme;
19
+ };
20
+ export const readTerminalFont = () => token(getComputedStyle(document.documentElement), '--font-mono');
@@ -0,0 +1,25 @@
1
+ import type { Terminal } from '@xterm/xterm';
2
+ /**
3
+ * Keeps the number of live WebGL renderers under the cap, however many terminals a page shows.
4
+ * A browser drops the oldest of its own contexts without telling anyone, which left a terminal on
5
+ * the DOM renderer for good; here a terminal that loses its context queues up again instead.
6
+ * `WebglSlots` decides who holds one, this class loads and disposes the addons.
7
+ */
8
+ declare class WebglBudget {
9
+ private readonly slots;
10
+ private readonly entries;
11
+ register(id: string, term: Terminal, refit: () => void): () => void;
12
+ focus(id: string): void;
13
+ blur(id: string): void;
14
+ touch(id: string): void;
15
+ holders(): Terminal[];
16
+ private apply;
17
+ private load;
18
+ private unload;
19
+ private handleLoss;
20
+ private drop;
21
+ private scheduleRefit;
22
+ }
23
+ export declare const webglBudget: WebglBudget;
24
+ export declare const webglTerminals: () => Terminal[];
25
+ export {};
@@ -0,0 +1,143 @@
1
+ import { DEFAULT_WEBGL_CONTEXTS, WebglSlots } from './webgl-slots.js';
2
+ let webgl2Available = null;
3
+ /* Loaded on the first grant, and only a terminal granted a context before that waits on it. An app
4
+ that registers the same import with the prefetcher has it in the module cache by then. */
5
+ let Addon = null;
6
+ let addonLoad = null;
7
+ /* Probed once for the page; the probe context is released so it does not count against the browser's cap. */
8
+ const hasWebgl2 = () => {
9
+ if (webgl2Available === null) {
10
+ const context = document.createElement('canvas').getContext('webgl2');
11
+ webgl2Available = context !== null;
12
+ context?.getExtension('WEBGL_lose_context')?.loseContext();
13
+ }
14
+ return webgl2Available;
15
+ };
16
+ /* The canvas the addon just added. xterm names the canvas of every render layer it owns
17
+ (`xterm-link-layer`); the WebGL one is the nameless one. */
18
+ const rendererCanvas = (term) => {
19
+ const canvases = term.element?.querySelectorAll('.xterm-screen canvas') ?? [];
20
+ return [...canvases].filter((canvas) => canvas.classList.length === 0).at(-1) ?? null;
21
+ };
22
+ /**
23
+ * Keeps the number of live WebGL renderers under the cap, however many terminals a page shows.
24
+ * A browser drops the oldest of its own contexts without telling anyone, which left a terminal on
25
+ * the DOM renderer for good; here a terminal that loses its context queues up again instead.
26
+ * `WebglSlots` decides who holds one, this class loads and disposes the addons.
27
+ */
28
+ class WebglBudget {
29
+ slots = new WebglSlots(DEFAULT_WEBGL_CONTEXTS);
30
+ entries = new Map();
31
+ /* A terminal that mounted visibly asks for a context. The returned function gives it back. */
32
+ register(id, term, refit) {
33
+ const entry = { term, refit, addon: null, canvas: null, frame: null };
34
+ this.entries.set(id, entry);
35
+ if (hasWebgl2()) {
36
+ this.apply(this.slots.request(id));
37
+ }
38
+ return () => {
39
+ if (this.entries.get(id) !== entry) {
40
+ return;
41
+ }
42
+ this.entries.delete(id);
43
+ // The context goes back before the next terminal asks for one, never after.
44
+ this.drop(entry);
45
+ this.apply(this.slots.release(id));
46
+ };
47
+ }
48
+ focus(id) {
49
+ this.apply(this.slots.focus(id));
50
+ }
51
+ blur(id) {
52
+ this.apply(this.slots.blur(id));
53
+ }
54
+ /* Output arrived; a busy terminal outranks an idle one. */
55
+ touch(id) {
56
+ this.apply(this.slots.touch(id));
57
+ }
58
+ /* The terminals holding a WebGL renderer, highest ranked first. */
59
+ holders() {
60
+ return this.slots.holders().flatMap((id) => {
61
+ const entry = this.entries.get(id);
62
+ return entry ? [entry.term] : [];
63
+ });
64
+ }
65
+ apply(change) {
66
+ // Free first, the browser has to be back under its own limit before the next context exists.
67
+ for (const id of change.revoked) {
68
+ this.unload(id);
69
+ }
70
+ for (const id of change.granted) {
71
+ this.load(id);
72
+ }
73
+ }
74
+ load(id) {
75
+ const entry = this.entries.get(id);
76
+ if (!entry || entry.addon) {
77
+ return;
78
+ }
79
+ if (Addon === null) {
80
+ addonLoad ??= import('@xterm/addon-webgl').then((module) => {
81
+ Addon = module.WebglAddon;
82
+ });
83
+ void addonLoad.then(() => {
84
+ if (this.slots.holders().includes(id)) {
85
+ this.load(id);
86
+ }
87
+ }, () => this.apply(this.slots.lost(id)));
88
+ return;
89
+ }
90
+ try {
91
+ const addon = new Addon();
92
+ addon.onContextLoss(() => this.handleLoss(id));
93
+ entry.term.loadAddon(addon);
94
+ entry.addon = addon;
95
+ entry.canvas = rendererCanvas(entry.term);
96
+ this.scheduleRefit(entry);
97
+ }
98
+ catch {
99
+ // WebGL refused this terminal; the DOM renderer keeps drawing and the slot goes to the next one.
100
+ this.apply(this.slots.lost(id));
101
+ }
102
+ }
103
+ unload(id) {
104
+ const entry = this.entries.get(id);
105
+ if (!entry?.addon) {
106
+ return;
107
+ }
108
+ this.drop(entry);
109
+ // Disposing the addon puts the DOM renderer back and queues a full repaint; the fit runs
110
+ // after that frame, on the cell size that renderer measured.
111
+ this.scheduleRefit(entry);
112
+ }
113
+ handleLoss(id) {
114
+ this.unload(id);
115
+ // Not gone for good, the terminal waits in line and takes a context again when it is focused.
116
+ this.apply(this.slots.lost(id));
117
+ }
118
+ drop(entry) {
119
+ if (entry.frame !== null) {
120
+ window.cancelAnimationFrame(entry.frame);
121
+ entry.frame = null;
122
+ }
123
+ entry.addon?.dispose();
124
+ entry.addon = null;
125
+ // The addon leaves its canvas to the garbage collector, and until that runs the browser
126
+ // still counts the context. Losing it by hand is what makes a released slot a free one.
127
+ entry.canvas?.getContext('webgl2')?.getExtension('WEBGL_lose_context')?.loseContext();
128
+ entry.canvas = null;
129
+ }
130
+ scheduleRefit(entry) {
131
+ if (entry.frame !== null) {
132
+ return;
133
+ }
134
+ entry.frame = window.requestAnimationFrame(() => {
135
+ entry.frame = null;
136
+ entry.refit();
137
+ });
138
+ }
139
+ }
140
+ /* One budget for the page, since the browser's cap is per page. */
141
+ export const webglBudget = /* @__PURE__ */ new WebglBudget();
142
+ /* The terminals drawing with WebGL right now, highest ranked first: the focused one, then the ones written to last. */
143
+ export const webglTerminals = () => webglBudget.holders();
@@ -0,0 +1,30 @@
1
+ export declare const DEFAULT_WEBGL_CONTEXTS = 10;
2
+ export interface SlotChange {
3
+ granted: string[];
4
+ revoked: string[];
5
+ }
6
+ /**
7
+ * Hands a fixed number of WebGL contexts to the terminals that deserve them most, the focused one
8
+ * first, then the ones most recently focused or written to. Knows nothing about xterm or the DOM;
9
+ * the budget turns a `SlotChange` into addons that are loaded and disposed.
10
+ */
11
+ export declare class WebglSlots {
12
+ private readonly members;
13
+ private cap;
14
+ private focusedId;
15
+ private stamp;
16
+ constructor(cap?: number);
17
+ holders(): string[];
18
+ holds(id: string): boolean;
19
+ waiting(): string[];
20
+ request(id: string): SlotChange;
21
+ release(id: string): SlotChange;
22
+ focus(id: string): SlotChange;
23
+ blur(id: string): SlotChange;
24
+ touch(id: string): SlotChange;
25
+ lost(id: string): SlotChange;
26
+ setCap(cap: number): SlotChange;
27
+ private ranked;
28
+ private rankOf;
29
+ private reconcile;
30
+ }