hojicha

Crates.iohojicha
lib.rshojicha
version0.2.2
created_at2025-08-07 17:40:43.306609+00
updated_at2025-08-29 17:13:16.896906+00
descriptionElm Architecture for terminal UIs in Rust, inspired by Bubbletea
homepagehttps://jgok76.gitea.cloud/femtomc/hojicha
repositoryhttps://jgok76.gitea.cloud/femtomc/hojicha
max_upload_size
id1785588
size41,552
McCoy R. Becker (femtomc)

documentation

https://docs.rs/hojicha

README

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

Commit count: 0

cargo fmt