Instantly share code, notes, and snippets.
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save darvil82/0d4ba1cfa8c5b089057ee3ce46deca80 to your computer and use it in GitHub Desktop.
C++20 Terminal menu helper.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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