Compare commits

...

7 commits

Author SHA1 Message Date
1233ae52ca docs: Update progress for Phase 9 - testing and optimization
Some checks are pending
Build and Release / build (linux, ubuntu-latest) (push) Waiting to run
Build and Release / build (macos, macos-latest) (push) Waiting to run
Build and Release / release (push) Blocked by required conditions
2025-12-28 02:11:21 +08:00
63fbee6d30 feat: Add comprehensive testing tools and improve help
- Add test_browser.sh interactive testing script
- Add TESTING.md comprehensive testing guide
- Update help text with form interaction details
- Include keyboard shortcuts for text input and dropdowns
- Add instructions for all new features

Improvements:
- Help now shows 'i' key for form focus
- Text input editing instructions
- Dropdown selection navigation guide
- Testing checklist for all features
- Interactive test script for easy website testing
2025-12-28 00:56:17 +08:00
c7c11e08f8 feat: Add image caching to avoid re-downloads
- Add ImageCacheEntry structure with timestamp and expiration
- Implement LRU cache for up to 100 images
- Cache images for 10 minutes (configurable)
- Show cache hit count in status message
- Display "cached: N" when loading images from cache
- Automatically evict oldest images when cache is full
- Improves performance by avoiding redundant downloads

Performance improvements:
- Images are only downloaded once within 10 minutes
- Subsequent page views use cached images
- Significantly faster page load times for image-heavy sites
2025-12-28 00:23:54 +08:00
5e2850f7d3 docs: Update progress for Phase 8 form interactions 2025-12-28 00:05:14 +08:00
58b7607074 feat: Add interactive dropdown selection for forms
- Parse and store OPTION elements in SELECT fields
- Display selected option text in dropdown UI
- Add SELECT_OPTION input mode for dropdown navigation
- Support Enter on SELECT to enter selection mode
- Use j/k or arrow keys to navigate through options
- Enter to confirm selection, Esc to cancel
- Auto-select first option or option marked with 'selected'
- Real-time option preview in status bar
- Status bar shows "-- SELECT --" mode

Data structure:
- Added options vector to DomNode (value, text pairs)
- Added selected_option index to track current selection

Keyboard shortcuts in SELECT mode:
- j/Down: Next option
- k/Up: Previous option
- Enter: Select current option
- Esc: Cancel selection
2025-12-28 00:03:39 +08:00
7e55ade793 feat: Add interactive form text input editing
- Add FORM_EDIT input mode for editing text fields
- Add actions: NEXT_FIELD, PREV_FIELD, EDIT_TEXT, ACTIVATE_FIELD
- Support 'i' key to focus first form field
- Tab/Shift+Tab to navigate between fields
- Enter on text input fields to edit them
- Real-time text editing with live preview
- Enter/Esc to exit edit mode
- Checkbox toggle support (press Enter on checkbox)
- Status bar shows "-- INSERT --" mode and current text
- Form fields highlighted when active

Keyboard shortcuts:
- i: Focus first form field
- Tab: Next field
- Shift+Tab: Previous field
- Enter: Activate/edit field or toggle checkbox
- Esc: Exit edit mode
2025-12-27 23:52:36 +08:00
55fc7c79f5 refactor: Use ANSI escape codes for cursor and screen operations
- Replace ncurses clear/refresh with ANSI codes for consistency
- Replace ncurses move/curs_set with ANSI cursor control codes
- Improves consistency since colors and attributes already use ANSI codes
- All tests pass successfully
2025-12-27 23:35:21 +08:00
11 changed files with 689 additions and 29 deletions

View file

