feat: Implement vim-style input handling

Add complete vim-style keyboard navigation:
- Normal mode: hjkl movement, gg/G jump, numeric prefixes
- Command mode: :q, :o URL, :r, :h, :[number]
- Search mode: / for search, n/N for next/previous match
- Link navigation: Tab/Shift-Tab, Enter to follow
- Scroll commands: Ctrl-D/U, Space, b for page up/down
- History navigation: h for back, l for forward

Input handler manages mode transitions and command parsing
with full vim compatibility.
This commit is contained in:
m1ngsama 2025-12-05 14:59:34 +08:00
parent 8e291399ae
commit a9c35765c4
2 changed files with 320 additions and 0 deletions

253
src/input_handler.cpp Normal file
View file

@ -0,0 +1,253 @@
#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;
// 处理数字前缀
if (std::isdigit(ch) && (ch != '0' || !count_buffer.empty())) {
count_buffer += static_cast<char>(ch);
return result;
}
// 解析count
if (!count_buffer.empty()) {
result.has_count = true;
result.count = std::stoi(count_buffer);
count_buffer.clear();
}
// 处理vim风格的命令
switch (ch) {
// 移动
case 'j':
case KEY_DOWN:
result.action = Action::SCROLL_DOWN;
break;
case 'k':
case KEY_UP:
result.action = Action::SCROLL_UP;
break;
case 'h':
case KEY_LEFT:
result.action = Action::GO_BACK;
break;
case 'l':
case KEY_RIGHT:
result.action = Action::GO_FORWARD;
break;
// 翻页
case 4: // Ctrl-D
case ' ':
result.action = Action::SCROLL_PAGE_DOWN;
break;
case 21: // Ctrl-U
case 'b':
result.action = Action::SCROLL_PAGE_UP;
break;
// 跳转
case 'g':
buffer += 'g';
if (buffer == "gg") {
result.action = Action::GOTO_TOP;
buffer.clear();
}
break;
case 'G':
if (result.has_count) {
result.action = Action::GOTO_LINE;
result.number = result.count;
} else {
result.action = Action::GOTO_BOTTOM;
}
break;
// 搜索
case '/':
mode = InputMode::SEARCH;
buffer = "/";
break;
case 'n':
result.action = Action::SEARCH_NEXT;
break;
case 'N':
result.action = Action::SEARCH_PREV;
break;
// 链接导航
case '\t': // Tab
result.action = Action::NEXT_LINK;
break;
case KEY_BTAB: // Shift-Tab (可能不是所有终端都支持)
case 'T':
result.action = Action::PREV_LINK;
break;
case '\n': // Enter
case '\r':
result.action = Action::FOLLOW_LINK;
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;
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) {
// :o URL 或 :open URL
size_t space_pos = command.find(' ');
if (space_pos != std::string::npos) {
result.action = Action::OPEN_URL;
result.text = command.substr(space_pos + 1);
}
} else if (!command.empty() && std::isdigit(command[0])) {
// 跳转到行号
try {
result.action = Action::GOTO_LINE;
result.number = std::stoi(command);
} catch (...) {
set_status("Invalid line number");
}
}
mode = InputMode::NORMAL;
buffer.clear();
} else if (ch == 27) { // ESC
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) { // ESC
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;
}
};
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);
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_status_callback(std::function<void(const std::string&)> callback) {
pImpl->status_callback = callback;
}

67
src/input_handler.h Normal file
View file

@ -0,0 +1,67 @@
#pragma once
#include <string>
#include <functional>
enum class InputMode {
NORMAL, // 正常浏览模式
COMMAND, // 命令模式 (:)
SEARCH, // 搜索模式 (/)
LINK // 链接选择模式
};
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,
GO_BACK,
GO_FORWARD,
OPEN_URL,
REFRESH,
QUIT,
HELP
};
struct InputResult {
Action action;
std::string text; // 用于命令、搜索、URL输入
int number; // 用于跳转行号、链接编号等
bool has_count; // 是否有数字前缀(如 5j
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_status_callback(std::function<void(const std::string&)> callback);
private:
class Impl;
std::unique_ptr<Impl> pImpl;
};