diff --git a/src/interactive/interactive.cpp b/src/interactive/interactive.cpp index c66a502..a55b121 100644 --- a/src/interactive/interactive.cpp +++ b/src/interactive/interactive.cpp @@ -11,15 +11,194 @@ #include #include #include "interactive.hpp" +#include namespace interactive { +// Forward declarations +void process_line(WINDOW* win, const std::string& text, const std::map& color_pair_map); + +// Function to convert ANSI escape codes to ncurses attributes and print text +void ansi_print(WINDOW* win, const std::string& text) { + // Ensure we're using a UTF-8 compatible locale + static bool locale_initialized = false; + if (!locale_initialized) { + setlocale(LC_ALL, ""); + locale_initialized = true; + } + + static int color_pairs_initialized = 0; + static std::map color_pair_map; + + // Initialize color pairs if not done yet + if (!color_pairs_initialized) { + // Check if terminal supports colors + if (has_colors()) { + // Define color pairs for common ANSI colors + init_pair(1, COLOR_RED, -1); + init_pair(2, COLOR_GREEN, -1); + init_pair(3, COLOR_YELLOW, -1); + init_pair(4, COLOR_BLUE, -1); + init_pair(5, COLOR_MAGENTA, -1); + init_pair(6, COLOR_CYAN, -1); + init_pair(7, COLOR_WHITE, -1); + + // Try to define extended colors if possible + if (COLORS >= 256) { + init_pair(8, 8, -1); // Dark grey + init_pair(9, 142, -1); // Dark yellow (approximation) + init_pair(10, 250, -1); // Light grey (approximation) + } else { + // Fallback for terminals with fewer colors + init_pair(8, COLOR_BLACK, -1); // Dark grey fallback + init_pair(9, COLOR_YELLOW, -1); // Dark yellow fallback + init_pair(10, COLOR_WHITE, -1); // Light grey fallback + } + } + + // Map ANSI codes to color pairs + color_pair_map["\033[1;31m"] = 1; // Red + color_pair_map["\033[1;32m"] = 2; // Green + color_pair_map["\033[1;33m"] = 3; // Yellow + color_pair_map["\033[1;34m"] = 4; // Blue + color_pair_map["\033[1;35m"] = 5; // Magenta + color_pair_map["\033[1;36m"] = 6; // Cyan + color_pair_map["\033[1;37m"] = 7; // White + color_pair_map["\033[90m"] = 8; // Dark Grey + color_pair_map["\033[38;5;142m"] = 9; // Dark Yellow + color_pair_map["\033[38;5;250m"] = 10; // Light Grey + + // Also map the non-bold versions + color_pair_map["\033[31m"] = 1; // Red + color_pair_map["\033[32m"] = 2; // Green + color_pair_map["\033[33m"] = 3; // Yellow + color_pair_map["\033[34m"] = 4; // Blue + color_pair_map["\033[35m"] = 5; // Magenta + color_pair_map["\033[36m"] = 6; // Cyan + color_pair_map["\033[37m"] = 7; // White + + color_pairs_initialized = 1; + } + + // Handle newlines in the text + size_t start = 0; + size_t end = text.find('\n'); + + // If no newlines, process the whole text at once + if (end == std::string::npos) { + process_line(win, text, color_pair_map); + return; + } + + // Process each line separately + while (end != std::string::npos) { + // Process this line + std::string line = text.substr(start, end - start); + process_line(win, line, color_pair_map); + + // Move to next line + waddch(win, '\n'); + + // Find next line + start = end + 1; + end = text.find('\n', start); + } + + // Process any remaining text after the last newline + if (start < text.length()) { + process_line(win, text.substr(start), color_pair_map); + } +} + +// Helper function to process a single line of text +void process_line(WINDOW* win, const std::string& text, const std::map& color_pair_map) { + size_t pos = 0; + int current_attrs = 0; + int current_color_pair = 0; + + while (pos < text.length()) { + // Check for ANSI escape sequence + if (text[pos] == '\033' && pos + 1 < text.length() && text[pos + 1] == '[') { + // Find the end of the escape sequence (marked by 'm') + size_t end_pos = text.find('m', pos); + if (end_pos != std::string::npos) { + std::string esc_seq = text.substr(pos, end_pos - pos + 1); + + // Reset attributes if the sequence is a reset code + if (esc_seq == "\033[0m") { + wattrset(win, A_NORMAL); + current_attrs = 0; + current_color_pair = 0; + } + // Handle bold attribute + else if (esc_seq == "\033[1m") { + current_attrs |= A_BOLD; + wattrset(win, COLOR_PAIR(current_color_pair) | current_attrs); + } + // Apply color pair if known + else if (color_pair_map.find(esc_seq) != color_pair_map.end()) { + current_color_pair = color_pair_map.at(esc_seq); + // Check if this is a bold sequence (contains "1;") + if (esc_seq.find("1;") != std::string::npos) { + current_attrs |= A_BOLD; + } + wattrset(win, COLOR_PAIR(current_color_pair) | current_attrs); + } + // Handle more complex color sequences (38;5;XXXm) + else if (esc_seq.find("38;5;") != std::string::npos) { + // Extract the color number + std::string color_num_str = esc_seq.substr(esc_seq.find("38;5;") + 5); + color_num_str = color_num_str.substr(0, color_num_str.length() - 1); // Remove 'm' + + try { + int color_num = std::stoi(color_num_str); + // Create a new color pair for this color if needed + int pair_num = 11 + color_num; // Start after our predefined pairs + + // Only initialize if in valid range and we haven't seen this color before + if (color_num < COLORS && color_pair_map.find(esc_seq) == color_pair_map.end()) { + // Try to initialize, but catch errors in case color is invalid + try { + init_pair(pair_num, color_num, -1); + const_cast&>(color_pair_map)[esc_seq] = pair_num; + } catch (...) { + // If we can't initialize this color, use a default + const_cast&>(color_pair_map)[esc_seq] = 7; // White + } + } + + if (color_pair_map.find(esc_seq) != color_pair_map.end()) { + current_color_pair = color_pair_map.at(esc_seq); + wattrset(win, COLOR_PAIR(current_color_pair) | current_attrs); + } + } catch (...) { + // If we can't parse the color, just ignore it + } + } + + pos = end_pos + 1; + continue; + } + } + + // Print the character as-is + waddch(win, text[pos]); + pos++; + } +} + int fullscreen_window::ncurses_streambuf::overflow(int c) { if (c != EOF) { buffer += static_cast(c); if (c == '\n') { // Process buffer with proper escape sequence handling - wprintw(win, "%s", buffer.c_str()); + ansi_print(win, buffer); + wrefresh(win); + buffer.clear(); + } else if (buffer.length() > 1024) { + // If buffer is getting too large without a newline, + // process it anyway to prevent memory issues + ansi_print(win, buffer); wrefresh(win); buffer.clear(); } @@ -31,8 +210,8 @@ fullscreen_window::fullscreen_window(std::string title) { // Set locale for wide character support std::setlocale(LC_ALL, ""); - // Initialize ncurses with proper terminal capabilities - putenv(const_cast("TERM=xterm-256color")); + // Initialize ncurses with basic terminal capabilities + // Don't force a specific TERM type which could cause compatibility issues initscr(); raw(); // Use raw mode for better control @@ -41,9 +220,11 @@ fullscreen_window::fullscreen_window(std::string title) { curs_set(0); // Hide cursor set_escdelay(25); // Set ESC delay to 25ms - // Enable color support - start_color(); - use_default_colors(); + // Enable color support if available + if (has_colors()) { + start_color(); + use_default_colors(); + } refresh(); @@ -102,6 +283,12 @@ fullscreen_window::~fullscreen_window() { close(); } +void fullscreen_window::clear_display() +{ + werase(display_win); + wrefresh(display_win); +} + void fullscreen_window::set_input_text_display(std::string text) { werase(input_win); box(input_win, 0, 0); @@ -152,7 +339,7 @@ std::string fullscreen_window::set_input_text_entry(std::string prompt) { return input; } -std::string fullscreen_window::set_input_multiple_choice(std::string prompt, std::vector choices) { +std::string fullscreen_window::set_input_multiple_choice(std::string prompt, std::vector choices, std::string default_choice) { werase(input_win); box(input_win, 0, 0); @@ -160,6 +347,14 @@ std::string fullscreen_window::set_input_multiple_choice(std::string prompt, std mvwprintw(input_win, 1, 2, "%s", prompt.c_str()); int selected = 0; + if (!default_choice.empty()) { + for (size_t i = 0; i < choices.size(); ++i) { + if (choices[i] == default_choice) { + selected = i; + break; + } + } + } std::string filter = ""; std::string last_filter = ""; auto last_key_time = std::chrono::steady_clock::now(); diff --git a/src/interactive/interactive.hpp b/src/interactive/interactive.hpp index a92b9c2..758b1bb 100644 --- a/src/interactive/interactive.hpp +++ b/src/interactive/interactive.hpp @@ -9,6 +9,9 @@ namespace interactive { +// Function to convert ANSI escape codes to ncurses attributes and print text +void ansi_print(WINDOW* win, const std::string& text); + class fullscreen_window { private: WINDOW* display_win; @@ -47,20 +50,21 @@ class fullscreen_window { // Destructor to restore original streambufs ~fullscreen_window(); + + // clears the display window and refreshes it + void clear_display(); // 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); + std::string set_input_multiple_choice(std::string prompt, std::vector choices, std::string default_choice = ""); // 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. diff --git a/src/servers.cpp b/src/servers.cpp index 29d709e..f3bf153 100644 --- a/src/servers.cpp +++ b/src/servers.cpp @@ -65,15 +65,22 @@ void interactive_mode() { list_servers(); - std::string server_name = iw.set_input_multiple_choice("Select a server", server_names); - if (server_name.empty()) { - return; + while (true) { + std::string server_name = iw.set_input_multiple_choice("Select a server", server_names); + if (server_name.empty()) + { + iw.close(); + return; + } + + while (!server_name.empty()) { + iw.clear_display(); + show_server_details(server_name); + server_name = iw.set_input_multiple_choice("Select a server", server_names, server_name); + } + iw.clear_display(); + list_servers(); } - - iw.close(); - - show_server_details(server_name); - } void list_servers() { @@ -151,6 +158,7 @@ void show_server_details(const std::string& server_name) { std::cout << "Status: Offline" << std::endl; } } + std::cout << std::endl; //--------------------- { diff --git a/src/tableprint.cpp b/src/tableprint.cpp index a1550d3..22bbcec 100644 --- a/src/tableprint.cpp +++ b/src/tableprint.cpp @@ -29,12 +29,12 @@ struct coloredText { }; const std::map kReplacements = { - {":tick:", {"✓", kTextColor_Green}}, - {":cross:", {"✗", kTextColor_Red}}, - {":warning:", {"⚠️", kTextColor_Yellow}}, - {":info:", {"ℹ️", kTextColor_Blue}}, - {":check:", {"✅", kTextColor_Green}}, - {":x:", {"❌", kTextColor_Red}} + {":tick:", {"+", kTextColor_Green}}, + {":cross:", {"x", kTextColor_Red}}, + {":warning:", {"!", kTextColor_Yellow}}, + {":info:", {"i", kTextColor_Blue}}, + {":check:", {"+", kTextColor_Green}}, + {":x:", {"x", kTextColor_Red}} }; // Helper function to get ANSI color code @@ -192,14 +192,14 @@ void tableprint::print() { // Print title if it exists if (!title.empty()) { std::cout << "\033[90m"; // Dark grey color for borders - std::cout << "┌"; + std::cout << "+"; for (size_t i = 0; i < rows[0].size(); ++i) { std::cout << std::string(col_widths[i] + 2, '-'); - if (i < rows[0].size() - 1) std::cout << "┬"; + if (i < rows[0].size() - 1) std::cout << "+"; } - std::cout << "┐" << std::endl; + std::cout << "+" << std::endl; - std::cout << "│"; // White color for title + std::cout << "|"; // White color for title size_t title_width = 0; for (size_t width : col_widths) { title_width += width + 2; // +2 for padding @@ -207,82 +207,82 @@ void tableprint::print() { title_width += col_widths.size() - 1; // Add space for vertical borders std::cout << width_print_centered(title,title_width,"\033[1;37m"); - std::cout << "\033[90m│" << std::endl; + std::cout << "\033[90m|" << std::endl; // Use └─┴─┘ for bottom of title box to connect with table - std::cout << "├"; + std::cout << "+"; for (size_t i = 0; i < rows[0].size(); ++i) { std::cout << std::string(col_widths[i] + 2, '-'); - if (i < rows[0].size() - 1) std::cout << "┼"; + if (i < rows[0].size() - 1) std::cout << "-"; } - std::cout << "┤" << std::endl; + std::cout << "+" << std::endl; } else { // Print top border if no title std::cout << "\033[90m"; // Dark grey color for borders std::cout << "┌"; for (size_t i = 0; i < rows[0].size(); ++i) { std::cout << std::string(col_widths[i] + 2, '-'); - if (i < rows[0].size() - 1) std::cout << "┬"; + if (i < rows[0].size() - 1) std::cout << "+"; } - std::cout << "┐" << std::endl; + std::cout << "+" << std::endl; } // Print header - std::cout << "│"; + std::cout << "|"; for (size_t i = 0; i < rows[0].size(); ++i) { std::cout << width_print_centered(rows[0][i],col_widths[i]+2,"\033[1;36m"); if (i < rows[0].size() - 1) { - std::cout << "\033[90m│\033[1;36m"; // Border color then back to cyan + std::cout << "\033[90m|\033[1;36m"; // Border color then back to cyan } else { - std::cout << "\033[90m│"; // Just border color for last column + std::cout << "\033[90m|"; // Just border color for last column } } std::cout << std::endl; // Print header separator if (!mCompact) { - std::cout << "├"; + std::cout << "+"; for (size_t i = 0; i < rows[0].size(); ++i) { for (size_t j = 0; j < col_widths[i] + 2; ++j) { std::cout << "-"; } - if (i < rows[0].size() - 1) std::cout << "┼"; + if (i < rows[0].size() - 1) std::cout << "+"; } - std::cout << "┤" << std::endl; + std::cout << "+" << std::endl; } // Print rows for (size_t row_idx = 1; row_idx < rows.size(); ++row_idx) { const auto& row = rows[row_idx]; - std::cout << "│"; + std::cout << "|"; for (size_t i = 0; i < row.size(); ++i) { // Set the appropriate color for the row std::string rowcolor = (row_idx % 2 == 1) ? "\033[38;5;142m" : "\033[38;5;250m"; std::cout << width_print_left(row[i],col_widths[i]+2,rowcolor); - std::cout << "\033[90m" << "│"; + std::cout << "\033[90m" << "|"; } std::cout << std::endl; // Print row separator if not the last row if (row_idx < rows.size() - 1 && !mCompact) { - std::cout << "├"; + std::cout << "+"; for (size_t i = 0; i < rows[0].size(); ++i) { for (size_t j = 0; j < col_widths[i] + 2; ++j) { std::cout << "-"; } - if (i < rows[0].size() - 1) std::cout << "┼"; + if (i < rows[0].size() - 1) std::cout << "+"; } - std::cout << "┤" << std::endl; + std::cout << "+" << std::endl; } } // Print bottom border - std::cout << "└"; + std::cout << "+"; for (size_t i = 0; i < rows[0].size(); ++i) { for (size_t j = 0; j < col_widths[i] + 2; ++j) { std::cout << "-"; } - if (i < rows[0].size() - 1) std::cout << "┴"; + if (i < rows[0].size() - 1) std::cout << "+"; } - std::cout << "┘" << "\033[0m" << std::endl; // Reset color + std::cout << "+" << "\033[0m" << std::endl; // Reset color }