HelixOverviewComponentsMapsGraphIntroductionInstallationDeriving a themeSandboxTheme builderVisual testingCortexDocsEngramBuildingTraceBuildingRecallDeclaredServicesInsightsGet in touchSource on GitHub
Essentials
TerminalsHelix Terminal puts a frame around a terminal drawn by xterm.js or an emulator
of your own. The frame adds a header with the terminal's name and controls, a
find bar, and a status line for a process that exited. @fusion.dev/helix-terminal
connects the frame to the emulator and themes it from your tokens. Helix does
not start processes. You connect the frame to yours, such as a pseudo-terminal
on your server or in a desktop app.Add a terminalInstall Helix, the terminal connector, xterm.js, and the xterm.js addons you
want:bun add @fusion.dev/helix @fusion.dev/helix-terminal @xterm/xterm @xterm/addon-fit @xterm/addon-search @xterm/addon-web-linksAdd the frame and an element for xterm.js to draw in:<helix-terminallabel="~/project"><divid="terminal"></div></helix-terminal>Create the terminal in your theme, open it in the frame, then connect the two:import'@fusion.dev/helix/terminal'import'@xterm/xterm/css/xterm.css'import{connectTerminal,terminalThemeReady}from'@fusion.dev/helix-terminal'import{xtermDriver,xtermOptions}from'@fusion.dev/helix-terminal/xterm'import{FitAddon}from'@xterm/addon-fit'import{SearchAddon}from'@xterm/addon-search'import{WebLinksAddon}from'@xterm/addon-web-links'import{Terminal}from'@xterm/xterm'constframe=document.querySelector('helix-terminal')constterminal=newTerminal(xtermOptions(awaitterminalThemeReady(frame)))terminal.open(document.getElementById('terminal'))constconnection=connectTerminal(frame,xtermDriver(terminal,{fit:newFitAddon(),search:newSearchAddon(),webLinks:WebLinksAddon,}),)// Later, when the terminal goes away:connection.disconnect()terminal.dispose()terminalThemeReady waits until your tokens can be read and the monospaced
face has loaded. xterm.js measures its cells in the face it opens with, so a
terminal created before the face loads keeps the fallback face's cell size.Pass each addon new. The driver loads them, so do not load them yourself.Connect a processThe frame takes the process's output and sends what the reader types and the
size of the grid. This example connects a pseudo-terminal on your server over
a WebSocket:constsocket=newWebSocket('wss://example.com/pty')socket.binaryType='arraybuffer'// The process's output, as the bytes it wrote.socket.addEventListener('message',(event)=>frame.write(newUint8Array(event.data)))// What the reader types and pastes, encoded for the program.frame.addEventListener('helix-terminal-input',(event)=>{socket.send(JSON.stringify({input:event.detail.data}))})// The grid's size, which the pseudo-terminal takes as rows and columns.frame.addEventListener('helix-terminal-resize',(event)=>{const{columns,rows}=event.detailsocket.send(JSON.stringify({columns,rows}))})In a Tauri app, a command writes input to the pseudo-terminal and an event
carries its output back. The frame takes the same three calls.write(data) takes a string or a Uint8Array of UTF-8. Output written
before connectTerminal runs is held and written first, so the process can
start before xterm.js loads.helix-terminal-input carries data and binary. binary is true for
some mouse reports, whose characters are each one byte: write them as bytes
with Uint8Array.from(data, (character) => character.charCodeAt(0)).helix-terminal-resize carries columns and rows, and width and
height in CSS pixels, which a pseudo-terminal's size also takes. It is sent
once when the frame connects and again whenever the grid changes size.paste(text) inputs text as a paste does. It arrives as
helix-terminal-input, wrapped in the bracketed paste codes when the program
asked for them.Keep up with a fast processwrite() returns a promise that resolves once xterm.js has parsed the output.
A process can write faster than a terminal draws, so pause it while too much
is waiting:letwaiting=0pty.onData(async(chunk)=>{waiting+=chunk.lengthif(waiting>1_000_000){pty.pause()}awaitframe.write(chunk)waiting-=chunk.lengthif(waiting<100_000){pty.resume()}})When the process exitsSet exit-code when the process exits. The status line says how it exited,
the frame stops sending input, and a Restart button appears. Restart sends
helix-terminal-restart. Start a new process, clear exitCode, and write the
new output:Put buttons of your own beside Restart in the exit-actions slot, such as one
that opens the whole log. They show once the process exits.pty.onExit((code)=>{frame.exitCode=code})frame.addEventListener('helix-terminal-restart',()=>{frame.exitCode=undefinedframe.reset()pty=start()})What the connector doesconnectTerminal links the frame to the emulator:The frame's output reaches the emulator, and the reader's input reaches the
frame.The grid fits the frame and fits again when the frame changes size. Set
columns and rows on the frame for a grid of a fixed size.The emulator is themed from your tokens, and themed again when your theme or
color mode changes.Every cell's text keeps a contrast of 4.5:1 against its ground, so a color a
program chose stays readable on your theme. Pass { contrast: 7 } for a
higher floor, or { contrast: 1 } to draw every color as the program wrote
it.The cursor stops blinking and scrolling stops animating when a reader has
asked for reduced motion.connection.fit() fits the grid again, for a page that resized the frame in
a way the connector cannot see.connection.disconnect() puts back everything connectTerminal changed.Use xtermDriver(terminal, addons) for xterm.js 6. xterm.js is not bundled
with Helix, so you can use the version you already have.Use other xterm.js addonsYou create the xterm.js terminal, so you load any addon on it the way
xterm.js describes, before or after you connect. This loads the WebGL renderer
and the Unicode 11 character widths:import{Unicode11Addon}from'@xterm/addon-unicode11'import{WebglAddon}from'@xterm/addon-webgl'constterminal=newTerminal({...xtermOptions(awaitterminalThemeReady(frame)),allowProposedApi:true,})terminal.open(document.getElementById('terminal'))terminal.loadAddon(newWebglAddon())terminal.loadAddon(newUnicode11Addon())terminal.unicode.activeVersion='11'The connector sets the theme, the face, and its options on the terminal
itself, so an addon such as the WebGL renderer draws in your theme and keeps
the contrast floor.Fit, search, and web links go to the driver. Pass them to xtermDriver
unloaded, and the driver loads them: an addon loaded twice breaks. They are
also what draws the frame's find bar and sends helix-terminal-link. With a
search addon of your own, the frame draws no Find control, and with a web
links addon of your own, a link it finds skips the event.Some addons need xterm.js's proposed API. xterm.js refuses its Unicode
handling, decorations, markers, and character joiners until
allowProposedApi is on, and the Unicode 11 addon uses the first of these.
The driver turns the option on while it holds a search addon and puts it
back as it was on disconnect, so set it when you create the terminal for any
other addon that needs it.The connection owns the link handler. While connected, the driver sets
xterm.js's linkHandler so every link reaches helix-terminal-link, and it
puts yours back on disconnect. Listen for the event to handle a link.The key handler stays yours. The frame takes its own keys before xterm.js
sees them, so attachCustomKeyEventHandler is free for keys of your own.Style the terminal with your themeThe frame reads each color from a custom property, and the connector draws
the terminal in it. The styled frame sets every one from your tokens. Set one
on the terminal to choose a color yourself:helix-terminal{--terminal-ansi-blue:#3b82f6;--terminal-cursor-color:var(--helix-color-accent);}PropertyDefaultDraws--terminal-active-match-background-colorWarning, mixed with the groundThe current match of a search--terminal-active-match-border-color--helix-color-warningThe edge of the current match--terminal-ansi-black--helix-color-foreground in light, --helix-color-border in darkANSI black--terminal-ansi-blue--helix-color-infoANSI blue--terminal-ansi-cyanInfo mixed with successANSI cyan--terminal-ansi-green--helix-color-successANSI green--terminal-ansi-magenta--helix-color-syntax-callableANSI magenta--terminal-ansi-red--helix-color-dangerANSI red--terminal-ansi-white--helix-color-foreground-subtle in light, between muted and the foreground in darkANSI white--terminal-ansi-yellow--helix-color-warningANSI yellow--terminal-background-color--helix-color-backgroundThe ground--terminal-color--helix-color-foregroundText where the program sets no color--terminal-cursor-color--helix-color-primaryThe cursor--terminal-match-background-colorWarning, mixed more quietly with the groundEvery other match of a search--terminal-scrollbar-thumb-color--helix-scrollbar-thumb-colorThe scrollbar--terminal-selection-background-colorPrimary at 30%Selected textEach bright color has a property of its own, such as --terminal-ansi-bright-red,
and defaults to its color mixed a quarter of the way to --helix-color-foreground,
so it is lighter in dark mode and darker in light. Set a color alone and its
bright form follows it. The bright gray, --terminal-ansi-bright-black,
defaults to --helix-color-foreground-muted, and bright white to
--helix-color-border in light and --helix-color-foreground in dark.On the headless tier, helix-headless-terminal sets none of them. Set the
ones you want, and xterm.js keeps its own color for the rest.The colors share Log's hues, so a program's output reads the same in a log and
in a terminal side by side. The four grays keep their order from dark to light
in each color mode: a program that fills a panel with black expects it to be
dark. Text a program writes in a gray that is faint against your ground is
lifted to the contrast floor.The grid is drawn in the viewport's face, which the styled frame sets from
--terminal-font-family, the monospaced face by default, and
--terminal-font-size, the small text step. It also takes
--terminal-block-size, --terminal-border-color,
--terminal-header-background-color, --terminal-padding, and
--terminal-radius. The Terminal reference lists
what each one draws.Show a command's outputSet readonly to show output that takes no input, such as a command an agent
ran. The frame draws no cursor and sends no input. Set columns and rows to
draw the output at the size the command ran at:<helix-terminalcontrols="find copy"label="bun run build"readonlycolumns="80"rows="12"><divid="transcript"></div></helix-terminal>Fill a paneSet fill for a terminal that fills a pane, such as one side of a Splitter.
The frame covers its container, which needs a position of its own, such as
position: relative. Set separation="plain" where the pane already draws its
edges. Put buttons of your own in the actions slot, before the built-in
controls, and in find-actions for the find bar. Each takes the size of the
buttons beside it:<divclass="pane"><helix-terminalfilllabel="zsh"separation="plain"><helix-icon-buttonlabel="New terminal"slot="actions"><helix-iconname="plus"></helix-icon></helix-icon-button><divid="terminal"></div></helix-terminal></div>The frame keeps its own label when a program sets its title.
helix-terminal-title carries the new title, so you can show it where it
belongs, such as on the tab the terminal sits under.LinksA link in the output sends helix-terminal-link with its href when a reader
activates it. Left alone, the frame opens an http or https address in a new
tab and opens nothing else, since a program can write any address. Cancel the
event to open the link yourself, as a desktop app does through the system's
browser:frame.addEventListener('helix-terminal-link',(event)=>{event.preventDefault()openInBrowser(event.detail.href)})Links a program writes as hyperlinks always work. Pass WebLinksAddon to
xtermDriver to also make the addresses in plain text into links.Keyboard and screen readersEvery key goes to the program, including Tab, Shift+Tab, and Escape, except
the keys below.Control+Shift+F, or Command+F on a Mac, opens the find bar. Enter moves to
the next match and Shift+Enter to the previous one. Escape closes the bar and
returns focus to the terminal. Screen readers hear how many matches there are.Control+Shift+C copies the selection. On a Mac, Command+C does.Control+M, or Control+Shift+M on a Mac, switches Tab between typing in the
terminal and moving focus out of it. The status line says when Tab moves
focus, and screen readers hear each change. The terminal's input describes
this key to a screen reader. In a terminal, Control+M types the same
character as Enter.The frame is a named group. Its controls come before the terminal in the
tab order, as one toolbar: Tab reaches it once, and the arrow keys move
between its buttons, including the ones you put in actions. The find bar's
buttons are a second toolbar after its field. The terminal's input is one tab
stop. Name each terminal with label so a reader can tell several apart.A terminal draws its grid as a picture of text, which a screen reader cannot
read. Set screen-reader-mode to have xterm.js keep the grid as lines a
screen reader can move through, and announce new output as it arrives. It
costs time on every write, so it is off by default. A browser cannot tell
whether a screen reader is running, so connect it to a setting your reader
can turn on:frame.screenReaderMode=settings.screenReadertext() returns the lines on screen as plain text, and text('all')
returns every line the scrollback holds, which is how an assistant reads a
terminal it has been allowed to see.Write a driverTo use another emulator, write a driver: an object that draws the frame's
output and tells the frame what the reader typed. connectTerminal calls it.
xtermDriver is one, and the TerminalDriver type in
@fusion.dev/helix-terminal describes every method.Required methodswrite(data, done) draws the program's output, a string or a Uint8Array,
and calls done once it has been parsed.onInput(input) calls input(data, binary) with what the reader types or
pastes, encoded for the program, and returns a function that stops.Optional methodsEach optional method turns on a feature. Without it, the feature is off and
nothing fails. A control in the header that a driver cannot answer is not
drawn.MethodWhat it must doTurns onclear()Clear the scrollback, keeping the cursor's lineThe clear button, and clear()element()Return the element the emulator draws inRefitting when it changes sizefit()Fit the grid to its element, and report the new size through onResizeA grid that fills the framefocus()Focus the emulator's inputfocus(), and focus returning from the find barinput()Return the element that takes keysNaming the input for screen readersonBell(rang)Call rang on the bell, and return a function that stopshelix-terminal-bell and the flashonLink(activated)Call activated(href) when a link is used, and return a function that stopshelix-terminal-linkonResize(resized)Call resized({ columns, rows }) when the grid changes sizehelix-terminal-resize after the firstonSelection(changed)Call changed when the selection changesThe copy button's stateonTitle(changed)Call changed(title) when the program sets its titlehelix-terminal-titleoptions(options)Apply contrast, cursor, input, motion, and screenReaderRead-only output, the contrast floor, reduced motion, and screen reader modepaste(text)Input text as a paste doespaste()pixels()Return the grid's width and height in CSS pixelsThe pixel size in helix-terminal-resizerelease()Put back what the driver changedA clean disconnect()reset()Reset the emulatorreset()resize(columns, rows)Set the grid's size in cellscolumns and rows on the framesearchAn object with find(query, options, matches), end(), and onResults(found)The find barselection()Return the selected textThe copy button and its keysize()Return the grid's { columns, rows }The first helix-terminal-resizetext(scope)Return the screen, or with all the scrollback, as texttext()theme(theme)Draw in theme.colors and theme.fontThemingconnection.disconnect() calls every function a method returned, detaches the
driver from the frame, and calls release().Draw your own terminalA page that draws a terminal without a driver hands the frame a renderer of
its own with frame.attach(renderer). It returns the calls that tell the frame
what happened: input, resized, title, bell, link, selected, found,
and detach. The TerminalRenderer and TerminalSink types in
@fusion.dev/helix-headless/terminal describe both.Test a driverOur specs test the xterm.js driver twice. A stand-in terminal records what the
driver asks of it, which runs anywhere. Then the browser specs connect a frame
to the real xterm.js, write output, type, search, and check what the frame
said. Test your driver the same way: connect it to a frame, write to the frame,
and read text() back.