Skip to content

@adecore/terminal ​

A terminal pane on xterm.js 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.

tsx
import { TerminalView, type TerminalViewHandle } from '@adecore/terminal';
tsx
import { useEffect, useRef } from 'react';
import { TerminalView, type TerminalViewHandle } from '@adecore/terminal';

const PROMPT = '\x1b[32m~\x1b[0m $ ';

/* A pretend shell that echoes what is typed, since a demo has no program behind it. */
export default function TerminalViewDemo() {
    const view = useRef<TerminalViewHandle>(null);
    const line = useRef('');

    useEffect(() => {
        view.current?.write(`\x1b[1mbun test\x1b[0m\r\n\x1b[32m 236 pass\x1b[0m\r\n\x1b[2m 0 fail\x1b[0m\r\nDocs: https://adecore.dev\r\n\r\n${PROMPT}`);
    }, []);

    const type = (data: string): void => {
        for (const key of data) {
            if (key === '\r') {
                view.current?.write(`\r\n${line.current === '' ? '' : `${line.current}\r\n`}${PROMPT}`);
                line.current = '';
            } else if (key === '\x7f') {
                if (line.current !== '') {
                    line.current = line.current.slice(0, -1);
                    view.current?.write('\b \b');
                }
            } else if (key >= ' ') {
                line.current += key;
                view.current?.write(key);
            }
        }
    };

    return <TerminalView ref={view} label="Demo shell" onData={type} className="h-56 w-full max-w-xl rounded-lg" />;
}

Install ​

xterm and its addons are peer dependencies, so the app picks the version:

sh
bun add @adecore/terminal @xterm/xterm @xterm/addon-fit @xterm/addon-web-links @xterm/addon-webgl

Import xterm's stylesheet and the package's own after the theme of @adecore/ui. The package's stylesheet holds the --term-* tokens and the box the terminal sits in; the colors, the selection and the font of the theme apply to the terminal too.

css
@import "tailwindcss";
@import "@adecore/ui/theme.css";
@import "@xterm/xterm/css/xterm.css";
@import "@adecore/terminal/terminal.css";

Feeding it ​

The handle is the way in. write draws a program's output and onData hears what the person types or pastes, so a pty is two lines:

tsx
const view = useRef<TerminalViewHandle>(null);

useEffect(() => session.onOutput((data) => view.current?.write(data)), [session]);

<TerminalView ref={view} label="Shell" onData={(data) => session.write(data)} onResize={(cols, rows) => session.resize(cols, rows)} className="h-full" />

The grid it fits at mount is size(), which is what a program starts with. After that onResize reports every new grid, debounced while a container is being dragged, and after a font change. Rows that do not fill the height are centered, and the background runs to the edges.

reset() clears the screen and the scrollback, in order with the output written before it, so a replay that starts over is reset() and a write of the whole text. visibleText() returns the rows on screen as plain text, such as for a question about what the terminal shows. paste, selectAll and selection() serve a context menu.

For captured output rather than a pty's, convertEol treats a lone line feed as a line break, and readOnly drops the typing and stops the cursor blinking.

Several clients on one program ​

When two windows show the same program, the last one to resize decides its grid. followGrid(size) draws the grid the program reports, clipped or with room to spare, until the next fit here changes the grid and onResize claims it back.

Theme and font ​

The colors come from the --term-* tokens in terminal.css: a background, a foreground, a cursor that follows the accent and the sixteen colors a program asks for by number. The selection is --selection and the font --font-mono. The terminal reads them again whenever data-theme, class or the inline style on <html> changes, so a theme switch, a new accent or another monospace font reaches it without a prop. fontSize (13 by default) and lineHeight (1) are props, since an app usually lets a person set them.

A URL in the output is a link. Without onOpenLink it opens in a new window; a desktop app passes the function that opens it in the browser. Whether the prop is set is read at mount.

WebGL ​

webgl draws with WebGL instead of the DOM, which is faster on a busy screen. A browser keeps about 16 contexts per page and silently drops the oldest, so every terminal with webgl shares one budget of ten: the focused one first, then the ones written to most recently. A terminal that loses its context falls back to the DOM and asks again on its next focus(). webglTerminals() lists the terminals that hold one, highest ranked first.

The WebGL addon loads on the first grant. Register its import with the prefetcher of @adecore/ui to have it ready before then:

ts
prefetcher.register(() => import('@xterm/addon-webgl'));

Under a scaled ancestor ​

A terminal on a canvas that zooms with a CSS transform measures the pointer against the scaled box, so a click lands on the wrong cell. scaledByAncestor corrects every pointer for the scale, for selection, links and mouse reporting alike. It is read at mount.

The terminal itself ​

terminal on the handle is the xterm instance, for what the props leave to the app: its own key handler (attachCustomKeyEventHandler), an OSC handler such as one for the clipboard, or a registry of open terminals. It exists from the first effect after mount until unmount, and a parent's effect already sees it.

Props ​

PropType
onData(data: string) => voidWhat the person types or pastes.
onResize(cols: number, rows: number) => voidA new grid after a resize or a font change.
onOpenLink(uri: string) => voidWhere a link goes. A new window by default.
fontSizenumber13 by default.
lineHeightnumber1 by default.
readOnlybooleanShows output only.
convertEolbooleanA lone line feed is a line break.
webglbooleanDraws with WebGL while the budget has room.
scaledByAncestorbooleanAn ancestor scales the terminal. Read at mount.
labelstringNames the terminal as a region.
classNamestringThe terminal has no size of its own; this gives it one.
refRef<TerminalViewHandle>The handle.

Handle ​

Member
terminalThe xterm instance, or null before mount.
write(data, done?)Draws output; done runs once it is drawn.
reset()Clears the screen and the scrollback.
focus(), blur()Moves the keyboard, and ranks the terminal for WebGL.
paste(text)Pastes, bracketed when the program asks for that.
selectAll(), selection()The selection, empty without one.
size()The grid as a TerminalSize, { cols, rows }.
visibleText()The rows on screen as text.
followGrid(size)Draws the grid another client set.

TerminalViewProps, TerminalViewHandle and TerminalSize are exported types.