Renderer
Terminal I/O, scheduling and runtime composition.
What is a renderer?
The renderer is the runtime object that drives Cascade. It is responsible for:
- terminal setup (raw mode, alternate screen, mouse)
- input decoding (keyboard, mouse)
- composing your UI tree through the root renderable
- scheduling frames (when and how often the terminal is redrawn)
- cleanup (restoring the terminal state on exit)
Most applications have exactly one renderer instance for the lifetime of the process.
Creating a renderer
Create a renderer with createCliRenderer. Ctrl+C exit is enabled by default. You typically add mouse support and an alternate screen for a clean UI.
import { createCliRenderer } from "@cascadetui/core"
const renderer = await createCliRenderer({
targetFps: 30,
useMouse: true,
useAlternateScreen: true,
})If you are building a “single-shot” UI (render once, then exit), you can still use the renderer, mount a tree, and destroy immediately after you are done. Most apps keep it alive.
Config reference (common options)
| Option | Type | Default | Notes |
|---|---|---|---|
useConsole | boolean | false | Shows an in-UI console overlay that captures logs (recommended in development). |
consoleOptions | ConsoleOptions | - | Customizes position/height/title/debug mode of the console overlay. |
openConsoleOnError | boolean | false | Opens the console automatically when a crash report is captured. |
logCrashReportsToConsole | boolean | false | Prints crash report details via console.error (useful in CI / debugging). |
useKittyKeyboard | KittyKeyboardOptions | null | undefined | Enables enhanced keyboard protocol features on terminals that support it. |
The root renderable
Every renderer has a root renderable. It behaves like the top container of your UI tree: it fills the terminal and updates its size on resize.
import { BoxRenderable, TextRenderable } from "@cascadetui/core"
const root = renderer.root
const panel = new BoxRenderable(renderer, { width: 40, height: 6, border: true, padding: 1 })
panel.add(new TextRenderable(renderer, { content: "Hello from root" }))
root.add(panel)With constructs (the declarative API), you add VNodes to the root as well (after instantiation).
Render loop control
Cascade supports multiple ways of driving rendering. Which one you use depends on whether your UI is static, interactive, or animated.
On-demand rendering: if you never call start(), Cascade can redraw only when the UI tree changes.
Continuous rendering: call start() to render continuously at the target FPS (useful for highly dynamic UIs).
renderer.start()
setTimeout(() => {
renderer.stop()
}, 2000)Live rendering: for short animations, request live mode temporarily. This pattern avoids running a full render loop forever.
renderer.requestLive()
setTimeout(() => {
renderer.dropLive()
}, 400)Pause vs suspend: pausing stops drawing but keeps the terminal configured; suspending fully releases control (mouse/input/raw mode) until resumed.
renderer.pause()
renderer.resume()
renderer.suspend()
renderer.resume()Events and runtime signals
The renderer emits runtime events. The most important is resize, which you can use to recompute layout or update derived state.
renderer.on("resize", (width, height) => {
console.log(`Resize: ${width}x${height}`)
})If you build apps that must react to theme changes (dark/light), keep your “theme tokens” in a single place and update component colors when the mode changes.
Cleanup and exit
By default, exitOnCtrlC: true handles clean exit automatically. When the user presses Ctrl+C, Cascade calls destroy() internally, restoring terminal state (cursor, input modes, alternate screen). No manual handling is required.
// Default behavior: Ctrl+C exits cleanly
const renderer = await createCliRenderer()
// Optional: run cleanup code before exit
renderer.onDestroy(() => {
// stop timers, close sockets, flush logs
})For broader signal handling, exitSignals (default: SIGINT, SIGTERM, SIGQUIT, SIGHUP, etc.) also triggers clean shutdown. Set to an empty array to disable.
If you need custom Ctrl+C behavior (e.g., confirmation prompt), disable the default and handle it yourself:
const renderer = await createCliRenderer({ exitOnCtrlC: false })
renderer.on("key", (event) => {
if (event.ctrl && event.name === "c") {
// custom logic, then:
renderer.destroy()
}
})