Skip to content

Repository files navigation

lazytui

everybody tui — a glue framework for the tools around your real work; AI writes it, you run it as TUI or CLI.

MIT License · Node.js · npm runtime deps: node-pty, @xterm/headless, js-yaml, eastasianwidth, wcwidth

╭─(1)─Containers───────╮╭─(0)─Actions────────────────────────────────╮
│ * pg                 ││ > Build (configure + make)             tab │
╰────────────────1 of 1╯│   Test (make check)                    tab │
╭─(2)─Groups───────────╮│   initdb (one-time)                        │
│ 1 postgres 16    1 ok││   Start postgres server                    │
╰────────────────1 of 1╯│   psql (interactive)              [spawn]  │
                        │   Server log (snapshot)                tab │
                        ╰──────────────────────────────────1 of 7────╯
                        ╭─(o)─Info─[Build]───────────────────────────╮
                        │ Configure and compile postgres 16 from     │
                        │ source inside the pg container. Streams    │
                        │ output to the detail panel. Slow on first  │
                        │ run (~5 min); subsequent rebuilds are      │
                        │ incremental.                               │
                        ╰────────────────────────────────────────────╯
 up/dn select  h/l panel  Enter run  ] tab  : cmd  ? help  q quit

Quickstart

git clone https://github.com/Tao-Ma/lazytui.git
cd lazytui

# One-time deps: node-pty + @xterm/headless + js-yaml + eastasianwidth + wcwidth.
npm install --omit=dev

# Try a worked demo. Requires Docker on the host.
cd demo/postgres && ./run

See DEMO.md to add your own demo.

The problem this solves

If your real work is a database kernel, a network stack, a compiler, or any other piece of systems code, you already know the shape of the problem. You spend more time than you would like writing the glue — shell scripts that bring up the dev environment, CI hooks that lint and package, OS-specific incantations that the team copies between Linux distros and macOS versions, ad-hoc flags that slowly multiply as the kernel grows another dependency.

The glue is dirty work, and it is essential work. It is also where energy goes to die. Every OS upgrade, every new dep, every new flag eventually breaks something. Debugging a shell script that worked last week is a tax on the time you wanted to spend on the database.

lazytui is a different shape for that glue layer.

The shape

Two things changed at roughly the same time:

  1. AI coding agents got good enough to write and test the glue. A deploy.sh with a year of accumulated flag sediment can now be a short YAML declaration plus agent-authored script bodies covered by tests the agent also wrote.
  2. lazygit / lazydocker / k9s proved the TUI shape. A column-major, keyboard-first, panel-based dashboard is the right interface for "operate on a fleet of named things with a fixed set of verbs."

lazytui sits in the same niche as shell, Perl, go-task, Make — generic glue between you and your systems — but with three properties those tools do not have together:

  • You describe intent in plain language. The AI produces the YAML, the script bodies, the Components, the tests. You review and run; you do not hand-author the glue. The YAML is an artifact you read and edit when you need to, not a file you start from a blank page.
  • Every action runs in two modes from one definition: TUI and CLI. No flags to memorize during interactive use. The same action is --exec group:action [args] from CI or from another agent.
  • The framework is generic. It has no built-in knowledge of Docker, Kubernetes, your database, your CI provider, or your OS. All of that lives in your YAML and Components. The renderer cannot leak domain knowledge because there is nowhere for it to leak.

The result: glue stops being a swamp of personal shell scripts only you can debug. It becomes a small, versioned, AI-maintained contract you operate through a TUI and that CI invokes through a CLI.

The loop

You never start by writing YAML. The loop is:

  1. You describe what you need to an AI coding agent, in whatever words come naturally. "Wrap our staging deploy and rollback scripts. I want logs in a side panel and a list of live pods. CI should be able to call deploy and rollback non-interactively."
  2. You hand the agent the contract with bin/lazytui --spec. That single command dumps every rule the agent needs — schema, Component API, markup rules, hub protocol — into one file.
  3. The agent produces a project: a YAML config, any script bodies, optional JS Components, and tests. You review the diff like any other PR.
  4. You run it. TUI for interactive ops, CLI (--exec) for CI and for other agents.

