KontraKontra
    DocumentationAbout
Docs
  • Getting Started
  • Installation
  • Hello World
  • Ansi
  • Todo App
  • Event Handling
  • Style Builder
  • Components
  • Border
  • Button
  • Checkbox
  • Flex
  • Input
  • List
  • Radio Buttons
  • Tabs
  • Text
AnsiEvent Handling
KontraKontra

✅ Building a Todo App in Kontra TUI

This example showcases how to create a dynamic, fully interactive Todo app in your terminal using Kontra TUI.

You'll learn:

  • State-driven rendering
  • Abstract keyboard and mouse event handling
  • Layouts with buttons, lists, and input
  • Managing component focus and updates

Have a look at what we are gonna build!

Todo App Example

📦 Step 1: Setup Your App Entry Point

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.


🧠 Step 2: Define State and Components

We’ll maintain a list of tasks and use List, InputBox, Button, etc., as building blocks.


⚙️ Step 3: Define Task Actions

These are the actions triggered by buttons or keys. We declare them first so they can be passed into our UI update logic.


🔁 Step 4: Render the UI Based on State

We’ll define update_ui() that rebuilds the component list every time the state changes.


🧩 Step 5: Implement the Actions

These functions modify the application state and then trigger a UI redraw by calling update_ui().

Call it once to draw the initial state:


🖼️ Step 6: Setup Layout & Screen

Wrap your main layout component with borders and padding for a polished look.


🧵 Step 7: Handle Input Events

This is the controller logic. It translates abstract InputEvents into application actions based on the current AppMode.


✅ Features Summary

  • Uses List, Text, InputBox, Button, Flex, and Border.
  • Fully state-driven layout ensures UI is always in sync with data.
  • Abstracted event system supports mouse and keyboard (including arrows).
  • Manages Navigating and Editing modes for a clean user experience.
  • Works well with any terminal size.

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;
          }
      }
  });