1 unstable release
new 0.0.1 | Feb 16, 2025 |
---|
#13 in #graphic
350KB
845 lines
hollowtea
A hollowed TUI framework.
tl;dr: Hollow Tea is your choice to go build TUI if love Bubbletea and Rust.
🚧 hollowtea is under development.
Originally, Hollow Tea was created to serve Boo text editor, however I started to use in other projects, so I have decided to move to it's own repository and create a proper documentation.
Disclaimer: It is not a project from the Charm.sh but the name is a tribute to the famous Bubbletea.
Motivation
Hollow Tea was created to make TUI creating much more simple and intuitive. You shouldn't need to know any other dependencies besides Hollow Tea to create your TUI app.
Hollow Tea follows "The Elm Architecture"
These three concepts are the core of The Elm Architecture:
- Model: the state of your application.
- View: a way to turn your state into HTML.
- Update: a way to update your state based on messages.
Let's see it in practice, by doing a quick TUI app.
Quick TUI app: A color picker

The following code:
use hollowtea::styles::*;
use hollowtea::{
hollows::{Hollow, Text},
term::event::{Event, KeyCode},
Application, Command, Message, Model,
};
struct ColorPicker<'a> {
current: usize,
items: Vec<(&'a str, Color, Color)>,
}
impl ColorPicker<'_> {
fn new() -> ColorPicker<'static> {
let items = vec![
("yellow", Color::LightYellow, Color::Yellow),
("red", Color::LightRed, Color::Red),
("blue", Color::LightBlue, Color::Blue),
("magenta", Color::LightMagenta, Color::Magenta),
("green", Color::LightGreen, Color::Green),
("cyan", Color::LightCyan, Color::Cyan),
("black", Color::LightBlack, Color::Black),
("white", Color::LightWhite, Color::White),
];
ColorPicker { current: 0, items }
}
}
impl Model for ColorPicker<'_> {
fn init(&self) -> Vec<Command> {
vec![]
}
fn update(&mut self, message: Message) -> Vec<Command> {
if let Some(Event::Key(key_event)) = message.as_ref::<Event>() {
match key_event.code {
KeyCode::Right => {
if self.current >= self.items.len() - 1 {
self.current = 0;
} else {
self.current += 1;
}
}
KeyCode::Left => {
if self.current == 0 {
self.current = self.items.len() - 1;
} else {
self.current -= 1;
}
}
KeyCode::Esc => {
return vec![Command::Quit];
}
_ => {}
}
}
vec![]
}
fn view(&self) -> Box<dyn Hollow> {
let mut content = String::default();
if let Some(current) = self.items.get(self.current) {
content.push_str(
&StyleSheet::new()
.add(Style::TextStyle(TextStyle::Bold))
.add(Style::TextColor(Color::LightBlack))
.build("Pick a color: "),
);
let idx = self.current + 1;
let indicator = String::from_utf8(vec![b' '; idx]).unwrap_or_default();
content.push_str(
&StyleSheet::new()
.add(Style::BackgroundColor(current.1.to_owned()))
.build(indicator),
);
let indicator =
String::from_utf8(vec![b' '; self.items.len() - idx]).unwrap_or_default();
content.push_str(
&StyleSheet::new()
.add(Style::BackgroundColor(current.2.to_owned()))
.build(indicator),
);
content.push_str(" ❮ ");
content.push_str(
&StyleSheet::new()
.add(Style::TextStyle(TextStyle::Bold))
.add(Style::TextColor(current.2.to_owned()))
.build(current.0),
);
content.push_str(" ❯ ");
}
Text::new(content)
}
}
fn main() {
Application {
inline: true,
..Application::default()
}
.run(&mut ColorPicker::new())
.expect("failed to run");
println!("app exited");
}
TODO
-
examples/color-picker
-
examples/textarea
-
examples/list
-
examples/table
-
examples/progress
-
examples/grid
- Interup
- Tutorial video building some TUI apps.
Dependencies
~2–11MB
~136K SLoC