tears 0.4.1

A simple and elegant framework for building TUI applications using The Elm Architecture (TEA)
Documentation

tears

Crates.io Documentation CI License Rust Version codecov

A simple and elegant framework for building TUI applications using The Elm Architecture (TEA).

Built on top of ratatui, Tears provides a clean, type-safe, and functional approach to terminal user interface development.

Features

  • 🎯 Simple & Predictable: Based on The Elm Architecture - easy to reason about and test
  • πŸ”„ Async-First: Built-in support for async operations via Commands
  • πŸ“‘ Subscriptions: Handle terminal events, timers, and custom event sources
  • πŸ§ͺ Testable: Pure functions for update logic make testing straightforward
  • πŸš€ Powered by Ratatui: Leverage the full power of the ratatui ecosystem
  • πŸ¦€ Type-Safe: Leverages Rust's type system for safer TUI applications

Installation

Add this to your Cargo.toml:

[dependencies]
tears = "0.4"
ratatui = "0.29"
crossterm = "0.28"
tokio = { version = "1", features = ["full"] }

Optional Features

Tears provides optional features that can be enabled in your Cargo.toml:

WebSocket Support

To enable WebSocket subscriptions, add the ws feature:

[dependencies]
tears = { version = "0.4", features = ["ws"] }

For secure WebSocket connections (wss://), you also need to enable a TLS backend:

[dependencies]
# Using native TLS (recommended for most cases)
tears = { version = "0.4", features = ["ws", "native-tls"] }

# Or using rustls with ring crypto provider (pure Rust implementation)
tears = { version = "0.4", features = ["ws", "rustls"] }

# Or using rustls with webpki roots
tears = { version = "0.4", features = ["ws", "rustls-tls-webpki-roots"] }

Getting Started

Minimal Example

Every tears application implements the Application trait with four required methods:

use tears::prelude::*;
use ratatui::Frame;

struct App;

enum Message {}

impl Application for App {
    type Message = Message;  // Your message type
    type Flags = ();         // Initialization data (use () if none)

    // Initialize your app
    fn new(_flags: ()) -> (Self, Command<Message>) {
        (App, Command::none())
    }

    // Handle messages and update state
    fn update(&mut self, _msg: Message) -> Command<Message> {
        Command::none()
    }

    // Render your UI
    fn view(&self, frame: &mut Frame) {
        // Use ratatui widgets here
    }

    // Subscribe to events (keyboard, timers, etc.)
    fn subscriptions(&self) -> Vec<Subscription<Message>> {
        vec![]
    }
}

To run your application, create a Runtime and call run():

#[tokio::main]
async fn main() -> Result<()> {
    let runtime = Runtime::<App>::new(());

    // Setup terminal (see complete example below)
    // ...

    runtime.run(&mut terminal, 60).await?;
    Ok(())
}

Complete Example

Here's a simple counter application that increments every second:

use color_eyre::eyre::Result;
use crossterm::event::{Event, KeyCode};
use ratatui::{Frame, text::Text};
use tears::prelude::*;
use tears::subscription::{terminal::TerminalEvents, time::{Message as TimerMessage, Timer}};

#[derive(Debug, Clone)]
enum Message {
    Tick,
    Input(Event),
    InputError(String),
}

struct Counter {
    count: u32,
}

impl Application for Counter {
    type Message = Message;
    type Flags = ();

    fn new(_flags: ()) -> (Self, Command<Message>) {
        (Counter { count: 0 }, Command::none())
    }

    fn update(&mut self, msg: Message) -> Command<Message> {
        match msg {
            Message::Tick => {
                self.count += 1;
                Command::none()
            }
            Message::Input(Event::Key(key)) if key.code == KeyCode::Char('q') => {
                Command::effect(Action::Quit)
            }
            Message::InputError(e) => {
                eprintln!("Input error: {e}");
                Command::effect(Action::Quit)
            }
            _ => Command::none(),
        }
    }

    fn view(&self, frame: &mut Frame) {
        let text = Text::raw(format!("Count: {} (Press 'q' to quit)", self.count));
        frame.render_widget(text, frame.area());
    }

    fn subscriptions(&self) -> Vec<Subscription<Message>> {
        vec![
            Subscription::new(Timer::new(1000)).map(|timer_msg| {
                match timer_msg {
                    TimerMessage::Tick => Message::Tick,
                }
            }),
            Subscription::new(TerminalEvents::new()).map(|result| match result {
                Ok(event) => Message::Input(event),
                Err(e) => Message::InputError(e.to_string()),
            }),
        ]
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    color_eyre::install()?;

    let runtime = Runtime::<Counter>::new(());

    // Setup terminal
    let mut terminal = ratatui::init();

    // Run application at 60 FPS
    let result = runtime.run(&mut terminal, 60).await;

    // Restore terminal
    ratatui::restore();

    result
}

Architecture

Tears follows The Elm Architecture (TEA) pattern:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  Model  │─────▢│  View  │─────▢│  UI  β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚       β–²                                      β”‚
β”‚       β”‚                                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚  β”‚  Update  │◀────│   Messages   β”‚           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚       β–²                   β–²                  β”‚
β”‚       β”‚                   β”‚                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚  β”‚ Commands β”‚      β”‚Subscriptionsβ”‚           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Core Concepts

  • Model: Your application state
  • Message: Events that trigger state changes
  • Update: Pure function that processes messages and returns new state + commands
  • View: Pure function that renders UI based on current state
  • Subscriptions: External event sources (keyboard, timers, network, etc.)
  • Commands: Asynchronous side effects that produce messages

Built-in Subscriptions

Tears provides several built-in subscription sources:

  • Terminal Events (subscription::terminal::TerminalEvents): Keyboard, mouse, and window resize events
  • Timer (subscription::time::Timer): Periodic tick events at configurable intervals
  • Signal (Unix: subscription::signal::Signal, Windows: subscription::signal::CtrlC, subscription::signal::CtrlBreak): OS signal handling for graceful shutdown and interrupt handling
  • WebSocket (subscription::websocket::WebSocket, requires ws feature): Real-time WebSocket connections for bi-directional communication
  • MockSource (subscription::mock::MockSource): Controllable mock for testing

You can also create custom subscriptions by implementing the SubscriptionSource trait.

Examples

Check out the examples/ directory for more examples:

  • counter.rs - A simple counter with timer and keyboard input
  • views.rs - Multiple view states with navigation and conditional subscriptions
  • signals.rs - OS signal handling with graceful shutdown (SIGINT, SIGTERM, etc.)
  • websocket.rs - WebSocket echo chat demonstrating real-time communication (requires ws feature)

Run an example:

cargo run --example counter
cargo run --example views
cargo run --example signals
cargo run --example websocket --features ws,rustls

Design Philosophy

Tears is designed with these principles in mind:

  1. Simplicity First: Keep the API minimal and easy to understand
  2. Thin Framework: Minimal abstraction over ratatui - you have full control
  3. Proven Patterns: Based on battle-tested architectures (Elm, iced)
  4. Type Safety: Leverage Rust's type system for correctness

Inspiration

This framework is heavily inspired by:

  • Elm: The original Elm Architecture
  • iced: Rust GUI framework (v0.12 design)
  • Bubble Tea: Go TUI framework with TEA

Minimum Supported Rust Version (MSRV)

Tears requires Rust 1.85.0 or later (uses edition 2024).

License

Licensed under the Apache License, Version 2.0. See LICENSE for details.

Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.


Built with ❀️ using ratatui