Page
Library
Module
Module type
Parameter
Class
Class type
Source
Symphony TUI is an OCaml terminal UI toolkit. It provides a renderer-owned component tree, buffered cell surfaces, ANSI and plain-text rendering, Toffee-backed flexbox layout, keyboard parsing, keymaps, viewport helpers, and reusable terminal UI widgets.
The package is intentionally framework-free. You build a tree of Tui.Node.t values, render it with Tui.Renderer, and decide whether your program prints a static snapshot or runs an interactive terminal loop.
Unless noted otherwise, commands in this README run from apps/tui.
Add the opam package:
opam install symphony-orchestrator-tuiThen add the library to a Dune executable:
(executable
(name my_console)
(libraries symphony-orchestrator-tui))Create a root node and render it:
open Tui
let root =
box
~style:Style.(make ~border:Rounded ~title:"Build" ~padding:(spacing_all 1) ())
[
text "Hello from OCaml";
progress_bar ~label:"compile" 0.72;
input ~placeholder:"type here" ();
]
let () =
let renderer = Renderer.create ~width:60 ~height:12 root in
print_endline (Renderer.render_to_string renderer)Run one of the bundled examples:
opam exec -- dune exec examples/demo.exeUse the lower layers when you need control. Use Components and Patterns for day-to-day application code.
Geometry, Color, Theme, Style, Surface, Node, Layout, Renderer, Terminal, Viewport, Key, and Keymap.text, rich_text, box, input, select, scroll_box, progress_bar, sparkline, panel, badge, tab_bar, key_value, table, split, row, column, divider, callout, empty_state, toolbar, and meter.Patterns.app_shell, header, rule_panel, metric_card, log_feed, message, timeline, composer, command_bar, footer, and modal.Presets.Open_code.wordmark, model_status, command_block, hint_bar, and tip.Components are the stable building blocks. Patterns are opinionated application layouts. Presets are example-shaped helpers and should not be treated as neutral primitives. Existing callers can still reach moved pattern and preset helpers through Components; prefer the canonical Patterns and Presets modules for new code.
Most applications start with box, row, column, and panel, then add focused widgets inside those containers. Components return Tui.Node.t, so they compose by nesting lists.
open Tui
let root =
Components.column
~style:Style.(make ~width:(Percent 1.) ~height:(Percent 1.) ~gap:1 ())
[
Components.row
~style:Style.(make ~height:(Cells 1) ~gap:2 ())
[
text "Queue";
Components.badge ~tone:Components.Success "ready";
];
Components.divider ~width:48 ~title:"Runs" ();
Components.panel "Current task"
[
text "Build package payload";
Components.meter ~label:"progress" ~value:"72%" 0.72;
];
]Use row when children should sit beside each other. Use column when they should stack. Style.make controls width, height, growth, padding, margin, gap, colors, and borders.
let queue_picker =
Components.panel "Queue"
[
select
~style:Style.(make ~height:(Cells 5) ())
[
option ~description:"ready to start" "bootstrap";
option ~description:"running tests" "backend";
option ~description:"waiting on review" "frontend";
];
input ~placeholder:"filter tasks" ();
]input, select, and scroll_box are focusable. Renderer.dispatch_key routes keyboard events to the focused node first, then falls back to the renderer keymap.
let run_summary =
Components.panel ~tone:Components.Info "Run summary"
[
Components.key_value
~label_width:12
[
("branch", "task/tui-readme");
("status", "running");
("worker", "agent-2");
];
Components.table
[ ("STEP", 18); ("STATE", 10); ("TIME", 8) ]
[
[ "build"; "OK"; "11s" ];
[ "test"; "RUNNING"; "04s" ];
];
]Use key_value for dense metadata and table when rows need alignment. Fixed cell widths are deliberate: terminal tables read better when columns stay put.
let activity =
Components.panel "Activity"
[
Patterns.log_feed
~style:Style.(make ~height:(Cells 6) ())
[
("12:00", "INFO", "created task branch");
("12:01", "OK", "backend tests passed");
("12:02", "WARN", "frontend build still running");
];
Components.callout
~tone:Components.Warning
~title:"Next check"
[ text "Review generated assets before publishing." ];
]
let no_results =
Components.empty_state
~detail:"No tasks match the current filter."
~action:"Press / to change the filter."
"Nothing to show"log_feed is a pattern because it has opinions about timestamp and level formatting. callout and empty_state are plain components.
This example builds a compact work queue view. It uses Patterns.app_shell for the frame, Components.split for the main layout, panels for sections, and smaller components inside each panel.
open Tui
let design =
Components.make_design ~theme:Theme.high_contrast_dark ()
let sidebar =
[
Components.toolbar
~design
[ ("n", "ew"); ("/", "filter"); ("?", "help") ];
Components.panel ~design "Queues"
[
select
~style:Style.(make ~height:(Cells 6) ())
[
option ~description:"3 tasks" "Ready";
option ~description:"1 task" "Running";
option ~description:"2 tasks" "Review";
];
];
Components.panel ~design "Runtime"
[
Components.key_value
~design
~label_width:10
[
("workspace", "demo");
("branch", "task/tui");
("agent", "builder");
];
];
]
let main =
[
Components.row
~style:Style.(make ~height:(Cells 7) ~gap:1 ())
[
Components.panel
~design
~tone:Components.Success
~style:Style.(make ~width:(Cells 28) ())
"Build"
[ Components.meter ~design ~label:"package" ~value:"72%" 0.72 ];
Components.panel
~design
~tone:Components.Info
~style:Style.(make ~width:(Cells 28) ())
"Tests"
[ Components.meter ~design ~label:"backend" ~value:"24/24" 1.0 ];
];
Components.panel ~design "Tasks"
[
Components.table
~design
[ ("TASK", 18); ("STATE", 10); ("OWNER", 10) ]
[
[ "bootstrap"; "OK"; "agent-1" ];
[ "backend"; "RUNNING"; "agent-2" ];
[ "frontend"; "REVIEW"; "agent-3" ];
];
];
Components.panel
~design
~tone:Components.Warning
"Recent events"
[
Patterns.log_feed
~design
~style:Style.(make ~height:(Cells 5) ())
[
("13:41", "INFO", "created task worktree");
("13:42", "OK", "package build finished");
("13:43", "WARN", "review still pending");
];
];
]
let root =
Patterns.app_shell
~title:"Task queue"
~subtitle:"workspace demo"
~design
~badges:[ (Components.Success, "live"); (Components.Info, "local") ]
~footer_items:[ ("q", "uit"); ("r", "efresh"); ("/", "filter"); ("Tab", "focus") ]
[ Components.split ~left_width:30 sidebar main ]
let () =
let renderer = Renderer.create ~width:120 ~height:32 root in
print_endline (Renderer.render_to_string renderer)The important bit is the shape: compose small nodes, put repeated chunks in let bindings, and keep stateful widgets inside panels that make their role obvious.
Most layout is configured through Style.make. The toolkit supports terminal-focused flexbox concepts:
width, height, min_width, and min_heightflex_direction, flex_grow, and flex_shrinkjustify_content and align_itemspadding, margin, gap, and bordersExample:
open Tui
let root =
Components.row
~style:Style.(make ~width:(Percent 1.) ~height:(Percent 1.) ~gap:1 ())
[
Components.panel
~style:Style.(make ~width:(Cells 28) ())
"Navigator"
[ text "Queue"; text "Runs"; text "Logs" ];
Components.panel
~style:Style.(make ~flex_grow:1. ())
"Detail"
[ text "Selected item" ];
]For a fixed snapshot, construct a renderer with an explicit size:
let renderer = Renderer.create ~width:96 ~height:24 root
let plain = Renderer.render_to_string renderer
let ansi = Renderer.render_to_string ~ansi:true rendererFor a terminal-sized view, read the current viewport:
let viewport = Terminal.viewport () in
let renderer =
Renderer.create ~width:viewport.width ~height:viewport.height (root viewport)For full-screen previews, enter the alternate screen, render, and restore the terminal when the program exits. The OpenCode-inspired examples show this pattern.
Components and patterns accept an optional ?design value. A design combines a theme with tone mapping:
open Tui
let design = Components.make_design ~theme:Theme.light ()
let root =
Components.panel ~design ~tone:Components.Success "Status"
[
Components.badge ~design ~tone:Components.Success "ready";
Patterns.log_feed ~design [ ("12:00", "OK", "started") ];
]Theme.dark is the default. Use Theme.light, Theme.high_contrast_dark, Theme.named, or a custom Theme.t when an application needs its own palette. For custom palettes, build a Theme.palette with Theme.make, convert it with Theme.of_palette, and override individual slots with Theme.with_slot.
For component styles that should follow the active design, use the Components.style, tone_style, and surface_style helpers:
let style =
Components.style
~design
~fg:Theme.Accent_primary
~bg:Theme.Bg_surface
~attrs:[ Attr.Bold ]
()Tui is a compatibility facade. The implementation is split into focused modules under lib/:
style.re, surface.re, node.re, layout.re, render.re, and renderer.re.theme.re.components/.components/component_panel.re, components/component_table.re, components/component_callout.re, and components/component_meter.re.components/components_core.re, components/components.re, patterns.re, and presets.re aggregate the public layers.Callers should prefer the stable Tui.Components, Tui.Patterns, and Tui.Presets namespaces. The split files keep implementation ownership clear without requiring consumers to learn every internal module.
Inputs, selects, and scroll boxes handle common keys through Renderer.dispatch_key. For application-level shortcuts, use Keymap:
let keymap = Keymap.create ()
let quit = ref false
let () =
Keymap.register keymap ~key:"q" ~name:"quit" ~run:(fun () -> quit := true)The renderer also supports focus routing for focusable nodes such as input, select, and scroll_box.
Example documentation lives in examples/README.md.
Example | Purpose | Run |
|---|---|---|
| Minimal component composition and fixed snapshot rendering. |
|
| Wide dashboard using panels, metrics, tables, logs, and app shell patterns. |
|
| Message-first agent workspace with navigator, transcript, composer, and run state. |
|
| Responsive full-screen splash using OpenCode-inspired presets. |
|
| Responsive session view with conversation, composer, footer, and optional right rail. |
|
The OpenCode-inspired examples open a full-screen preview and exit on any key when run in an interactive terminal. They size themselves from COLUMNS and LINES when those variables are exported, otherwise they query the active TTY. If your shell exports NO_COLOR, unset it for the preview:
env -u NO_COLOR opam exec -- dune exec examples/opencode_splash.exeRun tests:
opam exec -- dune runtestBuild everything in the package:
opam exec -- dune build @allBuild the opam metadata:
opam exec -- dune build @opamThe publishable opam package is symphony-orchestrator-tui. opam does not support npm-style scoped names such as @symphony-orchestrator/tui, so the package uses the same words joined with dashes. The installed OCaml module remains Tui.
Package metadata lives in dune-project, and symphony-orchestrator-tui.opam is generated from it.
The GitHub Actions TUI package workflow watches pushes that touch apps/tui. It continues only when apps/tui/package.json has a version bump in the same push. On the repository default branch, the workflow validates the package, builds dist/symphony-orchestrator-tui-<version>.tar.gz, and publishes that archive to a tui-v<version> GitHub release.
Before publishing manually, run the package checks from apps/tui:
opam lint symphony-orchestrator-tui.opam
opam install . --deps-only --with-test
opam exec -- dune build @all
opam exec -- dune runtestInstall opam-publish once if the command is missing:
opam install opam-publishBecause this package lives under apps/tui, publish an archive whose root is the TUI package directory. A full repository archive also contains the backend and frontend projects, which is not what opam needs for this package.
Create the package archive from a tag:
sh scripts/release-archive.sh 0.1.0 tui-v0.1.0Upload the generated dist/symphony-orchestrator-tui-0.1.0.tar.gz file to the GitHub release for that tag. Then publish through the standard opam-repository PR flow from apps/tui:
opam publish https://github.com/MatheusBBarni/symphony-orchestrator/releases/download/tui-v0.1.0/symphony-orchestrator-tui-0.1.0.tar.gz .