# tears
[](https://crates.io/crates/tears)
[](https://docs.rs/tears)
[](https://github.com/akiomik/tears/actions/workflows/ci.yml)
[](LICENSE)
[](https://www.rust-lang.org)
[](https://codecov.io/gh/akiomik/tears)
A simple and elegant framework for building TUI applications using **The Elm Architecture (TEA)**.
Built on top of [ratatui](https://ratatui.rs/), 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`:
```toml
[dependencies]
tears = "0.8"
ratatui = "0.30"
crossterm = "0.29"
tokio = { version = "1", features = ["full"] }
```
See the [Optional Features](#optional-features) section for information about enabling `ws` (WebSocket) and `http` (HTTP Query/Mutation) features.
## Getting Started
### Minimal Example
Every tears application implements the `Application` trait with four required methods:
```rust
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 an `Runtime` and call `run()`:
```rust
#[tokio::main]
async fn main() -> Result<()> {
let runtime = Runtime::<App>::new((), 60);
// Setup terminal (see complete example below)
// ...
runtime.run(&mut terminal).await?;
Ok(())
}
```
### Complete Example
Here's a simple counter application that increments every second:
```rust
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()?;
// Setup terminal
let mut terminal = ratatui::init();
// Run application at 60 FPS
let runtime = Runtime::<Counter>::new((), 60);
let result = runtime.run(&mut terminal).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
- **Terminal Events** (`terminal::TerminalEvents`): Keyboard, mouse, and resize events
- **Timer** (`time::Timer`): Periodic tick events
- **Signal** (`signal::Signal`): OS signal handling (Unix/Windows)
- **WebSocket** (`websocket::WebSocket`, requires `ws`): Real-time bidirectional communication
- **Query** (`http::Query`, requires `http`): HTTP data fetching with caching
- **Mutation** (`http::Mutation`, requires `http`): HTTP data modifications
- **MockSource** (`mock::MockSource`): Controllable mock for testing
Create custom subscriptions by implementing the `SubscriptionSource` trait.
## Examples
Check out the [`examples/`](examples/) directory for more examples:
- [`counter.rs`](examples/counter.rs) - A simple counter with timer and keyboard input
- [`views.rs`](examples/views.rs) - Multiple view states with navigation and conditional subscriptions
- [`signals.rs`](examples/signals.rs) - OS signal handling with graceful shutdown (SIGINT, SIGTERM, etc.)
- [`websocket.rs`](examples/websocket.rs) - WebSocket echo chat demonstrating real-time communication (requires `ws` feature)
- [`http_todo.rs`](examples/http_todo.rs) - HTTP Todo list with Query subscription, Mutation, and cache management (requires `http` feature)
Run an example:
```bash
cargo run --example counter
cargo run --example views
cargo run --example signals
cargo run --example websocket --features ws,rustls
cargo run --example http_todo --features http
```
## Optional Features
Tears supports optional features that can be enabled in your `Cargo.toml`:
### WebSocket Support
```toml
[dependencies]
tears = { version = "0.8", features = ["ws", "rustls"] }
```
- **`ws`**: Enables WebSocket subscription support
- **TLS backends** (choose one for `wss://` support):
- `native-tls` - Platform's native TLS
- `rustls` - Pure Rust TLS with native certificates
- `rustls-tls-webpki-roots` - Pure Rust TLS with webpki certificates
### HTTP Support
```toml
[dependencies]
tears = { version = "0.8", features = ["http"] }
```
- **`http`**: Enables HTTP Query and Mutation support
- `Query` subscription for automatic data fetching with caching
- `Mutation` for data modifications (POST, PUT, PATCH, DELETE)
- `QueryClient` for cache management and invalidation
## Inspiration & Design Philosophy
Tears is inspired by battle-tested architectures:
- **[Elm](https://elm-lang.org/)**: The original Elm Architecture
- **[iced](https://github.com/iced-rs/iced)**: Rust GUI framework (v0.12 design)
- **[Bubble Tea](https://github.com/charmbracelet/bubbletea)**: Go TUI framework with TEA
The framework is designed with these principles:
- **Simplicity First**: Minimal and easy-to-understand API
- **Thin Framework**: Minimal abstraction over ratatui - you have full control
- **Type Safety**: Leverage Rust's type system for correctness
## Minimum Supported Rust Version (MSRV)
Tears requires Rust 1.86.0 or later (uses edition 2024).
## License
Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details.
## Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
---
Built with β€οΈ using [ratatui](https://ratatui.rs/)