Skip to main content

bubbletea/
lib.rs

1#![forbid(unsafe_code)]
2// Allow pedantic lints for API ergonomics in early development.
3#![allow(clippy::cast_possible_truncation)]
4#![allow(clippy::doc_markdown)]
5#![allow(clippy::missing_const_for_fn)]
6#![allow(clippy::must_use_candidate)]
7#![allow(clippy::nursery)]
8#![allow(clippy::pedantic)]
9
10//! # Bubbletea
11//!
12//! A powerful TUI (Terminal User Interface) framework based on The Elm Architecture.
13//!
14//! Bubbletea provides a functional approach to building terminal applications with:
15//! - A simple **Model-Update-View** architecture
16//! - **Command-based** side effects
17//! - **Type-safe messages** with downcasting
18//! - Full **keyboard and mouse** support
19//! - **Frame-rate limited** rendering (60 FPS default)
20//!
21//! ## Role in `charmed_rust`
22//!
23//! Bubbletea is the core runtime and event loop for the entire ecosystem:
24//! - **bubbles** builds reusable widgets on top of the Model/Msg/Cmd pattern.
25//! - **huh** composes form flows using bubbletea models.
26//! - **wish** serves bubbletea programs over SSH.
27//! - **glow** uses bubbletea for pager-style Markdown viewing.
28//! - **demo_showcase** is the flagship multi-page bubbletea app.
29//!
30//! ## The Elm Architecture
31//!
32//! Bubbletea follows the Elm Architecture pattern:
33//!
34//! - **Model**: Your application state
35//! - **Update**: A pure function that processes messages and returns commands
36//! - **View**: A pure function that renders state to a string
37//! - **Cmd**: Lazy IO operations that produce messages
38//!
39//! ## Quick Start
40//!
41//! ```rust,ignore
42//! use bubbletea::{Program, Model, Message, Cmd, KeyMsg, KeyType};
43//!
44//! struct Counter {
45//!     count: i32,
46//! }
47//!
48//! struct IncrementMsg;
49//! struct DecrementMsg;
50//!
51//! impl Model for Counter {
52//!     fn init(&self) -> Option<Cmd> {
53//!         None
54//!     }
55//!
56//!     fn update(&mut self, msg: Message) -> Option<Cmd> {
57//!         if msg.is::<IncrementMsg>() {
58//!             self.count += 1;
59//!         } else if msg.is::<DecrementMsg>() {
60//!             self.count -= 1;
61//!         } else if let Some(key) = msg.downcast_ref::<KeyMsg>() {
62//!             match key.key_type {
63//!                 KeyType::CtrlC | KeyType::Esc => return Some(bubbletea::quit()),
64//!                 KeyType::Runes if key.runes == vec!['q'] => return Some(bubbletea::quit()),
65//!                 _ => {}
66//!             }
67//!         }
68//!         None
69//!     }
70//!
71//!     fn view(&self) -> String {
72//!         format!(
73//!             "Count: {}\n\nPress +/- to change, q to quit",
74//!             self.count
75//!         )
76//!     }
77//! }
78//!
79//! fn main() -> Result<(), bubbletea::Error> {
80//!     let model = Counter { count: 0 };
81//!     let final_model = Program::new(model)
82//!         .with_alt_screen()
83//!         .run()?;
84//!     println!("Final count: {}", final_model.count);
85//!     Ok(())
86//! }
87//! ```
88//!
89//! ## Messages
90//!
91//! Messages are type-erased using [`Message`]. You can create custom message types
92//! and downcast them in your update function:
93//!
94//! ```rust
95//! use bubbletea::Message;
96//!
97//! struct MyCustomMsg { value: i32 }
98//!
99//! let msg = Message::new(MyCustomMsg { value: 42 });
100//!
101//! // Check type
102//! if msg.is::<MyCustomMsg>() {
103//!     // Downcast to access
104//!     if let Some(custom) = msg.downcast::<MyCustomMsg>() {
105//!         assert_eq!(custom.value, 42);
106//!     }
107//! }
108//! ```
109//!
110//! ## Commands
111//!
112//! Commands are lazy IO operations that produce messages:
113//!
114//! ```rust
115//! use bubbletea::{Cmd, Message, batch, sequence};
116//! use std::time::Duration;
117//!
118//! // Simple command
119//! let cmd = Cmd::new(|| Message::new("done"));
120//!
121//! // Batch commands (run concurrently)
122//! let cmds = batch(vec![
123//!     Some(Cmd::new(|| Message::new(1))),
124//!     Some(Cmd::new(|| Message::new(2))),
125//! ]);
126//!
127//! // Sequence commands (run in order)
128//! let cmds = sequence(vec![
129//!     Some(Cmd::new(|| Message::new(1))),
130//!     Some(Cmd::new(|| Message::new(2))),
131//! ]);
132//! ```
133//!
134//! ## Keyboard Input
135//!
136//! Keyboard events are delivered as [`KeyMsg`]:
137//!
138//! ```rust
139//! use bubbletea::{KeyMsg, KeyType, Message};
140//!
141//! fn handle_key(msg: Message) {
142//!     if let Some(key) = msg.downcast_ref::<KeyMsg>() {
143//!         match key.key_type {
144//!             KeyType::Enter => println!("Enter pressed"),
145//!             KeyType::CtrlC => println!("Ctrl+C pressed"),
146//!             KeyType::Runes => println!("Typed: {:?}", key.runes),
147//!             _ => {}
148//!         }
149//!     }
150//! }
151//! ```
152//!
153//! ## Mouse Input
154//!
155//! Enable mouse tracking with `with_mouse_cell_motion()` or `with_mouse_all_motion()`:
156//!
157//! ```rust,ignore
158//! use bubbletea::{Program, MouseMsg, MouseButton, MouseAction};
159//!
160//! let program = Program::new(model)
161//!     .with_mouse_cell_motion()  // Track clicks and drags
162//!     .run()?;
163//!
164//! // In update:
165//! if let Some(mouse) = msg.downcast_ref::<MouseMsg>() {
166//!     if mouse.button == MouseButton::Left && mouse.action == MouseAction::Press {
167//!         println!("Click at ({}, {})", mouse.x, mouse.y);
168//!     }
169//! }
170//! ```
171//!
172//! ## Screen Control
173//!
174//! Control terminal features with screen commands:
175//!
176//! ```rust
177//! use bubbletea::screen;
178//!
179//! // In update, return a command:
180//! let cmd = screen::enter_alt_screen();
181//! let cmd = screen::hide_cursor();
182//! let cmd = screen::enable_mouse_cell_motion();
183//! ```
184
185pub mod command;
186pub mod key;
187pub mod message;
188pub mod mouse;
189pub mod program;
190pub mod screen;
191pub mod simulator;
192
193// Re-exports
194pub use command::{
195    Cmd, batch, every, printf, println, quit, sequence, set_window_title, tick, window_size,
196};
197
198#[cfg(feature = "async")]
199pub use command::{AsyncCmd, every_async, tick_async};
200pub use key::{KeyMsg, KeyType, parse_sequence, parse_sequence_prefix};
201pub use message::{
202    BlurMsg, FocusMsg, InterruptMsg, Message, QuitMsg, ResumeMsg, SuspendMsg, WindowSizeMsg,
203};
204pub use mouse::{MouseAction, MouseButton, MouseMsg, parse_mouse_event_sequence};
205pub use program::{Error, Model, Program, ProgramHandle, ProgramOptions, Result};
206
207// Re-export derive macro when macros feature is enabled.
208// Derive macros and traits live in different namespaces, so both can be named `Model`.
209// Users can write `#[derive(bubbletea::Model)]` for the macro and `impl bubbletea::Model` for the trait.
210#[cfg(feature = "macros")]
211#[doc(hidden)]
212pub use bubbletea_macros::*;
213
214/// Prelude module for convenient imports.
215pub mod prelude {
216    pub use crate::command::{Cmd, batch, every, printf, println, quit, sequence, tick};
217    pub use crate::key::{KeyMsg, KeyType};
218    pub use crate::message::{Message, QuitMsg, WindowSizeMsg};
219    pub use crate::mouse::{MouseAction, MouseButton, MouseMsg};
220    pub use crate::program::{Model, Program};
221
222    #[cfg(feature = "async")]
223    pub use crate::command::{AsyncCmd, every_async, tick_async};
224}