# photon-ui — Agent Reference
## Project overview
`photon-ui` is a blazing fast, minimal terminal UI framework for Rust, built on top of [`crossterm`](https://github.com/crossterm-rs/crossterm). It provides a component-based architecture with a custom differential renderer, ANSI-aware text wrapping, OSC 8 hyperlink support, terminal image rendering (Kitty and iTerm2 protocols), and a constraint-based layout engine.
- **Language:** Rust (Edition 2024)
- **Version:** 0.1.0
- **License:** MIT
- **Author:** Sienna Meridian Satterwhite <sienna@sunbeam.pt>
## Technology stack
### Core dependencies
| `crossterm` | Cross-platform terminal I/O (raw mode, events, cursor control) |
| `unicode-width` | Correct display-width calculation for Unicode characters |
| `unicode-segmentation` | Grapheme cluster boundary detection |
| `pulldown-cmark` | CommonMark markdown parsing |
| `base64` | Image encoding for terminal graphics protocols |
| `thiserror` | Derive macro for error types |
| `kasuari` | Cassowary constraint solver for the layout engine |
### Dev dependencies
| `insta` | Snapshot testing |
| `proptest` | Property-based testing |
| `criterion` | Benchmark harness (with HTML reports) |
## Build and test commands
```bash
# Build the library
cargo build
# Run all tests (unit + integration)
cargo test
# Run the interactive demo
cargo run --example demo
# Run benchmarks
cargo bench
# Format code (uses rustfmt.toml configuration)
cargo fmt
# Check formatting without modifying files
cargo fmt -- --check
```
### Benchmarks
Four Criterion benchmarks live in `benches/`:
- `wrap_text` — ANSI-aware text wrapping performance
- `render_components` — Component render throughput
- `frame_loop` — Full frame loop performance
- `event_handling` — Event dispatch overhead
## Code organization
```
src/
├── lib.rs # Crate root: Component & Focusable traits, public exports
├── components/ # All UI components (26 modules)
│ ├── mod.rs # Re-exports every component
│ ├── text.rs
│ ├── input.rs # Single-line input (Emacs + vim modes)
│ ├── editor.rs # Multi-line editor (Emacs + vim modes)
│ ├── select_list.rs
│ ├── settings_list.rs
│ ├── loader.rs
│ ├── markdown.rs
│ ├── image_widget.rs
│ ├── div.rs # Layout container
│ ├── table.rs
│ ├── tree_view.rs
│ └── ... (see components/mod.rs for full list)
├── layout/ # Constraint-based layout engine
│ ├── layout.rs # Cassowary solver integration (kasuari)
│ ├── constraint.rs # Length, Min, Max, Percentage, Ratio, Fill
│ ├── rect.rs # Terminal rectangle primitives
│ └── ...
├── theme/ # Beam Design Language theme system
│ ├── palette.rs # Light / Dark themes
│ ├── color.rs
│ ├── style.rs
│ └── ansi.rs
├── terminal.rs # Terminal trait + TestTerminal + ProcessTerminal
├── renderer.rs # Differential renderer (FirstRender / FullRedraw / Diff)
├── tui.rs # TUI runtime: focus management, overlays, modals, layout
├── events.rs # Event abstraction over crossterm
├── keybindings.rs # KeybindingsManager with default action maps
├── utils.rs # ANSI-aware text wrapping, truncation, width measurement
├── image.rs # Kitty / iTerm2 image protocol encoding + dimension parsing
├── autocomplete.rs # Fuzzy autocomplete engine
├── fuzzy.rs # Fuzzy matching logic
├── kill_ring.rs # Kill-ring (clipboard) for editors
├── undo_stack.rs # Undo / redo stacks
└── word_navigation.rs # Word-boundary navigation helpers
tests/ # Integration and component-specific tests (42+ files)
benches/ # Criterion benchmarks
examples/ # Interactive demos
├── demo.rs # Full 5-page component showcase
```
## Architecture at a glance
### Component trait
Every visible element implements `Component`:
```rust
pub trait Component {
fn render(&self, width: u16) -> Result<Rendered, RenderError>;
fn render_rect(&self, rect: Rect) -> Result<Rendered, RenderError>;
fn handle_input(&mut self, event: &Event) -> InputResult;
fn wants_key_release(&self) -> bool;
fn as_focusable(&self) -> Option<&dyn Focusable>;
fn as_focusable_mut(&mut self) -> Option<&mut dyn Focusable>;
}
```
- `render` produces lines of text, an optional cursor position, and image commands.
- `handle_input` receives events when the component has focus (or during fallthrough).
- Focusable components also implement `Focusable` to participate in tab-order managed by `TUI`.
### Renderer
`Renderer` tracks the previous frame and automatically selects one of three strategies:
- **FirstRender** — outputs all lines (assumes clean alternate screen).
- **FullRedraw** — clears screen + scrollback, then redraws everything (used on resize).
- **Diff** — computes first/last changed line and only rewrites the changed region.
The diff strategy emits `\x1b[?2026h`/`\x1b[?2026l` (synchronized updates) and `\x1b[2K` (clear line) to minimize terminal output.
### TUI runtime
`TUI` owns the terminal backend, a stack of components, overlays, and an optional modal. It handles:
- Focus cycling via `Tab` / `Shift+Tab`
- Layout splitting when a `Layout` is set
- Overlay positioning with `Anchor` and `OverlayConstraints`
- Modal capture (Esc dismisses, focus restores on close)
- Stale image cleanup for Kitty graphics protocol
### Layout engine
The layout system uses the `kasuari` Cassowary constraint solver. A `Layout` splits a `Rect` into sub-rects based on `Constraint` values (`Length`, `Min`, `Max`, `Percentage`, `Ratio`, `Fill`) with configurable `Direction`, `Flex`, `Margin`, and `Spacing`.
## Code style guidelines
The project enforces an **extremely strict** linting policy in `src/lib.rs`:
```rust
#![deny(dead_code)]
#![deny(unused)]
#![deny(unused_mut)]
#![deny(clippy::missing_safety_doc)]
#![deny(clippy::undocumented_unsafe_blocks)]
#![cfg_attr(not(test), deny(clippy::expect_used))]
#![cfg_attr(not(test), deny(clippy::unwrap_used))]
#![deny(clippy::question_mark_used)]
#![deny(clippy::needless_borrow)]
#![allow(bindings_with_variant_name)]
```
### Key rules for contributors
- **No `unwrap` or `expect` outside tests.** Every fallible operation must be propagated or handled explicitly.
- **No `?` operator.** The project explicitly denies `clippy::question_mark_used`; use explicit `match` or `if let` instead.
- **No dead code or unused imports.** The compiler will reject them.
- **No undocumented `unsafe` blocks.** (The codebase currently contains no `unsafe`.)
- **Match arm leading pipes are required.** `match_arm_leading_pipes = "Always"` in `rustfmt.toml`.
- **Import grouping:** `group_imports = "StdExternalCrate"` in `rustfmt.toml`.
### rustfmt configuration
The repository includes a `rustfmt.toml` with specific preferences (e.g., `brace_style = "PreferSameLine"`, `imports_layout = "Vertical"`, `wrap_comments = true`). Run `cargo fmt` before committing.
## Testing instructions
### Where tests live
- **Unit tests:** Embedded in source files under `#[cfg(test)]` modules.
- **Integration / component tests:** In the `tests/` directory, one file per module (e.g., `input_tests.rs`, `renderer_tests.rs`).
- **Property tests:** `tests/layout_engine_proptests.rs` and `tests/layout_type_integration_tests.rs` use `proptest`.
- **Snapshot tests:** Some test files use `insta` for snapshot assertions.
### Testing with `TestTerminal`
`TestTerminal` is the primary test double. It records all writes, cursor moves, and cursor visibility changes so tests can assert on exact ANSI sequences:
```rust
use photon_ui::terminal::TestTerminal;
let mut term = TestTerminal::new(80, 24);
term.write("hello").unwrap();
assert!(term.written().contains(&"hello".to_string()));
assert_eq!(term.cursor_moves(), &[(5, 10)]);
```
`ProcessTerminal::new_test()` (available only under `#[cfg(test)]`) provides another in-memory terminal that reports a fixed 80×24 size.
### Running specific tests
```bash
cargo test input_tests # Run tests matching "input_tests"
cargo test --test lib_tests # Run a specific integration test file
cargo test -- --nocapture # Show println! / eprintln! output
```
### Regression tests
The test suite contains many regression tests named `blit_into_rect_preserves_ansi_reset`, `diff_resets_ansi_before_clear`, etc. When fixing ANSI-related bugs, add a regression test that asserts on the exact escape sequences emitted.
## Development conventions
### Component implementation checklist
When adding a new component:
1. Create `src/components/your_component.rs`.
2. Add `pub mod your_component;` and a `pub use` in `src/components/mod.rs`.
3. Implement `Component`. If the component can receive focus, also implement `Focusable`.
4. Add a corresponding test file in `tests/` (e.g., `tests/your_component_tests.rs`).
5. If appropriate, add the component to `examples/demo.rs`.
### ANSI handling
The framework takes ANSI escape sequences very seriously:
- `utils::visible_width` skips CSI (`\x1b[…`) and OSC (`\x1b]…`) sequences when measuring width.
- `utils::truncate_to_width` preserves ANSI prefixes/infixes and appends `\x1b[0m` when truncation occurs inside styled text.
- `Rendered::blit_into_rect` preserves `\x1b[0m` at boundaries to prevent background color bleeding.
- The renderer always emits `\x1b[0m` before `\x1b[2K` or `\x1b[2J`.
### Error handling
Because `unwrap`, `expect`, and `?` are all denied in non-test code, the typical pattern is:
```rust
match some_fallible_operation() {
Ok(value) => value,
Err(e) => return Err(e.into()),
}
```
or propagate `io::Result` / `Result<T, RenderError>` manually.
### Image support
Images are supported via:
- **Kitty graphics protocol** — `image::encode_kitty`
- **iTerm2 inline images** — `image::encode_iterm2`
`ImageWidget` accepts raw image bytes and a MIME type. The TUI runtime automatically deletes stale images by tracking image IDs across frames.
### Vim vs Emacs editing modes
Both `Input` and `Editor` support dual editing modes. Vim mode is toggled at runtime via `set_vim_mode_enabled(true)`. When implementing new editor features, consider how they map in both modes.
## Security considerations
- **No `unsafe` code.** The crate denies `clippy::undocumented_unsafe_blocks` and currently contains zero `unsafe` blocks.
- **Terminal escape injection.** All user-facing text rendering goes through `utils::visible_width` and `utils::wrap_text_with_ansi`, which are designed to handle arbitrary ANSI sequences safely. However, when adding new components that accept external strings, ensure ANSI sequences are either escaped or measured correctly to prevent width-overflow panics.
- **Image data.** `ImageWidget` accepts raw image bytes; it does not validate image content beyond parsing dimensions from headers. Do not assume image data is well-formed.
## Useful environment variables
| `PHOTON_UI_HARDWARE_CURSOR` | If set, `TUI` shows the hardware cursor instead of hiding it. |