Files
bare-operating-system/developer-guide/20-tui-and-sdk.md
T
Raven Scott 15afc148d7
Release rolling / release (push) Successful in 9m30s
Updates
2026-08-13 00:13:03 -04:00

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 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:

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 — 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 · Testing →