TUT/src/main.cpp
m1ngsama 03422136dd feat: Add complete persistent bookmark system
Completed Phase 1 high priority task - comprehensive bookmark management:

BookmarkManager (New):
- JSON persistence to ~/.config/tut/bookmarks.json
- Add, remove, contains, getAll operations
- Automatic sorting by timestamp (newest first)
- Each bookmark stores: title, URL, timestamp
- Handles special characters with JSON escaping
- Auto-creates config directory if needed

UI Integration:
- Bookmark panel in bottom-left of UI
- Shows up to 5 most recent bookmarks
- Displays "[1] Title" format with yellow highlighting
- Shows "+N more..." indicator if >5 bookmarks
- Real-time update when bookmarks change

Keyboard Shortcuts:
- Ctrl+D: Toggle bookmark for current page
  * Adds if not bookmarked
  * Removes if already bookmarked
  * Shows status message confirmation
- F2: Toggle bookmark panel visibility
  * Refreshes bookmark list when opened

Features:
- Persistent storage across browser sessions
- Duplicate detection (one bookmark per URL)
- Toggle behavior (add/remove with same key)
- Real-time panel updates
- Empty state handling ("(empty)" message)
- Sorted display (newest first)

Technical Implementation:
- BookmarkManager class with Pimpl idiom
- Simple JSON format for easy manual editing
- Event-driven architecture (WindowEvent::AddBookmark)
- Lambda callback for bookmark updates
- Integrated with main browser engine

Storage Format:
[
  {"title": "Page Title", "url": "https://...", "timestamp": 1234567890},
  ...
]

Documentation:
- Updated KEYBOARD.md with bookmark shortcuts
- Updated STATUS.md to reflect completion
- Added bookmark feature to interactive features list

Next Step: History system! 📚
2026-01-01 14:08:42 +08:00

370 lines
14 KiB
C++

