diff --git a/LINK_NAVIGATION.md b/LINK_NAVIGATION.md deleted file mode 100644 index 3c669af..0000000 --- a/LINK_NAVIGATION.md +++ /dev/null @@ -1,76 +0,0 @@ -# Quick Link Navigation Guide - -The browser now supports vim-style quick navigation to links! - -## Features - -### 1. Visual Link Numbers -All links are displayed inline in the text with numbers like `[0]`, `[1]`, `[2]`, etc. -- Links are shown with yellow color and underline -- Active link (selected with Tab) has yellow background - -### 2. Quick Navigation Methods - -#### Method 1: Number + Enter -Type a number and press Enter to jump to that link: -``` -3 - Jump to link [3] -10 - Jump to link [10] -``` - -#### Method 2: 'f' command (follow) -Press `f` followed by a number to immediately open that link: -``` -f3 - Open link [3] directly -f10 - Open link [10] directly -``` - -Or type the number first: -``` -3f - Open link [3] directly -10f - Open link [10] directly -``` - -#### Method 3: Traditional Tab navigation (still works) -``` -Tab - Next link -Shift-Tab/T - Previous link -Enter - Follow current highlighted link -``` - -## Examples - -Given a page with these links: -- "Google[0]" -- "GitHub[1]" -- "Wikipedia[2]" - -You can: -- Press `1` to select GitHub link -- Press `f2` to immediately open Wikipedia -- Press `Tab` twice then `Enter` to open Wikipedia - -## Usage - -```bash -# Test with a real website -./tut https://example.com - -# View help -./tut -# Press ? for help -``` - -## Key Bindings Summary - -| Command | Action | -|---------|--------| -| `[N]` | Jump to link N | -| `f[N]` or `[N]f` | Open link N directly | -| `Tab` | Next link | -| `Shift-Tab` / `T` | Previous link | -| `Enter` | Follow current link | -| `h` | Go back | -| `l` | Go forward | - -All standard vim navigation keys (j/k, gg/G, /, n/N) still work as before! diff --git a/Makefile b/Makefile deleted file mode 100644 index b6da118..0000000 --- a/Makefile +++ /dev/null @@ -1,28 +0,0 @@ -# Simple Makefile wrapper for CMake build system -# Follows Unix convention: simple interface to underlying build - -BUILD_DIR = build -TARGET = tut - -.PHONY: all clean install test help - -all: - @mkdir -p $(BUILD_DIR) - @cd $(BUILD_DIR) && cmake .. && cmake --build . - @cp $(BUILD_DIR)/$(TARGET) . - -clean: - @rm -rf $(BUILD_DIR) $(TARGET) - -install: all - @install -m 755 $(TARGET) /usr/local/bin/ - -test: all - @./$(TARGET) https://example.com - -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" diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md deleted file mode 100644 index 396dedb..0000000 --- a/NEXT_STEPS.md +++ /dev/null @@ -1,266 +0,0 @@ -# TUT 2.0 - 下次继续从这里开始 - -## 当前位置 -- **阶段**: Phase 10 - 异步图片加载 (已完成!) -- **进度**: 多并发图片下载、渐进式渲染、非阻塞UI -- **最后提交**: `feat: Add async image loading with progressive rendering` - -## 立即可做的事 - -### 1. 使用书签功能 -- **B** - 添加当前页面到书签 -- **D** - 从书签中移除当前页面 -- **:bookmarks** 或 **:bm** - 查看书签列表 - -书签存储在 `~/.config/tut/bookmarks.json` - -### 2. 查看历史记录 -- **:history** 或 **:hist** - 查看浏览历史 - -历史记录存储在 `~/.config/tut/history.json` - -### 3. 表单交互 -- **i** - 聚焦到第一个表单字段 -- **Tab** - 下一个表单字段 -- **Shift+Tab** - 上一个表单字段 -- **Enter** - 激活字段(文本输入/下拉选择/复选框) -- 在文本输入模式下: - - 输入文字实时更新 - - **Enter** 或 **Esc** - 退出编辑模式 -- 在下拉选择模式下: - - **j/k** 或 **↓/↑** - 导航选项 - - **Enter** - 选择当前选项 - - **Esc** - 取消选择 - -## 已完成的功能清单 - -### Phase 10 - 异步图片加载 -- [x] 异步二进制下载接口 (HttpClient) -- [x] 图片下载队列管理 -- [x] 多并发下载 (最多3张图片同时下载) -- [x] 渐进式渲染 (图片下载完立即显示) -- [x] 非阻塞UI (下载时可正常浏览) -- [x] 实时进度显示 -- [x] Esc取消图片加载 -- [x] 保留图片缓存系统兼容 - -### Phase 9 - 性能优化和测试工具 -- [x] 图片 LRU 缓存 (100张,10分钟过期) -- [x] 缓存命中统计显示 -- [x] 交互式测试脚本 (test_browser.sh) -- [x] 完整测试指南 (TESTING.md) -- [x] 帮助文档更新(包含所有新功能) -- [x] 测试清单和成功标准 - -### Phase 8 - 表单交互增强 -- [x] 文本输入框编辑 -- [x] 实时文本编辑和预览 -- [x] Tab/Shift+Tab 字段导航 -- [x] 复选框切换 -- [x] 下拉选择(SELECT/OPTION) -- [x] SELECT 选项解析和存储 -- [x] j/k 导航选项 -- [x] 状态栏显示 INSERT/SELECT 模式 -- [x] 'i' 键聚焦首个表单字段 - -### Phase 7 - 历史记录持久化 -- [x] HistoryEntry 数据结构 (URL, 标题, 访问时间) -- [x] JSON 持久化存储 (~/.config/tut/history.json) -- [x] 自动记录访问历史 -- [x] 重复访问更新时间 -- [x] 最大 1000 条记录限制 -- [x] :history 命令查看历史页面 -- [x] 历史链接可点击跳转 - -### Phase 6 - 异步HTTP -- [x] libcurl multi接口实现非阻塞请求 -- [x] AsyncState状态管理 (IDLE/LOADING/COMPLETE/FAILED/CANCELLED) -- [x] start_async_fetch() 启动异步请求 -- [x] poll_async() 非阻塞轮询 -- [x] cancel_async() 取消请求 -- [x] 加载动画 (旋转spinner: ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏) -- [x] Esc键取消加载 -- [x] 主循环50ms轮询集成 - -### Phase 5 - 书签管理 -- [x] 书签数据结构 (URL, 标题, 添加时间) -- [x] JSON 持久化存储 (~/.config/tut/bookmarks.json) -- [x] 添加书签 (B 键) -- [x] 删除书签 (D 键) -- [x] 书签列表页面 (:bookmarks 命令) -- [x] 书签链接可点击跳转 - -### Phase 4 - 图片支持 -- [x] `` 标签解析 (src, alt, width, height) -- [x] 图片占位符显示 `[alt text]` 或 `[Image: filename]` -- [x] `BinaryResponse` 结构体 -- [x] `HttpClient::fetch_binary()` 方法 -- [x] `ImageRenderer` 类框架 -- [x] PPM 格式内置解码 -- [x] stb_image.h 集成 (PNG/JPEG/GIF/BMP 支持) -- [x] 浏览器中的图片下载和渲染 -- [x] ASCII Art 彩色渲染 (True Color) - -### Phase 3 - 性能优化 -- [x] LRU 页面缓存 (20页, 5分钟过期) -- [x] 差分渲染 (只更新变化的单元格) -- [x] 批量输出优化 -- [x] 加载状态指示 - -### Phase 2 - 交互增强 -- [x] 搜索功能 (/, n/N) -- [x] 搜索高亮 -- [x] Tab 切换链接时自动滚动 -- [x] 窗口大小动态调整 -- [x] 表单渲染 (input, button, checkbox, radio, select) -- [x] POST 表单提交 - -### Phase 1 - 核心架构 -- [x] Terminal 抽象层 (raw mode, True Color) -- [x] FrameBuffer 双缓冲 -- [x] Renderer 差分渲染 -- [x] LayoutEngine 布局引擎 -- [x] DocumentRenderer 文档渲染 -- [x] Unicode 宽度计算 (CJK 支持) -- [x] 温暖护眼配色方案 - -## 代码结构 - -``` -src/ -├── browser.cpp/h # 主浏览器 (pImpl模式) -├── main.cpp # 程序入口点 -├── http_client.cpp/h # HTTP 客户端 (支持二进制和异步) -├── dom_tree.cpp/h # DOM 树 -├── html_parser.cpp/h # HTML 解析 -├── input_handler.cpp/h # 输入处理 -├── bookmark.cpp/h # 书签管理 -├── history.cpp/h # 历史记录管理 -├── render/ -│ ├── terminal.cpp/h # 终端抽象 (ncurses) -│ ├── renderer.cpp/h # FrameBuffer + 差分渲染 -│ ├── layout.cpp/h # 布局引擎 + 文档渲染 -│ ├── image.cpp/h # 图片渲染器 (ASCII Art) -│ ├── colors.h # 配色方案定义 -│ └── decorations.h # Unicode 装饰字符 -└── utils/ - ├── unicode.cpp/h # Unicode 处理 - └── stb_image.h # 图片解码库 - -tests/ -├── test_terminal.cpp # Terminal 测试 -├── test_renderer.cpp # Renderer 测试 -├── test_layout.cpp # Layout + 图片占位符测试 -├── test_http_async.cpp # HTTP 异步测试 -├── test_html_parse.cpp # HTML 解析测试 -├── test_bookmark.cpp # 书签测试 -└── test_history.cpp # 历史记录测试 -``` - -## 构建与运行 - -```bash -# 构建 -cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug -cmake --build build - -# 运行 -./build/tut # 显示帮助 -./build/tut https://example.com # 打开网页 - -# 测试 -./build/test_terminal # 终端测试 -./build/test_renderer # 渲染测试 -./build/test_layout # 布局+图片测试 -./build/test_http_async # HTTP异步测试 -./build/test_html_parse # HTML解析测试 -./build/test_bookmark # 书签测试 -``` - -## 快捷键 - -| 键 | 功能 | -|---|---| -| j/k | 上下滚动 | -| Ctrl+d/u | 翻页 | -| gg/G | 顶部/底部 | -| Tab/Shift+Tab | 切换链接/表单字段 | -| Enter | 跟随链接/激活字段 | -| i | 聚焦首个表单字段 | -| h/l | 后退/前进 | -| / | 搜索 | -| n/N | 下一个/上一个匹配 | -| r | 刷新 (跳过缓存) | -| B | 添加书签 | -| D | 删除书签 | -| :o URL | 打开URL | -| :bookmarks | 查看书签 | -| :history | 查看历史 | -| :q | 退出 | -| ? | 帮助 | -| Esc | 取消加载/退出编辑 | - -**表单编辑模式** (INSERT): -- 输入字符 - 编辑文本 -- Enter/Esc - 完成编辑 - -**下拉选择模式** (SELECT): -- j/k, ↓/↑ - 导航选项 -- Enter - 选择选项 -- Esc - 取消选择 - -## 下一步功能优先级 - -1. **Cookie 持久化** - 保存和自动发送 Cookie (已有内存Cookie支持) -2. **表单提交改进** - 文件上传、multipart/form-data -3. **更多HTML5支持** - 表格渲染、
代码块
-4. **性能优化** - DNS缓存、连接复用、HTTP/2
-
-## 恢复对话时说
-
-> "continue"
-
-## Git 信息
-
-- **当前标签**: `v2.0.0-alpha`
-- **远程仓库**: https://github.com/m1ngsama/TUT
-
-```bash
-# 恢复开发
-git clone https://github.com/m1ngsama/TUT.git
-cd TUT
-cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug
-cmake --build build
-./build/tut
-```
-
-## 测试指南
-
-查看 `TESTING.md` 获取完整测试指南,或运行:
-
-```bash
-./test_browser.sh
-```
-
-## 浏览器特性总结
-
-✓ **核心功能** - 异步HTTP加载、页面缓存、差分渲染
-✓ **导航** - 滚动、链接、历史记录
-✓ **搜索** - 全文搜索、高亮、导航
-✓ **表单** - 文本输入、复选框、下拉选择
-✓ **书签** - 持久化书签管理
-✓ **历史** - 浏览历史记录
-✓ **图片** - ASCII艺术渲染、智能缓存、异步加载
-✓ **性能** - LRU缓存、差分渲染、异步加载、多并发下载
-
-## 技术亮点
-
-- **完全异步**: 页面和图片都使用异步加载,UI永不阻塞
-- **渐进式渲染**: 图片下载完立即显示,无需等待全部完成
-- **多并发下载**: 最多3张图片同时下载,显著提升加载速度
-- **智能缓存**: 页面5分钟缓存、图片10分钟缓存,LRU策略
-- **差分渲染**: 只更新变化的屏幕区域,减少闪烁
-- **真彩色支持**: 24位True Color图片渲染
-
----
-更新时间: 2025-12-28
diff --git a/README.md b/README.md
index 5e14fbb..6af9ab7 100644
--- a/README.md
+++ b/README.md
@@ -1,272 +1,201 @@
-TUT(1) - Terminal User Interface Browser
-========================================
+# TUT - Terminal UI Textual Browser
 
-NAME
-----
-tut - vim-style terminal web browser
+A lightweight, high-performance terminal browser with a btop-style interface.
 
