mirror of
https://github.com/m1ngsama/TUT.git
synced 2025-12-24 10:51:46 +00:00
Merge feat/browser-interaction-enhancements: Major simplification following Unix philosophy
This commit is contained in:
commit
feefbfcf90
16 changed files with 216 additions and 1059 deletions
15
.gitignore
vendored
15
.gitignore
vendored
|
|
@ -1,6 +1,21 @@
|
||||||
|
# Build artifacts
|
||||||
build/
|
build/
|
||||||
*.o
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Executables
|
||||||
tut
|
tut
|
||||||
|
nbtca_tui
|
||||||
|
|
||||||
|
# CMake artifacts
|
||||||
|
CMakeCache.txt
|
||||||
|
CMakeFiles/
|
||||||
|
cmake_install.cmake
|
||||||
|
install_manifest.txt
|
||||||
|
|
||||||
|
# Editor/IDE
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
cmake_minimum_required(VERSION 3.15)
|
cmake_minimum_required(VERSION 3.15)
|
||||||
project(TUT LANGUAGES CXX)
|
project(TUT VERSION 1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
# 优先使用带宽字符支持的 ncursesw
|
# Prefer wide character support (ncursesw)
|
||||||
set(CURSES_NEED_WIDE TRUE)
|
set(CURSES_NEED_WIDE TRUE)
|
||||||
|
|
||||||
# macOS: Homebrew ncurses 路径
|
# macOS: Use Homebrew ncurses if available
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
set(CMAKE_PREFIX_PATH "/opt/homebrew/opt/ncurses" ${CMAKE_PREFIX_PATH})
|
set(CMAKE_PREFIX_PATH "/opt/homebrew/opt/ncurses" ${CMAKE_PREFIX_PATH})
|
||||||
endif()
|
endif()
|
||||||
|
|
@ -15,6 +15,7 @@ endif()
|
||||||
find_package(Curses REQUIRED)
|
find_package(Curses REQUIRED)
|
||||||
find_package(CURL REQUIRED)
|
find_package(CURL REQUIRED)
|
||||||
|
|
||||||
|
# Executable
|
||||||
add_executable(tut
|
add_executable(tut
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/http_client.cpp
|
src/http_client.cpp
|
||||||
|
|
@ -27,4 +28,14 @@ add_executable(tut
|
||||||
target_include_directories(tut PRIVATE ${CURSES_INCLUDE_DIR})
|
target_include_directories(tut PRIVATE ${CURSES_INCLUDE_DIR})
|
||||||
target_link_libraries(tut PRIVATE ${CURSES_LIBRARIES} CURL::libcurl)
|
target_link_libraries(tut PRIVATE ${CURSES_LIBRARIES} CURL::libcurl)
|
||||||
|
|
||||||
|
# Compiler warnings
|
||||||
|
target_compile_options(tut PRIVATE
|
||||||
|
-Wall -Wextra -Wpedantic
|
||||||
|
$<$<CONFIG:RELEASE>:-O2>
|
||||||
|
$<$<CONFIG:DEBUG>:-g -O0>
|
||||||
|
)
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
install(TARGETS tut DESTINATION bin)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
54
Makefile
54
Makefile
|
|
@ -1,44 +1,28 @@
|
||||||
# Makefile for TUT Browser
|
# Simple Makefile wrapper for CMake build system
|
||||||
|
# Follows Unix convention: simple interface to underlying build
|
||||||
|
|
||||||
CXX = clang++
|
BUILD_DIR = build
|
||||||
CXXFLAGS = -std=c++17 -Wall -Wextra -O2
|
|
||||||
LDFLAGS = -lncurses -lcurl
|
|
||||||
|
|
||||||
# 源文件
|
|
||||||
SOURCES = src/main.cpp \
|
|
||||||
src/http_client.cpp \
|
|
||||||
src/html_parser.cpp \
|
|
||||||
src/text_renderer.cpp \
|
|
||||||
src/input_handler.cpp \
|
|
||||||
src/browser.cpp
|
|
||||||
|
|
||||||
# 目标文件
|
|
||||||
OBJECTS = $(SOURCES:.cpp=.o)
|
|
||||||
|
|
||||||
# 可执行文件
|
|
||||||
TARGET = tut
|
TARGET = tut
|
||||||
|
|
||||||
# 默认目标
|
.PHONY: all clean install test help
|
||||||
all: $(TARGET)
|
|
||||||
|
|
||||||
# 链接
|
all:
|
||||||
$(TARGET): $(OBJECTS)
|
@mkdir -p $(BUILD_DIR)
|
||||||
$(CXX) $(OBJECTS) $(LDFLAGS) -o $(TARGET)
|
@cd $(BUILD_DIR) && cmake .. && cmake --build .
|
||||||
|
@cp $(BUILD_DIR)/$(TARGET) .
|
||||||
|
|
||||||
# 编译
|
|
||||||
%.o: %.cpp
|
|
||||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
|
||||||
|
|
||||||
# 清理
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(OBJECTS) $(TARGET)
|
@rm -rf $(BUILD_DIR) $(TARGET)
|
||||||
|
|
||||||
# 运行
|
install: all
|
||||||
run: $(TARGET)
|
@install -m 755 $(TARGET) /usr/local/bin/
|
||||||
./$(TARGET)
|
|
||||||
|
|
||||||
# 安装
|
test: all
|
||||||
install: $(TARGET)
|
@./$(TARGET) https://example.com
|
||||||
install -m 755 $(TARGET) /usr/local/bin/
|
|
||||||
|
|
||||||
.PHONY: all clean run install
|
help:
|
||||||
|
@echo "TUT Browser - Simple make targets"
|
||||||
|
@echo " make - Build the browser"
|
||||||
|
@echo " make clean - Remove build artifacts"
|
||||||
|
@echo " make install - Install to /usr/local/bin"
|
||||||
|
@echo " make test - Quick test run"
|
||||||
|
|
|
||||||
18
README.md
18
README.md
|
|
@ -88,6 +88,24 @@ KEYBINDINGS
|
||||||
**N**
|
**N**
|
||||||
Jump to previous search match.
|
Jump to previous search match.
|
||||||
|
|
||||||
|
### Marks
|
||||||
|
|
||||||
|
**m***[a-z]*
|
||||||
|
Set mark at current position (e.g., **ma**, **mb**).
|
||||||
|
|
||||||
|
**'***[a-z]*
|
||||||
|
Jump to mark (e.g., **'a**, **'b**).
|
||||||
|
|
||||||
|
### Mouse
|
||||||
|
|
||||||
|
**Left Click**
|
||||||
|
Click on links to follow them directly.
|
||||||
|
|
||||||
|
**Scroll Wheel Up/Down**
|
||||||
|
Scroll page up or down.
|
||||||
|
|
||||||
|
Works with most modern terminal emulators that support mouse events.
|
||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
Press **:** to enter command mode. Available commands:
|
Press **:** to enter command mode. Available commands:
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include <clocale>
|
#include <clocale>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
class Browser::Impl {
|
class Browser::Impl {
|
||||||
public:
|
public:
|
||||||
|
|
@ -26,6 +27,9 @@ public:
|
||||||
int screen_height = 0;
|
int screen_height = 0;
|
||||||
int screen_width = 0;
|
int screen_width = 0;
|
||||||
|
|
||||||
|
// Marks support (vim-style position bookmarks)
|
||||||
|
std::map<char, int> marks;
|
||||||
|
|
||||||
void init_screen() {
|
void init_screen() {
|
||||||
setlocale(LC_ALL, "");
|
setlocale(LC_ALL, "");
|
||||||
initscr();
|
initscr();
|
||||||
|
|
@ -35,6 +39,11 @@ public:
|
||||||
keypad(stdscr, TRUE);
|
keypad(stdscr, TRUE);
|
||||||
curs_set(0);
|
curs_set(0);
|
||||||
timeout(0);
|
timeout(0);
|
||||||
|
|
||||||
|
// Enable mouse support
|
||||||
|
mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, NULL);
|
||||||
|
mouseinterval(0); // No click delay
|
||||||
|
|
||||||
getmaxyx(stdscr, screen_height, screen_width);
|
getmaxyx(stdscr, screen_height, screen_width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,6 +82,53 @@ public:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void handle_mouse(MEVENT& event) {
|
||||||
|
int visible_lines = screen_height - 2;
|
||||||
|
|
||||||
|
// Mouse wheel up (scroll up)
|
||||||
|
if (event.bstate & BUTTON4_PRESSED) {
|
||||||
|
scroll_pos = std::max(0, scroll_pos - 3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse wheel down (scroll down)
|
||||||
|
if (event.bstate & BUTTON5_PRESSED) {
|
||||||
|
int max_scroll = std::max(0, static_cast<int>(rendered_lines.size()) - visible_lines);
|
||||||
|
scroll_pos = std::min(max_scroll, scroll_pos + 3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left click
|
||||||
|
if (event.bstate & BUTTON1_CLICKED) {
|
||||||
|
int clicked_line = event.y;
|
||||||
|
int clicked_col = event.x;
|
||||||
|
|
||||||
|
// Check if clicked on a link
|
||||||
|
if (clicked_line >= 0 && clicked_line < visible_lines) {
|
||||||
|
int doc_line_idx = scroll_pos + clicked_line;
|
||||||
|
if (doc_line_idx < static_cast<int>(rendered_lines.size())) {
|
||||||
|
const auto& line = rendered_lines[doc_line_idx];
|
||||||
|
|
||||||
|
// Check if click is within any link range
|
||||||
|
for (const auto& [start, end] : line.link_ranges) {
|
||||||
|
if (clicked_col >= static_cast<int>(start) && clicked_col < static_cast<int>(end)) {
|
||||||
|
// Clicked on a link!
|
||||||
|
if (line.link_index >= 0 && line.link_index < static_cast<int>(current_doc.links.size())) {
|
||||||
|
load_page(current_doc.links[line.link_index].url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If clicked on a line with a link but not on the link text itself
|
||||||
|
if (line.is_link && line.link_index >= 0) {
|
||||||
|
current_link = line.link_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void draw_status_bar() {
|
void draw_status_bar() {
|
||||||
attron(COLOR_PAIR(COLOR_STATUS_BAR));
|
attron(COLOR_PAIR(COLOR_STATUS_BAR));
|
||||||
mvprintw(screen_height - 1, 0, "%s", std::string(screen_width, ' ').c_str());
|
mvprintw(screen_height - 1, 0, "%s", std::string(screen_width, ' ').c_str());
|
||||||
|
|
@ -378,6 +434,27 @@ public:
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case Action::SET_MARK:
|
||||||
|
if (!result.text.empty()) {
|
||||||
|
char mark = result.text[0];
|
||||||
|
marks[mark] = scroll_pos;
|
||||||
|
status_message = "Mark '" + std::string(1, mark) + "' set at line " + std::to_string(scroll_pos);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Action::GOTO_MARK:
|
||||||
|
if (!result.text.empty()) {
|
||||||
|
char mark = result.text[0];
|
||||||
|
auto it = marks.find(mark);
|
||||||
|
if (it != marks.end()) {
|
||||||
|
scroll_pos = std::min(it->second, max_scroll);
|
||||||
|
status_message = "Jumped to mark '" + std::string(1, mark) + "'";
|
||||||
|
} else {
|
||||||
|
status_message = "Mark '" + std::string(1, mark) + "' not set";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case Action::HELP:
|
case Action::HELP:
|
||||||
show_help();
|
show_help();
|
||||||
break;
|
break;
|
||||||
|
|
@ -429,10 +506,18 @@ public:
|
||||||
<< "<p>:r or :refresh - Refresh page</p>"
|
<< "<p>:r or :refresh - Refresh page</p>"
|
||||||
<< "<p>:h or :help - Show this help</p>"
|
<< "<p>:h or :help - Show this help</p>"
|
||||||
<< "<p>:[number] - Go to line number</p>"
|
<< "<p>:[number] - Go to line number</p>"
|
||||||
|
<< "<h2>Marks</h2>"
|
||||||
|
<< "<p>m[a-z]: Set mark at letter (e.g., ma, mb)</p>"
|
||||||
|
<< "<p>'[a-z]: Jump to mark (e.g., 'a, 'b)</p>"
|
||||||
|
<< "<h2>Mouse Support</h2>"
|
||||||
|
<< "<p>Click on links to follow them</p>"
|
||||||
|
<< "<p>Scroll wheel to scroll up/down</p>"
|
||||||
|
<< "<p>Works with most terminal emulators</p>"
|
||||||
<< "<h2>Other</h2>"
|
<< "<h2>Other</h2>"
|
||||||
<< "<p>r: Refresh current page</p>"
|
<< "<p>r: Refresh current page</p>"
|
||||||
<< "<p>q: Quit browser</p>"
|
<< "<p>q: Quit browser</p>"
|
||||||
<< "<p>?: Show help</p>"
|
<< "<p>?: Show help</p>"
|
||||||
|
<< "<p>ESC: Cancel current mode</p>"
|
||||||
<< "<h2>Important Limitations</h2>"
|
<< "<h2>Important Limitations</h2>"
|
||||||
<< "<p><strong>JavaScript/SPA Websites:</strong> This browser cannot execute JavaScript. "
|
<< "<p><strong>JavaScript/SPA Websites:</strong> This browser cannot execute JavaScript. "
|
||||||
<< "Single Page Applications (SPAs) built with React, Vue, Angular, etc. will not work properly "
|
<< "Single Page Applications (SPAs) built with React, Vue, Angular, etc. will not work properly "
|
||||||
|
|
@ -489,6 +574,15 @@ void Browser::run(const std::string& initial_url) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle mouse events
|
||||||
|
if (ch == KEY_MOUSE) {
|
||||||
|
MEVENT event;
|
||||||
|
if (getmouse(&event) == OK) {
|
||||||
|
pImpl->handle_mouse(event);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
auto result = pImpl->input_handler.handle_key(ch);
|
auto result = pImpl->input_handler.handle_key(ch);
|
||||||
|
|
||||||
if (result.action == Action::QUIT) {
|
if (result.action == Action::QUIT) {
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
#include "calendar.h"
|
|
||||||
#include <iostream>
|
|
||||||
#include <vector>
|
|
||||||
#include <string>
|
|
||||||
#include <chrono>
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
#include "ics_fetcher.h"
|
|
||||||
#include "ics_parser.h"
|
|
||||||
#include "tui_view.h"
|
|
||||||
|
|
||||||
void Calendar::run() {
|
|
||||||
try {
|
|
||||||
// 1. 获取 ICS 文本
|
|
||||||
std::string url = "https://ical.nbtca.space/nbtca.ics";
|
|
||||||
std::string icsData = fetch_ics(url);
|
|
||||||
|
|
||||||
// 2. 解析事件
|
|
||||||
auto allEvents = parse_ics(icsData);
|
|
||||||
|
|
||||||
// 3. 过滤未来一个月的事件(支持简单的每周 RRULE)
|
|
||||||
auto now = std::chrono::system_clock::now();
|
|
||||||
auto oneMonthLater = now + std::chrono::hours(24 * 30);
|
|
||||||
|
|
||||||
std::vector<IcsEvent> upcoming;
|
|
||||||
for (const auto &ev : allEvents) {
|
|
||||||
// 简单处理:如果包含 FREQ=WEEKLY,则视为每周重复事件
|
|
||||||
if (ev.rrule.find("FREQ=WEEKLY") != std::string::npos) {
|
|
||||||
// 从基准时间往后每 7 天生成一次,直到超过 oneMonthLater 或 UNTIL
|
|
||||||
auto curStart = ev.start;
|
|
||||||
auto curEnd = ev.end;
|
|
||||||
|
|
||||||
// 如果有 UNTIL,则作为上界
|
|
||||||
auto upper = oneMonthLater;
|
|
||||||
if (ev.until && *ev.until < upper) {
|
|
||||||
upper = *ev.until;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为避免意外死循环,限制最多展开约 10 年(~520 周)
|
|
||||||
const int maxIterations = 520;
|
|
||||||
int iter = 0;
|
|
||||||
|
|
||||||
while (curStart <= upper && iter < maxIterations) {
|
|
||||||
if (curStart >= now && curStart <= upper) {
|
|
||||||
IcsEvent occ = ev;
|
|
||||||
occ.start = curStart;
|
|
||||||
occ.end = curEnd;
|
|
||||||
upcoming.push_back(std::move(occ));
|
|
||||||
}
|
|
||||||
curStart += std::chrono::hours(24 * 7);
|
|
||||||
curEnd += std::chrono::hours(24 * 7);
|
|
||||||
++iter;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 非 RRULE 事件:直接按时间窗口筛选
|
|
||||||
if (ev.start >= now && ev.start <= oneMonthLater) {
|
|
||||||
upcoming.push_back(ev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保展示按时间排序
|
|
||||||
std::sort(upcoming.begin(), upcoming.end(),
|
|
||||||
[](const IcsEvent &a, const IcsEvent &b) { return a.start < b.start; });
|
|
||||||
|
|
||||||
// 4. 启动 TUI 展示(只展示未来一个月的活动)
|
|
||||||
run_tui(upcoming);
|
|
||||||
} catch (const std::exception &ex) {
|
|
||||||
std::cerr << "错误: " << ex.what() << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
class Calendar {
|
|
||||||
public:
|
|
||||||
void run();
|
|
||||||
};
|
|
||||||
|
|
@ -10,7 +10,7 @@ public:
|
||||||
bool keep_code_blocks = true;
|
bool keep_code_blocks = true;
|
||||||
bool keep_lists = true;
|
bool keep_lists = true;
|
||||||
|
|
||||||
// 简单的HTML标签清理
|
// Remove HTML tags
|
||||||
std::string remove_tags(const std::string& html) {
|
std::string remove_tags(const std::string& html) {
|
||||||
std::string result;
|
std::string result;
|
||||||
bool in_tag = false;
|
bool in_tag = false;
|
||||||
|
|
@ -26,12 +26,9 @@ public:
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解码HTML实体
|
// Decode HTML entities (named and numeric)
|
||||||
std::string decode_html_entities(const std::string& text) {
|
std::string decode_html_entities(const std::string& text) {
|
||||||
std::string result = text;
|
static const std::vector<std::pair<std::string, std::string>> named_entities = {
|
||||||
|
|
||||||
// 常见HTML实体
|
|
||||||
const std::vector<std::pair<std::string, std::string>> entities = {
|
|
||||||
{" ", " "},
|
{" ", " "},
|
||||||
{"&", "&"},
|
{"&", "&"},
|
||||||
{"<", "<"},
|
{"<", "<"},
|
||||||
|
|
@ -48,7 +45,10 @@ public:
|
||||||
{"’", "\u2019"}
|
{"’", "\u2019"}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const auto& [entity, replacement] : entities) {
|
std::string result = text;
|
||||||
|
|
||||||
|
// Replace named entities
|
||||||
|
for (const auto& [entity, replacement] : named_entities) {
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
while ((pos = result.find(entity, pos)) != std::string::npos) {
|
while ((pos = result.find(entity, pos)) != std::string::npos) {
|
||||||
result.replace(pos, entity.length(), replacement);
|
result.replace(pos, entity.length(), replacement);
|
||||||
|
|
@ -56,10 +56,51 @@ public:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace numeric entities ({ and «)
|
||||||
|
std::regex numeric_entity(R"(&#(\d+);|&#x([0-9a-fA-F]+);)");
|
||||||
|
std::smatch match;
|
||||||
|
std::string::const_iterator search_start(result.cbegin());
|
||||||
|
std::string temp;
|
||||||
|
size_t last_pos = 0;
|
||||||
|
|
||||||
|
while (std::regex_search(search_start, result.cend(), match, numeric_entity)) {
|
||||||
|
size_t match_pos = match.position(0) + (search_start - result.cbegin());
|
||||||
|
temp += result.substr(last_pos, match_pos - last_pos);
|
||||||
|
|
||||||
|
int code_point = 0;
|
||||||
|
if (match[1].length() > 0) {
|
||||||
|
// Decimal entity
|
||||||
|
code_point = std::stoi(match[1].str());
|
||||||
|
} else if (match[2].length() > 0) {
|
||||||
|
// Hex entity
|
||||||
|
code_point = std::stoi(match[2].str(), nullptr, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to UTF-8 (simplified - only handles ASCII and basic Unicode)
|
||||||
|
if (code_point < 128) {
|
||||||
|
temp += static_cast<char>(code_point);
|
||||||
|
} else if (code_point < 0x800) {
|
||||||
|
temp += static_cast<char>(0xC0 | (code_point >> 6));
|
||||||
|
temp += static_cast<char>(0x80 | (code_point & 0x3F));
|
||||||
|
} else if (code_point < 0x10000) {
|
||||||
|
temp += static_cast<char>(0xE0 | (code_point >> 12));
|
||||||
|
temp += static_cast<char>(0x80 | ((code_point >> 6) & 0x3F));
|
||||||
|
temp += static_cast<char>(0x80 | (code_point & 0x3F));
|
||||||
|
}
|
||||||
|
|
||||||
|
last_pos = match_pos + match.length(0);
|
||||||
|
search_start = result.cbegin() + last_pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!temp.empty()) {
|
||||||
|
temp += result.substr(last_pos);
|
||||||
|
result = temp;
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取标签内容
|
// Extract content between HTML tags
|
||||||
std::string extract_tag_content(const std::string& html, const std::string& tag) {
|
std::string extract_tag_content(const std::string& html, const std::string& tag) {
|
||||||
std::regex tag_regex("<" + tag + "[^>]*>([\\s\\S]*?)</" + tag + ">",
|
std::regex tag_regex("<" + tag + "[^>]*>([\\s\\S]*?)</" + tag + ">",
|
||||||
std::regex::icase);
|
std::regex::icase);
|
||||||
|
|
@ -70,7 +111,7 @@ public:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取所有匹配的标签
|
// Extract all matching tags
|
||||||
std::vector<std::string> extract_all_tags(const std::string& html, const std::string& tag) {
|
std::vector<std::string> extract_all_tags(const std::string& html, const std::string& tag) {
|
||||||
std::vector<std::string> results;
|
std::vector<std::string> results;
|
||||||
std::regex tag_regex("<" + tag + "[^>]*>([\\s\\S]*?)</" + tag + ">",
|
std::regex tag_regex("<" + tag + "[^>]*>([\\s\\S]*?)</" + tag + ">",
|
||||||
|
|
@ -87,7 +128,7 @@ public:
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取链接
|
// Extract links from HTML
|
||||||
std::vector<Link> extract_links(const std::string& html, const std::string& base_url) {
|
std::vector<Link> extract_links(const std::string& html, const std::string& base_url) {
|
||||||
std::vector<Link> links;
|
std::vector<Link> links;
|
||||||
std::regex link_regex(R"(<a\s+[^>]*href\s*=\s*["']([^"']*)["'][^>]*>([\s\S]*?)</a>)",
|
std::regex link_regex(R"(<a\s+[^>]*href\s*=\s*["']([^"']*)["'][^>]*>([\s\S]*?)</a>)",
|
||||||
|
|
@ -204,7 +245,7 @@ public:
|
||||||
return trim(result);
|
return trim(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理空白字符
|
// Trim whitespace
|
||||||
std::string trim(const std::string& str) {
|
std::string trim(const std::string& str) {
|
||||||
auto start = str.begin();
|
auto start = str.begin();
|
||||||
while (start != str.end() && std::isspace(*start)) {
|
while (start != str.end() && std::isspace(*start)) {
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
#include "ics_fetcher.h"
|
|
||||||
|
|
||||||
#include <curl/curl.h>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {
|
|
||||||
auto *buffer = static_cast<std::string *>(userdata);
|
|
||||||
buffer->append(ptr, size * nmemb);
|
|
||||||
return size * nmemb;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
std::string fetch_ics(const std::string &url) {
|
|
||||||
CURL *curl = curl_easy_init();
|
|
||||||
if (!curl) {
|
|
||||||
throw std::runtime_error("初始化 libcurl 失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string response;
|
|
||||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
|
||||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "nbtca_tui/1.0");
|
|
||||||
|
|
||||||
CURLcode res = curl_easy_perform(curl);
|
|
||||||
if (res != CURLE_OK) {
|
|
||||||
std::string err = curl_easy_strerror(res);
|
|
||||||
curl_easy_cleanup(curl);
|
|
||||||
throw std::runtime_error("请求 ICS 失败: " + err);
|
|
||||||
}
|
|
||||||
|
|
||||||
long http_code = 0;
|
|
||||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
|
||||||
curl_easy_cleanup(curl);
|
|
||||||
|
|
||||||
if (http_code < 200 || http_code >= 300) {
|
|
||||||
throw std::runtime_error("HTTP 状态码错误: " + std::to_string(http_code));
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
// 从给定 URL 获取 ICS 文本,失败抛出 std::runtime_error
|
|
||||||
std::string fetch_ics(const std::string &url);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
#include "ics_parser.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cctype>
|
|
||||||
#include <iomanip>
|
|
||||||
#include <optional>
|
|
||||||
#include <sstream>
|
|
||||||
#include <stdexcept>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// 去掉首尾空白
|
|
||||||
std::string trim(const std::string &s) {
|
|
||||||
size_t start = 0;
|
|
||||||
while (start < s.size() && std::isspace(static_cast<unsigned char>(s[start]))) ++start;
|
|
||||||
size_t end = s.size();
|
|
||||||
while (end > start && std::isspace(static_cast<unsigned char>(s[end - 1]))) --end;
|
|
||||||
return s.substr(start, end - start);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将 ICS 中换行折叠处理(以空格或 Tab 开头的行拼接到前一行)
|
|
||||||
std::vector<std::string> unfold_lines(const std::string &text) {
|
|
||||||
std::vector<std::string> lines;
|
|
||||||
std::string current;
|
|
||||||
std::istringstream iss(text);
|
|
||||||
std::string line;
|
|
||||||
while (std::getline(iss, line)) {
|
|
||||||
if (!line.empty() && (line.back() == '\r' || line.back() == '\n')) {
|
|
||||||
line.pop_back();
|
|
||||||
}
|
|
||||||
if (!line.empty() && (line[0] == ' ' || line[0] == '\t')) {
|
|
||||||
// continuation
|
|
||||||
current += trim(line);
|
|
||||||
} else {
|
|
||||||
if (!current.empty()) {
|
|
||||||
lines.push_back(current);
|
|
||||||
}
|
|
||||||
current = line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!current.empty()) {
|
|
||||||
lines.push_back(current);
|
|
||||||
}
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 仅支持几种常见格式:YYYYMMDD 或 YYYYMMDDTHHMMSSZ / 本地时间
|
|
||||||
std::chrono::system_clock::time_point parse_ics_datetime(const std::string &value) {
|
|
||||||
std::tm tm{};
|
|
||||||
if (value.size() == 8) {
|
|
||||||
// 日期
|
|
||||||
std::istringstream ss(value);
|
|
||||||
ss >> std::get_time(&tm, "%Y%m%d");
|
|
||||||
if (ss.fail()) {
|
|
||||||
throw std::runtime_error("无法解析日期: " + value);
|
|
||||||
}
|
|
||||||
tm.tm_hour = 0;
|
|
||||||
tm.tm_min = 0;
|
|
||||||
tm.tm_sec = 0;
|
|
||||||
} else if (value.size() >= 15 && value[8] == 'T') {
|
|
||||||
// 日期时间,如 20250101T090000Z 或无 Z
|
|
||||||
std::string fmt = "%Y%m%dT%H%M%S";
|
|
||||||
std::string v = value;
|
|
||||||
bool hasZ = false;
|
|
||||||
if (!v.empty() && v.back() == 'Z') {
|
|
||||||
hasZ = true;
|
|
||||||
v.pop_back();
|
|
||||||
}
|
|
||||||
std::istringstream ss(v);
|
|
||||||
ss >> std::get_time(&tm, fmt.c_str());
|
|
||||||
if (ss.fail()) {
|
|
||||||
throw std::runtime_error("无法解析日期时间: " + value);
|
|
||||||
}
|
|
||||||
// 这里简单按本地时间处理;如需严格 UTC 可改用 timegm
|
|
||||||
} else {
|
|
||||||
throw std::runtime_error("未知日期格式: " + value);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::time_t t = std::mktime(&tm);
|
|
||||||
return std::chrono::system_clock::from_time_t(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string get_prop_value(const std::string &line) {
|
|
||||||
auto pos = line.find(':');
|
|
||||||
if (pos == std::string::npos) return {};
|
|
||||||
return line.substr(pos + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取属性参数和值,例如 "DTSTART;TZID=Asia/Shanghai:20251121T203000"
|
|
||||||
// 返回 "DTSTART;TZID=Asia/Shanghai" 作为 key,冒号后的部分作为 value。
|
|
||||||
std::pair<std::string, std::string> split_prop(const std::string &line) {
|
|
||||||
auto pos = line.find(':');
|
|
||||||
if (pos == std::string::npos) return {line, {}};
|
|
||||||
return {line.substr(0, pos), line.substr(pos + 1)};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从 RRULE 中提取 UNTIL=... 的值(若存在)
|
|
||||||
std::optional<std::string> extract_until_str(const std::string &rrule) {
|
|
||||||
// 例:RRULE:FREQ=WEEKLY;BYDAY=FR;UNTIL=20260401T000000Z
|
|
||||||
auto pos = rrule.find("UNTIL=");
|
|
||||||
if (pos == std::string::npos) return std::nullopt;
|
|
||||||
pos += 6; // 跳过 "UNTIL="
|
|
||||||
size_t end = rrule.find(';', pos);
|
|
||||||
if (end == std::string::npos) end = rrule.size();
|
|
||||||
if (pos >= rrule.size()) return std::nullopt;
|
|
||||||
return rrule.substr(pos, end - pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool starts_with(const std::string &s, const std::string &prefix) {
|
|
||||||
return s.size() >= prefix.size() && std::equal(prefix.begin(), prefix.end(), s.begin());
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
std::vector<IcsEvent> parse_ics(const std::string &icsText) {
|
|
||||||
auto lines = unfold_lines(icsText);
|
|
||||||
std::vector<IcsEvent> events;
|
|
||||||
|
|
||||||
bool inEvent = false;
|
|
||||||
IcsEvent current{};
|
|
||||||
|
|
||||||
for (const auto &rawLine : lines) {
|
|
||||||
std::string line = trim(rawLine);
|
|
||||||
if (line == "BEGIN:VEVENT") {
|
|
||||||
inEvent = true;
|
|
||||||
current = IcsEvent{};
|
|
||||||
} else if (line == "END:VEVENT") {
|
|
||||||
if (inEvent) {
|
|
||||||
events.push_back(current);
|
|
||||||
}
|
|
||||||
inEvent = false;
|
|
||||||
} else if (inEvent) {
|
|
||||||
if (starts_with(line, "DTSTART")) {
|
|
||||||
auto [key, v] = split_prop(line);
|
|
||||||
current.start = parse_ics_datetime(v);
|
|
||||||
} else if (starts_with(line, "DTEND")) {
|
|
||||||
auto [key, v] = split_prop(line);
|
|
||||||
current.end = parse_ics_datetime(v);
|
|
||||||
} else if (starts_with(line, "SUMMARY")) {
|
|
||||||
current.summary = get_prop_value(line);
|
|
||||||
} else if (starts_with(line, "LOCATION")) {
|
|
||||||
current.location = get_prop_value(line);
|
|
||||||
} else if (starts_with(line, "DESCRIPTION")) {
|
|
||||||
current.description = get_prop_value(line);
|
|
||||||
} else if (starts_with(line, "RRULE")) {
|
|
||||||
current.rrule = get_prop_value(line);
|
|
||||||
if (auto untilStr = extract_until_str(current.rrule)) {
|
|
||||||
try {
|
|
||||||
current.until = parse_ics_datetime(*untilStr);
|
|
||||||
} catch (...) {
|
|
||||||
// UNTIL 解析失败时忽略截止时间,照常视为无限期
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按开始时间排序
|
|
||||||
std::sort(events.begin(), events.end(),
|
|
||||||
[](const IcsEvent &a, const IcsEvent &b) { return a.start < b.start; });
|
|
||||||
|
|
||||||
return events;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
struct IcsEvent {
|
|
||||||
std::chrono::system_clock::time_point start;
|
|
||||||
std::chrono::system_clock::time_point end;
|
|
||||||
std::string summary;
|
|
||||||
std::string location;
|
|
||||||
std::string description;
|
|
||||||
|
|
||||||
// 简单递归支持:保留 RRULE 原文,以及可选的 UNTIL 截止时间(若存在)
|
|
||||||
std::string rrule;
|
|
||||||
std::optional<std::chrono::system_clock::time_point> until;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 解析 ICS 文本,返回“基准事件”(不展开 RRULE)。
|
|
||||||
// 周期事件会在 IcsEvent::rrule 与 IcsEvent::until 中体现。
|
|
||||||
std::vector<IcsEvent> parse_ics(const std::string &icsText);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,21 +23,9 @@ public:
|
||||||
result.has_count = false;
|
result.has_count = false;
|
||||||
result.count = 1;
|
result.count = 1;
|
||||||
|
|
||||||
// Handle multi-char commands like 'gg', 'gt', 'gT', 'm', '
|
// Handle multi-char commands like 'gg', 'm', '
|
||||||
if (!buffer.empty()) {
|
if (!buffer.empty()) {
|
||||||
if (buffer == "g") {
|
if (buffer == "m") {
|
||||||
if (ch == 't') {
|
|
||||||
result.action = Action::NEXT_TAB;
|
|
||||||
buffer.clear();
|
|
||||||
count_buffer.clear();
|
|
||||||
return result;
|
|
||||||
} else if (ch == 'T') {
|
|
||||||
result.action = Action::PREV_TAB;
|
|
||||||
buffer.clear();
|
|
||||||
count_buffer.clear();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} else if (buffer == "m") {
|
|
||||||
// Set mark with letter
|
// Set mark with letter
|
||||||
if (std::isalpha(ch)) {
|
if (std::isalpha(ch)) {
|
||||||
result.action = Action::SET_MARK;
|
result.action = Action::SET_MARK;
|
||||||
|
|
@ -163,18 +151,6 @@ public:
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
count_buffer.clear();
|
count_buffer.clear();
|
||||||
break;
|
break;
|
||||||
case 'v':
|
|
||||||
// Enter visual mode
|
|
||||||
result.action = Action::ENTER_VISUAL_MODE;
|
|
||||||
mode = InputMode::VISUAL;
|
|
||||||
count_buffer.clear();
|
|
||||||
break;
|
|
||||||
case 'V':
|
|
||||||
// Enter visual line mode
|
|
||||||
result.action = Action::ENTER_VISUAL_LINE_MODE;
|
|
||||||
mode = InputMode::VISUAL_LINE;
|
|
||||||
count_buffer.clear();
|
|
||||||
break;
|
|
||||||
case 'm':
|
case 'm':
|
||||||
// Set mark (wait for next char)
|
// Set mark (wait for next char)
|
||||||
buffer = "m";
|
buffer = "m";
|
||||||
|
|
@ -348,42 +324,6 @@ public:
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
InputResult process_visual_mode(int ch) {
|
|
||||||
InputResult result;
|
|
||||||
result.action = Action::NONE;
|
|
||||||
|
|
||||||
if (ch == 27 || ch == 'v') {
|
|
||||||
// ESC or 'v' exits visual mode
|
|
||||||
mode = InputMode::NORMAL;
|
|
||||||
return result;
|
|
||||||
} else if (ch == 'y') {
|
|
||||||
// Yank (copy) selected text
|
|
||||||
result.action = Action::YANK;
|
|
||||||
mode = InputMode::NORMAL;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass through navigation commands
|
|
||||||
switch (ch) {
|
|
||||||
case 'j':
|
|
||||||
case KEY_DOWN:
|
|
||||||
result.action = Action::SCROLL_DOWN;
|
|
||||||
break;
|
|
||||||
case 'k':
|
|
||||||
case KEY_UP:
|
|
||||||
result.action = Action::SCROLL_UP;
|
|
||||||
break;
|
|
||||||
case 'h':
|
|
||||||
case KEY_LEFT:
|
|
||||||
// In visual mode, h/l could extend selection
|
|
||||||
break;
|
|
||||||
case 'l':
|
|
||||||
case KEY_RIGHT:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
InputHandler::InputHandler() : pImpl(std::make_unique<Impl>()) {}
|
InputHandler::InputHandler() : pImpl(std::make_unique<Impl>()) {}
|
||||||
|
|
@ -402,9 +342,6 @@ InputResult InputHandler::handle_key(int ch) {
|
||||||
return pImpl->process_link_mode(ch);
|
return pImpl->process_link_mode(ch);
|
||||||
case InputMode::LINK_HINTS:
|
case InputMode::LINK_HINTS:
|
||||||
return pImpl->process_link_hints_mode(ch);
|
return pImpl->process_link_hints_mode(ch);
|
||||||
case InputMode::VISUAL:
|
|
||||||
case InputMode::VISUAL_LINE:
|
|
||||||
return pImpl->process_visual_mode(ch);
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ enum class InputMode {
|
||||||
COMMAND,
|
COMMAND,
|
||||||
SEARCH,
|
SEARCH,
|
||||||
LINK,
|
LINK,
|
||||||
LINK_HINTS, // Vimium-style 'f' mode
|
LINK_HINTS // Vimium-style 'f' mode
|
||||||
VISUAL, // Visual mode
|
|
||||||
VISUAL_LINE // Visual line mode
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class Action {
|
enum class Action {
|
||||||
|
|
@ -39,16 +37,8 @@ enum class Action {
|
||||||
REFRESH,
|
REFRESH,
|
||||||
QUIT,
|
QUIT,
|
||||||
HELP,
|
HELP,
|
||||||
ENTER_VISUAL_MODE, // Start visual mode
|
|
||||||
ENTER_VISUAL_LINE_MODE, // Start visual line mode
|
|
||||||
SET_MARK, // Set a mark (m + letter)
|
SET_MARK, // Set a mark (m + letter)
|
||||||
GOTO_MARK, // Jump to mark (' + letter)
|
GOTO_MARK // Jump to mark (' + letter)
|
||||||
YANK, // Copy selected text
|
|
||||||
NEXT_TAB, // gt - next tab
|
|
||||||
PREV_TAB, // gT - previous tab
|
|
||||||
NEW_TAB, // :tabnew
|
|
||||||
CLOSE_TAB, // :tabc
|
|
||||||
TOGGLE_MOUSE // Toggle mouse support
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct InputResult {
|
struct InputResult {
|
||||||
|
|
|
||||||
598
src/tui_view.cpp
598
src/tui_view.cpp
|
|
@ -1,598 +0,0 @@
|
||||||
#include "tui_view.h"
|
|
||||||
|
|
||||||
#include <curses.h>
|
|
||||||
#include <chrono>
|
|
||||||
#include <clocale>
|
|
||||||
#include <iomanip>
|
|
||||||
#include <sstream>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <thread> // Added this line
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Define color pairs - Modern btop-inspired color scheme
|
|
||||||
enum ColorPairs {
|
|
||||||
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
|
|
||||||
|
|
||||||
// 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{};
|
|
||||||
#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 %H:%M");
|
|
||||||
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) {
|
|
||||||
// 让 ncurses 按当前终端 locale 处理 UTF-8
|
|
||||||
setlocale(LC_ALL, "");
|
|
||||||
|
|
||||||
initscr();
|
|
||||||
init_colors(); // Initialize colors
|
|
||||||
cbreak();
|
|
||||||
noecho();
|
|
||||||
keypad(stdscr, TRUE);
|
|
||||||
curs_set(0);
|
|
||||||
|
|
||||||
int height, width;
|
|
||||||
getmaxyx(stdscr, height, width);
|
|
||||||
|
|
||||||
// 如果没有任何事件,给出提示信息
|
|
||||||
if (events.empty()) {
|
|
||||||
clear();
|
|
||||||
attron(COLOR_PAIR(BANNER_TEXT));
|
|
||||||
mvprintw(0, 0, "NBTCA 未来一个月活动");
|
|
||||||
attroff(COLOR_PAIR(BANNER_TEXT));
|
|
||||||
mvprintw(2, 0, "未来一个月内暂无活动。");
|
|
||||||
mvprintw(4, 0, "按任意键退出...");
|
|
||||||
refresh();
|
|
||||||
getch();
|
|
||||||
endwin();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int top = 0;
|
|
||||||
int selected = 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
clear();
|
|
||||||
|
|
||||||
// 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]);
|
|
||||||
int banner_width = calendar_banner[0].length();
|
|
||||||
|
|
||||||
int start_col_banner = (width - banner_width) / 2;
|
|
||||||
if (start_col_banner < 0) start_col_banner = 0;
|
|
||||||
|
|
||||||
// Draw shadow
|
|
||||||
attron(COLOR_PAIR(SHADOW_TEXT));
|
|
||||||
for (int i = 0; i < banner_height; ++i) {
|
|
||||||
mvprintw(i + 1, start_col_banner + 1, "%s", calendar_banner[i].c_str());
|
|
||||||
}
|
|
||||||
attroff(COLOR_PAIR(SHADOW_TEXT));
|
|
||||||
|
|
||||||
// Draw main banner
|
|
||||||
attron(COLOR_PAIR(BANNER_TEXT));
|
|
||||||
for (int i = 0; i < banner_height; ++i) {
|
|
||||||
mvprintw(i, start_col_banner, "%s", calendar_banner[i].c_str());
|
|
||||||
}
|
|
||||||
attroff(COLOR_PAIR(BANNER_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 - 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 + events_visible_lines) {
|
|
||||||
top = selected - events_visible_lines + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
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];
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(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(text_color));
|
|
||||||
mvprintw(events_start_row + i, 3, "%s", line.c_str());
|
|
||||||
attroff(COLOR_PAIR(text_color));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
if (ch == 'q' || ch == 'Q') {
|
|
||||||
break;
|
|
||||||
} else if (ch == KEY_UP || ch == 'k') {
|
|
||||||
if (selected > 0) selected--;
|
|
||||||
} else if (ch == KEY_DOWN || ch == 'j') {
|
|
||||||
if (selected + 1 < (int)events.size()) selected++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endwin();
|
|
||||||
}
|
|
||||||
|
|
||||||
int run_portal_tui() {
|
|
||||||
setlocale(LC_ALL, "");
|
|
||||||
|
|
||||||
initscr();
|
|
||||||
init_colors(); // Initialize colors
|
|
||||||
cbreak();
|
|
||||||
noecho();
|
|
||||||
keypad(stdscr, TRUE);
|
|
||||||
curs_set(0);
|
|
||||||
|
|
||||||
int height, width;
|
|
||||||
getmaxyx(stdscr, height, width);
|
|
||||||
|
|
||||||
std::vector<std::string> menu_items = {"Calendar", "Exit"};
|
|
||||||
int selected = 0;
|
|
||||||
int choice = -1;
|
|
||||||
|
|
||||||
while (choice == -1) {
|
|
||||||
clear();
|
|
||||||
|
|
||||||
// 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]);
|
|
||||||
int banner_width = banner_art[0].length();
|
|
||||||
|
|
||||||
int start_col_banner = (width - banner_width) / 2;
|
|
||||||
if (start_col_banner < 0) start_col_banner = 0;
|
|
||||||
|
|
||||||
// Draw shadow
|
|
||||||
attron(COLOR_PAIR(SHADOW_TEXT));
|
|
||||||
for (int i = 0; i < banner_height; ++i) {
|
|
||||||
mvprintw(i + 1, start_col_banner + 1, "%s", banner_art[i].c_str());
|
|
||||||
}
|
|
||||||
attroff(COLOR_PAIR(SHADOW_TEXT));
|
|
||||||
|
|
||||||
// Draw main banner
|
|
||||||
attron(COLOR_PAIR(BANNER_TEXT));
|
|
||||||
for (int i = 0; i < banner_height; ++i) {
|
|
||||||
mvprintw(i, start_col_banner, "%s", banner_art[i].c_str());
|
|
||||||
}
|
|
||||||
attroff(COLOR_PAIR(BANNER_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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
|
|
||||||
int ch = getch();
|
|
||||||
switch (ch) {
|
|
||||||
case KEY_UP:
|
|
||||||
case 'k':
|
|
||||||
if (selected > 0) {
|
|
||||||
selected--;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case KEY_DOWN:
|
|
||||||
case 'j':
|
|
||||||
if (selected < (int)menu_items.size() - 1) {
|
|
||||||
selected++;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'q':
|
|
||||||
case 'Q':
|
|
||||||
choice = 1; // Corresponds to "Exit"
|
|
||||||
break;
|
|
||||||
case 10: // Enter key
|
|
||||||
choice = selected;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endwin();
|
|
||||||
return choice;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示启动画面
|
|
||||||
void display_splash_screen() {
|
|
||||||
setlocale(LC_ALL, "");
|
|
||||||
initscr();
|
|
||||||
init_colors(); // Initialize colors
|
|
||||||
cbreak();
|
|
||||||
noecho();
|
|
||||||
curs_set(0);
|
|
||||||
|
|
||||||
int height, width;
|
|
||||||
getmaxyx(stdscr, height, width);
|
|
||||||
|
|
||||||
// Animated splash screen with progress bar
|
|
||||||
for (int frame = 0; frame < 20; ++frame) {
|
|
||||||
clear();
|
|
||||||
|
|
||||||
// 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 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;
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
endwin();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "ics_parser.h"
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
// 运行 ncurses TUI,展示给定事件列表
|
|
||||||
// 当 events 为空时,在界面上提示“未来一个月暂无活动”
|
|
||||||
void run_tui(const std::vector<IcsEvent> &events);
|
|
||||||
|
|
||||||
// 运行 ncurses TUI for the portal
|
|
||||||
int run_portal_tui();
|
|
||||||
|
|
||||||
// 显示启动画面
|
|
||||||
void display_splash_screen();
|
|
||||||
|
|
||||||
Loading…
Reference in a new issue