trying interactive
This commit is contained in:
parent
bd0c48f427
commit
4117b3daaf
@ -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
|
||||||
|
@ -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();
|
||||||
|
338
src/interactive/interactive.cpp
Normal file
338
src/interactive/interactive.cpp
Normal 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
|
80
src/interactive/interactive.hpp
Normal file
80
src/interactive/interactive.hpp
Normal 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
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user