5.0 KiB
Chapter 20 — Guest TUI framework (ctx.tui)
Guest scripts (async function run(ctx, argv)), /bin utilities, and guest Pear apps can build full-screen terminal apps without import or require. The booter attaches ctx.tui (and ctx.sdk) after the REPL session exists, unless BARE_OS_TUI=0.
Source is first-party raw JS under lib/tui/src/ concatenated to /lib/bare-os/tui.js. It is not Holepunch ctx.bare.bareTui (that bundle still uses native TTY fds and require).
API reference: docs/reference/ctx-tui.md. Inspector: tui / man tui.
Minimal app
async function run(ctx) {
if (!ctx.tui || !ctx.tui.isTTY()) {
ctx.console.error('needs a TTY')
ctx.exitCode = 1
return
}
await ctx.tui.run({
init: function () {
this.n = 0
return null
},
update: function (msg) {
if (ctx.tui.key.matches(msg, 'q', 'ctrl+c')) return [this, ctx.tui.quit]
if (ctx.tui.key.matches(msg, 'up', 'k')) this.n++
if (ctx.tui.key.matches(msg, 'down', 'j')) this.n--
return [this, null]
},
view: function () {
return ctx.tui
.style()
.border(ctx.tui.style.borders.rounded)
.padding(1, 2)
.render('count: ' + this.n + '\n\n↑/↓ change · q quit')
}
})
}
Repo examples: examples/tui-counter/, examples/tui-form/, examples/tui-dashboard/, examples/tui-irc/, examples/tui-summon/. Live guest: tui demo.
TEA contract
init()→ first command ornullupdate(msg)→[model, cmd](or a bare model)view()→ string (pure; do not start I/O here)
Commands: ctx.tui.quit, tick, every, batch, sequence, suspend.
The Program suspends Fish (suspendReplForSubprocess), enters the alternate screen, and always restores the terminal in try/finally (including thrown update).
For editors and floating UI, pass { buffer: 'cell' }. The base view() stays a fixed-size screen; return overlay({ width, height }) or overlays(...) as { row, col, text } so a modal does not push later rows down. ctx.tui.modal uses this when the Program is in cell mode.
Mapping from Holepunch bare-tui
| Holepunch | Bare OS guest |
|---|---|
require('bare-tui') |
ctx.tui |
new Program(model).run() |
ctx.tui.run(model) |
list.create(...) |
ctx.tui.list.create(...) |
style() |
ctx.tui.style() |
host bare-fs filepicker |
ctx.tui.filepicker.create({ vfs: ctx.vfs }) |
Testing
Inject streams; fps: 0 renders synchronously:
await ctx.tui.run(model, {
input,
output,
isTTY: true,
fps: 0,
width: 80,
height: 24
})
Assert on ctx.tui.style.stripAnsi(model.view()), not only the write capture (the renderer is a diff stream).
ctx.sdk
Same object as ctx.tui at ctx.sdk.tui. Extra namespaces for app authors:
| Namespace | Role |
|---|---|
sdk.tty |
isTTY, size, acquire / release, withSession |
sdk.theme |
name, tokens, apply(name) → bareOsApplyTheme |
sdk.env |
get / has / term / colorDepth / noColor |
sdk.vfs |
list, readText, writeText |
sdk.proc |
read(name) under /proc/bare_os/ |
sdk.ipc |
pushJson / takeJson when ctx.bareOsIpc exists |
sdk.app |
run, confirm, prompt, select, form |
Stock utilities
TTY mode for dhttop, swarmmap, routeview, holepunch-view, swarmtop, chat, edit, nano, baretop, btop, irc, and summon calls ctx.tui.run when the SDK is attached (edit/nano/baretop/irc/summon use buffer: 'cell'). agent --setup uses ctx.tui.form. All of these keep the pre-SDK path if BARE_OS_TUI=0. irc defaults to Libera.Chat TLS (irc.libera.chat:6697); see irc-client.md. summon is the text web browser; see summon.md.
Environment
BARE_OS_TUI=0— omitctx.tui/ctx.sdkBARE_OS_TUI_NO_ALTSCREEN— full clear instead of?1049hBARE_OS_TUI_DEBUG— reservedNO_COLOR,BARE_OS_COLOR_DEPTH,BARE_OS_THEME— style / theme