trying interactive

This commit is contained in:
Your Name 2025-04-22 19:57:17 +12:00
parent bd0c48f427
commit 4117b3daaf
6 changed files with 449 additions and 3 deletions

View File

@ -6,6 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find required packages # Find required packages
find_package(Boost REQUIRED COMPONENTS program_options filesystem system) find_package(Boost REQUIRED COMPONENTS program_options filesystem system)
find_package(Curses REQUIRED)
# Auto-detect source files # Auto-detect source files
file(GLOB_RECURSE SOURCES "src/*.cpp") file(GLOB_RECURSE SOURCES "src/*.cpp")
@ -15,13 +16,17 @@ file(GLOB_RECURSE HEADERS "src/*.hpp")
add_executable(dropshell ${SOURCES}) add_executable(dropshell ${SOURCES})
# Set include directories # Set include directories
target_include_directories(dropshell PRIVATE src) target_include_directories(dropshell PRIVATE
src
${CURSES_INCLUDE_DIRS}
)
# Link libraries # Link libraries
target_link_libraries(dropshell PRIVATE target_link_libraries(dropshell PRIVATE
Boost::program_options Boost::program_options
Boost::filesystem Boost::filesystem
Boost::system Boost::system
${CURSES_LIBRARIES}
) )
# Install targets # Install targets

View File

@ -28,6 +28,7 @@ void check_status();
void list_servers(); void list_servers();
void list_templates(); void list_templates();
void show_server_details(const std::string& server_name); void show_server_details(const std::string& server_name);
void interactive_mode();
// Utility functions // Utility functions
std::vector<ServerInfo> get_configured_servers(); std::vector<ServerInfo> get_configured_servers();

View File

@ -0,0 +1,338 @@
#include "interactive/interactive.hpp"
#include <ncurses.h>
#include <string>
#include <vector>
#include <chrono>
#include <thread>
#include <algorithm>
namespace interactive {
int fullscreen_window::ncurses_streambuf::overflow(int c) {
if (c != EOF) {
buffer += static_cast<char>(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<std::string> 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<std::string> 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<std::chrono::milliseconds>(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

View File

@ -0,0 +1,80 @@
#ifndef INTERACTIVE_HPP
#define INTERACTIVE_HPP
#include <string>
#include <vector>
#include <ncurses.h>
#include <streambuf>
#include <iostream>
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<std::string> 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

View File

@ -71,8 +71,8 @@ int main(int argc, char* argv[]) {
// No arguments provided // No arguments provided
if (argc < 2) { if (argc < 2) {
dropshell::print_help(); dropshell::interactive_mode();
return 1; return 0;
} }
// Handle commands // Handle commands

View File

@ -3,6 +3,7 @@
#include "server_env.hpp" #include "server_env.hpp"
#include "server_service.hpp" #include "server_service.hpp"
#include "tableprint.hpp" #include "tableprint.hpp"
#include "interactive/interactive.hpp"
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <iomanip> #include <iomanip>
@ -51,6 +52,27 @@ std::vector<ServerInfo> get_configured_servers() {
return servers; return servers;
} }
void interactive_mode() {
interactive::fullscreen_window iw("DropShell Servers");
auto servers = get_configured_servers();
std::vector<std::string> 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() { void list_servers() {
auto servers = get_configured_servers(); auto servers = get_configured_servers();