@ -1,9 +1,9 @@
# TUT 2.0 - 下次继续从这里开始
## 当前位置
- **阶段**: Phase 7 - 历史记录持久化 (已完成!)
- **进度**: 历史记录自动保存,支持 :history 命令查看
- **最后提交**: `feat: Add persistent browsing history`
- **阶段**: Phase 9 - 性能优化和测试工具 (已完成!)
- **进度**: 图片缓存、测试工具、文档完善
- **最后提交**: `feat: Add comprehensive testing tools and improve help`
## 立即可做的事
@ -19,8 +19,40 @@
历史记录存储在 `~/.config/tut/history.json`
### 3. 表单交互
- **i** - 聚焦到第一个表单字段
- **Tab** - 下一个表单字段
- **Shift+Tab** - 上一个表单字段
- **Enter** - 激活字段(文本输入/下拉选择/复选框)
- 在文本输入模式下:
- 输入文字实时更新
- **Enter****Esc** - 退出编辑模式
- 在下拉选择模式下:
- **j/k****↓/↑** - 导航选项
- **Enter** - 选择当前选项
- **Esc** - 取消选择
## 已完成的功能清单
### 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)
@ -142,8 +174,9 @@ cmake --build build
| j/k | 上下滚动 |
| Ctrl+d/u | 翻页 |
| gg/G | 顶部/底部 |
| Tab/Shift+Tab | 切换链接 |
| Enter | 跟随链接 |
| Tab/Shift+Tab | 切换链接/表单字段 |
| Enter | 跟随链接/激活字段 |
| i | 聚焦首个表单字段 |
| h/l | 后退/前进 |
| / | 搜索 |
| n/N | 下一个/上一个匹配 |
@ -155,18 +188,27 @@ cmake --build build
| :history | 查看历史 |
| :q | 退出 |
| ? | 帮助 |
| Esc | 取消加载 |
| Esc | 取消加载/退出编辑 |
**表单编辑模式** (INSERT):
- 输入字符 - 编辑文本
- Enter/Esc - 完成编辑
**下拉选择模式** (SELECT):
- j/k, ↓/↑ - 导航选项
- Enter - 选择选项
- Esc - 取消选择
## 下一步功能优先级
1. **更多表单交互** - 文本输入编辑,下拉选择
2. **图片缓存** - 避免重复下载相同图片
3. **异步图片加载** - 图片也使用异步加载
4. **Cookie 支持** - 保存和发送 Cookie
1. **异步图片加载** - 图片也使用异步加载
2. **Cookie 支持** - 保存和发送 Cookie
3. **表单提交** - 实现 POST 表单提交
4. **更多HTML5支持** - 更完善的HTML渲染
## 恢复对话时说
> "继续TUT 2.0开发"
> "continue"
## Git 信息
@ -182,5 +224,24 @@ cmake --build build
./build/tut
```
## 测试指南
查看 `TESTING.md` 获取完整测试指南,或运行:
```bash
./test_browser.sh
```
## 浏览器特性总结
**核心功能** - 异步HTTP加载、页面缓存、差分渲染
**导航** - 滚动、链接、历史记录
**搜索** - 全文搜索、高亮、导航
**表单** - 文本输入、复选框、下拉选择
**书签** - 持久化书签管理
**历史** - 浏览历史记录
**图片** - ASCII艺术渲染、智能缓存
**性能** - LRU缓存、差分渲染、异步加载
---
更新时间: 2025-12-27
更新时间: 2025-12-28

146
TESTING.md Normal file
View file

@ -0,0 +1,146 @@
# 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

View file

@ -42,6 +42,18 @@ struct CacheEntry {
}
};
// 图片缓存条目
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<std::chrono::seconds>(now - timestamp).count();
return age > max_age_seconds;
}
};
class Browser::Impl {
public:
// 网络和解析
@ -85,6 +97,11 @@ public:
static constexpr int CACHE_MAX_AGE = 300; // 5分钟缓存
static constexpr size_t CACHE_MAX_SIZE = 20; // 最多缓存20个页面
// 图片缓存
std::map<std::string, ImageCacheEntry> 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
@ -380,6 +397,7 @@ public:
}
int loaded = 0;
int cached = 0;
int total = static_cast<int>(tree.images.size());
for (DomNode* img_node : tree.images) {
@ -387,9 +405,22 @@ public:
continue;
}
// 更新状态
loaded++;
status_message = "🖼 Loading image " + std::to_string(loaded) + "/" + std::to_string(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;
cached++;
status_message = "🖼 Loading image " + std::to_string(loaded) + "/" + std::to_string(total) +
" (cached: " + std::to_string(cached) + ")";
draw_screen();
continue;
}
// 更新状态
status_message = "🖼 Downloading image " + std::to_string(loaded) + "/" + std::to_string(total) + "...";
draw_screen();
// 下载图片
@ -401,9 +432,32 @@ public:
// 解码图片
tut::ImageData img_data = tut::ImageRenderer::load_from_memory(response.data);
if (img_data.is_valid()) {
img_node->image_data = std::move(img_data);
img_node->image_data = img_data;
// 添加到缓存
// 限制缓存大小
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[img_node->img_src] = std::move(entry);
}
}
if (cached > 0) {
status_message = "✓ Loaded " + std::to_string(total) + " images (" +
std::to_string(cached) + " from cache)";
}
}
// 从URL中提取主机名
@ -456,6 +510,8 @@ public:
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);
@ -540,7 +596,29 @@ public:
break;
case Action::FOLLOW_LINK:
if (active_link >= 0 && active_link < static_cast<int>(current_tree.links.size())) {
// If on a form field, activate it instead of following link
if (active_field >= 0 && active_field < static_cast<int>(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<int>(current_tree.links.size())) {
start_async_load(current_tree.links[active_link].url);
}
break;
@ -609,6 +687,97 @@ public:
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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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处理
@ -810,8 +979,29 @@ public:
<h2>Forms</h2>
<ul>
<li>Tab - Navigate links and form fields</li>
<li>Enter - Activate link or submit form</li>
<li>i - Focus first form field</li>
<li>Tab/Shift+Tab - Navigate between fields</li>
<li>Enter - Activate field (text input/checkbox/dropdown)</li>
</ul>
<h3>Text Input</h3>
<ul>
<li>Type to edit text</li>
<li>Backspace to delete</li>
<li>Enter or Esc to finish editing</li>
</ul>
<h3>Dropdown Selection</h3>
<ul>
<li>Enter on SELECT to open options</li>
<li>j/k or arrows to navigate options</li>
<li>Enter to select, Esc to cancel</li>
</ul>
<h2>Other</h2>
<ul>
<li>r - Refresh page (bypass cache)</li>
<li>Esc - Cancel loading</li>
</ul>
<hr>

View file

@ -369,7 +369,30 @@ std::unique_ptr<DomNode> DomTreeBuilder::convert_node(
}
}
}
// 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

View file

@ -58,6 +58,10 @@ struct DomNode {
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;

View file

@ -125,6 +125,7 @@ public:
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;
@ -135,7 +136,7 @@ public:
break;
case '\n':
case '\r':
// If count buffer has a number, jump to that link
// Enter can follow links or activate fields - browser will decide
if (result.has_count) {
result.action = Action::GOTO_LINK;
result.number = result.count;
@ -144,6 +145,11 @@ public:
}
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;
@ -334,6 +340,78 @@ public:
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>()) {}
@ -352,6 +430,10 @@ InputResult InputHandler::handle_key(int ch) {
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;
}
@ -375,6 +457,14 @@ void InputHandler::reset() {
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;
}

View file

@ -9,7 +9,9 @@ enum class InputMode {
COMMAND,
SEARCH,
LINK,
LINK_HINTS // Vimium-style 'f' mode
LINK_HINTS, // Vimium-style 'f' mode
FORM_EDIT, // Form field editing mode
SELECT_OPTION // Dropdown selection mode
};
enum class Action {
@ -42,7 +44,16 @@ enum class Action {
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)
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 {
@ -62,6 +73,8 @@ public:
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:

View file

@ -432,9 +432,13 @@ void LayoutEngine::layout_form_element(const DomNode* node, Context& /*ctx*/, st
span.field_index = node->field_index;
line.spans.push_back(span);
} else if (node->element_type == ElementType::SELECT) {
// 下拉选择
// 下拉选择 - 显示当前选中的选项
StyledSpan span;
span.text = "[▼ Select]";
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;

View file

@ -125,11 +125,14 @@ public:
}
void clear() {
::clear();
// 使用 ANSI 转义码清屏并移动光标到左上角
std::printf("\033[2J\033[H");
std::fflush(stdout);
}
void refresh() {
::refresh();
// ANSI 模式下不需要 ncurses 的 refresh只需 flush
std::fflush(stdout);
}
// ==================== True Color ====================
@ -223,15 +226,20 @@ public:
// ==================== 光标控制 ====================
void move_cursor(int x, int y) {
move(y, x); // ncurses使用 (y, x) 顺序
// 使用 ANSI 转义码移动光标(和 printf 输出兼容)
// 注意ANSI 坐标是 1-indexed所以需要 +1
std::printf("\033[%d;%dH", y + 1, x + 1);
std::fflush(stdout);
}
void hide_cursor() {
curs_set(0);
std::printf("\033[?25l"); // DECTCEM: 隐藏光标
std::fflush(stdout);
}
void show_cursor() {
curs_set(1);
std::printf("\033[?25h"); // DECTCEM: 显示光标
std::fflush(stdout);
}
// ==================== 文本输出 ====================

82
test_browser.sh Executable file
View file

@ -0,0 +1,82 @@
#!/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!"

39
test_form.html Normal file
View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<title>Form Test</title>
</head>
<body>
<h1>Test Form</h1>
<form action="/submit" method="POST">
<p>Name: <input type="text" name="name" placeholder="Enter your name"></p>
<p>Email: <input type="text" name="email" placeholder="email@example.com"></p>
<p>Password: <input type="password" name="password"></p>
<p>Country: <select name="country">
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
<option value="au">Australia</option>
</select></p>
<p>Age Group: <select name="age">
<option value="18-25" selected>18-25</option>
<option value="26-35">26-35</option>
<option value="36-50">36-50</option>
<option value="51+">51+</option>
</select></p>
<p><input type="checkbox" name="subscribe"> Subscribe to newsletter</p>
<p><input type="checkbox" name="agree"> I agree to terms</p>
<p><input type="submit" value="Submit Form"></p>
<p><button>Click Me</button></p>
</form>
<h2>Links</h2>
<p><a href="https://example.com">Example Link</a></p>
</body>
</html>