-SYNOPSIS
---------
-**tut** [*URL*]
+![Version](https://img.shields.io/badge/version-0.1.0-blue)
+![License](https://img.shields.io/badge/license-MIT-green)
+![C++](https://img.shields.io/badge/C%2B%2B-17-orange)
 
-**tut** **-h** | **--help**
+## Features
 
-DESCRIPTION
------------
-**tut** is a text-mode web browser designed for comfortable reading in the
-terminal. It extracts and displays the textual content of web pages with a
-clean, centered layout optimized for reading, while providing vim-style
-keyboard navigation.
+- **btop-style UI** - Modern four-panel layout with rounded borders
+- **Lightweight** - Binary size < 1MB, memory usage < 50MB
+- **Fast startup** - Launch in < 500ms
+- **Vim-style navigation** - j/k scrolling, / search, g/G jump
+- **Keyboard-driven** - Full keyboard navigation with function key shortcuts
+- **Themeable** - Multiple color themes (default, nord, gruvbox, solarized)
+- **Configurable** - TOML-based configuration
 
-The browser does not execute JavaScript or display images. It is designed
-for reading static HTML content, documentation, and text-heavy websites.
+## Screenshot
 
-OPTIONS
--------
-*URL*
-    Open the specified URL on startup. If omitted, displays the built-in
-    help page.
+```
+╭──────────────────────────────────────────────────────────────────────────────╮
+│[◀] [▶] [⟳] ╭────────────────────────────────────────────────────────╮ [⚙] [?]│
+│           │https://example.com                                      │        │
+│           ╰────────────────────────────────────────────────────────╯        │
+├──────────────────────────────────────────────────────────────────────────────┤
+│                          Example Domain                                      │
+├──────────────────────────────────────────────────────────────────────────────┤
+│This domain is for use in illustrative examples in documents.                 │
+│                                                                              │
+│[1] More information...                                                       │
+│                                                                              │
+├────────────────────────────────────────┬─────────────────────────────────────┤
+│📑 Bookmarks                            │📊 Status                            │
+│  example.com                           │  ⬇ 1.2 KB  🕐 0.3s                  │
+├────────────────────────────────────────┴─────────────────────────────────────┤
+│[F1]Help [F2]Bookmarks [F3]History [F10]Quit                                  │
+╰──────────────────────────────────────────────────────────────────────────────╯
+```
 
-**-h**, **--help**
-    Display usage information and exit.
+## Installation
 
-KEYBINDINGS
------------
-**tut** uses vim-style keybindings throughout.
-
-### Navigation
-
-**j**, **Down**
-    Scroll down one line.
-
-**k**, **Up**
-    Scroll up one line.
-
-**Ctrl-D**, **Space**
-    Scroll down one page.
-
-**Ctrl-U**, **b**
-    Scroll up one page.
-
-**gg**
-    Jump to top of page.
-
-**G**
-    Jump to bottom of page.
-
-**[***count***]G**
-    Jump to line *count* (e.g., **50G** jumps to line 50).
-
-**[***count***]j**, **[***count***]k**
-    Scroll down/up *count* lines (e.g., **5j** scrolls down 5 lines).
-
-### Link Navigation
-
-**Tab**
-    Move to next link.
-
-**Shift-Tab**, **T**
-    Move to previous link.
-
-**Enter**
-    Follow current link.
-
-**h**, **Left**
-    Go back in history.
-
-**l**, **Right**
-    Go forward in history.
-
-### Search
-
-**/**
-    Start search. Enter search term and press **Enter**.
-
-**n**
-    Jump to next search match.
-
-**N**
-    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
-
-Press **:** to enter command mode. Available commands:
-
-**:q**, **:quit**
-    Quit the browser.
-
-**:o** *URL*, **:open** *URL*
-    Open *URL*.
-
-**:r**, **:refresh**
-    Reload current page.
-
-**:h**, **:help**
-    Display help page.
-
-**:***number*
-    Jump to line *number*.
-
-### Other
-
-**r**
-    Reload current page.
-
-**q**
-    Quit the browser.
-
-**?**
-    Display help page.
-
-**ESC**
-    Cancel command or search input.
-
-LIMITATIONS
------------
-**tut** does not execute JavaScript. Modern single-page applications (SPAs)
-built with React, Vue, Angular, or similar frameworks will not work correctly,
-as they require JavaScript to render content.
-
-To determine if a site will work with **tut**, use:
-
-    curl https://example.com | less
-
-If you can see the actual content in the HTML source, the site will work.
-If you only see JavaScript code or empty div elements, it will not.
-
-Additionally:
-- No image display
-- No CSS layout support
-- No AJAX or dynamic content loading
-
-EXAMPLES
---------
-View the built-in help:
-
-    tut
-
-Browse Hacker News:
-
-    tut https://news.ycombinator.com
-
-Read Wikipedia:
-
-    tut https://en.wikipedia.org/wiki/Unix_philosophy
-
-Open a URL, search for "unix", and navigate:
-
-    tut https://example.com
-    /unix
-    n
-
-DEPENDENCIES
-------------
-- ncurses or ncursesw (for terminal UI)
-- libcurl (for HTTPS support)
-- CMake >= 3.15 (build time)
-- C++17 compiler (build time)
-
-INSTALLATION
-------------
-### From Source
+### Prerequisites
 
 **macOS (Homebrew):**
-
-    brew install cmake ncurses curl
-    mkdir -p build && cd build
-    cmake ..
-    cmake --build .
-    sudo install -m 755 tut /usr/local/bin/
+```bash
+brew install cmake gumbo-parser openssl ftxui cpp-httplib toml11
+```
 
 **Linux (Debian/Ubuntu):**
+```bash
+sudo apt install cmake libgumbo-dev libssl-dev
+```
 
-    sudo apt-get install cmake libncursesw5-dev libcurl4-openssl-dev
-    mkdir -p build && cd build
-    cmake ..
-    cmake --build .
-    sudo install -m 755 tut /usr/local/bin/
+### Building from Source
 
-**Linux (Fedora/RHEL):**
+```bash
+git clone https://github.com/m1ngsama/TUT.git
+cd TUT
+cmake -B build -DCMAKE_PREFIX_PATH=/opt/homebrew  # macOS
+cmake -B build                                      # Linux
+cmake --build build -j$(nproc)
+```
 
-    sudo dnf install cmake gcc-c++ ncurses-devel libcurl-devel
-    mkdir -p build && cd build
-    cmake ..
-    cmake --build .
-    sudo install -m 755 tut /usr/local/bin/
+### Running
 
-### Using Makefile
+```bash
+./build/tut                      # Start with blank page
+./build/tut https://example.com  # Open URL directly
+./build/tut --help               # Show help
+```
 
-    make
-    sudo make install
+## Keyboard Shortcuts
 
-FILES
------
-No configuration files are used. The browser is stateless and does not
-store history, cookies, or cache.
+### Navigation
+| Key | Action |
+|-----|--------|
+| `j` / `↓` | Scroll down |
+| `k` / `↑` | Scroll up |
+| `Space` | Page down |
+| `b` | Page up |
+| `g` | Go to top |
+| `G` | Go to bottom |
+| `Backspace` | Go back |
 
-ENVIRONMENT
------------
-**tut** respects the following environment variables:
+### Links
+| Key | Action |
+|-----|--------|
+| `Tab` | Next link |
+| `Shift+Tab` | Previous link |
+| `Enter` | Follow link |
+| `1-9` | Jump to link by number |
 
-**TERM**
-    Terminal type. Must support basic cursor movement and colors.
+### Search
+| Key | Action |
+|-----|--------|
+| `/` | Start search |
+| `n` | Next result |
+| `N` | Previous result |
 
-**LINES**, **COLUMNS**
-    Terminal size. Automatically detected via ncurses.
+### UI
+| Key | Action |
+|-----|--------|
+| `Ctrl+L` | Focus address bar |
+| `F1` / `?` | Help |
+| `F2` | Bookmarks |
+| `F3` | History |
+| `Ctrl+D` | Add bookmark |
+| `Ctrl+Q` / `F10` / `q` | Quit |
 
-EXIT STATUS
------------
-**0**
-    Success.
+## Configuration
 
-**1**
-    Error occurred (e.g., invalid URL, network error, ncurses initialization
-    failure).
+Configuration files are stored in `~/.config/tut/`:
 
-PHILOSOPHY
-----------
-**tut** follows the Unix philosophy:
+```
+~/.config/tut/
+├── config.toml      # Main configuration
+└── themes/          # Custom themes
+    └── mytheme.toml
+```
 
-1. Do one thing well: display and navigate text content from the web.
-2. Work with other programs: output can be piped, URLs can come from stdin.
-3. Simple and minimal: no configuration files, no persistent state.
-4. Text-focused: everything is text, processed and displayed cleanly.
+### Example config.toml
 
-The design emphasizes keyboard efficiency, clean output, and staying out
-of your way.
+```toml
+[general]
+theme = "default"
+homepage = "https://example.com"
+debug = false
 
-SEE ALSO
---------
-lynx(1), w3m(1), curl(1), vim(1)
+[browser]
+timeout = 30
+user_agent = "TUT/0.1.0"
 
-BUGS
-----
-Report bugs at: https://github.com/m1ngsama/TUT/issues
+[ui]
+word_wrap = true
+show_images = true
+```
 
-AUTHORS
--------
-m1ngsama 
+## Project Structure
 
-Inspired by lynx, w3m, and vim.
+```
+TUT/
+├── CMakeLists.txt          # Build configuration
+├── README.md               # This file
+├── LICENSE                 # MIT License
+├── cmake/                  # CMake modules
+│   └── version.hpp.in
+├── src/                    # Source code
+│   ├── main.cpp           # Entry point
+│   ├── core/              # Browser engine, HTTP, URL parsing
+│   ├── ui/                # FTXUI components
+│   ├── renderer/          # HTML rendering
+│   └── utils/             # Logger, config, themes
+├── tests/                  # Unit and integration tests
+│   ├── unit/
+│   └── integration/
+└── assets/                 # Default configurations
+    ├── config.toml
+    ├── themes/
+    └── keybindings/
+```
 
-LICENSE
--------
-MIT License. See LICENSE file for details.
+## Dependencies
+
+| Library | Purpose | Version |
+|---------|---------|---------|
+| [FTXUI](https://github.com/ArthurSonzogni/ftxui) | Terminal UI framework | 5.0+ |
+| [cpp-httplib](https://github.com/yhirose/cpp-httplib) | HTTP client | 0.14+ |
+| [gumbo-parser](https://github.com/google/gumbo-parser) | HTML parsing | 0.10+ |
+| [toml11](https://github.com/ToruNiina/toml11) | TOML configuration | 3.8+ |
+| [OpenSSL](https://www.openssl.org/) | HTTPS support | 1.1+ |
+
+## Limitations
+
+- **No JavaScript** - SPAs and dynamic content won't work
+- **No CSS layout** - Only basic text formatting
+- **No images** - ASCII art rendering planned for future
+- **Text-only** - Focused on readable content
+
+## Contributing
+
+Contributions are welcome! Please read the coding style guidelines:
+
+- C++17 standard
+- Google C++ Style Guide
+- Use `.hpp` for headers, `.cpp` for implementation
+- All public APIs must have documentation comments
+
+## License
+
+MIT License - see [LICENSE](LICENSE) file for details.
+
+## Authors
+
+- **m1ngsama** - [GitHub](https://github.com/m1ngsama)
+
+## Acknowledgments
+
+- Inspired by [btop](https://github.com/aristocratos/btop) for UI design
+- [FTXUI](https://github.com/ArthurSonzogni/ftxui) for the amazing TUI framework
+- [lynx](https://lynx.invisible-island.net/) and [w3m](http://w3m.sourceforge.net/) for inspiration
diff --git a/REAL_WORLD_TEST_REPORT.md b/REAL_WORLD_TEST_REPORT.md
deleted file mode 100644
index 212cda9..0000000
--- a/REAL_WORLD_TEST_REPORT.md
+++ /dev/null
@@ -1,390 +0,0 @@
-# TUT Browser - Real World Testing Report
-**Date**: 2025-12-28
-**Version**: 2.0.0 (with Phase 10 async image loading)
-**Tester**: Automated + Manual Evaluation
-
-## Testing Methodology
-
-We tested TUT with various website categories to evaluate:
-- ✅ **Loading Speed**: How quickly content becomes readable
-- ✅ **Image Loading**: Async behavior and progressive rendering
-- ✅ **Readability**: Content clarity and layout quality
-- ✅ **Responsiveness**: UI interaction during loading
-- ✅ **Overall UX**: Real human experience
-
----
-
-## Test Results by Category
-
-### 1️⃣ Simple Static Sites
-
-#### Example.com (https://example.com)
-- **Loading**: ⚡ Instant (< 1 second)
-- **Images**: No images
-- **Readability**: ⭐⭐⭐⭐⭐ Excellent
-- **Content**: Clean, centered text with proper spacing
-- **Experience**: Perfect for simple pages
-
-**Notes**:
-- Title renders correctly
-- Links are highlighted and navigable
-- Smooth scrolling experience
-- No issues detected
-
----
-
-#### Motherfucking Website (https://motherfuckingwebsite.com)
-- **Loading**: ⚡ Instant
-- **Images**: None
-- **Readability**: ⭐⭐⭐⭐⭐ Excellent
-- **Content**: Long-form text with good line width
-- **Experience**: Great for text-heavy content
-
-**Notes**:
-- Excellent for reading long articles
-- Good contrast with warm color scheme
-- vim-style navigation feels natural
-- Perfect for distraction-free reading
-
----
-
-### 2️⃣ News & Discussion Sites
-
-#### Hacker News (https://news.ycombinator.com)
-- **Loading**: ⚡ Fast (~1-2 seconds)
-- **Images**: Minimal (logo only)
-- **Readability**: ⭐⭐⭐⭐⭐ Excellent
-- **Content**: Compact list layout works well
-- **Experience**: Highly usable
-
-**Notes**:
-- News titles are clear and clickable
-- Point counts and metadata visible
-- Tab navigation between links works perfectly
-- Great for browsing headlines
-- No JavaScript needed - works flawlessly
-- **VERDICT**: One of the best use cases for TUT!
-
----
-
-#### Lobsters (https://lobste.rs)
-- **Loading**: ⚡ Fast (~1-2 seconds)
-- **Images**: User avatars (async load)
-- **Readability**: ⭐⭐⭐⭐ Very Good
-- **Content**: Clean thread list
-- **Experience**: Good
-
-**Notes**:
-- Links are well-organized
-- Tags and categories visible
-- Async image loading doesn't block content
-- Minor: Some layout elements from CSS missing
-- **VERDICT**: Very usable for tech news
-
----
-
-### 3️⃣ Documentation Sites
-
-#### curl Manual Page (https://curl.se/docs/manpage.html)
-- **Loading**: Medium (~2-3 seconds)
-- **Images**: Few technical diagrams
-- **Readability**: ⭐⭐⭐⭐ Very Good
-- **Content**: Code blocks and technical text
-- **Experience**: Usable
-
-**Notes**:
-- Long technical content renders well
-- Code blocks need better formatting (no `
` support yet)
-- Search function (`/`) very useful for finding options
-- Good for quick reference
-- **IMPROVEMENT NEEDED**: Better `
` and `` rendering
-
----
-
-#### Rust Book (https://doc.rust-lang.org/book/)
-- **Loading**: Medium (~2-3 seconds)
-- **Images**: Code diagrams (async)
-- **Readability**: ⭐⭐⭐⭐ Very Good
-- **Content**: Educational text with examples
-- **Experience**: Good for learning
-
-**Notes**:
-- Chapter navigation works
-- Code examples readable but could be better formatted
-- Async image loading for diagrams is smooth
-- Marks (`ma`, `'a`) useful for bookmarking chapters
-- **VERDICT**: Decent for technical reading
-
----
-
-### 4️⃣ Wikipedia
-
-#### Wikipedia - Unix (https://en.wikipedia.org/wiki/Unix)
-- **Loading**: Slow (~4-5 seconds for content + images)
-- **Images**: **Many** (diagrams, screenshots, photos)
-- **Readability**: ⭐⭐⭐ Good (with some issues)
-- **Content**: Dense encyclopedic text
-- **Experience**: Acceptable but needs improvements
-
-**Notes**:
-- **Main content loads fast** - can start reading immediately
-- **Images load progressively** - see them appear one by one
-- Infoboxes and tables have layout issues (no table support)
-- Reference links [1][2][3] visible
-- **ASYNC IMAGE LOADING WORKS WELL**:
-  - Page is usable while images download
-  - Progress indicator shows "Loading images 3/8"
-  - Can scroll and navigate during image loading
-- **IMPROVEMENT NEEDED**: Table rendering for infoboxes
-
-**Real UX Experience**:
-```
-0s:  Page title appears, can start reading
-1s:  Main text content loaded, fully readable
-2s:  First 3 images appear (3 concurrent downloads)
-3s:  Next batch of images loads
-4s:  All images complete
-```
-**UI stayed responsive throughout!** ✅
-
----
-
-#### Wikipedia - World Wide Web (https://en.wikipedia.org/wiki/World_Wide_Web)
-- **Loading**: Similar to Unix page (~4-5s total)
-- **Images**: Multiple historical diagrams and screenshots
-- **Readability**: ⭐⭐⭐ Good
-- **Content**: Technical history, well-structured
-- **Experience**: Good with progressive loading
-
-**Notes**:
-- Can read introduction while images load in background
-- Timeline sections readable
-- ASCII art rendering of diagrams is interesting but low-fi
-- **Progressive rendering really shines here**
-- No UI freezing even with 10+ images
-
----
-
-### 5️⃣ Tech Blogs
-
-#### LWN.net (https://lwn.net)
-- **Loading**: Fast (~2 seconds)
-- **Images**: Few embedded images
-- **Readability**: ⭐⭐⭐⭐ Very Good
-- **Content**: Article headlines and summaries
-- **Experience**: Good for browsing
-
-**Notes**:
-- Article summaries clear and navigable
-- Links to full articles work well
-- Subscription wall notice visible
-- Good for tech news consumption
-
----
-
-## Summary Statistics
-
-### Performance Metrics
-
-| Site Type | Avg Load Time | Image Count | Readability | Usability |
-|-----------|--------------|-------------|-------------|-----------|
-| Simple Static | 0.5s | 0 | ⭐⭐⭐⭐⭐ | Excellent |
-| News Sites | 1-2s | 0-5 | ⭐⭐⭐⭐⭐ | Excellent |
-| Documentation | 2-3s | 3-10 | ⭐⭐⭐⭐ | Very Good |
-| Wikipedia | 4-5s | 8-15 | ⭐⭐⭐ | Good |
-| Tech Blogs | 2s | 2-8 | ⭐⭐⭐⭐ | Very Good |
-
----
-
-## Async Image Loading - Real World Performance
-
-### ✅ What Works Great
-
-1. **Non-Blocking UI**
-   - Can scroll, navigate, search while images download
-   - Esc key cancels loading instantly
-   - No frozen UI at any point
-
-2. **Progressive Rendering**
-   - Content appears immediately
-   - Images pop in as they finish
-   - Always know what's loading (progress indicator)
-
-3. **Parallel Downloads**
-   - 3 concurrent downloads significantly faster than sequential
-   - Wikipedia with 10 images: ~4s vs estimated ~12s sequential
-   - **3x speedup confirmed in real usage!**
-
-4. **Cache Performance**
-   - Revisiting pages is instant
-   - Cached images don't re-download
-   - Status shows "cached: 5" when applicable
-
-### 📊 Before/After Comparison
-
-**OLD (Synchronous Loading)**:
-```
-Load Wikipedia Unix page:
-0s:  "Loading..." - UI FROZEN
-5s:  "Downloading image 1/10..." - UI FROZEN
-10s: "Downloading image 2/10..." - UI FROZEN
-15s: "Downloading image 3/10..." - UI FROZEN
-...
-50s: Page finally usable
-```
-
-**NEW (Async Loading)**:
-```
-Load Wikipedia Unix page:
-0s:  Title and text appear - START READING
-1s:  Can scroll, navigate, search
-2s:  First 3 images appear (parallel download)
-3s:  Next 3 images appear
-4s:  All images complete
-     UI responsive the ENTIRE time!
-```
-
-**Human Experience**: **MASSIVELY BETTER** ✅
-
----
-
-## Readability Assessment
-
-### What Makes Content Readable in TUT?
-
-✅ **Excellent for**:
-- News aggregators (HN, Lobsters, Reddit text)
-- Blog posts and articles
-- Documentation (with minor limitations)
-- Long-form reading
-- Technical reference material
-
-⚠️ **Limitations**:
-- No `
` support (infoboxes, data tables render poorly) -- No `
` formatting (code blocks not monospaced)
-- No CSS layout (multi-column layouts flatten)
-- JavaScript-heavy sites don't work
-
-### Content Clarity
-
-- **Font**: Readable terminal font with good spacing
-- **Colors**: Warm color scheme easy on eyes
-- **Contrast**: Good foreground/background contrast
-- **Line Width**: Appropriate for reading (not too wide)
-- **Scrolling**: Smooth with vim keys (j/k)
-
-### Navigation Experience
-
-- **Tab**: Jump between links - works great
-- **Enter**: Follow links - instant
-- **h/l**: Back/forward - smooth
-- **/**: Search - very useful for finding content
-- **Esc**: Cancel loading - responsive
-
----
-
-## Real Human Feelings 🧑‍💻
-
-### What Users Will Love ❤️
-
-1. **Speed**: "It's so fast! Content appears instantly."
-2. **Simplicity**: "No ads, no tracking, no distractions."
-3. **Keyboard Control**: "vim keys everywhere - feels natural."
-4. **Readability**: "Text-focused, perfect for reading articles."
-5. **Lightweight**: "Doesn't slow down my machine."
-
-### What Users Will Notice 🤔
-
-1. **Image Quality**: "ASCII art images are fun but low-resolution."
-2. **Missing Tables**: "Wikipedia infoboxes are messy."
-3. **Layout**: "Some sites look different without CSS."
-4. **No JavaScript**: "Modern web apps don't work."
-
-### Best Use Cases ⭐
-
-1. **News Browsing**: Hacker News, Lobsters, Reddit (text)
-2. **Documentation**: Reading technical docs and manuals
-3. **Wikipedia**: Quick research (despite table issues)
-4. **Blogs**: Reading articles and essays
-5. **Learning**: Following tutorials and guides
-
-### Not Ideal For ❌
-
-1. Shopping sites (complex layouts)
-2. Social media (JavaScript-heavy)
-3. Modern web apps (React/Vue sites)
-4. Video/audio content
-5. Complex data tables
-
----
-
-## Recommendations for Future Improvements
-
-### High Priority 🔥
-
-1. **`
` Support** - Would fix Wikipedia infoboxes -2. **`
` Formatting** - Monospace code blocks
-3. **Better Link Indicators** - Show external vs internal links
-
-### Medium Priority 💡
-
-1. **Cookie Persistence** - Stay logged into sites
-2. **Form Submit Improvements** - Better form handling
-3. **Download Progress** - More detailed loading feedback
-
-### Nice to Have ✨
-
-1. **Custom Color Schemes** - Light/dark mode toggle
-2. **Font Size Control** - Adjustability
-3. **Bookmarklet** - Quick add current page
-
----
-
-## Final Verdict 🎯
-
-### Overall Rating: ⭐⭐⭐⭐ (4/5 stars)
-
-**TUT is EXCELLENT for its intended purpose**: a fast, keyboard-driven, terminal-based browser for text-focused browsing.
-
-### Phase 10 Async Image Loading: ✅ **SUCCESS**
-
-The async image loading implementation is a **game changer**:
-- UI remains responsive at all times
-- Progressive rendering feels modern and smooth
-- 3x faster than synchronous loading
-- Real performance gains visible on image-heavy sites
-
-### Real Human Feeling
-
-> **"TUT feels like a breath of fresh air in the modern web."**
->
-> It strips away all the bloat and gives you what matters: **content**.
-> The async image loading makes it feel fast and responsive, even
-> on complex pages. For reading news, docs, and articles, it's
-> genuinely enjoyable to use.
->
-> Yes, it has limitations (tables, complex layouts), but for its
-> core use case - **focused, distraction-free reading** - it's
-> fantastic. The vim keybindings and instant response make it feel
-> like a native terminal tool, not a sluggish browser.
->
-> **Would I use it daily?** Yes, for HN, docs, and Wikipedia lookups.
-> **Would I replace Chrome?** No, but that's not the point.
-> **Is it readable?** Absolutely. Better than many terminal browsers.
-
----
-
-## Test Conclusion
-
-✅ **Async image loading works flawlessly in real-world usage**
-✅ **Content is highly readable for text-focused sites**
-✅ **Performance is excellent across all site categories**
-✅ **User experience is smooth and responsive**
-⚠️ **Some limitations remain (tables, complex CSS) - acceptable trade-offs**
-
-**TUT 2.0 with Phase 10 is production-ready for its target audience!** 🚀
-
----
-
-**Testing Completed**: 2025-12-28
-**Next Phase**: Consider table rendering support for even better Wikipedia experience
diff --git a/TESTING.md b/TESTING.md
deleted file mode 100644
index 4447ccd..0000000
--- a/TESTING.md
+++ /dev/null
@@ -1,146 +0,0 @@
-# TUT Browser Testing Guide
-
-This document provides comprehensive testing instructions to ensure the browser works correctly.
-
-## Quick Start
-
-```bash
-# Build the browser
-cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug
-cmake --build build
-
-# Run with a test site
-./build/tut http://example.com
-
-# Or use the interactive test script
-./test_browser.sh
-```
-
-## Feature Testing Checklist
-
-### Basic Navigation
-- [ ] Browser loads and displays page correctly
-- [ ] Scroll with j/k works smoothly
-- [ ] Page up/down (Ctrl+d/u) works
-- [ ] Go to top (gg) and bottom (G) works
-- [ ] Back (h) and forward (l) navigation works
-
-### Link Navigation
-- [ ] Tab cycles through links
-- [ ] Shift+Tab goes to previous link
-- [ ] Enter follows the active link
-- [ ] Links are highlighted when active
-- [ ] Link URLs shown in status bar
-
-### Search
-- [ ] Press / to enter search mode
-- [ ] Type search term and press Enter
-- [ ] Matches are highlighted
-- [ ] n/N navigate between matches
-- [ ] Search count shown in status bar
-
-### Form Interaction
-- [ ] Press 'i' to focus first form field
-- [ ] Tab/Shift+Tab navigate between fields
-- [ ] Enter on text input enters edit mode
-- [ ] Text can be typed and edited
-- [ ] Backspace removes characters
-- [ ] Enter or Esc exits edit mode
-- [ ] Checkbox toggles with Enter
-- [ ] SELECT dropdown shows options
-- [ ] j/k navigate dropdown options
-- [ ] Enter selects option in dropdown
-- [ ] Selected option displays correctly
-
-### Bookmarks
-- [ ] Press B to bookmark current page
-- [ ] Press D to remove bookmark
-- [ ] Type :bookmarks to view all bookmarks
-- [ ] Bookmarks persist between sessions
-- [ ] Can click bookmarks to open pages
-
-### History
-- [ ] Type :history to view browsing history
-- [ ] History shows URLs and titles
-- [ ] History entries are clickable
-- [ ] History persists between sessions
-- [ ] Recent pages appear at top
-
-### Performance
-- [ ] Page loads are async with spinner
-- [ ] Esc cancels page loading
-- [ ] Page cache works (revisit loads instantly)
-- [ ] Image cache works (images load from cache)
-- [ ] Status shows "cached: N" for cached images
-- [ ] Scrolling is smooth
-- [ ] No noticeable lag in UI
-
-### Commands
-- [ ] :o URL opens new URL
-- [ ] :q quits the browser
-- [ ] :bookmarks shows bookmarks
-- [ ] :history shows history
-- [ ] :help shows help page
-- [ ] ? also shows help page
-
-### Edge Cases
-- [ ] Window resize updates layout correctly
-- [ ] Very long pages scroll correctly
-- [ ] Pages without links/forms work
-- [ ] Unicode text displays correctly
-- [ ] CJK characters display correctly
-- [ ] Images render as ASCII art (if stb_image available)
-- [ ] Error handling for failed page loads
-
-## Test Websites
-
-### Simple Test Sites
-1. **http://example.com** - Basic HTML test
-2. **http://info.cern.ch** - First website ever, very simple
-3. **http://motherfuckingwebsite.com** - Minimalist design
-4. **http://textfiles.com** - Text-only content
-
-### Form Testing
-Create a local test file (test_form.html is provided):
-```bash
-python3 -m http.server 8000
-./build/tut http://localhost:8000/test_form.html
-```
-
-Test the form features:
-- Text input editing
-- Checkbox toggling
-- Dropdown selection
-- Tab navigation
-
-### Performance Testing
-1. Load a page
-2. Press 'r' to refresh (should use cache)
-3. Load the same page again (should be instant from cache)
-4. Check status bar shows "cached" messages
-
-## Known Limitations
-
-- HTTPS support depends on libcurl configuration
-- Some complex JavaScript-heavy sites won't work (static HTML only)
-- File:// URLs may not work depending on curl configuration
-- Form submission is not yet implemented
-- Cookies are not yet supported
-
-## Reporting Issues
-
-When reporting issues, please include:
-1. The URL you were trying to load
-2. The exact steps to reproduce
-3. Expected vs actual behavior
-4. Any error messages
-
-## Success Criteria
-
-The browser is working correctly if:
-1. ✓ Can load and display simple HTML pages
-2. ✓ Navigation (scroll, links) works smoothly
-3. ✓ Form interaction is responsive and intuitive
-4. ✓ Bookmarks and history persist correctly
-5. ✓ Caching improves performance noticeably
-6. ✓ No crashes or hangs during normal use
diff --git a/src/bookmark.cpp b/src/bookmark.cpp
deleted file mode 100644
index ffe3720..0000000
--- a/src/bookmark.cpp
+++ /dev/null
@@ -1,248 +0,0 @@
-#include "bookmark.h"
-#include 
-#include 
-#include 
-#include 
-#include 
-
-namespace tut {
-
-BookmarkManager::BookmarkManager() {
-    load();
-}
-
-BookmarkManager::~BookmarkManager() {
-    save();
-}
-
-std::string BookmarkManager::get_config_dir() {
-    const char* home = std::getenv("HOME");
-    if (!home) {
-        home = "/tmp";
-    }
-    return std::string(home) + "/.config/tut";
-}
-
-std::string BookmarkManager::get_bookmarks_path() {
-    return get_config_dir() + "/bookmarks.json";
-}
-
-bool BookmarkManager::ensure_config_dir() {
-    std::string dir = get_config_dir();
-
-    // 检查目录是否存在
-    struct stat st;
-    if (stat(dir.c_str(), &st) == 0) {
-        return S_ISDIR(st.st_mode);
-    }
-
-    // 创建 ~/.config 目录
-    std::string config_dir = std::string(std::getenv("HOME") ? std::getenv("HOME") : "/tmp") + "/.config";
-    mkdir(config_dir.c_str(), 0755);
-
-    // 创建 ~/.config/tut 目录
-    return mkdir(dir.c_str(), 0755) == 0 || errno == EEXIST;
-}
-
-// 简单的 JSON 转义
-static std::string json_escape(const std::string& s) {
-    std::string result;
-    result.reserve(s.size() + 10);
-    for (char c : s) {
-        switch (c) {
-            case '"': result += "\\\""; break;
-            case '\\': result += "\\\\"; break;
-            case '\n': result += "\\n"; break;
-            case '\r': result += "\\r"; break;
-            case '\t': result += "\\t"; break;
-            default: result += c; break;
-        }
-    }
-    return result;
-}
-
-// 简单的 JSON 反转义
-static std::string json_unescape(const std::string& s) {
-    std::string result;
-    result.reserve(s.size());
-    for (size_t i = 0; i < s.size(); ++i) {
-        if (s[i] == '\\' && i + 1 < s.size()) {
-            switch (s[i + 1]) {
-                case '"': result += '"'; ++i; break;
-                case '\\': result += '\\'; ++i; break;
-                case 'n': result += '\n'; ++i; break;
-                case 'r': result += '\r'; ++i; break;
-                case 't': result += '\t'; ++i; break;
-                default: result += s[i]; break;
-            }
-        } else {
-            result += s[i];
-        }
-    }
-    return result;
-}
-
-bool BookmarkManager::load() {
-    bookmarks_.clear();
-
-    std::ifstream file(get_bookmarks_path());
-    if (!file) {
-        return false;  // 文件不存在,这是正常的
-    }
-
-    std::string content((std::istreambuf_iterator(file)),
-                        std::istreambuf_iterator());
-    file.close();
-
-    // 简单的 JSON 解析
-    // 格式: [{"url":"...","title":"...","time":123}, ...]
-    size_t pos = content.find('[');
-    if (pos == std::string::npos) return false;
-
-    pos++;  // 跳过 '['
-
-    while (pos < content.size()) {
-        // 查找对象开始
-        pos = content.find('{', pos);
-        if (pos == std::string::npos) break;
-        pos++;
-
-        Bookmark bm;
-
-        // 解析字段
-        while (pos < content.size() && content[pos] != '}') {
-            // 跳过空白
-            while (pos < content.size() && (content[pos] == ' ' || content[pos] == '\n' ||
-                   content[pos] == '\r' || content[pos] == '\t' || content[pos] == ',')) {
-                pos++;
-            }
-
-            if (content[pos] == '}') break;
-
-            // 读取键名
-            if (content[pos] != '"') { pos++; continue; }
-            pos++;  // 跳过 '"'
-
-            size_t key_end = content.find('"', pos);
-            if (key_end == std::string::npos) break;
-            std::string key = content.substr(pos, key_end - pos);
-            pos = key_end + 1;
-
-            // 跳过 ':'
-            pos = content.find(':', pos);
-            if (pos == std::string::npos) break;
-            pos++;
-
-            // 跳过空白
-            while (pos < content.size() && (content[pos] == ' ' || content[pos] == '\n' ||
-                   content[pos] == '\r' || content[pos] == '\t')) {
-                pos++;
-            }
-
-            if (content[pos] == '"') {
-                // 字符串值
-                pos++;  // 跳过 '"'
-                size_t val_end = pos;
-                while (val_end < content.size()) {
-                    if (content[val_end] == '"' && content[val_end - 1] != '\\') break;
-                    val_end++;
-                }
-                std::string value = json_unescape(content.substr(pos, val_end - pos));
-                pos = val_end + 1;
-
-                if (key == "url") bm.url = value;
-                else if (key == "title") bm.title = value;
-            } else {
-                // 数字值
-                size_t val_end = pos;
-                while (val_end < content.size() && content[val_end] >= '0' && content[val_end] <= '9') {
-                    val_end++;
-                }
-                std::string value = content.substr(pos, val_end - pos);
-                pos = val_end;
-
-                if (key == "time") {
-                    bm.added_time = std::stoll(value);
-                }
-            }
-        }
-
-        if (!bm.url.empty()) {
-            bookmarks_.push_back(bm);
-        }
-
-        // 跳到下一个对象
-        pos = content.find('}', pos);
-        if (pos == std::string::npos) break;
-        pos++;
-    }
-
-    return true;
-}
-
-bool BookmarkManager::save() const {
-    if (!ensure_config_dir()) {
-        return false;
-    }
-
-    std::ofstream file(get_bookmarks_path());
-    if (!file) {
-        return false;
-    }
-
-    file << "[\n";
-    for (size_t i = 0; i < bookmarks_.size(); ++i) {
-        const auto& bm = bookmarks_[i];
-        file << "  {\n";
-        file << "    \"url\": \"" << json_escape(bm.url) << "\",\n";
-        file << "    \"title\": \"" << json_escape(bm.title) << "\",\n";
-        file << "    \"time\": " << bm.added_time << "\n";
-        file << "  }";
-        if (i + 1 < bookmarks_.size()) {
-            file << ",";
-        }
-        file << "\n";
-    }
-    file << "]\n";
-
-    return true;
-}
-
-bool BookmarkManager::add(const std::string& url, const std::string& title) {
-    // 检查是否已存在
-    if (contains(url)) {
-        return false;
-    }
-
-    bookmarks_.emplace_back(url, title);
-    return save();
-}
-
-bool BookmarkManager::remove(const std::string& url) {
-    auto it = std::find_if(bookmarks_.begin(), bookmarks_.end(),
-                           [&url](const Bookmark& bm) { return bm.url == url; });
-
-    if (it == bookmarks_.end()) {
-        return false;
-    }
-
-    bookmarks_.erase(it);
-    return save();
-}
-
-bool BookmarkManager::remove_at(size_t index) {
-    if (index >= bookmarks_.size()) {
-        return false;
-    }
-
-    bookmarks_.erase(bookmarks_.begin() + index);
-    return save();
-}
-
-bool BookmarkManager::contains(const std::string& url) const {
-    return std::find_if(bookmarks_.begin(), bookmarks_.end(),
-                        [&url](const Bookmark& bm) { return bm.url == url; })
-           != bookmarks_.end();
-}
-
-} // namespace tut
diff --git a/src/bookmark.h b/src/bookmark.h
deleted file mode 100644
index d1ae912..0000000
--- a/src/bookmark.h
+++ /dev/null
@@ -1,96 +0,0 @@
-#pragma once
-
-#include 
-#include 
-#include 
-
-namespace tut {
-
-/**
- * 书签条目
- */
-struct Bookmark {
-    std::string url;
-    std::string title;
-    std::time_t added_time;
-
-    Bookmark() : added_time(0) {}
-    Bookmark(const std::string& url, const std::string& title)
-        : url(url), title(title), added_time(std::time(nullptr)) {}
-};
-
-/**
- * 书签管理器
- *
- * 书签存储在 ~/.config/tut/bookmarks.json
- */
-class BookmarkManager {
-public:
-    BookmarkManager();
-    ~BookmarkManager();
-
-    /**
-     * 加载书签(从默认路径)
-     */
-    bool load();
-
-    /**
-     * 保存书签(到默认路径)
-     */
-    bool save() const;
-
-    /**
-     * 添加书签
-     * @return true 如果添加成功,false 如果已存在
-     */
-    bool add(const std::string& url, const std::string& title);
-
-    /**
-     * 删除书签
-     * @return true 如果删除成功
-     */
-    bool remove(const std::string& url);
-
-    /**
-     * 删除书签(按索引)
-     */
-    bool remove_at(size_t index);
-
-    /**
-     * 检查URL是否已收藏
-     */
-    bool contains(const std::string& url) const;
-
-    /**
-     * 获取书签列表
-     */
-    const std::vector& get_all() const { return bookmarks_; }
-
-    /**
-     * 获取书签数量
-     */
-    size_t count() const { return bookmarks_.size(); }
-
-    /**
-     * 清空所有书签
-     */
-    void clear() { bookmarks_.clear(); }
-
-    /**
-     * 获取配置目录路径
-     */
-    static std::string get_config_dir();
-
-    /**
-     * 获取书签文件路径
-     */
-    static std::string get_bookmarks_path();
-
-private:
-    std::vector bookmarks_;
-
-    // 确保配置目录存在
-    static bool ensure_config_dir();
-};
-
-} // namespace tut
diff --git a/src/browser.cpp b/src/browser.cpp
deleted file mode 100644
index 8e044f3..0000000
--- a/src/browser.cpp
+++ /dev/null
@@ -1,1279 +0,0 @@
-#include "browser.h"
-#include "dom_tree.h"
-#include "bookmark.h"
-#include "history.h"
-#include "render/colors.h"
-#include "render/decorations.h"
-#include "render/image.h"
-#include "utils/unicode.h"
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-using namespace tut;
-
-// 浏览器加载状态
-enum class LoadingState {
-    IDLE,           // 空闲
-    LOADING_PAGE,   // 正在加载页面
-    LOADING_IMAGES  // 正在加载图片
-};
-
-// 加载动画帧
-static const char* SPINNER_FRAMES[] = {
-    "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"
-};
-static const int SPINNER_FRAME_COUNT = 10;
-
-// 缓存条目
-struct CacheEntry {
-    DocumentTree tree;
-    std::string html;
-    std::chrono::steady_clock::time_point timestamp;
-
-    bool is_expired(int max_age_seconds = 300) const {
-        auto now = std::chrono::steady_clock::now();
-        auto age = std::chrono::duration_cast(now - timestamp).count();
-        return age > max_age_seconds;
-    }
-};
-
-// 图片缓存条目
-struct ImageCacheEntry {
-    tut::ImageData image_data;
-    std::chrono::steady_clock::time_point timestamp;
-
-    bool is_expired(int max_age_seconds = 600) const {  // 图片缓存10分钟
-        auto now = std::chrono::steady_clock::now();
-        auto age = std::chrono::duration_cast(now - timestamp).count();
-        return age > max_age_seconds;
-    }
-};
-
-class Browser::Impl {
-public:
-    // 网络和解析
-    HttpClient http_client;
-    HtmlParser html_parser;
-    InputHandler input_handler;
-    tut::BookmarkManager bookmark_manager;
-    tut::HistoryManager history_manager;
-
-    // 新渲染系统
-    Terminal terminal;
-    std::unique_ptr framebuffer;
-    std::unique_ptr renderer;
-    std::unique_ptr layout_engine;
-
-    // 文档状态
-    DocumentTree current_tree;
-    LayoutResult current_layout;
-    std::string current_url;
-    std::vector history;
-    int history_pos = -1;
-
-    // 视图状态
-    int scroll_pos = 0;
-    int active_link = -1;
-    int active_field = -1;
-    std::string status_message;
-    std::string search_term;
-
-    int screen_width = 0;
-    int screen_height = 0;
-
-    // Marks support
-    std::map marks;
-
-    // 搜索相关
-    SearchContext search_ctx;
-
-    // 页面缓存
-    std::map page_cache;
-    static constexpr int CACHE_MAX_AGE = 300;  // 5分钟缓存
-    static constexpr size_t CACHE_MAX_SIZE = 20;  // 最多缓存20个页面
-
-    // 图片下载状态
-    int images_total = 0;
-    int images_loaded = 0;
-    int images_cached = 0;
-
-    // 图片缓存
-    std::map image_cache;
-    static constexpr int IMAGE_CACHE_MAX_AGE = 600;  // 10分钟缓存
-    static constexpr size_t IMAGE_CACHE_MAX_SIZE = 100;  // 最多缓存100张图片
-
-    // 异步加载状态
-    LoadingState loading_state = LoadingState::IDLE;
-    std::string pending_url;  // 正在加载的URL
-    bool pending_force_refresh = false;
-    int spinner_frame = 0;
-    std::chrono::steady_clock::time_point last_spinner_update;
-
-    bool init_screen() {
-        if (!terminal.init()) {
-            return false;
-        }
-
-        terminal.get_size(screen_width, screen_height);
-        terminal.use_alternate_screen(true);
-        terminal.hide_cursor();
-
-        // 创建渲染组件
-        framebuffer = std::make_unique(screen_width, screen_height);
-        renderer = std::make_unique(terminal);
-        layout_engine = std::make_unique(screen_width);
-
-        return true;
-    }
-
-    void cleanup_screen() {
-        terminal.show_cursor();
-        terminal.use_alternate_screen(false);
-        terminal.cleanup();
-    }
-
-    void handle_resize() {
-        terminal.get_size(screen_width, screen_height);
-        framebuffer = std::make_unique(screen_width, screen_height);
-        layout_engine->set_viewport_width(screen_width);
-
-        // 重新布局当前文档
-        if (current_tree.root) {
-            current_layout = layout_engine->layout(current_tree);
-        }
-
-        renderer->force_redraw();
-    }
-
-    bool load_page(const std::string& url, bool force_refresh = false) {
-        // 检查缓存
-        auto cache_it = page_cache.find(url);
-        bool use_cache = !force_refresh && cache_it != page_cache.end() &&
-                        !cache_it->second.is_expired(CACHE_MAX_AGE);
-
-        if (use_cache) {
-            status_message = "⚡ Loading from cache...";
-            draw_screen();
-
-            // 使用缓存的文档树
-            // 注意:需要重新解析因为DocumentTree包含unique_ptr
-            current_tree = html_parser.parse_tree(cache_it->second.html, url);
-            status_message = "⚡ " + (current_tree.title.empty() ? url : current_tree.title);
-        } else {
-            status_message = "⏳ Connecting to " + extract_host(url) + "...";
-            draw_screen();
-
-            auto response = http_client.fetch(url);
-
-            if (!response.is_success()) {
-                status_message = "❌ " + (response.error_message.empty() ?
-                    "HTTP " + std::to_string(response.status_code) :
-                    response.error_message);
-                return false;
-            }
-
-            status_message = "📄 Parsing HTML...";
-            draw_screen();
-
-            // 解析HTML
-            current_tree = html_parser.parse_tree(response.body, url);
-
-            // 添加到缓存
-            add_to_cache(url, response.body);
-
-            status_message = current_tree.title.empty() ? url : current_tree.title;
-        }
-
-        // 下载图片(异步)
-        queue_images(current_tree);
-
-        // 布局计算
-        current_layout = layout_engine->layout(current_tree);
-
-        current_url = url;
-        scroll_pos = 0;
-        active_link = current_tree.links.empty() ? -1 : 0;
-        active_field = current_tree.form_fields.empty() ? -1 : 0;
-        search_ctx = SearchContext();  // 清除搜索状态
-        search_term.clear();
-
-        // 更新历史(仅在非刷新时)
-        if (!force_refresh) {
-            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;
-            // 持久化历史记录
-            history_manager.add(url, current_tree.title);
-        }
-
-        return true;
-    }
-
-    // 启动异步页面加载
-    void start_async_load(const std::string& url, bool force_refresh = false) {
-        // 取消任何正在进行的图片下载 (避免访问旧树的节点)
-        http_client.cancel_all_images();
-
-        // 检查缓存
-        auto cache_it = page_cache.find(url);
-        bool use_cache = !force_refresh && cache_it != page_cache.end() &&
-                        !cache_it->second.is_expired(CACHE_MAX_AGE);
-
-        if (use_cache) {
-            // 使用缓存,不需要网络请求
-            status_message = "⚡ Loading from cache...";
-            current_tree = html_parser.parse_tree(cache_it->second.html, url);
-            current_layout = layout_engine->layout(current_tree);
-            current_url = url;
-            scroll_pos = 0;
-            active_link = current_tree.links.empty() ? -1 : 0;
-            active_field = current_tree.form_fields.empty() ? -1 : 0;
-            search_ctx = SearchContext();
-            search_term.clear();
-            status_message = "⚡ " + (current_tree.title.empty() ? url : current_tree.title);
-
-            // 更新历史
-            if (!force_refresh) {
-                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;
-                // 持久化历史记录
-                history_manager.add(url, current_tree.title);
-            }
-
-            // 加载图片(异步)
-            queue_images(current_tree);
-            current_layout = layout_engine->layout(current_tree);
-            return;
-        }
-
-        // 需要网络请求,启动异步加载
-        pending_url = url;
-        pending_force_refresh = force_refresh;
-        loading_state = LoadingState::LOADING_PAGE;
-        spinner_frame = 0;
-        last_spinner_update = std::chrono::steady_clock::now();
-
-        status_message = std::string(SPINNER_FRAMES[0]) + " Connecting to " + extract_host(url) + "...";
-        http_client.start_async_fetch(url);
-    }
-
-    // 轮询异步加载状态,返回true表示还在加载中
-    bool poll_loading() {
-        if (loading_state == LoadingState::IDLE) {
-            return false;
-        }
-
-        // 更新spinner动画
-        auto now = std::chrono::steady_clock::now();
-        auto elapsed = std::chrono::duration_cast(now - last_spinner_update).count();
-        if (elapsed >= 80) {  // 每80ms更新一帧
-            spinner_frame = (spinner_frame + 1) % SPINNER_FRAME_COUNT;
-            last_spinner_update = now;
-            update_loading_status();
-        }
-
-        if (loading_state == LoadingState::LOADING_PAGE) {
-            auto state = http_client.poll_async();
-
-            switch (state) {
-                case AsyncState::COMPLETE:
-                    handle_load_complete();
-                    return false;
-
-                case AsyncState::FAILED: {
-                    auto result = http_client.get_async_result();
-                    status_message = "❌ " + (result.error_message.empty() ?
-                        "Connection failed" : result.error_message);
-                    loading_state = LoadingState::IDLE;
-                    return false;
-                }
-
-                case AsyncState::CANCELLED:
-                    status_message = "⚠ Loading cancelled";
-                    loading_state = LoadingState::IDLE;
-                    return false;
-
-                case AsyncState::LOADING:
-                    return true;
-
-                default:
-                    return false;
-            }
-        } else if (loading_state == LoadingState::LOADING_IMAGES) {
-            // 轮询图片下载
-            http_client.poll_image_downloads();
-
-            // 处理已完成的图片
-            auto completed = http_client.get_completed_images();
-            bool need_relayout = false;
-
-            for (auto& task : completed) {
-                images_loaded++;
-
-                if (!task.is_success() || task.data.empty()) {
-                    continue;  // 跳过失败的图片
-                }
-
-                // 解码图片
-                tut::ImageData img_data = tut::ImageRenderer::load_from_memory(task.data);
-                if (img_data.is_valid()) {
-                    // 设置到对应的DomNode
-                    DomNode* img_node = static_cast(task.user_data);
-
-                    // 验证节点仍然有效 (仍在当前树的images列表中)
-                    bool node_valid = false;
-                    if (img_node) {
-                        for (const auto* node : current_tree.images) {
-                            if (node == img_node) {
-                                node_valid = true;
-                                break;
-                            }
-                        }
-                    }
-
-                    if (node_valid) {
-                        img_node->image_data = img_data;
-                        need_relayout = true;
-
-                        // 添加到缓存
-                        if (image_cache.size() >= IMAGE_CACHE_MAX_SIZE) {
-                            // 移除最老的缓存条目
-                            auto oldest = image_cache.begin();
-                            for (auto it = image_cache.begin(); it != image_cache.end(); ++it) {
-                                if (it->second.timestamp < oldest->second.timestamp) {
-                                    oldest = it;
-                                }
-                            }
-                            image_cache.erase(oldest);
-                        }
-
-                        ImageCacheEntry entry;
-                        entry.image_data = std::move(img_data);
-                        entry.timestamp = std::chrono::steady_clock::now();
-                        image_cache[task.url] = std::move(entry);
-                    }
-                }
-            }
-
-            // 如果有图片完成,重新布局
-            if (need_relayout) {
-                current_layout = layout_engine->layout(current_tree);
-            }
-
-            // 检查是否所有图片都已完成
-            if (http_client.get_pending_image_count() == 0 &&
-                http_client.get_loading_image_count() == 0) {
-                if (images_total > 0) {
-                    status_message = "✓ Loaded " + std::to_string(images_total) + " images";
-                    if (images_cached > 0) {
-                        status_message += " (" + std::to_string(images_cached) + " from cache)";
-                    }
-                }
-                loading_state = LoadingState::IDLE;
-                return false;
-            }
-
-            return true;
-        }
-
-        return loading_state != LoadingState::IDLE;
-    }
-
-    // 更新加载状态消息
-    void update_loading_status() {
-        std::string spinner = SPINNER_FRAMES[spinner_frame];
-        if (loading_state == LoadingState::LOADING_PAGE) {
-            status_message = spinner + " Loading " + extract_host(pending_url) + "...";
-        } else if (loading_state == LoadingState::LOADING_IMAGES) {
-            status_message = spinner + " Loading images " + std::to_string(images_loaded) +
-                           "/" + std::to_string(images_total);
-            if (images_cached > 0) {
-                status_message += " (cached: " + std::to_string(images_cached) + ")";
-            }
-        }
-    }
-
-    // 处理页面加载完成
-    void handle_load_complete() {
-        auto response = http_client.get_async_result();
-
-        if (!response.is_success()) {
-            status_message = "❌ HTTP " + std::to_string(response.status_code);
-            loading_state = LoadingState::IDLE;
-            return;
-        }
-
-        // 解析HTML
-        current_tree = html_parser.parse_tree(response.body, pending_url);
-
-        // 添加到缓存
-        add_to_cache(pending_url, response.body);
-
-        // 布局计算
-        current_layout = layout_engine->layout(current_tree);
-
-        current_url = pending_url;
-        scroll_pos = 0;
-        active_link = current_tree.links.empty() ? -1 : 0;
-        active_field = current_tree.form_fields.empty() ? -1 : 0;
-        search_ctx = SearchContext();
-        search_term.clear();
-
-        // 更新历史(仅在非刷新时)
-        if (!pending_force_refresh) {
-            if (history_pos >= 0 && history_pos < static_cast(history.size()) - 1) {
-                history.erase(history.begin() + history_pos + 1, history.end());
-            }
-            history.push_back(pending_url);
-            history_pos = history.size() - 1;
-            // 持久化历史记录
-            history_manager.add(pending_url, current_tree.title);
-        }
-
-        status_message = current_tree.title.empty() ? pending_url : current_tree.title;
-
-        // 加载图片(异步)
-        queue_images(current_tree);
-        current_layout = layout_engine->layout(current_tree);
-
-        // 不设置为IDLE,等待图片加载完成
-        // loading_state will be set by poll_loading when images finish
-    }
-
-    // 取消加载
-    void cancel_loading() {
-        if (loading_state != LoadingState::IDLE) {
-            if (loading_state == LoadingState::LOADING_PAGE) {
-                http_client.cancel_async();
-            } else if (loading_state == LoadingState::LOADING_IMAGES) {
-                http_client.cancel_all_images();
-            }
-            loading_state = LoadingState::IDLE;
-            status_message = "⚠ Cancelled";
-        }
-    }
-
-    void add_to_cache(const std::string& url, const std::string& html) {
-        // 限制缓存大小
-        if (page_cache.size() >= CACHE_MAX_SIZE) {
-            // 移除最老的缓存条目
-            auto oldest = page_cache.begin();
-            for (auto it = page_cache.begin(); it != page_cache.end(); ++it) {
-                if (it->second.timestamp < oldest->second.timestamp) {
-                    oldest = it;
-                }
-            }
-            page_cache.erase(oldest);
-        }
-
-        CacheEntry entry;
-        entry.html = html;
-        entry.timestamp = std::chrono::steady_clock::now();
-        page_cache[url] = std::move(entry);
-    }
-
-    // 下载并解码页面中的图片
-    // 将图片加入异步下载队列
-    void queue_images(DocumentTree& tree) {
-        if (tree.images.empty()) {
-            loading_state = LoadingState::IDLE;
-            return;
-        }
-
-        images_cached = 0;
-        images_total = 0;
-        images_loaded = 0;
-
-        for (DomNode* img_node : tree.images) {
-            if (img_node->img_src.empty()) {
-                continue;
-            }
-
-            images_total++;
-
-            // 检查缓存
-            auto cache_it = image_cache.find(img_node->img_src);
-            if (cache_it != image_cache.end() && !cache_it->second.is_expired(IMAGE_CACHE_MAX_AGE)) {
-                // 使用缓存的图片
-                img_node->image_data = cache_it->second.image_data;
-                images_cached++;
-                images_loaded++;
-                continue;
-            }
-
-            // 添加到下载队列
-            http_client.add_image_download(img_node->img_src, img_node);
-        }
-
-        // 如果所有图片都在缓存中,直接完成
-        if (http_client.get_pending_image_count() == 0 &&
-            http_client.get_loading_image_count() == 0) {
-            if (images_cached > 0) {
-                status_message = "✓ Loaded " + std::to_string(images_total) + " images (" +
-                               std::to_string(images_cached) + " from cache)";
-            }
-            loading_state = LoadingState::IDLE;
-        } else {
-            loading_state = LoadingState::LOADING_IMAGES;
-            update_loading_status();
-        }
-    }
-
-    // 从URL中提取主机名
-    std::string extract_host(const std::string& url) {
-        // 简单提取:找到://之后的部分,到第一个/为止
-        size_t proto_end = url.find("://");
-        if (proto_end == std::string::npos) {
-            return url;
-        }
-        size_t host_start = proto_end + 3;
-        size_t host_end = url.find('/', host_start);
-        if (host_end == std::string::npos) {
-            return url.substr(host_start);
-        }
-        return url.substr(host_start, host_end - host_start);
-    }
-
-    void draw_screen() {
-        // 清空缓冲区
-        framebuffer->clear_with_color(colors::BG_PRIMARY);
-
-        int content_height = screen_height - 1;  // 留出状态栏
-
-        // 渲染文档内容
-        RenderContext render_ctx;
-        render_ctx.active_link = active_link;
-        render_ctx.active_field = active_field;
-        render_ctx.search = search_ctx.enabled ? &search_ctx : nullptr;
-
-        DocumentRenderer doc_renderer(*framebuffer);
-        doc_renderer.render(current_layout, scroll_pos, render_ctx);
-
-        // 渲染状态栏
-        draw_status_bar(content_height);
-
-        // 渲染到终端
-        renderer->render(*framebuffer);
-    }
-
-    void draw_status_bar(int y) {
-        // 状态栏背景
-        for (int x = 0; x < screen_width; ++x) {
-            framebuffer->set_cell(x, y, Cell{" ", colors::STATUSBAR_FG, colors::STATUSBAR_BG, ATTR_NONE});
-        }
-
-        // 左侧: 模式
-        std::string mode_str;
-        InputMode mode = input_handler.get_mode();
-        switch (mode) {
-            case InputMode::NORMAL: mode_str = "NORMAL"; break;
-            case InputMode::COMMAND:
-            case InputMode::SEARCH: mode_str = input_handler.get_buffer(); break;
-            case InputMode::FORM_EDIT: mode_str = "-- INSERT -- " + input_handler.get_buffer(); break;
-            case InputMode::SELECT_OPTION: mode_str = "-- SELECT --"; break;
-            default: mode_str = ""; break;
-        }
-        framebuffer->set_text(1, y, mode_str, colors::STATUSBAR_FG, colors::STATUSBAR_BG);
-
-        // 中间: 状态消息或链接URL
-        std::string display_msg;
-        if (mode == InputMode::NORMAL) {
-            if (active_link >= 0 && active_link < static_cast(current_tree.links.size())) {
-                display_msg = current_tree.links[active_link].url;
-            }
-            if (display_msg.empty()) {
-                display_msg = status_message;
-            }
-
-            if (!display_msg.empty()) {
-                // 截断过长的消息
-                size_t max_len = screen_width - mode_str.length() - 20;
-                if (display_msg.length() > max_len) {
-                    display_msg = display_msg.substr(0, max_len - 3) + "...";
-                }
-                int msg_x = static_cast(mode_str.length()) + 3;
-                framebuffer->set_text(msg_x, y, display_msg, colors::STATUSBAR_FG, colors::STATUSBAR_BG);
-            }
-        }
-
-        // 右侧: 位置信息
-        int total_lines = current_layout.total_lines;
-        int visible_lines = screen_height - 1;
-        int percentage = (total_lines > 0 && scroll_pos + visible_lines < total_lines) ?
-                         (scroll_pos * 100) / total_lines : 100;
-        if (total_lines == 0) percentage = 0;
-
-        std::string pos_str = std::to_string(scroll_pos + 1) + "/" +
-                             std::to_string(total_lines) + " " +
-                             std::to_string(percentage) + "%";
-        int pos_x = screen_width - static_cast(pos_str.length()) - 1;
-        framebuffer->set_text(pos_x, y, pos_str, colors::STATUSBAR_FG, colors::STATUSBAR_BG);
-    }
-
-    void handle_action(const InputResult& result) {
-        int visible_lines = screen_height - 1;
-        int max_scroll = std::max(0, current_layout.total_lines - 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) {
-                    scroll_pos = std::min(result.number - 1, max_scroll);
-                }
-                break;
-
-            case Action::NEXT_LINK:
-                if (!current_tree.links.empty()) {
-                    active_link = (active_link + 1) % current_tree.links.size();
-                    scroll_to_link(active_link);
-                }
-                break;
-
-            case Action::PREV_LINK:
-                if (!current_tree.links.empty()) {
-                    active_link = (active_link - 1 + current_tree.links.size()) % current_tree.links.size();
-                    scroll_to_link(active_link);
-                }
-                break;
-
-            case Action::FOLLOW_LINK:
-                // If on a form field, activate it instead of following link
-                if (active_field >= 0 && active_field < static_cast(current_tree.form_fields.size())) {
-                    auto* field = current_tree.form_fields[active_field];
-                    if (field) {
-                        if (field->input_type == "text" || field->input_type == "password") {
-                            // Enter edit mode
-                            input_handler.set_mode(InputMode::FORM_EDIT);
-                            input_handler.set_buffer(field->value);
-                            status_message = "-- INSERT --";
-                        } else if (field->input_type == "checkbox") {
-                            // Toggle checkbox
-                            field->checked = !field->checked;
-                            status_message = field->checked ? "☑ Checked" : "☐ Unchecked";
-                        } else if (field->input_type == "select") {
-                            // Enter dropdown selection mode
-                            input_handler.set_mode(InputMode::SELECT_OPTION);
-                            status_message = "-- SELECT -- (j/k to navigate, Enter to select, Esc to cancel)";
-                        } else if (field->input_type == "submit" || field->element_type == ElementType::BUTTON) {
-                            // TODO: Submit form
-                            status_message = "Form submit (not yet implemented)";
-                        }
-                    }
-                } else if (active_link >= 0 && active_link < static_cast(current_tree.links.size())) {
-                    start_async_load(current_tree.links[active_link].url);
-                }
-                break;
-
-            case Action::GO_BACK:
-                if (history_pos > 0) {
-                    history_pos--;
-                    start_async_load(history[history_pos]);
-                }
-                break;
-
-            case Action::GO_FORWARD:
-                if (history_pos < static_cast(history.size()) - 1) {
-                    history_pos++;
-                    start_async_load(history[history_pos]);
-                }
-                break;
-
-            case Action::OPEN_URL:
-                if (!result.text.empty()) {
-                    start_async_load(result.text);
-                }
-                break;
-
-            case Action::REFRESH:
-                if (!current_url.empty()) {
-                    start_async_load(current_url, true);  // 强制刷新,跳过缓存
-                }
-                break;
-
-            case Action::SEARCH_FORWARD: {
-                int count = perform_search(result.text);
-                if (count > 0) {
-                    status_message = "Match 1/" + std::to_string(count);
-                } else if (!result.text.empty()) {
-                    status_message = "Pattern not found: " + result.text;
-                }
-                break;
-            }
-
-            case Action::SEARCH_NEXT:
-                search_next();
-                break;
-
-            case Action::SEARCH_PREV:
-                search_prev();
-                break;
-
-            case Action::HELP:
-                show_help();
-                break;
-
-            case Action::ADD_BOOKMARK:
-                add_bookmark();
-                break;
-
-            case Action::REMOVE_BOOKMARK:
-                remove_bookmark();
-                break;
-
-            case Action::SHOW_BOOKMARKS:
-                show_bookmarks();
-                break;
-
-            case Action::SHOW_HISTORY:
-                show_history();
-                break;
-
-            case Action::NEXT_FIELD:
-                if (!current_tree.form_fields.empty()) {
-                    // Save current text if in edit mode
-                    if (input_handler.get_mode() == InputMode::FORM_EDIT &&
-                        active_field >= 0 && active_field < static_cast(current_tree.form_fields.size())) {
-                        auto* field = current_tree.form_fields[active_field];
-                        if (field && (field->input_type == "text" || field->input_type == "password")) {
-                            field->value = result.text;
-                        }
-                    }
-
-                    // Move to next field
-                    if (active_field < 0) {
-                        active_field = 0;  // First field
-                    } else {
-                        active_field = (active_field + 1) % current_tree.form_fields.size();
-                    }
-
-                    // Auto-scroll to field
-                    // TODO: Implement scroll to field
-                    status_message = "Field " + std::to_string(active_field + 1) + "/" +
-                                   std::to_string(current_tree.form_fields.size());
-                }
-                break;
-
-            case Action::PREV_FIELD:
-                if (!current_tree.form_fields.empty()) {
-                    // Save current text if in edit mode
-                    if (input_handler.get_mode() == InputMode::FORM_EDIT &&
-                        active_field >= 0 && active_field < static_cast(current_tree.form_fields.size())) {
-                        auto* field = current_tree.form_fields[active_field];
-                        if (field && (field->input_type == "text" || field->input_type == "password")) {
-                            field->value = result.text;
-                        }
-                    }
-
-                    // Move to previous field
-                    if (active_field < 0) {
-                        active_field = current_tree.form_fields.size() - 1;  // Last field
-                    } else {
-                        active_field = (active_field - 1 + current_tree.form_fields.size()) %
-                                     current_tree.form_fields.size();
-                    }
-
-                    status_message = "Field " + std::to_string(active_field + 1) + "/" +
-                                   std::to_string(current_tree.form_fields.size());
-                }
-                break;
-
-            case Action::EDIT_TEXT:
-                // Update field value in real-time
-                if (active_field >= 0 && active_field < static_cast(current_tree.form_fields.size())) {
-                    auto* field = current_tree.form_fields[active_field];
-                    if (field && (field->input_type == "text" || field->input_type == "password")) {
-                        field->value = result.text;
-                        status_message = "Editing: " + result.text;
-                    }
-                }
-                break;
-
-            case Action::NEXT_OPTION:
-                if (active_field >= 0 && active_field < static_cast(current_tree.form_fields.size())) {
-                    auto* field = current_tree.form_fields[active_field];
-                    if (field && field->input_type == "select" && !field->options.empty()) {
-                        field->selected_option = (field->selected_option + 1) % field->options.size();
-                        status_message = "Option: " + field->options[field->selected_option].second;
-                    }
-                }
-                break;
-
-            case Action::PREV_OPTION:
-                if (active_field >= 0 && active_field < static_cast(current_tree.form_fields.size())) {
-                    auto* field = current_tree.form_fields[active_field];
-                    if (field && field->input_type == "select" && !field->options.empty()) {
-                        field->selected_option = (field->selected_option - 1 + field->options.size()) %
-                                                field->options.size();
-                        status_message = "Option: " + field->options[field->selected_option].second;
-                    }
-                }
-                break;
-
-            case Action::SELECT_CURRENT_OPTION:
-                if (active_field >= 0 && active_field < static_cast(current_tree.form_fields.size())) {
-                    auto* field = current_tree.form_fields[active_field];
-                    if (field && field->input_type == "select" && !field->options.empty()) {
-                        field->value = field->options[field->selected_option].first;
-                        status_message = "Selected: " + field->options[field->selected_option].second;
-                    }
-                }
-                break;
-
-            case Action::QUIT:
-                break; // 在main loop处理
-
-            default:
-                break;
-        }
-    }
-
-    // 执行搜索,返回匹配数量
-    int perform_search(const std::string& term) {
-        search_ctx.matches.clear();
-        search_ctx.current_match_idx = -1;
-        search_ctx.enabled = false;
-
-        if (term.empty()) {
-            return 0;
-        }
-
-        search_term = term;
-        search_ctx.enabled = true;
-
-        // 遍历所有布局块和行,查找匹配
-        int doc_line = 0;
-        for (const auto& block : current_layout.blocks) {
-            // 上边距
-            doc_line += block.margin_top;
-
-            // 内容行
-            for (const auto& line : block.lines) {
-                // 构建整行文本用于搜索
-                std::string line_text;
-
-                for (const auto& span : line.spans) {
-                    line_text += span.text;
-                }
-
-                // 搜索匹配(大小写不敏感)
-                std::string lower_line = line_text;
-                std::string lower_term = term;
-                std::transform(lower_line.begin(), lower_line.end(), lower_line.begin(), ::tolower);
-                std::transform(lower_term.begin(), lower_term.end(), lower_term.begin(), ::tolower);
-
-                size_t pos = 0;
-                while ((pos = lower_line.find(lower_term, pos)) != std::string::npos) {
-                    SearchMatch match;
-                    match.line = doc_line;
-                    match.start_col = line.indent + static_cast(pos);
-                    match.length = static_cast(term.length());
-                    search_ctx.matches.push_back(match);
-                    pos += 1;  // 继续搜索下一个匹配
-                }
-
-                doc_line++;
-            }
-
-            // 下边距
-            doc_line += block.margin_bottom;
-        }
-
-        // 如果有匹配,跳转到第一个
-        if (!search_ctx.matches.empty()) {
-            search_ctx.current_match_idx = 0;
-            scroll_to_match(0);
-        }
-
-        return static_cast(search_ctx.matches.size());
-    }
-
-    // 跳转到指定匹配
-    void scroll_to_match(int idx) {
-        if (idx < 0 || idx >= static_cast(search_ctx.matches.size())) {
-            return;
-        }
-
-        search_ctx.current_match_idx = idx;
-        int match_line = search_ctx.matches[idx].line;
-        int visible_lines = screen_height - 1;
-
-        // 确保匹配行在可见区域
-        if (match_line < scroll_pos) {
-            scroll_pos = match_line;
-        } else if (match_line >= scroll_pos + visible_lines) {
-            scroll_pos = match_line - visible_lines / 2;
-        }
-
-        int max_scroll = std::max(0, current_layout.total_lines - visible_lines);
-        scroll_pos = std::max(0, std::min(scroll_pos, max_scroll));
-    }
-
-    // 搜索下一个
-    void search_next() {
-        if (search_ctx.matches.empty()) {
-            if (!search_term.empty()) {
-                status_message = "Pattern not found: " + search_term;
-            }
-            return;
-        }
-
-        search_ctx.current_match_idx = (search_ctx.current_match_idx + 1) % search_ctx.matches.size();
-        scroll_to_match(search_ctx.current_match_idx);
-        status_message = "Match " + std::to_string(search_ctx.current_match_idx + 1) +
-                        "/" + std::to_string(search_ctx.matches.size());
-    }
-
-    // 搜索上一个
-    void search_prev() {
-        if (search_ctx.matches.empty()) {
-            if (!search_term.empty()) {
-                status_message = "Pattern not found: " + search_term;
-            }
-            return;
-        }
-
-        search_ctx.current_match_idx = (search_ctx.current_match_idx - 1 + search_ctx.matches.size()) % search_ctx.matches.size();
-        scroll_to_match(search_ctx.current_match_idx);
-        status_message = "Match " + std::to_string(search_ctx.current_match_idx + 1) +
-                        "/" + std::to_string(search_ctx.matches.size());
-    }
-
-    // 滚动到链接位置
-    void scroll_to_link(int link_idx) {
-        if (link_idx < 0 || link_idx >= static_cast(current_layout.link_positions.size())) {
-            return;
-        }
-
-        const auto& pos = current_layout.link_positions[link_idx];
-        if (pos.start_line < 0) {
-            return;  // 链接位置无效
-        }
-
-        int visible_lines = screen_height - 1;
-        int link_line = pos.start_line;
-
-        // 确保链接行在可见区域
-        if (link_line < scroll_pos) {
-            // 链接在视口上方,滚动使其出现在顶部附近
-            scroll_pos = std::max(0, link_line - 2);
-        } else if (link_line >= scroll_pos + visible_lines) {
-            // 链接在视口下方,滚动使其出现在中间
-            scroll_pos = link_line - visible_lines / 2;
-        }
-
-        int max_scroll = std::max(0, current_layout.total_lines - visible_lines);
-        scroll_pos = std::max(0, std::min(scroll_pos, max_scroll));
-    }
-
-    void show_help() {
-        std::string help_html = R"(
-
-
-TUT 2.0 Help
-
-

TUT 2.0 - Terminal Browser

- -

Navigation

-
    -
  • j/k - Scroll down/up
  • -
  • Ctrl+d/Ctrl+u - Page down/up
  • -
  • gg - Go to top
  • -
  • G - Go to bottom
  • -
- -

Links

-
    -
  • Tab - Next link
  • -
  • Shift+Tab - Previous link
  • -
  • Enter - Follow link
  • -
- -

History

-
    -
  • h - Go back
  • -
  • l - Go forward
  • -
- -

Search

-
    -
  • / - Search forward
  • -
  • n - Next match
  • -
  • N - Previous match
  • -
- -

Bookmarks

-
    -
  • B - Add bookmark
  • -
  • D - Remove bookmark
  • -
  • :bookmarks - Show bookmarks
  • -
  • :history - Show history
  • -
- -

Commands

-
    -
  • :o URL - Open URL
  • -
  • :bookmarks - Show bookmarks
  • -
  • :history - Show history
  • -
  • :q - Quit
  • -
  • ? - Show this help
  • -
- -

Forms

-
    -
  • i - Focus first form field
  • -
  • Tab/Shift+Tab - Navigate between fields
  • -
  • Enter - Activate field (text input/checkbox/dropdown)
  • -
- -

Text Input

-
    -
  • Type to edit text
  • -
  • Backspace to delete
  • -
  • Enter or Esc to finish editing
  • -
- -

Dropdown Selection

-
    -
  • Enter on SELECT to open options
  • -
  • j/k or arrows to navigate options
  • -
  • Enter to select, Esc to cancel
  • -
- -

Other

-
    -
  • r - Refresh page (bypass cache)
  • -
  • Esc - Cancel loading
  • -
- -
-

TUT 2.0 - A modern terminal browser with True Color support

- - -)"; - current_tree = html_parser.parse_tree(help_html, "help://"); - current_layout = layout_engine->layout(current_tree); - scroll_pos = 0; - active_link = current_tree.links.empty() ? -1 : 0; - status_message = "Help - Press any key to continue"; - } - - void show_bookmarks() { - std::ostringstream html; - html << R"( - - -Bookmarks - -

Bookmarks

-)"; - - const auto& bookmarks = bookmark_manager.get_all(); - - if (bookmarks.empty()) { - html << "

No bookmarks yet.

\n"; - html << "

Press B on any page to add a bookmark.

\n"; - } else { - html << "\n"; - html << "
\n"; - html << "

" << bookmarks.size() << " bookmark(s). Press D on any page to remove its bookmark.

\n"; - } - - html << R"( - - -)"; - - current_tree = html_parser.parse_tree(html.str(), "bookmarks://"); - current_layout = layout_engine->layout(current_tree); - scroll_pos = 0; - active_link = current_tree.links.empty() ? -1 : 0; - status_message = "Bookmarks"; - } - - void show_history() { - std::ostringstream html; - html << R"( - - -History - -

History

-)"; - - const auto& entries = history_manager.get_all(); - - if (entries.empty()) { - html << "

No browsing history yet.

\n"; - } else { - html << "
    \n"; - // 显示最近的 100 条 - size_t count = std::min(entries.size(), static_cast(100)); - for (size_t i = 0; i < count; ++i) { - const auto& entry = entries[i]; - // 格式化时间 - char time_buf[64]; - std::strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M", - std::localtime(&entry.visit_time)); - html << "
  • " - << (entry.title.empty() ? entry.url : entry.title) - << " (" << time_buf << ")
  • \n"; - } - html << "
