diff --git a/src/browser.cpp b/src/browser.cpp new file mode 100644 index 0000000..9f50bd8 --- /dev/null +++ b/src/browser.cpp @@ -0,0 +1,443 @@ +#include "browser.h" +#include +#include +#include +#include + +class Browser::Impl { +public: + HttpClient http_client; + HtmlParser html_parser; + TextRenderer renderer; + InputHandler input_handler; + + ParsedDocument current_doc; + std::vector rendered_lines; + std::string current_url; + std::vector history; + int history_pos = -1; + + // 视图状态 + int scroll_pos = 0; + int current_link = -1; + std::string status_message; + std::string search_term; + std::vector search_results; // 匹配行号 + + // 屏幕尺寸 + int screen_height = 0; + int screen_width = 0; + + void init_screen() { + setlocale(LC_ALL, ""); + initscr(); + init_color_scheme(); + cbreak(); + noecho(); + keypad(stdscr, TRUE); + curs_set(0); + timeout(0); // non-blocking + getmaxyx(stdscr, screen_height, screen_width); + } + + void cleanup_screen() { + endwin(); + } + + bool load_page(const std::string& url) { + status_message = "Loading " + url + "..."; + draw_screen(); + refresh(); + + auto response = http_client.fetch(url); + + if (!response.is_success()) { + status_message = "Error: " + (response.error_message.empty() ? + "HTTP " + std::to_string(response.status_code) : + response.error_message); + return false; + } + + current_doc = html_parser.parse(response.body, url); + rendered_lines = renderer.render(current_doc, screen_width); + current_url = url; + scroll_pos = 0; + 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); + return true; + } + + void draw_status_bar() { + 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) { + case InputMode::NORMAL: + 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 = "???"; + 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) { + 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; + if (total_lines > 0) { + if (scroll_pos == 0) { + percentage = 0; + } else if (scroll_pos + visible_lines >= total_lines) { + percentage = 100; + } else { + percentage = (scroll_pos * 100) / total_lines; + } + } + + std::string pos_str = std::to_string(scroll_pos + 1) + "/" + + std::to_string(total_lines) + " " + + std::to_string(percentage) + "%"; + + if (current_link >= 0 && current_link < static_cast(current_doc.links.size())) { + pos_str = "[Link " + std::to_string(current_link) + "] " + pos_str; + } + + mvprintw(screen_height - 1, screen_width - pos_str.length() - 1, "%s", pos_str.c_str()); + + attroff(COLOR_PAIR(COLOR_STATUS_BAR)); + } + + void draw_screen() { + clear(); + + int visible_lines = screen_height - 2; + int content_lines = std::min(static_cast(rendered_lines.size()) - scroll_pos, visible_lines); + + for (int i = 0; i < content_lines; ++i) { + 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 { + attron(COLOR_PAIR(line.color_pair)); + if (line.is_bold) { + attron(A_BOLD); + } + } + + // 搜索高亮 + 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()); + + if (!search_term.empty() && + std::find(search_results.begin(), search_results.end(), line_idx) != search_results.end()) { + attroff(A_REVERSE); + } + + if (line.is_link && line.link_index == current_link) { + attroff(COLOR_PAIR(COLOR_LINK_ACTIVE)); + } else { + if (line.is_bold) { + attroff(A_BOLD); + } + attroff(COLOR_PAIR(line.color_pair)); + } + } + + draw_status_bar(); + } + + void handle_action(const InputResult& result) { + int visible_lines = screen_height - 2; + int max_scroll = std::max(0, static_cast(rendered_lines.size()) - visible_lines); + + int count = result.has_count ? result.count : 1; + + switch (result.action) { + case Action::SCROLL_UP: + scroll_pos = std::max(0, scroll_pos - count); + break; + + case Action::SCROLL_DOWN: + scroll_pos = std::min(max_scroll, scroll_pos + count); + break; + + case Action::SCROLL_PAGE_UP: + scroll_pos = std::max(0, scroll_pos - visible_lines); + break; + + case Action::SCROLL_PAGE_DOWN: + scroll_pos = std::min(max_scroll, scroll_pos + visible_lines); + break; + + case Action::GOTO_TOP: + scroll_pos = 0; + break; + + case Action::GOTO_BOTTOM: + scroll_pos = max_scroll; + break; + + case Action::GOTO_LINE: + if (result.number > 0 && result.number <= static_cast(rendered_lines.size())) { + scroll_pos = std::min(result.number - 1, max_scroll); + } + break; + + case Action::NEXT_LINK: + if (!current_doc.links.empty()) { + current_link = (current_link + 1) % current_doc.links.size(); + // 滚动到链接位置 + scroll_to_link(current_link); + } + break; + + case Action::PREV_LINK: + if (!current_doc.links.empty()) { + current_link = (current_link - 1 + current_doc.links.size()) % current_doc.links.size(); + scroll_to_link(current_link); + } + break; + + case Action::FOLLOW_LINK: + if (current_link >= 0 && current_link < static_cast(current_doc.links.size())) { + load_page(current_doc.links[current_link].url); + } + break; + + case Action::GO_BACK: + if (history_pos > 0) { + history_pos--; + load_page(history[history_pos]); + } else { + status_message = "No previous page"; + } + break; + + case Action::GO_FORWARD: + if (history_pos < static_cast(history.size()) - 1) { + history_pos++; + load_page(history[history_pos]); + } else { + status_message = "No next page"; + } + break; + + case Action::OPEN_URL: + if (!result.text.empty()) { + load_page(result.text); + } + break; + + case Action::REFRESH: + if (!current_url.empty()) { + load_page(current_url); + } + break; + + case Action::SEARCH_FORWARD: + search_term = result.text; + search_results.clear(); + for (size_t i = 0; i < rendered_lines.size(); ++i) { + if (rendered_lines[i].text.find(search_term) != std::string::npos) { + search_results.push_back(i); + } + } + if (!search_results.empty()) { + scroll_pos = search_results[0]; + status_message = "Found " + std::to_string(search_results.size()) + " matches"; + } else { + status_message = "Pattern not found: " + search_term; + } + break; + + case Action::SEARCH_NEXT: + if (!search_results.empty()) { + auto it = std::upper_bound(search_results.begin(), search_results.end(), scroll_pos); + if (it != search_results.end()) { + scroll_pos = *it; + } else { + scroll_pos = search_results[0]; + status_message = "Search wrapped to top"; + } + } + break; + + case Action::SEARCH_PREV: + if (!search_results.empty()) { + auto it = std::lower_bound(search_results.begin(), search_results.end(), scroll_pos); + if (it != search_results.begin()) { + scroll_pos = *(--it); + } else { + scroll_pos = search_results.back(); + status_message = "Search wrapped to bottom"; + } + } + break; + + case Action::HELP: + show_help(); + break; + + default: + break; + } + } + + void scroll_to_link(int link_idx) { + // 查找链接在渲染行中的位置 + for (size_t i = 0; i < rendered_lines.size(); ++i) { + if (rendered_lines[i].is_link && rendered_lines[i].link_index == link_idx) { + int visible_lines = screen_height - 2; + if (static_cast(i) < scroll_pos || static_cast(i) >= scroll_pos + visible_lines) { + scroll_pos = std::max(0, static_cast(i) - visible_lines / 2); + } + break; + } + } + } + + void show_help() { + std::ostringstream help_html; + help_html << "TUT Browser Help" + << "

