diff --git a/README.md b/README.md index f1a54dd..b247bd7 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,122 @@ # phux -**A libghostty-backed terminal control plane.** Spawn, observe, control, persist, and address terminals — locally or across a fleet — with a tmux-shaped TUI riding on top as one consumer among several. +**A terminal multiplexer that doesn't re-parse your terminal.** -The gap it fills, in the words of libghostty's author: +tmux and screen sit in the middle of the wire and re-interpret every byte your +programs emit — which is why kitty graphics, undercurl, pixel-precision mouse, +and half of OSC degrade or vanish across a detach/reattach. phux runs the *same* +VT parser ([libghostty][lghv]) on **both ends** of the connection, so modern +terminal protocols survive a remote attach losslessly. Structurally — not "we +patched the common cases." -> "Need to replace tmux with a libghostty-based multiplexer so it can understand KIP." -> — Mitchell Hashimoto +And the thing it multiplexes isn't a *session* or a *pane*. It's a **terminal**: +a first-class object you can spawn, observe, drive, and address — by hand from a +tmux-shaped TUI, or headless from a script or an agent over a typed wire, on this +box or across a fleet. -## What it is - -The unit of work is the **terminal**, not the session or the pane. Both ends of the wire run [`libghostty_vt::Terminal`][lghv]: the server's is the canonical state; the client's is a local mirror for rendering. Nothing in the middle re-parses VT. Kitty keyboard, true colour, OSC 8, OSC 133, images — they all pass through end-to-end because the parser is identical on both ends. - -The wire is layered. Consumers declare which tiers they speak and the server omits messages from layers they don't subscribe to: - -- **L1 — Terminal.** PTY, bytes-out, structured input, snapshot, lifecycle, events. -- **L2 — Collection.** Named lifecycle bundle of Terminals. Optional. -- **L3 — Metadata.** Opaque KV the server stores but doesn't interpret. Where the TUI keeps its layout, window names, focus pointer. - -Identity is federation-ready from the first byte. `TerminalId` is a `LOCAL { id } | SATELLITE { host, id }` tagged union. Day-1 servers construct `LOCAL` only; day-N hubs route `SATELLITE` ids to satellites on other machines. The wire bytes don't change. - -[lghv]: https://github.com/Uzaaft/libghostty-rs - -## Why it looks different from tmux underneath - -- **No re-parse in the middle.** libghostty parses on both ends. Modern terminal protocols pass through unchanged. -- **The terminal is the substrate, not the pane.** Sessions, windows, panes, splits live in L3 metadata, not on the wire — an agent SDK never hears them. -- **Federation is in the addressing.** Local UDS and the remote hub speak the same wire. Not a single-machine tool that one day might support remote attach. -- **No consumer is privileged.** The reference TUI ships in-tree because the substrate is only real if a real consumer rides it. - -For the full mental model, read [`docs/CONCEPTS.md`](./docs/CONCEPTS.md). For the long arc, read [`docs/vision.md`](./docs/vision.md). - -## Philosophy - -phux solves one problem and refuses to grow a second: spawn, observe, control, persist, and address libghostty terminals — locally or across a fleet — over a layered wire whose tiers a consumer can target without inheriting everything else. The terminal is the substrate; a consumer is anything that rides it. The reference TUI is the proof that the substrate is real, not the product the substrate exists to serve. - -Everything that doesn't make terminals more spawnable, observable, or addressable is out of scope on purpose — see Non-goals below. - -## When to use phux - -**You are a tmux user wanting a modern replacement:** -✅ Yes. The TUI is tmux-shaped and the underlying substrate is stronger: libghostty instead of VT re-parse, federation-ready addressing, and a clean wire contract for agents. Tradeoff: L2 Collection (named sessions) and full CLI surface land in v0.2. Read [`docs/QUICKSTART.md`](./docs/QUICKSTART.md) to get started. +```sh +phux # attach (auto-starts a server) — the tmux-shaped TUI +phux ls --json # list sessions, machine-readable +phux send-keys build:0.1 'cargo test' Enter +phux run build "cargo test" # run in a real pane, get the exit code back +phux wait build --until "0 failed" # block until output matches, then exit 0 +phux watch --json work:1.0 | jq . # live event stream: bells, titles, dirty/idle, lifecycle +``` -**You are SSHing into one box and want an ephemeral multiplexer:** -⚠️ Maybe not yet. phux assumes a persistent server-per-user; one-off SSH + detach-on-disconnect works but isn't a first-class use case. tmux remains the practical choice for "I just logged in and want to split panes." + -**You are an agent (Claude, Cursor, etc.) orchestrating terminals:** -✅ Yes, eventually. L1's wire protocol is stable and publishable today; the agent SDK is coming in v0.2. For now, use L1 directly — see [`CONTRIBUTING.md`](./CONTRIBUTING.md) §"Building on phux" for examples. Your code will work unchanged when the SDK ships. +## What actually works today -**You are a fleet orchestrator managing terminals across 10+ machines:** -✅ Yes, this is the 10-year vision. Federation (v0.2+) isn't here yet, but the addressing is ready from day 1. `SATELLITE { host, id }` TerminalIds already live on the wire. Start building on L1 now; your code will route end-to-end when satellites land. +Two surfaces ride the same source-of-truth libghostty `Terminal`: -**You want to script and automate everything:** -✅ Yes. phux refuses an embedded scripting language (see Non-goals), so you shell out to a real runtime. Typed L1 wire messages are far better than string-scraping terminal output. See [`docs/spec/TUTORIAL.md`](./docs/spec/TUTORIAL.md) for wire examples. +- **The TUI** — auto-attach/detach/re-attach, multi-pane splits, status bar, + keybindings, multi-client attach. Full modern-protocol passthrough (Kitty + keyboard, truecolor, OSC 8 hyperlinks, OSC 133, images) because the parser is + identical on both ends. +- **The headless / agent surface** — every command above is real and tested. + A selector grammar (`name`, `name:window.pane`, `@id`, `.` focused, `=` + last-focused) addresses any pane; reads are side-effect-free (no attach, no + resize) and take `--json`. Same surface is exposed over MCP by the `phux-mcp` + crate — six tools, JSON-RPC over stdio, no protocol privilege. **This is the + part you point an agent at.** -**You want tmux key bindings unchanged or a drop-in replacement:** -⚠️ We are not tmux. We will be better in some places and different in others. Read [`docs/consumers/tui.md`](./docs/consumers/tui.md) to learn the surface before committing. +Both are real binaries you can run now, not a roadmap. The honest line on what's +stable vs. still moving is in [Status](#status). -**Not sure which box you check?** Read [`docs/CONCEPTS.md`](./docs/CONCEPTS.md) — it explains the design philosophy and mental model. That will tell you if this is aligned with how you think about terminals. +## The idea -## Status +Two decisions do all the work. -**v0.1: L1 Terminal substrate, stable and shipped.** +**The same parser on both ends.** The server's libghostty `Terminal` is +canonical; the client's is a local mirror for rendering. Nothing in the middle +re-parses VT — so new terminal features light up on the next libghostty bump, on +both ends, for free. Older multiplexers re-parse mid-path and degrade fidelity. +phux structurally can't. -**L1 — Terminal control plane (stable, shipped today):** -- Single-session auto-attach, detach, re-attach -- Multi-pane splits with full terminal protocol support (Kitty keyboard, OSC 8, images) -- Status bar, keybindings, multi-client attach -- libghostty-backed wire protocol (VT bytes server→client, structured input client→server) +**The terminal is the unit, not the session.** Sessions, windows, panes, splits +— the whole tmux vocabulary — live in the TUI's metadata layer, never on the +wire. An agent speaks to *terminals* and never hears the word "window." That's +why a non-human consumer is a first-class citizen instead of a screen-scraping +hack bolted onto a tool built for one human at a keyboard. -**L2 & L3 — Collections and metadata (roadmapped, spec'd, wire hooks in place):** -- Named session lifecycle (CREATE_SESSION, KILL_COLLECTION) -- TUI layout persistence, window management, metadata storage -- Full `phux` CLI surface (new, ls, kill, rename, send-keys, run, snapshot, wait) +The wire is layered — L1 Terminal / L2 Collection / L3 Metadata — and consumers +declare which tiers they speak so the server omits the rest. Identity is +federation-ready from byte zero: `TerminalId` is `LOCAL{id} | SATELLITE{host,id}`. +v0.1 constructs `LOCAL`; the wire already accepts `SATELLITE`; v0.2 routes it; +the bytes never change. Full mental model: [`docs/CONCEPTS.md`](./docs/CONCEPTS.md). -**Federation and agent SDK (v0.2+, addressing ready from day 1):** -- Control plane routing across satellites (SATELLITE{host, id} TerminalIds already accepted) -- Agent SDK with structured L1 interface (wire hooks in place, implementation pending) -- Predictive local echo and lazy state sync - -The L1 substrate is the part worth building against now. For contributors, [`CONTRIBUTING.md`](./CONTRIBUTING.md) has the exact roadmap and constraints. +[lghv]: https://github.com/Uzaaft/libghostty-rs ## Install -Prebuilt binaries via Homebrew (macOS arm64/x86_64, Linux x86_64): +Homebrew (macOS, Linux x86_64): ```sh brew install phall1/phux/phux ``` -To hack on it instead, build from source — see Quickstart below. The binary is not on crates.io (it needs zig to build `libghostty-vt`); only the [`phux-protocol`](https://crates.io/crates/phux-protocol) wire crate publishes there. Release mechanics live in [`docs/RELEASING.md`](./docs/RELEASING.md). - -## Quickstart +From source — the toolchain is Nix-pinned (Rust 1.90 + Zig for libghostty's +build), so the dev shell is the supported path: ```sh -nix develop # or direnv allow once -just ci # the bar — fmt-check + lint + test + deny + doc -cargo run --bin phux # auto-spawns a server and attaches +nix develop # or: direnv allow +cargo run --bin phux # auto-spawns a server and attaches ``` -Detach with the default prefix (`Ctrl-A d`). Walk-through in [`docs/QUICKSTART.md`](./docs/QUICKSTART.md). +Detach with the default prefix, `Ctrl-A d`. Full walk-through: +[`docs/QUICKSTART.md`](./docs/QUICKSTART.md). + +## Status + +**Stable, shipped — L1 terminal control plane:** +- TUI: attach / detach / re-attach, multi-pane splits, status bar, keybindings, + multi-client attach +- libghostty wire protocol (VT bytes server→client, structured input + client→server), version-negotiated. `phux-protocol` is the only crate + published to crates.io. + +**Shipped and tested, API still moving (pre-1.0):** +- Headless verbs: `ls`, `snapshot`, `send-keys`, `run`, `wait`, `watch`, `new`, + `kill`, `rename`, `config` — selector grammar + `--json` +- `phux-mcp`: MCP adapter exposing six tools over JSON-RPC stdio +- L2 agent protocol layer (collections, terminal-state reads, event subscriptions) + +**Not yet — addressing is in the wire, routing is not:** +- Control-plane routing across satellites (`SATELLITE{host,id}` ids are accepted + today, not yet routed) +- Native GUI consumer over libghostty's surface API + +Building against it now? L1 is the part that won't move under you. Contributor +roadmap + constraints: [`CONTRIBUTING.md`](./CONTRIBUTING.md). ## Where to go next @@ -113,6 +125,7 @@ Detach with the default prefix (`Ctrl-A d`). Walk-through in [`docs/QUICKSTART.m | Understand the model | [`docs/CONCEPTS.md`](./docs/CONCEPTS.md) | | Run it | [`docs/QUICKSTART.md`](./docs/QUICKSTART.md) | | Customize config and keybindings | [`docs/CONFIG.md`](./docs/CONFIG.md) | +| Drive it from an agent | [`docs/consumers/agents.md`](./docs/consumers/agents.md) · [`docs/consumers/mcp.md`](./docs/consumers/mcp.md) | | Read the wire spec | [`docs/spec/`](./docs/spec/) | | Understand how it's built | [`docs/architecture/`](./docs/architecture/) | | Read the TUI surface | [`docs/consumers/tui.md`](./docs/consumers/tui.md) | @@ -120,33 +133,43 @@ Detach with the default prefix (`Ctrl-A d`). Walk-through in [`docs/QUICKSTART.m | See past decisions | [`ADR/README.md`](./ADR/README.md) | | Contribute | [`CONTRIBUTING.md`](./CONTRIBUTING.md) | -The doc system itself is defined in [`docs/CONVENTIONS.md`](./docs/CONVENTIONS.md) — frontmatter schema, TL;DR rule, ADR template, CI gates. +The doc system itself is defined in [`docs/CONVENTIONS.md`](./docs/CONVENTIONS.md) +— frontmatter schema, TL;DR rule, ADR template, CI gates. ## Crates | Crate | Purpose | |---|---| -| `phux` | Binary; `attach` and `server` subcommands today | +| `phux` | Binary; `attach`/`server` + the headless verbs | | `phux-protocol` | Wire types, codec, version negotiation. The only crate intended for publication | | `phux-core` | Domain types: in-process terminal / collection registries | | `phux-server` | Daemon: per-terminal actor, PTY supervision, output fanout | -| `phux-client` | TUI client: local libghostty Terminal + RenderState redraw + ratatui chrome | +| `phux-client-core` | Renderer + protocol client, ratatui-free (the boundary is compiler-enforced) | +| `phux-client` | TUI chrome (ratatui) over `phux-client-core` | | `phux-config` | TOML config schema + status widget contract | +| `phux-mcp` | MCP adapter: the agent CLI surface over JSON-RPC stdio | -Future, not yet started: `phux-client-sdk` (L1-only typed Rust handle for agents) and `phux-client-gui` (native GUI consumer over libghostty's surface API). +Future, not yet started: `phux-client-gui` (native GUI consumer over libghostty's +surface API). ## Non-goals Each of these is a "no" that keeps the substrate honest, not a feature deferred: -- **No embedded scripting language.** Commands are typed IPC messages. Logic that wants a runtime can shell out to one. -- **No plugin host.** Hooks are typed events. A plugin contract, if it ever lands, comes after we know what is genuinely pluggable. -- **No copy-mode reinvention.** Selection and extraction belong to libghostty and the host terminal. phux owns exactly one primitive libghostty doesn't provide: literal search over scrollback. +- **No embedded scripting language.** Commands are typed IPC messages. Logic that + wants a runtime can shell out to one. +- **No plugin host.** Hooks are typed events. A plugin contract, if it ever lands, + comes after we know what is genuinely pluggable. +- **No copy-mode reinvention.** Selection and extraction belong to libghostty and + the host terminal. phux owns exactly one primitive libghostty doesn't provide: + literal search over scrollback. - **No homegrown crypto.** SSH and Unix-socket permissions are the trust model. -- **No format-template DSL.** The status bar takes typed widgets, not a printf dialect to maintain forever. +- **No format-template DSL.** The status bar takes typed widgets, not a printf + dialect to maintain forever. Full rationale in [`CONTRIBUTING.md`](./CONTRIBUTING.md). ## License -Dual-licensed under [MIT](./LICENSE-MIT) or [Apache-2.0](./LICENSE-APACHE) at your option. +Dual-licensed under [MIT](./LICENSE-MIT) or [Apache-2.0](./LICENSE-APACHE) at your +option. diff --git a/docs/CONCEPTS.md b/docs/CONCEPTS.md index 99f295a..ee9f8d5 100644 --- a/docs/CONCEPTS.md +++ b/docs/CONCEPTS.md @@ -6,25 +6,25 @@ last-reviewed: 2026-05-28 # Concepts -**TL;DR.** phux is a libghostty-backed terminal control plane. The unit of work is the *terminal* — spawned, observed, controlled, persisted, addressable across hosts. Sessions, windows, and panes are one consumer's way to arrange terminals on screen. The wire is layered so that a consumer speaks only the tiers it needs and federation is in the addressing, not bolted on. +**TL;DR.** phux is a libghostty-backed terminal control plane. The terminal is the model: spawned, observed, controlled, persisted, addressable across hosts. Sessions, windows, panes are the TUI's way to arrange them. The wire is layered; federation is in the addressing, not bolted on. --- -## The unit of work is the terminal +## Terminals, not panes -A terminal is stateful: runs a process, parses bytes into a grid, accepts structured input, reports events (title, cwd, command lifecycle, hyperlinks, bells). +A terminal: runs a process, parses bytes into a grid, accepts structured input, reports events (title, cwd, command lifecycle, hyperlinks, bells). -Everything else — sessions, windows, panes, splits, status bars — is one way to arrange terminals on screen. Not load-bearing. +Sessions, windows, panes, splits — the entire tmux vocabulary — live in the TUI layer. Not on the wire. Not load-bearing for agents or control planes. -Why frame it this way? The consumer category widened: -- Humans want the tmux experience. -- Agents want to spawn, observe, and tear down terminals. -- CI fleets want stable event streams. -- Control planes want addressable resources. +Why? The consumer category expanded beyond humans: +- Humans want windows and splits (the TUI layer handles this) +- Agents want to spawn, observe, tear down terminals +- CI fleets want event streams +- Control planes want addressable resources across machines -The terminal is the greatest common factor. +The terminal is what everyone needs. Everything else is optional. -The reference TUI ships in-tree because the substrate is only real if a real consumer rides it. The substrate is the point. +The reference TUI ships in-tree because the substrate is only real if someone rides it. --- @@ -36,11 +36,11 @@ Modern terminal protocols (Kitty keyboard, true colour, OSC 8, OSC 133, images, See [ADR-0004 (libghostty-vt as the canonical grid)](../ADR/0004-libghostty-vt-as-grid.md), [ADR-0008 (use libghostty types directly)](../ADR/0008-use-libghostty-types-directly.md), and [ADR-0013 (libghostty bytes on the wire)](../ADR/0013-libghostty-bytes-on-wire.md). -### Practical consequences +### In practice -- **Asymmetric wire.** Server→client: VT bytes (capability-rewritten per client). Client→server: structured events (key, mouse, focus, paste) from libghostty atoms. -- **Input types are libghostty's.** Direct re-export: `libghostty_vt::key::{Key, Action, Mods}`, `libghostty_vt::mouse::{Action, Button}`. No parallel phux enum. -- **New libghostty features auto-light.** New escape sequences or input atoms land in both client and server at the next pin. No phux bridge needed. +- **Asymmetric wire.** Server→client: VT bytes. Client→server: structured events (key, mouse, focus, paste). +- **Input types are libghostty's.** Direct re-export. No parallel phux enum. +- **New libghostty features auto-light.** Next pin bump, both client and server get them. --- @@ -58,49 +58,29 @@ Each layer references only lower layers. Consumers declare which tiers they spea Example: an agent SDK speaks L1 alone. It never sees "session" or "window" (TUI L3 metadata). A GUI might speak L1 + its own L3 and ignore the TUI's. No consumer is privileged on the wire. -### Wire vs consumer boundary - -| Concept | Owner | -|---|---| -| Terminal — PTY, bytes, input, events | L1 (wire) | -| Collection — named bundle, lifecycle | L2 (wire) | -| Metadata blob — opaque KV | L3 (wire) | -| TUI layout tree | TUI consumer; L3 metadata under `phux.tui.layout/v1` | -| TUI window ordering, focus | TUI consumer; L3 metadata | -| Status bar, keybindings, hooks | TUI consumer; local config | -| Predictive local echo | Client-side, transport-agnostic | - -Test: does *every* plausible consumer need it? If only the TUI needs it, it's TUI local config or L3 metadata, not a wire message. - --- ## Identity is federation-ready -Every terminal is addressed by a `TerminalId` tagged union: +Every terminal: ``` TerminalId = LOCAL { id: u32 } | SATELLITE { host: String, id: u32 } ``` -v0.1 constructs `LOCAL` only. The wire accepts both from byte zero. A client can write `SATELLITE{host: "prod-box-3", id: 42}` today. v0.1 rejects it with `UnsupportedSatelliteRoute` (not a crash). When v0.2 lands with satellite routing, the wire bytes are identical. The same command that got rejected in v0.1 just works in v0.2. Forward compatibility, in place before satellites exist. +v0.1 constructs `LOCAL` only. The wire accepts both from byte zero. Write `SATELLITE{host: "prod-box-3", id: 42}` today; v0.1 rejects it gracefully; v0.2 routes it. Same command, same wire bytes. -**Unlike tmux:** tmux SSH-attach is client-side (connect to remote, spawn local client). A fleet control plane managing terminals across machines must speak different protocols to each server. phux's wire knows about remote identity from day 1. A single control plane addresses terminals uniformly across all machines; addressing is in L1, the terminal substrate. +This is intentional forward compatibility. phux is a control plane from the first byte, not a single-machine tool with remote attach bolted on later. -phux is a control plane from the first byte, not a single-machine tool with remote attach bolted on. The load-bearing case: a fleet of agents working across cloud boxes with terminals as addressable, persistent, observable resources. - -See [ADR-0007 (Mosh-class transport and satellites)](../ADR/0007-mosh-class-transport-and-satellites.md) and [ADR-0016 (TerminalId as wire primary)](../ADR/0016-terminal-id-as-wire-primary.md). Transport (SSH, QUIC, etc.) is pluggable via `Transport` trait; see [`architecture/transport.md`](./architecture/transport.md). +See [ADR-0007](../ADR/0007-mosh-class-transport-and-satellites.md) and [ADR-0016](../ADR/0016-terminal-id-as-wire-primary.md). --- ## Comparison to other tools -phux is not competing with every terminal tool. Different tools solve different problems. Here's where phux stands. - ### Traditional multiplexers -These are human-first session managers: tmux, zellij, screen. They arrange terminals on screen with windows, panes, and splits. phux aims to be in this category but rethought from libghostty up. - | Dimension | phux | tmux | zellij | screen | |---|---|---|---|---| | Modern terminal protocol support[^1] | ✓ | ◐ | ◐ | ✗ | @@ -114,8 +94,6 @@ These are human-first session managers: tmux, zellij, screen. They arrange termi ### Agent-focused tools -These solve agent automation and control, not primary human use. They're in a different category than traditional multiplexers. - | Dimension | phux | zmx | cmux | rmux | |---|---|---|---|---| | Agent SDK or programmatic API | ✓ (planned L1 SDK) | ✗ | ✓ (macOS native) | ✓ (Rust async) | @@ -124,13 +102,13 @@ These solve agent automation and control, not primary human use. They're in a di | Maturity | Pre-release v0.1 | Minimal scope, SSH bugs | Production (20k+ stars) | Public preview (v0.3.1) | | Wire protocol published | ✓ (phux-proto in docs/spec/) | ✗ | Proprietary | ✓ (rmux-proto crate) | -**What's going on here:** These are not multiplexer competitors. +**The difference:** These are not multiplexer competitors. -- **zmx** is deliberately minimal: session persistence only, delegates window/split logic to the OS. Known terminal state bugs in nested SSH. No agent SDK. -- **cmux** is Swift + AppKit, macOS-only, focused on agent coordination (Claude, Cursor). Shows git branches, PRs, agent notifications. Not a tmux replacement. -- **rmux** is a fresh public preview agent SDK (Rust, Playwright-style async). All 90 tmux commands available. No federation. +- **zmx**: minimal (session persistence only), SSH support, no agent SDK +- **cmux**: macOS agent UI (Claude, Cursor), not a replacement +- **rmux**: agent SDK (Rust async), no federation -phux is *both* a human multiplexer *and* a federation-ready control plane. The agent SDK is one consumer of the same wire that the TUI rides. See [ADR-0017 (TUI not protocol-privileged)](../ADR/0017-tui-not-protocol-privileged.md). +phux is both a human multiplexer and a federation-ready control plane. See [ADR-0017](../ADR/0017-tui-not-protocol-privileged.md). [^1]: Kitty keyboard, true colour, OSC 8, OSC 133, images, pixel-precision mouse. phux passes through unchanged (libghostty parses once, bytes forward). Others re-parse mid-path and degrade fidelity. [^2]: phux wire knows remote identity from day 1. Design in [ADR-0016 (TerminalId as wire primary)](../ADR/0016-terminal-id-as-wire-primary.md) and [ADR-0007 (Mosh-class transport and satellites)](../ADR/0007-mosh-class-transport-and-satellites.md). Traditional multiplexers treat remote attach as client-side (SSH + local connect).