\n"; - if (entries.size() > 100) { - html << "

Showing 100 of " << entries.size() << " entries

\n"; - } - html << "
\n"; - html << "

" << entries.size() << " entries in history.

\n"; - } - - html << R"( - - -)"; - - current_tree = html_parser.parse_tree(html.str(), "history://"); - current_layout = layout_engine->layout(current_tree); - scroll_pos = 0; - active_link = current_tree.links.empty() ? -1 : 0; - status_message = "History"; - } - - void add_bookmark() { - if (current_url.empty() || current_url.find("://") == std::string::npos) { - status_message = "Cannot bookmark this page"; - return; - } - - // 不要书签特殊页面 - if (current_url.find("help://") == 0 || current_url.find("bookmarks://") == 0 || - current_url.find("history://") == 0) { - status_message = "Cannot bookmark special pages"; - return; - } - - std::string title = current_tree.title.empty() ? current_url : current_tree.title; - - if (bookmark_manager.add(current_url, title)) { - status_message = "Bookmarked: " + title; - } else { - status_message = "Already bookmarked"; - } - } - - void remove_bookmark() { - if (current_url.empty()) { - status_message = "No page to unbookmark"; - return; - } - - if (bookmark_manager.remove(current_url)) { - status_message = "Bookmark removed"; - } else { - status_message = "Not bookmarked"; - } - } -}; - -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) { - if (!pImpl->init_screen()) { - throw std::runtime_error("Failed to initialize terminal"); - } - - if (!initial_url.empty()) { - pImpl->start_async_load(initial_url); - } else { - pImpl->show_help(); - } - - bool running = true; - while (running) { - // 轮询异步加载状态 - pImpl->poll_loading(); - - // 渲染屏幕 - pImpl->draw_screen(); - - // 获取输入(非阻塞,50ms超时) - int ch = pImpl->terminal.get_key(50); - if (ch == -1) continue; - - // 处理窗口大小变化 - if (ch == KEY_RESIZE) { - pImpl->handle_resize(); - continue; - } - - // 如果正在加载,Esc可以取消 - if (pImpl->loading_state != LoadingState::IDLE && ch == 27) { // 27 = Esc - pImpl->cancel_loading(); - continue; - } - - // 加载时忽略大部分输入,只允许取消和退出 - if (pImpl->loading_state != LoadingState::IDLE) { - if (ch == 'q' || ch == 'Q') { - running = false; - } - 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 deleted file mode 100644 index d0ed837..0000000 --- a/src/browser.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "http_client.h" -#include "html_parser.h" -#include "input_handler.h" -#include "render/terminal.h" -#include "render/renderer.h" -#include "render/layout.h" -#include -#include -#include - -/** - * Browser - TUT 终端浏览器 - * - * 使用 Terminal + FrameBuffer + Renderer + LayoutEngine 架构 - * 支持 True Color, Unicode, 差分渲染 - */ -class Browser { -public: - Browser(); - ~Browser(); - - void run(const std::string& initial_url = ""); - bool load_url(const std::string& url); - std::string get_current_url() const; - -private: - class Impl; - std::unique_ptr pImpl; -}; diff --git a/src/core/types.hpp b/src/core/types.hpp new file mode 100644 index 0000000..271dddb --- /dev/null +++ b/src/core/types.hpp @@ -0,0 +1,23 @@ +/** + * @file types.hpp + * @brief Common types used across TUT modules + * @author m1ngsama + * @date 2024-12-31 + */ + +#pragma once + +#include + +namespace tut { + +/** + * @brief 链接信息结构体 + */ +struct LinkInfo { + std::string url; ///< 链接 URL + std::string text; ///< 链接文本 + int line{0}; ///< 所在行号 +}; + +} // namespace tut diff --git a/src/dom_tree.cpp b/src/dom_tree.cpp deleted file mode 100644 index a9d24ad..0000000 --- a/src/dom_tree.cpp +++ /dev/null @@ -1,705 +0,0 @@ -#include "dom_tree.h" -#include -#include -#include -#include -#include - -// ============================================================================ -// DomNode 辅助方法实现 -// ============================================================================ - -bool DomNode::is_block_element() const { - if (node_type != NodeType::ELEMENT) return false; - - switch (element_type) { - case ElementType::HEADING1: - case ElementType::HEADING2: - case ElementType::HEADING3: - case ElementType::HEADING4: - case ElementType::HEADING5: - case ElementType::HEADING6: - case ElementType::PARAGRAPH: - case ElementType::LIST_ITEM: - case ElementType::ORDERED_LIST_ITEM: - case ElementType::BLOCKQUOTE: - case ElementType::CODE_BLOCK: - case ElementType::HORIZONTAL_RULE: - case ElementType::TABLE: - case ElementType::SECTION_START: - case ElementType::SECTION_END: - case ElementType::NAV_START: - case ElementType::NAV_END: - case ElementType::HEADER_START: - case ElementType::HEADER_END: - case ElementType::ASIDE_START: - case ElementType::ASIDE_END: - case ElementType::FORM: - return true; - default: - // 通过标签名判断 - return tag_name == "div" || tag_name == "section" || - tag_name == "article" || tag_name == "main" || - tag_name == "header" || tag_name == "footer" || - tag_name == "nav" || tag_name == "aside" || - tag_name == "ul" || tag_name == "ol" || - tag_name == "li" || tag_name == "dl" || - tag_name == "dt" || tag_name == "dd" || - tag_name == "pre" || tag_name == "hr" || - tag_name == "table" || tag_name == "tr" || - tag_name == "th" || tag_name == "td" || - tag_name == "tbody" || tag_name == "thead" || - tag_name == "tfoot" || tag_name == "caption" || - tag_name == "form" || tag_name == "fieldset" || - tag_name == "figure" || tag_name == "figcaption" || - tag_name == "details" || tag_name == "summary" || - tag_name == "center" || tag_name == "address"; - } -} - -bool DomNode::is_inline_element() const { - if (node_type != NodeType::ELEMENT) return false; - - switch (element_type) { - case ElementType::LINK: - case ElementType::TEXT: - case ElementType::INPUT: - case ElementType::TEXTAREA: - case ElementType::SELECT: - case ElementType::BUTTON: - case ElementType::OPTION: - return true; - default: - // 通过标签名判断常见的内联元素 - return tag_name == "a" || tag_name == "span" || - tag_name == "strong" || tag_name == "b" || - tag_name == "em" || tag_name == "i" || - tag_name == "code" || tag_name == "kbd" || - tag_name == "mark" || tag_name == "small" || - tag_name == "sub" || tag_name == "sup" || - tag_name == "u" || tag_name == "abbr" || - tag_name == "cite" || tag_name == "q" || - tag_name == "label"; - } -} - -bool DomNode::should_render() const { - // 过滤不应该渲染的元素 - if (tag_name == "script" || tag_name == "style" || - tag_name == "noscript" || tag_name == "template" || - (tag_name == "input" && input_type == "hidden")) { - return false; - } - return true; -} - -std::string DomNode::get_all_text() const { - std::string result; - - // 过滤不应该提取文本的元素 - if (!should_render()) { - return ""; - } - - if (node_type == NodeType::TEXT) { - result = text_content; - } else { - // Special handling for form elements to return their value/placeholder for representation - if (element_type == ElementType::INPUT) { - // For inputs, we might want to return nothing here as they are rendered specially, - // or return their value. For simple text extraction, maybe empty is better. - } else if (element_type == ElementType::TEXTAREA) { - for (const auto& child : children) { - result += child->get_all_text(); - } - } else { - for (const auto& child : children) { - result += child->get_all_text(); - } - } - } - - return result; -} - -// ============================================================================ -// DomTreeBuilder 实现 -// ============================================================================ - -// Add a member to track current form ID -namespace { - int g_current_form_id = -1; - int g_next_form_id = 0; -} - -DomTreeBuilder::DomTreeBuilder() = default; -DomTreeBuilder::~DomTreeBuilder() = default; - -DocumentTree DomTreeBuilder::build(const std::string& html, const std::string& base_url) { - // Reset form tracking - g_current_form_id = -1; - g_next_form_id = 0; - - // 1. 使用gumbo解析HTML - GumboOutput* output = gumbo_parse(html.c_str()); - - // 2. 转换为DomNode树 - DocumentTree tree; - tree.url = base_url; - tree.root = convert_node(output->root, tree.links, tree.form_fields, tree.images, base_url); - - // 3. 提取标题 - if (tree.root) { - tree.title = extract_title(tree.root.get()); - } - - // 4. 清理gumbo资源 - gumbo_destroy_output(&kGumboDefaultOptions, output); - - return tree; -} - -std::unique_ptr DomTreeBuilder::convert_node( - GumboNode* gumbo_node, - std::vector& links, - std::vector& form_fields, - std::vector& images, - const std::string& base_url -) { - if (!gumbo_node) return nullptr; - - auto node = std::make_unique(); - - if (gumbo_node->type == GUMBO_NODE_ELEMENT) { - node->node_type = NodeType::ELEMENT; - GumboElement& element = gumbo_node->v.element; - - // 设置标签名 - node->tag_name = gumbo_normalized_tagname(element.tag); - node->element_type = map_gumbo_tag_to_element_type(element.tag); - - // 跳过 script、style 等不需要渲染的标签(包括其所有子节点) - if (element.tag == GUMBO_TAG_SCRIPT || element.tag == GUMBO_TAG_STYLE || - element.tag == GUMBO_TAG_NOSCRIPT || element.tag == GUMBO_TAG_TEMPLATE) { - return nullptr; - } - - // Assign current form ID to children - node->form_id = g_current_form_id; - - // Special handling for FORM tag - if (element.tag == GUMBO_TAG_FORM) { - node->form_id = g_next_form_id++; - g_current_form_id = node->form_id; - - GumboAttribute* action_attr = gumbo_get_attribute(&element.attributes, "action"); - if (action_attr) node->action = resolve_url(action_attr->value, base_url); - else node->action = base_url; // Default to current URL - - GumboAttribute* method_attr = gumbo_get_attribute(&element.attributes, "method"); - if (method_attr) node->method = method_attr->value; - else node->method = "GET"; - - // Transform to uppercase - std::transform(node->method.begin(), node->method.end(), node->method.begin(), ::toupper); - } - - // Handle INPUT - if (element.tag == GUMBO_TAG_INPUT) { - GumboAttribute* type_attr = gumbo_get_attribute(&element.attributes, "type"); - node->input_type = type_attr ? type_attr->value : "text"; - - GumboAttribute* name_attr = gumbo_get_attribute(&element.attributes, "name"); - if (name_attr) node->name = name_attr->value; - - GumboAttribute* value_attr = gumbo_get_attribute(&element.attributes, "value"); - if (value_attr) node->value = value_attr->value; - - GumboAttribute* placeholder_attr = gumbo_get_attribute(&element.attributes, "placeholder"); - if (placeholder_attr) node->placeholder = placeholder_attr->value; - - if (gumbo_get_attribute(&element.attributes, "checked")) { - node->checked = true; - } - - // Register form field - if (node->input_type != "hidden") { - node->field_index = form_fields.size(); - form_fields.push_back(node.get()); - } - } - - // Handle TEXTAREA - if (element.tag == GUMBO_TAG_TEXTAREA) { - node->input_type = "textarea"; - GumboAttribute* name_attr = gumbo_get_attribute(&element.attributes, "name"); - if (name_attr) node->name = name_attr->value; - - GumboAttribute* placeholder_attr = gumbo_get_attribute(&element.attributes, "placeholder"); - if (placeholder_attr) node->placeholder = placeholder_attr->value; - - // Register form field - node->field_index = form_fields.size(); - form_fields.push_back(node.get()); - } - - // Handle SELECT - if (element.tag == GUMBO_TAG_SELECT) { - node->input_type = "select"; - GumboAttribute* name_attr = gumbo_get_attribute(&element.attributes, "name"); - if (name_attr) node->name = name_attr->value; - - // Register form field - node->field_index = form_fields.size(); - form_fields.push_back(node.get()); - } - - // Handle OPTION - if (element.tag == GUMBO_TAG_OPTION) { - node->input_type = "option"; - GumboAttribute* value_attr = gumbo_get_attribute(&element.attributes, "value"); - if (value_attr) node->value = value_attr->value; - if (gumbo_get_attribute(&element.attributes, "selected")) { - node->checked = true; - } - } - - // Handle BUTTON - if (element.tag == GUMBO_TAG_BUTTON) { - GumboAttribute* type_attr = gumbo_get_attribute(&element.attributes, "type"); - node->input_type = type_attr ? type_attr->value : "submit"; - - GumboAttribute* name_attr = gumbo_get_attribute(&element.attributes, "name"); - if (name_attr) node->name = name_attr->value; - - GumboAttribute* value_attr = gumbo_get_attribute(&element.attributes, "value"); - if (value_attr) node->value = value_attr->value; - - // Register form field - node->field_index = form_fields.size(); - form_fields.push_back(node.get()); - } - - // Handle IMG - if (element.tag == GUMBO_TAG_IMG) { - GumboAttribute* src_attr = gumbo_get_attribute(&element.attributes, "src"); - if (src_attr && src_attr->value) { - node->img_src = resolve_url(src_attr->value, base_url); - } - - GumboAttribute* alt_attr = gumbo_get_attribute(&element.attributes, "alt"); - if (alt_attr) node->alt_text = alt_attr->value; - - GumboAttribute* width_attr = gumbo_get_attribute(&element.attributes, "width"); - if (width_attr && width_attr->value) { - try { node->img_width = std::stoi(width_attr->value); } catch (...) {} - } - - GumboAttribute* height_attr = gumbo_get_attribute(&element.attributes, "height"); - if (height_attr && height_attr->value) { - try { node->img_height = std::stoi(height_attr->value); } catch (...) {} - } - - // 添加到图片列表(用于后续下载) - if (!node->img_src.empty()) { - images.push_back(node.get()); - } - } - - - // 处理标签 - if (element.tag == GUMBO_TAG_A) { - GumboAttribute* href_attr = gumbo_get_attribute(&element.attributes, "href"); - if (href_attr && href_attr->value) { - std::string href = href_attr->value; - // 过滤锚点链接和javascript链接 - if (!href.empty() && href[0] != '#' && - href.find("javascript:") != 0 && - href.find("mailto:") != 0) { - - node->href = resolve_url(href, base_url); - - // 注册到全局链接列表 - Link link; - link.text = extract_text_from_gumbo(gumbo_node); - link.url = node->href; - link.position = links.size(); - - links.push_back(link); - node->link_index = links.size() - 1; - node->element_type = ElementType::LINK; - } - } - } - - // 处理表格单元格属性 - if (element.tag == GUMBO_TAG_TH) { - node->is_table_header = true; - } - - if (element.tag == GUMBO_TAG_TD || element.tag == GUMBO_TAG_TH) { - GumboAttribute* colspan_attr = gumbo_get_attribute(&element.attributes, "colspan"); - if (colspan_attr && colspan_attr->value) { - node->colspan = std::stoi(colspan_attr->value); - } - - GumboAttribute* rowspan_attr = gumbo_get_attribute(&element.attributes, "rowspan"); - if (rowspan_attr && rowspan_attr->value) { - node->rowspan = std::stoi(rowspan_attr->value); - } - } - - // 递归处理子节点 - GumboVector* children = &element.children; - for (unsigned int i = 0; i < children->length; ++i) { - auto child = convert_node( - static_cast(children->data[i]), - links, - form_fields, - images, - base_url - ); - if (child) { - child->parent = node.get(); - node->children.push_back(std::move(child)); - - // For TEXTAREA, content is value - if (element.tag == GUMBO_TAG_TEXTAREA && child->node_type == NodeType::TEXT) { - node->value += child->text_content; - } - } - } - - // For SELECT, collect all OPTION children - if (element.tag == GUMBO_TAG_SELECT) { - for (const auto& child : node->children) { - if (child->element_type == ElementType::OPTION) { - std::string option_value = child->value.empty() ? child->get_all_text() : child->value; - std::string option_text = child->get_all_text(); - node->options.push_back({option_value, option_text}); - - // Set selected option if marked - if (child->checked) { - node->selected_option = node->options.size() - 1; - node->value = option_value; - } - } - } - - // Set default value to first option if no option is selected - if (!node->options.empty() && node->value.empty()) { - node->value = node->options[0].first; - node->selected_option = 0; - } - } - - // Reset form ID if we are exiting a form - if (element.tag == GUMBO_TAG_FORM) { - g_current_form_id = -1; // Assuming no nested forms - } - } - else if (gumbo_node->type == GUMBO_NODE_TEXT) { - node->node_type = NodeType::TEXT; - std::string text = gumbo_node->v.text.text; - - // 解码HTML实体 - node->text_content = decode_html_entities(text); - node->form_id = g_current_form_id; - } - else if (gumbo_node->type == GUMBO_NODE_DOCUMENT) { - node->node_type = NodeType::DOCUMENT; - node->tag_name = "document"; - - // 处理文档节点的子节点 - GumboDocument& doc = gumbo_node->v.document; - for (unsigned int i = 0; i < doc.children.length; ++i) { - auto child = convert_node( - static_cast(doc.children.data[i]), - links, - form_fields, - images, - base_url - ); - if (child) { - child->parent = node.get(); - node->children.push_back(std::move(child)); - } - } - } - - return node; -} - -std::string DomTreeBuilder::extract_title(DomNode* root) { - if (!root) return ""; - - // 递归查找标签 - std::function<std::string(DomNode*)> find_title = [&](DomNode* node) -> std::string { - if (!node) return ""; - - if (node->tag_name == "title") { - return node->get_all_text(); - } - - for (auto& child : node->children) { - std::string title = find_title(child.get()); - if (!title.empty()) return title; - } - - return ""; - }; - - std::string title = find_title(root); - - // 如果没有<title>,尝试找第一个<h1> - if (title.empty()) { - std::function<std::string(DomNode*)> find_h1 = [&](DomNode* node) -> std::string { - if (!node) return ""; - - if (node->tag_name == "h1") { - return node->get_all_text(); - } - - for (auto& child : node->children) { - std::string h1 = find_h1(child.get()); - if (!h1.empty()) return h1; - } - - return ""; - }; - - title = find_h1(root); - } - - // 清理标题中的多余空白 - title = std::regex_replace(title, std::regex(R"(\s+)"), " "); - - // 去除首尾空白 - size_t start = title.find_first_not_of(" \t\n\r"); - if (start == std::string::npos) return ""; - - size_t end = title.find_last_not_of(" \t\n\r"); - return title.substr(start, end - start + 1); -} - -std::string DomTreeBuilder::extract_text_from_gumbo(GumboNode* node) { - if (!node) return ""; - - std::string text; - - if (node->type == GUMBO_NODE_TEXT) { - text = node->v.text.text; - } else if (node->type == GUMBO_NODE_ELEMENT) { - GumboVector* children = &node->v.element.children; - for (unsigned int i = 0; i < children->length; ++i) { - text += extract_text_from_gumbo(static_cast<GumboNode*>(children->data[i])); - } - } - - return text; -} - -ElementType DomTreeBuilder::map_gumbo_tag_to_element_type(int gumbo_tag) { - switch (gumbo_tag) { - case GUMBO_TAG_H1: return ElementType::HEADING1; - case GUMBO_TAG_H2: return ElementType::HEADING2; - case GUMBO_TAG_H3: return ElementType::HEADING3; - case GUMBO_TAG_H4: return ElementType::HEADING4; - case GUMBO_TAG_H5: return ElementType::HEADING5; - case GUMBO_TAG_H6: return ElementType::HEADING6; - case GUMBO_TAG_P: return ElementType::PARAGRAPH; - case GUMBO_TAG_A: return ElementType::LINK; - case GUMBO_TAG_LI: return ElementType::LIST_ITEM; - case GUMBO_TAG_BLOCKQUOTE: return ElementType::BLOCKQUOTE; - case GUMBO_TAG_PRE: return ElementType::CODE_BLOCK; - case GUMBO_TAG_HR: return ElementType::HORIZONTAL_RULE; - case GUMBO_TAG_BR: return ElementType::LINE_BREAK; - case GUMBO_TAG_TABLE: return ElementType::TABLE; - case GUMBO_TAG_IMG: return ElementType::IMAGE; - case GUMBO_TAG_FORM: return ElementType::FORM; - case GUMBO_TAG_INPUT: return ElementType::INPUT; - case GUMBO_TAG_TEXTAREA: return ElementType::TEXTAREA; - case GUMBO_TAG_SELECT: return ElementType::SELECT; - case GUMBO_TAG_OPTION: return ElementType::OPTION; - case GUMBO_TAG_BUTTON: return ElementType::BUTTON; - default: return ElementType::TEXT; - } -} - -std::string DomTreeBuilder::resolve_url(const std::string& url, const std::string& base_url) { - if (url.empty()) return ""; - - // 绝对URL(http://或https://) - if (url.find("http://") == 0 || url.find("https://") == 0) { - return url; - } - - // 协议相对URL(//example.com) - if (url.size() >= 2 && url[0] == '/' && url[1] == '/') { - // 从base_url提取协议 - size_t proto_end = base_url.find("://"); - if (proto_end != std::string::npos) { - return base_url.substr(0, proto_end) + ":" + url; - } - return "https:" + url; - } - - if (base_url.empty()) return url; - - // 绝对路径(/path) - if (url[0] == '/') { - // 提取base_url的scheme和host - size_t proto_end = base_url.find("://"); - if (proto_end == std::string::npos) return url; - - size_t host_start = proto_end + 3; - size_t path_start = base_url.find('/', host_start); - - std::string base_origin; - if (path_start != std::string::npos) { - base_origin = base_url.substr(0, path_start); - } else { - base_origin = base_url; - } - - return base_origin + url; - } - - // 相对路径(relative/path) - // 找到base_url的路径部分 - size_t proto_end = base_url.find("://"); - if (proto_end == std::string::npos) return url; - - size_t host_start = proto_end + 3; - size_t path_start = base_url.find('/', host_start); - - std::string base_path; - if (path_start != std::string::npos) { - // 找到最后一个/ - size_t last_slash = base_url.rfind('/'); - if (last_slash != std::string::npos) { - base_path = base_url.substr(0, last_slash + 1); - } else { - base_path = base_url + "/"; - } - } else { - base_path = base_url + "/"; - } - - return base_path + url; -} - -const std::map<std::string, std::string>& DomTreeBuilder::get_entity_map() { - static std::map<std::string, std::string> entity_map = { - {" ", " "}, {"<", "<"}, {">", ">"}, - {"&", "&"}, {""", "\""}, {"'", "'"}, - {"©", "©"}, {"®", "®"}, {"™", "™"}, - {"€", "€"}, {"£", "£"}, {"¥", "¥"}, - {"¢", "¢"}, {"§", "§"}, {"¶", "¶"}, - {"†", "†"}, {"‡", "‡"}, {"•", "•"}, - {"…", "…"}, {"′", "′"}, {"″", "″"}, - {"‹", "‹"}, {"›", "›"}, {"«", "«"}, - {"»", "»"}, {"‘", "'"}, {"’", "'"}, - {"“", "\u201C"}, {"”", "\u201D"}, {"—", "—"}, - {"–", "–"}, {"¡", "¡"}, {"¿", "¿"}, - {"×", "×"}, {"÷", "÷"}, {"±", "±"}, - {"°", "°"}, {"µ", "µ"}, {"·", "·"}, - {"¼", "¼"}, {"½", "½"}, {"¾", "¾"}, - {"¹", "¹"}, {"²", "²"}, {"³", "³"}, - {"α", "α"}, {"β", "β"}, {"γ", "γ"}, - {"δ", "δ"}, {"ε", "ε"}, {"θ", "θ"}, - {"λ", "λ"}, {"μ", "μ"}, {"π", "π"}, - {"σ", "σ"}, {"τ", "τ"}, {"φ", "φ"}, - {"ω", "ω"} - }; - return entity_map; -} - -std::string DomTreeBuilder::decode_html_entities(const std::string& text) { - std::string result = text; - const auto& entity_map = get_entity_map(); - - // 替换命名实体 - for (const auto& [entity, replacement] : entity_map) { - size_t pos = 0; - while ((pos = result.find(entity, pos)) != std::string::npos) { - result.replace(pos, entity.length(), replacement); - pos += replacement.length(); - } - } - - // 替换数字实体 { 或 « - std::regex numeric_entity(R"(&#(\d+);)"); - std::regex hex_entity(R"(&#x([0-9A-Fa-f]+);)"); - - // 处理十进制数字实体 - std::string temp; - size_t last_pos = 0; - std::smatch match; - std::string::const_iterator search_start(result.cbegin()); - - while (std::regex_search(search_start, result.cend(), match, numeric_entity)) { - size_t match_pos = match.position() + std::distance(result.cbegin(), search_start); - temp += result.substr(last_pos, match_pos - last_pos); - - int code = std::stoi(match[1].str()); - if (code > 0 && code < 0x110000) { - // 简单的UTF-8编码(仅支持基本多文种平面) - if (code < 0x80) { - temp += static_cast<char>(code); - } else if (code < 0x800) { - temp += static_cast<char>(0xC0 | (code >> 6)); - temp += static_cast<char>(0x80 | (code & 0x3F)); - } else if (code < 0x10000) { - temp += static_cast<char>(0xE0 | (code >> 12)); - temp += static_cast<char>(0x80 | ((code >> 6) & 0x3F)); - temp += static_cast<char>(0x80 | (code & 0x3F)); - } else { - temp += static_cast<char>(0xF0 | (code >> 18)); - temp += static_cast<char>(0x80 | ((code >> 12) & 0x3F)); - temp += static_cast<char>(0x80 | ((code >> 6) & 0x3F)); - temp += static_cast<char>(0x80 | (code & 0x3F)); - } - } - - last_pos = match_pos + match[0].length(); - search_start = result.cbegin() + last_pos; - } - temp += result.substr(last_pos); - result = temp; - - // 处理十六进制数字实体 - temp.clear(); - last_pos = 0; - search_start = result.cbegin(); - - while (std::regex_search(search_start, result.cend(), match, hex_entity)) { - size_t match_pos = match.position() + std::distance(result.cbegin(), search_start); - temp += result.substr(last_pos, match_pos - last_pos); - - int code = std::stoi(match[1].str(), nullptr, 16); - if (code > 0 && code < 0x110000) { - if (code < 0x80) { - temp += static_cast<char>(code); - } else if (code < 0x800) { - temp += static_cast<char>(0xC0 | (code >> 6)); - temp += static_cast<char>(0x80 | (code & 0x3F)); - } else if (code < 0x10000) { - temp += static_cast<char>(0xE0 | (code >> 12)); - temp += static_cast<char>(0x80 | ((code >> 6) & 0x3F)); - temp += static_cast<char>(0x80 | (code & 0x3F)); - } else { - temp += static_cast<char>(0xF0 | (code >> 18)); - temp += static_cast<char>(0x80 | ((code >> 12) & 0x3F)); - temp += static_cast<char>(0x80 | ((code >> 6) & 0x3F)); - temp += static_cast<char>(0x80 | (code & 0x3F)); - } - } - - last_pos = match_pos + match[0].length(); - search_start = result.cbegin() + last_pos; - } - temp += result.substr(last_pos); - - return temp; -} diff --git a/src/dom_tree.h b/src/dom_tree.h deleted file mode 100644 index a8d7de0..0000000 --- a/src/dom_tree.h +++ /dev/null @@ -1,118 +0,0 @@ -#pragma once - -#include "html_parser.h" -#include "render/image.h" -#include <string> -#include <vector> -#include <memory> -#include <map> - -// Forward declaration for gumbo -struct GumboInternalNode; -struct GumboInternalOutput; -typedef struct GumboInternalNode GumboNode; -typedef struct GumboInternalOutput GumboOutput; - -// DOM节点类型 -enum class NodeType { - ELEMENT, // 元素节点(h1, p, div等) - TEXT, // 文本节点 - DOCUMENT // 文档根节点 -}; - -// DOM节点结构 -struct DomNode { - NodeType node_type; - ElementType element_type; // 复用现有的ElementType - std::string tag_name; // "div", "p", "h1"等 - std::string text_content; // TEXT节点的文本内容 - - // 树结构 - std::vector<std::unique_ptr<DomNode>> children; - DomNode* parent = nullptr; // 非拥有指针 - - // 链接属性 - std::string href; - int link_index = -1; // -1表示非链接 - int field_index = -1; // -1表示非表单字段 - - // 图片属性 - std::string img_src; // 图片URL - std::string alt_text; // 图片alt文本 - int img_width = -1; // 图片宽度 (-1表示未指定) - int img_height = -1; // 图片高度 (-1表示未指定) - tut::ImageData image_data; // 解码后的图片数据 - - // 表格属性 - bool is_table_header = false; - int colspan = 1; - int rowspan = 1; - - // 表单属性 - std::string action; - std::string method; - std::string name; - std::string value; - std::string input_type; // text, password, checkbox, radio, submit, hidden - std::string placeholder; - bool checked = false; - int form_id = -1; - - // SELECT元素的选项 - std::vector<std::pair<std::string, std::string>> options; // (value, text) pairs - int selected_option = 0; // 当前选中的选项索引 - - // 辅助方法 - bool is_block_element() const; - bool is_inline_element() const; - bool should_render() const; // 是否应该渲染(过滤script、style等) - std::string get_all_text() const; // 递归获取所有文本内容 -}; - -// 文档树结构 -struct DocumentTree { - std::unique_ptr<DomNode> root; - std::vector<Link> links; // 全局链接列表 - std::vector<DomNode*> form_fields; // 全局表单字段列表 (非拥有指针) - std::vector<DomNode*> images; // 全局图片列表 (非拥有指针) - std::string title; - std::string url; -}; - -// DOM树构建器 -class DomTreeBuilder { -public: - DomTreeBuilder(); - ~DomTreeBuilder(); - - // 从HTML构建DOM树 - DocumentTree build(const std::string& html, const std::string& base_url); - -private: - // 将GumboNode转换为DomNode - std::unique_ptr<DomNode> convert_node( - GumboNode* gumbo_node, - std::vector<Link>& links, - std::vector<DomNode*>& form_fields, - std::vector<DomNode*>& images, - const std::string& base_url - ); - - // 提取文档标题 - std::string extract_title(DomNode* root); - - // 从GumboNode提取所有文本 - std::string extract_text_from_gumbo(GumboNode* node); - - // 将GumboTag映射为ElementType - ElementType map_gumbo_tag_to_element_type(int gumbo_tag); - - // URL解析 - std::string resolve_url(const std::string& url, const std::string& base_url); - - // HTML实体解码 - std::string decode_html_entities(const std::string& text); - - // HTML实体映射表 - static const std::map<std::string, std::string>& get_entity_map(); -}; \ No newline at end of file diff --git a/src/history.cpp b/src/history.cpp deleted file mode 100644 index 74719fb..0000000 --- a/src/history.cpp +++ /dev/null @@ -1,217 +0,0 @@ -#include "history.h" -#include <fstream> -#include <sstream> -#include <algorithm> -#include <sys/stat.h> -#include <cstdlib> - -namespace tut { - -HistoryManager::HistoryManager() { - load(); -} - -HistoryManager::~HistoryManager() { - save(); -} - -std::string HistoryManager::get_history_path() { - const char* home = std::getenv("HOME"); - if (!home) { - home = "/tmp"; - } - return std::string(home) + "/.config/tut/history.json"; -} - -bool HistoryManager::ensure_config_dir() { - const char* home = std::getenv("HOME"); - if (!home) home = "/tmp"; - - std::string config_dir = std::string(home) + "/.config"; - std::string tut_dir = config_dir + "/tut"; - - struct stat st; - if (stat(tut_dir.c_str(), &st) == 0) { - return S_ISDIR(st.st_mode); - } - - mkdir(config_dir.c_str(), 0755); - return mkdir(tut_dir.c_str(), 0755) == 0 || errno == EEXIST; -} - -// JSON escape/unescape -static std::string json_escape(const std::string& s) { - std::string result; - result.reserve(s.size() + 10); - for (char c : s) { - switch (c) { - case '"': result += "\\\""; break; - case '\\': result += "\\\\"; break; - case '\n': result += "\\n"; break; - case '\r': result += "\\r"; break; - case '\t': result += "\\t"; break; - default: result += c; break; - } - } - return result; -} - -static std::string json_unescape(const std::string& s) { - std::string result; - result.reserve(s.size()); - for (size_t i = 0; i < s.size(); ++i) { - if (s[i] == '\\' && i + 1 < s.size()) { - switch (s[i + 1]) { - case '"': result += '"'; ++i; break; - case '\\': result += '\\'; ++i; break; - case 'n': result += '\n'; ++i; break; - case 'r': result += '\r'; ++i; break; - case 't': result += '\t'; ++i; break; - default: result += s[i]; break; - } - } else { - result += s[i]; - } - } - return result; -} - -bool HistoryManager::load() { - entries_.clear(); - - std::ifstream file(get_history_path()); - if (!file) { - return false; - } - - std::string content((std::istreambuf_iterator<char>(file)), - std::istreambuf_iterator<char>()); - file.close(); - - size_t pos = content.find('['); - if (pos == std::string::npos) return false; - pos++; - - while (pos < content.size()) { - pos = content.find('{', pos); - if (pos == std::string::npos) break; - pos++; - - HistoryEntry entry; - - while (pos < content.size() && content[pos] != '}') { - while (pos < content.size() && (content[pos] == ' ' || content[pos] == '\n' || - content[pos] == '\r' || content[pos] == '\t' || content[pos] == ',')) { - pos++; - } - - if (content[pos] == '}') break; - - if (content[pos] != '"') { pos++; continue; } - pos++; - - size_t key_end = content.find('"', pos); - if (key_end == std::string::npos) break; - std::string key = content.substr(pos, key_end - pos); - pos = key_end + 1; - - pos = content.find(':', pos); - if (pos == std::string::npos) break; - pos++; - - while (pos < content.size() && (content[pos] == ' ' || content[pos] == '\n' || - content[pos] == '\r' || content[pos] == '\t')) { - pos++; - } - - if (content[pos] == '"') { - pos++; - size_t val_end = pos; - while (val_end < content.size()) { - if (content[val_end] == '"' && content[val_end - 1] != '\\') break; - val_end++; - } - std::string value = json_unescape(content.substr(pos, val_end - pos)); - pos = val_end + 1; - - if (key == "url") entry.url = value; - else if (key == "title") entry.title = value; - } else { - size_t val_end = pos; - while (val_end < content.size() && content[val_end] >= '0' && content[val_end] <= '9') { - val_end++; - } - std::string value = content.substr(pos, val_end - pos); - pos = val_end; - - if (key == "time") { - entry.visit_time = std::stoll(value); - } - } - } - - if (!entry.url.empty()) { - entries_.push_back(entry); - } - - pos = content.find('}', pos); - if (pos == std::string::npos) break; - pos++; - } - - return true; -} - -bool HistoryManager::save() const { - if (!ensure_config_dir()) { - return false; - } - - std::ofstream file(get_history_path()); - if (!file) { - return false; - } - - file << "[\n"; - for (size_t i = 0; i < entries_.size(); ++i) { - const auto& entry = entries_[i]; - file << " {\n"; - file << " \"url\": \"" << json_escape(entry.url) << "\",\n"; - file << " \"title\": \"" << json_escape(entry.title) << "\",\n"; - file << " \"time\": " << entry.visit_time << "\n"; - file << " }"; - if (i + 1 < entries_.size()) { - file << ","; - } - file << "\n"; - } - file << "]\n"; - - return true; -} - -void HistoryManager::add(const std::string& url, const std::string& title) { - // Remove existing entry with same URL - auto it = std::find_if(entries_.begin(), entries_.end(), - [&url](const HistoryEntry& e) { return e.url == url; }); - if (it != entries_.end()) { - entries_.erase(it); - } - - // Add new entry at the front - entries_.insert(entries_.begin(), HistoryEntry(url, title)); - - // Enforce max entries limit - if (entries_.size() > MAX_ENTRIES) { - entries_.resize(MAX_ENTRIES); - } - - save(); -} - -void HistoryManager::clear() { - entries_.clear(); - save(); -} - -} // namespace tut diff --git a/src/history.h b/src/history.h deleted file mode 100644 index 4686623..0000000 --- a/src/history.h +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once - -#include <string> -#include <vector> -#include <ctime> - -namespace tut { - -/** - * 历史记录条目 - */ -struct HistoryEntry { - std::string url; - std::string title; - std::time_t visit_time; - - HistoryEntry() : visit_time(0) {} - HistoryEntry(const std::string& url, const std::string& title) - : url(url), title(title), visit_time(std::time(nullptr)) {} -}; - -/** - * 历史记录管理器 - * - * 历史记录存储在 ~/.config/tut/history.json - * 最多保存 MAX_ENTRIES 条记录 - */ -class HistoryManager { -public: - static constexpr size_t MAX_ENTRIES = 1000; - - HistoryManager(); - ~HistoryManager(); - - /** - * 加载历史记录 - */ - bool load(); - - /** - * 保存历史记录 - */ - bool save() const; - - /** - * 添加历史记录 - * 如果 URL 已存在,会更新访问时间并移到最前面 - */ - void add(const std::string& url, const std::string& title); - - /** - * 清空历史记录 - */ - void clear(); - - /** - * 获取历史记录列表(最新的在前面) - */ - const std::vector<HistoryEntry>& get_all() const { return entries_; } - - /** - * 获取历史记录数量 - */ - size_t count() const { return entries_.size(); } - - /** - * 获取历史记录文件路径 - */ - static std::string get_history_path(); - -private: - std::vector<HistoryEntry> entries_; - - // 确保配置目录存在 - static bool ensure_config_dir(); -}; - -} // namespace tut diff --git a/src/html_parser.cpp b/src/html_parser.cpp deleted file mode 100644 index c7528f9..0000000 --- a/src/html_parser.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include "html_parser.h" -#include "dom_tree.h" -#include <stdexcept> - -// ============================================================================ -// HtmlParser::Impl 实现 -// ============================================================================ - -class HtmlParser::Impl { -public: - bool keep_code_blocks = true; - bool keep_lists = true; - - DomTreeBuilder tree_builder; - - DocumentTree parse_tree(const std::string& html, const std::string& base_url) { - return tree_builder.build(html, base_url); - } - - // 将DocumentTree转换为ParsedDocument(向后兼容) - ParsedDocument convert_to_parsed_document(const DocumentTree& tree) { - ParsedDocument doc; - doc.title = tree.title; - doc.url = tree.url; - doc.links = tree.links; - - // 递归遍历DOM树,收集ContentElement - if (tree.root) { - collect_content_elements(tree.root.get(), doc.elements); - } - - return doc; - } - -private: - void collect_content_elements(DomNode* node, std::vector<ContentElement>& elements) { - if (!node || !node->should_render()) return; - - if (node->node_type == NodeType::ELEMENT) { - ContentElement elem; - elem.type = node->element_type; - elem.url = node->href; - elem.level = 0; // TODO: 根据需要计算层级 - elem.list_number = 0; - elem.nesting_level = 0; - - // 提取文本内容 - elem.text = node->get_all_text(); - - // 收集内联链接 - collect_inline_links(node, elem.inline_links); - - // 只添加有内容的元素 - if (!elem.text.empty() || node->element_type == ElementType::HORIZONTAL_RULE) { - elements.push_back(elem); - } - } - - // 递归处理子节点 - for (const auto& child : node->children) { - collect_content_elements(child.get(), elements); - } - } - - void collect_inline_links(DomNode* node, std::vector<InlineLink>& links) { - if (!node) return; - - if (node->element_type == ElementType::LINK && node->link_index >= 0) { - InlineLink link; - link.text = node->get_all_text(); - link.url = node->href; - link.link_index = node->link_index; - link.start_pos = 0; // 简化:不计算精确位置 - link.end_pos = link.text.length(); - links.push_back(link); - } - - for (const auto& child : node->children) { - collect_inline_links(child.get(), links); - } - } -}; - -// ============================================================================ -// HtmlParser 公共接口实现 -// ============================================================================ - -HtmlParser::HtmlParser() : pImpl(std::make_unique<Impl>()) {} - -HtmlParser::~HtmlParser() = default; - -DocumentTree HtmlParser::parse_tree(const std::string& html, const std::string& base_url) { - return pImpl->parse_tree(html, base_url); -} - -ParsedDocument HtmlParser::parse(const std::string& html, const std::string& base_url) { - // 使用新的DOM树解析,然后转换为旧格式 - DocumentTree tree = pImpl->parse_tree(html, base_url); - return pImpl->convert_to_parsed_document(tree); -} - -void HtmlParser::set_keep_code_blocks(bool keep) { - pImpl->keep_code_blocks = keep; -} - -void HtmlParser::set_keep_lists(bool keep) { - pImpl->keep_lists = keep; -} diff --git a/src/html_parser.h b/src/html_parser.h deleted file mode 100644 index 7ede7ac..0000000 --- a/src/html_parser.h +++ /dev/null @@ -1,136 +0,0 @@ -#pragma once - -#include <string> -#include <vector> -#include <memory> - -// Forward declaration -struct DocumentTree; - -enum class ElementType { - TEXT, - HEADING1, - HEADING2, - HEADING3, - HEADING4, - HEADING5, - HEADING6, - PARAGRAPH, - LINK, - LIST_ITEM, - ORDERED_LIST_ITEM, - BLOCKQUOTE, - CODE_BLOCK, - HORIZONTAL_RULE, - LINE_BREAK, - TABLE, - IMAGE, - FORM, - INPUT, - TEXTAREA, - SELECT, - OPTION, - BUTTON, - SECTION_START, - SECTION_END, - NAV_START, - NAV_END, - HEADER_START, - HEADER_END, - ASIDE_START, - ASIDE_END -}; - -struct Link { - std::string text; - std::string url; - int position; -}; - -struct InlineLink { - std::string text; - std::string url; - size_t start_pos; // Position in the text where link starts - size_t end_pos; // Position in the text where link ends - int link_index; // Index in the document's links array - int field_index = -1; // Index in the document's form_fields array -}; - -struct TableCell { - std::string text; - std::vector<InlineLink> inline_links; - bool is_header; - int colspan; - int rowspan; -}; - -struct TableRow { - std::vector<TableCell> cells; -}; - -struct Table { - std::vector<TableRow> rows; - bool has_header; -}; - -struct Image { - std::string src; - std::string alt; - int width; // -1 if not specified - int height; // -1 if not specified -}; - -struct FormField { - enum Type { TEXT, PASSWORD, CHECKBOX, RADIO, SUBMIT, BUTTON } type; - std::string name; - std::string value; - std::string placeholder; - bool checked; -}; - -struct Form { - std::string action; - std::string method; - std::vector<FormField> fields; -}; - -struct ContentElement { - ElementType type; - std::string text; - std::string url; - int level; - int list_number; // For ordered lists - int nesting_level; // For nested lists - std::vector<InlineLink> inline_links; // Links within this element's text - - // Extended content types - Table table_data; - Image image_data; - Form form_data; -}; - -struct ParsedDocument { - std::string title; - std::string url; - std::vector<ContentElement> elements; - std::vector<Link> links; -}; - -class HtmlParser { -public: - HtmlParser(); - ~HtmlParser(); - - // 新接口:使用DOM树解析 - DocumentTree parse_tree(const std::string& html, const std::string& base_url = ""); - - // 旧接口:保持向后兼容(已废弃,内部使用parse_tree) - ParsedDocument parse(const std::string& html, const std::string& base_url = ""); - - void set_keep_code_blocks(bool keep); - void set_keep_lists(bool keep); - -private: - class Impl; - std::unique_ptr<Impl> pImpl; -}; diff --git a/src/http_client.cpp b/src/http_client.cpp deleted file mode 100644 index a56c8e2..0000000 --- a/src/http_client.cpp +++ /dev/null @@ -1,631 +0,0 @@ -#include "http_client.h" -#include <curl/curl.h> -#include <stdexcept> - -// 回调函数用于接收文本数据 -static size_t write_callback(void* contents, size_t size, size_t nmemb, std::string* userp) { - size_t total_size = size * nmemb; - userp->append(static_cast<char*>(contents), total_size); - return total_size; -} - -// 回调函数用于接收二进制数据 -static size_t binary_write_callback(void* contents, size_t size, size_t nmemb, std::vector<uint8_t>* userp) { - size_t total_size = size * nmemb; - uint8_t* data = static_cast<uint8_t*>(contents); - userp->insert(userp->end(), data, data + total_size); - return total_size; -} - -// 内部图片下载任务 -struct InternalImageTask { - CURL* easy_handle = nullptr; - std::string url; - void* user_data = nullptr; - std::vector<uint8_t> data; - bool is_loading = false; -}; - -class HttpClient::Impl { -public: - CURL* curl; - long timeout; - std::string user_agent; - bool follow_redirects; - std::string cookie_file; - - // 异步请求相关 (页面) - CURLM* multi_handle = nullptr; - CURL* async_easy = nullptr; - AsyncState async_state = AsyncState::IDLE; - std::string async_response_body; - HttpResponse async_result; - - // 异步图片下载相关 - CURLM* image_multi = nullptr; // 专用于图片的multi handle - std::vector<InternalImageTask> pending_images; // 待下载队列 - std::vector<InternalImageTask> loading_images; // 正在下载 - std::vector<ImageDownloadTask> completed_images; // 已完成 - int max_concurrent_images = 3; - - Impl() : timeout(30), - user_agent("TUT-Browser/2.0 (Terminal User Interface Browser)"), - follow_redirects(true) { - curl = curl_easy_init(); - if (!curl) { - throw std::runtime_error("Failed to initialize CURL"); - } - // Enable cookie engine by default (in-memory) - curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); - // Enable automatic decompression of supported encodings (gzip, deflate, etc.) - curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); - - // 初始化multi handle用于异步请求 - multi_handle = curl_multi_init(); - if (!multi_handle) { - throw std::runtime_error("Failed to initialize CURL multi handle"); - } - - // 初始化image multi handle用于图片下载 - image_multi = curl_multi_init(); - if (!image_multi) { - throw std::runtime_error("Failed to initialize image CURL multi handle"); - } - } - - ~Impl() { - // 清理异步请求 - cleanup_async(); - cleanup_all_images(); - - if (image_multi) { - curl_multi_cleanup(image_multi); - } - if (multi_handle) { - curl_multi_cleanup(multi_handle); - } - if (curl) { - curl_easy_cleanup(curl); - } - } - - void cleanup_async() { - if (async_easy) { - curl_multi_remove_handle(multi_handle, async_easy); - curl_easy_cleanup(async_easy); - async_easy = nullptr; - } - async_state = AsyncState::IDLE; - async_response_body.clear(); - } - - void setup_easy_handle(CURL* handle, const std::string& url) { - curl_easy_setopt(handle, CURLOPT_URL, url.c_str()); - curl_easy_setopt(handle, CURLOPT_TIMEOUT, timeout); - curl_easy_setopt(handle, CURLOPT_CONNECTTIMEOUT, 10L); - curl_easy_setopt(handle, CURLOPT_USERAGENT, user_agent.c_str()); - - if (follow_redirects) { - curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(handle, CURLOPT_MAXREDIRS, 10L); - } - - curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 2L); - - if (!cookie_file.empty()) { - curl_easy_setopt(handle, CURLOPT_COOKIEFILE, cookie_file.c_str()); - curl_easy_setopt(handle, CURLOPT_COOKIEJAR, cookie_file.c_str()); - } else { - curl_easy_setopt(handle, CURLOPT_COOKIEFILE, ""); - } - - curl_easy_setopt(handle, CURLOPT_ACCEPT_ENCODING, ""); - } - - void cleanup_all_images() { - // 清理所有正在加载的图片 - for (auto& task : loading_images) { - if (task.easy_handle) { - curl_multi_remove_handle(image_multi, task.easy_handle); - curl_easy_cleanup(task.easy_handle); - } - } - loading_images.clear(); - - // 清理待下载的图片 - for (auto& task : pending_images) { - if (task.easy_handle) { - curl_easy_cleanup(task.easy_handle); - } - } - pending_images.clear(); - completed_images.clear(); - } - - // 启动一个图片下载任务 - void start_image_download(InternalImageTask& task) { - task.easy_handle = curl_easy_init(); - if (!task.easy_handle) { - return; // 跳过失败的任务 - } - - // 配置请求 - setup_easy_handle(task.easy_handle, task.url); - - // 设置写回调 - task.data.clear(); - curl_easy_setopt(task.easy_handle, CURLOPT_WRITEFUNCTION, binary_write_callback); - curl_easy_setopt(task.easy_handle, CURLOPT_WRITEDATA, &task.data); - - // 添加到multi handle - curl_multi_add_handle(image_multi, task.easy_handle); - task.is_loading = true; - } -}; - -HttpClient::HttpClient() : pImpl(std::make_unique<Impl>()) {} - -HttpClient::~HttpClient() = default; - -HttpResponse HttpClient::fetch(const std::string& url) { - HttpResponse response; - response.status_code = 0; - - if (!pImpl->curl) { - response.error_message = "CURL not initialized"; - return response; - } - - // 重置选项 (Note: curl_easy_reset clears cookies setting if not careful, - // but here we might want to preserve them or reset and re-apply options) - // Actually curl_easy_reset clears ALL options including cookie engine state? - // No, it resets options to default. It does NOT clear the cookie engine state (cookies held in memory). - // BUT it resets CURLOPT_COOKIEFILE/JAR settings. - - curl_easy_reset(pImpl->curl); - - // Re-apply settings - // 设置URL - curl_easy_setopt(pImpl->curl, CURLOPT_URL, url.c_str()); - - // 设置写回调 - std::string response_body; - curl_easy_setopt(pImpl->curl, CURLOPT_WRITEFUNCTION, write_callback); - curl_easy_setopt(pImpl->curl, CURLOPT_WRITEDATA, &response_body); - - // 设置超时 - curl_easy_setopt(pImpl->curl, CURLOPT_TIMEOUT, pImpl->timeout); - curl_easy_setopt(pImpl->curl, CURLOPT_CONNECTTIMEOUT, 10L); - - // 设置用户代理 - curl_easy_setopt(pImpl->curl, CURLOPT_USERAGENT, pImpl->user_agent.c_str()); - - // 设置是否跟随重定向 - if (pImpl->follow_redirects) { - curl_easy_setopt(pImpl->curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(pImpl->curl, CURLOPT_MAXREDIRS, 10L); - } - - // 支持 HTTPS - curl_easy_setopt(pImpl->curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(pImpl->curl, CURLOPT_SSL_VERIFYHOST, 2L); - - // Cookie settings - if (!pImpl->cookie_file.empty()) { - curl_easy_setopt(pImpl->curl, CURLOPT_COOKIEFILE, pImpl->cookie_file.c_str()); - curl_easy_setopt(pImpl->curl, CURLOPT_COOKIEJAR, pImpl->cookie_file.c_str()); - } else { - curl_easy_setopt(pImpl->curl, CURLOPT_COOKIEFILE, ""); - } - - // 执行请求 - CURLcode res = curl_easy_perform(pImpl->curl); - - if (res != CURLE_OK) { - response.error_message = curl_easy_strerror(res); - return response; - } - - // 获取响应码 - long http_code = 0; - curl_easy_getinfo(pImpl->curl, CURLINFO_RESPONSE_CODE, &http_code); - response.status_code = static_cast<int>(http_code); - - // 获取 Content-Type - char* content_type = nullptr; - curl_easy_getinfo(pImpl->curl, CURLINFO_CONTENT_TYPE, &content_type); - if (content_type) { - response.content_type = content_type; - } - - response.body = std::move(response_body); - - return response; -} - -BinaryResponse HttpClient::fetch_binary(const std::string& url) { - BinaryResponse response; - response.status_code = 0; - - if (!pImpl->curl) { - response.error_message = "CURL not initialized"; - return response; - } - - curl_easy_reset(pImpl->curl); - - // 设置URL - curl_easy_setopt(pImpl->curl, CURLOPT_URL, url.c_str()); - - // 设置写回调 - std::vector<uint8_t> response_data; - curl_easy_setopt(pImpl->curl, CURLOPT_WRITEFUNCTION, binary_write_callback); - curl_easy_setopt(pImpl->curl, CURLOPT_WRITEDATA, &response_data); - - // 设置超时 - curl_easy_setopt(pImpl->curl, CURLOPT_TIMEOUT, pImpl->timeout); - curl_easy_setopt(pImpl->curl, CURLOPT_CONNECTTIMEOUT, 10L); - - // 设置用户代理 - curl_easy_setopt(pImpl->curl, CURLOPT_USERAGENT, pImpl->user_agent.c_str()); - - // 设置是否跟随重定向 - if (pImpl->follow_redirects) { - curl_easy_setopt(pImpl->curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(pImpl->curl, CURLOPT_MAXREDIRS, 10L); - } - - // 支持 HTTPS - curl_easy_setopt(pImpl->curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(pImpl->curl, CURLOPT_SSL_VERIFYHOST, 2L); - - // Cookie settings - if (!pImpl->cookie_file.empty()) { - curl_easy_setopt(pImpl->curl, CURLOPT_COOKIEFILE, pImpl->cookie_file.c_str()); - curl_easy_setopt(pImpl->curl, CURLOPT_COOKIEJAR, pImpl->cookie_file.c_str()); - } else { - curl_easy_setopt(pImpl->curl, CURLOPT_COOKIEFILE, ""); - } - - // 执行请求 - CURLcode res = curl_easy_perform(pImpl->curl); - - if (res != CURLE_OK) { - response.error_message = curl_easy_strerror(res); - return response; - } - - // 获取响应码 - long http_code = 0; - curl_easy_getinfo(pImpl->curl, CURLINFO_RESPONSE_CODE, &http_code); - response.status_code = static_cast<int>(http_code); - - // 获取 Content-Type - char* content_type = nullptr; - curl_easy_getinfo(pImpl->curl, CURLINFO_CONTENT_TYPE, &content_type); - if (content_type) { - response.content_type = content_type; - } - - response.data = std::move(response_data); - - return response; -} - -HttpResponse HttpClient::post(const std::string& url, const std::string& data, - const std::string& content_type) { - HttpResponse response; - response.status_code = 0; - - if (!pImpl->curl) { - response.error_message = "CURL not initialized"; - return response; - } - - curl_easy_reset(pImpl->curl); - - // Re-apply settings - curl_easy_setopt(pImpl->curl, CURLOPT_URL, url.c_str()); - - // Set write callback - std::string response_body; - curl_easy_setopt(pImpl->curl, CURLOPT_WRITEFUNCTION, write_callback); - curl_easy_setopt(pImpl->curl, CURLOPT_WRITEDATA, &response_body); - - // Set timeout - curl_easy_setopt(pImpl->curl, CURLOPT_TIMEOUT, pImpl->timeout); - curl_easy_setopt(pImpl->curl, CURLOPT_CONNECTTIMEOUT, 10L); - - // Set user agent - curl_easy_setopt(pImpl->curl, CURLOPT_USERAGENT, pImpl->user_agent.c_str()); - - // Set redirect following - if (pImpl->follow_redirects) { - curl_easy_setopt(pImpl->curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(pImpl->curl, CURLOPT_MAXREDIRS, 10L); - } - - // HTTPS support - curl_easy_setopt(pImpl->curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(pImpl->curl, CURLOPT_SSL_VERIFYHOST, 2L); - - // Cookie settings - if (!pImpl->cookie_file.empty()) { - curl_easy_setopt(pImpl->curl, CURLOPT_COOKIEFILE, pImpl->cookie_file.c_str()); - curl_easy_setopt(pImpl->curl, CURLOPT_COOKIEJAR, pImpl->cookie_file.c_str()); - } else { - curl_easy_setopt(pImpl->curl, CURLOPT_COOKIEFILE, ""); - } - - // Enable automatic decompression - curl_easy_setopt(pImpl->curl, CURLOPT_ACCEPT_ENCODING, ""); - - // Set POST method - curl_easy_setopt(pImpl->curl, CURLOPT_POST, 1L); - - // Set POST data - curl_easy_setopt(pImpl->curl, CURLOPT_POSTFIELDS, data.c_str()); - curl_easy_setopt(pImpl->curl, CURLOPT_POSTFIELDSIZE, data.length()); - - // Set Content-Type header - struct curl_slist* headers = nullptr; - std::string content_type_header = "Content-Type: " + content_type; - headers = curl_slist_append(headers, content_type_header.c_str()); - curl_easy_setopt(pImpl->curl, CURLOPT_HTTPHEADER, headers); - - // Perform request - CURLcode res = curl_easy_perform(pImpl->curl); - - // Clean up headers - curl_slist_free_all(headers); - - if (res != CURLE_OK) { - response.error_message = curl_easy_strerror(res); - return response; - } - - // Get response code - long http_code = 0; - curl_easy_getinfo(pImpl->curl, CURLINFO_RESPONSE_CODE, &http_code); - response.status_code = static_cast<int>(http_code); - - // Get Content-Type - char* resp_content_type = nullptr; - curl_easy_getinfo(pImpl->curl, CURLINFO_CONTENT_TYPE, &resp_content_type); - if (resp_content_type) { - response.content_type = resp_content_type; - } - - response.body = std::move(response_body); - - return response; -} - -void HttpClient::set_timeout(long timeout_seconds) { - pImpl->timeout = timeout_seconds; -} - -void HttpClient::set_user_agent(const std::string& user_agent) { - pImpl->user_agent = user_agent; -} - -void HttpClient::set_follow_redirects(bool follow) { - pImpl->follow_redirects = follow; -} - -void HttpClient::enable_cookies(const std::string& cookie_file) { - pImpl->cookie_file = cookie_file; -} - -// ==================== 异步请求实现 ==================== - -void HttpClient::start_async_fetch(const std::string& url) { - // 如果有正在进行的请求,先取消 - if (pImpl->async_easy) { - cancel_async(); - } - - // 创建新的easy handle - pImpl->async_easy = curl_easy_init(); - if (!pImpl->async_easy) { - pImpl->async_state = AsyncState::FAILED; - pImpl->async_result.error_message = "Failed to create CURL handle"; - return; - } - - // 配置请求 - pImpl->setup_easy_handle(pImpl->async_easy, url); - - // 设置写回调 - pImpl->async_response_body.clear(); - curl_easy_setopt(pImpl->async_easy, CURLOPT_WRITEFUNCTION, write_callback); - curl_easy_setopt(pImpl->async_easy, CURLOPT_WRITEDATA, &pImpl->async_response_body); - - // 添加到multi handle - curl_multi_add_handle(pImpl->multi_handle, pImpl->async_easy); - - pImpl->async_state = AsyncState::LOADING; - pImpl->async_result = HttpResponse{}; // 重置结果 -} - -AsyncState HttpClient::poll_async() { - if (pImpl->async_state != AsyncState::LOADING) { - return pImpl->async_state; - } - - // 执行非阻塞的multi perform - int still_running = 0; - CURLMcode mc = curl_multi_perform(pImpl->multi_handle, &still_running); - - if (mc != CURLM_OK) { - pImpl->async_result.error_message = curl_multi_strerror(mc); - pImpl->async_state = AsyncState::FAILED; - pImpl->cleanup_async(); - return pImpl->async_state; - } - - // 检查是否有完成的请求 - int msgs_left = 0; - CURLMsg* msg; - while ((msg = curl_multi_info_read(pImpl->multi_handle, &msgs_left))) { - if (msg->msg == CURLMSG_DONE) { - CURL* easy = msg->easy_handle; - CURLcode result = msg->data.result; - - if (result == CURLE_OK) { - // 获取响应信息 - long http_code = 0; - curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &http_code); - pImpl->async_result.status_code = static_cast<int>(http_code); - - char* content_type = nullptr; - curl_easy_getinfo(easy, CURLINFO_CONTENT_TYPE, &content_type); - if (content_type) { - pImpl->async_result.content_type = content_type; - } - - pImpl->async_result.body = std::move(pImpl->async_response_body); - pImpl->async_state = AsyncState::COMPLETE; - } else { - pImpl->async_result.error_message = curl_easy_strerror(result); - pImpl->async_state = AsyncState::FAILED; - } - - // 清理handle但保留状态供获取结果 - curl_multi_remove_handle(pImpl->multi_handle, pImpl->async_easy); - curl_easy_cleanup(pImpl->async_easy); - pImpl->async_easy = nullptr; - } - } - - return pImpl->async_state; -} - -HttpResponse HttpClient::get_async_result() { - HttpResponse result = std::move(pImpl->async_result); - pImpl->async_result = HttpResponse{}; - pImpl->async_state = AsyncState::IDLE; - return result; -} - -void HttpClient::cancel_async() { - if (pImpl->async_easy) { - pImpl->cleanup_async(); - pImpl->async_state = AsyncState::CANCELLED; - } -} - -bool HttpClient::is_async_active() const { - return pImpl->async_state == AsyncState::LOADING; -} - -// ========== 异步图片下载接口 ========== - -void HttpClient::add_image_download(const std::string& url, void* user_data) { - InternalImageTask task; - task.url = url; - task.user_data = user_data; - pImpl->pending_images.push_back(std::move(task)); -} - -void HttpClient::poll_image_downloads() { - // 启动新的下载任务,直到达到最大并发数 - while (!pImpl->pending_images.empty() && - static_cast<int>(pImpl->loading_images.size()) < pImpl->max_concurrent_images) { - InternalImageTask task = std::move(pImpl->pending_images.front()); - pImpl->pending_images.erase(pImpl->pending_images.begin()); - - pImpl->start_image_download(task); - pImpl->loading_images.push_back(std::move(task)); - } - - if (pImpl->loading_images.empty()) { - return; // 没有正在下载的任务 - } - - // 执行非阻塞的multi perform - int still_running = 0; - CURLMcode mc = curl_multi_perform(pImpl->image_multi, &still_running); - - if (mc != CURLM_OK) { - // 发生错误,放弃所有正在下载的任务 - pImpl->cleanup_all_images(); - return; - } - - // 检查是否有完成的请求 - int msgs_left = 0; - CURLMsg* msg; - std::vector<std::pair<CURL*, CURLcode>> to_remove; // 记录需要移除的handles和结果 - - while ((msg = curl_multi_info_read(pImpl->image_multi, &msgs_left))) { - if (msg->msg == CURLMSG_DONE) { - to_remove.push_back({msg->easy_handle, msg->data.result}); - } - } - - // 处理完成的任务 - for (const auto& [easy, curl_result] : to_remove) { - // 找到对应的任务 - for (auto it = pImpl->loading_images.begin(); it != pImpl->loading_images.end(); ++it) { - if (it->easy_handle == easy) { - ImageDownloadTask completed; - completed.url = it->url; - completed.user_data = it->user_data; - - if (curl_result == CURLE_OK) { - // 获取响应信息 - long http_code = 0; - curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &http_code); - completed.status_code = static_cast<int>(http_code); - - char* content_type = nullptr; - curl_easy_getinfo(easy, CURLINFO_CONTENT_TYPE, &content_type); - if (content_type) { - completed.content_type = content_type; - } - - completed.data = std::move(it->data); - } else { - completed.error_message = curl_easy_strerror(curl_result); - completed.status_code = 0; - } - - pImpl->completed_images.push_back(std::move(completed)); - - // 清理easy handle - curl_multi_remove_handle(pImpl->image_multi, easy); - curl_easy_cleanup(easy); - - // 从loading列表中移除 - pImpl->loading_images.erase(it); - break; - } - } - } -} - -std::vector<ImageDownloadTask> HttpClient::get_completed_images() { - std::vector<ImageDownloadTask> result = std::move(pImpl->completed_images); - pImpl->completed_images.clear(); - return result; -} - -void HttpClient::cancel_all_images() { - pImpl->cleanup_all_images(); -} - -int HttpClient::get_pending_image_count() const { - return static_cast<int>(pImpl->pending_images.size()); -} - -int HttpClient::get_loading_image_count() const { - return static_cast<int>(pImpl->loading_images.size()); -} - -void HttpClient::set_max_concurrent_images(int max) { - if (max > 0 && max <= 10) { // 限制在1-10之间 - pImpl->max_concurrent_images = max; - } -} \ No newline at end of file diff --git a/src/http_client.h b/src/http_client.h deleted file mode 100644 index 26bb82b..0000000 --- a/src/http_client.h +++ /dev/null @@ -1,93 +0,0 @@ -#pragma once - -#include <string> -#include <vector> -#include <cstdint> -#include <memory> - -// 异步请求状态 -enum class AsyncState { - IDLE, // 无活跃请求 - LOADING, // 请求进行中 - COMPLETE, // 请求成功完成 - FAILED, // 请求失败 - CANCELLED // 请求被取消 -}; - -struct HttpResponse { - int status_code; - std::string body; - std::string content_type; - std::string error_message; - - bool is_success() const { - return status_code >= 200 && status_code < 300; - } - - bool is_image() const { - return content_type.find("image/") == 0; - } -}; - -struct BinaryResponse { - int status_code; - std::vector<uint8_t> data; - std::string content_type; - std::string error_message; - - bool is_success() const { - return status_code >= 200 && status_code < 300; - } -}; - -// 异步图片下载任务 -struct ImageDownloadTask { - std::string url; - void* user_data; // 用户自定义数据 (例如 DomNode*) - std::vector<uint8_t> data; - std::string content_type; - int status_code = 0; - std::string error_message; - - bool is_success() const { - return status_code >= 200 && status_code < 300; - } -}; - -class HttpClient { -public: - HttpClient(); - ~HttpClient(); - - // 同步请求接口 - HttpResponse fetch(const std::string& url); - BinaryResponse fetch_binary(const std::string& url); - HttpResponse post(const std::string& url, const std::string& data, - const std::string& content_type = "application/x-www-form-urlencoded"); - - // 异步请求接口 (页面) - void start_async_fetch(const std::string& url); - AsyncState poll_async(); // 非阻塞轮询,返回当前状态 - HttpResponse get_async_result(); // 获取结果并重置状态 - void cancel_async(); // 取消当前异步请求 - bool is_async_active() const; // 是否有活跃的异步请求 - - // 异步图片下载接口 (支持多并发) - void add_image_download(const std::string& url, void* user_data = nullptr); - void poll_image_downloads(); // 非阻塞轮询所有图片下载 - std::vector<ImageDownloadTask> get_completed_images(); // 获取并移除已完成的图片 - void cancel_all_images(); // 取消所有图片下载 - int get_pending_image_count() const; // 获取待下载图片数量 - int get_loading_image_count() const; // 获取正在下载的图片数量 - void set_max_concurrent_images(int max); // 设置最大并发数 (默认3) - - // 配置 - void set_timeout(long timeout_seconds); - void set_user_agent(const std::string& user_agent); - void set_follow_redirects(bool follow); - void enable_cookies(const std::string& cookie_file = ""); - -private: - class Impl; - std::unique_ptr<Impl> pImpl; -}; diff --git a/src/input_handler.cpp b/src/input_handler.cpp deleted file mode 100644 index e1c1865..0000000 --- a/src/input_handler.cpp +++ /dev/null @@ -1,470 +0,0 @@ -#include "input_handler.h" -#include <curses.h> -#include <cctype> -#include <sstream> - -class InputHandler::Impl { -public: - InputMode mode = InputMode::NORMAL; - std::string buffer; - std::string count_buffer; - std::function<void(const std::string&)> status_callback; - - void set_status(const std::string& msg) { - if (status_callback) { - status_callback(msg); - } - } - - InputResult process_normal_mode(int ch) { - InputResult result; - result.action = Action::NONE; - result.number = 0; - result.has_count = false; - result.count = 1; - - // Handle multi-char commands like 'gg', 'm', ' - if (!buffer.empty()) { - if (buffer == "m") { - // Set mark with letter - if (std::isalpha(ch)) { - result.action = Action::SET_MARK; - result.text = std::string(1, static_cast<char>(ch)); - buffer.clear(); - count_buffer.clear(); - return result; - } - buffer.clear(); - count_buffer.clear(); - return result; - } else if (buffer == "'") { - // Jump to mark - if (std::isalpha(ch)) { - result.action = Action::GOTO_MARK; - result.text = std::string(1, static_cast<char>(ch)); - buffer.clear(); - count_buffer.clear(); - return result; - } - buffer.clear(); - count_buffer.clear(); - return result; - } - } - - // Handle digit input for count - if (std::isdigit(ch) && (ch != '0' || !count_buffer.empty())) { - count_buffer += static_cast<char>(ch); - return result; - } - - if (!count_buffer.empty()) { - result.has_count = true; - result.count = std::stoi(count_buffer); - } - - switch (ch) { - case 'j': - case KEY_DOWN: - result.action = Action::SCROLL_DOWN; - count_buffer.clear(); - break; - case 'k': - case KEY_UP: - result.action = Action::SCROLL_UP; - count_buffer.clear(); - break; - case 'h': - case KEY_LEFT: - result.action = Action::GO_BACK; - count_buffer.clear(); - break; - case 'l': - case KEY_RIGHT: - result.action = Action::GO_FORWARD; - count_buffer.clear(); - break; - case 4: - case ' ': - result.action = Action::SCROLL_PAGE_DOWN; - count_buffer.clear(); - break; - case 21: - case 'b': - result.action = Action::SCROLL_PAGE_UP; - count_buffer.clear(); - break; - case 'g': - buffer += 'g'; - if (buffer == "gg") { - result.action = Action::GOTO_TOP; - buffer.clear(); - } - count_buffer.clear(); - break; - case 'G': - if (result.has_count) { - result.action = Action::GOTO_LINE; - result.number = result.count; - } else { - result.action = Action::GOTO_BOTTOM; - } - count_buffer.clear(); - break; - case '/': - mode = InputMode::SEARCH; - buffer = "/"; - count_buffer.clear(); - break; - case 'n': - result.action = Action::SEARCH_NEXT; - count_buffer.clear(); - break; - case 'N': - result.action = Action::SEARCH_PREV; - count_buffer.clear(); - break; - case '\t': - // Tab can navigate both links and fields - browser will decide - result.action = Action::NEXT_LINK; - count_buffer.clear(); - break; - case KEY_BTAB: - case 'T': - result.action = Action::PREV_LINK; - count_buffer.clear(); - break; - case '\n': - case '\r': - // Enter can follow links or activate fields - browser will decide - if (result.has_count) { - result.action = Action::GOTO_LINK; - result.number = result.count; - } else { - result.action = Action::FOLLOW_LINK; - } - count_buffer.clear(); - break; - case 'i': - // 'i' to focus on first form field (like vim insert mode) - result.action = Action::NEXT_FIELD; - count_buffer.clear(); - break; - case 'f': - // 'f' command: vimium-style link hints - result.action = Action::SHOW_LINK_HINTS; - mode = InputMode::LINK_HINTS; - buffer.clear(); - count_buffer.clear(); - break; - case 'm': - // Set mark (wait for next char) - buffer = "m"; - count_buffer.clear(); - break; - case '\'': - // Jump to mark (wait for next char) - buffer = "'"; - count_buffer.clear(); - break; - case ':': - mode = InputMode::COMMAND; - buffer = ":"; - break; - case 'r': - result.action = Action::REFRESH; - break; - case 'q': - result.action = Action::QUIT; - break; - case '?': - result.action = Action::HELP; - break; - case 'B': - result.action = Action::ADD_BOOKMARK; - break; - case 'D': - result.action = Action::REMOVE_BOOKMARK; - break; - default: - buffer.clear(); - break; - } - - return result; - } - - InputResult process_command_mode(int ch) { - InputResult result; - result.action = Action::NONE; - - if (ch == '\n' || ch == '\r') { - std::string command = buffer.substr(1); - - if (command == "q" || command == "quit") { - result.action = Action::QUIT; - } else if (command == "h" || command == "help") { - result.action = Action::HELP; - } else if (command == "r" || command == "refresh") { - result.action = Action::REFRESH; - } else if (command.rfind("o ", 0) == 0 || command.rfind("open ", 0) == 0) { - 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 == "bookmarks" || command == "bm" || command == "b") { - result.action = Action::SHOW_BOOKMARKS; - } else if (command == "history" || command == "hist" || command == "hi") { - result.action = Action::SHOW_HISTORY; - } else if (!command.empty() && std::isdigit(command[0])) { - try { - result.action = Action::GOTO_LINE; - result.number = std::stoi(command); - } catch (...) { - set_status("Invalid line number"); - } - } - - mode = InputMode::NORMAL; - buffer.clear(); - } else if (ch == 27) { - mode = InputMode::NORMAL; - buffer.clear(); - } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { - if (buffer.length() > 1) { - buffer.pop_back(); - } else { - mode = InputMode::NORMAL; - buffer.clear(); - } - } else if (std::isprint(ch)) { - buffer += static_cast<char>(ch); - } - - return result; - } - - InputResult process_search_mode(int ch) { - InputResult result; - result.action = Action::NONE; - - if (ch == '\n' || ch == '\r') { - if (buffer.length() > 1) { - result.action = Action::SEARCH_FORWARD; - result.text = buffer.substr(1); - } - mode = InputMode::NORMAL; - buffer.clear(); - } else if (ch == 27) { - mode = InputMode::NORMAL; - buffer.clear(); - } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { - if (buffer.length() > 1) { - buffer.pop_back(); - } else { - mode = InputMode::NORMAL; - buffer.clear(); - } - } else if (std::isprint(ch)) { - buffer += static_cast<char>(ch); - } - - return result; - } - - InputResult process_link_mode(int ch) { - InputResult result; - result.action = Action::NONE; - - if (std::isdigit(ch)) { - buffer += static_cast<char>(ch); - } else if (ch == '\n' || ch == '\r') { - // Follow the link number entered - if (buffer.length() > 1) { - try { - int link_num = std::stoi(buffer.substr(1)); - result.action = Action::FOLLOW_LINK_NUM; - result.number = link_num; - } catch (...) { - set_status("Invalid link number"); - } - } - mode = InputMode::NORMAL; - buffer.clear(); - } else if (ch == 27) { - // ESC cancels - mode = InputMode::NORMAL; - buffer.clear(); - } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { - if (buffer.length() > 1) { - buffer.pop_back(); - } else { - mode = InputMode::NORMAL; - buffer.clear(); - } - } - - return result; - } - - InputResult process_link_hints_mode(int ch) { - InputResult result; - result.action = Action::NONE; - - if (ch == 27) { - // ESC cancels link hints mode - mode = InputMode::NORMAL; - buffer.clear(); - return result; - } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { - // Backspace removes last character - if (!buffer.empty()) { - buffer.pop_back(); - } else { - mode = InputMode::NORMAL; - } - return result; - } else if (std::isalpha(ch)) { - // Add character to buffer - buffer += std::tolower(static_cast<char>(ch)); - - // Try to match link hint - result.action = Action::FOLLOW_LINK_HINT; - result.text = buffer; - - // Mode will be reset by browser if link is followed - return result; - } - - return result; - } - - InputResult process_form_edit_mode(int ch) { - InputResult result; - result.action = Action::NONE; - - if (ch == 27) { - // ESC exits form edit mode - mode = InputMode::NORMAL; - buffer.clear(); - return result; - } else if (ch == '\n' || ch == '\r') { - // Enter submits the text - result.action = Action::EDIT_TEXT; - result.text = buffer; - mode = InputMode::NORMAL; - buffer.clear(); - return result; - } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { - // Backspace removes last character - if (!buffer.empty()) { - buffer.pop_back(); - } - return result; - } else if (ch == '\t') { - // Tab moves to next field while saving current text - result.action = Action::NEXT_FIELD; - result.text = buffer; - buffer.clear(); - return result; - } else if (ch == KEY_BTAB) { - // Shift+Tab moves to previous field while saving current text - result.action = Action::PREV_FIELD; - result.text = buffer; - buffer.clear(); - return result; - } else if (std::isprint(ch)) { - // Add printable characters to buffer - buffer += static_cast<char>(ch); - // Return EDIT_TEXT to update in real-time - result.action = Action::EDIT_TEXT; - result.text = buffer; - return result; - } - - return result; - } - - InputResult process_select_option_mode(int ch) { - InputResult result; - result.action = Action::NONE; - - if (ch == 27) { - // ESC cancels selection - mode = InputMode::NORMAL; - return result; - } else if (ch == '\n' || ch == '\r') { - // Enter selects current option - result.action = Action::SELECT_CURRENT_OPTION; - mode = InputMode::NORMAL; - return result; - } else if (ch == 'j' || ch == KEY_DOWN) { - // Next option - result.action = Action::NEXT_OPTION; - return result; - } else if (ch == 'k' || ch == KEY_UP) { - // Previous option - result.action = Action::PREV_OPTION; - return result; - } - - return result; - } - -}; - -InputHandler::InputHandler() : pImpl(std::make_unique<Impl>()) {} - -InputHandler::~InputHandler() = default; - -InputResult InputHandler::handle_key(int ch) { - switch (pImpl->mode) { - case InputMode::NORMAL: - return pImpl->process_normal_mode(ch); - case InputMode::COMMAND: - return pImpl->process_command_mode(ch); - case InputMode::SEARCH: - return pImpl->process_search_mode(ch); - case InputMode::LINK: - return pImpl->process_link_mode(ch); - case InputMode::LINK_HINTS: - return pImpl->process_link_hints_mode(ch); - case InputMode::FORM_EDIT: - return pImpl->process_form_edit_mode(ch); - case InputMode::SELECT_OPTION: - return pImpl->process_select_option_mode(ch); - default: - break; - } - - InputResult result; - result.action = Action::NONE; - return result; -} - -InputMode InputHandler::get_mode() const { - return pImpl->mode; -} - -std::string InputHandler::get_buffer() const { - return pImpl->buffer; -} - -void InputHandler::reset() { - pImpl->mode = InputMode::NORMAL; - pImpl->buffer.clear(); - pImpl->count_buffer.clear(); -} - -void InputHandler::set_mode(InputMode mode) { - pImpl->mode = mode; -} - -void InputHandler::set_buffer(const std::string& buffer) { - pImpl->buffer = buffer; -} - -void InputHandler::set_status_callback(std::function<void(const std::string&)> callback) { - pImpl->status_callback = callback; -} diff --git a/src/input_handler.h b/src/input_handler.h deleted file mode 100644 index efae508..0000000 --- a/src/input_handler.h +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -#include <string> -#include <functional> -#include <memory> - -enum class InputMode { - NORMAL, - COMMAND, - SEARCH, - LINK, - LINK_HINTS, // Vimium-style 'f' mode - FORM_EDIT, // Form field editing mode - SELECT_OPTION // Dropdown selection mode -}; - -enum class Action { - NONE, - SCROLL_UP, - SCROLL_DOWN, - SCROLL_PAGE_UP, - SCROLL_PAGE_DOWN, - GOTO_TOP, - GOTO_BOTTOM, - GOTO_LINE, - SEARCH_FORWARD, - SEARCH_NEXT, - SEARCH_PREV, - NEXT_LINK, - PREV_LINK, - FOLLOW_LINK, - GOTO_LINK, // Jump to specific link by number - FOLLOW_LINK_NUM, // Follow specific link by number (f command) - SHOW_LINK_HINTS, // Activate link hints mode ('f') - FOLLOW_LINK_HINT, // Follow link by hint letters - GO_BACK, - GO_FORWARD, - OPEN_URL, - REFRESH, - QUIT, - HELP, - SET_MARK, // Set a mark (m + letter) - GOTO_MARK, // Jump to mark (' + letter) - ADD_BOOKMARK, // Add current page to bookmarks (B) - REMOVE_BOOKMARK, // Remove current page from bookmarks (D) - SHOW_BOOKMARKS, // Show bookmarks page (:bookmarks) - SHOW_HISTORY, // Show history page (:history) - NEXT_FIELD, // Move to next form field (Tab) - PREV_FIELD, // Move to previous form field (Shift+Tab) - ACTIVATE_FIELD, // Activate current field for editing (Enter) - TOGGLE_CHECKBOX, // Toggle checkbox state - EDIT_TEXT, // Edit text input (updates text buffer) - SUBMIT_FORM, // Submit form (Enter on submit button) - NEXT_OPTION, // Move to next dropdown option (j/down) - PREV_OPTION, // Move to previous dropdown option (k/up) - SELECT_CURRENT_OPTION // Select current dropdown option (Enter) -}; - -struct InputResult { - Action action; - std::string text; - int number; - bool has_count; - int count; -}; - -class InputHandler { -public: - InputHandler(); - ~InputHandler(); - - InputResult handle_key(int ch); - InputMode get_mode() const; - std::string get_buffer() const; - void reset(); - void set_mode(InputMode mode); - void set_buffer(const std::string& buffer); - void set_status_callback(std::function<void(const std::string&)> callback); - -private: - class Impl; - std::unique_ptr<Impl> pImpl; -}; diff --git a/src/render/colors.h b/src/render/colors.h deleted file mode 100644 index 8bac59b..0000000 --- a/src/render/colors.h +++ /dev/null @@ -1,116 +0,0 @@ -#pragma once - -#include <cstdint> - -namespace tut { - -/** - * 颜色定义 - True Color (24-bit RGB) - * - * 使用温暖的配色方案,适合长时间阅读 - */ -namespace colors { - -// ==================== 基础颜色 ==================== - -// 背景色 -constexpr uint32_t BG_PRIMARY = 0x1A1A1A; // 主背景 - 深灰 -constexpr uint32_t BG_SECONDARY = 0x252525; // 次背景 - 稍浅灰 -constexpr uint32_t BG_ELEVATED = 0x2A2A2A; // 抬升背景 - 用于卡片/区块 -constexpr uint32_t BG_SELECTION = 0x3A3A3A; // 选中背景 - -// 前景色 -constexpr uint32_t FG_PRIMARY = 0xD0D0D0; // 主文本 - 浅灰 -constexpr uint32_t FG_SECONDARY = 0x909090; // 次文本 - 中灰 -constexpr uint32_t FG_DIM = 0x606060; // 暗淡文本 - -// ==================== 语义颜色 ==================== - -// 标题 -constexpr uint32_t H1_FG = 0xE8C48C; // H1 - 暖金色 -constexpr uint32_t H2_FG = 0x88C0D0; // H2 - 冰蓝色 -constexpr uint32_t H3_FG = 0xA3BE8C; // H3 - 柔绿色 - -// 链接 -constexpr uint32_t LINK_FG = 0x81A1C1; // 链接 - 柔蓝色 -constexpr uint32_t LINK_ACTIVE = 0x88C0D0; // 活跃链接 - 亮蓝色 -constexpr uint32_t LINK_VISITED = 0xB48EAD; // 已访问链接 - 柔紫色 - -// 表单元素 -constexpr uint32_t INPUT_BG = 0x2E3440; // 输入框背景 -constexpr uint32_t INPUT_BORDER = 0x4C566A; // 输入框边框 -constexpr uint32_t INPUT_FOCUS = 0x5E81AC; // 聚焦边框 - -// 状态颜色 -constexpr uint32_t SUCCESS = 0xA3BE8C; // 成功 - 绿色 -constexpr uint32_t WARNING = 0xEBCB8B; // 警告 - 黄色 -constexpr uint32_t ERROR = 0xBF616A; // 错误 - 红色 -constexpr uint32_t INFO = 0x88C0D0; // 信息 - 蓝色 - -// ==================== UI元素颜色 ==================== - -// 状态栏 -constexpr uint32_t STATUSBAR_BG = 0x2E3440; // 状态栏背景 -constexpr uint32_t STATUSBAR_FG = 0xD8DEE9; // 状态栏文本 - -// URL栏 -constexpr uint32_t URLBAR_BG = 0x3B4252; // URL栏背景 -constexpr uint32_t URLBAR_FG = 0xECEFF4; // URL栏文本 - -// 搜索高亮 -constexpr uint32_t SEARCH_MATCH_BG = 0x4C566A; -constexpr uint32_t SEARCH_MATCH_FG = 0xECEFF4; -constexpr uint32_t SEARCH_CURRENT_BG = 0x5E81AC; -constexpr uint32_t SEARCH_CURRENT_FG = 0xFFFFFF; - -// 装饰元素 -constexpr uint32_t BORDER = 0x4C566A; // 边框 -constexpr uint32_t DIVIDER = 0x3B4252; // 分隔线 - -// 代码块 -constexpr uint32_t CODE_BG = 0x2E3440; // 代码背景 -constexpr uint32_t CODE_FG = 0xD8DEE9; // 代码文本 - -// 引用块 -constexpr uint32_t QUOTE_BORDER = 0x4C566A; // 引用边框 -constexpr uint32_t QUOTE_FG = 0x909090; // 引用文本 - -// 表格 -constexpr uint32_t TABLE_BORDER = 0x4C566A; -constexpr uint32_t TABLE_HEADER_BG = 0x2E3440; -constexpr uint32_t TABLE_ROW_ALT = 0x252525; // 交替行 - -} // namespace colors - -/** - * RGB辅助函数 - */ -inline constexpr uint32_t rgb(uint8_t r, uint8_t g, uint8_t b) { - return (static_cast<uint32_t>(r) << 16) | - (static_cast<uint32_t>(g) << 8) | - static_cast<uint32_t>(b); -} - -inline constexpr uint8_t get_red(uint32_t color) { - return (color >> 16) & 0xFF; -} - -inline constexpr uint8_t get_green(uint32_t color) { - return (color >> 8) & 0xFF; -} - -inline constexpr uint8_t get_blue(uint32_t color) { - return color & 0xFF; -} - -/** - * 颜色混合(线性插值) - */ -inline uint32_t blend_colors(uint32_t c1, uint32_t c2, float t) { - uint8_t r = static_cast<uint8_t>(get_red(c1) * (1 - t) + get_red(c2) * t); - uint8_t g = static_cast<uint8_t>(get_green(c1) * (1 - t) + get_green(c2) * t); - uint8_t b = static_cast<uint8_t>(get_blue(c1) * (1 - t) + get_blue(c2) * t); - return rgb(r, g, b); -} - -} // namespace tut diff --git a/src/render/decorations.h b/src/render/decorations.h deleted file mode 100644 index 4afa9ad..0000000 --- a/src/render/decorations.h +++ /dev/null @@ -1,153 +0,0 @@ -#pragma once - -namespace tut { - -/** - * Unicode装饰字符 - * - * 用于绘制边框、列表符号等装饰元素 - */ -namespace chars { - -// ==================== 框线字符 (Box Drawing) ==================== - -// 双线框 -constexpr const char* DBL_HORIZONTAL = "═"; -constexpr const char* DBL_VERTICAL = "║"; -constexpr const char* DBL_TOP_LEFT = "╔"; -constexpr const char* DBL_TOP_RIGHT = "╗"; -constexpr const char* DBL_BOTTOM_LEFT = "╚"; -constexpr const char* DBL_BOTTOM_RIGHT = "╝"; -constexpr const char* DBL_T_DOWN = "╦"; -constexpr const char* DBL_T_UP = "╩"; -constexpr const char* DBL_T_RIGHT = "╠"; -constexpr const char* DBL_T_LEFT = "╣"; -constexpr const char* DBL_CROSS = "╬"; - -// 单线框 -constexpr const char* SGL_HORIZONTAL = "─"; -constexpr const char* SGL_VERTICAL = "│"; -constexpr const char* SGL_TOP_LEFT = "┌"; -constexpr const char* SGL_TOP_RIGHT = "┐"; -constexpr const char* SGL_BOTTOM_LEFT = "└"; -constexpr const char* SGL_BOTTOM_RIGHT = "┘"; -constexpr const char* SGL_T_DOWN = "┬"; -constexpr const char* SGL_T_UP = "┴"; -constexpr const char* SGL_T_RIGHT = "├"; -constexpr const char* SGL_T_LEFT = "┤"; -constexpr const char* SGL_CROSS = "┼"; - -// 粗线框 -constexpr const char* HEAVY_HORIZONTAL = "━"; -constexpr const char* HEAVY_VERTICAL = "┃"; -constexpr const char* HEAVY_TOP_LEFT = "┏"; -constexpr const char* HEAVY_TOP_RIGHT = "┓"; -constexpr const char* HEAVY_BOTTOM_LEFT = "┗"; -constexpr const char* HEAVY_BOTTOM_RIGHT= "┛"; - -// 圆角框 -constexpr const char* ROUND_TOP_LEFT = "╭"; -constexpr const char* ROUND_TOP_RIGHT = "╮"; -constexpr const char* ROUND_BOTTOM_LEFT = "╰"; -constexpr const char* ROUND_BOTTOM_RIGHT= "╯"; - -// ==================== 列表符号 ==================== - -constexpr const char* BULLET = "•"; -constexpr const char* BULLET_HOLLOW = "◦"; -constexpr const char* BULLET_SQUARE = "▪"; -constexpr const char* CIRCLE = "◦"; -constexpr const char* SQUARE = "▪"; -constexpr const char* TRIANGLE = "‣"; -constexpr const char* DIAMOND = "◆"; -constexpr const char* QUOTE_LEFT = "│"; -constexpr const char* ARROW = "➤"; -constexpr const char* DASH = "–"; -constexpr const char* STAR = "★"; -constexpr const char* CHECK = "✓"; -constexpr const char* CROSS = "✗"; - -// ==================== 箭头 ==================== - -constexpr const char* ARROW_RIGHT = "→"; -constexpr const char* ARROW_LEFT = "←"; -constexpr const char* ARROW_UP = "↑"; -constexpr const char* ARROW_DOWN = "↓"; -constexpr const char* ARROW_DOUBLE_RIGHT= "»"; -constexpr const char* ARROW_DOUBLE_LEFT = "«"; - -// ==================== 装饰符号 ==================== - -constexpr const char* SECTION = "§"; -constexpr const char* PARAGRAPH = "¶"; -constexpr const char* ELLIPSIS = "…"; -constexpr const char* MIDDOT = "·"; -constexpr const char* DEGREE = "°"; - -// ==================== 进度/状态 ==================== - -constexpr const char* BLOCK_FULL = "█"; -constexpr const char* BLOCK_3_4 = "▓"; -constexpr const char* BLOCK_HALF = "▒"; -constexpr const char* BLOCK_1_4 = "░"; -constexpr const char* SPINNER[] = {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}; -constexpr int SPINNER_FRAMES = 10; - -// ==================== 分隔线样式 ==================== - -constexpr const char* HR_LIGHT = "─"; -constexpr const char* HR_HEAVY = "━"; -constexpr const char* HR_DOUBLE = "═"; -constexpr const char* HR_DASHED = "╌"; -constexpr const char* HR_DOTTED = "┄"; - -} // namespace chars - -/** - * 生成水平分隔线 - */ -inline std::string make_horizontal_line(int width, const char* ch = chars::SGL_HORIZONTAL) { - std::string result; - for (int i = 0; i < width; i++) { - result += ch; - } - return result; -} - -/** - * 绘制简单边框(单线) - */ -struct BoxChars { - const char* top_left; - const char* top_right; - const char* bottom_left; - const char* bottom_right; - const char* horizontal; - const char* vertical; -}; - -constexpr BoxChars BOX_SINGLE = { - chars::SGL_TOP_LEFT, chars::SGL_TOP_RIGHT, - chars::SGL_BOTTOM_LEFT, chars::SGL_BOTTOM_RIGHT, - chars::SGL_HORIZONTAL, chars::SGL_VERTICAL -}; - -constexpr BoxChars BOX_DOUBLE = { - chars::DBL_TOP_LEFT, chars::DBL_TOP_RIGHT, - chars::DBL_BOTTOM_LEFT, chars::DBL_BOTTOM_RIGHT, - chars::DBL_HORIZONTAL, chars::DBL_VERTICAL -}; - -constexpr BoxChars BOX_HEAVY = { - chars::HEAVY_TOP_LEFT, chars::HEAVY_TOP_RIGHT, - chars::HEAVY_BOTTOM_LEFT, chars::HEAVY_BOTTOM_RIGHT, - chars::HEAVY_HORIZONTAL, chars::HEAVY_VERTICAL -}; - -constexpr BoxChars BOX_ROUND = { - chars::ROUND_TOP_LEFT, chars::ROUND_TOP_RIGHT, - chars::ROUND_BOTTOM_LEFT, chars::ROUND_BOTTOM_RIGHT, - chars::SGL_HORIZONTAL, chars::SGL_VERTICAL -}; - -} // namespace tut diff --git a/src/render/image.cpp b/src/render/image.cpp deleted file mode 100644 index 5d5b600..0000000 --- a/src/render/image.cpp +++ /dev/null @@ -1,265 +0,0 @@ -#include "image.h" -#include <algorithm> -#include <cmath> -#include <fstream> -#include <sstream> - -// 尝试加载stb_image(如果存在) -#if __has_include("../utils/stb_image.h") -#define STB_IMAGE_IMPLEMENTATION -#include "../utils/stb_image.h" -#define HAS_STB_IMAGE 1 -#else -#define HAS_STB_IMAGE 0 -#endif - -// 简单的PPM格式解码器(不需要外部库) -static tut::ImageData decode_ppm(const std::vector<uint8_t>& data) { - tut::ImageData result; - - if (data.size() < 10) return result; - - // 检查PPM magic number - if (data[0] != 'P' || (data[1] != '6' && data[1] != '3')) { - return result; - } - - std::string header(data.begin(), data.begin() + std::min(data.size(), size_t(256))); - std::istringstream iss(header); - - std::string magic; - int width, height, max_val; - iss >> magic >> width >> height >> max_val; - - if (width <= 0 || height <= 0 || max_val <= 0) return result; - - result.width = width; - result.height = height; - result.channels = 4; // 输出RGBA - - // 找到header结束位置 - size_t header_end = iss.tellg(); - while (header_end < data.size() && (data[header_end] == ' ' || data[header_end] == '\n')) { - header_end++; - } - - if (data[1] == '6') { - // Binary PPM (P6) - size_t pixel_count = width * height; - result.pixels.resize(pixel_count * 4); - - for (size_t i = 0; i < pixel_count && header_end + i * 3 + 2 < data.size(); ++i) { - result.pixels[i * 4 + 0] = data[header_end + i * 3 + 0]; // R - result.pixels[i * 4 + 1] = data[header_end + i * 3 + 1]; // G - result.pixels[i * 4 + 2] = data[header_end + i * 3 + 2]; // B - result.pixels[i * 4 + 3] = 255; // A - } - } - - return result; -} - -namespace tut { - -// ==================== ImageRenderer ==================== - -ImageRenderer::ImageRenderer() = default; - -AsciiImage ImageRenderer::render(const ImageData& data, int max_width, int max_height) { - AsciiImage result; - - if (!data.is_valid()) { - return result; - } - - // 计算缩放比例,保持宽高比 - // 终端字符通常是2:1的高宽比,所以height需要除以2 - float aspect = static_cast<float>(data.width) / data.height; - int target_width = max_width; - int target_height = static_cast<int>(target_width / aspect / 2.0f); - - if (target_height > max_height) { - target_height = max_height; - target_width = static_cast<int>(target_height * aspect * 2.0f); - } - - target_width = std::max(1, std::min(target_width, max_width)); - target_height = std::max(1, std::min(target_height, max_height)); - - // 缩放图片 - ImageData scaled = resize(data, target_width, target_height); - - result.width = target_width; - result.height = target_height; - result.lines.resize(target_height); - result.colors.resize(target_height); - - for (int y = 0; y < target_height; ++y) { - result.lines[y].reserve(target_width); - result.colors[y].resize(target_width); - - for (int x = 0; x < target_width; ++x) { - int idx = (y * target_width + x) * scaled.channels; - - uint8_t r = scaled.pixels[idx]; - uint8_t g = scaled.pixels[idx + 1]; - uint8_t b = scaled.pixels[idx + 2]; - uint8_t a = (scaled.channels == 4) ? scaled.pixels[idx + 3] : 255; - - // 如果像素透明,使用空格 - if (a < 128) { - result.lines[y] += ' '; - result.colors[y][x] = 0; - continue; - } - - if (mode_ == Mode::ASCII) { - // ASCII模式:使用亮度映射字符 - int brightness = pixel_brightness(r, g, b); - result.lines[y] += brightness_to_char(brightness); - } else if (mode_ == Mode::BLOCKS) { - // 块模式:使用全块字符,颜色表示像素 - result.lines[y] += "\u2588"; // █ 全块 - } else { - // 默认使用块 - result.lines[y] += "\u2588"; - } - - if (color_enabled_) { - result.colors[y][x] = rgb_to_color(r, g, b); - } else { - int brightness = pixel_brightness(r, g, b); - result.colors[y][x] = rgb_to_color(brightness, brightness, brightness); - } - } - } - - return result; -} - -ImageData ImageRenderer::load_from_file(const std::string& path) { - ImageData data; - -#if HAS_STB_IMAGE - int width, height, channels; - unsigned char* pixels = stbi_load(path.c_str(), &width, &height, &channels, 4); - - if (pixels) { - data.width = width; - data.height = height; - data.channels = 4; - data.pixels.assign(pixels, pixels + width * height * 4); - stbi_image_free(pixels); - } -#else - (void)path; // 未使用参数 -#endif - - return data; -} - -ImageData ImageRenderer::load_from_memory(const std::vector<uint8_t>& buffer) { - ImageData data; - -#if HAS_STB_IMAGE - int width, height, channels; - unsigned char* pixels = stbi_load_from_memory( - buffer.data(), - static_cast<int>(buffer.size()), - &width, &height, &channels, 4 - ); - - if (pixels) { - data.width = width; - data.height = height; - data.channels = 4; - data.pixels.assign(pixels, pixels + width * height * 4); - stbi_image_free(pixels); - } -#else - // 尝试PPM格式解码 - data = decode_ppm(buffer); -#endif - - return data; -} - -char ImageRenderer::brightness_to_char(int brightness) const { - // brightness: 0-255 -> 字符索引 - int len = 10; // strlen(ASCII_CHARS) - int idx = (brightness * (len - 1)) / 255; - return ASCII_CHARS[idx]; -} - -uint32_t ImageRenderer::rgb_to_color(uint8_t r, uint8_t g, uint8_t b) { - return (static_cast<uint32_t>(r) << 16) | - (static_cast<uint32_t>(g) << 8) | - static_cast<uint32_t>(b); -} - -int ImageRenderer::pixel_brightness(uint8_t r, uint8_t g, uint8_t b) { - // 使用加权平均计算亮度 (ITU-R BT.601) - return static_cast<int>(0.299f * r + 0.587f * g + 0.114f * b); -} - -ImageData ImageRenderer::resize(const ImageData& src, int new_width, int new_height) { - ImageData dst; - dst.width = new_width; - dst.height = new_height; - dst.channels = src.channels; - dst.pixels.resize(new_width * new_height * src.channels); - - float x_ratio = static_cast<float>(src.width) / new_width; - float y_ratio = static_cast<float>(src.height) / new_height; - - for (int y = 0; y < new_height; ++y) { - for (int x = 0; x < new_width; ++x) { - // 双线性插值(简化版:最近邻) - int src_x = static_cast<int>(x * x_ratio); - int src_y = static_cast<int>(y * y_ratio); - - src_x = std::min(src_x, src.width - 1); - src_y = std::min(src_y, src.height - 1); - - int src_idx = (src_y * src.width + src_x) * src.channels; - int dst_idx = (y * new_width + x) * dst.channels; - - for (int c = 0; c < src.channels; ++c) { - dst.pixels[dst_idx + c] = src.pixels[src_idx + c]; - } - } - } - - return dst; -} - -// ==================== Helper Functions ==================== - -std::string make_image_placeholder(const std::string& alt_text, const std::string& src) { - std::string result = "["; - - if (!alt_text.empty()) { - result += alt_text; - } else if (!src.empty()) { - // 从URL提取文件名 - size_t last_slash = src.rfind('/'); - if (last_slash != std::string::npos && last_slash + 1 < src.length()) { - std::string filename = src.substr(last_slash + 1); - // 去掉查询参数 - size_t query = filename.find('?'); - if (query != std::string::npos) { - filename = filename.substr(0, query); - } - result += "Image: " + filename; - } else { - result += "Image"; - } - } else { - result += "Image"; - } - - result += "]"; - return result; -} - -} // namespace tut diff --git a/src/render/image.h b/src/render/image.h deleted file mode 100644 index 470aa14..0000000 --- a/src/render/image.h +++ /dev/null @@ -1,110 +0,0 @@ -#pragma once - -#include <string> -#include <vector> -#include <cstdint> - -namespace tut { - -/** - * ImageData - 解码后的图片数据 - */ -struct ImageData { - std::vector<uint8_t> pixels; // RGBA像素数据 - int width = 0; - int height = 0; - int channels = 0; // 通道数 (3=RGB, 4=RGBA) - - bool is_valid() const { return width > 0 && height > 0 && !pixels.empty(); } -}; - -/** - * AsciiImage - ASCII艺术渲染结果 - */ -struct AsciiImage { - std::vector<std::string> lines; // 每行的ASCII字符 - std::vector<std::vector<uint32_t>> colors; // 每个字符的颜色 (True Color) - int width = 0; // 字符宽度 - int height = 0; // 字符高度 -}; - -/** - * ImageRenderer - 图片渲染器 - * - * 将图片转换为ASCII艺术或彩色块字符 - */ -class ImageRenderer { -public: - /** - * 渲染模式 - */ - enum class Mode { - ASCII, // 使用ASCII字符 (@#%*+=-:. ) - BLOCKS, // 使用Unicode块字符 (▀▄█) - BRAILLE // 使用盲文点阵字符 - }; - - ImageRenderer(); - - /** - * 从原始RGBA数据创建ASCII图像 - * @param data 图片数据 - * @param max_width 最大字符宽度 - * @param max_height 最大字符高度 - * @return ASCII渲染结果 - */ - AsciiImage render(const ImageData& data, int max_width, int max_height); - - /** - * 从文件加载图片 (需要stb_image) - * @param path 文件路径 - * @return 图片数据 - */ - static ImageData load_from_file(const std::string& path); - - /** - * 从内存加载图片 (需要stb_image) - * @param data 图片二进制数据 - * @return 图片数据 - */ - static ImageData load_from_memory(const std::vector<uint8_t>& data); - - /** - * 设置渲染模式 - */ - void set_mode(Mode mode) { mode_ = mode; } - - /** - * 是否启用颜色 - */ - void set_color_enabled(bool enabled) { color_enabled_ = enabled; } - -private: - Mode mode_ = Mode::BLOCKS; - bool color_enabled_ = true; - - // ASCII字符集 (按亮度从暗到亮) - static constexpr const char* ASCII_CHARS = " .:-=+*#%@"; - - // 将像素亮度映射到字符 - char brightness_to_char(int brightness) const; - - // 将RGB转换为True Color值 - static uint32_t rgb_to_color(uint8_t r, uint8_t g, uint8_t b); - - // 计算像素亮度 - static int pixel_brightness(uint8_t r, uint8_t g, uint8_t b); - - // 缩放图片 - static ImageData resize(const ImageData& src, int new_width, int new_height); -}; - -/** - * 生成图片占位符文本 - * @param alt_text 替代文本 - * @param src 图片URL (用于显示文件名) - * @return 占位符字符串 - */ -std::string make_image_placeholder(const std::string& alt_text, const std::string& src = ""); - -} // namespace tut diff --git a/src/render/layout.cpp b/src/render/layout.cpp deleted file mode 100644 index ea63704..0000000 --- a/src/render/layout.cpp +++ /dev/null @@ -1,933 +0,0 @@ -#include "layout.h" -#include "decorations.h" -#include "image.h" -#include <sstream> -#include <algorithm> - -namespace tut { - -// ==================== LayoutEngine ==================== - -LayoutEngine::LayoutEngine(int viewport_width) - : viewport_width_(viewport_width) - , content_width_(viewport_width - MARGIN_LEFT - MARGIN_RIGHT) -{ -} - -LayoutResult LayoutEngine::layout(const DocumentTree& doc) { - LayoutResult result; - result.title = doc.title; - result.url = doc.url; - - if (!doc.root) { - return result; - } - - Context ctx; - layout_node(doc.root.get(), ctx, result.blocks); - - // 计算总行数并收集链接和字段位置 - int total = 0; - - // 预分配位置数组 - size_t num_links = doc.links.size(); - size_t num_fields = doc.form_fields.size(); - result.link_positions.resize(num_links, {-1, -1}); - result.field_lines.resize(num_fields, -1); - - for (const auto& block : result.blocks) { - total += block.margin_top; - - for (const auto& line : block.lines) { - for (const auto& span : line.spans) { - // 记录链接位置 - if (span.link_index >= 0 && span.link_index < static_cast<int>(num_links)) { - auto& pos = result.link_positions[span.link_index]; - if (pos.start_line < 0) { - pos.start_line = total; - } - pos.end_line = total; - } - // 记录字段位置 - if (span.field_index >= 0 && span.field_index < static_cast<int>(num_fields)) { - if (result.field_lines[span.field_index] < 0) { - result.field_lines[span.field_index] = total; - } - } - } - total++; - } - - total += block.margin_bottom; - } - result.total_lines = total; - - return result; -} - -void LayoutEngine::layout_node(const DomNode* node, Context& ctx, std::vector<LayoutBlock>& blocks) { - if (!node || !node->should_render()) { - return; - } - - if (node->node_type == NodeType::DOCUMENT) { - for (const auto& child : node->children) { - layout_node(child.get(), ctx, blocks); - } - return; - } - - // 处理表单内联元素 - if (node->element_type == ElementType::INPUT || - node->element_type == ElementType::BUTTON || - node->element_type == ElementType::TEXTAREA || - node->element_type == ElementType::SELECT) { - layout_form_element(node, ctx, blocks); - return; - } - - // 处理图片元素 - if (node->element_type == ElementType::IMAGE) { - layout_image_element(node, ctx, blocks); - return; - } - - // 处理块级元素 - if (node->is_block_element()) { - layout_block_element(node, ctx, blocks); - return; - } - - // 处理链接 - 当链接单独出现时(不在段落内),创建一个单独的块 - if (node->element_type == ElementType::LINK && node->link_index >= 0) { - // 检查链接是否有可见文本 - std::string link_text = node->get_all_text(); - // 去除空白 - size_t start = link_text.find_first_not_of(" \t\n\r"); - size_t end = link_text.find_last_not_of(" \t\n\r"); - if (start != std::string::npos && end != std::string::npos) { - link_text = link_text.substr(start, end - start + 1); - } else { - link_text = ""; - } - - if (!link_text.empty()) { - LayoutBlock block; - block.type = ElementType::PARAGRAPH; - block.margin_top = 0; - block.margin_bottom = 0; - - LayoutLine line; - line.indent = MARGIN_LEFT; - - StyledSpan span; - span.text = link_text; - span.fg = colors::LINK_FG; - span.attrs = ATTR_UNDERLINE; - span.link_index = node->link_index; - line.spans.push_back(span); - - block.lines.push_back(line); - blocks.push_back(block); - } - return; - } - - // 处理容器元素 - 递归处理子节点 - // 这包括:html, body, div, table, span, center 等所有容器类元素 - if (node->node_type == NodeType::ELEMENT && !node->children.empty()) { - for (const auto& child : node->children) { - layout_node(child.get(), ctx, blocks); - } - return; - } - - // 处理独立文本节点 - if (node->node_type == NodeType::TEXT && !node->text_content.empty()) { - std::string text = node->text_content; - // 去除首尾空白 - size_t start = text.find_first_not_of(" \t\n\r"); - size_t end = text.find_last_not_of(" \t\n\r"); - if (start != std::string::npos && end != std::string::npos) { - text = text.substr(start, end - start + 1); - } else { - return; // 空白文本,跳过 - } - - if (!text.empty()) { - LayoutBlock block; - block.type = ElementType::TEXT; - block.margin_top = 0; - block.margin_bottom = 0; - - std::vector<StyledSpan> spans; - StyledSpan span; - span.text = text; - span.fg = colors::FG_PRIMARY; - spans.push_back(span); - - block.lines = wrap_text(spans, content_width_, MARGIN_LEFT); - if (!block.lines.empty()) { - blocks.push_back(block); - } - } - } -} - -void LayoutEngine::layout_block_element(const DomNode* node, Context& ctx, std::vector<LayoutBlock>& blocks) { - LayoutBlock block; - block.type = node->element_type; - - // 设置边距 - switch (node->element_type) { - case ElementType::HEADING1: - block.margin_top = 1; - block.margin_bottom = 1; - break; - case ElementType::HEADING2: - case ElementType::HEADING3: - block.margin_top = 1; - block.margin_bottom = 0; - break; - case ElementType::PARAGRAPH: - block.margin_top = 0; - block.margin_bottom = 1; - break; - case ElementType::LIST_ITEM: - case ElementType::ORDERED_LIST_ITEM: - block.margin_top = 0; - block.margin_bottom = 0; - break; - case ElementType::BLOCKQUOTE: - block.margin_top = 1; - block.margin_bottom = 1; - break; - case ElementType::CODE_BLOCK: - block.margin_top = 1; - block.margin_bottom = 1; - break; - case ElementType::HORIZONTAL_RULE: - block.margin_top = 1; - block.margin_bottom = 1; - break; - default: - block.margin_top = 0; - block.margin_bottom = 0; - break; - } - - // 处理特殊块元素 - if (node->element_type == ElementType::HORIZONTAL_RULE) { - // 水平线 - LayoutLine line; - StyledSpan hr_span; - hr_span.text = make_horizontal_line(content_width_, chars::SGL_HORIZONTAL); - hr_span.fg = colors::DIVIDER; - line.spans.push_back(hr_span); - line.indent = MARGIN_LEFT; - block.lines.push_back(line); - blocks.push_back(block); - return; - } - - // 检查是否是列表容器(通过tag_name判断) - if (node->tag_name == "ul" || node->tag_name == "ol") { - // 列表:递归处理子元素 - ctx.list_depth++; - bool is_ordered = (node->tag_name == "ol"); - if (is_ordered) { - ctx.ordered_list_counter = 1; - } - - for (const auto& child : node->children) { - if (child->element_type == ElementType::LIST_ITEM || - child->element_type == ElementType::ORDERED_LIST_ITEM) { - layout_block_element(child.get(), ctx, blocks); - if (is_ordered) { - ctx.ordered_list_counter++; - } - } - } - - ctx.list_depth--; - return; - } - - if (node->element_type == ElementType::BLOCKQUOTE) { - ctx.in_blockquote = true; - } - - if (node->element_type == ElementType::CODE_BLOCK) { - ctx.in_pre = true; - } - - // 收集内联内容 - std::vector<StyledSpan> spans; - - // 列表项的标记 - if (node->element_type == ElementType::LIST_ITEM) { - StyledSpan marker; - marker.text = get_list_marker(ctx.list_depth, ctx.ordered_list_counter > 0, ctx.ordered_list_counter); - marker.fg = colors::FG_SECONDARY; - spans.push_back(marker); - } - - collect_inline_content(node, ctx, spans); - - if (node->element_type == ElementType::BLOCKQUOTE) { - ctx.in_blockquote = false; - } - - if (node->element_type == ElementType::CODE_BLOCK) { - ctx.in_pre = false; - } - - // 计算缩进 - int indent = MARGIN_LEFT; - if (ctx.list_depth > 0) { - indent += ctx.list_depth * 2; - } - if (ctx.in_blockquote) { - indent += 2; - } - - // 换行 - int available_width = content_width_ - (indent - MARGIN_LEFT); - if (ctx.in_pre) { - // 预格式化文本不换行 - for (const auto& span : spans) { - LayoutLine line; - line.indent = indent; - line.spans.push_back(span); - block.lines.push_back(line); - } - } else { - block.lines = wrap_text(spans, available_width, indent); - } - - // 引用块添加边框 - if (node->element_type == ElementType::BLOCKQUOTE && !block.lines.empty()) { - for (auto& line : block.lines) { - StyledSpan border; - border.text = chars::QUOTE_LEFT + std::string(" "); - border.fg = colors::QUOTE_BORDER; - line.spans.insert(line.spans.begin(), border); - } - } - - if (!block.lines.empty()) { - blocks.push_back(block); - } - - // 处理子块元素 - for (const auto& child : node->children) { - if (child->is_block_element()) { - layout_node(child.get(), ctx, blocks); - } - } -} - -void LayoutEngine::layout_form_element(const DomNode* node, Context& /*ctx*/, std::vector<LayoutBlock>& blocks) { - LayoutBlock block; - block.type = node->element_type; - block.margin_top = 0; - block.margin_bottom = 0; - - LayoutLine line; - line.indent = MARGIN_LEFT; - - if (node->element_type == ElementType::INPUT) { - // 渲染输入框 - std::string input_type = node->input_type; - - if (input_type == "submit" || input_type == "button") { - // 按钮样式: [ Submit ] - std::string label = node->value.empty() ? "Submit" : node->value; - StyledSpan span; - span.text = "[ " + label + " ]"; - span.fg = colors::INPUT_FOCUS; - span.bg = colors::INPUT_BG; - span.attrs = ATTR_BOLD; - span.field_index = node->field_index; - line.spans.push_back(span); - } else if (input_type == "checkbox") { - // 复选框: [x] 或 [ ] - StyledSpan span; - span.text = node->checked ? "[x]" : "[ ]"; - span.fg = colors::INPUT_FOCUS; - span.field_index = node->field_index; - line.spans.push_back(span); - - // 添加标签(如果有name) - if (!node->name.empty()) { - StyledSpan label; - label.text = " " + node->name; - label.fg = colors::FG_PRIMARY; - line.spans.push_back(label); - } - } else if (input_type == "radio") { - // 单选框: (o) 或 ( ) - StyledSpan span; - span.text = node->checked ? "(o)" : "( )"; - span.fg = colors::INPUT_FOCUS; - span.field_index = node->field_index; - line.spans.push_back(span); - - if (!node->name.empty()) { - StyledSpan label; - label.text = " " + node->name; - label.fg = colors::FG_PRIMARY; - line.spans.push_back(label); - } - } else { - // 文本输入框: [placeholder____] - std::string display_text; - if (!node->value.empty()) { - display_text = node->value; - } else if (!node->placeholder.empty()) { - display_text = node->placeholder; - } else { - display_text = ""; - } - - // 限制显示宽度 - int field_width = 20; - if (display_text.length() > static_cast<size_t>(field_width)) { - display_text = display_text.substr(0, field_width - 1) + "…"; - } else { - display_text += std::string(field_width - display_text.length(), '_'); - } - - StyledSpan span; - span.text = "[" + display_text + "]"; - span.fg = node->value.empty() ? colors::FG_DIM : colors::FG_PRIMARY; - span.bg = colors::INPUT_BG; - span.field_index = node->field_index; - line.spans.push_back(span); - } - } else if (node->element_type == ElementType::BUTTON) { - // 按钮 - std::string label = node->get_all_text(); - if (label.empty()) { - label = node->value.empty() ? "Button" : node->value; - } - StyledSpan span; - span.text = "[ " + label + " ]"; - span.fg = colors::INPUT_FOCUS; - span.bg = colors::INPUT_BG; - span.attrs = ATTR_BOLD; - span.field_index = node->field_index; - line.spans.push_back(span); - } else if (node->element_type == ElementType::TEXTAREA) { - // 文本区域 - std::string content = node->value.empty() ? node->placeholder : node->value; - if (content.empty()) { - content = "(empty)"; - } - - StyledSpan span; - span.text = "[" + content + "]"; - span.fg = colors::FG_PRIMARY; - span.bg = colors::INPUT_BG; - span.field_index = node->field_index; - line.spans.push_back(span); - } else if (node->element_type == ElementType::SELECT) { - // 下拉选择 - 显示当前选中的选项 - StyledSpan span; - std::string selected_text = "Select"; - if (node->selected_option >= 0 && node->selected_option < static_cast<int>(node->options.size())) { - selected_text = node->options[node->selected_option].second; - } - span.text = "[▼ " + selected_text + "]"; - span.fg = colors::INPUT_FOCUS; - span.bg = colors::INPUT_BG; - span.field_index = node->field_index; - line.spans.push_back(span); - } - - if (!line.spans.empty()) { - block.lines.push_back(line); - blocks.push_back(block); - } -} - -void LayoutEngine::layout_image_element(const DomNode* node, Context& /*ctx*/, std::vector<LayoutBlock>& blocks) { - LayoutBlock block; - block.type = ElementType::IMAGE; - block.margin_top = 0; - block.margin_bottom = 1; - - // 检查是否有解码后的图片数据 - if (node->image_data.is_valid()) { - // 渲染 ASCII Art - ImageRenderer renderer; - renderer.set_mode(ImageRenderer::Mode::BLOCKS); - renderer.set_color_enabled(true); - - // 计算图片最大尺寸(留出左边距) - int max_width = content_width_; - int max_height = 30; // 限制高度 - - // 如果节点指定了尺寸,使用更小的值 - if (node->img_width > 0) { - max_width = std::min(max_width, node->img_width); - } - if (node->img_height > 0) { - max_height = std::min(max_height, node->img_height / 2); // 考虑字符高宽比 - } - - AsciiImage ascii = renderer.render(node->image_data, max_width, max_height); - - if (!ascii.lines.empty()) { - for (size_t i = 0; i < ascii.lines.size(); ++i) { - LayoutLine line; - line.indent = MARGIN_LEFT; - - // 将每一行作为一个 span - // 但由于颜色可能不同,需要逐字符处理 - const std::string& line_text = ascii.lines[i]; - const std::vector<uint32_t>& line_colors = ascii.colors[i]; - - // 为了效率,尝试合并相同颜色的字符 - size_t pos = 0; - while (pos < line_text.size()) { - // 获取当前字符的字节数(UTF-8) - int char_bytes = 1; - unsigned char c = line_text[pos]; - if ((c & 0x80) == 0) { - char_bytes = 1; - } else if ((c & 0xE0) == 0xC0) { - char_bytes = 2; - } else if ((c & 0xF0) == 0xE0) { - char_bytes = 3; - } else if ((c & 0xF8) == 0xF0) { - char_bytes = 4; - } - - // 获取颜色索引(基于显示宽度位置) - size_t color_idx = 0; - for (size_t j = 0; j < pos; ) { - unsigned char ch = line_text[j]; - int bytes = 1; - if ((ch & 0x80) == 0) bytes = 1; - else if ((ch & 0xE0) == 0xC0) bytes = 2; - else if ((ch & 0xF0) == 0xE0) bytes = 3; - else if ((ch & 0xF8) == 0xF0) bytes = 4; - color_idx++; - j += bytes; - } - - uint32_t color = (color_idx < line_colors.size()) ? line_colors[color_idx] : colors::FG_PRIMARY; - - StyledSpan span; - span.text = line_text.substr(pos, char_bytes); - span.fg = color; - span.attrs = ATTR_NONE; - line.spans.push_back(span); - - pos += char_bytes; - } - - block.lines.push_back(line); - } - blocks.push_back(block); - return; - } - } - - // 回退到占位符 - LayoutLine line; - line.indent = MARGIN_LEFT; - - std::string placeholder = make_image_placeholder(node->alt_text, node->img_src); - - StyledSpan span; - span.text = placeholder; - span.fg = colors::FG_DIM; // 使用较暗的颜色表示占位符 - span.attrs = ATTR_NONE; - - line.spans.push_back(span); - block.lines.push_back(line); - blocks.push_back(block); -} - -// 辅助函数:检查是否需要在两个文本之间添加空格 -static bool needs_space_between(const std::string& prev, const std::string& next) { - if (prev.empty() || next.empty()) return false; - - char last_char = prev.back(); - char first_char = next.front(); - - // 检查前一个是否以空白结尾 - bool prev_ends_with_space = (last_char == ' ' || last_char == '\t' || - last_char == '\n' || last_char == '\r'); - if (prev_ends_with_space) return false; - - // 检查当前是否以空白开头 - bool curr_starts_with_space = (first_char == ' ' || first_char == '\t' || - first_char == '\n' || first_char == '\r'); - if (curr_starts_with_space) return false; - - // 检查是否是标点符号(不需要空格) - bool is_punct = (first_char == '.' || first_char == ',' || - first_char == '!' || first_char == '?' || - first_char == ':' || first_char == ';' || - first_char == ')' || first_char == ']' || - first_char == '}' || first_char == '|' || - first_char == '\'' || first_char == '"'); - if (is_punct) return false; - - // 检查前一个字符是否是特殊符号(不需要空格) - bool prev_is_open = (last_char == '(' || last_char == '[' || - last_char == '{' || last_char == '\'' || - last_char == '"'); - if (prev_is_open) return false; - - return true; -} - -void LayoutEngine::collect_inline_content(const DomNode* node, Context& ctx, std::vector<StyledSpan>& spans) { - if (!node) return; - - if (node->node_type == NodeType::TEXT) { - layout_text(node, ctx, spans); - return; - } - - if (node->is_inline_element() || node->node_type == NodeType::ELEMENT) { - // 设置样式 - uint32_t fg = get_element_fg_color(node->element_type); - uint8_t attrs = get_element_attrs(node->element_type); - - // 处理链接 - int link_idx = node->link_index; - - // 递归处理子节点 - for (size_t i = 0; i < node->children.size(); ++i) { - const auto& child = node->children[i]; - - if (child->node_type == NodeType::TEXT) { - std::string text = child->text_content; - - // 检查是否需要在之前的内容和当前内容之间添加空格 - if (!spans.empty() && !text.empty()) { - if (needs_space_between(spans.back().text, text)) { - spans.back().text += " "; - } - } - - StyledSpan span; - span.text = text; - span.fg = fg; - span.attrs = attrs; - span.link_index = link_idx; - - if (ctx.in_blockquote) { - span.fg = colors::QUOTE_FG; - } - - spans.push_back(span); - } else if (!child->is_block_element()) { - // 获取子节点的全部文本,用于检查是否需要空格 - std::string child_text = child->get_all_text(); - - // 在递归调用前检查空格 - if (!spans.empty() && !child_text.empty()) { - if (needs_space_between(spans.back().text, child_text)) { - spans.back().text += " "; - } - } - - collect_inline_content(child.get(), ctx, spans); - } - } - } -} - -void LayoutEngine::layout_text(const DomNode* node, Context& ctx, std::vector<StyledSpan>& spans) { - if (!node || node->text_content.empty()) return; - - std::string text = node->text_content; - - // 检查是否需要在之前的内容和当前内容之间添加空格 - if (!spans.empty() && !text.empty()) { - if (needs_space_between(spans.back().text, text)) { - spans.back().text += " "; - } - } - - StyledSpan span; - span.text = text; - span.fg = colors::FG_PRIMARY; - - if (ctx.in_blockquote) { - span.fg = colors::QUOTE_FG; - } - - spans.push_back(span); -} - -std::vector<LayoutLine> LayoutEngine::wrap_text(const std::vector<StyledSpan>& spans, int available_width, int indent) { - std::vector<LayoutLine> lines; - - if (spans.empty()) { - return lines; - } - - LayoutLine current_line; - current_line.indent = indent; - size_t current_width = 0; - bool is_line_start = true; // 整行的开始标记 - - for (const auto& span : spans) { - // 分词处理 - std::istringstream iss(span.text); - std::string word; - bool first_word_in_span = true; - - while (iss >> word) { - size_t word_width = Unicode::display_width(word); - - // 检查是否需要换行 - if (current_width > 0 && current_width + 1 + word_width > static_cast<size_t>(available_width)) { - // 当前行已满,开始新行 - if (!current_line.spans.empty()) { - lines.push_back(current_line); - } - current_line = LayoutLine(); - current_line.indent = indent; - current_width = 0; - is_line_start = true; - } - - // 添加空格(如果不是行首且不是第一个单词) - // 需要在不同 span 之间也添加空格 - if (!is_line_start) { - // 检查是否需要空格(避免在标点前加空格) - char first_char = word.front(); - bool is_punct = (first_char == '.' || first_char == ',' || - first_char == '!' || first_char == '?' || - first_char == ':' || first_char == ';' || - first_char == ')' || first_char == ']' || - first_char == '}' || first_char == '|'); - if (!is_punct && !current_line.spans.empty()) { - current_line.spans.back().text += " "; - current_width += 1; - } - } - - // 添加单词 - StyledSpan word_span = span; - word_span.text = word; - current_line.spans.push_back(word_span); - current_width += word_width; - is_line_start = false; - first_word_in_span = false; - } - } - - // 添加最后一行 - if (!current_line.spans.empty()) { - lines.push_back(current_line); - } - - return lines; -} - -uint32_t LayoutEngine::get_element_fg_color(ElementType type) const { - switch (type) { - case ElementType::HEADING1: - return colors::H1_FG; - case ElementType::HEADING2: - return colors::H2_FG; - case ElementType::HEADING3: - case ElementType::HEADING4: - case ElementType::HEADING5: - case ElementType::HEADING6: - return colors::H3_FG; - case ElementType::LINK: - return colors::LINK_FG; - case ElementType::CODE_BLOCK: - return colors::CODE_FG; - case ElementType::BLOCKQUOTE: - return colors::QUOTE_FG; - default: - return colors::FG_PRIMARY; - } -} - -uint8_t LayoutEngine::get_element_attrs(ElementType type) const { - switch (type) { - case ElementType::HEADING1: - case ElementType::HEADING2: - case ElementType::HEADING3: - case ElementType::HEADING4: - case ElementType::HEADING5: - case ElementType::HEADING6: - return ATTR_BOLD; - case ElementType::LINK: - return ATTR_UNDERLINE; - default: - return ATTR_NONE; - } -} - -std::string LayoutEngine::get_list_marker(int depth, bool ordered, int counter) const { - if (ordered) { - return std::to_string(counter) + ". "; - } - - // 不同层级使用不同的标记 - switch ((depth - 1) % 3) { - case 0: return std::string(chars::BULLET) + " "; - case 1: return std::string(chars::BULLET_HOLLOW) + " "; - case 2: return std::string(chars::BULLET_SQUARE) + " "; - default: return std::string(chars::BULLET) + " "; - } -} - -// ==================== DocumentRenderer ==================== - -DocumentRenderer::DocumentRenderer(FrameBuffer& buffer) - : buffer_(buffer) -{ -} - -void DocumentRenderer::render(const LayoutResult& layout, int scroll_offset, const RenderContext& ctx) { - int buffer_height = buffer_.height(); - int y = 0; // 缓冲区行位置 - int doc_line = 0; // 文档行位置 - - for (const auto& block : layout.blocks) { - // 处理上边距 - for (int i = 0; i < block.margin_top; ++i) { - if (doc_line >= scroll_offset && y < buffer_height) { - // 空行 - y++; - } - doc_line++; - } - - // 渲染内容行 - for (const auto& line : block.lines) { - if (doc_line >= scroll_offset) { - if (y >= buffer_height) { - return; // 超出视口 - } - render_line(line, y, doc_line, ctx); - y++; - } - doc_line++; - } - - // 处理下边距 - for (int i = 0; i < block.margin_bottom; ++i) { - if (doc_line >= scroll_offset && y < buffer_height) { - // 空行 - y++; - } - doc_line++; - } - } -} - -int DocumentRenderer::find_match_at(const SearchContext* search, int doc_line, int col) const { - if (!search || !search->enabled || search->matches.empty()) { - return -1; - } - - for (size_t i = 0; i < search->matches.size(); ++i) { - const auto& m = search->matches[i]; - if (m.line == doc_line && col >= m.start_col && col < m.start_col + m.length) { - return static_cast<int>(i); - } - } - return -1; -} - -void DocumentRenderer::render_line(const LayoutLine& line, int y, int doc_line, const RenderContext& ctx) { - int x = line.indent; - - for (const auto& span : line.spans) { - // 检查是否需要搜索高亮 - bool has_search_match = (ctx.search && ctx.search->enabled && !ctx.search->matches.empty()); - - if (has_search_match) { - // 按字符渲染以支持部分高亮 - const std::string& text = span.text; - int char_col = x; - - for (size_t i = 0; i < text.size(); ) { - // 获取字符宽度(处理UTF-8) - int char_bytes = 1; - unsigned char c = text[i]; - if ((c & 0x80) == 0) { - char_bytes = 1; - } else if ((c & 0xE0) == 0xC0) { - char_bytes = 2; - } else if ((c & 0xF0) == 0xE0) { - char_bytes = 3; - } else if ((c & 0xF8) == 0xF0) { - char_bytes = 4; - } - - std::string ch = text.substr(i, char_bytes); - int char_width = static_cast<int>(Unicode::display_width(ch)); - - uint32_t fg = span.fg; - uint32_t bg = span.bg; - uint8_t attrs = span.attrs; - - // 检查搜索匹配 - int match_idx = find_match_at(ctx.search, doc_line, char_col); - if (match_idx >= 0) { - // 搜索高亮 - if (match_idx == ctx.search->current_match_idx) { - fg = colors::SEARCH_CURRENT_FG; - bg = colors::SEARCH_CURRENT_BG; - } else { - fg = colors::SEARCH_MATCH_FG; - bg = colors::SEARCH_MATCH_BG; - } - attrs |= ATTR_BOLD; - } else if (span.link_index >= 0 && span.link_index == ctx.active_link) { - // 活跃链接高亮 - fg = colors::LINK_ACTIVE; - attrs |= ATTR_BOLD; - } else if (span.field_index >= 0 && span.field_index == ctx.active_field) { - // 活跃表单字段高亮 - fg = colors::SEARCH_CURRENT_FG; - bg = colors::INPUT_FOCUS; - attrs |= ATTR_BOLD; - } - - buffer_.set_text(char_col, y, ch, fg, bg, attrs); - char_col += char_width; - i += char_bytes; - } - x = char_col; - } else { - // 无搜索匹配时,整体渲染(更高效) - uint32_t fg = span.fg; - uint32_t bg = span.bg; - uint8_t attrs = span.attrs; - - // 高亮活跃链接 - if (span.link_index >= 0 && span.link_index == ctx.active_link) { - fg = colors::LINK_ACTIVE; - attrs |= ATTR_BOLD; - } - // 高亮活跃表单字段 - else if (span.field_index >= 0 && span.field_index == ctx.active_field) { - fg = colors::SEARCH_CURRENT_FG; - bg = colors::INPUT_FOCUS; - attrs |= ATTR_BOLD; - } - - buffer_.set_text(x, y, span.text, fg, bg, attrs); - x += static_cast<int>(span.display_width()); - } - } -} - -} // namespace tut diff --git a/src/render/layout.h b/src/render/layout.h deleted file mode 100644 index b51029c..0000000 --- a/src/render/layout.h +++ /dev/null @@ -1,197 +0,0 @@ -#pragma once - -#include "renderer.h" -#include "colors.h" -#include "../dom_tree.h" -#include "../utils/unicode.h" -#include <vector> -#include <string> -#include <memory> - -namespace tut { - -/** - * StyledSpan - 带样式的文本片段 - * - * 表示一段具有统一样式的文本 - */ -struct StyledSpan { - std::string text; - uint32_t fg = colors::FG_PRIMARY; - uint32_t bg = colors::BG_PRIMARY; - uint8_t attrs = ATTR_NONE; - int link_index = -1; // -1表示非链接 - int field_index = -1; // -1表示非表单字段 - - size_t display_width() const { - return Unicode::display_width(text); - } -}; - -/** - * LayoutLine - 布局行 - * - * 表示一行渲染内容,由多个StyledSpan组成 - */ -struct LayoutLine { - std::vector<StyledSpan> spans; - int indent = 0; // 行首缩进(字符数) - bool is_blank = false; - - size_t total_width() const { - size_t width = indent; - for (const auto& span : spans) { - width += span.display_width(); - } - return width; - } -}; - -/** - * LayoutBlock - 布局块 - * - * 表示一个块级元素的布局结果 - * 如段落、标题、列表项等 - */ -struct LayoutBlock { - std::vector<LayoutLine> lines; - int margin_top = 0; // 上边距(行数) - int margin_bottom = 0; // 下边距(行数) - ElementType type = ElementType::PARAGRAPH; -}; - -/** - * LinkPosition - 链接位置信息 - */ -struct LinkPosition { - int start_line; // 起始行 - int end_line; // 结束行(可能跨多行) -}; - -/** - * LayoutResult - 布局结果 - * - * 整个文档的布局结果 - */ -struct LayoutResult { - std::vector<LayoutBlock> blocks; - int total_lines = 0; // 总行数(包括边距) - std::string title; - std::string url; - - // 链接位置映射 (link_index -> LinkPosition) - std::vector<LinkPosition> link_positions; - - // 表单字段位置映射 (field_index -> line_number) - std::vector<int> field_lines; -}; - -/** - * LayoutEngine - 布局引擎 - * - * 将DOM树转换为布局结果 - */ -class LayoutEngine { -public: - explicit LayoutEngine(int viewport_width); - - /** - * 计算文档布局 - */ - LayoutResult layout(const DocumentTree& doc); - - /** - * 设置视口宽度 - */ - void set_viewport_width(int width) { viewport_width_ = width; } - -private: - int viewport_width_; - int content_width_; // 实际内容宽度(视口宽度减去边距) - static constexpr int MARGIN_LEFT = 2; - static constexpr int MARGIN_RIGHT = 2; - - // 布局上下文 - struct Context { - int list_depth = 0; - int ordered_list_counter = 0; - bool in_blockquote = false; - bool in_pre = false; - }; - - // 布局处理方法 - void layout_node(const DomNode* node, Context& ctx, std::vector<LayoutBlock>& blocks); - void layout_block_element(const DomNode* node, Context& ctx, std::vector<LayoutBlock>& blocks); - void layout_form_element(const DomNode* node, Context& ctx, std::vector<LayoutBlock>& blocks); - void layout_image_element(const DomNode* node, Context& ctx, std::vector<LayoutBlock>& blocks); - void layout_text(const DomNode* node, Context& ctx, std::vector<StyledSpan>& spans); - - // 收集内联内容 - void collect_inline_content(const DomNode* node, Context& ctx, std::vector<StyledSpan>& spans); - - // 文本换行 - std::vector<LayoutLine> wrap_text(const std::vector<StyledSpan>& spans, int available_width, int indent = 0); - - // 获取元素样式 - uint32_t get_element_fg_color(ElementType type) const; - uint8_t get_element_attrs(ElementType type) const; - - // 获取列表标记 - std::string get_list_marker(int depth, bool ordered, int counter) const; -}; - -/** - * SearchMatch - 搜索匹配信息 - */ -struct SearchMatch { - int line; // 文档行号 - int start_col; // 行内起始列 - int length; // 匹配长度 -}; - -/** - * SearchContext - 搜索上下文 - */ -struct SearchContext { - std::vector<SearchMatch> matches; - int current_match_idx = -1; // 当前高亮的匹配索引 - bool enabled = false; -}; - -/** - * RenderContext - 渲染上下文 - */ -struct RenderContext { - int active_link = -1; // 当前活跃链接索引 - int active_field = -1; // 当前活跃表单字段索引 - const SearchContext* search = nullptr; // 搜索上下文 -}; - -/** - * DocumentRenderer - 文档渲染器 - * - * 将LayoutResult渲染到FrameBuffer - */ -class DocumentRenderer { -public: - explicit DocumentRenderer(FrameBuffer& buffer); - - /** - * 渲染布局结果到缓冲区 - * - * @param layout 布局结果 - * @param scroll_offset 滚动偏移(行数) - * @param ctx 渲染上下文 - */ - void render(const LayoutResult& layout, int scroll_offset, const RenderContext& ctx = {}); - -private: - FrameBuffer& buffer_; - - void render_line(const LayoutLine& line, int y, int doc_line, const RenderContext& ctx); - - // 检查位置是否在搜索匹配中 - int find_match_at(const SearchContext* search, int doc_line, int col) const; -}; - -} // namespace tut diff --git a/src/render/renderer.cpp b/src/render/renderer.cpp deleted file mode 100644 index 0313dfa..0000000 --- a/src/render/renderer.cpp +++ /dev/null @@ -1,227 +0,0 @@ -#include "renderer.h" -#include "../utils/unicode.h" - -namespace tut { - -// ============================================================================ -// FrameBuffer Implementation -// ============================================================================ - -FrameBuffer::FrameBuffer(int width, int height) - : width_(width), height_(height) { - empty_cell_.content = " "; - resize(width, height); -} - -void FrameBuffer::resize(int width, int height) { - width_ = width; - height_ = height; - cells_.resize(height); - for (auto& row : cells_) { - row.resize(width, empty_cell_); - } -} - -void FrameBuffer::clear() { - for (auto& row : cells_) { - std::fill(row.begin(), row.end(), empty_cell_); - } -} - -void FrameBuffer::clear_with_color(uint32_t bg) { - Cell cell = empty_cell_; - cell.bg = bg; - for (auto& row : cells_) { - std::fill(row.begin(), row.end(), cell); - } -} - -void FrameBuffer::set_cell(int x, int y, const Cell& cell) { - if (x >= 0 && x < width_ && y >= 0 && y < height_) { - cells_[y][x] = cell; - } -} - -const Cell& FrameBuffer::get_cell(int x, int y) const { - if (x >= 0 && x < width_ && y >= 0 && y < height_) { - return cells_[y][x]; - } - return empty_cell_; -} - -void FrameBuffer::set_text(int x, int y, const std::string& text, - uint32_t fg, uint32_t bg, uint8_t attrs) { - if (y < 0 || y >= height_) return; - - size_t i = 0; - int cur_x = x; - - while (i < text.length() && cur_x < width_) { - if (cur_x < 0) { - // Skip characters before visible area - i += Unicode::char_byte_length(text, i); - cur_x++; - continue; - } - - size_t byte_len = Unicode::char_byte_length(text, i); - std::string ch = text.substr(i, byte_len); - - // Determine character width - size_t char_width = 1; - unsigned char c = text[i]; - if ((c & 0xF0) == 0xE0 || (c & 0xF8) == 0xF0) { - char_width = 2; // CJK or emoji - } - - Cell cell; - cell.content = ch; - cell.fg = fg; - cell.bg = bg; - cell.attrs = attrs; - - set_cell(cur_x, y, cell); - - // For wide characters, mark next cell as placeholder - if (char_width == 2 && cur_x + 1 < width_) { - Cell placeholder; - placeholder.content = ""; // Empty = continuation of previous cell - placeholder.fg = fg; - placeholder.bg = bg; - placeholder.attrs = attrs; - set_cell(cur_x + 1, y, placeholder); - } - - cur_x += char_width; - i += byte_len; - } -} - -// ============================================================================ -// Renderer Implementation -// ============================================================================ - -Renderer::Renderer(Terminal& terminal) - : terminal_(terminal), prev_buffer_(1, 1) { - int w, h; - terminal_.get_size(w, h); - prev_buffer_.resize(w, h); -} - -void Renderer::render(const FrameBuffer& buffer) { - int w = buffer.width(); - int h = buffer.height(); - - // Check if resize needed - if (prev_buffer_.width() != w || prev_buffer_.height() != h) { - prev_buffer_.resize(w, h); - need_full_redraw_ = true; - } - - terminal_.hide_cursor(); - - uint32_t last_fg = 0xFFFFFFFF; // Invalid color to force first set - uint32_t last_bg = 0xFFFFFFFF; - uint8_t last_attrs = 0xFF; - int last_x = -2; - - // 批量输出缓冲 - std::string batch_text; - int batch_start_x = 0; - int batch_y = 0; - uint32_t batch_fg = 0; - uint32_t batch_bg = 0; - uint8_t batch_attrs = 0; - - auto flush_batch = [&]() { - if (batch_text.empty()) return; - - terminal_.move_cursor(batch_start_x, batch_y); - - if (batch_fg != last_fg) { - terminal_.set_foreground(batch_fg); - last_fg = batch_fg; - } - if (batch_bg != last_bg) { - terminal_.set_background(batch_bg); - last_bg = batch_bg; - } - if (batch_attrs != last_attrs) { - terminal_.reset_attributes(); - if (batch_attrs & ATTR_BOLD) terminal_.set_bold(true); - if (batch_attrs & ATTR_ITALIC) terminal_.set_italic(true); - if (batch_attrs & ATTR_UNDERLINE) terminal_.set_underline(true); - if (batch_attrs & ATTR_REVERSE) terminal_.set_reverse(true); - if (batch_attrs & ATTR_DIM) terminal_.set_dim(true); - last_attrs = batch_attrs; - terminal_.set_foreground(batch_fg); - terminal_.set_background(batch_bg); - } - - terminal_.print(batch_text); - batch_text.clear(); - }; - - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - const Cell& cell = buffer.get_cell(x, y); - const Cell& prev = prev_buffer_.get_cell(x, y); - - // Skip if unchanged and not forcing redraw - if (!need_full_redraw_ && cell == prev) { - flush_batch(); - last_x = -2; - continue; - } - - // Skip placeholder cells (continuation of wide chars) - if (cell.content.empty()) { - continue; - } - - // 检查是否可以添加到批量输出 - bool can_batch = (y == batch_y) && - (x == last_x + 1 || batch_text.empty()) && - (cell.fg == batch_fg || batch_text.empty()) && - (cell.bg == batch_bg || batch_text.empty()) && - (cell.attrs == batch_attrs || batch_text.empty()); - - if (!can_batch) { - flush_batch(); - batch_start_x = x; - batch_y = y; - batch_fg = cell.fg; - batch_bg = cell.bg; - batch_attrs = cell.attrs; - } - - batch_text += cell.content; - last_x = x; - } - - // 行末刷新 - flush_batch(); - last_x = -2; - } - - flush_batch(); - - terminal_.reset_colors(); - terminal_.reset_attributes(); - terminal_.refresh(); - - // Copy current buffer to previous for next diff - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - const_cast<FrameBuffer&>(prev_buffer_).set_cell(x, y, buffer.get_cell(x, y)); - } - } - - need_full_redraw_ = false; -} - -void Renderer::force_redraw() { - need_full_redraw_ = true; -} - -} // namespace tut diff --git a/src/render/renderer.h b/src/render/renderer.h deleted file mode 100644 index 906d438..0000000 --- a/src/render/renderer.h +++ /dev/null @@ -1,103 +0,0 @@ -#pragma once - -#include "terminal.h" -#include <vector> -#include <string> -#include <cstdint> - -namespace tut { - -/** - * 文本属性位标志 - */ -enum CellAttr : uint8_t { - ATTR_NONE = 0, - ATTR_BOLD = 1 << 0, - ATTR_ITALIC = 1 << 1, - ATTR_UNDERLINE = 1 << 2, - ATTR_REVERSE = 1 << 3, - ATTR_DIM = 1 << 4 -}; - -/** - * Cell - 单个字符单元格 - * - * 存储一个UTF-8字符及其渲染属性 - */ -struct Cell { - std::string content; // UTF-8字符(可能1-4字节) - uint32_t fg = 0xD0D0D0; // 前景色 (默认浅灰) - uint32_t bg = 0x1A1A1A; // 背景色 (默认深灰) - uint8_t attrs = ATTR_NONE; - - bool operator==(const Cell& other) const { - return content == other.content && - fg == other.fg && - bg == other.bg && - attrs == other.attrs; - } - - bool operator!=(const Cell& other) const { - return !(*this == other); - } -}; - -/** - * FrameBuffer - 帧缓冲区 - * - * 双缓冲渲染:维护当前帧和上一帧,只渲染变化的部分 - */ -class FrameBuffer { -public: - FrameBuffer(int width, int height); - - void resize(int width, int height); - void clear(); - void clear_with_color(uint32_t bg); - - void set_cell(int x, int y, const Cell& cell); - const Cell& get_cell(int x, int y) const; - - // 便捷方法:设置文本(处理宽字符) - void set_text(int x, int y, const std::string& text, uint32_t fg, uint32_t bg, uint8_t attrs = ATTR_NONE); - - int width() const { return width_; } - int height() const { return height_; } - -private: - std::vector<std::vector<Cell>> cells_; - int width_; - int height_; - Cell empty_cell_; -}; - -/** - * Renderer - 渲染器 - * - * 负责将FrameBuffer的内容渲染到终端 - * 实现差分渲染以提高性能 - */ -class Renderer { -public: - explicit Renderer(Terminal& terminal); - - /** - * 渲染帧缓冲区到终端 - * 使用差分算法只更新变化的部分 - */ - void render(const FrameBuffer& buffer); - - /** - * 强制全屏重绘 - */ - void force_redraw(); - -private: - Terminal& terminal_; - FrameBuffer prev_buffer_; // 上一帧,用于差分渲染 - bool need_full_redraw_ = true; - - void apply_cell_style(const Cell& cell); -}; - -} // namespace tut diff --git a/src/render/terminal.cpp b/src/render/terminal.cpp deleted file mode 100644 index 3e5a3c1..0000000 --- a/src/render/terminal.cpp +++ /dev/null @@ -1,418 +0,0 @@ -#include "terminal.h" -#include <ncurses.h> -#include <cstdlib> -#include <cstdio> -#include <cstring> -#include <locale.h> - -namespace tut { - -// ==================== Terminal::Impl ==================== - -class Terminal::Impl { -public: - Impl() - : initialized_(false) - , has_true_color_(false) - , has_mouse_(false) - , has_unicode_(false) - , has_italic_(false) - , width_(0) - , height_(0) - , mouse_enabled_(false) - {} - - ~Impl() { - if (initialized_) { - cleanup(); - } - } - - bool init() { - if (initialized_) { - return true; - } - - // 设置locale以支持UTF-8 - setlocale(LC_ALL, ""); - - // 初始化ncurses - initscr(); - if (stdscr == nullptr) { - return false; - } - - // 基础设置 - raw(); // 禁用行缓冲 - noecho(); // 不回显输入 - keypad(stdscr, TRUE); // 启用功能键 - nodelay(stdscr, TRUE); // 非阻塞输入(默认) - - // 检测终端能力 - detect_capabilities(); - - // 获取屏幕尺寸 - getmaxyx(stdscr, height_, width_); - - // 隐藏光标(默认) - curs_set(0); - - // 启用鼠标支持 - if (has_mouse_) { - enable_mouse(true); - } - - // 使用替代屏幕缓冲区 - use_alternate_screen(true); - - initialized_ = true; - return true; - } - - void cleanup() { - if (!initialized_) { - return; - } - - // 恢复光标 - curs_set(1); - - // 禁用鼠标 - if (mouse_enabled_) { - enable_mouse(false); - } - - // 退出替代屏幕 - use_alternate_screen(false); - - // 清理ncurses - endwin(); - - initialized_ = false; - } - - void detect_capabilities() { - // 检测True Color支持 - const char* colorterm = std::getenv("COLORTERM"); - has_true_color_ = (colorterm != nullptr && - (std::strcmp(colorterm, "truecolor") == 0 || - std::strcmp(colorterm, "24bit") == 0)); - - // 检测鼠标支持 - has_mouse_ = has_mouse(); - - // 检测Unicode支持(通过locale) - const char* lang = std::getenv("LANG"); - has_unicode_ = (lang != nullptr && - (std::strstr(lang, "UTF-8") != nullptr || - std::strstr(lang, "utf8") != nullptr)); - - // 检测斜体支持(大多数现代终端支持) - const char* term = std::getenv("TERM"); - has_italic_ = (term != nullptr && - (std::strstr(term, "xterm") != nullptr || - std::strstr(term, "screen") != nullptr || - std::strstr(term, "tmux") != nullptr || - std::strstr(term, "kitty") != nullptr || - std::strstr(term, "alacritty") != nullptr)); - } - - void get_size(int& width, int& height) { - // 每次调用时获取最新尺寸,以支持窗口大小调整 - getmaxyx(stdscr, height_, width_); - width = width_; - height = height_; - } - - void clear() { - // 使用 ANSI 转义码清屏并移动光标到左上角 - std::printf("\033[2J\033[H"); - std::fflush(stdout); - } - - void refresh() { - // ANSI 模式下不需要 ncurses 的 refresh,只需 flush - std::fflush(stdout); - } - - // ==================== True Color ==================== - - void set_foreground(uint32_t rgb) { - if (has_true_color_) { - // ANSI escape: ESC[38;2;R;G;Bm - int r = (rgb >> 16) & 0xFF; - int g = (rgb >> 8) & 0xFF; - int b = rgb & 0xFF; - std::printf("\033[38;2;%d;%d;%dm", r, g, b); - std::fflush(stdout); - } else { - // 降级到基础色(简化映射) - // 这里可以实现256色或8色的映射 - // 暂时使用默认色 - } - } - - void set_background(uint32_t rgb) { - if (has_true_color_) { - // ANSI escape: ESC[48;2;R;G;Bm - int r = (rgb >> 16) & 0xFF; - int g = (rgb >> 8) & 0xFF; - int b = rgb & 0xFF; - std::printf("\033[48;2;%d;%d;%dm", r, g, b); - std::fflush(stdout); - } - } - - void reset_colors() { - // ESC[39m 重置前景色, ESC[49m 重置背景色 - std::printf("\033[39m\033[49m"); - std::fflush(stdout); - } - - // ==================== 文本属性 ==================== - - void set_bold(bool enabled) { - if (enabled) { - std::printf("\033[1m"); // ESC[1m - } else { - std::printf("\033[22m"); // ESC[22m (normal intensity) - } - std::fflush(stdout); - } - - void set_italic(bool enabled) { - if (!has_italic_) return; - - if (enabled) { - std::printf("\033[3m"); // ESC[3m - } else { - std::printf("\033[23m"); // ESC[23m - } - std::fflush(stdout); - } - - void set_underline(bool enabled) { - if (enabled) { - std::printf("\033[4m"); // ESC[4m - } else { - std::printf("\033[24m"); // ESC[24m - } - std::fflush(stdout); - } - - void set_reverse(bool enabled) { - if (enabled) { - std::printf("\033[7m"); // ESC[7m - } else { - std::printf("\033[27m"); // ESC[27m - } - std::fflush(stdout); - } - - void set_dim(bool enabled) { - if (enabled) { - std::printf("\033[2m"); // ESC[2m - } else { - std::printf("\033[22m"); // ESC[22m - } - std::fflush(stdout); - } - - void reset_attributes() { - std::printf("\033[0m"); // ESC[0m (reset all) - std::fflush(stdout); - } - - // ==================== 光标控制 ==================== - - void move_cursor(int x, int y) { - // 使用 ANSI 转义码移动光标(和 printf 输出兼容) - // 注意:ANSI 坐标是 1-indexed,所以需要 +1 - std::printf("\033[%d;%dH", y + 1, x + 1); - std::fflush(stdout); - } - - void hide_cursor() { - std::printf("\033[?25l"); // DECTCEM: 隐藏光标 - std::fflush(stdout); - } - - void show_cursor() { - std::printf("\033[?25h"); // DECTCEM: 显示光标 - std::fflush(stdout); - } - - // ==================== 文本输出 ==================== - - void print(const std::string& text) { - // 直接输出到stdout(配合ANSI escape sequences) - std::printf("%s", text.c_str()); - std::fflush(stdout); - } - - void print_at(int x, int y, const std::string& text) { - move_cursor(x, y); - print(text); - } - - // ==================== 输入处理 ==================== - - int get_key(int timeout_ms) { - if (timeout_ms == -1) { - // 阻塞等待 - nodelay(stdscr, FALSE); - int ch = getch(); - nodelay(stdscr, TRUE); - return ch; - } else if (timeout_ms == 0) { - // 非阻塞 - return getch(); - } else { - // 超时等待 - timeout(timeout_ms); - int ch = getch(); - nodelay(stdscr, TRUE); - return ch; - } - } - - bool get_mouse_event(MouseEvent& event) { - if (!mouse_enabled_) { - return false; - } - - MEVENT mevent; - int ch = getch(); - - if (ch == KEY_MOUSE) { - if (getmouse(&mevent) == OK) { - event.x = mevent.x; - event.y = mevent.y; - - // 解析鼠标事件类型 - if (mevent.bstate & BUTTON1_CLICKED) { - event.type = MouseEvent::Type::CLICK; - event.button = 0; - return true; - } else if (mevent.bstate & BUTTON2_CLICKED) { - event.type = MouseEvent::Type::CLICK; - event.button = 1; - return true; - } else if (mevent.bstate & BUTTON3_CLICKED) { - event.type = MouseEvent::Type::CLICK; - event.button = 2; - return true; - } -#ifdef BUTTON4_PRESSED - else if (mevent.bstate & BUTTON4_PRESSED) { - event.type = MouseEvent::Type::SCROLL_UP; - return true; - } -#endif -#ifdef BUTTON5_PRESSED - else if (mevent.bstate & BUTTON5_PRESSED) { - event.type = MouseEvent::Type::SCROLL_DOWN; - return true; - } -#endif - } - } - - return false; - } - - // ==================== 终端能力 ==================== - - bool supports_true_color() const { return has_true_color_; } - bool supports_mouse() const { return has_mouse_; } - bool supports_unicode() const { return has_unicode_; } - bool supports_italic() const { return has_italic_; } - - // ==================== 高级功能 ==================== - - void enable_mouse(bool enabled) { - if (enabled) { - // 启用所有鼠标事件 - mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, nullptr); - // 发送启用鼠标跟踪的ANSI序列 - std::printf("\033[?1003h"); // 启用所有鼠标事件 - std::fflush(stdout); - mouse_enabled_ = true; - } else { - mousemask(0, nullptr); - std::printf("\033[?1003l"); // 禁用鼠标跟踪 - std::fflush(stdout); - mouse_enabled_ = false; - } - } - - void use_alternate_screen(bool enabled) { - if (enabled) { - std::printf("\033[?1049h"); // 进入替代屏幕 - } else { - std::printf("\033[?1049l"); // 退出替代屏幕 - } - std::fflush(stdout); - } - -private: - bool initialized_; - bool has_true_color_; - bool has_mouse_; - bool has_unicode_; - bool has_italic_; - int width_; - int height_; - bool mouse_enabled_; -}; - -// ==================== Terminal 公共接口 ==================== - -Terminal::Terminal() : pImpl(std::make_unique<Impl>()) {} -Terminal::~Terminal() = default; - -bool Terminal::init() { return pImpl->init(); } -void Terminal::cleanup() { pImpl->cleanup(); } - -void Terminal::get_size(int& width, int& height) { - pImpl->get_size(width, height); -} -void Terminal::clear() { pImpl->clear(); } -void Terminal::refresh() { pImpl->refresh(); } - -void Terminal::set_foreground(uint32_t rgb) { pImpl->set_foreground(rgb); } -void Terminal::set_background(uint32_t rgb) { pImpl->set_background(rgb); } -void Terminal::reset_colors() { pImpl->reset_colors(); } - -void Terminal::set_bold(bool enabled) { pImpl->set_bold(enabled); } -void Terminal::set_italic(bool enabled) { pImpl->set_italic(enabled); } -void Terminal::set_underline(bool enabled) { pImpl->set_underline(enabled); } -void Terminal::set_reverse(bool enabled) { pImpl->set_reverse(enabled); } -void Terminal::set_dim(bool enabled) { pImpl->set_dim(enabled); } -void Terminal::reset_attributes() { pImpl->reset_attributes(); } - -void Terminal::move_cursor(int x, int y) { pImpl->move_cursor(x, y); } -void Terminal::hide_cursor() { pImpl->hide_cursor(); } -void Terminal::show_cursor() { pImpl->show_cursor(); } - -void Terminal::print(const std::string& text) { pImpl->print(text); } -void Terminal::print_at(int x, int y, const std::string& text) { - pImpl->print_at(x, y, text); -} - -int Terminal::get_key(int timeout_ms) { return pImpl->get_key(timeout_ms); } -bool Terminal::get_mouse_event(MouseEvent& event) { - return pImpl->get_mouse_event(event); -} - -bool Terminal::supports_true_color() const { return pImpl->supports_true_color(); } -bool Terminal::supports_mouse() const { return pImpl->supports_mouse(); } -bool Terminal::supports_unicode() const { return pImpl->supports_unicode(); } -bool Terminal::supports_italic() const { return pImpl->supports_italic(); } - -void Terminal::enable_mouse(bool enabled) { pImpl->enable_mouse(enabled); } -void Terminal::use_alternate_screen(bool enabled) { - pImpl->use_alternate_screen(enabled); -} - -} // namespace tut diff --git a/src/render/terminal.h b/src/render/terminal.h deleted file mode 100644 index 42bce9c..0000000 --- a/src/render/terminal.h +++ /dev/null @@ -1,218 +0,0 @@ -#pragma once - -#include <string> -#include <cstdint> -#include <memory> - -namespace tut { - -// 鼠标事件类型 -struct MouseEvent { - enum class Type { - CLICK, - SCROLL_UP, - SCROLL_DOWN, - MOVE, - DRAG - }; - - Type type; - int x; - int y; - int button; // 0=left, 1=middle, 2=right -}; - -/** - * Terminal - 现代终端抽象层 - * - * 提供True Color (24-bit RGB)支持的终端接口 - * 目标终端: iTerm2, Kitty, Alacritty等现代终端 - * - * 设计理念: - * - 优先使用ANSI escape sequences而非ncurses color pairs (突破256色限制) - * - 检测终端能力并自动降级 - * - 提供清晰的、面向对象的API - */ -class Terminal { -public: - Terminal(); - ~Terminal(); - - // ==================== 初始化与清理 ==================== - - /** - * 初始化终端 - * - 设置原始模式 - * - 检测终端能力 - * - 启用鼠标支持(如果可用) - * @return 是否成功初始化 - */ - bool init(); - - /** - * 清理并恢复终端状态 - */ - void cleanup(); - - // ==================== 屏幕管理 ==================== - - /** - * 获取终端尺寸(每次调用都会获取最新尺寸) - */ - void get_size(int& width, int& height); - - /** - * 清空屏幕 - */ - void clear(); - - /** - * 刷新显示(将缓冲区内容显示到屏幕) - */ - void refresh(); - - // ==================== True Color 支持 ==================== - - /** - * 设置前景色 (24-bit RGB) - * @param rgb RGB颜色值,格式: 0xRRGGBB - * 示例: 0xE8C48C (暖金色) - */ - void set_foreground(uint32_t rgb); - - /** - * 设置背景色 (24-bit RGB) - * @param rgb RGB颜色值,格式: 0xRRGGBB - */ - void set_background(uint32_t rgb); - - /** - * 重置颜色为默认值 - */ - void reset_colors(); - - // ==================== 文本属性 ==================== - - /** - * 设置粗体 - */ - void set_bold(bool enabled); - - /** - * 设置斜体 - */ - void set_italic(bool enabled); - - /** - * 设置下划线 - */ - void set_underline(bool enabled); - - /** - * 设置反色显示 - */ - void set_reverse(bool enabled); - - /** - * 设置暗淡显示 - */ - void set_dim(bool enabled); - - /** - * 重置所有文本属性 - */ - void reset_attributes(); - - // ==================== 光标控制 ==================== - - /** - * 移动光标到指定位置 - * @param x 列位置 (0-based) - * @param y 行位置 (0-based) - */ - void move_cursor(int x, int y); - - /** - * 隐藏光标 - */ - void hide_cursor(); - - /** - * 显示光标 - */ - void show_cursor(); - - // ==================== 文本输出 ==================== - - /** - * 在当前光标位置输出文本 - */ - void print(const std::string& text); - - /** - * 在指定位置输出文本 - */ - void print_at(int x, int y, const std::string& text); - - // ==================== 输入处理 ==================== - - /** - * 获取按键 - * @param timeout_ms 超时时间(毫秒),-1表示阻塞等待 - * @return 按键代码,超时返回-1 - */ - int get_key(int timeout_ms = -1); - - /** - * 获取鼠标事件 - * @param event 输出参数,存储鼠标事件 - * @return 是否成功获取鼠标事件 - */ - bool get_mouse_event(MouseEvent& event); - - // ==================== 终端能力检测 ==================== - - /** - * 是否支持True Color (24-bit) - * 检测方法: 环境变量 COLORTERM=truecolor 或 COLORTERM=24bit - */ - bool supports_true_color() const; - - /** - * 是否支持鼠标 - */ - bool supports_mouse() const; - - /** - * 是否支持Unicode - */ - bool supports_unicode() const; - - /** - * 是否支持斜体 - */ - bool supports_italic() const; - - // ==================== 高级功能 ==================== - - /** - * 启用/禁用鼠标支持 - */ - void enable_mouse(bool enabled); - - /** - * 启用/禁用替代屏幕缓冲区 - * (用于全屏应用,退出时恢复原屏幕内容) - */ - void use_alternate_screen(bool enabled); - -private: - class Impl; - std::unique_ptr<Impl> pImpl; - - // 禁止拷贝 - Terminal(const Terminal&) = delete; - Terminal& operator=(const Terminal&) = delete; -}; - -} // namespace tut diff --git a/test_browser.sh b/test_browser.sh deleted file mode 100755 index f3a0e4a..0000000 --- a/test_browser.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/bash - -# TUT Browser Interactive Test Script -# This script helps test the browser with various real websites - -echo "========================================" -echo " TUT 2.0 Browser Interactive Testing" -echo "========================================" -echo "" -echo "This script will help you test the browser with real websites." -echo "Press Ctrl+C to exit at any time." -echo "" - -# Build the browser -echo "Building the browser..." -cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug > /dev/null 2>&1 -cmake --build build > /dev/null 2>&1 - -if [ $? -ne 0 ]; then - echo "❌ Build failed!" - exit 1 -fi - -echo "✓ Build successful!" -echo "" - -# Test sites -declare -a sites=( - "http://example.com" - "http://info.cern.ch" - "http://motherfuckingwebsite.com" - "http://textfiles.com" -) - -echo "Available test sites:" -echo "1. example.com - Simple static page" -echo "2. info.cern.ch - First website ever (historical)" -echo "3. motherfuckingwebsite.com - Minimalist design manifesto" -echo "4. textfiles.com - Text-only content" -echo "5. Custom URL" -echo "" - -read -p "Select a site (1-5): " choice - -case $choice in - 1) url="${sites[0]}" ;; - 2) url="${sites[1]}" ;; - 3) url="${sites[2]}" ;; - 4) url="${sites[3]}" ;; - 5) - read -p "Enter URL (include http://): " url - ;; - *) - echo "Invalid choice, using example.com" - url="${sites[0]}" - ;; -esac - -echo "" -echo "========================================" -echo " Launching TUT Browser" -echo "========================================" -echo "URL: $url" -echo "" -echo "Keyboard shortcuts:" -echo " j/k - Scroll up/down" -echo " Tab - Next link/field" -echo " Enter - Follow link/activate field" -echo " i - Focus first form field" -echo " / - Search" -echo " h/l - Back/Forward" -echo " B - Bookmark" -echo " :o URL - Open URL" -echo " :q - Quit" -echo "" -read -p "Press Enter to start..." - -# Launch the browser -./build/tut "$url" - -echo "" -echo "Browser exited. Test complete!" diff --git a/test_form.html b/test_form.html deleted file mode 100644 index eea86d0..0000000 --- a/test_form.html +++ /dev/null @@ -1,39 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <title>Form Test - - -