/**
* @file main.cpp
* @brief TUT 程序入口
* @author m1ngsama
* @date 2024-12-29
*/
#include <iostream>
#include <string>
#include <cstring>
#include <chrono>
#include "tut/version.hpp"
#include "core/browser_engine.hpp"
#include "core/bookmark_manager.hpp"
#include "ui/main_window.hpp"
#include "utils/logger.hpp"
#include "utils/config.hpp"
#include "utils/theme.hpp"
namespace {
void printVersion() {
std::cout << tut::PROJECT_NAME << " " << tut::VERSION_STRING << "\n"
<< tut::PROJECT_DESCRIPTION << "\n"
<< "Homepage: " << tut::PROJECT_HOMEPAGE << "\n"
<< "Built with " << tut::COMPILER_ID << " " << tut::COMPILER_VERSION << "\n";
}
void printHelp(const char* prog_name) {
std::cout << "TUT - Terminal UI Textual Browser\n"
<< "A lightweight terminal browser with btop-style interface\n\n"
<< "Usage: " << prog_name << " [OPTIONS] [URL]\n\n"
<< "Options:\n"
<< " -h, --help Show this help message\n"
<< " -v, --version Show version information\n"
<< " -c, --config Specify config file path\n"
<< " -t, --theme Specify theme name\n"
<< " -d, --debug Enable debug logging\n\n"
<< "Examples:\n"
<< " " << prog_name << "\n"
<< " " << prog_name << " https://example.com\n"
<< " " << prog_name << " --theme nord https://github.com\n\n"
<< "Keyboard shortcuts:\n"
<< " j/k, ↓/↑ Scroll down/up\n"
<< " g/G Go to top/bottom\n"
<< " Space Page down\n"
<< " b Page up\n"
<< " Tab Next link\n"
<< " Shift+Tab Previous link\n"
<< " Enter Follow link\n"
<< " Backspace Go back\n"
<< " f Go forward\n"
<< " / Search in page\n"
<< " n/N Next/previous search result\n"
<< " Ctrl+L Focus address bar\n"
<< " F1 Help\n"
<< " F2 Bookmarks\n"
<< " F3 History\n"
<< " F5, r Refresh\n"
<< " Ctrl+D Add bookmark\n"
<< " Ctrl+Q, F10 Quit\n";
}
} // namespace
int main(int argc, char* argv[]) {
using namespace tut;
std::string initial_url;
std::string config_file;
std::string theme_name;
bool debug_mode = false;
// 解析命令行参数
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "-h") == 0 || std::strcmp(argv[i], "--help") == 0) {
printHelp(argv[0]);
return 0;
}
if (std::strcmp(argv[i], "-v") == 0 || std::strcmp(argv[i], "--version") == 0) {
printVersion();
return 0;
}
if (std::strcmp(argv[i], "-d") == 0 || std::strcmp(argv[i], "--debug") == 0) {
debug_mode = true;
continue;
}
if ((std::strcmp(argv[i], "-c") == 0 || std::strcmp(argv[i], "--config") == 0) &&
i + 1 < argc) {
config_file = argv[++i];
continue;
}
if ((std::strcmp(argv[i], "-t") == 0 || std::strcmp(argv[i], "--theme") == 0) &&
i + 1 < argc) {
theme_name = argv[++i];
continue;
}
// 假定其他参数是 URL
if (argv[i][0] != '-') {
initial_url = argv[i];
}
}
// 初始化日志系统
Logger& logger = Logger::instance();
logger.setLevel(debug_mode ? LogLevel::Debug : LogLevel::Info);
LOG_INFO << "Starting TUT " << VERSION_STRING;
// 加载配置
Config& config = Config::instance();
if (!config_file.empty()) {
config.load(config_file);
} else {
std::string default_config = config.getConfigPath() + "/config.toml";
config.load(default_config);
}
// 加载主题
ThemeManager& theme_manager = ThemeManager::instance();
theme_manager.loadThemesFromDirectory(config.getConfigPath() + "/themes");
if (!theme_name.empty()) {
if (!theme_manager.setTheme(theme_name)) {
LOG_WARN << "Theme not found: " << theme_name << ", using default";
}
} else {
theme_manager.setTheme(config.getDefaultTheme());
}
try {
// 创建浏览器引擎
BrowserEngine engine;
// 创建书签管理器
BookmarkManager bookmarks;
// 创建主窗口
MainWindow window;
// 设置导航回调
window.onNavigate([&engine, &window](const std::string& url) {
LOG_INFO << "Navigating to: " << url;
window.setLoading(true);
auto start_time = std::chrono::steady_clock::now();
if (engine.loadUrl(url)) {
auto end_time = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(end_time - start_time).count();
// Update window content
window.setTitle(engine.getTitle());
window.setContent(engine.getRenderedContent());
window.setUrl(url);
// Convert LinkInfo to DisplayLink
std::vector<DisplayLink> display_links;
for (const auto& link : engine.extractLinks()) {
DisplayLink dl;
dl.text = link.text;
dl.url = link.url;
dl.visited = false;
display_links.push_back(dl);
}
window.setLinks(display_links);
// Update navigation state
window.setCanGoBack(engine.canGoBack());
window.setCanGoForward(engine.canGoForward());
// Update stats (assuming response body size)
size_t content_size = engine.getRenderedContent().size();
window.setLoadStats(elapsed, content_size, static_cast<int>(display_links.size()));
window.setStatusMessage("Loaded: " + url);
} else {
window.setStatusMessage("Failed to load: " + url);
}
window.setLoading(false);
});
// 设置链接点击回调
window.onLinkClick([&engine, &window](int index) {
auto links = engine.extractLinks();
if (index >= 0 && index < static_cast<int>(links.size())) {
const std::string& link_url = links[index].url;
LOG_INFO << "Following link [" << index + 1 << "]: " << link_url;
// Trigger navigation
window.setLoading(true);
auto start_time = std::chrono::steady_clock::now();
if (engine.loadUrl(link_url)) {
auto end_time = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(end_time - start_time).count();
window.setTitle(engine.getTitle());
window.setContent(engine.getRenderedContent());
window.setUrl(link_url);
// Convert LinkInfo to DisplayLink
std::vector<DisplayLink> display_links;
for (const auto& link : engine.extractLinks()) {
DisplayLink dl;
dl.text = link.text;
dl.url = link.url;
dl.visited = false;
display_links.push_back(dl);
}
window.setLinks(display_links);
window.setCanGoBack(engine.canGoBack());
window.setCanGoForward(engine.canGoForward());
size_t content_size = engine.getRenderedContent().size();
window.setLoadStats(elapsed, content_size, static_cast<int>(display_links.size()));
window.setStatusMessage("Loaded: " + link_url);
} else {
window.setStatusMessage("Failed to load: " + link_url);
}
window.setLoading(false);
}
});
// Helper to update bookmark display
auto updateBookmarks = [&bookmarks, &window]() {
std::vector<DisplayBookmark> display_bookmarks;
for (const auto& bm : bookmarks.getAll()) {
DisplayBookmark db;
db.title = bm.title;
db.url = bm.url;
display_bookmarks.push_back(db);
}
window.setBookmarks(display_bookmarks);
};
// Initialize bookmarks display
updateBookmarks();
// 设置窗口事件回调
window.onEvent([&engine, &window, &bookmarks, &updateBookmarks](WindowEvent event) {
switch (event) {
case WindowEvent::AddBookmark:
{
std::string current_url = engine.getCurrentUrl();
std::string current_title = engine.getTitle();
if (!current_url.empty() && current_url != "about:blank") {
if (bookmarks.contains(current_url)) {
bookmarks.remove(current_url);
window.setStatusMessage("Removed bookmark: " + current_title);
} else {
bookmarks.add(current_title, current_url);
window.setStatusMessage("Added bookmark: " + current_title);
}
updateBookmarks();
}
}
break;
case WindowEvent::OpenBookmarks:
updateBookmarks();
break;
case WindowEvent::Back:
if (engine.goBack()) {
window.setTitle(engine.getTitle());
window.setContent(engine.getRenderedContent());
window.setUrl(engine.getCurrentUrl());
std::vector<DisplayLink> display_links;
for (const auto& link : engine.extractLinks()) {
DisplayLink dl;
dl.text = link.text;
dl.url = link.url;
dl.visited = false;
display_links.push_back(dl);
}
window.setLinks(display_links);
window.setCanGoBack(engine.canGoBack());
window.setCanGoForward(engine.canGoForward());
}
break;
case WindowEvent::Forward:
if (engine.goForward()) {
window.setTitle(engine.getTitle());
window.setContent(engine.getRenderedContent());
window.setUrl(engine.getCurrentUrl());
std::vector<DisplayLink> display_links;
for (const auto& link : engine.extractLinks()) {
DisplayLink dl;
dl.text = link.text;
dl.url = link.url;
dl.visited = false;
display_links.push_back(dl);
}
window.setLinks(display_links);
window.setCanGoBack(engine.canGoBack());
window.setCanGoForward(engine.canGoForward());
}
break;
case WindowEvent::Refresh:
if (engine.refresh()) {
window.setTitle(engine.getTitle());
window.setContent(engine.getRenderedContent());
std::vector<DisplayLink> display_links;
for (const auto& link : engine.extractLinks()) {
DisplayLink dl;
dl.text = link.text;
dl.url = link.url;
dl.visited = false;
display_links.push_back(dl);
}
window.setLinks(display_links);
}
break;
default:
break;
}
});
// 初始化窗口
if (!window.init()) {
LOG_FATAL << "Failed to initialize window";
return 1;
}
// 加载初始 URL
if (!initial_url.empty()) {
engine.loadUrl(initial_url);
window.setUrl(initial_url);
window.setTitle(engine.getTitle());
window.setContent(engine.getRenderedContent());
std::vector<DisplayLink> display_links;
for (const auto& link : engine.extractLinks()) {
DisplayLink dl;
dl.text = link.text;
dl.url = link.url;
dl.visited = false;
display_links.push_back(dl);
}
window.setLinks(display_links);
window.setCanGoBack(engine.canGoBack());
window.setCanGoForward(engine.canGoForward());
} else {
window.setUrl("about:blank");
window.setTitle("TUT - Terminal UI Textual Browser");
window.setContent("Welcome to TUT!\n\nPress Ctrl+L to enter a URL.");
}
// 运行主循环
int exit_code = window.run();
LOG_INFO << "TUT exiting with code " << exit_code;
return exit_code;
} catch (const std::exception& e) {
LOG_FATAL << "Fatal error: " << e.what();
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
}