-
Notifications
You must be signed in to change notification settings - Fork 0
Examples
Caution
Examples are not yet updated for v5.0.0-rc.1.
Here’s a simple terminal UI counter that you can increment and decrement interactively.
When running the code, always run in release mode if possible.
For this example, the optional featurereduce_abstractionneeds to be enabled.
use feather_tui as tui;
fn container() -> tui::FtuiResult<(tui::Container, u16, u16, u16)>{
let (mut increment_id, mut decrement_id, mut counter_id) = (0u16, 0u16, 0u16);
let container = tui::ContainerBuilder::new()
.header("Number Counter")?
.option_id("Increment", None, &mut increment_id)?
.option_id("Decrement", None, &mut decrement_id)?
.separator_normal(tui::SeparatorStyle::Medium)
.text_id(
"0",
tui::TextFlags::ALIGN_MIDDLE
| tui::TextFlags::COLOR_RED, &mut counter_id)?
.selector_no_triggers()
.text(
"w, s, e to navigate / q to quit",
tui::TextFlags::ALIGN_RIGHT
| tui::TextFlags::ALIGN_BOTTOM | tui::TextFlags::COLOR_YELLOW_BACK)?
.build();
Ok((container, increment_id, decrement_id, counter_id))
}
fn main() -> tui::FtuiResult<()> {
let (mut container, increment_id, decrement_id, counter_id) = container()?;
let mut renderer = tui::Renderer::fullscreen()?;
let mut should_update = true;
let mut counter = 0;
tui::ready()?;
loop {
if container.option_mut(increment_id)?.is_selc() {
counter += 1;
}
if container.option_mut(decrement_id)?.is_selc() {
counter -= 1;
}
if should_update {
container.text_mut(counter_id)?.set_label(counter.to_string());
renderer.simple_draw(&mut container)?;
}
should_update = match tui::key_char()? {
Some('q') => break,
Some('w') => container.selector_up()?,
Some('s') => container.selector_down()?,
Some('e') => container.selector_select()?,
_ => false,
}
}
tui::unready()?;
Ok(())
}Here’s a minimal example of a terminal-based TODO list.
When running the code, always run in release mode if possible.
For this example, the optional featurereduce_abstractionneeds to be enabled.
use feather_tui as tui;
use std::collections::HashSet;
fn container() -> tui::FtuiResult<(tui::Container, u16, u16, u16)> {
let (mut add_task_id, mut delete_task_id, mut show_task_id) = (0u16, 0u16, 0u16);
Ok((tui::ContainerBuilder::new()
.header("TODO App")?
.option_id("Add Task", None, &mut add_task_id)?
.option_id("Delete Task", None, &mut delete_task_id)?
.separator_normal(tui::SeparatorStyle::Thin)
.option_id("Show List", None, &mut show_task_id)?
.text(
"w/s/e to navigate, q to quit",
tui::TextFlags::COLOR_YELLOW_BACK |
tui::TextFlags::ALIGN_RIGHT | tui::TextFlags::ALIGN_BOTTOM)?
.selector_no_triggers()
.build(), add_task_id, delete_task_id, show_task_id))
}
fn list() -> tui::FtuiResult<tui::List> {
Ok(tui::ListBuilder::new()
.header("Task List")?
.default_flags(tui::TextFlags::COLOR_MAGENTA)?
.number()
.build())
}
fn task_list_loop(
list: &mut tui::List, renderer: &mut tui::Renderer
) -> tui::FtuiResult<()> {
let mut should_update = true;
loop {
if should_update {
renderer.clear();
renderer.render_list(list)?;
renderer.draw()?;
}
should_update = match tui::key_char()? {
Some('w') => list.scroll_up(),
Some('s') => list.scroll_down(),
Some('q') => break,
_ => false,
}
}
Ok(())
}
fn show_error(message: &str) -> tui::FtuiResult<()> {
tui::ContainerBuilder::new()
.text(
"Error!",
tui::TextFlags::ALIGN_MIDDLE | tui::TextFlags::COLOR_RED_BACK)?
.text(message, None)?
.text(
"Press any key to continue.",
tui::TextFlags::ALIGN_BOTTOM
| tui::TextFlags::ALIGN_RIGHT | tui::TextFlags::COLOR_YELLOW_BACK)?
.build()
.draw_fullscreen()?;
loop {
if let Some(_) = tui::key()? {
break;
}
}
Ok(())
}
fn main() -> tui::FtuiResult<()> {
let (mut container, add_task_id, delete_task_id, show_task_id) = container()?;
let mut task_list = list()?;
let mut task_names: HashSet<String> = HashSet::new();
let mut renderer = tui::Renderer::fullscreen()?;
let mut should_update = true;
tui::ready()?;
loop {
if container.option_mut(add_task_id)?.is_selc() {
let task_name = tui::line("Task Name")?.trim().to_owned();
if !task_names.contains(&task_name) {
task_list.add(&task_name, None)?;
task_names.insert(task_name);
} else {
show_error("Task already exist.")?;
}
}
if container.option_mut(delete_task_id)?.is_selc() {
match tui::line("Task Number")?.trim().parse::<usize>() {
Ok(task_number) => {
match task_list.at(task_number - 1) {
Ok(task) => {
task_names.remove(task.label());
task_list.remove(task_number - 1).unwrap();
},
Err(_) => show_error("Invalid task number.")?
}
},
Err(_) => show_error("Pls enter a valid number.")?,
}
}
if container.option_mut(show_task_id)?.is_selc() {
task_list_loop(&mut task_list, &mut renderer)?;
}
if should_update {
renderer.simple_draw(&mut container)?;
}
should_update = match tui::key_char()? {
Some('w') => container.selector_up()?,
Some('s') => container.selector_down()?,
Some('e') => container.selector_select()?,
Some('q') => break,
_ => false,
}
}
tui::unready()?;
Ok(())
}Here’s an example of how to use Triggers and Callbacks to create a simple static UI.
When running the code, always run in release mode if possible.
For this example, the optional featurereduce_abstractionneeds to be enabled.
use feather_tui as tui;
use std::thread;
use std::time::Duration;
// Trigger function for 'w' key (move up)
tui::trg_new_trigger_func!(up_trig_func, key_char, {
match tui::trigger::cast_arg::<Option<char>>(key_char)? {
Some(c) => Ok(*c == 'w'),
None => Ok(false),
}
});
// Trigger function for 's' key (move down)
tui::trg_new_trigger_func!(down_trig_func, key_char, {
match tui::trigger::cast_arg::<Option<char>>(key_char)? {
Some(c) => Ok(*c == 's'),
None => Ok(false),
}
});
// Trigger function for 'e' key (select)
tui::trg_new_trigger_func!(selc_trig_func, key_char, {
match tui::trigger::cast_arg::<Option<char>>(key_char)? {
Some(c) => Ok(*c == 'e'),
None => Ok(false),
}
});
// Trigger function for 'q' key (quit)
tui::trg_new_trigger_func!(quit_trig_func, key_char, {
match tui::trigger::cast_arg::<Option<char>>(key_char)? {
Some(c) => Ok(*c == 'q'),
None => Ok(false),
}
});
// Callback function triggered when a menu option is selected
tui::cbk_new_callback_func!(callback_func, argument, {
let number = tui::callback::cast_arg::<u32>(argument)?.to_string();
// Show a simple confirmation message
tui::ContainerBuilder::new()
.header("Callback Trigger")?
.text(
&format!("Option {} selected", number),
tui::TextFlags::COLOR_RED_BACK,
)?
.build()
.draw_fullscreen()?;
// Wait 1 second before returning
thread::sleep(Duration::from_secs(1));
Ok(())
});
fn tui_main() -> tui::FtuiResult<()> {
// Read initial key press
let mut key_char = tui::key_char()?;
// Create a container with header, options, text, and a selector
let mut container = tui::ContainerBuilder::new()
.header("Main Menu")?
.option("Option1", tui::Callback::new(callback_func, 1u32))?
.option("Option2", tui::Callback::new(callback_func, 2u32))?
.separator_normal(tui::SeparatorStyle::Thin)
.text(
"The line above is called a separator.",
tui::TextFlags::ALIGN_MIDDLE | tui::TextFlags::COLOR_RED,
)?
.text(
"w, s, e to navigate / q to quit",
tui::TextFlags::COLOR_YELLOW_BACK
| tui::TextFlags::ALIGN_BOTTOM
| tui::TextFlags::ALIGN_RIGHT,
)?
.selector(
tui::Trigger::new(up_trig_func, key_char),
tui::Trigger::new(down_trig_func, key_char),
tui::Trigger::new(selc_trig_func, key_char),
)
.build();
let mut renderer = tui::Renderer::fullscreen()?;
let mut quit_trig = tui::Trigger::new(quit_trig_func, key_char);
let mut should_update = true;
tui::ready()?; // Prepare the renderer
// Main loop
loop {
// Get next key press
key_char = tui::key_char()?;
// Update trigger arguments
container.selector_mut()?.update_trig_arg(key_char, key_char, key_char)?;
quit_trig.update_arg(key_char);
// Quit if 'q' is pressed
if quit_trig.check()? {
break;
}
// Redraw UI if needed
if should_update {
renderer.clear();
renderer.render(&mut container)?;
renderer.draw()?;
}
// Check for any updates (like selection)
should_update = container.looper()?;
}
tui::unready()?; // Clean up renderer
Ok(())
}
fn main() -> tui::FtuiResult<()> {
match tui_main() {
Err(e) => {
tui::unready()?;
if e == tui::FtuiError::RendererContainerTooBig {
eprintln!("Terminal UI: Please increase your terminal size.");
} else {
eprintln!("{}", e);
}
return Err(e);
}
_ => return Ok(()),
}
}Last Updated: November 8, 2025 • Maintainer: Taj Borthwick