This example showcases how to create a dynamic, fully interactive Todo app in your terminal using Kontra TUI.
You'll learn:
Let’s start with the structure.
// KONTRA TUI: todo_app_showcase.cpp
#include "../include/kontra.hpp"
#include "../include/core/utils.hpp"
#include <vector>
#include <string>
#include <memory>
enum class AppMode {
Navigating,
Editing
};
This AppMode
enum helps us toggle between navigation and editing state.
We’ll maintain a list of tasks and use List
, InputBox
, Button
, etc., as building blocks.
These are the actions triggered by buttons or keys. We declare them first so they can be passed into our UI update logic.
We’ll define update_ui()
that rebuilds the component list every time the state changes.
These functions modify the application state and then trigger a UI redraw by calling update_ui()
.
Call it once to draw the initial state:
Wrap your main layout component with borders and padding for a polished look.
This is the controller logic. It translates abstract InputEvent
s into application actions based on the current AppMode
.
List
, Text
, InputBox
, Button
, Flex
, and Border
.Navigating
and Editing
modes for a clean user experience.Happy building with Kontra TUI! 🎉
// --- State ---
std::vector<std::string> tasks;
int selected_task = -1;
AppMode current_mode = AppMode::Navigating;
// --- Components ---
auto input_box = std::make_shared<InputBox>();
input_box->set_label("Enter todo!");
auto main_list = std::make_shared<List>();
std::vector<std::shared_ptr<Text>> task_text_components;
std::vector<std::shared_ptr<Button>> buttons;
// Declare handlers
std::function<void()> add_task;
std::function<void()> remove_task;
std::function<void()> clear_tasks;
// UI Update Logic
auto update_ui = [&]() {
input_box->set_active(current_mode == AppMode::Editing);
main_list->clear();
task_text_components.clear();
buttons.clear();
auto header_text = std::make_shared<Text>([&]() {
return (current_mode == AppMode::Editing)
? "EDITING | Enter: Add | Esc: Cancel"
: "NAVIGATION | i:Insert | Arrows:Nav | d:Del | Mouse";
}, TextStyle(ansi::FG_WHITE, ansi::BG_DEFAULT, true));
// --- REUSABLE BUTTON STYLES ---
auto add_style = ButtonStyleBuilder()
.set_inactive_style(TextStyleBuilder().set_color(ansi::FG_BLACK).set_background_color(ansi::BG_GREEN).set_bold(true).build())
.set_active_style(TextStyleBuilder().set_color(ansi::FG_WHITE).set_background_color(ansi::BG_BRIGHT_GREEN).set_bold(true).build())
.build();
auto remove_style = ButtonStyleBuilder()
.set_inactive_style(TextStyleBuilder().set_color(ansi::FG_WHITE).set_background_color(ansi::BG_RED).set_bold(true).build())
.set_active_style(TextStyleBuilder().set_color(ansi::FG_WHITE).set_background_color(ansi::BG_BRIGHT_RED).set_bold(true).build())
.build();
auto clear_style = ButtonStyleBuilder()
.set_inactive_style(TextStyleBuilder().set_color(ansi::FG_WHITE).set_background_color(ansi::BG_BRIGHT_BLACK).set_bold(true).build())
.set_active_style(TextStyleBuilder().set_color(ansi::FG_BLACK).set_background_color(ansi::BG_WHITE).set_bold(true).build())
.build();
// --- Create buttons and apply the new styles ---
auto add_button = std::make_shared<Button>(" Add ", add_task, add_style);
auto remove_button = std::make_shared<Button>(" Remove ", remove_task, remove_style);
auto clear_button = std::make_shared<Button>(" Clear All ", clear_tasks, clear_style);
buttons = { add_button, remove_button, clear_button };
auto button_bar = std::make_shared<Flex>(FlexDirection::Row, add_button, remove_button, clear_button);
button_bar->set_gap(1);
main_list->add(header_text);
main_list->add(input_box);
main_list->add(button_bar);
for (size_t i = 0; i < tasks.size(); ++i) {
auto style = (selected_task == static_cast<int>(i))
? TextStyle(ansi::FG_BLACK, ansi::BG_BRIGHT_WHITE, true)
: TextStyle(ansi::FG_WHITE, ansi::BG_DEFAULT, false);
auto txt = std::make_shared<Text>(tasks[i], style);
main_list->add(txt);
task_text_components.push_back(txt);
}
};
// Add Task
add_task = [&]() {
if (!input_box->get_text().empty()) {
tasks.push_back(input_box->get_text());
input_box->set_text("");
selected_task = tasks.size() - 1;
current_mode = AppMode::Navigating;
update_ui();
}
};
// Remove Selected Task
remove_task = [&]() {
if (selected_task >= 0 && selected_task < (int)tasks.size()) {
tasks.erase(tasks.begin() + selected_task);
selected_task = tasks.empty() ? -1 : std::min(selected_task, (int)tasks.size() - 1);
update_ui();
}
};
// Clear All
clear_tasks = [&]() {
tasks.clear();
selected_task = -1;
update_ui();
};
update_ui();
// Layout and screen
main_list->set_gap(1);
auto screen = std::make_shared<Screen>(
chain(std::make_shared<Border>(
main_list,
BorderStyleBuilder()
.set_title("tOdO!!!")
.set_characters(BorderPreset::DOUBLE)
.set_title_alignment(TitleAlignment::Center)
.build()),
[](Border &b)
{ b.set_padding(1); }));
kontra::run(screen, [&](const InputEvent& event) {
if (current_mode == AppMode::Editing) {
// Handle input while the user is typing in the input box
switch (event.type) {
case EventType::KEY_ENTER:
add_task();
break;
case EventType::KEY_ESCAPE:
current_mode = AppMode::Navigating;
update_ui();
break;
default:
// Pass all other events (KEY_PRESS, BACKSPACE, etc.) to the input box
input_box->handle_event(event);
break;
}
} else { // Navigation Mode
switch (event.type) {
case EventType::KEY_DOWN:
if (!tasks.empty() && selected_task < (int)tasks.size() - 1) {
selected_task++;
update_ui();
}
break;
case EventType::KEY_UP:
if (selected_task > 0) {
selected_task--;
update_ui();
}
break;
case EventType::KEY_PRESS:
if (event.key == 'i') {
current_mode = AppMode::Editing;
update_ui();
} else if (event.key == 'd') {
remove_task();
}
break;
case EventType::MOUSE_PRESS:
for(const auto& btn : buttons) {
if (btn->contains(event.mouse_x, event.mouse_y)) {
btn->click();
return;
}
}
for (size_t i = 0; i < task_text_components.size(); ++i) {
if (task_text_components[i]->contains(event.mouse_x, event.mouse_y)) {
selected_task = i;
update_ui();
return;
}
}
if (input_box->contains(event.mouse_x, event.mouse_y)) {
current_mode = AppMode::Editing;
update_ui();
}
break;
case EventType::MOUSE_SCROLL_UP: main_list->scroll_up(); break;
case EventType::MOUSE_SCROLL_DOWN: main_list->scroll_down(); break;
default: break;
}
}
});