mirror of
https://github.com/m1ngsama/TUT.git
synced 2025-12-24 10:51:46 +00:00
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:
parent
8e291399ae
commit
a9c35765c4
2 changed files with 320 additions and 0 deletions
253
src/input_handler.cpp
Normal file
253
src/input_handler.cpp
Normal 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
67
src/input_handler.h
Normal 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;
|
||||
};
|
||||
Loading…
Reference in a new issue