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_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
|
||||
|
@ -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<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
|
||||
if (argc < 2) {
|
||||
dropshell::print_help();
|
||||
return 1;
|
||||
dropshell::interactive_mode();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handle commands
|
||||
|
@ -3,6 +3,7 @@
|
||||
#include "server_env.hpp"
|
||||
#include "server_service.hpp"
|
||||
#include "tableprint.hpp"
|
||||
#include "interactive/interactive.hpp"
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
@ -51,6 +52,27 @@ std::vector<ServerInfo> get_configured_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() {
|
||||
auto servers = get_configured_servers();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user