Test Form

- -
-

Name:

-

Email:

-

Password:

- -

Country:

- -

Age Group:

- -

Subscribe to newsletter

-

I agree to terms

- -

-

- - -

Links

-

Example Link

- - diff --git a/test_real_world.sh b/test_real_world.sh deleted file mode 100755 index d159f08..0000000 --- a/test_real_world.sh +++ /dev/null @@ -1,143 +0,0 @@ -#!/bin/bash -# Real-world browser testing script -# Tests TUT with various website types and provides UX feedback - -echo "════════════════════════════════════════════════════════════" -echo " TUT Real-World Browser Testing" -echo "════════════════════════════════════════════════════════════" -echo "" -echo "This script will test TUT with various website types:" -echo "" -echo "1. News sites (text-heavy, many images)" -echo "2. Documentation (code blocks, technical content)" -echo "3. Simple static sites (basic HTML)" -echo "4. Image galleries (many concurrent images)" -echo "5. Forums/discussions (mixed content)" -echo "" -echo "For each site, we'll evaluate:" -echo " • Loading speed and responsiveness" -echo " • Image loading behavior" -echo " • Content readability" -echo " • Navigation smoothness" -echo " • Overall user experience" -echo "" -echo "════════════════════════════════════════════════════════════" -echo "" - -# Test categories -declare -A SITES - -# Category 1: News/Content sites -SITES["news_hn"]="https://news.ycombinator.com" -SITES["news_lobsters"]="https://lobste.rs" - -# Category 2: Documentation -SITES["doc_curl"]="https://curl.se/docs/manpage.html" -SITES["doc_rust"]="https://doc.rust-lang.org/book/ch01-01-installation.html" - -# Category 3: Simple/Static -SITES["simple_example"]="https://example.com" -SITES["simple_motherfuckingwebsite"]="https://motherfuckingwebsite.com" - -# Category 4: Wikipedia (images + content) -SITES["wiki_unix"]="https://en.wikipedia.org/wiki/Unix" -SITES["wiki_web"]="https://en.wikipedia.org/wiki/World_Wide_Web" - -# Category 5: Tech blogs -SITES["blog_lwn"]="https://lwn.net" - -echo "Available test sites:" -echo "" -echo "News & Content:" -echo " 1. Hacker News - ${SITES[news_hn]}" -echo " 2. Lobsters - ${SITES[news_lobsters]}" -echo "" -echo "Documentation:" -echo " 3. curl manual - ${SITES[doc_curl]}" -echo " 4. Rust Book - ${SITES[doc_rust]}" -echo "" -echo "Simple Sites:" -echo " 5. Example.com - ${SITES[simple_example]}" -echo " 6. Motherfucking Web - ${SITES[simple_motherfuckingwebsite]}" -echo "" -echo "Wikipedia:" -echo " 7. Unix - ${SITES[wiki_unix]}" -echo " 8. World Wide Web - ${SITES[wiki_web]}" -echo "" -echo "Tech News:" -echo " 9. LWN.net - ${SITES[blog_lwn]}" -echo "" -echo "════════════════════════════════════════════════════════════" -echo "" - -# Function to test a site -test_site() { - local name=$1 - local url=$2 - - echo "" - echo "──────────────────────────────────────────────────────────" - echo "Testing: $name" - echo "URL: $url" - echo "──────────────────────────────────────────────────────────" - echo "" - echo "Starting browser... (Press 'q' to quit and move to next)" - echo "" - - ./build/tut "$url" - - echo "" - echo "Test completed for: $name" - echo "" -} - -# Interactive mode -echo "Select test mode:" -echo " a) Test all sites automatically" -echo " m) Manual site selection" -echo " q) Quit" -echo "" -read -p "Choice: " choice - -case $choice in - a) - echo "" - echo "Running automated tests..." - echo "Note: Each site will open. Press 'q' to move to next." - echo "" - sleep 2 - - test_site "Hacker News" "${SITES[news_hn]}" - test_site "Example.com" "${SITES[simple_example]}" - test_site "Wikipedia - Unix" "${SITES[wiki_unix]}" - ;; - m) - echo "" - read -p "Enter site number (1-9): " num - case $num in - 1) test_site "Hacker News" "${SITES[news_hn]}" ;; - 2) test_site "Lobsters" "${SITES[news_lobsters]}" ;; - 3) test_site "curl manual" "${SITES[doc_curl]}" ;; - 4) test_site "Rust Book" "${SITES[doc_rust]}" ;; - 5) test_site "Example.com" "${SITES[simple_example]}" ;; - 6) test_site "Motherfucking Website" "${SITES[simple_motherfuckingwebsite]}" ;; - 7) test_site "Wikipedia - Unix" "${SITES[wiki_unix]}" ;; - 8) test_site "Wikipedia - WWW" "${SITES[wiki_web]}" ;; - 9) test_site "LWN.net" "${SITES[blog_lwn]}" ;; - *) echo "Invalid selection" ;; - esac - ;; - q) - echo "Exiting..." - exit 0 - ;; - *) - echo "Invalid choice" - exit 1 - ;; -esac - -echo "" -echo "════════════════════════════════════════════════════════════" -echo " Testing Complete!" -echo "════════════════════════════════════════════════════════════" diff --git a/tests/test_async_images.cpp b/tests/test_async_images.cpp deleted file mode 100644 index 213cdec..0000000 --- a/tests/test_async_images.cpp +++ /dev/null @@ -1,122 +0,0 @@ -#include "../src/http_client.h" -#include -#include -#include -#include - -void test_async_image_downloads() { - std::cout << "Testing async image downloads...\n"; - - HttpClient client; - - // Add multiple image downloads - // Using small test images from httpbin.org - const char* test_images[] = { - "https://httpbin.org/image/png", - "https://httpbin.org/image/jpeg", - "https://httpbin.org/image/webp" - }; - - // Add images to queue - for (int i = 0; i < 3; i++) { - client.add_image_download(test_images[i], (void*)(intptr_t)i); - std::cout << " Queued: " << test_images[i] << "\n"; - } - - std::cout << " Pending: " << client.get_pending_image_count() << "\n"; - assert(client.get_pending_image_count() == 3); - - // Poll until all images are downloaded - int iterations = 0; - int max_iterations = 200; // 10 seconds max (50ms * 200) - - while ((client.get_pending_image_count() > 0 || - client.get_loading_image_count() > 0) && - iterations < max_iterations) { - client.poll_image_downloads(); - - // Check for completed images - auto completed = client.get_completed_images(); - for (const auto& img : completed) { - std::cout << " Downloaded: " << img.url - << " (status: " << img.status_code - << ", size: " << img.data.size() << " bytes)\n"; - } - - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - iterations++; - } - - std::cout << " Completed after " << iterations << " iterations\n"; - std::cout << " Final state - Pending: " << client.get_pending_image_count() - << ", Loading: " << client.get_loading_image_count() << "\n"; - - std::cout << "✓ Async image download test passed!\n\n"; -} - -void test_image_cancellation() { - std::cout << "Testing image download cancellation...\n"; - - HttpClient client; - - // Add images - client.add_image_download("https://httpbin.org/image/png", nullptr); - client.add_image_download("https://httpbin.org/image/jpeg", nullptr); - - std::cout << " Queued 2 images\n"; - - // Start downloads - client.poll_image_downloads(); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - // Cancel all - std::cout << " Cancelling all downloads\n"; - client.cancel_all_images(); - - assert(client.get_pending_image_count() == 0); - assert(client.get_loading_image_count() == 0); - - std::cout << "✓ Image cancellation test passed!\n\n"; -} - -void test_concurrent_limit() { - std::cout << "Testing concurrent download limit...\n"; - - HttpClient client; - client.set_max_concurrent_images(2); - - // Add 5 images - for (int i = 0; i < 5; i++) { - client.add_image_download("https://httpbin.org/delay/1", nullptr); - } - - std::cout << " Queued 5 images with max_concurrent=2\n"; - assert(client.get_pending_image_count() == 5); - - // Start downloads - client.poll_image_downloads(); - - // Should have max 2 loading at a time - int loading = client.get_loading_image_count(); - std::cout << " Loading: " << loading << " (should be <= 2)\n"; - assert(loading <= 2); - - client.cancel_all_images(); - std::cout << "✓ Concurrent limit test passed!\n\n"; -} - -int main() { - std::cout << "=== Async Image Download Tests ===\n\n"; - - try { - test_async_image_downloads(); - test_image_cancellation(); - test_concurrent_limit(); - - std::cout << "=== All tests passed! ===\n"; - return 0; - } catch (const std::exception& e) { - std::cerr << "Test failed: " << e.what() << "\n"; - return 1; - } -} diff --git a/tests/test_bookmark.cpp b/tests/test_bookmark.cpp deleted file mode 100644 index 1cb05ba..0000000 --- a/tests/test_bookmark.cpp +++ /dev/null @@ -1,103 +0,0 @@ -#include "bookmark.h" -#include -#include - -int main() { - std::cout << "=== TUT 2.0 Bookmark Test ===" << std::endl; - - // Note: Uses default path ~/.config/tut/bookmarks.json - // We'll test in-memory operations and clean up - - tut::BookmarkManager manager; - - // Store original count to restore later - size_t original_count = manager.count(); - std::cout << " Original bookmark count: " << original_count << std::endl; - - // Test 1: Add bookmarks - std::cout << "\n[Test 1] Add bookmarks..." << std::endl; - - // Use unique URLs to avoid conflicts with existing bookmarks - std::string test_url1 = "https://test-example-12345.com"; - std::string test_url2 = "https://test-google-12345.com"; - std::string test_url3 = "https://test-github-12345.com"; - - bool added1 = manager.add(test_url1, "Test Example"); - bool added2 = manager.add(test_url2, "Test Google"); - bool added3 = manager.add(test_url3, "Test GitHub"); - - if (added1 && added2 && added3) { - std::cout << " ✓ Added 3 bookmarks" << std::endl; - } else { - std::cout << " ✗ Failed to add bookmarks" << std::endl; - return 1; - } - - // Test 2: Duplicate detection - std::cout << "\n[Test 2] Duplicate detection..." << std::endl; - - bool duplicate = manager.add(test_url1, "Duplicate"); - if (!duplicate) { - std::cout << " ✓ Duplicate correctly rejected" << std::endl; - } else { - std::cout << " ✗ Duplicate was incorrectly added" << std::endl; - // Clean up and fail - manager.remove(test_url1); - manager.remove(test_url2); - manager.remove(test_url3); - return 1; - } - - // Test 3: Check existence - std::cout << "\n[Test 3] Check existence..." << std::endl; - - if (manager.contains(test_url1) && !manager.contains("https://notexist-12345.com")) { - std::cout << " ✓ Existence check passed" << std::endl; - } else { - std::cout << " ✗ Existence check failed" << std::endl; - manager.remove(test_url1); - manager.remove(test_url2); - manager.remove(test_url3); - return 1; - } - - // Test 4: Count check - std::cout << "\n[Test 4] Count check..." << std::endl; - - if (manager.count() == original_count + 3) { - std::cout << " ✓ Bookmark count correct: " << manager.count() << std::endl; - } else { - std::cout << " ✗ Bookmark count incorrect" << std::endl; - manager.remove(test_url1); - manager.remove(test_url2); - manager.remove(test_url3); - return 1; - } - - // Test 5: Remove bookmark - std::cout << "\n[Test 5] Remove bookmark..." << std::endl; - - bool removed = manager.remove(test_url2); - if (removed && !manager.contains(test_url2) && manager.count() == original_count + 2) { - std::cout << " ✓ Bookmark removed successfully" << std::endl; - } else { - std::cout << " ✗ Bookmark removal failed" << std::endl; - manager.remove(test_url1); - manager.remove(test_url3); - return 1; - } - - // Clean up test bookmarks - std::cout << "\n[Cleanup] Removing test bookmarks..." << std::endl; - manager.remove(test_url1); - manager.remove(test_url3); - - if (manager.count() == original_count) { - std::cout << " ✓ Cleanup successful, restored to " << original_count << " bookmarks" << std::endl; - } else { - std::cout << " ⚠ Cleanup may have issues" << std::endl; - } - - std::cout << "\n=== All bookmark tests passed! ===" << std::endl; - return 0; -} diff --git a/tests/test_history.cpp b/tests/test_history.cpp deleted file mode 100644 index 6dae435..0000000 --- a/tests/test_history.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include "history.h" -#include -#include -#include -#include - -using namespace tut; - -int main() { - std::cout << "=== TUT 2.0 History Test ===" << std::endl; - - // 记录初始状态 - HistoryManager manager; - size_t initial_count = manager.count(); - std::cout << " Original history count: " << initial_count << std::endl; - - // Test 1: 添加历史记录 - std::cout << "\n[Test 1] Add history entries..." << std::endl; - manager.add("https://example.com", "Example Site"); - manager.add("https://test.com", "Test Site"); - manager.add("https://demo.com", "Demo Site"); - - if (manager.count() == initial_count + 3) { - std::cout << " ✓ Added 3 entries" << std::endl; - } else { - std::cout << " ✗ Failed to add entries" << std::endl; - return 1; - } - - // Test 2: 重复 URL 更新 - std::cout << "\n[Test 2] Duplicate URL update..." << std::endl; - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - manager.add("https://example.com", "Example Site Updated"); - - // 计数应该不变(因为重复的会被移到前面而不是新增) - if (manager.count() == initial_count + 3) { - std::cout << " ✓ Duplicate correctly handled" << std::endl; - } else { - std::cout << " ✗ Duplicate handling failed" << std::endl; - return 1; - } - - // Test 3: 最新在前面 - std::cout << "\n[Test 3] Most recent first..." << std::endl; - const auto& entries = manager.get_all(); - if (!entries.empty() && entries[0].url == "https://example.com") { - std::cout << " ✓ Most recent entry is first" << std::endl; - } else { - std::cout << " ✗ Order incorrect" << std::endl; - return 1; - } - - // Test 4: 持久化 - std::cout << "\n[Test 4] Persistence..." << std::endl; - { - HistoryManager manager2; // 创建新实例会加载 - if (manager2.count() >= initial_count + 3) { - std::cout << " ✓ History persisted to file" << std::endl; - } else { - std::cout << " ✗ Persistence failed" << std::endl; - return 1; - } - } - - // Cleanup: 移除测试条目 - std::cout << "\n[Cleanup] Removing test entries..." << std::endl; - HistoryManager cleanup_manager; - // 由于我们没有删除单条的方法,这里只验证功能 - // 在实际使用中,历史会随着时间自然过期 - - std::cout << "\n=== All history tests passed! ===" << std::endl; - return 0; -} diff --git a/tests/test_html_parse.cpp b/tests/test_html_parse.cpp deleted file mode 100644 index ed7d6f1..0000000 --- a/tests/test_html_parse.cpp +++ /dev/null @@ -1,129 +0,0 @@ -#include "html_parser.h" -#include "dom_tree.h" -#include -#include - -int main() { - std::cout << "=== TUT 2.0 HTML Parser Test ===" << std::endl; - - HtmlParser parser; - - // Test 1: Basic HTML parsing - std::cout << "\n[Test 1] Basic HTML parsing..." << std::endl; - std::string html1 = R"( - - - Test Page - -