The YAML is small and readable on purpose — when you need to step in and edit it by hand, you can. But the default mode is "the agent maintains it; you maintain the intent."

What a real project looks like

Excerpt from demo/postgres/tui.yml, which an AI agent produced from a one-page prompt:

groups:
  pg:
    label: postgres 16
    compose: docker-compose.yml
    containers:
      - pg
    actions:
      build:
        label: Build (configure + make)
        desc: Configure and compile postgres 16 from source inside the
              pg container...
        type: run
        tab: true
        script: docker compose exec -T pg bash /scripts/build.sh

      psql:
        label: psql (interactive)
        type: spawn
        script: docker compose exec -it pg /opt/pg/bin/psql -U postgres

Run it interactively (the TUI you see at the top of this README):

cd demo/postgres && ./run

Or headlessly, with the same definition, exiting with the action's rc:

./run --exec pg:build
./run --list                   # enumerate every action

When the project outgrows one file, the agent splits it into YAML or JS Components against js/panel/api.js, declared in the config's components: list so they load without editing the framework tree. The framework dogfoods that same API for its own built-in panels — there is no privileged path the agent cannot reach.

Worked demos

Demo Target Shape Status
postgres PostgreSQL 16 from source Single container, build → test → psql Verified end-to-end on Docker. See POSTMORTEM.md for the loop discipline applied to a live discovery (DinD bind-mount).
cloudberrydb Apache Cloudberry main Wraps upstream's devops/sandbox/ — lazytui adds the YAML/CLI surface on top of upstream's docker YAML parses; live build not yet verified (~30 min cold). See POSTMORTEM_v1.md for the upstream-pivot decision.

The two demos prove the two shapes demos can take — produce a docker stack from scratch, or wrap an upstream project's existing docker infrastructure. Picking rule and conventions in DEMO.md.

How is this different from Make / shell / Taskfile?

Make / shell scripts go-task / Taskfile lazytui
Authoring You write it You write YAML An AI writes both YAML and scripts from your intent
Surface CLI only CLI only TUI + CLI from one definition
Discoverability Read the script task --list Panel of named actions; : cmdline; --list; --spec for agents
Maintenance You debug when it rots You debug when it rots Re-run the loop; agent reads --spec and produces a fresh version
Domain knowledge In the scripts In the Taskfile In your YAML / Components; the framework knows nothing

It is fine to keep using Make for tasks you actually enjoy maintaining. lazytui is for the other tasks — the glue layer that already costs you more than it should.

--spec and --exec — the load-bearing flags

Two flags carry the AI-augmented loop.

--spec is how the agent learns the contract. It prints the consolidated authoring bundle — SPEC, PRINCIPLES, PLUGINS, PROJECT, HUB, LAYOUT — to stdout in one shot. Hand it to your agent and every rule it needs to produce a valid project is in a single file.

bin/lazytui --spec > /tmp/lazytui-spec.md

--exec is how the agent (or CI) invokes what it produced. Same action definition; no render pipeline loaded; exits with the action's return code. CI calls it. Another agent calls it. No second wrapper for "the same thing but headless."

What you get out of the box

Surface What it does
N-column layout (v0.6.2+) Ordered columns list, default 2 columns. Soft caps: 6 panes in the first, 3 in the last. Detail panes live in any column — and since v0.6.4 a layout may declare several of them, each the sole tab of its pane. YAML-configurable content + column count. Grow / shrink at runtime via drag-edge spawn or :add-column / :remove-column.
Action types run (capture output), spawn (full-screen interactive), background (fire-and-forget). One uniform schema.
Built-in panel types groups, actions, files, history, detail, info, terminal, text-view, agent, component-ports, fabric-wires, plus docker container / stats panels.
Embedded terminals First-class terminal panes (from type: spawn actions, docker exec, group terminals:, or :terminal), persistent across group switches. Scrollback via mouse-wheel and Shift+PageUp/PageDown/Home/End, with smart mouse-forwarding — bytes reach the child only when it enabled mouse reporting (vim, htop, less --mouse); otherwise the wheel scrolls scrollback, and a [↑N] indicator shows how far back the view sits (v0.6.5).
Component dataflow fabric (v0.6.8+) Components publish typed output ports and consume typed input ports. Wires are standing producer→consumer connections; injects are one-shot by-value pushes (right-click "Send selection to port…", or an in-grid field edit). A consumer's run: is a no-shell argv template ({{holes}} = bound parameters, executed via execve) so command injection is structurally impossible. Inspect it live via the component-ports pane and the fabric-wires wire list. See docs/ports-and-wires.md.
Live agent pane (v0.6.9+) :agent [backend] mints a chat pane driving a long-lived AI agent: Enter starts/activates, keystrokes compose a draft, ↑/↓ recall previously-sent messages, Esc interrupts a running turn or leaves the mode. Replies stream into the transcript as they arrive (throttled ~10 Hz). The transcript is modeled pure-TEA, so a recorded session replays without re-calling any LLM. Backends plug in behind a normalized protocol: mock (built-in) and pi (Pi via pi --mode rpc). See docs/live-agent.md.
Event hub In-process pub/sub for Components. Time-series, snapshot, matrix shapes. Cost scales with subscribers.
Cmdline (:) :quit, :refresh, :help, plus Component-registered verbs, with positional-arg plumbing.
Running overlay (<leader> j, v0.6.2+) Modal listing every live child lazytui spawned (streamed actions, PTYs, background + tmux spawns). Enter jumps to the relevant tab; Esc closes. A tab running a live stream shows a ● indicator in its pane's tab strip.
Pane menu ([≡], v0.6.3+, unified v0.6.4) Every pane has a [≡] glyph at top-left; click (or T) opens one dropdown listing panes + this pane's tabs. The pick is projection-aware: in normal view it swaps which pool entry occupies the cell, in half view it places the pick into the clicked slot, in full view it switches focus. Mouse + keyboard nav.
Mouse actions (v0.6.4+) Left-click focuses + selects, double-click activates (Enter-equivalent), right-click opens a context menu at the cursor (copy line / copy selection + general entries; click-outside dismisses), wheel scrolls the pane under the cursor; drag-select persists like a v visual selection. Remappable via a YAML mouse: block; the context menu is extensible via a context-menu: block with keys-style verbs and per-pane gates.
Diagnostics window (<leader> e, v0.6.4+) Modal listing the warnings (⚠) and errors (✕) raised this session — boot config warnings, runtime errors, and multi-instance footgun guards. j/k/g/G nav, y copies the highlighted entry to the register + clipboard, c clears, s saves to lazytui-diagnostics.json, Esc closes. Backed by a dedicated buffer separate from the replay event-log so diagnostics aren't evicted by input noise.
Navigation history (<leader> o / <leader> i, v0.6.7+) Browser/vim-jumplist back (o = older) / forward (i = newer) over visited locations — group + focused pane + active tab + selected item, captured by stable identity so the cursor lands on the same item after a list reorders. A location whose group is gone is pruned and the travel continues.
Configurable keys (v0.6.7+) Remap normal-mode single keys via a YAML keymap: block (key → verb, noop to disable, or {action|command} targets); the focus/mode-branching keys (nav / return / escape / x / …) stay reserved. lazytui --keymap dumps the verb catalog + reserved keys + effective bindings for discovery (AI-config-friendly). See docs/keymap.md.
Global user config ~/.config/lazytui/config.yml (XDG-aware) carries your app-behavior preferences — theme, keys, keymap, mouse, context-menu entries, selection default, editor, color depth, keyboard protocol — layered under every project's config (a project setting wins per key). A broken global file warns and is ignored, never fatal. See docs/global-config.md.
Truecolor + graphs (v0.6.13+) The 7 themes carry their schemes' real hex palettes (24-bit color, auto-detected; quantizes cleanly to 256/16 — color_depth: / LAZYTUI_COLOR override); stats panes draw braille graphs colored by value through a per-theme gradient, with current-value meters on percent metrics.
Kitty keyboard protocol (v0.6.14+) On supporting terminals, negotiates CSI-u "disambiguate escape codes" at boot (query + Primary-DA fence handshake) so Escape and ctrl/alt combos arrive unambiguously; events normalize back to the legacy keymap, so nothing else changes. keyboard_protocol: (auto/legacy/kitty) or LAZYTUI_KBD override; popped on suspend/exit so a spawned shell is never left in it. Terminals without it are untouched. The negotiated protocol shows in the <leader> e diagnostics window. See docs/kitty-keyboard.md.
Edit in your editor e on a files row, :edit <path>, or :config / :config global opens your editor (editor: config / $VISUAL / $EDITOR / vi) in an embedded terminal tab — auto-zoomed, back where you were on quit. An open doc tab showing the file refreshes on a clean exit; config edits remind you they apply on the next start.
7 themes + free-config mode :free-config opens an interactive layout editor — drag/swap/resize/spawn columns and panels, hide/show from a pool of declared panel definitions, save back to YAML.
--spec flag Prints the Component-authoring bundle for AI agents (every rule in one file).
Compile to a native binary (v0.6.23+) lazytui build config.yml bundles the framework + config + Components into one self-contained executable (Bun --compile) — no Node/Bun to run it. Releases also ship prebuilt per-platform lazytui CLI binaries. See docs/packaging.md.

