Skip to content

Instantly share code, notes, and snippets.

@darvil82
Last active March 21, 2025 23:16
Show Gist options
  • Save darvil82/0d4ba1cfa8c5b089057ee3ce46deca80 to your computer and use it in GitHub Desktop.
Save darvil82/0d4ba1cfa8c5b089057ee3ce46deca80 to your computer and use it in GitHub Desktop.
C++20 Terminal menu helper.
#pragma once
#include <initializer_list>
#include <functional>
#include <cinttypes>
#include <variant>
#include <vector>
#include <iostream>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <iterator>
namespace menu {
class Menu;
class MenuEntry {
using SimpleFunc = std::function<void()>;
using MenuRefFunc = std::function<void(Menu&)>;
using FuncVariant = std::variant<SimpleFunc, MenuRefFunc>;
friend Menu;
FuncVariant func;
std::optional<uint8_t> num;
std::string info;
public:
/**
* @param num The number of the entry the user must input to select.
* @param info The information of the entry that will be displayed.
* @param func The function to execute when the entry is selected.
*/
MenuEntry(uint8_t num, std::string&& info, FuncVariant&& func)
: func{std::move(func)}, num{num}, info{std::move(info)} {}
/**
* @param info The information of the entry that will be displayed.
* @param func The function to execute when the entry is selected.
*/
MenuEntry(std::string&& info, FuncVariant&& func)
: func{std::move(func)}, info{std::move(info)} {}
/**
* Calls the function of the entry.
* If the function is a MenuRefFunc, it will pass the menu as an argument.
* @param menu The menu that contains the entry.
*/
void operator()(Menu& menu) const {
if (std::holds_alternative<MenuRefFunc>(func)) {
std::get<MenuRefFunc>(func)(menu);
return;
}
std::get<SimpleFunc>(func)();
}
/**
* Returns the number of the entry. Throws an exception if the number is not set.
* @return The number of the entry.
*/
uint8_t get_num() const {
if (!this->num)
throw std::runtime_error("Entry number is not set.");
return *this->num;
}
std::string to_string() const {
std::ostringstream buff;
buff << " " << static_cast<int>(*this->num) << ": " << this->info;
return buff.str();
}
};
class Menu {
bool is_asking = false;
std::optional<uint8_t> next_entry_num;
std::vector<MenuEntry> entries;
std::string title = "Select an option:";
std::function<std::string(const std::string&)> fail_msg = [](const auto& value) {
return "Option '" + value + "' does not exist.";
};
/**
* Adds an entry to the menu. Properly sets the entry number if it is not set.
* Also sets Menu::next_entry_num properly.
* @param entry The entry to add.
*/
void add_entry(MenuEntry&& entry) {
if (!this->next_entry_num) {
this->next_entry_num = (entry.num ? *entry.num : 1);
entry.num = *this->next_entry_num;
} else if (entry.num) {
if (entry.num <= this->next_entry_num)
throw std::invalid_argument("Entry number must be greater than the last one.");
this->next_entry_num = *entry.num;
} else {
const auto v = &*this->next_entry_num;
entry.num = ++(*v);
}
this->entries.push_back(std::move(entry));
}
template<std::ranges::range T>
void init_from_iterable(T&& iterable) {
auto size = std::distance(std::begin(iterable), std::end(iterable));
if (size == 0)
throw std::invalid_argument("Menu must have at least one entry.");
this->entries.reserve(size);
for (auto entry : iterable)
this->add_entry(std::move(entry));
}
public:
/**
* @param iterable An iterable containing the entries of the menu.
*/
template<std::ranges::range T>
explicit Menu(T&& iterable) {
this->init_from_iterable(std::forward<T>(iterable));
}
/**
* @param entries The entries of the menu.
*/
Menu(std::initializer_list<MenuEntry> entries) {
this->init_from_iterable(entries);
}
/**
* Sets the title of the menu.
* @param title The title of the menu.
*/
Menu& with_title(std::string&& title) {
this->title = std::move(title);
return *this;
}
/**
* Sets the function that will be called when the user inputs an invalid option.
* @param supplier A function that receives the invalid input from the user and returns the message to display.
*/
Menu& with_fail_msg(decltype(fail_msg)&& supplier) {
this->fail_msg = std::move(supplier);
return *this;
}
/**
* Prints the menu to the console.
*/
void show() const {
std::ostringstream buff;
buff << this->title << "\n";
for (const auto& entry : this->entries) {
buff << entry.to_string() << "\n";
}
std::cout << buff.str() << std::flush;
}
/**
* Asks the user to select an option.
* @return The selected MenuEntry or std::nullopt if the option does not exist.
*/
std::optional<MenuEntry> ask() {
std::string input;
std::getline(std::cin, input);
uint16_t option;
std::istringstream(input) >> option;
for (auto entry : this->entries) {
if (entry.num == option)
return entry;
}
std::cout << this->fail_msg(input) << std::endl;
return {};
}
/**
* Asks the user to select an option until a valid one is selected.
* @return The selected MenuEntry.
*/
MenuEntry ask_until_valid() {
std::optional<MenuEntry> entry;
this->show();
while (!entry)
entry = this->ask();
return *entry;
}
/**
* Asks the user to select an option until Menu::stop_asking is called.
*/
void ask_loop() {
this->is_asking = true;
this->show();
while (true) {
auto entry = this->ask();
if (!entry) continue;
(*entry)(*this);
if (!this->is_asking)
break;
this->show();
}
}
/**
* Stops the menu from asking if Menu::ask_loop was called.
*/
void stop_asking() {
this->is_asking = false;
}
};
/**
* Contains functions that create MenuEntry objects.
*/
namespace entries {
/**
* A MenuEntry that stops the menu from asking when selected. (Calls Menu::stop)
* @param info The information of the entry that will be displayed. Default: "Exit."
*/
constexpr MenuEntry exit(std::string&& info = "Exit.") {
return {std::move(info), [](Menu& m) { m.stop_asking(); }};
}
}
/**
* Creates a menu and asks the user to select one of the options.
* Adds an exit option to the menu.
* @param entries The entries of the menu.
*/
inline void menu_ask(std::initializer_list<MenuEntry>&& entries) {
std::vector<MenuEntry> vec;
vec.reserve(entries.size());
vec.insert(vec.end(), entries.begin(), entries.end());
vec.push_back(entries::exit());
Menu(std::move(vec)).ask_loop();
}
}
#include <iostream>
#include "menu.hpp"
// more code here
int main() {
// blocking. will wait for user to pick an option
menu::menu_ask({
{ "Insert person.", ask_and_insert },
{ "Show people.", show_people },
{ "Find person.", find_person },
{ "Thing.", []() { std::cout << "hi\n"; } }
});
// ----------------------- or -----------------------
menu::Menu my_menu({
{ "Thing.", thing },
{ "Another thing.", another },
menu::entries::exit()
});
// -- more code here --
my_menu.ask_until_valid()(my_menu);
// ----------------------- or -----------------------
if (auto result = my_menu.ask()) {
std::cout << "You selected: "
<< static_cast<uint16_t>(result->get_num())
<< ". Running." << std::endl;
(*result)(my_menu);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment