Skip to content

Cookbook ​

Self-contained patterns for interface problems that come up in most terminal applications. Each recipe is a complete program from the repository's example/ directory — it compiles as part of the project's example build, so you can paste it into a project and adapt it.

The recipes rely on three RTXUI idioms worth internalizing:

  • State lives in C++ members; the template reads it. Templates interpolate bound names — they do not evaluate expressions, contain statements, or perform assignments.
  • Logic is a computed method. A const method returns a class string or a boolean; the template binds it with class="{method_name}" or condition="{method_name}". Ternaries and comparisons live in C++.
  • Event handlers name a bound method, optionally with one argument: onclick="Select(home)" invokes void Select(std::string) with "home".

Tabbed navigation ​

Track the active tab in a state variable. Buttons call a single parameterized method; each button's class and each pane's visibility come from computed methods. The <if>/<elif>/<else> chain shows one pane at a time.

Fullscreen Demo
cpp
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Recipe: tabs built by hand.
//
// The same result as the built-in <tabs>, assembled from plain elements and a
// bound string -- useful when you want full control of the markup.
#include <rtxui/rtxui.hpp>
#include <string>
#include <string_view>

using namespace rtxui;

// Cookbook recipe: tabbed navigation built by hand.
//
// The active tab lives in a state variable. Buttons call a single
// parameterized method; each button's class and each pane's visibility come
// from computed methods. The <if>/<elif>/<else> chain shows one pane at a
// time.
class TabbedApp : public Component<TabbedApp> {
 public:
  std::string active_tab = "home";

  void Select(std::string tab) { active_tab = tab; }

  bool is_home() const { return active_tab == "home"; }
  bool is_settings() const { return active_tab == "settings"; }
  bool is_about() const { return active_tab == "about"; }

  std::string home_class() const { return TabClass("home"); }
  std::string settings_class() const { return TabClass("settings"); }
  std::string about_class() const { return TabClass("about"); }

  TabbedApp() {
    Bind(active_tab);
    Bind(Select);
    Bind(is_home);
    Bind(is_settings);
    Bind(is_about);
    Bind(home_class);
    Bind(settings_class);
    Bind(about_class);
  }

  std::string_view view = R"html(
    <div class="container">

      <div class="tabs-header">
        <button class="{home_class}" onclick="Select(home)">Home</button>
        <button class="{settings_class}" onclick="Select(settings)">Settings</button>
        <button class="{about_class}" onclick="Select(about)">About</button>
      </div>

      <div class="tab-content">
        <div if="{is_home}">Welcome to the home screen.</div>
        <div if="{is_settings}">Settings go here.</div>
        <div if="{is_about}">About this application.</div>
      </div>
    </div>

    <style>
      .container {
        display: flex;
        flex-direction: column;
        max-width: 50;
      }
      .tabs-header {
        display: flex;
        flex-direction: row;
        width: 100%;
        border-bottom: solid;
        border-color: #334155;
      }
      .tab-btn {
        padding: 0 2;
        cursor: pointer;
        background-color: black;
      }
      .tab-btn.active {
        background-color: #1e3a8a;
        color: #fff;
        font-weight: bold;
      }
      .tab-content {
        padding: 1;
        min-height: 5;
      }
    </style>
  )html";

 private:
  std::string TabClass(const std::string& tab) const {
    return active_tab == tab ? "tab-btn active" : "tab-btn";
  }
};

int main() {
  auto app = Ref<TabbedApp>::New();
  Screen screen(app);
  screen.Loop();
  return 0;
}

For most tabbed interfaces the built-in <tabs>/<tab-pane> components are enough; this recipe is the underlying pattern for when you need full control over the header.


Background work without freezing the UI ​

Callbacks run on the main thread, so blocking inside one (network requests, large file reads) freezes rendering. Run the work on a std::thread and post the result back to the main loop with rtxui::PostTask(...), which may be called from any thread. State mutations posted this way are picked up by the normal digest cycle; a task posted after the application exited is dropped.

Fullscreen Demo
cpp
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Recipe: updating the UI from a worker thread.
//
// The UI is single-threaded. A background thread must hand results back through
// the task runner, which applies them between frames.
#include <chrono>
#include <rtxui/rtxui.hpp>
#include <string>
#include <string_view>
#include <thread>

using namespace rtxui;

