# Architecture
## Overview
xtui is a lib + binary crate. `lib.rs` re-exports all modules; `main.rs` is the entry point.
```
CLI args -> find_workspace_root -> all_sources() -> App::new -> App::run (event loop)
```
## Modules
| `main.rs` | CLI entry, workspace root discovery (walks up for Cargo.lock) |
| `app.rs` | App state, event loop, keybindings, args-input mode |
| `ui.rs` | ratatui rendering — pure, no state mutation |
| `source.rs` | `CommandSource` trait + 8 source implementations |
| `discover.rs` | xtask `main.rs` parser (regex, match arm extraction) |
| `bin_schema.rs` | Cargo-bin subcommand cache (JSON, mtime-invalidated) |
| `runner.rs` | Child process spawning, stdout/stderr streaming via mpsc |
| `pipeline.rs` | Sequential command chaining state machine |
| `search.rs` | Output search with match cycling |
| `history.rs` | JSON history + `.log` files in `~/.config/xtui/` |
| `status.rs` | Git status via `Command::new("git")` |
| `depview.rs` | Dep graph domain types; `collect_direct_deps` via krates |
| `meta_cache.rs` | `MetadataCache` port + `RedbCache` adapter (redb, TTL-based) |
| `meta_fetch.rs` | `MetadataFetcher` port + `HttpMetadataFetcher` adapter (ureq) |
| `registry.rs` | Project scanner/cache (not wired into UI — reserved) |
Each module owns exactly one concern. No cross-module state mutation.
## Command Sources
`CommandSource` is a port trait in `source.rs`:
```rust
pub trait CommandSource: Send + Sync {
fn name(&self) -> &str;
fn discover(&self, project: &Path) -> Result<Vec<SourceCommand>>;
}
```
`discover()` returns an empty vec when the source is not applicable — it never errors for
"not found". `all_sources()` returns implementations in tab order:
| 0 | xtask | `xtask/src/main.rs` | `cargo run -p xtask -- <cmd>` |
| 1 | cargo | `Cargo.toml` | `cargo <cmd>` |
| 2 | just | `Justfile` / `justfile` | `just <recipe>` |
| 3 | nu | `scripts/*.nu` | `nu scripts/<name>.nu` |
| 4 | npm | `package.json` scripts | `npm run <script>` |
| 5 | make | `Makefile` | `make <target>` |
| 6 | mise | `mise.toml` / `.mise.toml` | `mise run <task>` |
| 7 | cargo-bin | `~/.cargo/bin/` | `<binary> [subcmd]` |
Empty tabs are hidden in the UI.
## Cargo-Bin Schema Cache
`bin_schema.rs` provides lazy per-binary subcommand discovery with mtime-invalidated JSON
cache at `~/.config/xtui/bin-schema/<binary>.json`.
```
CargoBinSource::discover()
└── get_schema(dir, binary_name)
├── load_cached() reads JSON, checks mtime + schema version
│ └── stale → cache miss
└── probe_and_cache()
├── blocklist check (daemons, GUIs, profilers — 13 entries)
├── spawn thread: <binary> --help, recv_timeout(500ms)
├── parse_help_subcommands() finds Commands:/Subcommands: section
└── save_schema() writes JSON cache
```
Emits `"<bin> <subcmd>"` names when subcommands are found; bare binary name otherwise.
The runner's `cmd.name.split_whitespace()` dispatch handles both cases transparently.
## Process Runner
`runner.rs` spawns a child process with piped stdout/stderr. Two tokio tasks read each
stream via `LinesCodec` into a shared mpsc channel. `RunningTask` exposes `poll_lines()`,
`try_exit_code()`, and `cancel()`. The runner dispatches on `cmd.source` to pick the
right program and args.
## Pipeline State Machine
`pipeline.rs` is a pure state machine for sequential command chaining:
```
Idle -> Running(idx) -> Done
-> Failed(idx)
```
The caller handles actual execution — pipeline only tracks state.
## UI Layout
```
+------------------------------------------+
| | |
| Commands | Output (ANSI, streaming) |
| (~30%) | (~70%) |
| | |
+-------------+----------------------------+
| workspace · state · N commands | <- status bar / flash messages
+------------------------------------------+
```
Output pane auto-scrolls to bottom; focusable for manual scroll. Flash messages replace
the status bar for 2 seconds. Args-input mode overlays a modal prompt before run.
## Key Bindings
| `Tab`/`Shift+Tab` | Commands focus | Cycle source tabs |
| `1`-`9` | Commands focus | Jump to tab by index |
| `j` / `Down` | Commands focus | Next command |
| `k` / `Up` | Commands focus | Previous command |
| `Enter` | Commands focus | Run selected command |
| `a` | Commands focus | Open args-input mode |
| `o` | Any | Focus output pane |
| `j` / `Down` | Output focus | Scroll down |
| `k` / `Up` | Output focus | Scroll up |
| `g` | Output focus | Scroll to top |
| `G` | Output focus | Scroll to bottom |
| `n` / `N` | Output focus | Next / previous search match |
| `Tab` / `Enter` | Output focus | Return to Commands focus |
| `/` | Any | Start output search |
| `s` | Any | Toggle git status tab |
| `D` | Any | Toggle dependency graph view |
| `r` | Any | Refresh commands |
| `c` | Any | Copy output to clipboard (OSC52) |
| `P` | Any | Run all tab commands as pipeline |
| `Esc` | Any | Cancel task / close search / exit output focus |
| `Ctrl+C` | Any | Cancel task or quit |
| `q` | Any | Quit |
## Key Design Decisions
- **OSC52 clipboard** — avoids a platform clipboard dependency; works in any terminal
that supports OSC52. Custom base64 encoder avoids adding a dep for one function.
- **`anyhow::Result` throughout** — no custom error types. Consider `thiserror` only if
error variants need matching at call sites.
- **Buffer caps** — 10k output lines (drains oldest 1k on overflow), 1024-line channel
buffer, 50ms poll interval.
- **History caps** — 50 entries per project, 100 log files per project.