first working
This commit is contained in:
parent
479ee0bb75
commit
f279af99a7
20
README.md
20
README.md
@ -1,2 +1,22 @@
|
|||||||
# poomer-ftxui-modal_select
|
# poomer-ftxui-modal_select
|
||||||
Example prototype demonstrating ftxui modal interface
|
Example prototype demonstrating ftxui modal interface
|
||||||
|
|
||||||
|
This program is a simple example of how to use the ftxui library to create a modal dialog.
|
||||||
|
- It allows the user to select an address from a list of addresses in a modal dialog.
|
||||||
|
- It allows the user to select a file from a list of files in a modal dialog.
|
||||||
|
- It allows the user to enter an address manually.
|
||||||
|
- It creates a JSON file to store the addresses.
|
||||||
|
|
||||||
|
FTXUI is a library that allows you to create text-based user interfaces.
|
||||||
|
It is a lightweight library that is easy to use and understand.
|
||||||
|
It is a modern C++ library that provides a functional approach to building terminal applications.
|
||||||
|
|
||||||
|
ftxui is
|
||||||
|
|
||||||
|
Build
|
||||||
|
|
||||||
|
Build https://github.com/ArthurSonzogni/FTXUI relative to this repo
|
||||||
|
|
||||||
|
```
|
||||||
|
g++ poomer-fxtui-modal_select.cpp -o poomer-fxtui-modal_select -std=c++17 -I../FTXUI/include/ -L../FTXUI/build -lftxui-component -lftxui-dom -lftxui-screen -I.
|
||||||
|
```
|
||||||
452
poomer-fxtui-modal_select.cpp
Normal file
452
poomer-fxtui-modal_select.cpp
Normal file
@ -0,0 +1,452 @@
|
|||||||
|
// This program is a simple example of how to use the ftxui library to create a modal dialog.
|
||||||
|
// It allows the user to select an address from a list of addresses in a modal dialog.
|
||||||
|
// It allows the user to select a file from a list of files in a modal dialog.
|
||||||
|
// It allows the user to enter an address manually.
|
||||||
|
// It creates a JSON file to store the addresses.
|
||||||
|
|
||||||
|
// FTXUI is a library that allows you to create text-based user interfaces.
|
||||||
|
// It is a lightweight library that is easy to use and understand.
|
||||||
|
// It is a modern C++ library that provides a functional approach to building terminal applications.
|
||||||
|
|
||||||
|
#include <memory> // for allocator, shared_ptr, __shared_ptr_access
|
||||||
|
#include <string> // for string, basic_string, char_traits, operator+
|
||||||
|
#include <vector> // for vector
|
||||||
|
#include <regex> // for regex validation
|
||||||
|
#include <sstream> // for string stream
|
||||||
|
#include <iostream>// for cout
|
||||||
|
#include <fstream> // for ifstream, ofstream
|
||||||
|
#include <json.hpp>// for nlohmann/json
|
||||||
|
#include <thread> // for std::this_thread
|
||||||
|
#include <chrono> // for std::chrono
|
||||||
|
|
||||||
|
#include "ftxui/component/captured_mouse.hpp" // for ftxui
|
||||||
|
#include "ftxui/component/component.hpp" // for Button, Renderer, Horizontal, Tab
|
||||||
|
#include "ftxui/component/component_base.hpp" // for ComponentBase
|
||||||
|
#include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive
|
||||||
|
#include "ftxui/dom/elements.hpp" // for operator|, Element, filler, text, hbox, separator, center, vbox, bold, border, clear_under, dbox, size, GREATER_THAN, HEIGHT
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
using namespace ftxui;
|
||||||
|
auto screen = ScreenInteractive::TerminalOutput();
|
||||||
|
|
||||||
|
// There are two layers. One at depth = 0 and the modal window at depth = 1;
|
||||||
|
int depth = 0;
|
||||||
|
float percentage = 0.0f;
|
||||||
|
|
||||||
|
// Define different modal types using enum for clarity
|
||||||
|
enum ModalType {
|
||||||
|
MAIN_SCREEN = 0,
|
||||||
|
SERVER_SELECTION = 1,
|
||||||
|
FILE_SELECTION = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
// The current rating of FTXUI.
|
||||||
|
std::string address = "";
|
||||||
|
std::string address_input = "";
|
||||||
|
std::string console_message = "Welcome to Bellatui 0.5\n Running in client mode";
|
||||||
|
std::string validation_message = "";
|
||||||
|
|
||||||
|
std::vector<std::string> server_address_labels = {
|
||||||
|
"localhost",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sample BSZ files for the file selection modal
|
||||||
|
std::vector<std::string> bsz_files = {
|
||||||
|
"scene1.bsz",
|
||||||
|
"scene2.bsz",
|
||||||
|
"example.bsz",
|
||||||
|
"test_scene.bsz"
|
||||||
|
};
|
||||||
|
std::string selected_bsz_file = "No file selected";
|
||||||
|
int selected_file_index = 0;
|
||||||
|
|
||||||
|
bool address_valid = false;
|
||||||
|
|
||||||
|
// Function to validate IPv4 address
|
||||||
|
// Uses a lambda function by inline definition
|
||||||
|
// The benefit of this is that it is a local function to the main function
|
||||||
|
// and it has access to the variables in the main function
|
||||||
|
// For larger functions, it is better to define them outside the main function
|
||||||
|
auto validate_ipv4 = [](const std::string& ip) -> bool {
|
||||||
|
std::regex ipv4_pattern(
|
||||||
|
"^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\."
|
||||||
|
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\."
|
||||||
|
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\."
|
||||||
|
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
|
||||||
|
return std::regex_match(ip, ipv4_pattern);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to validate FQDN
|
||||||
|
// Uses a lambda function by inline definition
|
||||||
|
auto validate_fqdn = [](const std::string& fqdn) -> bool {
|
||||||
|
// Basic FQDN validation: letters, numbers, hyphens, dots
|
||||||
|
// At least one dot, no consecutive dots, no leading/trailing dots
|
||||||
|
// Each label max 63 chars, total max 253 chars
|
||||||
|
if (fqdn.length() > 253) return false;
|
||||||
|
if (fqdn.find('.') == std::string::npos) return false;
|
||||||
|
std::regex fqdn_pattern(
|
||||||
|
"^([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$");
|
||||||
|
return std::regex_match(fqdn, fqdn_pattern);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combined validation function
|
||||||
|
// Uses a lambda function by inline definition
|
||||||
|
// We need to combine because we need to validate both IPv4 and FQDN
|
||||||
|
auto validate_address = [&]() {
|
||||||
|
if (validate_ipv4(address_input)) {
|
||||||
|
address_valid = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validate_fqdn(address_input)) {
|
||||||
|
address_valid = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console_message = "Invalid address format";
|
||||||
|
address_valid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// At depth=0, two buttons. One for rating FTXUI and one for quitting.
|
||||||
|
auto ftxui_button_bsz = Button("Select .bsz", [&] { depth = FILE_SELECTION; });
|
||||||
|
auto ftxui_button_render = Button("Upload and render", [&] {
|
||||||
|
depth = MAIN_SCREEN;
|
||||||
|
|
||||||
|
// Reset percentage to start animation
|
||||||
|
percentage = 0.0f;
|
||||||
|
|
||||||
|
// Launch a separate thread for updating the progress
|
||||||
|
std::thread animation_thread([&]() {
|
||||||
|
// Update percentage from 0 to 100% in small increments
|
||||||
|
for (float p = 0.0f; p <= 1.0f; p += 0.01f) {
|
||||||
|
percentage = p;
|
||||||
|
|
||||||
|
// Post an event to trigger a UI redraw
|
||||||
|
screen.PostEvent(Event::Custom);
|
||||||
|
|
||||||
|
// Sleep to control animation speed
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we reach exactly 100%
|
||||||
|
percentage = 1.0f;
|
||||||
|
screen.PostEvent(Event::Custom);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Detach the thread so it continues running independently
|
||||||
|
animation_thread.detach();
|
||||||
|
});
|
||||||
|
auto ftxui_button_address = Button("Select Server", [&] { depth = SERVER_SELECTION; });
|
||||||
|
auto ftxui_button_quit = Button("Quit", screen.ExitLoopClosure());
|
||||||
|
auto ftxui_input_address = Input(&address_input, "FQDN or IP address");
|
||||||
|
|
||||||
|
// 1. Read existing JSON file
|
||||||
|
nlohmann::json j;
|
||||||
|
try {
|
||||||
|
std::ifstream input_file("servers.json");
|
||||||
|
if (!input_file.is_open())
|
||||||
|
{
|
||||||
|
// Create an empty servers.json file with basic structure
|
||||||
|
console_message = "servers.json not found. Creating new empty file.";
|
||||||
|
// Initialize with empty bellatui_servers array
|
||||||
|
j["bellatui_servers"] = nlohmann::json::array();
|
||||||
|
// Save the initial empty structure
|
||||||
|
std::ofstream new_file("servers.json");
|
||||||
|
if (!new_file.is_open())
|
||||||
|
{
|
||||||
|
std::cerr << "Failed to create new servers.json file!" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
new_file << j.dump(2);
|
||||||
|
new_file.close();
|
||||||
|
|
||||||
|
std::cout << "Created new data.json with empty servers list" << std::endl;
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
input_file >> j; // Parse JSON directly into j
|
||||||
|
input_file.close();
|
||||||
|
|
||||||
|
// Read the server addresses from the JSON file
|
||||||
|
for (const auto& server : j["bellatui_servers"]) {
|
||||||
|
server_address_labels.push_back(server["address"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const nlohmann::json::parse_error& e)
|
||||||
|
{
|
||||||
|
std::cout << "JSON parse error: " + std::string(e.what()) << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FTXUI is a library that allows you to create text-based user interfaces.
|
||||||
|
// It is a modern C++ library that provides a functional approach to building terminal applications.
|
||||||
|
// Here are some notes on FTXUI:
|
||||||
|
// Elements are visual, non-interactive parts of the UI:
|
||||||
|
// Components are interactive parts of the UI.
|
||||||
|
// A Container is a component that contains other components.
|
||||||
|
// A component is a function that returns an Element
|
||||||
|
// Renderer is a component that renders a component
|
||||||
|
// CatchEvent is a component that catches events from the user
|
||||||
|
|
||||||
|
// Create a component that renders text
|
||||||
|
// Containers cannot contain elements, so we need to create a component that renders text
|
||||||
|
auto ftxui_text_component = Renderer
|
||||||
|
([]{
|
||||||
|
return text("Add server address");
|
||||||
|
});
|
||||||
|
|
||||||
|
auto depth_0_right_container = Container::Vertical
|
||||||
|
({
|
||||||
|
ftxui_button_render,
|
||||||
|
ftxui_button_quit,
|
||||||
|
});
|
||||||
|
|
||||||
|
auto depth_0_address_container = Container::Vertical
|
||||||
|
({
|
||||||
|
ftxui_text_component,
|
||||||
|
ftxui_input_address | vscroll_indicator
|
||||||
|
| size(HEIGHT, EQUAL, 2)
|
||||||
|
| size(WIDTH, EQUAL, 30),
|
||||||
|
}) | CatchEvent([&](Event event) {
|
||||||
|
// Check if the Return/Enter key was pressed
|
||||||
|
if (event == Event::Return) { // Handle the Return key press
|
||||||
|
validate_address();
|
||||||
|
if (address_valid)
|
||||||
|
{
|
||||||
|
console_message = "Server address " + address_input + " added";
|
||||||
|
} else {
|
||||||
|
address_input = ""; // Clear the input if the address is invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can perform actions here, like:
|
||||||
|
// - Validating the input
|
||||||
|
// - Submitting the form
|
||||||
|
// - Changing focus
|
||||||
|
// - Updating state
|
||||||
|
|
||||||
|
// Add the new server to the JSON
|
||||||
|
j["bellatui_servers"].push_back(nlohmann::json{
|
||||||
|
{"address", address_input},
|
||||||
|
{"commandport", 22},
|
||||||
|
{"heartport", 22},
|
||||||
|
{"pubkeyport", 22}
|
||||||
|
});
|
||||||
|
// Add the new server to the server_address_labels vector
|
||||||
|
server_address_labels.push_back(address_input);
|
||||||
|
address_input=""; // Clear the input field
|
||||||
|
|
||||||
|
std::ofstream output_file("servers.json");
|
||||||
|
if (!output_file.is_open())
|
||||||
|
{
|
||||||
|
std::cerr << "Could not open servers.json for writing!" << std::endl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
output_file << j.dump(2);
|
||||||
|
output_file.close();
|
||||||
|
|
||||||
|
// FTXUI is a functional library, so it is important to return true to indicate that the event has been handled
|
||||||
|
// If you return false, the event will be passed to other components
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Return false for other events to let them propagate to child components
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Container::Horizontal contain nest other containers and components
|
||||||
|
auto depth_0_container = Container::Horizontal
|
||||||
|
({
|
||||||
|
ftxui_button_bsz,
|
||||||
|
ftxui_button_address,
|
||||||
|
depth_0_address_container,
|
||||||
|
depth_0_right_container,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Renderer is a component that renders a component
|
||||||
|
auto depth_0_renderer = Renderer(depth_0_container, [&] {
|
||||||
|
return vbox({
|
||||||
|
text("Demo modal / menu / json / files"),
|
||||||
|
separator(),
|
||||||
|
hbox({
|
||||||
|
text("Selected file: ") | bold,
|
||||||
|
text(selected_bsz_file),
|
||||||
|
}),
|
||||||
|
hbox({
|
||||||
|
text("Selected server: ") | bold,
|
||||||
|
text(address.empty() ? "None" : address),
|
||||||
|
}),
|
||||||
|
// Display percentage value alongside the gauge
|
||||||
|
hbox({
|
||||||
|
text(std::to_string(int(percentage * 100)) + "% "),
|
||||||
|
gauge(percentage) | flex,
|
||||||
|
}),
|
||||||
|
filler(),
|
||||||
|
separator(),
|
||||||
|
// Render the container's components
|
||||||
|
depth_0_container->Render(),
|
||||||
|
paragraph(console_message) | bold
|
||||||
|
| size(WIDTH, EQUAL, 30)
|
||||||
|
| size(HEIGHT, EQUAL, 3),
|
||||||
|
}) |
|
||||||
|
border | size(HEIGHT, GREATER_THAN, 18) | center;
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is a lambda function that will be called when a menu item is selected
|
||||||
|
auto on_address = [&](std::string new_address) {
|
||||||
|
address = new_address;
|
||||||
|
depth = MAIN_SCREEN; // switch back to the main menu
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a menu component for server selection
|
||||||
|
int selected_server_index = 0;
|
||||||
|
auto server_menu = Menu(&server_address_labels, &selected_server_index)
|
||||||
|
| CatchEvent([&](Event event) {
|
||||||
|
if (event == Event::Return) { // When Enter pressed, select that server
|
||||||
|
on_address(server_address_labels[selected_server_index]);
|
||||||
|
console_message = "";
|
||||||
|
depth = MAIN_SCREEN;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event == Event::Character("-")) { // When minus key pressed, remove the selected server
|
||||||
|
// Make sure we have at least one server and a valid selection
|
||||||
|
if (!server_address_labels.empty() && selected_server_index >= 0 &&
|
||||||
|
selected_server_index < static_cast<int>(server_address_labels.size())) {
|
||||||
|
|
||||||
|
// Get the address to remove
|
||||||
|
std::string address_to_remove = server_address_labels[selected_server_index];
|
||||||
|
|
||||||
|
// Remove from the vector so display is updated
|
||||||
|
server_address_labels.erase(server_address_labels.begin() + selected_server_index);
|
||||||
|
|
||||||
|
// Update the selected index to avoid out-of-bounds
|
||||||
|
if (selected_server_index >= static_cast<int>(server_address_labels.size())) {
|
||||||
|
selected_server_index = std::max(0, static_cast<int>(server_address_labels.size()) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from the JSON so it is not saved
|
||||||
|
auto& servers = j["bellatui_servers"];
|
||||||
|
for (size_t i = 0; i < servers.size(); ++i) {
|
||||||
|
if (servers[i]["address"] == address_to_remove) {
|
||||||
|
servers.erase(servers.begin() + i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the updated JSON
|
||||||
|
std::ofstream output_file("servers.json");
|
||||||
|
if (output_file.is_open()) {
|
||||||
|
output_file << j.dump(2); // 2 is the indent level
|
||||||
|
output_file.close();
|
||||||
|
console_message = "Server address '" + address_to_remove + "' removed";
|
||||||
|
} else {
|
||||||
|
console_message = "Error: Could not save changes to servers.json";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event == Event::Escape) {
|
||||||
|
depth = MAIN_SCREEN;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
| size(HEIGHT, LESS_THAN, 10) // Limit height but allow scrolling
|
||||||
|
| border
|
||||||
|
| vscroll_indicator; // Add a scroll indicator for when the list gets long
|
||||||
|
|
||||||
|
auto depth_1_container = Container::Vertical({
|
||||||
|
server_menu,
|
||||||
|
});
|
||||||
|
|
||||||
|
auto depth_1_renderer = Renderer(depth_1_container, [&] {
|
||||||
|
return vbox({
|
||||||
|
text("Select bellatui server"),
|
||||||
|
separator(),
|
||||||
|
filler(),
|
||||||
|
depth_1_container->Render(), // This now renders the menu
|
||||||
|
filler(),
|
||||||
|
text("Enter selects, - deletes, Esc cancels") | center,
|
||||||
|
}) |
|
||||||
|
border;
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is a lambda function that will be called when a file is selected
|
||||||
|
auto on_file_select = [&](std::string file_name) {
|
||||||
|
selected_bsz_file = file_name;
|
||||||
|
depth = MAIN_SCREEN; // switch back to the main menu
|
||||||
|
console_message = "Selected file: " + file_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a menu component for file selection
|
||||||
|
auto file_menu = Menu(&bsz_files, &selected_file_index)
|
||||||
|
| CatchEvent([&](Event event) {
|
||||||
|
// When Enter is pressed on a menu item, select that file
|
||||||
|
if (event == Event::Return) {
|
||||||
|
on_file_select(bsz_files[selected_file_index]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// When Escape is pressed, return to main screen
|
||||||
|
if (event == Event::Escape) {
|
||||||
|
depth = MAIN_SCREEN;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
| size(HEIGHT, LESS_THAN, 10) // Limit height but allow scrolling
|
||||||
|
| border
|
||||||
|
| vscroll_indicator; // Add a scroll indicator for when the list gets long
|
||||||
|
|
||||||
|
// Create a container for the file selection modal
|
||||||
|
auto depth_2_container = Container::Vertical({
|
||||||
|
file_menu,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a renderer for modal file dialog
|
||||||
|
auto depth_2_renderer = Renderer(depth_2_container, [&] {
|
||||||
|
return vbox({
|
||||||
|
text("Select .bsz File") | bold,
|
||||||
|
separator(),
|
||||||
|
filler(),
|
||||||
|
depth_2_container->Render(),
|
||||||
|
filler(),
|
||||||
|
text("Press Enter to select, Escape to cancel") | center,
|
||||||
|
}) |
|
||||||
|
border;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a container for the main screen
|
||||||
|
auto main_container = Container::Tab( //Tab allows switching between elements
|
||||||
|
{
|
||||||
|
depth_0_renderer,
|
||||||
|
depth_1_renderer,
|
||||||
|
depth_2_renderer,
|
||||||
|
},
|
||||||
|
&depth);
|
||||||
|
|
||||||
|
// Main renderer is the main component that renders the main container
|
||||||
|
auto main_renderer = Renderer(main_container, [&] {
|
||||||
|
Element document = depth_0_renderer->Render();
|
||||||
|
|
||||||
|
// Show the appropriate modal based on depth value
|
||||||
|
if (depth == SERVER_SELECTION) {
|
||||||
|
document = dbox({ //dbox allows stacking of elements
|
||||||
|
document,
|
||||||
|
depth_1_renderer->Render() | clear_under | center,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (depth == FILE_SELECTION) {
|
||||||
|
document = dbox({ //dbox allows stacking of elements
|
||||||
|
document,
|
||||||
|
depth_2_renderer->Render() | clear_under | center,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return document;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ftxui main loop
|
||||||
|
// loop will run until the user quits the application
|
||||||
|
// [TODO] other threads need to be added before this loop
|
||||||
|
// [TODO] ie ZMQ connections, etc
|
||||||
|
screen.Loop(main_renderer);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user