Hello World

-

This is a link.

- - - )"; - - auto tree1 = parser.parse_tree(html1, "https://test.com"); - std::cout << " ✓ Title: " << tree1.title << std::endl; - std::cout << " ✓ Links found: " << tree1.links.size() << std::endl; - - if (tree1.title == "Test Page" && tree1.links.size() == 1) { - std::cout << " ✓ Basic parsing passed" << std::endl; - } else { - std::cout << " ✗ Basic parsing failed" << std::endl; - return 1; - } - - // Test 2: Link URL resolution - std::cout << "\n[Test 2] Link URL resolution..." << std::endl; - std::string html2 = R"( - - - Relative - Absolute - Same dir - - - )"; - - auto tree2 = parser.parse_tree(html2, "https://base.com/dir/"); - std::cout << " Found " << tree2.links.size() << " links:" << std::endl; - for (const auto& link : tree2.links) { - std::cout << " - " << link.url << std::endl; - } - - if (tree2.links.size() == 3) { - std::cout << " ✓ Link resolution passed" << std::endl; - } else { - std::cout << " ✗ Link resolution failed" << std::endl; - return 1; - } - - // Test 3: Form parsing - std::cout << "\n[Test 3] Form parsing..." << std::endl; - std::string html3 = R"( - - -
- - - - - - - )"; - - auto tree3 = parser.parse_tree(html3, "https://form.com"); - std::cout << " Form fields found: " << tree3.form_fields.size() << std::endl; - - if (tree3.form_fields.size() >= 2) { - std::cout << " ✓ Form parsing passed" << std::endl; - } else { - std::cout << " ✗ Form parsing failed" << std::endl; - return 1; - } - - // Test 4: Image parsing - std::cout << "\n[Test 4] Image parsing..." << std::endl; - std::string html4 = R"( - - - Image 1 - Image 2 - - - )"; - - auto tree4 = parser.parse_tree(html4, "https://images.com/page/"); - std::cout << " Images found: " << tree4.images.size() << std::endl; - - if (tree4.images.size() == 2) { - std::cout << " ✓ Image parsing passed" << std::endl; - } else { - std::cout << " ✗ Image parsing failed" << std::endl; - return 1; - } - - // Test 5: Unicode content - std::cout << "\n[Test 5] Unicode content..." << std::endl; - std::string html5 = R"( - - 中文标题 - -

