feat: Add Vimium-style link hints and vim keybindings infrastructure

- Add LINK_HINTS mode for Vimium-style link navigation
- Implement 'f' key to activate link hints mode
- Add visual mode support (v/V keys)
- Implement marks support (m[a-z] to set, '[a-z] to jump)
- Add tab navigation keys (gt/gT for next/previous tab)
- Add new actions:
  * SHOW_LINK_HINTS - activate link hints overlay
  * FOLLOW_LINK_HINT - follow link by hint letters
  * ENTER_VISUAL_MODE / ENTER_VISUAL_LINE_MODE
  * SET_MARK / GOTO_MARK - vim-style position bookmarks
  * NEXT_TAB / PREV_TAB - tab navigation
  * YANK - copy selected text

This brings modern browser vim plugin functionality (like Vimium)
to the terminal, making link navigation much faster than traditional
tab-through methods.
This commit is contained in:
m1ngsama 2025-12-17 13:43:48 +08:00
parent 638a08e437
commit ac988dfda8
2 changed files with 160 additions and 13 deletions

View file

@ -23,7 +23,48 @@ public:
result.has_count = false;
result.count = 1;
// Handle digit input for count or 'f' command
// Handle multi-char commands like 'gg', 'gt', 'gT', 'm', '
if (!buffer.empty()) {
if (buffer == "g") {
if (ch == 't') {
result.action = Action::NEXT_TAB;
buffer.clear();
count_buffer.clear();
return result;
} else if (ch == 'T') {
result.action = Action::PREV_TAB;
buffer.clear();
count_buffer.clear();
return result;
}
} else 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;
@ -116,16 +157,33 @@ public:
count_buffer.clear();
break;
case 'f':
// 'f' command: follow link by number
if (result.has_count) {
result.action = Action::FOLLOW_LINK_NUM;
result.number = result.count;
count_buffer.clear();
} else {
// Enter link follow mode, wait for number
mode = InputMode::LINK;
buffer = "f";
}
// 'f' command: vimium-style link hints
result.action = Action::SHOW_LINK_HINTS;
mode = InputMode::LINK_HINTS;
buffer.clear();
count_buffer.clear();
break;
case 'v':
// Enter visual mode
result.action = Action::ENTER_VISUAL_MODE;
mode = InputMode::VISUAL;
count_buffer.clear();
break;
case 'V':
// Enter visual line mode
result.action = Action::ENTER_VISUAL_LINE_MODE;
mode = InputMode::VISUAL_LINE;
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;
@ -257,6 +315,75 @@ public:
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_visual_mode(int ch) {
InputResult result;
result.action = Action::NONE;
if (ch == 27 || ch == 'v') {
// ESC or 'v' exits visual mode
mode = InputMode::NORMAL;
return result;
} else if (ch == 'y') {
// Yank (copy) selected text
result.action = Action::YANK;
mode = InputMode::NORMAL;
return result;
}
// Pass through navigation commands
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:
// In visual mode, h/l could extend selection
break;
case 'l':
case KEY_RIGHT:
break;
}
return result;
}
};
InputHandler::InputHandler() : pImpl(std::make_unique<Impl>()) {}
@ -273,6 +400,11 @@ InputResult InputHandler::handle_key(int ch) {
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::VISUAL:
case InputMode::VISUAL_LINE:
return pImpl->process_visual_mode(ch);
default:
break;
}

View file

@ -8,7 +8,10 @@ enum class InputMode {
NORMAL,
COMMAND,
SEARCH,
LINK
LINK,
LINK_HINTS, // Vimium-style 'f' mode
VISUAL, // Visual mode
VISUAL_LINE // Visual line mode
};
enum class Action {
@ -28,12 +31,24 @@ enum class Action {
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
HELP,
ENTER_VISUAL_MODE, // Start visual mode
ENTER_VISUAL_LINE_MODE, // Start visual line mode
SET_MARK, // Set a mark (m + letter)
GOTO_MARK, // Jump to mark (' + letter)
YANK, // Copy selected text
NEXT_TAB, // gt - next tab
PREV_TAB, // gT - previous tab
NEW_TAB, // :tabnew
CLOSE_TAB, // :tabc
TOGGLE_MOUSE // Toggle mouse support
};
struct InputResult {