// Cookbook recipe: background work without freezing the UI.
//
// Callbacks run on the main thread, so blocking inside one freezes rendering.
// Run the work on a std::thread and hand the result back to the main loop
// with rtxui::PostTask, which may be called from any thread.
class AsyncApp : public Component<AsyncApp> {
 public:
  std::string status = "Idle";
  bool is_loading = false;

  void StartBackgroundTask() {
    if (is_loading) {
      return;
    }
    status = "Fetching data...";
    is_loading = true;

    std::thread([this]() {
      // Stand-in for real work: a network call, a database query, ...
      std::this_thread::sleep_for(std::chrono::seconds(2));

      // Only the main thread may touch component state. PostTask schedules
      // this lambda on the UI loop and wakes it; if the app has already
      // exited, it is dropped rather than run against a destroyed component.
      PostTask([this]() {
        status = "Loaded 42 items.";
        is_loading = false;
      });
    }).detach();
  }

  AsyncApp() {
    Bind(status);
    Bind(is_loading);
    Bind(StartBackgroundTask);
  }

  std::string_view view = R"html(
    <div class="panel">
      <div>Status: <strong>{status}</strong></div>
      <if condition="{is_loading}">
        <div class="loader">Processing...</div>
      </if>
      <else>
        <button onclick="StartBackgroundTask">Trigger load</button>
      </else>
    </div>

    <style>
      .panel {
        padding: 1;
        border: solid;
        width: 40;
      }
      .loader {
        color: #f59e0b;
      }
    </style>
  )html";
};

int main() {
  auto app = Ref<AsyncApp>::New();
  Screen screen(app);
  screen.Loop();
  return 0;
}

A detached thread must not outlive the component it captures. In real applications, join or signal worker threads before the component is destroyed, or route results through state that survives the component.


Confirmation dialog ​

The built-in <dialog> element renders centered above the rest of the interface with a dimmed backdrop, and closes on Escape. Bind its open attribute to a boolean and flip that boolean from methods.

Fullscreen Demo
cpp
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Recipe: a confirmation dialog.
//
// The built-in <dialog> renders centered above the interface with a dimmed
// backdrop and closes on Escape. Bind its `open` attribute to a bool.
#include <rtxui/rtxui.hpp>
#include <string>
#include <string_view>

using namespace rtxui;

// Cookbook recipe: confirmation dialog.
//
// The built-in <dialog> element renders centered above the rest of the
// interface with a dimmed backdrop, and closes on Escape. Bind its `open`
// attribute to a boolean and flip that boolean from methods.
class ConfirmApp : public Component<ConfirmApp> {
 public:
  bool confirming = false;
  std::string result = "No action taken.";

  void Ask() { confirming = true; }
  void Confirm() {
    result = "Action confirmed.";
    confirming = false;
  }
  void Cancel() {
    result = "Action cancelled.";
    confirming = false;
  }

  ConfirmApp() {
    Bind(confirming);
    Bind(result);
    Bind(Ask);
    Bind(Confirm);
    Bind(Cancel);
  }

  std::string_view view = R"html(
    <div class="main">
      <div>{result}</div>
      <button onclick="Ask">Delete everything</button>

      <dialog open="{confirming}" title="Confirm">
        <p>Are you sure? This cannot be undone.</p>
        <div class="actions">
          <button class="danger" onclick="Confirm">Confirm</button>
          <button onclick="Cancel">Cancel</button>
        </div>
      </dialog>
    </div>

    <style>
      .main {
        padding: 1;
        width: 60;
        height: 10;
      }
      .actions {
        display: flex;
        flex-direction: row;
        justify-content: flex-end;
        gap: 1;
        margin-top: 1;
      }
      .danger {
        background-color: #ef4444;
        color: #fff;
      }
    </style>
  )html";
};

int main() {
  auto app = Ref<ConfirmApp>::New();
  Screen screen(app);
  screen.SetInline(false);
  screen.Loop();
  return 0;
}

To build an overlay by hand instead — a dropdown, a toast, a context menu — use position: absolute (or fixed) with z-index inside a position: relative ancestor, and gate it behind an <if>. The positioning guide covers the mechanics.


Command palette ​

A palette is a <dialog> holding an <input> and the list of commands matching what is typed. Three pieces make it work:

  • The input sits inside an <if> on the dialog's flag and carries autofocus, so it appears, and takes the focus, each time the palette opens.
  • A Digest() override recomputes the matches from the bound query before each render, and marks the highlighted one with a class.
  • An OnEvent() override handles the keys the focused input leaves alone: Ctrl+P to open, Up and Down to move the highlight, Enter to run it. Escape closes the dialog by itself.
