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.
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 InputEvents 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! 🎉
// 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
};
// --- 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;
}
}
});