package symphony-orchestrator-tui

  1. Overview
  2. Docs

Symphony TUI

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.

Quick start

Add the opam package:

opam install symphony-orchestrator-tui

Then 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.exe

Public API layers

Use the lower layers when you need control. Use Components and Patterns for day-to-day application code.

  • Core: Geometry, Color, Theme, Style, Surface, Node, Layout, Renderer, Terminal, Viewport, Key, and Keymap.
  • Components: 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: Patterns.app_shell, header, rule_panel, metric_card, log_feed, message, timeline, composer, command_bar, footer, and modal.
  • Presets: 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.

Component guide

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.

Text and layout

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.

Forms and selection

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.

Status and data

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.

Messages, logs, and empty states

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.

Putting components together

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.

Layout usage

Most layout is configured through Style.make. The toolkit supports terminal-focused flexbox concepts:

  • width, height, min_width, and min_height
  • flex_direction, flex_grow, and flex_shrink
  • justify_content and align_items
  • padding, margin, gap, and borders
  • absolute positioning for overlays such as modals

Example:

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" ];
    ]

Rendering modes

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 renderer

For 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.

Theming

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 ]
    ()

Source layout

Tui is a compatibility facade. The implementation is split into focused modules under lib/:

  • Core rendering and input modules live in files such as style.re, surface.re, node.re, layout.re, render.re, and renderer.re.
  • Theme tokens and palette helpers live in theme.re.
  • Component design context and reusable widgets live under components/.
  • Reusable components use one file per component, for example 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.

Keyboard usage

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.

Examples

Example documentation lives in examples/README.md.

Example

Purpose

Run

demo

Minimal component composition and fixed snapshot rendering.

opam exec -- dune exec examples/demo.exe

operations_dashboard

Wide dashboard using panels, metrics, tables, logs, and app shell patterns.

opam exec -- dune exec examples/operations_dashboard.exe

agent_workspace

Message-first agent workspace with navigator, transcript, composer, and run state.

opam exec -- dune exec examples/agent_workspace.exe

opencode_splash

Responsive full-screen splash using OpenCode-inspired presets.

opam exec -- dune exec examples/opencode_splash.exe

opencode_session

Responsive session view with conversation, composer, footer, and optional right rail.

opam exec -- dune exec examples/opencode_session.exe

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.exe

Development

Run tests:

opam exec -- dune runtest

Build everything in the package:

opam exec -- dune build @all

Build the opam metadata:

opam exec -- dune build @opam

Package release

The 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 runtest

Install opam-publish once if the command is missing:

opam install opam-publish

Because 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.0

Upload 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 .