Source of truth for all contributors and AI agents working on this project.
An AI agent given these instructions should be able to go from zero to a running Horizon binary.
Repository: https://github.com/peters/horizon.git
Pre-built binaries are attached to GitHub releases. Prefer the latest non-prerelease tag. Use gh release download --repo peters/horizon or download from https://github.com/peters/horizon/releases/latest.
| Platform | Asset | Contents |
|---|---|---|
| Linux x64 | horizon-linux-x64.tar.gz |
Single horizon binary — extract and make executable |
| macOS x64 | horizon-osx-x64.tar.gz |
Single horizon binary — extract and make executable |
| Windows x64 | horizon-windows-x64.exe |
Ready-to-run executable |
No Rust toolchain or system headers are needed for this path.
- Rust stable ≥ 1.85 (edition 2024). Install via rustup if not present.
- Linux only: the eframe/wgpu rendering stack needs system headers. Install them before
cargo build:- Debian/Ubuntu:
sudo apt install -y build-essential pkg-config libxkbcommon-dev libwayland-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libvulkan-dev libgl-dev cmake - Fedora:
sudo dnf install -y gcc pkg-config wayland-devel libxkbcommon-devel vulkan-loader-devel mesa-libGL-devel cmake - Arch:
sudo pacman -S --needed base-devel wayland libxkbcommon vulkan-icd-loader cmake
- Debian/Ubuntu:
- macOS: Xcode Command Line Tools (
xcode-select --install). Metal ships with the OS. - Windows: MSVC build tools (installed automatically by
rustupon themsvctarget). DX12/Vulkan drivers ship with the GPU driver.
git clone https://github.com/peters/horizon.git
cd horizon
cargo run --releasecargo fmt --all -- --check
cargo test --workspace
cargo clippy --all-targets --all-features -- -D warningsIf any step fails, read the error, fix the prerequisite, and retry. On Linux, missing system headers are the most common issue — look for pkg-config or linker errors and install the corresponding -dev package.
Horizon is a GPU-accelerated terminal board — a visual workspace for managing multiple terminal sessions as freely positioned, resizable panels on a canvas.
Stack: Rust (edition 2024) · eframe/egui (wgpu backend) · vt100 · portable-pty
crates/
horizon-core/ Core: terminal emulation, PTY, board & panel management
horizon-ui/ Binary: eframe application, UI rendering, input handling
error.rs— Typed error enum via thiserrorterminal.rs— vt100 parser wrapper (screen buffer, resize)panel.rs— Panel = terminal + PTY session + identityboard.rs— Board = collection of panels + focus management
main.rs— Entry point, tracing init, eframe launchapp/—eframe::Apporchestration split by canvas, panels, sidebar, settings, session, persistenceterminal_widget/— Terminal widget split by layout, input, render, scrollbar logicinput/— Keyboard translation, mouse reporting, escape-sequence buildingtheme.rs— Color palette (Catppuccin Mocha), styling constants
cargo fmt --all -- --check
./scripts/check-maintainability.sh
RUSTFLAGS="-D warnings" cargo test --workspace
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --workspace --lib --bins -- -D warnings -D clippy::unwrap_used -D clippy::expect_used
cargo clippy --workspace --all-targets --all-features -- -D warnings -W clippy::pedantic- When changing default presets, CLI flags, or any config-related code in
horizon-core/src/config.rs, always sync the user's local config file (~/.horizon/config.yaml) to match
- Self-documenting code preferred over comments; add comments only for invariants, non-obvious tradeoffs, or safety contracts
- Use idiomatic Rust naming:
snake_case(functions/modules),CamelCase(types),SCREAMING_SNAKE_CASE(consts) - Typed error enums (thiserror) — no
Box<dyn Error>or.unwrap()in library code - Keep APIs explicit: prefer
Result<T, E>and typed structs/enums over ad-hoc tuples - Prefer
tracingfor structured logging #![forbid(unsafe_code)]on all crates- Consolidate repeated helpers into shared modules in horizon-core
- Keep new or edited modules single-purpose; avoid mixing rendering, persistence, session bootstrap, and filesystem logic in one file
- If UI code needs shared layout math, state conversion, or template-sync logic, move it into
horizon-coreinstead of duplicating it inhorizon-ui - Treat roughly 600 lines as the point to split a Rust source file; the CI guardrail fails non-test files above 1000 lines under
crates/horizon-core/srcandcrates/horizon-ui/src - Do not use
#[allow(clippy::too_many_lines)]in core or UI source files; decompose the code instead - Keep inline
#[cfg(test)]modules at the end of the file so maintainability checks can measure production code cleanly - Minimize allocations in the render hot path (per-frame code)
- Every
unsafeblock (if ever needed) must have a// SAFETY:rationale - Avoid unnecessary
crate::path prefixes in module-local code/tests when imports already provide the item - Prefer
unwrap_or_else(std::sync::PoisonError::into_inner)over manualmatchfor poisoned mutex recovery expect_usedis treated the same asunwrap_used: both can hide panic paths in runtime code- Fix clippy/rustc warnings instead of suppressing them
- Prefer small module trees over large flat files:
mod.rsshould orchestrate, leaf modules should do one job - UI modules render or collect UI actions; domain state mutation belongs in
horizon-coreunless it is purely presentational state - When editing a file that is already large, split it as part of the change instead of adding another responsibility
- Keep architecture notes current in
docs/architecture/maintainability.mdwhen module boundaries or guardrails change
| Tier | Command | Status |
|---|---|---|
| Blocking | cargo clippy --all-targets --all-features -- -D warnings |
Must pass |
| Strict | cargo clippy --workspace --lib --bins --examples -- -D warnings -D clippy::unwrap_used -D clippy::expect_used |
Must pass |
| Pedantic | cargo clippy ... -W clippy::pedantic |
Advisory (will promote) |
- Concise imperative messages, optionally scoped:
feat(board):,fix(render):,ci: - One logical change per commit
- PRs include: purpose, behavior impact, test evidence
- Always fix pre-existing clippy warnings in touched files before committing; a commit must leave the blocking and strict CI tiers green
- The authoritative version lives in
GitVersion.yml(next-version);[workspace.package].versioninCargo.tomland the internalhorizon-coredependency version incrates/horizon-ui/Cargo.tomlmust always match - CI validates this with
scripts/check-version-sync.sh— update all three files together when bumping the version
Alpha → Beta → Stable, managed via CI and the Release workflow dispatch.
Every push to main publishes an alpha prerelease to crates.io and GitHub Releases (e.g. 0.1.0-alpha.3). No manual steps required.
When main is ready for stabilization:
- Go to Actions → Release → Run workflow
- Select branch:
main - Choose action: promote-beta
This creates a release/X.Y.Z branch from main. Subsequent pushes to that branch auto-publish beta prereleases (e.g. 0.1.0-beta.1). Cherry-pick bug fixes to the release branch for additional betas. Meanwhile, main continues the next alpha cycle (0.2.0-alpha.1).
When the release branch is ready:
- Go to Actions → Release → Run workflow
- Select branch:
release/X.Y.Z - Choose action: cut-stable
This tags vX.Y.Z on the release branch. CI publishes the stable version to crates.io and creates a draft GitHub release for you to review and publish.
# promote to beta
git checkout main && git pull
git checkout -b release/0.1.0
git push origin release/0.1.0
# cut stable
git checkout release/0.1.0 && git pull
git tag v0.1.0
git push origin v0.1.0- Always check crates.io for the latest stable version before adding
- Prefer workspace-level dependencies (root
Cargo.toml) - New dependencies require justification
- Unit tests close to code (
#[cfg(test)]) - Integration tests under
crates/*/tests/ - Test panel creation, PTY lifecycle, resize, input routing
- For UI/layout changes, verify with a live screenshot after launch and after resize/fit interactions; build success alone is not sufficient
- Prefer repeatable workloads over ad-hoc observation: profile idle, panning, mouse-move, resize, and scroll as separate cases instead of treating "high CPU" as one bucket
- Start with
cargo build --profile profiling --features trace-profiling, then useHORIZON_TRACE_SPANS=1 RUST_LOG=info target/profiling/horizonorscripts/profile.sh <seconds>; the script already falls back to traced spans whenperfis blocked - Keep before/after comparisons workload-matched: same board layout, same interaction script, same binary profile, same tracing mode
- When profiling pointer-driven regressions, automate the interaction with
xdotoolso hover paths can be compared consistently instead of relying on manual movement - For UI perf changes, capture a live screenshot after the profiled interaction as well as at launch; some "wins" are just incorrect culling
crates/horizon-ui/src/app/panels.rs— panel composition, titlebar chrome, history meter, hover work, and context-menu setupcrates/horizon-ui/src/terminal_widget/render.rs— per-cell iteration, text batching, default-color conversion, decorations, and redundant fillscrates/horizon-ui/src/terminal_widget/input.rs— per-panel event cloning/scanning and pointer-move fan-outcrates/horizon-ui/src/app/workspace.rsandcrates/horizon-ui/src/app/canvas.rs— workspace hover effects, cursor changes, and canvas redraws while moving the mousecrates/horizon-ui/src/app/canvas.rsand the minimap path — repeated static geometry such as dot grids, glows, and overview rects can become tessellation-heavy under pointer-only redraws- Mouse-move spikes are often hover/redraw problems, not PTY/output problems; compare against an idle trace before touching terminal I/O
- Moving the pointer anywhere inside the Horizon window can trigger full-scene redraws; "empty canvas" is not a free case if visible panels still repaint
- Safe optimizations usually reduce repeated work on the common path: batch default terminal text, fast-path default colors, gate per-panel pointer processing behind actual interaction, and remove redundant paint passes
- Off-screen culling is generally safe; active-workspace culling is not. Do not skip panel body rendering just because a panel belongs to a non-active workspace if it can still be visible on screen
- If a perf change affects visibility, focus, hover, or selection behavior, treat it as correctness-sensitive and verify it live before keeping it
- Record the exact workload used for any reported perf gain in the commit message or PR notes so later agents can reproduce it
- Any new UI feature must identify its redraw surface up front: what pointer movement, hover state, animation, terminal output, or config changes will cause it to update
- Treat pointer-only frames as a first-class perf budget; if a feature adds hover behavior, compare idle vs scripted mouse-move before and after, including a pass over empty canvas outside any workspace
- Do not add unconditional per-frame work across every panel or workspace for convenience. Compute lazily on interaction, gate by on-screen visibility, or cache by stable keys
- Prefer cached meshes or cached shapes for repeated static decoration such as grids, minimaps, badges, panel chrome, and other geometry that does not semantically change every frame
- Avoid broad
request_repaintor animation loops for passive UI polish. Repaint continuously only when there is active interaction, active animation, or real terminal/output change - New menus, tooltips, badges, and summary labels should avoid eager text layout, string building, or list construction for every visible panel each frame
- If a feature needs per-panel pointer inspection, keep the hot path narrow: no cloning/scanning full input state for inactive panels unless the pointer is actually relevant to that panel
- When a feature adds a new always-visible overlay, include the overlay in the perf trace and live screenshot verification; overlays can dominate pointer redraw cost even when the cursor is elsewhere
- If a feature cannot be made lazy or cache-friendly, document why in the commit message or PR notes and include the measured cost on the target workload
- If Horizon "doesn't launch", first distinguish a crash from an unmapped window:
ps -C horizonthenxwininfo -root -tree | rg Horizon - When
xwininfo -id <window-id> -statsreportsMap State: IsUnMapped, the process created a root window but the desktop never surfaced it; inspect first-frame UI/input code before blaming PTY startup - When the map state is
IsViewable, treat it as a focus, placement, or window-manager issue instead of a launch failure
- Main thread: egui event loop + rendering
- Per-panel reader thread: reads PTY output, sends via
mpsc::channel - Input: main thread writes directly to PTY stdin
Shell → PTY slave → PTY master reader → [thread] → channel → main thread → vt100 → egui
Keyboard → main thread → PTY master writer → PTY slave → Shell
Board::create_panel()opens a PTY, spawns$SHELL- Reader thread continuously sends output chunks to main thread
- Each frame: drain channel → feed vt100 parser → render grid
- On resize: recalculate rows/cols → resize vt100 + PTY
- On close: drop Panel (PTY handles cleaned up automatically)