| Crates.io | teapot |
| lib.rs | teapot |
| version | 0.1.0-alpha.1 |
| created_at | 2025-12-31 06:45:32.439929+00 |
| updated_at | 2025-12-31 06:45:32.439929+00 |
| description | A Rust-native terminal UI framework following the Elm Architecture, inspired by Bubble Tea |
| homepage | |
| repository | https://github.com/inferadb/teapot |
| max_upload_size | |
| id | 2014103 |
| size | 627,678 |
teapot provides a functional, declarative approach to building terminal user interfaces:
Run the following Cargo command in your project directory:
cargo add teapot
use teapot::{Model, Program, Cmd, Event, KeyCode};
struct Counter {
count: i32,
}
enum Msg {
Increment,
Decrement,
Quit,
}
impl Model for Counter {
type Message = Msg;
fn init(&self) -> Option<Cmd<Self::Message>> {
None
}
fn update(&mut self, msg: Self::Message) -> Option<Cmd<Self::Message>> {
match msg {
Msg::Increment => self.count += 1,
Msg::Decrement => self.count -= 1,
Msg::Quit => return Some(Cmd::quit()),
}
None
}
fn view(&self) -> String {
format!("Count: {}\n\nPress +/- to change, q to quit", self.count)
}
fn handle_event(&self, event: Event) -> Option<Self::Message> {
match event {
Event::Key(key) => match key.code {
KeyCode::Char('+') => Some(Msg::Increment),
KeyCode::Char('-') => Some(Msg::Decrement),
KeyCode::Char('q') => Some(Msg::Quit),
_ => None,
},
_ => None,
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
Program::new(Counter { count: 0 }).run()?;
Ok(())
}
Single-line text input with cursor support, placeholder text, and optional password masking.
use teapot::components::TextInput;
let input = TextInput::new()
.placeholder("Enter your name...")
.prompt("> ");
Multi-line text editor with cursor navigation, scrolling, and line editing.
use teapot::components::TextArea;
let textarea = TextArea::new()
.placeholder("Enter your message...")
.height(10)
.width(60);
Open content in your preferred editor with Ctrl+O:
let textarea = TextArea::new()
.placeholder("Enter code...")
.editor("code --wait") // Use VS Code (default: $VISUAL or $EDITOR)
.editor_extension("rs"); // File extension for syntax highlighting
Single-choice selection from a list of options.
use teapot::components::Select;
let select = Select::new("Choose a color")
.options(vec!["Red", "Green", "Blue"]);
Multiple-choice selection with checkboxes and optional min/max constraints.
use teapot::components::MultiSelect;
let select = MultiSelect::new("Choose colors")
.options(vec!["Red", "Green", "Blue"])
.min(1)
.max(2);
Yes/No confirmation prompt with customizable default.
use teapot::components::Confirm;
let confirm = Confirm::new("Are you sure?")
.default(false);
Filterable, paginated list with keyboard navigation and search.
use teapot::components::List;
let list = List::new("Select a file")
.items(vec!["main.rs", "lib.rs", "Cargo.toml"])
.height(10)
.filterable(true);
Animated loading indicator for indeterminate operations.
use teapot::components::{Spinner, SpinnerStyle};
let spinner = Spinner::new()
.style(SpinnerStyle::Dots)
.message("Loading...");
Progress bar for operations with known completion percentage.
use teapot::components::Progress;
let progress = Progress::new()
.total(100)
.current(45)
.message("Downloading...");
Parallel progress bars for tracking multiple concurrent tasks.
use teapot::components::MultiProgress;
let mp = MultiProgress::new()
.add_task("download", "Downloading files...", 100)
.add_task("compile", "Compiling...", 50)
.add_task("test", "Running tests...", 200);
Scrollable container for long content with keyboard navigation.
use teapot::components::Viewport;
let viewport = Viewport::new(80, 20)
.content("Long scrollable content here...");
Data table with columns, alignment options, and row selection.
use teapot::components::{Table, Column};
let table = Table::new()
.columns(vec![
Column::new("Name").width(20),
Column::new("Age").width(5),
Column::new("City").width(15),
])
.rows(vec![
vec!["Alice", "30", "New York"],
vec!["Bob", "25", "Los Angeles"],
])
.height(10);
Build multi-step forms with validation, inspired by Huh.
use teapot::forms::{Form, Group, InputField, SelectField, ConfirmField};
let form = Form::new()
.title("User Registration")
.group(
Group::new()
.title("Personal Info")
.field(InputField::new("name").title("Your name").required().build())
.field(InputField::new("email").title("Email").build())
)
.group(
Group::new()
.title("Preferences")
.field(SelectField::new("theme").title("Theme")
.options(["Light", "Dark", "System"]).build())
.field(ConfirmField::new("newsletter").title("Subscribe?").build())
);
Control how form groups are displayed:
use teapot::forms::{Form, FormLayout};
// Default: one group at a time (wizard-style)
let wizard = Form::new().layout(FormLayout::Default);
// Stack: all groups visible at once
let stacked = Form::new().layout(FormLayout::Stack);
// Columns: side-by-side layout
let columns = Form::new().layout(FormLayout::Columns(2));
use teapot::forms::{
InputField, SelectField, MultiSelectField, ConfirmField,
NoteField, FilePickerField
};
// Text input with validation
InputField::new("email")
.title("Email Address")
.placeholder("user@example.com")
.required()
.build();
// Single selection
SelectField::new("country")
.title("Country")
.options(["USA", "Canada", "UK", "Germany"])
.build();
// Multiple selection with constraints
MultiSelectField::new("languages")
.title("Languages")
.options(["Rust", "Go", "Python", "TypeScript"])
.min(1)
.max(3)
.build();
// Yes/No confirmation
ConfirmField::new("agree")
.title("Accept terms?")
.default(false)
.build();
// Display-only note
NoteField::new("Please review carefully before proceeding.")
.title("Important")
.build();
// File/directory picker
FilePickerField::new("config_file")
.title("Select config file")
.directory("/etc")
.extensions(["toml", "yaml", "json"])
.build();
Field titles and descriptions can update dynamically:
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let attempt = Arc::new(AtomicUsize::new(1));
let attempt_clone = attempt.clone();
InputField::new("password")
.title_fn(move || format!("Password (attempt {})", attempt_clone.load(Ordering::SeqCst)))
.description_fn(|| "Must be at least 8 characters".to_string())
.build();
Browse and select files/directories:
use teapot::components::FilePicker;
let picker = FilePicker::new()
.title("Select a file")
.directory("/home/user/projects")
.extensions(["rs", "toml"]) // Filter by extension
.show_hidden(false) // Hide dotfiles
.height(15); // Visible rows
// Or for directory selection only
let dir_picker = FilePicker::new()
.title("Select output directory")
.dirs_only();
Teapot includes a comprehensive styling system inspired by Lip Gloss.
use teapot::style::{Style, Color, Border};
let styled = Style::new()
.fg(Color::Cyan)
.bg(Color::Black)
.bold()
.italic()
.border(Border::Rounded)
.render("Hello, World!");
Padding and margin support CSS-style shorthand (1, 2, 3, or 4 values):
use teapot::style::Style;
// All sides: 2
Style::new().padding(&[2]);
// Vertical: 1, Horizontal: 2
Style::new().padding(&[1, 2]);
// Top: 1, Horizontal: 2, Bottom: 3
Style::new().margin(&[1, 2, 3]);
// Top: 1, Right: 2, Bottom: 3, Left: 4 (clockwise)
Style::new().margin(&[1, 2, 3, 4]);
Control width, height, and alignment:
use teapot::style::{Style, Position};
let box_style = Style::new()
.width(40)
.height(10)
.max_width(80)
.align(Position::Center, Position::Center)
.border(Border::Rounded);
Compose blocks horizontally or vertically:
use teapot::style::{join_horizontal_with, join_vertical_with, place, Position};
// Side-by-side blocks (aligned at top)
let combined = join_horizontal_with(Position::Top, &[left_block, right_block]);
// Stacked blocks (centered horizontally)
let stacked = join_vertical_with(Position::Center, &[header, content, footer]);
// Position content in a box
let centered = place(80, 24, Position::Center, Position::Center, "Centered!");
Colors that adapt to light/dark terminal backgrounds:
use teapot::style::Color;
// Different colors for light vs dark backgrounds
let adaptive = Color::Adaptive {
light: Box::new(Color::Ansi256(236)), // Dark gray for light bg
dark: Box::new(Color::Ansi256(252)), // Light gray for dark bg
};
// Full color specification for all terminal types
let complete = Color::Complete {
true_color: "#ff6600".to_string(),
ansi256: 208,
ansi: 3, // Yellow fallback
};
Build styles incrementally:
use teapot::style::Style;
let base = Style::new()
.fg(Color::White)
.bold();
let highlight = Style::new()
.inherit(&base) // Copy unset properties from base
.bg(Color::Blue);
// Unset specific properties
let plain = highlight.unset_bold().unset_bg();
Configure the program with a fluent builder API:
use teapot::{Program, Model};
use std::time::Duration;
Program::new(my_model)
.with_alt_screen() // Use alternate screen buffer
.with_mouse() // Enable mouse events
.with_bracketed_paste() // Enable paste detection
.with_focus_change() // Enable focus/blur events
.with_tick_rate(Duration::from_millis(16)) // ~60 FPS
.with_accessible() // Force accessible mode
.run()?;
Pre-process or block messages before they reach your update function:
Program::new(my_model)
.with_filter(|model, msg| {
// Block all messages while loading
if model.is_loading {
return None;
}
// Transform or pass through
Some(msg)
})
.run()?;
The cmd module provides Bubble Tea-style command functions:
use teapot::cmd;
use std::time::Duration;
// Quit the program
cmd::quit()
// Batch multiple commands
cmd::batch(vec![cmd1, cmd2, cmd3])
// Sequential execution
cmd::sequence(vec![cmd1, cmd2, cmd3])
// Periodic tick
cmd::tick(Duration::from_secs(1), |_| Msg::Tick)
// No-op command
cmd::none()
Spawn external processes (editors, etc.) with terminal teardown/restore:
use teapot::Cmd;
use std::process::Command;
let mut cmd = Command::new("vim");
cmd.arg("file.txt");
Cmd::run_process(cmd, |result| {
match result {
Ok(status) => Msg::EditorClosed(status.success()),
Err(_) => Msg::EditorFailed,
}
})
The framework follows The Elm Architecture:
flowchart TD
subgraph Runtime["Runtime Loop"]
direction LR
Model["Model<br/>(State)"]
View["View<br/>(Render)"]
Update["Update<br/>(Logic)"]
end
Model --> View
View -->|"returns String"| Terminal["Terminal Output"]
Events["User Events"] --> Update
Update -->|"New Model + Cmd"| Model
subgraph Commands["Commands (Effects)"]
Tick["Tick timers"]
Async["Async I/O"]
Quit["Quit signal"]
end
Update --> Commands
Commands --> Update
The framework automatically detects non-interactive environments:
Teapot supports accessible mode for screen reader users and other assistive technologies.
Set the ACCESSIBLE environment variable:
ACCESSIBLE=1 ./my-app
Use Form::run_accessible() for a fully accessible form experience:
use teapot::forms::{Form, Group, InputField, SelectField, ConfirmField};
let mut form = Form::new()
.title("User Survey")
.group(
Group::new()
.field(InputField::new("name").title("Your name").build())
.field(SelectField::new("color").title("Favorite color")
.options(["Red", "Green", "Blue"]).build())
.field(ConfirmField::new("subscribe").title("Subscribe to newsletter?").build())
);
// Run in accessible mode (line-based prompts)
match form.run_accessible() {
Ok(Some(results)) => {
println!("Name: {}", results.get_string("name").unwrap_or(""));
println!("Color: {}", results.get_string("color").unwrap_or(""));
println!("Subscribe: {}", results.get_bool("subscribe").unwrap_or(false));
}
Ok(None) => println!("Form cancelled"),
Err(e) => eprintln!("Error: {}", e),
}
=== User Survey ===
Your name
?
> Alice
Favorite color
? Favorite color
1) Red
2) Green
* 3) Blue
Enter number (or q to cancel): 3
Subscribe to newsletter?
? Subscribe to newsletter? (y/N) y
Form completed!
Components can implement the Accessible trait for custom accessible rendering:
use teapot::{Accessible, Model};
impl Accessible for MyComponent {
type Message = MyMsg;
fn accessible_prompt(&self) -> String {
// Return plain text prompt
format!("? {}\n> ", self.title)
}
fn parse_accessible_input(&self, input: &str) -> Option<Self::Message> {
// Parse line input and return message
Some(MyMsg::SetValue(input.trim().to_string()))
}
fn is_accessible_complete(&self) -> bool {
self.submitted
}
}
| Variable | Description |
|---|---|
ACCESSIBLE=1 |
Enable accessible mode |
NO_COLOR=1 |
Disable colors (respected automatically) |
REDUCE_MOTION=1 |
Disable animations |