# oxi-tui Guide
This guide covers the component lifecycle, rendering pipeline, and key systems in `oxi-tui`.
## Component Lifecycle
Every UI component follows this lifecycle:
```
new() ──────► render() ──────► handle_event() ──────► render() ──────► ...
(draw to (process input) (re-draw with
surface) new state)
```
### Methods
```rust
pub trait Component: Send {
/// Create and configure the component
fn new() -> Self;
/// Request a render on the next frame
fn request_render(&mut self);
/// Check if component needs re-rendering
fn is_dirty(&self) -> bool;
/// Clear dirty flag after render
fn clear_dirty(&mut self);
/// Handle user input, return true if consumed
fn handle_event(&mut self, event: &Event) -> bool;
/// Draw to the surface
fn render(&mut self, surface: &mut Surface, area: Rect);
/// Minimum size this component needs
fn min_size(&self) -> Size;
/// Desired size (optional override)
fn desired_size(&self) -> Option<Size> { None }
/// Called when component gains focus
fn on_focus(&mut self) {}
/// Called when component loses focus
fn on_unfocus(&mut self) {}
}
```
### Lifecycle Example
```rust
struct MyComponent {
text: String,
dirty: bool,
}
impl Component for MyComponent {
fn new() -> Self {
Self { text: String::new(), dirty: true }
}
fn request_render(&mut self) {
self.dirty = true;
}
fn is_dirty(&self) -> bool {
self.dirty
}
fn clear_dirty(&mut self) {
self.dirty = false;
}
fn handle_event(&mut self, event: &Event) -> bool {
match event {
Event::Key(KeyEvent { code: KeyCode::Char(c), .. }) => {
self.text.push(*c);
self.request_render(); // Re-render after change
true
}
_ => false,
}
}
fn render(&mut self, surface: &mut Surface, area: Rect) {
surface.write_string(area.y, area.x, &self.text);
self.clear_dirty();
}
fn min_size(&self) -> Size {
Size { width: 10, height: 1 }
}
}
```
## Rendering Pipeline
### Surface System
`Surface` is a 2D grid of `Cell`s representing the terminal:
```rust
pub struct Surface {
width: u16,
height: u16,
cells: Vec<Vec<Cell>>,
dirty_cells: Vec<Vec<bool>>,
first_dirty_row: Option<u16>,
last_dirty_row: Option<u16>,
}
```
### Rendering Flow
```
┌─────────────┐
│ Components │
│ (dirty?) │
└──────┬──────┘
│
▼
┌─────────────────────────┐
│ Render to Surface │
│ (clear dirty cells) │
└──────┬──────────────────┘
│
▼
┌─────────────────────────┐
│ Diff Detection │ ──► Compute SGR (escape codes)
│ (mark changed cells) │ Only update changed cells
└──────┬──────────────────┘
│
▼
┌─────────────────────────┐
│ Flush to Terminal │
│ (SGR + cursor moves) │
└─────────────────────────┘
```
### Dirty Tracking
```rust
// Mark cells as dirty when changing
surface.set(row, col, cell); // Marks cell as dirty
// Query dirty range for optimal updates
if let Some(first) = surface.first_dirty() {
let last = surface.last_dirty().unwrap();
// Only redraw rows first..=last
}
// Clear after rendering
surface.clear_dirty();
```
### Cell Structure
```rust
pub struct Cell {
pub char: char,
pub fg: Color,
pub bg: Color,
pub attrs: Attributes,
}
pub struct Attributes {
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strikethrough: bool,
pub reverse: bool,
}
```
### SGR Escape Codes
Only changed cells are sent to the terminal:
```rust
// SGR sequence builder
fn cell_to_sgr(cell: &Cell) -> String {
let mut parts = vec![];
parts.push("38;2;{};{};{}".format(r, g, b)); // fg
parts.push("48;2;{};{};{}".format(r, g, b)); // bg
if attrs.bold { parts.push("1"); }
if attrs.italic { parts.push("3"); }
format!("\x1b[{}m", parts.join(";"))
}
```
### Wide Character Support
CJK characters occupy 2 columns:
```rust
surface.write_string(0, 0, "Hello 世界");
// Cells:
// [H][e][l][l][o][ ][W][o] │ └── wide continuation
// └─────────────── wide continuation
```
### Flush Pattern
```rust
fn flush_surface(surface: &Surface) {
let first_dirty = surface.first_dirty().unwrap_or(0);
let last_dirty = surface.last_dirty().unwrap_or(surface.height - 1);
for row in first_dirty..=last_dirty {
// Move cursor
print!("\x1b[{};{}H", row + 1, 1);
// Build row content with SGR codes
for col in 0..surface.width() {
let cell = surface.get(row, col).unwrap();
// Emit SGR + char
}
// Clear to end of line
print!("\x1b[0m\x1b[K");
}
}
```
## Event System
### Event Types
```rust
pub enum Event {
/// Keyboard input
Key(KeyEvent),
/// Mouse input
Mouse(MouseEvent),
/// Terminal resize
Resize(ResizeEvent),
/// Focus gained/lost
Focus(FocusEvent),
}
```
### KeyEvent Structure
```rust
pub struct KeyEvent {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
pub enum KeyCode {
Char(char),
Enter,
Escape,
Left,
Right,
Up,
Down,
Tab,
Backspace,
Home,
End,
PageUp,
PageDown,
F(u8),
}
pub struct KeyModifiers {
pub control: bool,
pub shift: bool,
pub alt: bool,
}
```
### Key Input Handling
#### Kitty Keyboard Protocol
Modern terminals use the Kitty keyboard protocol for extended keys:
```
\x1b[...;A u (modifier + key with unicode codepoint)
```
The parser handles:
- Modifier flags in escape sequence
- Unicode codepoint for special keys
#### xterm Extensions
xterm provides additional sequences:
- F1-F4 via application mode
- Bracketed paste mode
- Focus in/out events
### Mouse Input
```rust
pub struct MouseEvent {
pub kind: MouseEventKind,
pub button: MouseButton,
pub modifiers: KeyModifiers,
pub position: Position,
}
pub enum MouseEventKind {
Press,
Release,
Drag,
ScrollUp,
ScrollDown,
}
```
## Theme System
### Theme Structure
```rust
pub struct Theme {
pub name: String,
pub colors: ColorScheme,
pub fonts: FontScheme,
pub spacing: Spacing,
}
pub struct ColorScheme {
pub fg: Color,
pub bg: Color,
pub cursor: Color,
pub selection: Color,
pub scrollbar: Color,
pub border: Color,
// ... component-specific colors
}
pub struct FontScheme {
pub regular: String,
pub bold: String,
pub italic: String,
}
```
### Theme Files
Themes are TOML files:
```toml
[colors]
fg = "#f8f8f2"
bg = "#282a36"
cursor = "#f8f8f0"
selection = "#44475a"
[colors.chat]
user_fg = "#8be9fd"
assistant_fg = "#50fa7b"
system_fg = "#ffb86c"
[fonts]
regular = "JetBrains Mono"
size = 14
line_height = 1.4
```
### Loading Themes
```rust
let manager = ThemeManager::new();
manager.load_theme("nord")?;
manager.set_active("nord");
// Apply to surface
surface.fill(Cell::default().with_bg(theme.colors.bg));
```
## Built-in Components
### Text
Simple text display:
```rust
let text = Text::new("Hello, world!".to_string());
```
### Spacer
Flexible spacing:
```rust
// Vertical spacer
let spacer = Spacer::vertical(10);
// Horizontal spacer
let spacer = Spacer::horizontal(20);
// Flexible spacer (takes remaining space)
let spacer = Spacer::flexible();
```
### SelectList
Selectable list items:
```rust
let list = SelectList::new(vec![
SelectItem::new("Option 1", Some("description")),
SelectItem::new("Option 2", None),
]);
list.on_select(|item| { /* ... */ });
```
### CommandPalette
Fuzzy-filtered command list:
```rust
let palette = CommandPalette::new();
## Layout System
### Container
```rust
use oxi_tui::{Container, Direction, Constraint};
let container = Container::new(Direction::Vertical)
.add(Box::new(header).height(Constraint::Length(1)))
.add(Box::new(content).height(Constraint::Fill))
.add(Box::new(footer).height(Constraint::Length(1)));
```
### Constraints
```rust
pub enum Constraint {
/// Fixed pixel size
Length(u16),
/// Percentage of parent
Percent(u16),
/// Fill remaining space
Fill,
}
```
## Overlay System
### Creating Overlays
```rust
let overlay = OverlayContent::new()
.title("Settings")
.component(Box::new(settings_panel))
.on_close(|| { /* cleanup */ });
tui.push_overlay(overlay);
```
### Modal Pattern
```rust
surface.fill_row(rect.y, rect.x, ' ', Color::Blue, Color::DarkBlue);
// ...
});
```
## Keyboard Shortcuts
### KeyBindings
```rust
use oxi_tui::KeyBindings;
let bindings = KeyBindings::new()
.bind("C-s", Action::Save)
.bind("C-o", Action::Open)
.bind("C-c", Action::Cancel);
```
### Action Handling
```rust
fn handle_key(&mut self, event: &Event) -> bool {
if let Some(action) = self.bindings.resolve(event) {
match action {
Action::Save => self.save(),
Action::Open => self.open(),
Action::Cancel => return false, // Pass through
}
true
} else {
false
}
}
```