@fusion.dev/helix-docs 0.208.1 → 0.210.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.
@@ -0,0 +1,422 @@
1
+ ---
2
+ description: Add a terminal that runs your own shell or process to your site, styled with your theme and drawn by xterm.js.
3
+ section: Essentials
4
+ title: Terminals
5
+ ---
6
+
7
+ # Terminals
8
+
9
+ Helix Terminal puts a frame around a terminal drawn by xterm.js or an emulator
10
+ of your own. The frame adds a header with the terminal's name and controls, a
11
+ find bar, and a status line for a process that exited. `@fusion.dev/helix-terminal`
12
+ connects the frame to the emulator and themes it from your tokens. Helix does
13
+ not start processes. You connect the frame to yours, such as a pseudo-terminal
14
+ on your server or in a desktop app.
15
+
16
+ ## Add a terminal
17
+
18
+ Install Helix, the terminal connector, xterm.js, and the xterm.js addons you
19
+ want:
20
+
21
+ ```sh
22
+ bun add @fusion.dev/helix @fusion.dev/helix-terminal @xterm/xterm @xterm/addon-fit @xterm/addon-search @xterm/addon-web-links
23
+ ```
24
+
25
+ Add the frame and an element for xterm.js to draw in:
26
+
27
+ ```html
28
+ <helix-terminal label="~/project">
29
+ <div id="terminal"></div>
30
+ </helix-terminal>
31
+ ```
32
+
33
+ Create the terminal in your theme, open it in the frame, then connect the two:
34
+
35
+ ```js
36
+ import '@fusion.dev/helix/terminal'
37
+ import '@xterm/xterm/css/xterm.css'
38
+ import { connectTerminal, terminalThemeReady } from '@fusion.dev/helix-terminal'
39
+ import { xtermDriver, xtermOptions } from '@fusion.dev/helix-terminal/xterm'
40
+ import { FitAddon } from '@xterm/addon-fit'
41
+ import { SearchAddon } from '@xterm/addon-search'
42
+ import { WebLinksAddon } from '@xterm/addon-web-links'
43
+ import { Terminal } from '@xterm/xterm'
44
+
45
+ const frame = document.querySelector('helix-terminal')
46
+ const terminal = new Terminal(xtermOptions(await terminalThemeReady(frame)))
47
+ terminal.open(document.getElementById('terminal'))
48
+
49
+ const connection = connectTerminal(
50
+ frame,
51
+ xtermDriver(terminal, {
52
+ fit: new FitAddon(),
53
+ search: new SearchAddon(),
54
+ webLinks: WebLinksAddon,
55
+ }),
56
+ )
57
+
58
+ // Later, when the terminal goes away:
59
+ connection.disconnect()
60
+ terminal.dispose()
61
+ ```
62
+
63
+ `terminalThemeReady` waits until your tokens can be read and the monospaced
64
+ face has loaded. xterm.js measures its cells in the face it opens with, so a
65
+ terminal created before the face loads keeps the fallback face's cell size.
66
+
67
+ Pass each addon new. The driver loads them, so do not load them yourself.
68
+
69
+ ## Connect a process
70
+
71
+ The frame takes the process's output and sends what the reader types and the
72
+ size of the grid. This example connects a pseudo-terminal on your server over
73
+ a WebSocket:
74
+
75
+ ```js
76
+ const socket = new WebSocket('wss://example.com/pty')
77
+ socket.binaryType = 'arraybuffer'
78
+
79
+ // The process's output, as the bytes it wrote.
80
+ socket.addEventListener('message', (event) => frame.write(new Uint8Array(event.data)))
81
+
82
+ // What the reader types and pastes, encoded for the program.
83
+ frame.addEventListener('helix-terminal-input', (event) => {
84
+ socket.send(JSON.stringify({ input: event.detail.data }))
85
+ })
86
+
87
+ // The grid's size, which the pseudo-terminal takes as rows and columns.
88
+ frame.addEventListener('helix-terminal-resize', (event) => {
89
+ const { columns, rows } = event.detail
90
+ socket.send(JSON.stringify({ columns, rows }))
91
+ })
92
+ ```
93
+
94
+ In a Tauri app, a command writes input to the pseudo-terminal and an event
95
+ carries its output back. The frame takes the same three calls.
96
+
97
+ - `write(data)` takes a string or a `Uint8Array` of UTF-8. Output written
98
+ before `connectTerminal` runs is held and written first, so the process can
99
+ start before xterm.js loads.
100
+ - `helix-terminal-input` carries `data` and `binary`. `binary` is `true` for
101
+ some mouse reports, whose characters are each one byte: write them as bytes
102
+ with `Uint8Array.from(data, (character) => character.charCodeAt(0))`.
103
+ - `helix-terminal-resize` carries `columns` and `rows`, and `width` and
104
+ `height` in CSS pixels, which a pseudo-terminal's size also takes. It is sent
105
+ once when the frame connects and again whenever the grid changes size.
106
+ - `paste(text)` inputs text as a paste does. It arrives as
107
+ `helix-terminal-input`, wrapped in the bracketed paste codes when the program
108
+ asked for them.
109
+
110
+ ### Keep up with a fast process
111
+
112
+ `write()` returns a promise that resolves once xterm.js has parsed the output.
113
+ A process can write faster than a terminal draws, so pause it while too much
114
+ is waiting:
115
+
116
+ ```js
117
+ let waiting = 0
118
+
119
+ pty.onData(async (chunk) => {
120
+ waiting += chunk.length
121
+ if (waiting > 1_000_000) {
122
+ pty.pause()
123
+ }
124
+ await frame.write(chunk)
125
+ waiting -= chunk.length
126
+ if (waiting < 100_000) {
127
+ pty.resume()
128
+ }
129
+ })
130
+ ```
131
+
132
+ ### When the process exits
133
+
134
+ Set `exit-code` when the process exits. The status line says how it exited,
135
+ the frame stops sending input, and a Restart button appears. Restart sends
136
+ `helix-terminal-restart`. Start a new process, clear `exitCode`, and write the
137
+ new output:
138
+
139
+ Put buttons of your own beside Restart in the `exit-actions` slot, such as one
140
+ that opens the whole log. They show once the process exits.
141
+
142
+ ```js
143
+ pty.onExit((code) => {
144
+ frame.exitCode = code
145
+ })
146
+
147
+ frame.addEventListener('helix-terminal-restart', () => {
148
+ frame.exitCode = undefined
149
+ frame.reset()
150
+ pty = start()
151
+ })
152
+ ```
153
+
154
+ ## What the connector does
155
+
156
+ `connectTerminal` links the frame to the emulator:
157
+
158
+ - The frame's output reaches the emulator, and the reader's input reaches the
159
+ frame.
160
+ - The grid fits the frame and fits again when the frame changes size. Set
161
+ `columns` and `rows` on the frame for a grid of a fixed size.
162
+ - The emulator is themed from your tokens, and themed again when your theme or
163
+ color mode changes.
164
+ - Every cell's text keeps a contrast of 4.5:1 against its ground, so a color a
165
+ program chose stays readable on your theme. Pass `{ contrast: 7 }` for a
166
+ higher floor, or `{ contrast: 1 }` to draw every color as the program wrote
167
+ it.
168
+ - The cursor stops blinking and scrolling stops animating when a reader has
169
+ asked for reduced motion.
170
+ - `connection.fit()` fits the grid again, for a page that resized the frame in
171
+ a way the connector cannot see.
172
+ - `connection.disconnect()` puts back everything `connectTerminal` changed.
173
+
174
+ Use `xtermDriver(terminal, addons)` for xterm.js 6. xterm.js is not bundled
175
+ with Helix, so you can use the version you already have.
176
+
177
+ ## Use other xterm.js addons
178
+
179
+ You create the xterm.js terminal, so you load any addon on it the way
180
+ xterm.js describes, before or after you connect. This loads the WebGL renderer
181
+ and the Unicode 11 character widths:
182
+
183
+ ```js
184
+ import { Unicode11Addon } from '@xterm/addon-unicode11'
185
+ import { WebglAddon } from '@xterm/addon-webgl'
186
+
187
+ const terminal = new Terminal({
188
+ ...xtermOptions(await terminalThemeReady(frame)),
189
+ allowProposedApi: true,
190
+ })
191
+ terminal.open(document.getElementById('terminal'))
192
+ terminal.loadAddon(new WebglAddon())
193
+ terminal.loadAddon(new Unicode11Addon())
194
+ terminal.unicode.activeVersion = '11'
195
+ ```
196
+
197
+ The connector sets the theme, the face, and its options on the terminal
198
+ itself, so an addon such as the WebGL renderer draws in your theme and keeps
199
+ the contrast floor.
200
+
201
+ - **Fit, search, and web links go to the driver.** Pass them to `xtermDriver`
202
+ unloaded, and the driver loads them: an addon loaded twice breaks. They are
203
+ also what draws the frame's find bar and sends `helix-terminal-link`. With a
204
+ search addon of your own, the frame draws no Find control, and with a web
205
+ links addon of your own, a link it finds skips the event.
206
+ - **Some addons need xterm.js's proposed API.** xterm.js refuses its Unicode
207
+ handling, decorations, markers, and character joiners until
208
+ `allowProposedApi` is on, and the Unicode 11 addon uses the first of these.
209
+ The driver turns the option on while it holds a search addon and puts it
210
+ back as it was on disconnect, so set it when you create the terminal for any
211
+ other addon that needs it.
212
+ - **The connection owns the link handler.** While connected, the driver sets
213
+ xterm.js's `linkHandler` so every link reaches `helix-terminal-link`, and it
214
+ puts yours back on disconnect. Listen for the event to handle a link.
215
+ - **The key handler stays yours.** The frame takes its own keys before xterm.js
216
+ sees them, so `attachCustomKeyEventHandler` is free for keys of your own.
217
+
218
+ ## Style the terminal with your theme
219
+
220
+ The frame reads each color from a custom property, and the connector draws
221
+ the terminal in it. The styled frame sets every one from your tokens. Set one
222
+ on the terminal to choose a color yourself:
223
+
224
+ ```css
225
+ helix-terminal {
226
+ --terminal-ansi-blue: #3b82f6;
227
+ --terminal-cursor-color: var(--helix-color-accent);
228
+ }
229
+ ```
230
+
231
+ | Property | Default | Draws |
232
+ |---|---|---|
233
+ | `--terminal-active-match-background-color` | Warning, mixed with the ground | The current match of a search |
234
+ | `--terminal-active-match-border-color` | `--helix-color-warning` | The edge of the current match |
235
+ | `--terminal-ansi-black` | `--helix-color-foreground` in light, `--helix-color-border` in dark | ANSI black |
236
+ | `--terminal-ansi-blue` | `--helix-color-info` | ANSI blue |
237
+ | `--terminal-ansi-cyan` | Info mixed with success | ANSI cyan |
238
+ | `--terminal-ansi-green` | `--helix-color-success` | ANSI green |
239
+ | `--terminal-ansi-magenta` | `--helix-color-syntax-callable` | ANSI magenta |
240
+ | `--terminal-ansi-red` | `--helix-color-danger` | ANSI red |
241
+ | `--terminal-ansi-white` | `--helix-color-foreground-subtle` in light, between muted and the foreground in dark | ANSI white |
242
+ | `--terminal-ansi-yellow` | `--helix-color-warning` | ANSI yellow |
243
+ | `--terminal-background-color` | `--helix-color-background` | The ground |
244
+ | `--terminal-color` | `--helix-color-foreground` | Text where the program sets no color |
245
+ | `--terminal-cursor-color` | `--helix-color-primary` | The cursor |
246
+ | `--terminal-match-background-color` | Warning, mixed more quietly with the ground | Every other match of a search |
247
+ | `--terminal-scrollbar-thumb-color` | `--helix-scrollbar-thumb-color` | The scrollbar |
248
+ | `--terminal-selection-background-color` | Primary at 30% | Selected text |
249
+
250
+ Each bright color has a property of its own, such as `--terminal-ansi-bright-red`,
251
+ and defaults to its color mixed a quarter of the way to `--helix-color-foreground`,
252
+ so it is lighter in dark mode and darker in light. Set a color alone and its
253
+ bright form follows it. The bright gray, `--terminal-ansi-bright-black`,
254
+ defaults to `--helix-color-foreground-muted`, and bright white to
255
+ `--helix-color-border` in light and `--helix-color-foreground` in dark.
256
+
257
+ On the headless tier, `helix-headless-terminal` sets none of them. Set the
258
+ ones you want, and xterm.js keeps its own color for the rest.
259
+
260
+ The colors share Log's hues, so a program's output reads the same in a log and
261
+ in a terminal side by side. The four grays keep their order from dark to light
262
+ in each color mode: a program that fills a panel with black expects it to be
263
+ dark. Text a program writes in a gray that is faint against your ground is
264
+ lifted to the contrast floor.
265
+
266
+ The grid is drawn in the viewport's face, which the styled frame sets from
267
+ `--terminal-font-family`, the monospaced face by default, and
268
+ `--terminal-font-size`, the small text step. It also takes
269
+ `--terminal-block-size`, `--terminal-border-color`,
270
+ `--terminal-header-background-color`, `--terminal-padding`, and
271
+ `--terminal-radius`. The [Terminal reference](/helix/components/terminal) lists
272
+ what each one draws.
273
+
274
+ ## Show a command's output
275
+
276
+ Set `readonly` to show output that takes no input, such as a command an agent
277
+ ran. The frame draws no cursor and sends no input. Set `columns` and `rows` to
278
+ draw the output at the size the command ran at:
279
+
280
+ ```html
281
+ <helix-terminal controls="find copy" label="bun run build" readonly columns="80" rows="12">
282
+ <div id="transcript"></div>
283
+ </helix-terminal>
284
+ ```
285
+
286
+ ## Fill a pane
287
+
288
+ Set `fill` for a terminal that fills a pane, such as one side of a `Splitter`.
289
+ The frame covers its container, which needs a position of its own, such as
290
+ `position: relative`. Set `separation="plain"` where the pane already draws its
291
+ edges. Put buttons of your own in the `actions` slot, before the built-in
292
+ controls, and in `find-actions` for the find bar. Each takes the size of the
293
+ buttons beside it:
294
+
295
+ ```html
296
+ <div class="pane">
297
+ <helix-terminal fill label="zsh" separation="plain">
298
+ <helix-icon-button label="New terminal" slot="actions">
299
+ <helix-icon name="plus"></helix-icon>
300
+ </helix-icon-button>
301
+ <div id="terminal"></div>
302
+ </helix-terminal>
303
+ </div>
304
+ ```
305
+
306
+ The frame keeps its own `label` when a program sets its title.
307
+ `helix-terminal-title` carries the new title, so you can show it where it
308
+ belongs, such as on the tab the terminal sits under.
309
+
310
+ ## Links
311
+
312
+ A link in the output sends `helix-terminal-link` with its `href` when a reader
313
+ activates it. Left alone, the frame opens an `http` or `https` address in a new
314
+ tab and opens nothing else, since a program can write any address. Cancel the
315
+ event to open the link yourself, as a desktop app does through the system's
316
+ browser:
317
+
318
+ ```js
319
+ frame.addEventListener('helix-terminal-link', (event) => {
320
+ event.preventDefault()
321
+ openInBrowser(event.detail.href)
322
+ })
323
+ ```
324
+
325
+ Links a program writes as hyperlinks always work. Pass `WebLinksAddon` to
326
+ `xtermDriver` to also make the addresses in plain text into links.
327
+
328
+ ## Keyboard and screen readers
329
+
330
+ - Every key goes to the program, including Tab, Shift+Tab, and Escape, except
331
+ the keys below.
332
+ - Control+Shift+F, or Command+F on a Mac, opens the find bar. Enter moves to
333
+ the next match and Shift+Enter to the previous one. Escape closes the bar and
334
+ returns focus to the terminal. Screen readers hear how many matches there are.
335
+ - Control+Shift+C copies the selection. On a Mac, Command+C does.
336
+ - Control+M, or Control+Shift+M on a Mac, switches Tab between typing in the
337
+ terminal and moving focus out of it. The status line says when Tab moves
338
+ focus, and screen readers hear each change. The terminal's input describes
339
+ this key to a screen reader. In a terminal, Control+M types the same
340
+ character as Enter.
341
+ - The frame is a named group. Its controls come before the terminal in the
342
+ tab order, as one toolbar: Tab reaches it once, and the arrow keys move
343
+ between its buttons, including the ones you put in `actions`. The find bar's
344
+ buttons are a second toolbar after its field. The terminal's input is one tab
345
+ stop. Name each terminal with `label` so a reader can tell several apart.
346
+ - A terminal draws its grid as a picture of text, which a screen reader cannot
347
+ read. Set `screen-reader-mode` to have xterm.js keep the grid as lines a
348
+ screen reader can move through, and announce new output as it arrives. It
349
+ costs time on every write, so it is off by default. A browser cannot tell
350
+ whether a screen reader is running, so connect it to a setting your reader
351
+ can turn on:
352
+
353
+ ```js
354
+ frame.screenReaderMode = settings.screenReader
355
+ ```
356
+
357
+ - `text()` returns the lines on screen as plain text, and `text('all')`
358
+ returns every line the scrollback holds, which is how an assistant reads a
359
+ terminal it has been allowed to see.
360
+
361
+ ## Write a driver
362
+
363
+ To use another emulator, write a driver: an object that draws the frame's
364
+ output and tells the frame what the reader typed. `connectTerminal` calls it.
365
+ `xtermDriver` is one, and the `TerminalDriver` type in
366
+ `@fusion.dev/helix-terminal` describes every method.
367
+
368
+ ### Required methods
369
+
370
+ - `write(data, done)` draws the program's output, a string or a `Uint8Array`,
371
+ and calls `done` once it has been parsed.
372
+ - `onInput(input)` calls `input(data, binary)` with what the reader types or
373
+ pastes, encoded for the program, and returns a function that stops.
374
+
375
+ ### Optional methods
376
+
377
+ Each optional method turns on a feature. Without it, the feature is off and
378
+ nothing fails. A control in the header that a driver cannot answer is not
379
+ drawn.
380
+
381
+ | Method | What it must do | Turns on |
382
+ |---|---|---|
383
+ | `clear()` | Clear the scrollback, keeping the cursor's line | The clear button, and `clear()` |
384
+ | `element()` | Return the element the emulator draws in | Refitting when it changes size |
385
+ | `fit()` | Fit the grid to its element, and report the new size through `onResize` | A grid that fills the frame |
386
+ | `focus()` | Focus the emulator's input | `focus()`, and focus returning from the find bar |
387
+ | `input()` | Return the element that takes keys | Naming the input for screen readers |
388
+ | `onBell(rang)` | Call `rang` on the bell, and return a function that stops | `helix-terminal-bell` and the flash |
389
+ | `onLink(activated)` | Call `activated(href)` when a link is used, and return a function that stops | `helix-terminal-link` |
390
+ | `onResize(resized)` | Call `resized({ columns, rows })` when the grid changes size | `helix-terminal-resize` after the first |
391
+ | `onSelection(changed)` | Call `changed` when the selection changes | The copy button's state |
392
+ | `onTitle(changed)` | Call `changed(title)` when the program sets its title | `helix-terminal-title` |
393
+ | `options(options)` | Apply `contrast`, `cursor`, `input`, `motion`, and `screenReader` | Read-only output, the contrast floor, reduced motion, and screen reader mode |
394
+ | `paste(text)` | Input text as a paste does | `paste()` |
395
+ | `pixels()` | Return the grid's `width` and `height` in CSS pixels | The pixel size in `helix-terminal-resize` |
396
+ | `release()` | Put back what the driver changed | A clean `disconnect()` |
397
+ | `reset()` | Reset the emulator | `reset()` |
398
+ | `resize(columns, rows)` | Set the grid's size in cells | `columns` and `rows` on the frame |
399
+ | `search` | An object with `find(query, options, matches)`, `end()`, and `onResults(found)` | The find bar |
400
+ | `selection()` | Return the selected text | The copy button and its key |
401
+ | `size()` | Return the grid's `{ columns, rows }` | The first `helix-terminal-resize` |
402
+ | `text(scope)` | Return the screen, or with `all` the scrollback, as text | `text()` |
403
+ | `theme(theme)` | Draw in `theme.colors` and `theme.font` | Theming |
404
+
405
+ `connection.disconnect()` calls every function a method returned, detaches the
406
+ driver from the frame, and calls `release()`.
407
+
408
+ ### Draw your own terminal
409
+
410
+ A page that draws a terminal without a driver hands the frame a renderer of
411
+ its own with `frame.attach(renderer)`. It returns the calls that tell the frame
412
+ what happened: `input`, `resized`, `title`, `bell`, `link`, `selected`, `found`,
413
+ and `detach`. The `TerminalRenderer` and `TerminalSink` types in
414
+ `@fusion.dev/helix-headless/terminal` describe both.
415
+
416
+ ### Test a driver
417
+
418
+ Our specs test the xterm.js driver twice. A stand-in terminal records what the
419
+ driver asks of it, which runs anywhere. Then the browser specs connect a frame
420
+ to the real xterm.js, write output, type, search, and check what the frame
421
+ said. Test your driver the same way: connect it to a frame, write to the frame,
422
+ and read `text()` back.
@@ -237,6 +237,12 @@ export const docPages = [
237
237
  slug: 'svelte',
238
238
  title: 'Svelte',
239
239
  },
240
+ {
241
+ description: 'Add a terminal that runs your own shell or process to your site, styled with your theme and drawn by xterm.js.',
242
+ section: 'Essentials',
243
+ slug: 'terminals',
244
+ title: 'Terminals',
245
+ },
240
246
  {
241
247
  description: 'Enter your theme inputs and see every token Helix computes from them.',
242
248
  section: 'Tooling',