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