From 4117b3daafb5f823c822421ac236676e2741f91a Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 22 Apr 2025 19:57:17 +1200 Subject: [PATCH] trying interactive --- CMakeLists.txt | 7 +- src/dropshell.hpp | 1 + src/interactive/interactive.cpp | 338 ++++++++++++++++++++++++++++++++ src/interactive/interactive.hpp | 80 ++++++++ src/main.cpp | 4 +- src/servers.cpp | 22 +++ 6 files changed, 449 insertions(+), 3 deletions(-) create mode 100644 src/interactive/interactive.cpp create mode 100644 src/interactive/interactive.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9653739..a5445df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # Find required packages find_package(Boost REQUIRED COMPONENTS program_options filesystem system) +find_package(Curses REQUIRED) # Auto-detect source files file(GLOB_RECURSE SOURCES "src/*.cpp") @@ -15,13 +16,17 @@ file(GLOB_RECURSE HEADERS "src/*.hpp") add_executable(dropshell ${SOURCES}) # Set include directories -target_include_directories(dropshell PRIVATE src) +target_include_directories(dropshell PRIVATE + src + ${CURSES_INCLUDE_DIRS} +) # Link libraries target_link_libraries(dropshell PRIVATE Boost::program_options Boost::filesystem Boost::system + ${CURSES_LIBRARIES} ) # Install targets diff --git a/src/dropshell.hpp b/src/dropshell.hpp index 86381f1..ccc8837 100644 --- a/src/dropshell.hpp +++ b/src/dropshell.hpp @@ -28,6 +28,7 @@ void check_status(); void list_servers(); void list_templates(); void show_server_details(const std::string& server_name); +void interactive_mode(); // Utility functions std::vector get_configured_servers(); diff --git a/src/interactive/interactive.cpp b/src/interactive/interactive.cpp new file mode 100644 index 0000000..6eb088e --- /dev/null +++ b/src/interactive/interactive.cpp @@ -0,0 +1,338 @@ +#include "interactive/interactive.hpp" +#include +#include +#include +#include +#include +#include + +namespace interactive { + +int fullscreen_window::ncurses_streambuf::overflow(int c) { + if (c != EOF) { + buffer += static_cast(c); + if (c == '\n') { + wprintw(win, "%s", buffer.c_str()); + wrefresh(win); + buffer.clear(); + } + } + return c; +} + +fullscreen_window::fullscreen_window(std::string title) { + initscr(); + cbreak(); + noecho(); + keypad(stdscr, TRUE); + curs_set(0); + refresh(); + + // Create display window (takes up all but bottom 4 lines) + int max_y, max_x; + getmaxyx(stdscr, max_y, max_x); + display_win = newwin(max_y - 4, max_x, 0, 0); + scrollok(display_win, TRUE); + wrefresh(display_win); + + // Create input window (bottom 4 lines) + input_win = newwin(4, max_x, max_y - 4, 0); + box(input_win, 0, 0); + wrefresh(input_win); + + // Set up output redirection + cout_buf = new ncurses_streambuf(display_win); + cerr_buf = new ncurses_streambuf(display_win); + + // Save original streambufs + original_cout = std::cout.rdbuf(); + original_cerr = std::cerr.rdbuf(); + + // Redirect cout and cerr to our custom streambuf + std::cout.rdbuf(cout_buf); + std::cerr.rdbuf(cerr_buf); +} + +void fullscreen_window::close() { + if (is_closed) return; + is_closed = true; + + // Restore original streambufs + std::cout.rdbuf(original_cout); + std::cerr.rdbuf(original_cerr); + + // Clean up streambufs + delete cout_buf; + delete cerr_buf; + + // Clean up ncurses windows + delwin(display_win); + delwin(input_win); + + // End ncurses + endwin(); +} + +fullscreen_window::~fullscreen_window() { + close(); +} + +void fullscreen_window::set_input_text_display(std::string text) { + werase(input_win); + box(input_win, 0, 0); + + // Center the text + int max_y, max_x; + getmaxyx(input_win, max_y, max_x); + int start_x = (max_x - text.length()) / 2; + mvwprintw(input_win, max_y/2, start_x, "%s", text.c_str()); + + wrefresh(input_win); +} + +std::string fullscreen_window::set_input_text_entry(std::string prompt) { + werase(input_win); + box(input_win, 0, 0); + + // Display prompt + mvwprintw(input_win, 1, 2, "%s", prompt.c_str()); + wrefresh(input_win); + + // Set up input + echo(); + curs_set(1); + std::string input; + int ch; + int x = prompt.length() + 3; + + while ((ch = wgetch(input_win)) != '\n') { + if (ch == 27) { // ESC key + input = ""; + break; + } + if (ch == KEY_BACKSPACE || ch == 127) { + if (!input.empty()) { + input.pop_back(); + x--; + mvwdelch(input_win, 1, x); + } + } else if (isprint(ch)) { + input += ch; + mvwaddch(input_win, 1, x++, ch); + } + } + + noecho(); + curs_set(0); + return input; +} + +std::string fullscreen_window::set_input_multiple_choice(std::string prompt, std::vector choices) { + werase(input_win); + box(input_win, 0, 0); + + // Display prompt + mvwprintw(input_win, 1, 2, "%s", prompt.c_str()); + + int selected = 0; + std::string filter = ""; + auto last_key_time = std::chrono::steady_clock::now(); + int scroll_offset = 0; + + while (true) { + // Filter choices based on input + std::vector filtered_choices; + for (const auto& choice : choices) { + if (filter.empty() || choice.find(filter) == 0) { + filtered_choices.push_back(choice); + } + } + + if (filtered_choices.empty()) { + filtered_choices = choices; + filter = ""; + } + + // Calculate total width needed + int total_width = 0; + for (const auto& choice : filtered_choices) { + total_width += choice.length() + 3; // +3 for " | " separator + } + + // Get window dimensions + int max_y, max_x; + getmaxyx(input_win, max_y, max_x); + int available_width = max_x - 4; // -4 for borders + + // Calculate scroll offset to keep selected item visible + if (selected >= 0 && selected < filtered_choices.size()) { + int item_start = 0; + for (int i = 0; i < selected; i++) { + item_start += filtered_choices[i].length() + 3; + } + int item_end = item_start + filtered_choices[selected].length(); + + if (item_start < scroll_offset) { + scroll_offset = item_start; + } else if (item_end > scroll_offset + available_width) { + scroll_offset = item_end - available_width; + } + } + + // Display filtered choices horizontally + int x = 2 - scroll_offset; + for (size_t i = 0; i < filtered_choices.size(); i++) { + if (i == selected) { + wattron(input_win, A_REVERSE); + } + + // Only draw if visible + if (x + filtered_choices[i].length() > 0 && x < max_x - 2) { + mvwprintw(input_win, 2, x, "%s", filtered_choices[i].c_str()); + } + + if (i == selected) { + wattroff(input_win, A_REVERSE); + } + + x += filtered_choices[i].length() + 3; // +3 for " | " separator + + // Draw separator if not last item + if (i < filtered_choices.size() - 1) { + if (x - 3 + 3 > 0 && x - 3 < max_x - 2) { + mvwprintw(input_win, 2, x - 3, " | "); + } + } + } + + wrefresh(input_win); + + int ch = wgetch(input_win); + + // Handle key input + if (ch == '\n') { + if (!filtered_choices.empty()) { + return filtered_choices[selected]; + } + } else if (ch == 27) { // ESC key + if (!filter.empty()) { + // Clear the filter and reset selection + filter = ""; + selected = 0; + scroll_offset = 0; + } else { + // Only return if filter is already empty + return ""; + } + } else if (ch == KEY_LEFT || ch == KEY_UP) { + if (selected > 0) { + selected--; + } else { + selected = filtered_choices.size() - 1; // Wrap to end + } + continue; // Skip the rest of the loop + } else if (ch == KEY_RIGHT || ch == KEY_DOWN) { + if (selected < filtered_choices.size() - 1) { + selected++; + } else { + selected = 0; // Wrap to beginning + } + continue; // Skip the rest of the loop + } else if (isprint(ch)) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - last_key_time).count(); + + if (elapsed > 500) { // Reset filter if too much time has passed + filter = ""; + } + + filter += ch; + selected = 0; + last_key_time = now; + } + } +} + +bool fullscreen_window::set_input_yes_no(std::string prompt) { + werase(input_win); + box(input_win, 0, 0); + + // Display prompt + mvwprintw(input_win, 1, 2, "%s", prompt.c_str()); + + int selected = 0; // 0 for Yes, 1 for No + const char* options[] = {"Yes", "No"}; + + while (true) { + // Display options + for (int i = 0; i < 2; i++) { + if (i == selected) { + wattron(input_win, A_REVERSE); + } + mvwprintw(input_win, 2, 2 + i * 10, "%s", options[i]); + if (i == selected) { + wattroff(input_win, A_REVERSE); + } + } + + wrefresh(input_win); + + int ch = wgetch(input_win); + + if (ch == '\n') { + return selected == 0; + } else if (ch == 'y' || ch == 'Y') { + return true; + } else if (ch == 'n' || ch == 'N') { + return false; + } else if (ch == KEY_LEFT) { + selected = 0; + } else if (ch == KEY_RIGHT) { + selected = 1; + } + } +} + +char fullscreen_window::set_input_yes_no_cancel(std::string prompt) { + werase(input_win); + box(input_win, 0, 0); + + // Display prompt + mvwprintw(input_win, 1, 2, "%s", prompt.c_str()); + + int selected = 0; // 0 for Yes, 1 for No, 2 for Cancel + const char* options[] = {"Yes", "No", "Cancel"}; + + while (true) { + // Display options + for (int i = 0; i < 3; i++) { + if (i == selected) { + wattron(input_win, A_REVERSE); + } + mvwprintw(input_win, 2, 2 + i * 10, "%s", options[i]); + if (i == selected) { + wattroff(input_win, A_REVERSE); + } + } + + wrefresh(input_win); + + int ch = wgetch(input_win); + + if (ch == '\n') { + return selected == 0 ? 'y' : (selected == 1 ? 'n' : 'c'); + } else if (ch == 'y' || ch == 'Y') { + return 'y'; + } else if (ch == 'n' || ch == 'N') { + return 'n'; + } else if (ch == 'c' || ch == 'C') { + return 'c'; + } else if (ch == KEY_LEFT) { + selected = (selected - 1 + 3) % 3; + } else if (ch == KEY_RIGHT) { + selected = (selected + 1) % 3; + } + } +} + +} // namespace interactive \ No newline at end of file diff --git a/src/interactive/interactive.hpp b/src/interactive/interactive.hpp new file mode 100644 index 0000000..a92b9c2 --- /dev/null +++ b/src/interactive/interactive.hpp @@ -0,0 +1,80 @@ +#ifndef INTERACTIVE_HPP +#define INTERACTIVE_HPP + +#include +#include +#include +#include +#include + +namespace interactive { + +class fullscreen_window { + private: + WINDOW* display_win; + WINDOW* input_win; + + // Original streambufs to restore later + std::streambuf* original_cout; + std::streambuf* original_cerr; + + // Custom streambuf for redirecting output + class ncurses_streambuf : public std::streambuf { + private: + WINDOW* win; + std::string buffer; + + protected: + virtual int overflow(int c) override; + + public: + ncurses_streambuf(WINDOW* w) : win(w) {} + }; + + ncurses_streambuf* cout_buf; + ncurses_streambuf* cerr_buf; + bool is_closed = false; + + public: + // uses ncurses to create a fullscreen text area, with two windows: + // one (display) that shows everything sent to stdout or stderr, displaying any text output from the program + // and the other (input) for interactive input + // the input window is always at the bottom of the screen, and the display window takes up the rest of the screen + // the input window is 4 rows high, and the display window takes up the rest of the screen + // the two windows take up the entire screen, and the program can handle resizing + // pressing the ` key at any time will bring up a confirmation dialog to quit the program + fullscreen_window(std::string title); + + // Destructor to restore original streambufs + ~fullscreen_window(); + + // just display centered text in the input window. returns immediately. + void set_input_text_display(std::string text); + + + // displays the prompt and a text entry box. + // only returns after the user has entered text and pressed enter. pressing escape returns an empty string. + std::string set_input_text_entry(std::string prompt); + + + // displays the prompt and then a list of choices. + // returns when user selects a choice using arrow keys and presses enter. pressing escape returns an empty string. + // letter keys filter by choices to those that start with that letter, and if multiple letters are typed with a delay it will filter by that string of letters. + std::string set_input_multiple_choice(std::string prompt, std::vector choices); + + // displays a yes/no confirmation prompt, with arrow keys to select yes or no. returns true if yes, false if no. + // allows y as shortcut for yes and n as shortcut for no. + bool set_input_yes_no(std::string prompt); + + // displays a yes/no/cancel confirmation prompt, with arrow keys to select yes, no, or cancel. + // allows y as shortcut for yes, n as shortcut for no, and c as shortcut for cancel. + // returns y for yes, n for no, and c for cancel. + char set_input_yes_no_cancel(std::string prompt); + + // closes the fullscreen window and restores the original streambufs + void close(); +}; + +} // namespace interactive + +#endif diff --git a/src/main.cpp b/src/main.cpp index e2d05fd..0c342a8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -71,8 +71,8 @@ int main(int argc, char* argv[]) { // No arguments provided if (argc < 2) { - dropshell::print_help(); - return 1; + dropshell::interactive_mode(); + return 0; } // Handle commands diff --git a/src/servers.cpp b/src/servers.cpp index 98fd7cf..9435d4c 100644 --- a/src/servers.cpp +++ b/src/servers.cpp @@ -3,6 +3,7 @@ #include "server_env.hpp" #include "server_service.hpp" #include "tableprint.hpp" +#include "interactive/interactive.hpp" #include #include #include @@ -51,6 +52,27 @@ std::vector get_configured_servers() { return servers; } +void interactive_mode() { + interactive::fullscreen_window iw("DropShell Servers"); + auto servers = get_configured_servers(); + std::vector server_names; + for (const auto& server : servers) { + server_names.push_back(server.name); + } + + list_servers(); + + std::string server_name = iw.set_input_multiple_choice("Select a server", server_names); + if (server_name.empty()) { + return; + } + + iw.close(); + + show_server_details(server_name); + +} + void list_servers() { auto servers = get_configured_servers();