Use when working on terminal emulation, the TerminalBackend trait, libghostty integration, or PTY management
Invoke this skill when:
GhosttyBackend in crates/hom-terminal/src/ghostty.rsVt100Backend in crates/hom-terminal/src/fallback_vt100.rsTerminalBackend trait in crates/hom-core/src/traits.rscrates/hom-pty/src/manager.rscrates/hom-pty/src/async_reader.rsghostty-backend feature flagThis is the hardest part of HOM. Each pane runs a REAL terminal emulator:
Harness Process (e.g., claude)
↓ writes ANSI escape sequences to stdout
PTY slave → PTY master
↓ raw bytes
AsyncPtyReader (tokio channel)
↓ Vec<u8> chunks
terminal.process(bytes) ← TerminalBackend implementation
↓ updates internal screen state
terminal.screen_snapshot() → ScreenSnapshot { rows: Vec<Vec<Cell>> }
↓ cell-by-cell mapping
pane_render.rs → ratatui Buffer
↓
ratatui renders to real terminal
Every byte from the harness flows through this pipeline. A bug anywhere means garbled output.
Defined in crates/hom-core/src/traits.rs. Any terminal emulator must implement:
| Method | What it does | Failure mode |
|---|---|---|
new(cols, rows, scrollback) | Create terminal state | — |
process(bytes) | Feed PTY output into VT state machine | Garbled screen |
resize(cols, rows) | Update terminal dimensions | Content clipped or wrapped wrong |
screen_snapshot() | Return all cells with colors and attributes | Wrong rendering |
cursor() | Return cursor position and visibility | Cursor in wrong place |
title() | Return window title set by child process | Cosmetic only |
ghostty-backend, DEFAULT)Current status: Fully implemented in ghostty.rs. This is the default backend.
To build (default):
cargo build # ghostty-backend is the default feature
Requires Zig ≥0.15.x at build time. Install Zig: brew install zig or download from ziglang.org.
Critical risk: libghostty-rs is v0.1.1, pre-1.0. Pin commits. The API WILL change.
vt100-backend)The opt-in fallback in fallback_vt100.rs. Uses the vt100 crate (v0.16).
To build with vt100 fallback (no Zig required):
cargo build --no-default-features --features vt100-backend
Known limitations:
title() always returns None (vt100 0.16 doesn't expose it)screen_mut().set_size() (not parser.set_size())These limitations are acceptable for the current implementation. Most harness TUIs use basic text, colors, and cursor positioning.
crates/hom-terminal/src/color_map.rs maps TermColor → ratatui::style::Color.
When adding a new backend, you must map its color representation to TermColor. The 16 named colors (Black through BrightWhite) plus Indexed(0-255) and Rgb(r,g,b) must all be handled.
crates/hom-pty/src/manager.rs uses portable-pty to:
Critical timing issue: After spawning, the harness needs time to initialize before accepting input. Don't send prompts immediately — wait for detect_completion() == WaitingForInput.
#[test]
fn test_process_simple_text() {
let mut term = Vt100Backend::new(80, 24, 0).unwrap();
term.process(b"Hello, World!");
let snap = term.screen_snapshot();
let first_row: String = snap.rows[0].iter().map(|c| c.character).collect();
assert!(first_row.starts_with("Hello, World!"));
}
#[test]
fn test_process_color_escape() {
let mut term = Vt100Backend::new(80, 24, 0).unwrap();
term.process(b"\x1b[31mRed text\x1b[0m");
let snap = term.screen_snapshot();
assert!(matches!(snap.rows[0][0].fg, TermColor::Red));
}
#[test]
fn test_cursor_movement() {
let mut term = Vt100Backend::new(80, 24, 0).unwrap();
term.process(b"Line 1\nLine 2");
let cursor = term.cursor();
assert_eq!(cursor.row, 1);
}
#[test]
fn test_resize() {
let mut term = Vt100Backend::new(80, 24, 0).unwrap();
term.resize(40, 12);
let snap = term.screen_snapshot();
assert_eq!(snap.cols, 40);
assert_eq!(snap.num_rows, 12);
}
#[cfg(feature = "ghostty-backend")] must never reference vt100 and vice versa.AsyncPtyReader. Blocking the tokio runtime freezes the entire TUI.cargo check passes with default features (ghostty)cargo check --no-default-features --features vt100-backend passes (vt100 fallback path)cargo test -p hom-terminalcargo test -p hom-ptyTermColor variants