Rusty Bubble Tea is a complete, from-scratch Rust port of Bubble Tea, the Elm-architecture TUI framework that powers Charmbracelet's terminal apps. It tracks upstream Go releases on a rolling basis under the family's porting policies: versions mirror upstream exactly, never ahead or behind, with a hard goal of 1:1 behavioural, visual and license parity, favouring fidelity to upstream semantics over Rust-native rewrites.
It's part of the Rusty port family of the Bubble Tea ecosystem and builds on rusty-ultraviolet (terminal renderer & input), rusty-lipgloss (styling), rusty-x-ansi (ANSI primitives), and rusty-colorprofile — with UI components available in rusty-bubbles.
About Bubble Tea
The fun, functional and stateful way to build terminal apps. A Rust port based on The Elm Architecture and upstream charmbracelet/bubbletea. Bubble Tea is well-suited for simple and complex terminal applications, either inline, full-window, or a mix of both.
cargo add rusty-bubbleteaThen build your application around a rusty_bubbletea::Program — see the tutorial below to get started.
Bubble Tea is in use in production and includes a number of features and performance optimizations. Among those is a framerate-based renderer, mouse support, focus reporting and more.
To get started, see the tutorial below, the [examples][examples], the [docs][docs], the video tutorials and some common resources.
Be sure to check out [Bubbles][bubbles], a library of common UI components for Bubble Tea.
Bubble Tea is based on the functional design paradigms of The Elm Architecture. It's a delightful way to build applications.
For this tutorial, we're making a shopping list.
Bubble Tea programs are comprised of a model that describes the application state and three simple methods on that model:
- init, a function that returns an initial command for the application to run.
- update, a function that handles incoming events and updates the model accordingly.
- view, a function that renders the UI based on the data in the model.
use rusty_bubbletea::model::Model as ModelTrait;
use rusty_bubbletea::{quit, Cmd, KeyPressMsg, Msg, Program, View};
use std::collections::HashSet;
struct Model {
choices: Vec<String>,
cursor: usize,
selected: HashSet<usize>,
}
impl ModelTrait for Model {
// The initial command. We don't need to kick off anything, so return None.
fn init(&self) -> Cmd {
None
}
// Handle incoming events. Key presses move the cursor, toggle a choice,
// or quit.
fn update(&mut self, msg: &dyn Msg) -> Cmd {
if let Some(k) = msg.as_any().downcast_ref::<KeyPressMsg>() {
match k.0.to_string().as_str() {
"j" | "down" => self.cursor = (self.cursor + 1).min(self.choices.len() - 1),
"k" | "up" => self.cursor = self.cursor.saturating_sub(1),
"enter" | " " => {
if !self.selected.insert(self.cursor) {
self.selected.remove(&self.cursor);
}
}
"q" | "ctrl+c" => return quit(),
_ => {}
}
}
None
}
// Render the current state to a View. Build a plain string and wrap it.
fn view(&self) -> View {
let mut s = String::from("What should we buy at the market?\n\n");
for (i, choice) in self.choices.iter().enumerate() {
let cursor = if self.cursor == i { ">" } else { " " };
let checked = if self.selected.contains(&i) { "x" } else { " " };
s.push_str(&format!("{cursor} [{checked}] {choice}\n"));
}
s.push_str("\nPress q to quit.\n");
View::new(&s)
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let model = Model {
choices: vec![
"Buy carrots".to_string(),
"Buy celery".to_string(),
"Buy kohlrabi".to_string(),
],
cursor: 0,
selected: HashSet::new(),
};
let program = Program::new(model);
program.run()?;
Ok(())
}Save that to main.rs and run it with cargo run:
- j/k or the arrow keys move the cursor up and down.
- enter or space toggles an item on and off the list.
- q or ctrl+c quits.
That's it — the three methods of the Elm Architecture are all you need. The Msg type is a trait object, so your update downcasts the messages you care about (keys, mouse, window size, timers, and any custom messages you define) and ignores the rest. View is a declarative description of the frame — plain text plus optional cursor, alt-screen, and mouse-mode settings — which the renderer diffs against the previous frame to produce minimal terminal output.
From here, the best next step is to browse the [examples][examples] directory — every upstream Bubble Tea example is ported there and each one is verified byte-for-byte against the Go build by the E2E harness. For common UI components such as text inputs, spinners and lists, see [rusty-bubbles][bubbles].



