diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8bd9c06..8c2ec8d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,13 +47,13 @@ jobs: - name: Rename binary with platform suffix run: | - mv build/nbtca_tui build/nbtca_tui-${{ matrix.name }} + mv build/tut build/tut-${{ matrix.name }} - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: nbtca_tui-${{ matrix.name }} - path: build/nbtca_tui-${{ matrix.name }} + name: tut-${{ matrix.name }} + path: build/tut-${{ matrix.name }} release: needs: build @@ -85,13 +85,13 @@ jobs: Automated release for commit ${{ github.sha }} ## Download - - **macOS**: `nbtca_tui-macos` - - **Linux**: `nbtca_tui-linux` + - **macOS**: `tut-macos` + - **Linux**: `tut-linux` ## Build from source See the [README](https://github.com/${{ github.repository }}/blob/main/README.md) for build instructions. files: | - artifacts/nbtca_tui-macos/nbtca_tui-macos - artifacts/nbtca_tui-linux/nbtca_tui-linux + artifacts/tut-macos/tut-macos + artifacts/tut-linux/tut-linux env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 48ff0b9..ae3e6ad 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ build/ *.o tut .DS_Store +*.swp +*.swo +*~ +.vscode/ +.idea/ diff --git a/src/browser.cpp b/src/browser.cpp index 9f50bd8..7afb06f 100644 --- a/src/browser.cpp +++ b/src/browser.cpp @@ -52,9 +52,9 @@ public: auto response = http_client.fetch(url); if (!response.is_success()) { - status_message = "Error: " + (response.error_message.empty() ? + status_message = response.error_message.empty() ? "HTTP " + std::to_string(response.status_code) : - response.error_message); + response.error_message; return false; } @@ -65,14 +65,13 @@ public: current_link = -1; search_results.clear(); - // 更新历史 if (history_pos >= 0 && history_pos < static_cast(history.size()) - 1) { history.erase(history.begin() + history_pos + 1, history.end()); } history.push_back(url); history_pos = history.size() - 1; - status_message = "Loaded: " + (current_doc.title.empty() ? url : current_doc.title); + status_message = current_doc.title.empty() ? url : current_doc.title; return true; } @@ -80,7 +79,6 @@ public: attron(COLOR_PAIR(COLOR_STATUS_BAR)); mvprintw(screen_height - 1, 0, "%s", std::string(screen_width, ' ').c_str()); - // 显示模式和缓冲 std::string mode_str; InputMode mode = input_handler.get_mode(); switch (mode) { @@ -88,29 +86,24 @@ public: mode_str = "NORMAL"; break; case InputMode::COMMAND: - mode_str = input_handler.get_buffer(); - break; case InputMode::SEARCH: mode_str = input_handler.get_buffer(); break; default: - mode_str = "???"; + mode_str = ""; break; } - // 左侧:模式或命令 mvprintw(screen_height - 1, 0, " %s", mode_str.c_str()); - // 中间:状态消息 if (!status_message.empty() && mode == InputMode::NORMAL) { int msg_x = (screen_width - status_message.length()) / 2; - if (msg_x < mode_str.length() + 2) { + if (msg_x < static_cast(mode_str.length()) + 2) { msg_x = mode_str.length() + 2; } mvprintw(screen_height - 1, msg_x, "%s", status_message.c_str()); } - // 右侧:位置信息 int total_lines = rendered_lines.size(); int visible_lines = screen_height - 2; int percentage = 0; @@ -147,7 +140,6 @@ public: int line_idx = scroll_pos + i; const auto& line = rendered_lines[line_idx]; - // 高亮当前链接 if (line.is_link && line.link_index == current_link) { attron(COLOR_PAIR(COLOR_LINK_ACTIVE)); } else { @@ -157,15 +149,12 @@ public: } } - // 搜索高亮 - std::string display_text = line.text; if (!search_term.empty() && std::find(search_results.begin(), search_results.end(), line_idx) != search_results.end()) { - // 简单高亮:整行反色(实际应该只高亮匹配部分) attron(A_REVERSE); } - mvprintw(i, 0, "%s", display_text.c_str()); + mvprintw(i, 0, "%s", line.text.c_str()); if (!search_term.empty() && std::find(search_results.begin(), search_results.end(), line_idx) != search_results.end()) { @@ -225,7 +214,6 @@ public: case Action::NEXT_LINK: if (!current_doc.links.empty()) { current_link = (current_link + 1) % current_doc.links.size(); - // 滚动到链接位置 scroll_to_link(current_link); } break; diff --git a/src/input_handler.cpp b/src/input_handler.cpp index bc21e17..c8ad265 100644 --- a/src/input_handler.cpp +++ b/src/input_handler.cpp @@ -23,22 +23,18 @@ public: result.has_count = false; result.count = 1; - // 处理数字前缀 if (std::isdigit(ch) && (ch != '0' || !count_buffer.empty())) { count_buffer += static_cast(ch); return result; } - // 解析count if (!count_buffer.empty()) { result.has_count = true; result.count = std::stoi(count_buffer); count_buffer.clear(); } - // 处理vim风格的命令 switch (ch) { - // 移动 case 'j': case KEY_DOWN: result.action = Action::SCROLL_DOWN; @@ -55,18 +51,14 @@ public: case KEY_RIGHT: result.action = Action::GO_FORWARD; break; - - // 翻页 - case 4: // Ctrl-D + case 4: case ' ': result.action = Action::SCROLL_PAGE_DOWN; break; - case 21: // Ctrl-U + case 21: case 'b': result.action = Action::SCROLL_PAGE_UP; break; - - // 跳转 case 'g': buffer += 'g'; if (buffer == "gg") { @@ -82,8 +74,6 @@ public: result.action = Action::GOTO_BOTTOM; } break; - - // 搜索 case '/': mode = InputMode::SEARCH; buffer = "/"; @@ -94,27 +84,21 @@ public: case 'N': result.action = Action::SEARCH_PREV; break; - - // 链接导航 - case '\t': // Tab + case '\t': result.action = Action::NEXT_LINK; break; - case KEY_BTAB: // Shift-Tab (可能不是所有终端都支持) + case KEY_BTAB: case 'T': result.action = Action::PREV_LINK; break; - case '\n': // Enter + case '\n': case '\r': result.action = Action::FOLLOW_LINK; break; - - // 命令模式 case ':': mode = InputMode::COMMAND; buffer = ":"; break; - - // 其他操作 case 'r': result.action = Action::REFRESH; break; @@ -124,7 +108,6 @@ public: case '?': result.action = Action::HELP; break; - default: buffer.clear(); break; @@ -138,8 +121,7 @@ public: result.action = Action::NONE; if (ch == '\n' || ch == '\r') { - // 执行命令 - std::string command = buffer.substr(1); // 去掉':' + std::string command = buffer.substr(1); if (command == "q" || command == "quit") { result.action = Action::QUIT; @@ -148,14 +130,12 @@ public: } else if (command == "r" || command == "refresh") { result.action = Action::REFRESH; } else if (command.rfind("o ", 0) == 0 || command.rfind("open ", 0) == 0) { - // :o URL 或 :open URL size_t space_pos = command.find(' '); if (space_pos != std::string::npos) { result.action = Action::OPEN_URL; result.text = command.substr(space_pos + 1); } } else if (!command.empty() && std::isdigit(command[0])) { - // 跳转到行号 try { result.action = Action::GOTO_LINE; result.number = std::stoi(command); @@ -166,7 +146,7 @@ public: mode = InputMode::NORMAL; buffer.clear(); - } else if (ch == 27) { // ESC + } else if (ch == 27) { mode = InputMode::NORMAL; buffer.clear(); } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { @@ -188,14 +168,13 @@ public: result.action = Action::NONE; if (ch == '\n' || ch == '\r') { - // 执行搜索 if (buffer.length() > 1) { result.action = Action::SEARCH_FORWARD; - result.text = buffer.substr(1); // 去掉'/' + result.text = buffer.substr(1); } mode = InputMode::NORMAL; buffer.clear(); - } else if (ch == 27) { // ESC + } else if (ch == 27) { mode = InputMode::NORMAL; buffer.clear(); } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { diff --git a/src/main.cpp b/src/main.cpp index 5e1b137..e13b2a7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,19 +12,18 @@ void print_usage(const char* prog_name) { << " " << prog_name << " https://example.com\n" << " " << prog_name << " https://news.ycombinator.com\n\n" << "Vim-style keybindings:\n" - << " j/k - Scroll down/up\n" - << " gg/G - Go to top/bottom\n" - << " / - Search\n" - << " Tab - Next link\n" - << " Enter - Follow link\n" - << " h/l - Back/Forward\n" - << " :o URL - Open URL\n" - << " :q - Quit\n" - << " ? - Show help\n"; + << " j/k - Scroll down/up\n" + << " gg/G - Go to top/bottom\n" + << " / - Search\n" + << " Tab - Next link\n" + << " Enter - Follow link\n" + << " h/l - Back/Forward\n" + << " :o URL - Open URL\n" + << " :q - Quit\n" + << " ? - Show help\n"; } int main(int argc, char* argv[]) { - // 解析命令行参数 std::string initial_url; if (argc > 1) { diff --git a/src/text_renderer.cpp b/src/text_renderer.cpp index 3696e5d..61a15e0 100644 --- a/src/text_renderer.cpp +++ b/src/text_renderer.cpp @@ -7,7 +7,6 @@ class TextRenderer::Impl { public: RenderConfig config; - // 自动换行处理 std::vector wrap_text(const std::string& text, int width) { std::vector lines; if (text.empty()) { @@ -19,20 +18,17 @@ public: std::string current_line; while (words_stream >> word) { - // 处理单个词超长的情况 if (word.length() > static_cast(width)) { if (!current_line.empty()) { lines.push_back(current_line); current_line.clear(); } - // 强制分割长词 for (size_t i = 0; i < word.length(); i += width) { lines.push_back(word.substr(i, width)); } continue; } - // 正常换行逻辑 if (current_line.empty()) { current_line = word; } else if (current_line.length() + 1 + word.length() <= static_cast(width)) { @@ -54,7 +50,6 @@ public: return lines; } - // 添加缩进 std::string add_indent(const std::string& text, int indent) { return std::string(indent, ' ') + text; } @@ -69,20 +64,17 @@ TextRenderer::~TextRenderer() = default; std::vector TextRenderer::render(const ParsedDocument& doc, int screen_width) { std::vector lines; - // 计算实际内容宽度 int content_width = std::min(pImpl->config.max_width, screen_width - 4); if (content_width < 40) { content_width = screen_width - 4; } - // 计算左边距(如果居中) int margin = 0; if (pImpl->config.center_content && content_width < screen_width) { margin = (screen_width - content_width) / 2; } pImpl->config.margin_left = margin; - // 渲染标题 if (!doc.title.empty()) { RenderedLine title_line; title_line.text = std::string(margin, ' ') + doc.title; @@ -92,7 +84,6 @@ std::vector TextRenderer::render(const ParsedDocument& doc, int sc title_line.link_index = -1; lines.push_back(title_line); - // 标题下划线 RenderedLine underline; underline.text = std::string(margin, ' ') + std::string(std::min((int)doc.title.length(), content_width), '='); underline.color_pair = COLOR_HEADING1; @@ -101,7 +92,6 @@ std::vector TextRenderer::render(const ParsedDocument& doc, int sc underline.link_index = -1; lines.push_back(underline); - // 空行 RenderedLine empty; empty.text = ""; empty.color_pair = COLOR_NORMAL; @@ -111,7 +101,6 @@ std::vector TextRenderer::render(const ParsedDocument& doc, int sc lines.push_back(empty); } - // 渲染URL if (!doc.url.empty()) { RenderedLine url_line; url_line.text = std::string(margin, ' ') + "URL: " + doc.url; @@ -130,7 +119,6 @@ std::vector TextRenderer::render(const ParsedDocument& doc, int sc lines.push_back(empty); } - // 渲染内容元素 for (const auto& elem : doc.elements) { int color = COLOR_NORMAL; bool bold = false; @@ -179,7 +167,6 @@ std::vector TextRenderer::render(const ParsedDocument& doc, int sc break; } - // 换行处理 auto wrapped_lines = pImpl->wrap_text(elem.text, content_width - prefix.length()); for (size_t i = 0; i < wrapped_lines.size(); ++i) { RenderedLine line; @@ -195,7 +182,6 @@ std::vector TextRenderer::render(const ParsedDocument& doc, int sc lines.push_back(line); } - // 段落间距 if (elem.type == ElementType::PARAGRAPH || elem.type == ElementType::HEADING1 || elem.type == ElementType::HEADING2 || @@ -212,7 +198,6 @@ std::vector TextRenderer::render(const ParsedDocument& doc, int sc } } - // 渲染链接列表 if (!doc.links.empty() && pImpl->config.show_link_indicators) { RenderedLine separator; std::string sepline(content_width, '-'); @@ -254,7 +239,6 @@ std::vector TextRenderer::render(const ParsedDocument& doc, int sc lines.push_back(link_line); } - // URL on next line auto url_wrapped = pImpl->wrap_text(link.url, content_width - 6); for (const auto& url_line_text : url_wrapped) { RenderedLine url_line;