Skip to content

Oil

Oil is Crucible’s terminal UI layer. It provides a declarative, flex-based renderer for the TUI and exposes the same primitives to Lua plugins so they can build views without knowing about cursor positioning, ANSI escape codes, or terminal dimensions.

The Rust crate is crucible-oil. The Lua bindings live in crucible-lua and are exposed as cru.oil.*.

Why a Dedicated Layer

Crucible’s TUI runs across several surfaces — the chat viewport, popups, overlays, Lua-defined custom views — that all need consistent spacing, theming, and layout. Rather than each surface driving crossterm directly, everything builds a tree of Nodes and hands it to the Oil renderer. The renderer handles:

  • Flex layout (col, row, gaps, padding, alignment)
  • Theming (named colors, bold/italic/underline, borders)
  • Wrapping (terminal-aware text wrap, CJK-aware width)
  • Caching (frame diffing so only changed cells get repainted)

The Primitives

The Rust node tree and the Lua DSL mirror each other. The building blocks fall into four groups:

Layoutcol, row, fragment, spacer
Contenttext, divider, hr, spinner, progress, badge
Listsbullet_list, numbered_list, kv
Inputinput, popup (dropdowns/menus)

Control flow helpers (when, either, each, match_state) let you build reactive views without imperative branching, and every node supports chainable style/padding/margin/border methods.

For the full Lua API with signatures and examples, see Oil Lua API.

Relation to the TUI

Every frame of the Crucible TUI is built by composing an Oil tree top-down: chat containers, the input area, the status bar, popups. The Container trait in the chat app builds sub-trees; the root tree is laid out in one pass and rendered to a CellGrid, which is diffed against the previous frame and flushed to the terminal.

Plugins can participate in two ways:

  1. Status bar customizationcru.statusline.setup{} composes the rows around the input from Lua items (badges, expressions, conditionals), with daemon-computed values pushed via sl.expr.
  2. Themingcru.colorscheme.setup{}, cru.hl.set/link, and cru.geometry.setup{} restyle colors, highlight groups, and per-surface geometry without restarting.

Relation to crucible-oil (the crate)

The crate is kept separate from crucible-cli because crucible-lua depends on it (the Lua bindings need the Rust types). Everything the renderer produces is a plain data structure — no I/O until the final render_to_string() call — which is what lets the same tree drive live TUI output, snapshot-tested golden strings, and GIF-replay fixtures.

See Also