Status

  • Renderer + parser: Node.js. Runtime npm deps: node-pty and @xterm/headless for embedded PTY tabs, js-yaml for config parsing, eastasianwidth (UAX #11 wide) + wcwidth (POSIX zero-width) for the Unicode character-width truth function.
  • Tests: JS unit suites under js/test/ (202 files), an opt-in pre-release smoke harness under js/test/smoke/ (22 scenarios), and a live integration harness under test/. See docs/TESTING.md.
  • Two worked demos at the time of initial public release; both ship with the human-authored intent (.agent-prompt.md) checked in so the loop is reproducible by another agent.

Read next

Using lazytui:

  • docs/SPEC.md — Component authoring quickstart; brief any agent with this.
  • DEMO.md — convention for adding your own demo, including the two demo shapes and the "fix the prompt, not the artifact" rule.
  • docs/PROJECT.md — what a user project looks like on disk and how path discovery works, including the components: config key for consumer-authored panel types.
  • docs/packaging.md — compile an app (framework + config + Components) into one self-contained native binary.

Contributing to lazytui itself:

History (archived): docs/history/ keeps the round-1 refactor retrospective, the dev9-era resume snapshot, and the feature backlog. Not load-bearing; useful for context.

Run

# Interactive
bin/lazytui path/to/config.yml

# CLI — single action, exits with the action's rc
bin/lazytui path/to/config.yml --exec group:action [args...]

# Enumerate every action
bin/lazytui path/to/config.yml --list [filter]

# Print the Component-authoring bundle (feed to an AI agent)
bin/lazytui --spec > spec.md

Debugging / replay (session recording shipped in v0.6.6, debugger layer v0.6.7):

# Record a session from boot (also via the :record-save verb mid-session)
bin/lazytui path/to/config.yml --record-save session.wal

# Replay interactively — a scrubber pane (checkpoints, play/pause/reverse,
# per-Msg model diff, skip-to-change). The recording carries its own config.
bin/lazytui --record-load session.wal

# Headless: reconstruct a frame and print it (--seq <n> picks a WAL sequence)
bin/lazytui --record-print session.wal

# Headless WAL console: dump a recording's entries for grepping
bin/lazytui --dev session.wal --filter lane=root,type=key --seq-range 0..200 --diff --json

About

everybody tui — a glue framework for the tools around your real work; AI writes it, you run it as TUI or CLI.

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages