Skip to content

Commit ebe9ead

Browse files
console: add Ctrl+Left/Right word navigation
- Add KEY_CTRL_ARROW_LEFT and KEY_CTRL_ARROW_RIGHT codes - Windows: detect CTRL modifier via dwControlKeyState - Linux: parse ANSI sequences with modifier (1;5D/C) - Implement move_word_left/right with space-skipping logic - Refactor escape sequence parsing to accumulate params
1 parent abd3e9f commit ebe9ead

File tree

1 file changed

+161
-27
lines changed

1 file changed

+161
-27
lines changed

common/console.cpp

Lines changed: 161 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#include <iostream>
44
#include <cassert>
55
#include <cstddef>
6+
#include <cctype>
7+
#include <cwctype>
68

79
#if defined(_WIN32)
810
#define WIN32_LEAN_AND_MEAN
@@ -41,12 +43,14 @@ namespace console {
4143
// Use private-use unicode values to represent special keys that are not reported
4244
// as characters (e.g. arrows on Windows). These values should never clash with
4345
// real input and let the rest of the code handle navigation uniformly.
44-
static constexpr char32_t KEY_ARROW_LEFT = 0xE000;
45-
static constexpr char32_t KEY_ARROW_RIGHT = 0xE001;
46-
static constexpr char32_t KEY_ARROW_UP = 0xE002;
47-
static constexpr char32_t KEY_ARROW_DOWN = 0xE003;
48-
static constexpr char32_t KEY_HOME = 0xE004;
49-
static constexpr char32_t KEY_END = 0xE005;
46+
static constexpr char32_t KEY_ARROW_LEFT = 0xE000;
47+
static constexpr char32_t KEY_ARROW_RIGHT = 0xE001;
48+
static constexpr char32_t KEY_ARROW_UP = 0xE002;
49+
static constexpr char32_t KEY_ARROW_DOWN = 0xE003;
50+
static constexpr char32_t KEY_HOME = 0xE004;
51+
static constexpr char32_t KEY_END = 0xE005;
52+
static constexpr char32_t KEY_CTRL_ARROW_LEFT = 0xE006;
53+
static constexpr char32_t KEY_CTRL_ARROW_RIGHT = 0xE007;
5054
}
5155

5256
//
@@ -192,9 +196,11 @@ namespace console {
192196
if (record.EventType == KEY_EVENT && record.Event.KeyEvent.bKeyDown) {
193197
wchar_t wc = record.Event.KeyEvent.uChar.UnicodeChar;
194198
if (wc == 0) {
199+
const DWORD ctrl_mask = LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED;
200+
const bool ctrl_pressed = (record.Event.KeyEvent.dwControlKeyState & ctrl_mask) != 0;
195201
switch (record.Event.KeyEvent.wVirtualKeyCode) {
196-
case VK_LEFT: return KEY_ARROW_LEFT;
197-
case VK_RIGHT: return KEY_ARROW_RIGHT;
202+
case VK_LEFT: return ctrl_pressed ? KEY_CTRL_ARROW_LEFT : KEY_ARROW_LEFT;
203+
case VK_RIGHT: return ctrl_pressed ? KEY_CTRL_ARROW_RIGHT : KEY_ARROW_RIGHT;
198204
case VK_UP: return KEY_ARROW_UP;
199205
case VK_DOWN: return KEY_ARROW_DOWN;
200206
case VK_HOME: return KEY_HOME;
@@ -407,6 +413,8 @@ namespace console {
407413
}
408414

409415
static void move_cursor(int delta);
416+
static void move_word_left(size_t & char_pos, size_t & byte_pos, const std::vector<int> & widths, const std::string & line);
417+
static void move_word_right(size_t & char_pos, size_t & byte_pos, const std::vector<int> & widths, const std::string & line);
410418
static void move_to_line_start(size_t & char_pos, size_t & byte_pos, const std::vector<int> & widths);
411419
static void move_to_line_end(size_t & char_pos, size_t & byte_pos, const std::vector<int> & widths, const std::string & line);
412420

@@ -467,6 +475,123 @@ namespace console {
467475
byte_pos = line.length();
468476
}
469477

478+
static bool has_ctrl_modifier(const std::string & params) {
479+
size_t start = 0;
480+
while (start < params.size()) {
481+
size_t end = params.find(';', start);
482+
size_t len = (end == std::string::npos) ? params.size() - start : end - start;
483+
if (len > 0) {
484+
int value = 0;
485+
for (size_t i = 0; i < len; ++i) {
486+
char ch = params[start + i];
487+
if (!std::isdigit(static_cast<unsigned char>(ch))) {
488+
value = -1;
489+
break;
490+
}
491+
value = value * 10 + (ch - '0');
492+
}
493+
if (value == 5) {
494+
return true;
495+
}
496+
}
497+
498+
if (end == std::string::npos) {
499+
break;
500+
}
501+
start = end + 1;
502+
}
503+
return false;
504+
}
505+
506+
static bool is_space_codepoint(char32_t cp) {
507+
return std::iswspace(static_cast<wint_t>(cp)) != 0;
508+
}
509+
510+
static void move_word_left(size_t & char_pos, size_t & byte_pos, const std::vector<int> & widths, const std::string & line) {
511+
if (char_pos == 0) {
512+
return;
513+
}
514+
515+
size_t new_char_pos = char_pos;
516+
size_t new_byte_pos = byte_pos;
517+
int move_width = 0;
518+
519+
while (new_char_pos > 0) {
520+
size_t prev_byte = prev_utf8_char_pos(line, new_byte_pos);
521+
size_t advance = 0;
522+
char32_t cp = decode_utf8(line, prev_byte, advance);
523+
if (!is_space_codepoint(cp)) {
524+
break;
525+
}
526+
move_width += widths[new_char_pos - 1];
527+
new_char_pos--;
528+
new_byte_pos = prev_byte;
529+
}
530+
531+
while (new_char_pos > 0) {
532+
size_t prev_byte = prev_utf8_char_pos(line, new_byte_pos);
533+
size_t advance = 0;
534+
char32_t cp = decode_utf8(line, prev_byte, advance);
535+
if (is_space_codepoint(cp)) {
536+
break;
537+
}
538+
move_width += widths[new_char_pos - 1];
539+
new_char_pos--;
540+
new_byte_pos = prev_byte;
541+
}
542+
543+
move_cursor(-move_width);
544+
char_pos = new_char_pos;
545+
byte_pos = new_byte_pos;
546+
}
547+
548+
static void move_word_right(size_t & char_pos, size_t & byte_pos, const std::vector<int> & widths, const std::string & line) {
549+
if (char_pos >= widths.size()) {
550+
return;
551+
}
552+
553+
size_t new_char_pos = char_pos;
554+
size_t new_byte_pos = byte_pos;
555+
int move_width = 0;
556+
557+
while (new_char_pos < widths.size()) {
558+
size_t advance = 0;
559+
char32_t cp = decode_utf8(line, new_byte_pos, advance);
560+
if (!is_space_codepoint(cp)) {
561+
break;
562+
}
563+
move_width += widths[new_char_pos];
564+
new_char_pos++;
565+
new_byte_pos += advance;
566+
}
567+
568+
while (new_char_pos < widths.size()) {
569+
size_t advance = 0;
570+
char32_t cp = decode_utf8(line, new_byte_pos, advance);
571+
if (is_space_codepoint(cp)) {
572+
break;
573+
}
574+
move_width += widths[new_char_pos];
575+
new_char_pos++;
576+
new_byte_pos += advance;
577+
}
578+
579+
while (new_char_pos < widths.size()) {
580+
size_t advance = 0;
581+
char32_t cp = decode_utf8(line, new_byte_pos, advance);
582+
if (!is_space_codepoint(cp)) {
583+
break;
584+
}
585+
move_width += widths[new_char_pos];
586+
new_char_pos++;
587+
new_byte_pos += advance;
588+
}
589+
590+
move_cursor(move_width);
591+
char_pos = new_char_pos;
592+
byte_pos = new_byte_pos;
593+
}
594+
470595
static void move_cursor(int delta) {
471596
if (delta == 0) return;
472597
#if defined(_WIN32)
@@ -540,16 +665,30 @@ namespace console {
540665
if (input_char == '\033') { // Escape sequence
541666
char32_t code = getchar32();
542667
if (code == '[') {
543-
code = getchar32();
668+
std::string params;
669+
while (true) {
670+
code = getchar32();
671+
if ((code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z') || code == '~' || code == (char32_t) WEOF) {
672+
break;
673+
}
674+
params.push_back(static_cast<char>(code));
675+
}
676+
677+
const bool ctrl_modifier = has_ctrl_modifier(params);
678+
544679
if (code == 'D') { // left
545-
if (char_pos > 0) {
680+
if (ctrl_modifier) {
681+
move_word_left(char_pos, byte_pos, widths, line);
682+
} else if (char_pos > 0) {
546683
int w = widths[char_pos - 1];
547684
move_cursor(-w);
548685
char_pos--;
549686
byte_pos = prev_utf8_char_pos(line, byte_pos);
550687
}
551688
} else if (code == 'C') { // right
552-
if (char_pos < widths.size()) {
689+
if (ctrl_modifier) {
690+
move_word_right(char_pos, byte_pos, widths, line);
691+
} else if (char_pos < widths.size()) {
553692
int w = widths[char_pos];
554693
move_cursor(w);
555694
char_pos++;
@@ -578,16 +717,15 @@ namespace console {
578717
}
579718
}
580719
}
581-
} else if (code >= '0' && code <= '9') {
720+
} else if ((code == '~' || (code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z')) && !params.empty()) {
582721
std::string digits;
583-
digits.push_back(static_cast<char>(code));
584-
while (true) {
585-
code = getchar32();
586-
if (code >= '0' && code <= '9') {
587-
digits.push_back(static_cast<char>(code));
588-
continue;
722+
for (char ch : params) {
723+
if (ch == ';') {
724+
break;
725+
}
726+
if (std::isdigit(static_cast<unsigned char>(ch))) {
727+
digits.push_back(ch);
589728
}
590-
break;
591729
}
592730

593731
if (code == '~') {
@@ -597,15 +735,7 @@ namespace console {
597735
move_to_line_end(char_pos, byte_pos, widths, line);
598736
}
599737
}
600-
} else {
601-
// Discard the rest of the escape sequence
602-
while ((code = getchar32()) != (char32_t) WEOF) {
603-
if ((code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z') || code == '~') {
604-
break;
605-
}
606-
}
607738
}
608-
// TODO: Handle Ctrl+Arrow
609739
} else if (code == 0x1B) {
610740
// Discard the rest of the escape sequence
611741
while ((code = getchar32()) != (char32_t) WEOF) {
@@ -629,6 +759,10 @@ namespace console {
629759
char_pos++;
630760
byte_pos = next_utf8_char_pos(line, byte_pos);
631761
}
762+
} else if (input_char == KEY_CTRL_ARROW_LEFT) {
763+
move_word_left(char_pos, byte_pos, widths, line);
764+
} else if (input_char == KEY_CTRL_ARROW_RIGHT) {
765+
move_word_right(char_pos, byte_pos, widths, line);
632766
} else if (input_char == KEY_HOME) {
633767
move_to_line_start(char_pos, byte_pos, widths);
634768
} else if (input_char == KEY_END) {

0 commit comments

Comments
 (0)