hojicha 0.2.0

Elm Architecture for terminal UIs in Rust, built on Ratatui
Documentation

Hojicha

The Elm Architecture for Terminal UIs in Rust

Crates.io Documentation License

Hojicha implements The Elm Architecture for terminal applications. Built on Ratatui, inspired by Bubbletea.

Features

  • Simple Architecture - Model-View-Update pattern
  • High Performance - Priority event processing with adaptive queues
  • Async Native - First-class async/await, cancellation, streams
  • Component Library - Pre-built UI components
  • Testing Utilities - Headless mode, deterministic testing
  • Metrics - Built-in performance monitoring

Installation

[dependencies]
hojicha-core = "0.2"
hojicha-runtime = "0.2"

Quick Start

use hojicha_core::prelude::*;
use hojicha_runtime::prelude::*;
use ratatui::widgets::{Block, Borders, Paragraph};

struct Counter {
    value: i32,
}

impl Model for Counter {
    type Message = ();

    fn update(&mut self, event: Event<()>) -> Cmd<()> {
        match event {
            Event::Key(key) => match key.key {
                Key::Up => self.value += 1,
                Key::Down => self.value -= 1,
                Key::Char('q') => return quit(),
                _ => {}
            },
            _ => {}
        }
        Cmd::none()
    }

    fn view(&self, frame: &mut Frame, area: Rect) {
        let text = format!("Counter: {}\n\nUp/Down: change | q: quit", self.value);
        let widget = Paragraph::new(text)
            .block(Block::default().borders(Borders::ALL).title("Counter"));
        frame.render_widget(widget, area);
    }
}

fn main() -> Result<()> {
    Program::new(Counter { value: 0 })?.run()
}

Architecture

The Elm Architecture consists of:

Component Purpose
Model Application state
Message Events that trigger state changes
Update Handle events and update state
View Render UI from state
Command Side effects (async operations, I/O)

Common Patterns

Async Operations

fn update(&mut self, event: Event<Msg>) -> Cmd<Msg> {
    match event {
        Event::User(Msg::FetchData) => {
            spawn(async {
                let data = fetch_api().await.ok()?;
                Some(Msg::DataLoaded(data))
            })
        }
        _ => Cmd::none()
    }
}

Timers

fn init(&mut self) -> Cmd<Msg> {
    every(Duration::from_secs(1), |_| Msg::Tick)
}

Stream Subscriptions

let stream = websocket.messages().map(Msg::WsMessage);
let subscription = program.subscribe(stream);

Components

Available via hojicha-pearls:

Input: TextInput, TextArea, Button
Display: List, Table, Tabs, Modal, ProgressBar, Spinner
Layout: Grid, FloatingElement, StatusBar, Viewport

Testing

#[test]
fn test_counter() {
    TestHarness::new(Counter { value: 0 })
        .send_event(Event::Key(KeyEvent::new(Key::Up)))
        .run()
        .assert_model(|m| m.value == 1);
}

Performance Metrics

let program = Program::new(model)?
    .with_priority_config(PriorityConfig {
        enable_metrics: true,
        ..Default::default()
    });

// Export metrics
program.metrics_json();
program.metrics_prometheus();

Advanced Features

Priority Event Processing

  1. Critical: Quit, suspend
  2. High: User input (keyboard, mouse)
  3. Normal: User messages, timers
  4. Low: Resize, background tasks

Cancellable Operations

let handle = program.spawn_cancellable(|token| async move {
    while !token.is_cancelled() {
        process_batch().await;
    }
});

Documentation

Examples

cargo run --example counter
cargo run --example todo_list
cargo run --example text_editor

Contributing

See CONTRIBUTING.md.

License

GPL-3.0