Fullscreen Demo
cpp
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Recipe: a command palette.
//
// Ctrl+P opens a <dialog> holding an <input> that takes the focus through
// `autofocus`. Each keystroke filters the commands by fuzzy match; Up and Down
// move the highlight, Enter runs the highlighted command, Escape closes.
//
// Try it: press Ctrl+P (or click the button), type "th", then press Enter.
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <rtxui/rtxui.hpp>
#include <string>
#include <string_view>
#include <vector>

using namespace rtxui;

struct Match {
  std::string name;
  std::string row_class;
  bool operator==(const Match&) const = default;
};

// What a template reads of a Match: {match.name} and {match.row_class}.
void Fields(const Match& match, FieldSink& out) {
  out("name", match.name);
  out("row_class", match.row_class);
}

class CommandPalette : public Component<CommandPalette> {
 public:
  const std::vector<std::string> commands = {
      "Open file",     "Save file",           "Close tab",
      "Toggle theme",  "Toggle line numbers", "Go to line",
      "Find in files", "Rename symbol",       "Quit",
  };

  bool open = false;
  std::string query;
  std::vector<Match> matches;
  int selected = 0;
  std::string last = "nothing yet";

  void Open() {
    open = true;
    query.clear();
    selected = 0;
  }

  // Runs the command at `index` in `matches`; from a click, it arrives as
  // text.
  void Run(std::string index) {
    const size_t i = index.empty() ? 0 : std::stoul(index);
    if (i < matches.size()) {
      last = matches[i].name;
    }
    open = false;
  }

  CommandPalette() {
    Bind(open);
    Bind(query);
    Bind(last);
    Bind(Open);
    Bind(Run);
    Bind(matches);
  }

  // Whether `pattern`'s letters appear in `text` in order, ignoring case:
  // "tln" matches "Toggle line numbers".
  static bool FuzzyMatch(std::string_view pattern, std::string_view text) {
    size_t at = 0;
    for (const char c : pattern) {
      const auto lower = [](char x) {
        return std::tolower(static_cast<unsigned char>(x));
      };
      while (at < text.size() && lower(text[at]) != lower(c)) {
        ++at;
      }
      if (at == text.size()) {
        return false;
      }
      ++at;
    }
    return true;
  }

  // Recomputes the matches from the query before every render.
  bool Digest() override {
    matches.clear();
    for (const auto& command : commands) {
      if (FuzzyMatch(query, command)) {
        matches.push_back({command, ""});
      }
    }
    const int count = static_cast<int>(matches.size());
    selected = count == 0 ? 0 : std::clamp(selected, 0, count - 1);
    if (count > 0) {
      matches[selected].row_class = "selected";
    }
    return Component<CommandPalette>::Digest();
  }

  // Keys the focused input does not use reach here.
  bool OnEvent(Event event) override {
    if (event == Event::CtrlP()) {
      Open();
      return true;
    }
    if (open && event == Event::ArrowDown()) {
      selected++;
      return true;
    }
    if (open && event == Event::ArrowUp()) {
      selected = std::max(0, selected - 1);
      return true;
    }
    if (open && event == Event::Return()) {
      Run(std::to_string(selected));
      return true;
    }
    return Component<CommandPalette>::OnEvent(event);
  }

  std::string_view view = R"html(
    <div class="main">
      <p>Last command: {last}</p>
      <button onclick="Open">Command palette (Ctrl+P)</button>

      <dialog open="{open}" title="Run a command">
        <if condition="{open}">
          <input value="{query}" placeholder="Type to filter" autofocus=""/>
        </if>
        <div class="list">
          <for each="{matches}" as="match">
            <div class="{match.row_class}" onclick="Run({$index})">{match.name}</div>
          </for>
        </div>
      </dialog>
    </div>

    <style>
      .main {
        padding: 1;
        width: 60;
        height: 16;
      }
      .list {
        margin-top: 1;
        height: 9;
      }
      .selected {
        background-color: rgb(37, 99, 235);
        color: white;
      }
    </style>
  )html";
};

int main() {
  auto app = Ref<CommandPalette>::New();
  Screen screen(app);
  screen.SetInline(false);
  screen.Loop();
  return 0;
}