日本語テスト

-

한국어 테스트

- - - )"; - - auto tree5 = parser.parse_tree(html5, "https://unicode.com"); - std::cout << " ✓ Title: " << tree5.title << std::endl; - - if (tree5.title == "中文标题") { - std::cout << " ✓ Unicode parsing passed" << std::endl; - } else { - std::cout << " ✗ Unicode parsing failed" << std::endl; - return 1; - } - - std::cout << "\n=== All HTML parser tests passed! ===" << std::endl; - return 0; -} diff --git a/tests/test_http_async.cpp b/tests/test_http_async.cpp deleted file mode 100644 index 37de242..0000000 --- a/tests/test_http_async.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include "http_client.h" -#include -#include -#include - -int main() { - std::cout << "=== TUT 2.0 HTTP Async Test ===" << std::endl; - - HttpClient client; - - // Test 1: Synchronous fetch - std::cout << "\n[Test 1] Synchronous fetch..." << std::endl; - auto response = client.fetch("https://example.com"); - if (response.is_success()) { - std::cout << " ✓ Status: " << response.status_code << std::endl; - std::cout << " ✓ Content-Type: " << response.content_type << std::endl; - std::cout << " ✓ Body length: " << response.body.length() << " bytes" << std::endl; - } else { - std::cout << " ✗ Failed: " << response.error_message << std::endl; - return 1; - } - - // Test 2: Asynchronous fetch - std::cout << "\n[Test 2] Asynchronous fetch..." << std::endl; - client.start_async_fetch("https://example.com"); - - int polls = 0; - auto start = std::chrono::steady_clock::now(); - - while (true) { - auto state = client.poll_async(); - polls++; - - if (state == AsyncState::COMPLETE) { - auto end = std::chrono::steady_clock::now(); - auto ms = std::chrono::duration_cast(end - start).count(); - - auto result = client.get_async_result(); - std::cout << " ✓ Completed in " << ms << "ms after " << polls << " polls" << std::endl; - std::cout << " ✓ Status: " << result.status_code << std::endl; - std::cout << " ✓ Body length: " << result.body.length() << " bytes" << std::endl; - break; - } else if (state == AsyncState::FAILED) { - auto result = client.get_async_result(); - std::cout << " ✗ Failed: " << result.error_message << std::endl; - return 1; - } else if (state == AsyncState::LOADING) { - // Non-blocking poll - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } else { - std::cout << " ✗ Unexpected state" << std::endl; - return 1; - } - - if (polls > 1000) { - std::cout << " ✗ Timeout" << std::endl; - return 1; - } - } - - // Test 3: Cancel async - std::cout << "\n[Test 3] Cancel async..." << std::endl; - client.start_async_fetch("https://httpbin.org/delay/10"); - - // Poll a few times then cancel - for (int i = 0; i < 5; i++) { - client.poll_async(); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - - client.cancel_async(); - std::cout << " ✓ Request cancelled" << std::endl; - - // Verify state is CANCELLED or IDLE - if (!client.is_async_active()) { - std::cout << " ✓ No active request after cancel" << std::endl; - } else { - std::cout << " ✗ Request still active after cancel" << std::endl; - return 1; - } - - std::cout << "\n=== All tests passed! ===" << std::endl; - return 0; -} diff --git a/tests/test_image_minimal.cpp b/tests/test_image_minimal.cpp deleted file mode 100644 index 1a09552..0000000 --- a/tests/test_image_minimal.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "../src/http_client.h" -#include -#include -#include - -int main() { - std::cout << "=== Minimal Async Image Test ===\n"; - - try { - HttpClient client; - std::cout << "1. Client created\n"; - - client.add_image_download("https://httpbin.org/image/png", nullptr); - std::cout << "2. Image queued\n"; - std::cout << " Pending: " << client.get_pending_image_count() << "\n"; - - std::cout << "3. First poll...\n"; - client.poll_image_downloads(); - std::cout << " After poll - Pending: " << client.get_pending_image_count() - << ", Loading: " << client.get_loading_image_count() << "\n"; - - std::cout << "4. Wait a bit...\n"; - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - - std::cout << "5. Second poll...\n"; - client.poll_image_downloads(); - std::cout << " After poll - Pending: " << client.get_pending_image_count() - << ", Loading: " << client.get_loading_image_count() << "\n"; - - std::cout << "6. Get completed...\n"; - auto completed = client.get_completed_images(); - std::cout << " Completed: " << completed.size() << "\n"; - - std::cout << "7. Destroying client...\n"; - } catch (const std::exception& e) { - std::cerr << "ERROR: " << e.what() << "\n"; - return 1; - } - - std::cout << "8. Test completed successfully!\n"; - return 0; -} diff --git a/tests/test_layout.cpp b/tests/test_layout.cpp deleted file mode 100644 index 28a0cd0..0000000 --- a/tests/test_layout.cpp +++ /dev/null @@ -1,269 +0,0 @@ -/** - * test_layout.cpp - Layout引擎测试 - * - * 测试内容: - * 1. DOM树构建 - * 2. 布局计算 - * 3. 文档渲染演示 - */ - -#include "render/terminal.h" -#include "render/renderer.h" -#include "render/layout.h" -#include "render/colors.h" -#include "dom_tree.h" -#include -#include -#include - -using namespace tut; - -void test_image_placeholder() { - std::cout << "=== 图片占位符测试 ===\n"; - - std::string html = R"( - - -图片测试 - -

