MIRROR: javascript for 馃悳's, a tiny runtime with big ambitions
1#include <ctype.h>
2#include <stdint.h>
3#include <signal.h>
4#include <stdio.h>
5#include <stdlib.h>
6#include <string.h>
7#include <crprintf.h>
8#include <sys/stat.h>
9
10#ifdef _WIN32
11#include <conio.h>
12#define WIN32_LEAN_AND_MEAN
13#include <windows.h>
14#include <direct.h>
15#define STDIN_FILENO 0
16#define mkdir_p(path) _mkdir(path)
17#else
18#include <sys/ioctl.h>
19#include <termios.h>
20#include <unistd.h>
21#define mkdir_p(path) mkdir(path, 0755)
22#endif
23
24#include "utf8.h"
25#include "utils.h"
26#include "readline.h"
27#include "highlight.h"
28
29#define MAX_LINE_LENGTH 4096
30
31static const unsigned char HISTORY_FILE_MAGIC[] = {
32 0x61, 0x6E, 0x74, 0x72, 0x65, 0x70, 0x6C, 0x73,
33 0x63, 0x68, 0x65, 0x6D, 0x61, 0x32, 0x0A, 0x00
34};
35
36static volatile sig_atomic_t ctrl_c_pressed = 0;
37static crprintf_compiled *hl_prog = NULL;
38
39static highlight_state hl_line_state = HL_STATE_INIT;
40static int repl_last_cursor_row = 0;
41
42static void sigint_handler(int sig) {
43 ctrl_c_pressed++;
44}
45
46void ant_readline_install_signal_handler(void) {
47#ifdef _WIN32
48 signal(SIGINT, sigint_handler);
49#else
50 struct sigaction sa;
51 memset(&sa, 0, sizeof(sa));
52 sa.sa_handler = sigint_handler;
53 sigemptyset(&sa.sa_mask);
54 sigaction(SIGINT, &sa, NULL);
55#endif
56}
57
58void ant_readline_shutdown(void) {
59 if (hl_prog) {
60 crprintf_compiled_free(hl_prog);
61 hl_prog = NULL;
62 }
63}
64
65void ant_history_init(ant_history_t *hist, int capacity) {
66 hist->capacity = (capacity > 0) ? capacity : 512;
67 hist->lines = malloc(sizeof(char *) * (size_t)hist->capacity);
68 if (!hist->lines) hist->capacity = 0;
69 hist->count = 0;
70 hist->current = -1;
71}
72
73void ant_history_add(ant_history_t *hist, const char *line) {
74 if (!hist || !hist->lines || hist->capacity <= 0 || !line || line[0] == '\0') return;
75
76 if (hist->count > 0 && strcmp(hist->lines[hist->count - 1], line) == 0) {
77 hist->current = hist->count;
78 return;
79 }
80
81 if (hist->count >= hist->capacity) {
82 free(hist->lines[0]);
83 memmove(hist->lines, hist->lines + 1, sizeof(char *) * (size_t)(hist->capacity - 1));
84 hist->count--;
85 }
86
87 hist->lines[hist->count++] = strdup(line);
88 hist->current = hist->count;
89}
90
91const char *ant_history_prev(ant_history_t *hist) {
92 if (!hist || !hist->lines || hist->count == 0) return NULL;
93 if (hist->current > 0) hist->current--;
94 return hist->lines[hist->current];
95}
96
97const char *ant_history_next(ant_history_t *hist) {
98 if (!hist || !hist->lines || hist->count == 0) return NULL;
99 if (hist->current < hist->count - 1) {
100 hist->current++;
101 return hist->lines[hist->current];
102 }
103 hist->current = hist->count;
104 return "";
105}
106
107void ant_history_free(ant_history_t *hist) {
108 if (!hist || !hist->lines) return;
109 for (int i = 0; i < hist->count; i++) free(hist->lines[i]);
110 free(hist->lines);
111 hist->lines = NULL;
112 hist->count = 0;
113 hist->capacity = 0;
114 hist->current = -1;
115}
116
117static char *get_history_path(void) {
118 char dir[4096];
119
120 if (ant_xdg_state_path(dir, sizeof(dir), NULL) != 0) return NULL;
121 if (ant_mkdir_p(dir) != 0) return NULL;
122
123 size_t len = strlen(dir) + sizeof("/repl_history");
124 char *path = malloc(len);
125
126 if (!path) return NULL;
127 snprintf(path, len, "%s/repl_history", dir);
128
129 return path;
130}
131
132void ant_history_load(ant_history_t *hist) {
133 if (!hist || !hist->lines || hist->capacity <= 0) return;
134
135 char *path = get_history_path();
136 if (!path) return;
137
138 FILE *fp = fopen(path, "r");
139 free(path);
140
141 if (!fp) return;
142 char header[sizeof(HISTORY_FILE_MAGIC)];
143
144 if (
145 !fgets(header, sizeof(header), fp) ||
146 strcmp(header, (const char *)HISTORY_FILE_MAGIC) != 0
147 ) {
148 fclose(fp);
149 return;
150 }
151
152 char lenbuf[32];
153 while (fgets(lenbuf, sizeof(lenbuf), fp)) {
154 char *end = NULL;
155 unsigned long long record_len = strtoull(lenbuf, &end, 10);
156
157 if (end == lenbuf || (*end != '\n' && *end != '\0')) break;
158 if (record_len > (unsigned long long)SIZE_MAX - 1) break;
159
160 size_t line_len = (size_t)record_len;
161 char *line = malloc(line_len + 1);
162
163 if (!line) break;
164
165 if (fread(line, 1, line_len, fp) != line_len) {
166 free(line);
167 break;
168 }
169
170 line[line_len] = '\0';
171 if (line[0]) ant_history_add(hist, line);
172 free(line);
173
174 int sep = fgetc(fp);
175 if (sep != '\n' && sep != EOF) break;
176 }
177
178 fclose(fp);
179}
180
181void ant_history_save(const ant_history_t *hist) {
182 if (!hist || !hist->lines) return;
183
184 char *path = get_history_path();
185 if (!path) return;
186
187 FILE *fp = fopen(path, "w");
188 free(path);
189
190 if (!fp) return;
191 fputs((const char *)HISTORY_FILE_MAGIC, fp);
192
193 for (int i = 0; i < hist->count; i++) {
194 size_t len = strlen(hist->lines[i]);
195 fprintf(fp, "%zu\n", len);
196 fwrite(hist->lines[i], 1, len, fp);
197 fputc('\n', fp);
198 }
199
200 fclose(fp);
201}
202
203typedef enum {
204 KEY_NONE, KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT,
205 KEY_HOME, KEY_END, KEY_DELETE, KEY_BACKSPACE, KEY_ENTER, KEY_EOF, KEY_CHAR
206} key_type_t;
207
208typedef struct {
209 key_type_t type;
210 int ch;
211} key_event_t;
212
213static int repl_terminal_cols(void) {
214 int cols = 80;
215#ifdef _WIN32
216 CONSOLE_SCREEN_BUFFER_INFO csbi;
217 if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi)) {
218 cols = csbi.srWindow.Right - csbi.srWindow.Left + 1;
219 }
220#else
221 struct winsize ws;
222 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
223 cols = ws.ws_col;
224 }
225#endif
226 return (cols > 0) ? cols : 80;
227}
228
229static void repl_jump_cursor(int x_pos, int y_offset) {
230#ifdef _WIN32
231 if (y_offset != 0) {
232 char seq[32];
233 snprintf(seq, sizeof(seq), "\033[%d%c", abs(y_offset), (y_offset > 0) ? 'B' : 'A');
234 fputs(seq, stdout);
235 }
236 char seq[32];
237 snprintf(seq, sizeof(seq), "\033[%dG", x_pos + 1);
238 fputs(seq, stdout);
239#else
240 if (y_offset != 0) {
241 char seq[32];
242 snprintf(seq, sizeof(seq), "\033[%d%c", abs(y_offset), (y_offset > 0) ? 'B' : 'A');
243 fputs(seq, stdout);
244 }
245 char seq[32];
246 snprintf(seq, sizeof(seq), "\033[%dG", x_pos + 1);
247 fputs(seq, stdout);
248#endif
249}
250
251static void repl_clear_to_end(void) {
252 fputs("\033[J", stdout);
253}
254
255static void repl_set_cursor_visible(bool visible) {
256#ifdef _WIN32
257 (void)visible;
258#else
259 fputs(visible ? "\033[?25h" : "\033[?25l", stdout);
260#endif
261}
262
263static size_t repl_skip_ansi_escape(const char *s, size_t len, size_t i) {
264 if (i >= len || (unsigned char)s[i] != 0x1B) return i;
265 if (i + 1 >= len) return i + 1;
266
267 unsigned char next = (unsigned char)s[i + 1];
268
269 if (next == '[') {
270 i += 2;
271 while (i < len) {
272 unsigned char ch = (unsigned char)s[i++];
273 if (ch >= 0x40 && ch <= 0x7E) break;
274 }
275 return i;
276 }
277
278 if (next == ']') {
279 i += 2;
280 while (i < len) {
281 unsigned char ch = (unsigned char)s[i];
282 if (ch == '\a') return i + 1;
283 if (ch == 0x1B && i + 1 < len && s[i + 1] == '\\') return i + 2;
284 i++;
285 }
286 return i;
287 }
288
289 return i + 2;
290}
291
292static void repl_virtual_render(
293 const char *s, size_t n,
294 int screen_cols, int prompt_len,
295 int *x, int *y
296) {
297 bool wrapped = false;
298 size_t i = 0;
299 while (i < n) {
300 unsigned char c = (unsigned char)s[i];
301 if (c == '\n' || c == '\r') {
302 if (c == '\n' && !wrapped) (*y)++;
303 *x = prompt_len;
304 i++;
305 wrapped = false;
306 continue;
307 }
308
309 if (c == 0x1B) {
310 i = repl_skip_ansi_escape(s, n, i);
311 continue;
312 }
313
314 utf8proc_int32_t cp = 0;
315 utf8proc_ssize_t clen =
316 utf8_next((const utf8proc_uint8_t *)(s + i), (utf8proc_ssize_t)(n - i), &cp);
317 if (clen <= 0) {
318 clen = 1;
319 cp = c;
320 }
321
322 int w = utf8proc_charwidth(cp);
323 if (w < 0) w = 1;
324
325 if (w > 0) {
326 *x += w;
327 wrapped = false;
328 if (*x >= screen_cols) {
329 *x = 0;
330 (*y)++;
331 wrapped = true;
332 }
333 }
334
335 i += (size_t)clen;
336 }
337}
338
339static int repl_prompt_width(const char *prompt, int cols) {
340 int x = 0;
341 int y = 0;
342 repl_virtual_render(prompt, strlen(prompt), cols, 0, &x, &y);
343 return x;
344}
345
346static void refresh_line(const char *line, int len, int pos, const char *prompt) {
347 int cols = repl_terminal_cols();
348 int prompt_len = repl_prompt_width(prompt, cols);
349 int x_cursor = prompt_len;
350 int y_cursor = 0;
351 int x_end = prompt_len;
352 int y_end = 0;
353
354 repl_virtual_render(line, (size_t)pos, cols, prompt_len, &x_cursor, &y_cursor);
355 repl_virtual_render(line, (size_t)len, cols, prompt_len, &x_end, &y_end);
356
357 repl_set_cursor_visible(false);
358 repl_jump_cursor(prompt_len, -repl_last_cursor_row);
359 repl_clear_to_end();
360 if (crprintf_get_color() && len > 0) {
361 if (len <= 2048) {
362 char tagged[8192];
363 char rendered[8192];
364
365 highlight_state state = hl_line_state;
366 ant_highlight_stateful(line, (size_t)len, tagged, sizeof(tagged), &state);
367
368 hl_prog = crprintf_recompile(hl_prog, tagged);
369 crprintf_state *rs = crprintf_state_new();
370 crsprintf_compiled(rendered, sizeof(rendered), rs, hl_prog);
371 crprintf_state_free(rs);
372
373 fputs(rendered, stdout);
374 } else fwrite(line, 1, (size_t)len, stdout);
375 } else if (len > 0) fwrite(line, 1, (size_t)len, stdout);
376
377#ifndef _WIN32
378 if ((x_end == 0) && (y_end > 0) && (len > 0) && (line[len - 1] != '\n')) {
379 fputs("\n", stdout);
380 }
381#endif
382
383 repl_jump_cursor(x_cursor, -(y_end - y_cursor));
384 repl_set_cursor_visible(true);
385 repl_last_cursor_row = y_cursor;
386 fflush(stdout);
387}
388
389static void move_cursor_only(const char *line, int pos, const char *prompt) {
390 int cols = repl_terminal_cols();
391 int prompt_len = repl_prompt_width(prompt, cols);
392 int x_cursor = prompt_len;
393 int y_cursor = 0;
394 repl_virtual_render(line, (size_t)pos, cols, prompt_len, &x_cursor, &y_cursor);
395 repl_jump_cursor(x_cursor, -(repl_last_cursor_row - y_cursor));
396 repl_last_cursor_row = y_cursor;
397 fflush(stdout);
398}
399
400static int utf8_prev_pos(const char *line, int pos) {
401 if (pos <= 0) return 0;
402 int prev = 0;
403 int i = 0;
404 while (i < pos) {
405 prev = i;
406 utf8proc_int32_t cp = 0;
407 utf8proc_ssize_t n = utf8_next(
408 (const utf8proc_uint8_t *)(line + i),
409 (utf8proc_ssize_t)(pos - i),
410 &cp
411 );
412 i += (int)((n > 0) ? n : 1);
413 }
414 return prev;
415}
416
417static int utf8_next_pos(const char *line, int len, int pos) {
418 if (pos >= len) return len;
419 utf8proc_int32_t cp = 0;
420 utf8proc_ssize_t n = utf8_next(
421 (const utf8proc_uint8_t *)(line + pos),
422 (utf8proc_ssize_t)(len - pos),
423 &cp
424 );
425 int next = pos + (int)((n > 0) ? n : 1);
426 return (next > len) ? len : next;
427}
428
429static void line_set(char *line, int *pos, int *len, const char *str, const char *prompt) {
430 size_t n = strlen(str);
431 if (n >= (size_t)MAX_LINE_LENGTH) n = (size_t)MAX_LINE_LENGTH - 1;
432 memcpy(line, str, n);
433 line[n] = '\0';
434 *len = (int)strlen(line);
435 *pos = *len;
436 refresh_line(line, *len, *pos, prompt);
437}
438
439static void line_backspace(char *line, int *pos, int *len, const char *prompt) {
440 if (*pos <= 0) return;
441
442 int prev = utf8_prev_pos(line, *pos);
443 memmove(line + prev, line + *pos, (size_t)(*len - *pos + 1));
444 *len -= (*pos - prev);
445 *pos = prev;
446 refresh_line(line, *len, *pos, prompt);
447}
448
449static void line_delete(char *line, int *pos, int *len, const char *prompt) {
450 if (*pos >= *len) return;
451 int next = utf8_next_pos(line, *len, *pos);
452 memmove(line + *pos, line + next, (size_t)(*len - next + 1));
453 *len -= (next - *pos);
454 refresh_line(line, *len, *pos, prompt);
455}
456
457static void line_insert(char *line, int *pos, int *len, int c, const char *prompt) {
458 if (*len >= MAX_LINE_LENGTH - 1) return;
459
460 memmove(line + *pos + 1, line + *pos, (size_t)(*len - *pos + 1));
461 line[*pos] = (char)c;
462 (*pos)++;
463 (*len)++;
464 refresh_line(line, *len, *pos, prompt);
465}
466
467#ifdef _WIN32
468static key_event_t read_key(void) {
469 if (ctrl_c_pressed > 0) return (key_event_t){ KEY_EOF, 0 };
470 int c = _getch();
471 if (c == 0 || c == 0xE0) {
472 int ext = _getch();
473 switch (ext) {
474 case 72: return (key_event_t){ KEY_UP, 0 };
475 case 80: return (key_event_t){ KEY_DOWN, 0 };
476 case 77: return (key_event_t){ KEY_RIGHT, 0 };
477 case 75: return (key_event_t){ KEY_LEFT, 0 };
478 case 71: return (key_event_t){ KEY_HOME, 0 };
479 case 79: return (key_event_t){ KEY_END, 0 };
480 case 83: return (key_event_t){ KEY_DELETE, 0 };
481 default: return (key_event_t){ KEY_NONE, 0 };
482 }
483 }
484 if (c == 8) return (key_event_t){ KEY_BACKSPACE, 0 };
485 if (c == '\r' || c == '\n') return (key_event_t){ KEY_ENTER, 0 };
486 if (c == 3) {
487 ctrl_c_pressed++;
488 return (key_event_t){ KEY_EOF, 0 };
489 }
490 if (c == 4 || c == 26) return (key_event_t){ KEY_EOF, 0 };
491 if (isprint(c) || (unsigned char)c >= 0x80) return (key_event_t){ KEY_CHAR, c };
492 return (key_event_t){ KEY_NONE, 0 };
493}
494#else
495static struct termios saved_tio;
496
497static key_event_t read_key(void) {
498 if (ctrl_c_pressed > 0) return (key_event_t){ KEY_EOF, 0 };
499 int c = getchar();
500 if (c == EOF && !feof(stdin)) {
501 clearerr(stdin);
502 return (key_event_t){ KEY_EOF, 0 };
503 }
504 if (c == EOF) return (key_event_t){ KEY_EOF, 0 };
505
506 if (c == 27) {
507 int seq1 = getchar();
508 if (seq1 == EOF) return (key_event_t){ KEY_NONE, 0 };
509 if (seq1 == 'O') {
510 int seq2 = getchar();
511 if (seq2 == 'H') return (key_event_t){ KEY_HOME, 0 };
512 if (seq2 == 'F') return (key_event_t){ KEY_END, 0 };
513 return (key_event_t){ KEY_NONE, 0 };
514 }
515 if (seq1 != '[') return (key_event_t){ KEY_NONE, 0 };
516 int seq2 = getchar();
517 if (seq2 == EOF) return (key_event_t){ KEY_NONE, 0 };
518 switch (seq2) {
519 case 'A': return (key_event_t){ KEY_UP, 0 };
520 case 'B': return (key_event_t){ KEY_DOWN, 0 };
521 case 'C': return (key_event_t){ KEY_RIGHT, 0 };
522 case 'D': return (key_event_t){ KEY_LEFT, 0 };
523 case 'H': return (key_event_t){ KEY_HOME, 0 };
524 case 'F': return (key_event_t){ KEY_END, 0 };
525 default: {
526 if (seq2 >= '0' && seq2 <= '9') {
527 int seq3 = getchar();
528 if (seq3 == '~') {
529 if (seq2 == '1' || seq2 == '7') return (key_event_t){ KEY_HOME, 0 };
530 if (seq2 == '4' || seq2 == '8') return (key_event_t){ KEY_END, 0 };
531 if (seq2 == '3') return (key_event_t){ KEY_DELETE, 0 };
532 }
533 }
534 return (key_event_t){ KEY_NONE, 0 };
535 }
536 }
537 }
538
539 if (c == 127 || c == 8) return (key_event_t){ KEY_BACKSPACE, 0 };
540 if (c == '\n' || c == '\r') return (key_event_t){ KEY_ENTER, 0 };
541 if (isprint(c) || (unsigned char)c >= 0x80) return (key_event_t){ KEY_CHAR, c };
542 return (key_event_t){ KEY_NONE, 0 };
543}
544#endif
545
546static void term_restore(void) {
547#ifndef _WIN32
548 tcsetattr(STDIN_FILENO, TCSANOW, &saved_tio);
549#endif
550}
551
552static char *read_line_with_history(ant_history_t *hist, const char *prompt) {
553 char *line = malloc(MAX_LINE_LENGTH);
554 if (!line) return NULL;
555 int pos = 0;
556 int len = 0;
557 line[0] = '\0';
558 repl_last_cursor_row = 0;
559
560#ifndef _WIN32
561 struct termios new_tio;
562 tcgetattr(STDIN_FILENO, &saved_tio);
563 new_tio = saved_tio;
564 new_tio.c_lflag &= ~(ICANON | ECHO);
565 tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
566#endif
567
568 for (;;) {
569 key_event_t key = read_key();
570 static const void *dispatch[] = {
571 [KEY_NONE] = &&l_none,
572 [KEY_UP] = &&l_up,
573 [KEY_DOWN] = &&l_down,
574 [KEY_LEFT] = &&l_left,
575 [KEY_RIGHT] = &&l_right,
576 [KEY_HOME] = &&l_home,
577 [KEY_END] = &&l_end,
578 [KEY_DELETE] = &&l_delete,
579 [KEY_BACKSPACE] = &&l_backspace,
580 [KEY_ENTER] = &&l_enter,
581 [KEY_EOF] = &&l_eof,
582 [KEY_CHAR] = &&l_char,
583 };
584
585 unsigned int t = (unsigned int)key.type;
586 if (t < (sizeof(dispatch) / sizeof(*dispatch)) && dispatch[t]) goto *dispatch[t];
587 goto l_none;
588
589 l_enter:
590 putchar('\n');
591 term_restore();
592 return line;
593
594 l_eof:
595 putchar('\n');
596 term_restore();
597 free(line);
598 return NULL;
599
600 l_up: {
601 const char *h = ant_history_prev(hist);
602 if (h) line_set(line, &pos, &len, h, prompt);
603 continue;
604 }
605
606 l_down: {
607 const char *h = ant_history_next(hist);
608 if (h) line_set(line, &pos, &len, h, prompt);
609 continue;
610 }
611
612 l_left:
613 if (pos > 0) {
614 pos = utf8_prev_pos(line, pos);
615 move_cursor_only(line, pos, prompt);
616 }
617 continue;
618
619 l_right:
620 if (pos < len) {
621 pos = utf8_next_pos(line, len, pos);
622 move_cursor_only(line, pos, prompt);
623 }
624 continue;
625
626 l_home:
627 if (pos != 0) {
628 pos = 0;
629 move_cursor_only(line, pos, prompt);
630 }
631 continue;
632
633 l_end:
634 if (pos != len) {
635 pos = len;
636 move_cursor_only(line, pos, prompt);
637 }
638 continue;
639
640 l_delete:
641 line_delete(line, &pos, &len, prompt);
642 continue;
643
644 l_backspace:
645 line_backspace(line, &pos, &len, prompt);
646 continue;
647
648 l_char:
649 line_insert(line, &pos, &len, key.ch, prompt);
650 continue;
651
652 l_none:
653 continue;
654 }
655}
656
657ant_readline_result_t ant_readline(
658 ant_history_t *hist,
659 const char *prompt,
660 highlight_state line_state,
661 char **out_line
662) {
663 if (out_line) *out_line = NULL;
664
665 hl_line_state = line_state;
666 ctrl_c_pressed = 0;
667 char *line = read_line_with_history(hist, prompt);
668
669 if (ctrl_c_pressed > 0) {
670 if (line) free(line);
671 return ANT_READLINE_INTERRUPT;
672 }
673 if (!line) return ANT_READLINE_EOF;
674
675 if (out_line) *out_line = line;
676 return ANT_READLINE_LINE;
677}