Skip to content

Commit 77ca259

Browse files
committed
console: allow using arrow left/right to edit the line (with UTF-8 support)
1 parent db97837 commit 77ca259

File tree

1 file changed

+143
-28
lines changed

1 file changed

+143
-28
lines changed

common/console.cpp

Lines changed: 143 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "console.h"
22
#include <vector>
33
#include <iostream>
4+
#include <cassert>
45

56
#if defined(_WIN32)
67
#define WIN32_LEAN_AND_MEAN
@@ -336,20 +337,55 @@ namespace console {
336337
}
337338

338339
// Helper function to remove the last UTF-8 character from a string
339-
static void pop_back_utf8_char(std::string & line) {
340-
if (line.empty()) {
341-
return;
340+
static size_t prev_utf8_char_pos(const std::string & line, size_t pos) {
341+
if (pos == 0) return 0;
342+
pos--;
343+
while (pos > 0 && (line[pos] & 0xC0) == 0x80) {
344+
pos--;
342345
}
346+
return pos;
347+
}
343348

344-
size_t pos = line.length() - 1;
349+
static size_t next_utf8_char_pos(const std::string & line, size_t pos) {
350+
if (pos >= line.length()) return line.length();
351+
pos++;
352+
while (pos < line.length() && (line[pos] & 0xC0) == 0x80) {
353+
pos++;
354+
}
355+
return pos;
356+
}
345357

346-
// Find the start of the last UTF-8 character (checking up to 4 bytes back)
347-
for (size_t i = 0; i < 3 && pos > 0; ++i, --pos) {
348-
if ((line[pos] & 0xC0) != 0x80) {
349-
break; // Found the start of the character
358+
static void move_cursor(int delta) {
359+
if (delta == 0) return;
360+
#if defined(_WIN32)
361+
if (hConsole != NULL) {
362+
CONSOLE_SCREEN_BUFFER_INFO bufferInfo;
363+
GetConsoleScreenBufferInfo(hConsole, &bufferInfo);
364+
COORD newCursorPosition = bufferInfo.dwCursorPosition;
365+
int width = bufferInfo.dwSize.X;
366+
int newX = newCursorPosition.X + delta;
367+
int newY = newCursorPosition.Y;
368+
369+
while (newX >= width) {
370+
newX -= width;
371+
newY++;
350372
}
373+
while (newX < 0) {
374+
newX += width;
375+
newY--;
376+
}
377+
378+
newCursorPosition.X = newX;
379+
newCursorPosition.Y = newY;
380+
SetConsoleCursorPosition(hConsole, newCursorPosition);
381+
}
382+
#else
383+
if (delta < 0) {
384+
for (int i = 0; i < -delta; i++) fprintf(out, "\b");
385+
} else {
386+
for (int i = 0; i < delta; i++) fprintf(out, "\033[C");
351387
}
352-
line.erase(pos);
388+
#endif
353389
}
354390

355391
static bool readline_advanced(std::string & line, bool multiline_input) {
@@ -362,8 +398,14 @@ namespace console {
362398
bool is_special_char = false;
363399
bool end_of_stream = false;
364400

401+
size_t byte_pos = 0; // current byte index
402+
size_t char_pos = 0; // current character index (one char can be multiple bytes)
403+
365404
char32_t input_char;
366405
while (true) {
406+
assert(char_pos <= byte_pos);
407+
assert(char_pos <= widths.size());
408+
367409
fflush(out); // Ensure all output is displayed before waiting for input
368410
input_char = getchar32();
369411

@@ -384,7 +426,35 @@ namespace console {
384426

385427
if (input_char == '\033') { // Escape sequence
386428
char32_t code = getchar32();
387-
if (code == '[' || code == 0x1B) {
429+
if (code == '[') {
430+
code = getchar32();
431+
if (code == 'D') { // left
432+
if (char_pos > 0) {
433+
int w = widths[char_pos - 1];
434+
move_cursor(-w);
435+
char_pos--;
436+
byte_pos = prev_utf8_char_pos(line, byte_pos);
437+
}
438+
} else if (code == 'C') { // right
439+
if (char_pos < widths.size()) {
440+
int w = widths[char_pos];
441+
move_cursor(w);
442+
char_pos++;
443+
byte_pos = next_utf8_char_pos(line, byte_pos);
444+
}
445+
} else if (code == 'A' || code == 'B') {
446+
// up/down
447+
// TODO: Implement history navigation
448+
} else {
449+
// Discard the rest of the escape sequence
450+
while ((code = getchar32()) != (char32_t) WEOF) {
451+
if ((code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z') || code == '~') {
452+
break;
453+
}
454+
}
455+
}
456+
// TODO: Handle Ctrl+Arrow
457+
} else if (code == 0x1B) {
388458
// Discard the rest of the escape sequence
389459
while ((code = getchar32()) != (char32_t) WEOF) {
390460
if ((code >= 'A' && code <= 'Z') || (code >= 'a' && code <= 'z') || code == '~') {
@@ -393,27 +463,72 @@ namespace console {
393463
}
394464
}
395465
} else if (input_char == 0x08 || input_char == 0x7F) { // Backspace
396-
if (!widths.empty()) {
397-
int count;
398-
do {
399-
count = widths.back();
400-
widths.pop_back();
401-
// Move cursor back, print space, and move cursor back again
402-
for (int i = 0; i < count; i++) {
403-
replace_last(' ');
404-
pop_cursor();
405-
}
406-
pop_back_utf8_char(line);
407-
} while (count == 0 && !widths.empty());
466+
if (char_pos > 0) {
467+
int w = widths[char_pos - 1];
468+
move_cursor(-w);
469+
char_pos--;
470+
size_t prev_pos = prev_utf8_char_pos(line, byte_pos);
471+
size_t char_len = byte_pos - prev_pos;
472+
byte_pos = prev_pos;
473+
474+
// remove the character
475+
line.erase(byte_pos, char_len);
476+
widths.erase(widths.begin() + char_pos);
477+
478+
// redraw tail
479+
size_t p = byte_pos;
480+
int tail_width = 0;
481+
for (size_t i = char_pos; i < widths.size(); ++i) {
482+
size_t next_p = next_utf8_char_pos(line, p);
483+
put_codepoint(line.c_str() + p, next_p - p, widths[i]);
484+
tail_width += widths[i];
485+
p = next_p;
486+
}
487+
488+
// clear display
489+
for (int i = 0; i < w; ++i) {
490+
fputc(' ', out);
491+
}
492+
move_cursor(-(tail_width + w));
408493
}
409494
} else {
410-
int offset = line.length();
411-
append_utf8(input_char, line);
412-
int width = put_codepoint(line.c_str() + offset, line.length() - offset, estimateWidth(input_char));
413-
if (width < 0) {
414-
width = 0;
495+
// insert character
496+
std::string new_char_str;
497+
append_utf8(input_char, new_char_str);
498+
int w = estimateWidth(input_char);
499+
500+
if (char_pos == widths.size()) {
501+
// insert at the end
502+
line += new_char_str;
503+
int real_w = put_codepoint(new_char_str.c_str(), new_char_str.length(), w);
504+
if (real_w < 0) real_w = 0;
505+
widths.push_back(real_w);
506+
byte_pos += new_char_str.length();
507+
char_pos++;
508+
} else {
509+
// insert in middle
510+
line.insert(byte_pos, new_char_str);
511+
512+
int real_w = put_codepoint(new_char_str.c_str(), new_char_str.length(), w);
513+
if (real_w < 0) real_w = 0;
514+
515+
widths.insert(widths.begin() + char_pos, real_w);
516+
517+
// print the tail
518+
size_t p = byte_pos + new_char_str.length();
519+
int tail_width = 0;
520+
for (size_t i = char_pos + 1; i < widths.size(); ++i) {
521+
size_t next_p = next_utf8_char_pos(line, p);
522+
put_codepoint(line.c_str() + p, next_p - p, widths[i]);
523+
tail_width += widths[i];
524+
p = next_p;
525+
}
526+
527+
move_cursor(-tail_width);
528+
529+
byte_pos += new_char_str.length();
530+
char_pos++;
415531
}
416-
widths.push_back(width);
417532
}
418533

419534
if (!line.empty() && (line.back() == '\\' || line.back() == '/')) {

0 commit comments

Comments
 (0)