图片测试页面

-

下面是一些图片:

- Example Photo -

中间文本

- - Only alt text - - - -)"; - - DomTreeBuilder builder; - DocumentTree doc = builder.build(html, "test://"); - - LayoutEngine engine(80); - LayoutResult layout = engine.layout(doc); - - std::cout << "图片测试 - 总块数: " << layout.blocks.size() << "\n"; - std::cout << "图片测试 - 总行数: " << layout.total_lines << "\n"; - - // 检查渲染输出 - int img_count = 0; - for (const auto& block : layout.blocks) { - if (block.type == ElementType::IMAGE) { - img_count++; - if (!block.lines.empty() && !block.lines[0].spans.empty()) { - std::cout << " 图片 " << img_count << ": " << block.lines[0].spans[0].text << "\n"; - } - } - } - std::cout << "找到 " << img_count << " 个图片块\n\n"; -} - -void test_layout_basic() { - std::cout << "=== Layout 基础测试 ===\n"; - - // 测试HTML - std::string html = R"( - - -测试页面 - -

TUT 2.0 布局引擎测试

-

这是一个段落,用于测试文本换行功能。当文本超过视口宽度时,应该自动换行到下一行。

-

列表测试

-
    -
  • 无序列表项目 1
  • -
  • 无序列表项目 2
  • -
  • 无序列表项目 3
  • -
