Skip to content

Examples

Taj borthwick edited this page Nov 8, 2025 · 6 revisions

Caution

Examples are not yet updated for v5.0.0-rc.1.

Counter Example

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 feature reduce_abstraction needs 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(())
}

Simple Todo

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 feature reduce_abstraction needs 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(())
}

Triggers And Callbacks

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 feature reduce_abstraction needs 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(()),
    }
}

Clone this wiki locally