# 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/`](../lib/tui/src/) concatenated to [`/lib/bare-os/tui.js`](../kernel/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](../docs/reference/ctx-tui.md). Inspector: **`tui`** / **`man tui`**. ## Minimal app ```js 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-counter/), [`examples/tui-form/`](../examples/tui-form/), [`examples/tui-dashboard/`](../examples/tui-dashboard/), [`examples/tui-irc/`](../examples/tui-irc/), [`examples/tui-summon/`](../examples/tui-summon/). Live guest: **`tui demo`**. ## TEA contract - **`init()`** → first command or `null` - **`update(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: ```js 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](../docs/reference/irc-client.md). **`summon`** is the text web browser; see [summon.md](../docs/reference/summon.md). ## Environment - **`BARE_OS_TUI=0`** — omit `ctx.tui` / `ctx.sdk` - **`BARE_OS_TUI_NO_ALTSCREEN`** — full clear instead of `?1049h` - **`BARE_OS_TUI_DEBUG`** — reserved - **`NO_COLOR`**, **`BARE_OS_COLOR_DEPTH`**, **`BARE_OS_THEME`** — style / theme --- [← Apps beyond the shell](07-apps-beyond-the-shell.md) · [Testing →](08-testing-and-debugging.md)