mirror of
https://github.com/m1ngsama/TUT.git
synced 2026-02-08 00:54:05 +00:00
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! 📚✅
370 lines
14 KiB
C++
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;
|
|
}
|
|
}
|