TUT Browser - Vim-style Terminal Browser

" + << "

Navigation

" + << "

j/k or ↓/↑: Scroll down/up

" + << "

Ctrl-D or Space: Scroll page down

" + << "

Ctrl-U or b: Scroll page up

" + << "

gg: Go to top

" + << "

G: Go to bottom

" + << "

[number]G: Go to line number

" + << "

Links

" + << "

Tab: Next link

" + << "

Shift-Tab or T: Previous link

" + << "

Enter: Follow link

" + << "

h: Go back

" + << "

l: Go forward

" + << "

Search

" + << "

/: Start search

" + << "

n: Next match

" + << "

N: Previous match

" + << "

Commands

" + << "

:q or :quit - Quit browser

" + << "

:o URL or :open URL - Open URL

" + << "

:r or :refresh - Refresh page

" + << "

:h or :help - Show this help

" + << "

:[number] - Go to line number

" + << "

Other

" + << "

r: Refresh current page

" + << "

q: Quit browser

" + << "

?: Show help

" + << "

Important Limitations

" + << "

JavaScript/SPA Websites: This browser cannot execute JavaScript. " + << "Single Page Applications (SPAs) built with React, Vue, Angular, etc. will not work properly " + << "as they render content dynamically with JavaScript.

" + << "

Works best with:

" + << "
    " + << "
  • Static HTML websites
  • " + << "
  • Server-side rendered pages
  • " + << "
  • Documentation sites
  • " + << "
  • News sites with HTML content
  • " + << "
  • Blogs with traditional HTML
  • " + << "
" + << "

Example sites that work well:

" + << "

- https://example.com

" + << "

- https://en.wikipedia.org

" + << "

- Text-based news sites

" + << "

For JavaScript-heavy sites: You may need to find alternative URLs " + << "that provide the same content in plain HTML format.

" + << ""; + + current_doc = html_parser.parse(help_html.str(), "help://"); + rendered_lines = renderer.render(current_doc, screen_width); + scroll_pos = 0; + current_link = -1; + status_message = "Help - Press q to return"; + } +}; + +Browser::Browser() : pImpl(std::make_unique()) { + pImpl->input_handler.set_status_callback([this](const std::string& msg) { + pImpl->status_message = msg; + }); +} + +Browser::~Browser() = default; + +void Browser::run(const std::string& initial_url) { + pImpl->init_screen(); + + if (!initial_url.empty()) { + load_url(initial_url); + } else { + pImpl->show_help(); + } + + bool running = true; + while (running) { + pImpl->draw_screen(); + refresh(); + + int ch = getch(); + if (ch == ERR) { + napms(50); // 50ms sleep + continue; + } + + auto result = pImpl->input_handler.handle_key(ch); + + if (result.action == Action::QUIT) { + running = false; + } else if (result.action != Action::NONE) { + pImpl->handle_action(result); + } + } + + pImpl->cleanup_screen(); +} + +bool Browser::load_url(const std::string& url) { + return pImpl->load_page(url); +} + +std::string Browser::get_current_url() const { + return pImpl->current_url; +} diff --git a/src/browser.h b/src/browser.h new file mode 100644 index 0000000..64ddfde --- /dev/null +++ b/src/browser.h @@ -0,0 +1,28 @@ +#pragma once + +#include "http_client.h" +#include "html_parser.h" +#include "text_renderer.h" +#include "input_handler.h" +#include +#include +#include + +class Browser { +public: + Browser(); + ~Browser(); + + // 启动浏览器(进入主循环) + void run(const std::string& initial_url = ""); + + // 加载URL + bool load_url(const std::string& url); + + // 获取当前URL + std::string get_current_url() const; + +private: + class Impl; + std::unique_ptr pImpl; +};