hojicha 0.2.2

Elm Architecture for terminal UIs in Rust, inspired by Bubbletea
Documentation

Hojicha

The Elm Architecture for Terminal UIs in Rust

Crates.io Documentation License

Hojicha implements The Elm Architecture for terminal applications in Rust, inspired by Charm's Bubbletea.

Features

  • The Elm Architecture - Clean Model-View-Update pattern for terminal UIs
  • Async Support - Native async/await integration and cancellable operations
  • Performance - Zero-cost abstractions with optimized event processing
  • Testing - Comprehensive test harness with time control and event simulation
  • Error Handling - Robust panic recovery and structured error types
  • Cross-platform - Works on Windows, macOS, and Linux

Installation

[dependencies]
hojicha = "0.2"           # Framework + runtime (recommended)

# Or use individual crates:
hojicha-core = "0.2"      # Core framework only
hojicha-runtime = "0.2"   # Runtime and program management

Quick Start

use hojicha::prelude::*;

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::noop()
    }

    fn view(&self) -> String {
        format!(
            "╭─────────────────────╮\n\
             │      Counter        │\n\
             ├─────────────────────┤\n\
             │  Value: {:^11} │\n\
             ├─────────────────────┤\n\
             │  Up/Down: change    │\n\
             │  q: quit            │\n\
             ╰─────────────────────╯",
            self.value
        )
    }
}

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

use hojicha_core::commands;

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

Timers

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

Command Composition

// Combine multiple commands
let cmd = commands::batch(vec![
    commands::spawn(async { Some(Msg::LoadData) }),
    commands::tick(Duration::from_millis(100), || Msg::Refresh),
]);

Core Features

Hojicha provides:

Event Handling: Keyboard, mouse, resize, and custom events
Async Support: Commands for HTTP, file I/O, timers, and background tasks
Testing: Comprehensive test harness with time control and event simulation
Performance: Zero-cost abstractions with optimized event processing

Testing

use hojicha_core::testing::TestHarness;

#[test]
fn test_counter() {
    let result = TestHarness::new(Counter { value: 0 })
        .send_event(Event::Key(KeyEvent::from_char('+')))
        .run();
    
    assert_eq!(result.model.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

Here's a complete working example:

use hojicha::prelude::*;
use std::time::Duration;

#[derive(Debug)]
struct TodoApp {
    todos: Vec<String>,
    input: String,
}

#[derive(Debug, Clone)]
enum Message {
    AddTodo,
    UpdateInput(String),
    ClearCompleted,
    Tick,
}

impl Model for TodoApp {
    type Message = Message;

    fn init(&mut self) -> Cmd<Message> {
        // Start a timer that ticks every second
        commands::every(Duration::from_secs(1), |_| Message::Tick)
    }

    fn update(&mut self, event: Event<Message>) -> Cmd<Message> {
        match event {
            Event::Key(key) => match key.key {
                Key::Char('q') => commands::quit(),
                Key::Char('\n') => {
                    if !self.input.is_empty() {
                        self.todos.push(self.input.clone());
                        self.input.clear();
                    }
                    Cmd::noop()
                }
                Key::Char(c) => {
                    self.input.push(c);
                    Cmd::noop()
                }
                Key::Backspace => {
                    self.input.pop();
                    Cmd::noop()
                }
                _ => Cmd::noop(),
            },
            Event::User(Message::Tick) => {
                // Handle periodic updates
                Cmd::noop()
            }
            _ => Cmd::noop(),
        }
    }

    fn view(&self) -> String {
        let mut output = String::new();
        output.push_str("Todo App\n");
        output.push_str("--------\n");
        
        for (i, todo) in self.todos.iter().enumerate() {
            output.push_str(&format!("{}. {}\n", i + 1, todo));
        }
        
        output.push_str("\nAdd todo: ");
        output.push_str(&self.input);
        output.push_str("\n\nPress 'q' to quit, Enter to add todo");
        
        output
    }
}

fn main() -> Result<()> {
    let app = TodoApp {
        todos: vec!["Learn Rust".to_string(), "Build a TUI".to_string()],
        input: String::new(),
    };
    
    Program::new(app)?.run()
}

Contributing

Contributions are welcome! Please check the issues page and feel free to submit pull requests.

License

GPL-3.0