-

链接测试

-

这是一个 链接示例,点击可以访问。

-
这是一段引用文本,应该带有左边框标记。
-
-

页面结束。

- - -)"; - - // 构建DOM树 - DomTreeBuilder builder; - DocumentTree doc = builder.build(html, "test://"); - std::cout << "DOM树构建: OK\n"; - std::cout << "标题: " << doc.title << "\n"; - std::cout << "链接数: " << doc.links.size() << "\n"; - - // 布局计算 - LayoutEngine engine(80); - LayoutResult layout = engine.layout(doc); - std::cout << "布局计算: OK\n"; - std::cout << "布局块数: " << layout.blocks.size() << "\n"; - std::cout << "总行数: " << layout.total_lines << "\n"; - - // 打印布局块信息 - std::cout << "\n布局块详情:\n"; - int block_num = 0; - for (const auto& block : layout.blocks) { - std::cout << " Block " << block_num++ << ": " - << block.lines.size() << " lines, " - << "margin_top=" << block.margin_top << ", " - << "margin_bottom=" << block.margin_bottom << "\n"; - } - - std::cout << "\nLayout 基础测试完成!\n"; -} - -void demo_layout_render(Terminal& term) { - int w, h; - term.get_size(w, h); - - // 创建测试HTML - std::string html = R"( - - -TUT 2.0 布局演示 - -

TUT 2.0 - 终端浏览器

- -

这是一个现代化的终端浏览器,支持 True Color 渲染、Unicode 字符以及差分渲染优化。

- -

主要特性

-
    -
  • True Color 24位色彩支持
  • -
  • Unicode 字符正确显示(包括CJK字符)
  • -
  • 差分渲染提升性能
  • -
  • 温暖护眼的配色方案
  • -
- -

链接示例

-

访问 ExampleGitHub 了解更多信息。

- -

引用块

-
Unix哲学:做一件事,把它做好。
- -
- -

使用 j/k 滚动,q 退出。

- - -)"; - - // 构建DOM树 - DomTreeBuilder builder; - DocumentTree doc = builder.build(html, "demo://"); - - // 布局计算 - LayoutEngine engine(w); - LayoutResult layout = engine.layout(doc); - - // 创建帧缓冲区和渲染器 - FrameBuffer fb(w, h - 2); // 留出状态栏空间 - Renderer renderer(term); - DocumentRenderer doc_renderer(fb); - - int scroll_offset = 0; - int max_scroll = std::max(0, layout.total_lines - (h - 2)); - int active_link = -1; - int num_links = static_cast(doc.links.size()); - - bool running = true; - while (running) { - // 清空缓冲区 - fb.clear_with_color(colors::BG_PRIMARY); - - // 渲染文档 - RenderContext render_ctx; - render_ctx.active_link = active_link; - doc_renderer.render(layout, scroll_offset, render_ctx); - - // 渲染状态栏 - std::string status = layout.title + " | 行 " + std::to_string(scroll_offset + 1) + - "/" + std::to_string(layout.total_lines); - if (active_link >= 0 && active_link < num_links) { - status += " | 链接: " + doc.links[active_link].url; - } - // 截断过长的状态栏 - if (Unicode::display_width(status) > static_cast(w - 2)) { - status = status.substr(0, w - 5) + "..."; - } - - // 状态栏在最后一行 - for (int x = 0; x < w; ++x) { - fb.set_cell(x, h - 2, Cell{" ", colors::STATUSBAR_FG, colors::STATUSBAR_BG, ATTR_NONE}); - } - fb.set_text(1, h - 2, status, colors::STATUSBAR_FG, colors::STATUSBAR_BG); - - // 渲染到终端 - renderer.render(fb); - - // 处理输入 - int key = term.get_key(100); - switch (key) { - case 'q': - case 'Q': - running = false; - break; - case 'j': - case KEY_DOWN: - if (scroll_offset < max_scroll) scroll_offset++; - break; - case 'k': - case KEY_UP: - if (scroll_offset > 0) scroll_offset--; - break; - case ' ': - case KEY_NPAGE: - scroll_offset = std::min(scroll_offset + (h - 3), max_scroll); - break; - case 'b': - case KEY_PPAGE: - scroll_offset = std::max(scroll_offset - (h - 3), 0); - break; - case 'g': - case KEY_HOME: - scroll_offset = 0; - break; - case 'G': - case KEY_END: - scroll_offset = max_scroll; - break; - case '\t': // Tab键切换链接 - if (num_links > 0) { - active_link = (active_link + 1) % num_links; - } - break; - case KEY_BTAB: // Shift+Tab - if (num_links > 0) { - active_link = (active_link - 1 + num_links) % num_links; - } - break; - } - } -} - -int main() { - // 先运行非终端测试 - test_image_placeholder(); - test_layout_basic(); - - std::cout << "\n按回车键进入交互演示 (或 Ctrl+C 退出)...\n"; - std::cin.get(); - - // 交互演示 - Terminal term; - if (!term.init()) { - std::cerr << "终端初始化失败!\n"; - return 1; - } - - term.use_alternate_screen(true); - term.hide_cursor(); - - demo_layout_render(term); - - term.show_cursor(); - term.use_alternate_screen(false); - term.cleanup(); - - std::cout << "Layout 测试完成!\n"; - return 0; -} diff --git a/tests/test_renderer.cpp b/tests/test_renderer.cpp deleted file mode 100644 index 2f46f88..0000000 --- a/tests/test_renderer.cpp +++ /dev/null @@ -1,156 +0,0 @@ -/** - * test_renderer.cpp - FrameBuffer 和 Renderer 测试 - * - * 测试内容: - * 1. Unicode字符宽度计算 - * 2. FrameBuffer操作 - * 3. 差分渲染演示 - */ - -#include "render/terminal.h" -#include "render/renderer.h" -#include "render/colors.h" -#include "render/decorations.h" -#include "utils/unicode.h" -#include -#include -#include - -using namespace tut; - -void test_unicode() { - std::cout << "=== Unicode 测试 ===\n"; - - // 测试用例 - struct TestCase { - std::string text; - size_t expected_width; - const char* description; - }; - - TestCase tests[] = { - {"Hello", 5, "ASCII"}, - {"你好", 4, "中文(2字符,宽度4)"}, - {"Hello世界", 9, "混合ASCII+中文"}, - {"🎉", 2, "Emoji"}, - {"café", 4, "带重音符号"}, - }; - - bool all_passed = true; - for (const auto& tc : tests) { - size_t width = Unicode::display_width(tc.text); - bool pass = (width == tc.expected_width); - std::cout << (pass ? "[OK] " : "[FAIL] ") - << tc.description << ": \"" << tc.text << "\" " - << "width=" << width - << " (expected " << tc.expected_width << ")\n"; - if (!pass) all_passed = false; - } - - std::cout << (all_passed ? "\n所有Unicode测试通过!\n" : "\n部分测试失败!\n"); -} - -void test_framebuffer() { - std::cout << "\n=== FrameBuffer 测试 ===\n"; - - FrameBuffer fb(80, 24); - std::cout << "创建 80x24 FrameBuffer: OK\n"; - - // 测试set_text - fb.set_text(0, 0, "Hello World", colors::FG_PRIMARY, colors::BG_PRIMARY); - std::cout << "set_text ASCII: OK\n"; - - fb.set_text(0, 1, "你好世界", colors::H1_FG, colors::BG_PRIMARY); - std::cout << "set_text 中文: OK\n"; - - // 验证单元格 - const Cell& cell = fb.get_cell(0, 0); - if (cell.content == "H" && cell.fg == colors::FG_PRIMARY) { - std::cout << "get_cell 验证: OK\n"; - } else { - std::cout << "get_cell 验证: FAIL\n"; - } - - std::cout << "FrameBuffer 测试完成!\n"; -} - -void demo_renderer(Terminal& term) { - int w, h; - term.get_size(w, h); - - FrameBuffer fb(w, h); - Renderer renderer(term); - - // 清屏并显示标题 - fb.clear_with_color(colors::BG_PRIMARY); - - // 标题 - std::string title = "TUT 2.0 - Renderer Demo"; - int title_x = (w - Unicode::display_width(title)) / 2; - fb.set_text(title_x, 1, title, colors::H1_FG, colors::BG_PRIMARY, ATTR_BOLD); - - // 分隔线 - std::string line = make_horizontal_line(w - 4, chars::SGL_HORIZONTAL); - fb.set_text(2, 2, line, colors::BORDER, colors::BG_PRIMARY); - - // 颜色示例 - fb.set_text(2, 4, "颜色示例:", colors::FG_PRIMARY, colors::BG_PRIMARY, ATTR_BOLD); - fb.set_text(4, 5, chars::BULLET + std::string(" H1标题色"), colors::H1_FG, colors::BG_PRIMARY); - fb.set_text(4, 6, chars::BULLET + std::string(" H2标题色"), colors::H2_FG, colors::BG_PRIMARY); - fb.set_text(4, 7, chars::BULLET + std::string(" H3标题色"), colors::H3_FG, colors::BG_PRIMARY); - fb.set_text(4, 8, chars::BULLET + std::string(" 链接色"), colors::LINK_FG, colors::BG_PRIMARY); - - // 装饰字符示例 - fb.set_text(2, 10, "装饰字符:", colors::FG_PRIMARY, colors::BG_PRIMARY, ATTR_BOLD); - fb.set_text(4, 11, std::string(chars::DBL_TOP_LEFT) + make_horizontal_line(20, chars::DBL_HORIZONTAL) + chars::DBL_TOP_RIGHT, - colors::BORDER, colors::BG_PRIMARY); - fb.set_text(4, 12, std::string(chars::DBL_VERTICAL) + " 双线边框示例 " + chars::DBL_VERTICAL, - colors::BORDER, colors::BG_PRIMARY); - fb.set_text(4, 13, std::string(chars::DBL_BOTTOM_LEFT) + make_horizontal_line(20, chars::DBL_HORIZONTAL) + chars::DBL_BOTTOM_RIGHT, - colors::BORDER, colors::BG_PRIMARY); - - // Unicode宽度示例 - fb.set_text(2, 15, "Unicode宽度:", colors::FG_PRIMARY, colors::BG_PRIMARY, ATTR_BOLD); - fb.set_text(4, 16, "ASCII: Hello (5)", colors::FG_SECONDARY, colors::BG_PRIMARY); - fb.set_text(4, 17, "中文: 你好世界 (8)", colors::FG_SECONDARY, colors::BG_PRIMARY); - - // 提示 - fb.set_text(2, h - 2, "按 'q' 退出", colors::FG_DIM, colors::BG_PRIMARY); - - // 渲染 - renderer.render(fb); - - // 等待退出 - while (true) { - int key = term.get_key(100); - if (key == 'q' || key == 'Q') break; - } -} - -int main() { - // 先运行非终端测试 - test_unicode(); - test_framebuffer(); - - std::cout << "\n按回车键进入交互演示 (或 Ctrl+C 退出)...\n"; - std::cin.get(); - - // 交互演示 - Terminal term; - if (!term.init()) { - std::cerr << "终端初始化失败!\n"; - return 1; - } - - term.use_alternate_screen(true); - term.hide_cursor(); - - demo_renderer(term); - - term.show_cursor(); - term.use_alternate_screen(false); - term.cleanup(); - - std::cout << "Renderer 测试完成!\n"; - return 0; -} diff --git a/tests/test_simple_image.cpp b/tests/test_simple_image.cpp deleted file mode 100644 index 08b8391..0000000 --- a/tests/test_simple_image.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "../src/http_client.h" -#include - -int main() { - std::cout << "Testing basic image download...\n"; - - try { - HttpClient client; - std::cout << "Client created\n"; - - client.add_image_download("https://httpbin.org/image/png", nullptr); - std::cout << "Image queued\n"; - std::cout << "Pending: " << client.get_pending_image_count() << "\n"; - - std::cout << "Client will be destroyed\n"; - } catch (const std::exception& e) { - std::cerr << "Error: " << e.what() << "\n"; - return 1; - } - - std::cout << "Test completed successfully\n"; - return 0; -} diff --git a/tests/test_terminal.cpp b/tests/test_terminal.cpp deleted file mode 100644 index 9b0068b..0000000 --- a/tests/test_terminal.cpp +++ /dev/null @@ -1,222 +0,0 @@ -/** - * test_terminal.cpp - Terminal类True Color功能测试 - * - * 测试内容: - * 1. True Color (24-bit RGB) 支持 - * 2. 文本属性 (粗体、斜体、下划线) - * 3. Unicode字符显示 - * 4. 终端能力检测 - */ - -#include "terminal.h" -#include -#include -#include - -using namespace tut; - -void test_true_color(Terminal& term) { - term.clear(); - - // 标题 - term.move_cursor(0, 0); - term.set_bold(true); - term.set_foreground(0xE8C48C); // 暖金色 - term.print("TUT 2.0 - True Color Test"); - term.reset_attributes(); - - // 能力检测报告 - int y = 2; - term.move_cursor(0, y++); - term.print("Terminal Capabilities:"); - - term.move_cursor(0, y++); - term.print(" True Color: "); - if (term.supports_true_color()) { - term.set_foreground(0x00FF00); - term.print("✓ Supported"); - } else { - term.set_foreground(0xFF0000); - term.print("✗ Not Supported"); - } - term.reset_colors(); - - term.move_cursor(0, y++); - term.print(" Mouse: "); - if (term.supports_mouse()) { - term.set_foreground(0x00FF00); - term.print("✓ Supported"); - } else { - term.set_foreground(0xFF0000); - term.print("✗ Not Supported"); - } - term.reset_colors(); - - term.move_cursor(0, y++); - term.print(" Unicode: "); - if (term.supports_unicode()) { - term.set_foreground(0x00FF00); - term.print("✓ Supported"); - } else { - term.set_foreground(0xFF0000); - term.print("✗ Not Supported"); - } - term.reset_colors(); - - term.move_cursor(0, y++); - term.print(" Italic: "); - if (term.supports_italic()) { - term.set_foreground(0x00FF00); - term.print("✓ Supported"); - } else { - term.set_foreground(0xFF0000); - term.print("✗ Not Supported"); - } - term.reset_colors(); - - y++; - - // 报纸风格颜色主题测试 - term.move_cursor(0, y++); - term.set_bold(true); - term.print("Newspaper Color Theme:"); - term.reset_attributes(); - - y++; - - // H1 颜色 - term.move_cursor(0, y++); - term.set_bold(true); - term.set_foreground(0xE8C48C); // 暖金色 - term.print(" H1 Heading - Warm Gold (0xE8C48C)"); - term.reset_attributes(); - - // H2 颜色 - term.move_cursor(0, y++); - term.set_bold(true); - term.set_foreground(0xD4B078); // 较暗金色 - term.print(" H2 Heading - Dark Gold (0xD4B078)"); - term.reset_attributes(); - - // H3 颜色 - term.move_cursor(0, y++); - term.set_bold(true); - term.set_foreground(0xC09C64); // 青铜色 - term.print(" H3 Heading - Bronze (0xC09C64)"); - term.reset_attributes(); - - y++; - - // 链接颜色 - term.move_cursor(0, y++); - term.set_foreground(0x87AFAF); // 青色 - term.set_underline(true); - term.print(" Link - Teal (0x87AFAF)"); - term.reset_attributes(); - - // 悬停链接 - term.move_cursor(0, y++); - term.set_foreground(0xA7CFCF); // 浅青色 - term.set_underline(true); - term.print(" Link Hover - Light Teal (0xA7CFCF)"); - term.reset_attributes(); - - y++; - - // 正文颜色 - term.move_cursor(0, y++); - term.set_foreground(0xD0D0D0); // 浅灰 - term.print(" Body Text - Light Gray (0xD0D0D0)"); - term.reset_colors(); - - // 次要文本 - term.move_cursor(0, y++); - term.set_foreground(0x909090); // 中灰 - term.print(" Secondary Text - Medium Gray (0x909090)"); - term.reset_colors(); - - y++; - - // Unicode装饰测试 - term.move_cursor(0, y++); - term.set_bold(true); - term.print("Unicode Box Drawing:"); - term.reset_attributes(); - - y++; - - // 双线框 - term.move_cursor(0, y++); - term.set_foreground(0x404040); - term.print(" ╔═══════════════════════════════════╗"); - term.move_cursor(0, y++); - term.print(" ║ Double Border for H1 Headings ║"); - term.move_cursor(0, y++); - term.print(" ╚═══════════════════════════════════╝"); - term.reset_colors(); - - y++; - - // 单线框 - term.move_cursor(0, y++); - term.set_foreground(0x404040); - term.print(" ┌───────────────────────────────────┐"); - term.move_cursor(0, y++); - term.print(" │ Single Border for Code Blocks │"); - term.move_cursor(0, y++); - term.print(" └───────────────────────────────────┘"); - term.reset_colors(); - - y++; - - // 引用块 - term.move_cursor(0, y++); - term.set_foreground(0x6A8F8F); - term.print(" ┃ Blockquote with heavy vertical bar"); - term.reset_colors(); - - y++; - - // 列表符号 - term.move_cursor(0, y++); - term.print(" • Bullet point (level 1)"); - term.move_cursor(0, y++); - term.print(" ◦ Circle (level 2)"); - term.move_cursor(0, y++); - term.print(" ▪ Square (level 3)"); - - y += 2; - - // 提示 - term.move_cursor(0, y++); - term.set_dim(true); - term.print("Press any key to exit..."); - term.reset_attributes(); - - term.refresh(); -} - -int main() { - Terminal term; - - if (!term.init()) { - std::cerr << "Failed to initialize terminal" << std::endl; - return 1; - } - - try { - test_true_color(term); - - // 等待按键 - term.get_key(-1); - - term.cleanup(); - - } catch (const std::exception& e) { - term.cleanup(); - std::cerr << "Error: " << e.what() << std::endl; - return 1; - } - - return 0; -}