Merge pull request #7 from m1ngsama/feature/tui-interface-enhancement

feat: Enhance TUI interface with modern design and ASCII compatibility
This commit is contained in:
m1ng 2025-11-20 10:13:01 +08:00 committed by GitHub
commit a276295db1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -11,25 +11,109 @@
namespace {
// Define color pairs
// Define color pairs - Modern btop-inspired color scheme
enum ColorPairs {
NORMAL_TEXT = 1,
SHADOW_TEXT,
BANNER_TEXT,
SELECTED_ITEM
NORMAL_TEXT = 1, // Default white text
SHADOW_TEXT, // Dim shadow text
BANNER_TEXT, // Bright cyan for banners
SELECTED_ITEM, // Bright yellow for selected items
BORDER_LINE, // Gray borders and boxes
SUCCESS_TEXT, // Green for success states
WARNING_TEXT, // Orange/yellow for warnings
ERROR_TEXT, // Red for errors
INFO_TEXT, // Blue for information
ACCENT_TEXT, // Magenta for accents
DIM_TEXT, // Dimmed secondary text
PROGRESS_BAR, // Green progress bars
CALENDAR_HEADER, // Calendar header styling
EVENT_PAST, // Grayed out past events
EVENT_TODAY, // Highlighted today's events
EVENT_UPCOMING // Default upcoming events
};
void init_colors() {
if (has_colors()) {
start_color();
use_default_colors(); // Use terminal's default background
init_pair(NORMAL_TEXT, COLOR_WHITE, -1); // White foreground, default background
init_pair(SHADOW_TEXT, COLOR_BLACK, -1); // Black foreground, default background (for shadow effect)
init_pair(BANNER_TEXT, COLOR_CYAN, -1); // Cyan foreground, default background
init_pair(SELECTED_ITEM, COLOR_YELLOW, -1); // Yellow foreground, default background
// Modern color scheme inspired by btop
init_pair(NORMAL_TEXT, COLOR_WHITE, -1); // White text
init_pair(SHADOW_TEXT, COLOR_BLACK, -1); // Black shadow text
init_pair(BANNER_TEXT, COLOR_CYAN, -1); // Bright cyan for banners
init_pair(SELECTED_ITEM, COLOR_YELLOW, -1); // Bright yellow selection
init_pair(BORDER_LINE, COLOR_BLUE, -1); // Blue borders/boxes
init_pair(SUCCESS_TEXT, COLOR_GREEN, -1); // Green for success
init_pair(WARNING_TEXT, COLOR_YELLOW, -1); // Orange/yellow warnings
init_pair(ERROR_TEXT, COLOR_RED, -1); // Red for errors
init_pair(INFO_TEXT, COLOR_BLUE, -1); // Blue for info
init_pair(ACCENT_TEXT, COLOR_MAGENTA, -1); // Magenta accents
init_pair(DIM_TEXT, COLOR_BLACK, -1); // Dimmed text
init_pair(PROGRESS_BAR, COLOR_GREEN, -1); // Green progress
init_pair(CALENDAR_HEADER, COLOR_CYAN, -1); // Calendar headers
init_pair(EVENT_PAST, COLOR_BLACK, -1); // Grayed past events
init_pair(EVENT_TODAY, COLOR_YELLOW, -1); // Today's events highlighted
init_pair(EVENT_UPCOMING, COLOR_WHITE, -1); // Default upcoming events
}
}
// Helper function to draw a box
void draw_box(int start_y, int start_x, int width, int height, bool shadow = false) {
if (shadow) {
// Draw shadow first
attron(COLOR_PAIR(SHADOW_TEXT));
for (int i = 0; i < height; i++) {
mvprintw(start_y + i + 1, start_x + 1, "%s", std::string(width, ' ').c_str());
}
attroff(COLOR_PAIR(SHADOW_TEXT));
}
attron(COLOR_PAIR(BORDER_LINE));
// Draw corners
mvprintw(start_y, start_x, "");
mvprintw(start_y, start_x + width - 1, "");
mvprintw(start_y + height - 1, start_x, "");
mvprintw(start_y + height - 1, start_x + width - 1, "");
// Draw horizontal lines
for (int i = 1; i < width - 1; i++) {
mvprintw(start_y, start_x + i, "");
mvprintw(start_y + height - 1, start_x + i, "");
}
// Draw vertical lines
for (int i = 1; i < height - 1; i++) {
mvprintw(start_y + i, start_x, "");
mvprintw(start_y + i, start_x + width - 1, "");
}
attroff(COLOR_PAIR(BORDER_LINE));
}
// Helper function to draw a progress bar
void draw_progress_bar(int y, int x, int width, float percentage) {
int filled_width = static_cast<int>(width * percentage);
attron(COLOR_PAIR(BORDER_LINE));
mvprintw(y, x, "[");
mvprintw(y, x + width - 1, "]");
attroff(COLOR_PAIR(BORDER_LINE));
attron(COLOR_PAIR(PROGRESS_BAR));
for (int i = 1; i < filled_width && i < width - 1; i++) {
mvprintw(y, x + i, "");
}
attroff(COLOR_PAIR(PROGRESS_BAR));
}
// Helper function to center text within a box
void draw_centered_text(int y, int box_start_x, int box_width, const std::string& text, int color_pair = NORMAL_TEXT) {
int text_x = box_start_x + (box_width - text.length()) / 2;
if (text_x < box_start_x) text_x = box_start_x;
attron(COLOR_PAIR(color_pair));
mvprintw(y, text_x, "%s", text.c_str());
attroff(COLOR_PAIR(color_pair));
}
std::string format_date(const std::chrono::system_clock::time_point &tp) {
auto tt = std::chrono::system_clock::to_time_t(tp);
std::tm tm{};
@ -43,6 +127,23 @@ std::string format_date(const std::chrono::system_clock::time_point &tp) {
return oss.str();
}
// Helper function to check if event is today, past, or upcoming
int get_event_status(const std::chrono::system_clock::time_point &event_time) {
auto now = std::chrono::system_clock::now();
if (event_time < now) {
return EVENT_PAST;
} else {
// Simple check if it's today (within 24 hours)
auto hours_until_event = std::chrono::duration_cast<std::chrono::hours>(event_time - now);
if (hours_until_event.count() <= 24) {
return EVENT_TODAY;
} else {
return EVENT_UPCOMING;
}
}
}
} // namespace
void run_tui(const std::vector<IcsEvent> &events) {
@ -79,14 +180,11 @@ void run_tui(const std::vector<IcsEvent> &events) {
while (true) {
clear();
// ASCII art banner for "CALENDAR"
// Modern ASCII art banner for "CALENDAR" - smaller and cleaner
std::string calendar_banner[] = {
" ██████╗ █████╗ ██╗ ███████╗███╗ ██╗██████╗ █████╗ ██████╗ ",
"██╔════╝██╔══██╗██║ ██╔════╝████╗ ██║██╔══██╗██╔══██╗██╔══██╗",
"██║ ███████║██║ █████╗ ██╔██╗ ██║██║ ██║███████║██████╔╝",
"██║ ██╔══██║██║ ██╔══╝ ██║╚██╗██║██║ ██║██╔══██║██╔══██╗",
"╚██████╗██║ ██║███████╗███████╗██║ ╚████║██████╔╝██║ ██║██║ ██║",
" ╚═════╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═╝╚╝ ╚═╝"
" ╔═══════════════════════════════════╗ ",
" ║ [CAL] NBTCA CALENDAR [CAL] ║ ",
" ╚═══════════════════════════════════╝ "
};
int banner_height = sizeof(calendar_banner) / sizeof(calendar_banner[0]);
@ -109,51 +207,131 @@ void run_tui(const std::vector<IcsEvent> &events) {
}
attroff(COLOR_PAIR(BANNER_TEXT));
attron(COLOR_PAIR(NORMAL_TEXT));
std::string instruction_msg = "(q 退出, ↑↓ 滚动)";
mvprintw(banner_height + 1, (width - instruction_msg.length()) / 2, "%s", instruction_msg.c_str());
attroff(COLOR_PAIR(NORMAL_TEXT));
// Draw status bar with current date and event count
attron(COLOR_PAIR(BORDER_LINE));
mvprintw(banner_height + 1, 0, "");
mvprintw(banner_height + 1, width - 1, "");
for (int i = 1; i < width - 1; i++) {
mvprintw(banner_height + 1, i, "");
}
attroff(COLOR_PAIR(BORDER_LINE));
// Status information
auto now = std::chrono::system_clock::now();
auto tt = std::chrono::system_clock::to_time_t(now);
std::tm tm{};
#if defined(_WIN32)
localtime_s(&tm, &tt);
#else
localtime_r(&tt, &tm);
#endif
std::ostringstream oss;
oss << std::put_time(&tm, "%Y-%m-%d %A");
std::string current_date = "Today: " + oss.str() + " | Events: " + std::to_string(events.size());
attron(COLOR_PAIR(INFO_TEXT));
mvprintw(banner_height + 1, 2, "%s", current_date.c_str());
attroff(COLOR_PAIR(INFO_TEXT));
attron(COLOR_PAIR(DIM_TEXT));
std::string instruction_msg = "[q:Exit ↑↓:Scroll]";
mvprintw(banner_height + 1, width - instruction_msg.length() - 2, "%s", instruction_msg.c_str());
attroff(COLOR_PAIR(DIM_TEXT));
int start_event_row = banner_height + 3; // Start events below the banner and instruction
int visibleLines = height - start_event_row - 1; // Adjust visible lines
int visibleLines = height - start_event_row - 2; // Leave space for bottom border
// Draw main event container box
draw_box(start_event_row - 1, 0, width, visibleLines + 2, true);
// Header for events list
attron(COLOR_PAIR(CALENDAR_HEADER));
mvprintw(start_event_row, 2, "╓ Upcoming Events");
attroff(COLOR_PAIR(CALENDAR_HEADER));
int events_start_row = start_event_row + 1;
int events_visible_lines = visibleLines - 2;
if (selected < top) {
top = selected;
} else if (selected >= top + visibleLines) {
top = selected - visibleLines + 1;
} else if (selected >= top + events_visible_lines) {
top = selected - events_visible_lines + 1;
}
for (int i = 0; i < visibleLines; ++i) {
for (int i = 0; i < events_visible_lines; ++i) {
int idx = top + i;
if (idx >= static_cast<int>(events.size())) break;
const auto &ev = events[idx];
std::string line = format_date(ev.start) + " " + ev.summary;
int event_status = get_event_status(ev.start);
// Event icon based on status
std::string icon = "";
if (event_status == EVENT_TODAY) {
icon = "*";
} else if (event_status == EVENT_PAST) {
icon = "v";
}
// Format date more compactly
auto tt = std::chrono::system_clock::to_time_t(ev.start);
std::tm tm{};
#if defined(_WIN32)
localtime_s(&tm, &tt);
#else
localtime_r(&tt, &tm);
#endif
std::ostringstream oss;
oss << std::put_time(&tm, "%m/%d %H:%M");
std::string date_str = oss.str();
// Build the event line
std::string line = icon + " " + date_str + " " + ev.summary;
if (!ev.location.empty()) {
line += " @" + ev.location;
}
if ((int)line.size() > width - 1) {
line.resize(width - 1);
// Truncate if too long
if ((int)line.length() > width - 4) {
line.resize(width - 4);
}
// Determine colors based on event status and selection
int text_color = (event_status == EVENT_PAST) ? DIM_TEXT :
(event_status == EVENT_TODAY) ? EVENT_TODAY :
EVENT_UPCOMING;
if (idx == selected) {
attron(A_REVERSE | COLOR_PAIR(SELECTED_ITEM));
mvprintw(start_event_row + i, 0, "%s", line.c_str());
mvprintw(events_start_row + i, 2, "%s", std::string(width - 4, ' ').c_str());
mvprintw(events_start_row + i, 3, "%s", line.c_str());
attroff(A_REVERSE | COLOR_PAIR(SELECTED_ITEM));
} else {
attron(COLOR_PAIR(NORMAL_TEXT));
mvprintw(start_event_row + i, 0, "%s", line.c_str());
attroff(COLOR_PAIR(NORMAL_TEXT));
attron(COLOR_PAIR(text_color));
mvprintw(events_start_row + i, 3, "%s", line.c_str());
attroff(COLOR_PAIR(text_color));
}
// Add a separator after each event
if (i < visibleLines - 1 && idx + 1 < static_cast<int>(events.size())) {
attron(COLOR_PAIR(SHADOW_TEXT));
mvprintw(start_event_row + i + 1, 0, "%s", std::string(width, '-').c_str()); // Dynamic separator
attroff(COLOR_PAIR(SHADOW_TEXT));
// Add a subtle separator
if (i < events_visible_lines - 1 && idx + 1 < static_cast<int>(events.size())) {
attron(COLOR_PAIR(BORDER_LINE));
mvprintw(events_start_row + i + 1, 2, "");
for (int j = 3; j < width - 2; j++) {
mvprintw(events_start_row + i + 1, j, "");
}
mvprintw(events_start_row + i + 1, width - 2, "");
attroff(COLOR_PAIR(BORDER_LINE));
}
}
// Add scroll indicator if there are more events
if (events.size() > events_visible_lines) {
float scroll_percentage = static_cast<float>(top + events_visible_lines) / events.size();
int scroll_y = start_event_row + 1;
int scroll_x = width - 3;
draw_progress_bar(scroll_y, scroll_x, 1, scroll_percentage);
}
refresh();
int ch = getch();
@ -189,14 +367,11 @@ int run_portal_tui() {
while (choice == -1) {
clear();
// ASCII art banner for "NBTCA Tools"
// Modern ASCII art banner for "NBTCA Tools" - smaller and cleaner
std::string banner_art[] = {
"███╗ ██╗██████╗ ████████╗ ██████╗ █████╗ ████████╗ ██████╗ ██████╗ ██╗ ███████╗",
"████╗ ██║██╔══██╗╚══██╔══╝██╔════╝██╔══██╗ ╚══██╔══╝██╔═══██╗██╔═══██╗██║ ██╔════╝",
"██╔██╗ ██║██████╔╝ ██║ ██║ ███████║ ██║ ██║ ██║██║ ██║██║ ███████╗",
"██║╚██╗██║██╔══██╗ ██║ ██║ ██╔══██║ ██║ ██║ ██║██║ ██║██║ ╚════██║",
"██║ ╚████║██████╔╝ ██║ ╚██████╗██║ ██║ ██║ ╚██████╔╝╚██████╔╝███████╗███████║",
"╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝"
" ╔══════════════════════════════════════╗ ",
" ║ [TOOL] NBTCA UTILITY TOOLS [TOOL] ║ ",
" ╚══════════════════════════════════════╝ "
};
int banner_height = sizeof(banner_art) / sizeof(banner_art[0]);
@ -219,22 +394,79 @@ int run_portal_tui() {
}
attroff(COLOR_PAIR(BANNER_TEXT));
attron(COLOR_PAIR(NORMAL_TEXT));
// Feature Portal message below the banner
std::string portal_msg = "Feature Portal (q or Enter to select)";
mvprintw(banner_height + 1, (width - portal_msg.length()) / 2, "%s", portal_msg.c_str());
attroff(COLOR_PAIR(NORMAL_TEXT));
// Draw info bar
attron(COLOR_PAIR(BORDER_LINE));
mvprintw(banner_height + 1, 0, "");
mvprintw(banner_height + 1, width - 1, "");
for (int i = 1; i < width - 1; i++) {
mvprintw(banner_height + 1, i, "");
}
attroff(COLOR_PAIR(BORDER_LINE));
// Status information
auto now = std::chrono::system_clock::now();
auto tt = std::chrono::system_clock::to_time_t(now);
std::tm tm{};
#if defined(_WIN32)
localtime_s(&tm, &tt);
#else
localtime_r(&tt, &tm);
#endif
std::ostringstream oss;
oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
std::string current_time = "Current: " + oss.str();
attron(COLOR_PAIR(INFO_TEXT));
mvprintw(banner_height + 1, 2, "%s", current_time.c_str());
attroff(COLOR_PAIR(INFO_TEXT));
attron(COLOR_PAIR(DIM_TEXT));
std::string help_msg = "[↑↓:Navigate Enter:Select q:Exit]";
mvprintw(banner_height + 1, width - help_msg.length() - 2, "%s", help_msg.c_str());
attroff(COLOR_PAIR(DIM_TEXT));
// Draw menu box
int menu_box_y = banner_height + 3;
int menu_box_height = menu_items.size() + 4;
int menu_box_width = 30;
int menu_box_x = (width - menu_box_width) / 2;
if (menu_box_x < 2) menu_box_x = 2;
draw_box(menu_box_y, menu_box_x, menu_box_width, menu_box_height, true);
// Menu box title
attron(COLOR_PAIR(CALENDAR_HEADER));
draw_centered_text(menu_box_y + 1, menu_box_x, menu_box_width, "Select Module");
attroff(COLOR_PAIR(CALENDAR_HEADER));
// Draw menu items with icons
for (size_t i = 0; i < menu_items.size(); ++i) {
std::string display_item = menu_items[i];
// Add icons to menu items
if (display_item == "Calendar") {
display_item = "[CAL] " + display_item;
} else if (display_item == "Exit") {
display_item = "[X] " + display_item;
}
int item_y = menu_box_y + 2 + i;
if ((int)i == selected) {
// Draw selection highlight box
attron(COLOR_PAIR(BORDER_LINE));
mvprintw(item_y, menu_box_x + 1, "");
mvprintw(item_y, menu_box_x + menu_box_width - 2, "");
attroff(COLOR_PAIR(BORDER_LINE));
attron(A_REVERSE | COLOR_PAIR(SELECTED_ITEM));
draw_centered_text(item_y, menu_box_x, menu_box_width, display_item, SELECTED_ITEM);
attroff(A_REVERSE | COLOR_PAIR(SELECTED_ITEM));
} else {
attron(COLOR_PAIR(NORMAL_TEXT));
draw_centered_text(item_y, menu_box_x, menu_box_width, display_item);
attroff(COLOR_PAIR(NORMAL_TEXT));
}
// Adjust vertical position based on banner height and portal message
mvprintw(banner_height + 3 + i, 2, "%s", menu_items[i].c_str());
attroff(A_REVERSE | COLOR_PAIR(SELECTED_ITEM));
attroff(COLOR_PAIR(NORMAL_TEXT));
}
refresh();
@ -279,48 +511,86 @@ void display_splash_screen() {
int height, width;
getmaxyx(stdscr, height, width);
clear();
// Animated splash screen with progress bar
for (int frame = 0; frame < 20; ++frame) {
clear();
// Simple ASCII art for "NBTCA Tools"
std::string splash_art[] = {
"███╗ ██╗██████╗ ████████╗ ██████╗ █████╗ ████████╗ ██████╗ ██████╗ ██╗ ███████╗",
"████╗ ██║██╔══██╗╚══██╔══╝██╔════╝██╔══██╗ ╚══██╔══╝██╔═══██╗██╔═══██╗██║ ██╔════╝",
"██╔██╗ ██║██████╔╝ ██║ ██║ ███████║ ██║ ██║ ██║██║ ██║██║ ███████╗",
"██║╚██╗██║██╔══██╗ ██║ ██║ ██╔══██║ ██║ ██║ ██║██║ ██║██║ ╚════██║",
"██║ ╚████║██████╔╝ ██║ ╚██████╗██║ ██║ ██║ ╚██████╔╝╚██████╔╝███████╗███████║",
"╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝"
};
// Modern ASCII art for "NBTCA Tools" - cleaner design
std::string splash_art[] = {
" ╔══════════════════════════════════════╗ ",
" ║ [TOOL] NBTCA UTILITY TOOLS [TOOL] ║ ",
" ╚══════════════════════════════════════╝ "
};
int art_height = sizeof(splash_art) / sizeof(splash_art[0]);
int art_width = splash_art[0].length();
int art_height = sizeof(splash_art) / sizeof(splash_art[0]);
int art_width = splash_art[0].length();
int start_row = (height - art_height) / 2;
int start_col = (width - art_width) / 2;
int start_row = (height - art_height) / 2 - 3;
int start_col = (width - art_width) / 2;
if (start_row < 0) start_row = 0;
if (start_col < 0) start_col = 0;
if (start_row < 0) start_row = 0;
if (start_col < 0) start_col = 0;
// Draw shadow
attron(COLOR_PAIR(SHADOW_TEXT));
for (int i = 0; i < art_height; ++i) {
mvprintw(start_row + i + 1, start_col + 1, "%s", splash_art[i].c_str());
// Draw shadow
attron(COLOR_PAIR(SHADOW_TEXT));
for (int i = 0; i < art_height; ++i) {
mvprintw(start_row + i + 1, start_col + 1, "%s", splash_art[i].c_str());
}
attroff(COLOR_PAIR(SHADOW_TEXT));
// Draw main art
attron(COLOR_PAIR(BANNER_TEXT));
for (int i = 0; i < art_height; ++i) {
mvprintw(start_row + i, start_col, "%s", splash_art[i].c_str());
}
attroff(COLOR_PAIR(BANNER_TEXT));
// Version info
attron(COLOR_PAIR(INFO_TEXT));
draw_centered_text(start_row + art_height + 1, start_col, art_width, "Version 0.0.1");
attroff(COLOR_PAIR(INFO_TEXT));
// Loading text with rotating animation
const char* spinner[] = {"|", "/", "-", "\\"};
std::string loading_msg = std::string(spinner[frame % 4]) + " Initializing system components...";
attron(COLOR_PAIR(NORMAL_TEXT));
draw_centered_text(start_row + art_height + 3, start_col, art_width, loading_msg);
attroff(COLOR_PAIR(NORMAL_TEXT));
// Progress bar
int progress_bar_y = start_row + art_height + 5;
int progress_bar_width = 40;
int progress_bar_x = (width - progress_bar_width) / 2;
float progress = static_cast<float>(frame) / 19.0f;
draw_progress_bar(progress_bar_y, progress_bar_x, progress_bar_width, progress);
// Progress percentage
attron(COLOR_PAIR(SUCCESS_TEXT));
std::string progress_text = std::to_string(static_cast<int>(progress * 100)) + "% Complete";
draw_centered_text(progress_bar_y + 1, progress_bar_x, progress_bar_width, progress_text);
attroff(COLOR_PAIR(SUCCESS_TEXT));
// Status messages
std::vector<std::string> status_msgs = {
"Loading calendar module...",
"Initializing network stack...",
"Fetching latest events...",
"Preparing user interface...",
"System ready!"
};
int msg_index = (frame * status_msgs.size()) / 20;
if (msg_index >= status_msgs.size()) msg_index = status_msgs.size() - 1;
attron(COLOR_PAIR(DIM_TEXT));
draw_centered_text(progress_bar_y + 2, progress_bar_x, progress_bar_width, status_msgs[msg_index]);
attroff(COLOR_PAIR(DIM_TEXT));
refresh();
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 100ms per frame
}
attroff(COLOR_PAIR(SHADOW_TEXT));
// Draw main art
attron(COLOR_PAIR(BANNER_TEXT));
for (int i = 0; i < art_height; ++i) {
mvprintw(start_row + i, start_col, "%s", splash_art[i].c_str());
}
attroff(COLOR_PAIR(BANNER_TEXT));
attron(COLOR_PAIR(NORMAL_TEXT));
std::string loading_msg = "Loading...";
mvprintw(start_row + art_height + 2, (width - loading_msg.length()) / 2, "%s", loading_msg.c_str());
attroff(COLOR_PAIR(NORMAL_TEXT));
refresh();
std::this_thread::sleep_for(std::chrono::seconds(2)); // Display for 2 seconds
endwin();
}