MIRROR: javascript for 馃悳's, a tiny runtime with big ambitions
1#include <compat.h> // IWYU pragma: keep
2
3#include <stdbool.h>
4#include <errno.h>
5#include <stdio.h>
6#include <stdlib.h>
7#include <string.h>
8#include <ctype.h>
9
10#include <uv.h>
11
12#ifdef _WIN32
13#include <io.h>
14#define WIN32_LEAN_AND_MEAN
15#include <windows.h>
16#define ANT_ISATTY _isatty
17#define ANT_STDIN_FD 0
18#define ANT_STDOUT_FD 1
19#define ANT_STDERR_FD 2
20#else
21#include <signal.h>
22#include <sys/ioctl.h>
23#include <termios.h>
24#include <unistd.h>
25#define ANT_ISATTY isatty
26#define ANT_STDIN_FD STDIN_FILENO
27#define ANT_STDOUT_FD STDOUT_FILENO
28#define ANT_STDERR_FD STDERR_FILENO
29#endif
30
31#include "ant.h"
32#include "gc/roots.h"
33#include "descriptors.h"
34#include "errors.h"
35#include "internal.h"
36#include "runtime.h"
37#include "tty_ctrl.h"
38#include "silver/engine.h"
39
40#include "modules/stream.h"
41#include "modules/buffer.h"
42#include "modules/events.h"
43#include "modules/symbol.h"
44#include "modules/tty.h"
45
46static ant_value_t g_tty_readstream_proto = 0;
47static ant_value_t g_tty_readstream_ctor = 0;
48static ant_value_t g_tty_writestream_proto = 0;
49static ant_value_t g_tty_writestream_ctor = 0;
50
51typedef struct tty_read_stream_state {
52 ant_t *js;
53 ant_value_t stream_obj;
54 uv_tty_t tty;
55
56 int fd;
57 bool initialized;
58 bool reading;
59 bool closing;
60} tty_read_stream_state_t;
61
62static void invoke_callback_if_needed(ant_t *js, ant_value_t cb, ant_value_t arg) {
63 if (!is_callable(cb)) return;
64 ant_value_t cb_args[1] = { arg };
65 sv_vm_call(js->vm, js, cb, js_mkundef(), cb_args, 1, NULL, false);
66}
67
68static bool parse_fd(ant_value_t value, int *fd_out) {
69 int fd = 0;
70 if (!tty_ctrl_parse_int_value(value, &fd)) return false;
71 if (fd < 0) return false;
72 *fd_out = fd;
73 return true;
74}
75
76static bool is_tty_fd(int fd) {
77 if (fd < 0) return false;
78 return uv_guess_handle(fd) == UV_TTY;
79}
80
81static int stream_fd_from_this(ant_t *js, int fallback_fd) {
82 ant_value_t this_obj = js_getthis(js);
83 if (!is_special_object(this_obj)) return fallback_fd;
84
85 ant_value_t fd_val = js_get(js, this_obj, "fd");
86 int fd = 0;
87 if (!parse_fd(fd_val, &fd)) return fallback_fd;
88 return fd;
89}
90
91static tty_read_stream_state_t *tty_read_stream_state_from_obj(ant_value_t stream_obj) {
92 return (tty_read_stream_state_t *)stream_get_attached_state(stream_obj);
93}
94
95static ant_value_t make_stream_error(ant_t *js, const char *op, int fd, int uv_code) {
96 if (uv_code != 0) return js_mkerr_typed(
97 js, JS_ERR_GENERIC,
98 "tty stream %s failed for fd %d: %s",
99 op, fd, uv_strerror(uv_code)
100 );
101
102 return js_mkerr_typed(js, JS_ERR_GENERIC, "tty stream %s failed for fd %d", op, fd);
103}
104
105static void tty_read_stream_emit_error(tty_read_stream_state_t *state, const char *op, int uv_code) {
106 ant_value_t err = 0;
107 if (!state || !state->js || !is_object_type(state->stream_obj)) return;
108 err = make_stream_error(state->js, op, state->fd, uv_code);
109 eventemitter_emit_args(state->js, state->stream_obj, "error", &err, 1);
110}
111
112static void tty_read_stream_push_chunk(tty_read_stream_state_t *state, const char *data, size_t len) {
113 ArrayBufferData *ab = NULL;
114 ant_value_t chunk = 0;
115
116 if (!state || !state->js || !data || len == 0) return;
117 ab = create_array_buffer_data(len);
118 if (ab) memcpy(ab->data, data, len);
119
120 chunk = ab
121 ? create_typed_array(state->js, TYPED_ARRAY_UINT8, ab, 0, len, "Buffer")
122 : js_mkstr(state->js, data, len);
123 (void)stream_readable_push(state->js, state->stream_obj, chunk, js_mkundef());
124}
125
126static void tty_read_stream_alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
127 buf->base = malloc(suggested_size);
128#ifdef _WIN32
129 buf->len = (ULONG)suggested_size;
130#else
131 buf->len = suggested_size;
132#endif
133}
134
135static void tty_read_stream_stop(tty_read_stream_state_t *state) {
136 if (!state || !state->initialized || !state->reading) return;
137 uv_read_stop((uv_stream_t *)&state->tty);
138 state->reading = false;
139}
140
141static void tty_read_stream_close_cb(uv_handle_t *handle) {
142 tty_read_stream_state_t *state = (tty_read_stream_state_t *)handle->data;
143 free(state);
144}
145
146static void tty_read_stream_finalize(ant_t *js, ant_value_t stream_obj, void *data) {
147 tty_read_stream_state_t *state = (tty_read_stream_state_t *)data;
148
149 if (!state) return;
150 tty_read_stream_stop(state);
151
152 if (state->initialized && !state->closing) {
153 state->closing = true;
154 uv_close((uv_handle_t *)&state->tty, tty_read_stream_close_cb);
155 return;
156 }
157
158 if (!state->initialized) free(state);
159}
160
161static void tty_read_stream_on_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
162 tty_read_stream_state_t *state = (tty_read_stream_state_t *)stream->data;
163
164 if (!state || !state->js) goto cleanup;
165
166 if (nread > 0) {
167 tty_read_stream_push_chunk(state, buf->base, (size_t)nread);
168 goto cleanup;
169 }
170
171 if (nread == UV_EOF) {
172 tty_read_stream_stop(state);
173 (void)stream_readable_push(state->js, state->stream_obj, js_mknull(), js_mkundef());
174 goto cleanup;
175 }
176
177 if (nread < 0) {
178 tty_read_stream_stop(state);
179 tty_read_stream_emit_error(state, "read", (int)nread);
180 }
181
182cleanup:
183 if (buf->base) free(buf->base);
184}
185
186static ant_value_t tty_readstream__read(ant_t *js, ant_value_t *args, int nargs) {
187 ant_value_t stream_obj = js_getthis(js);
188 tty_read_stream_state_t *state = tty_read_stream_state_from_obj(stream_obj);
189 int rc = 0;
190
191 if (!state) return js_mkundef();
192 if (state->closing || js_truthy(js, js_get(js, stream_obj, "destroyed"))) return js_mkundef();
193
194 if (!state->initialized) {
195 rc = uv_tty_init(uv_default_loop(), &state->tty, state->fd, 0);
196 if (rc != 0) {
197 tty_read_stream_emit_error(state, "open", rc);
198 return js_mkundef();
199 }
200 state->tty.data = state;
201 state->initialized = true;
202 }
203
204 if (state->reading) return js_mkundef();
205 rc = uv_read_start((uv_stream_t *)&state->tty, tty_read_stream_alloc_buffer, tty_read_stream_on_read);
206
207 if (rc != 0) {
208 tty_read_stream_emit_error(state, "read", rc);
209 return js_mkundef();
210 }
211
212 state->reading = true;
213 return js_mkundef();
214}
215
216static ant_value_t tty_readstream__destroy(ant_t *js, ant_value_t *args, int nargs) {
217 ant_value_t stream_obj = js_getthis(js);
218 tty_read_stream_state_t *state = tty_read_stream_state_from_obj(stream_obj);
219 ant_value_t cb = nargs > 1 ? args[1] : js_mkundef();
220
221 if (!state) {
222 invoke_callback_if_needed(js, cb, js_mknull());
223 return js_mkundef();
224 }
225
226 tty_read_stream_stop(state);
227 stream_clear_attached_state(stream_obj);
228
229 if (state->initialized && !state->closing) {
230 state->closing = true;
231 uv_close((uv_handle_t *)&state->tty, tty_read_stream_close_cb);
232 } else if (!state->initialized) free(state);
233
234 invoke_callback_if_needed(js, cb, js_mknull());
235 return js_mkundef();
236}
237
238static void get_tty_size(int fd, int *rows, int *cols) {
239 int out_rows = 24;
240 int out_cols = 80;
241
242#ifdef _WIN32
243 HANDLE handle = INVALID_HANDLE_VALUE;
244 if (fd == ANT_STDOUT_FD) {
245 handle = GetStdHandle(STD_OUTPUT_HANDLE);
246 } else if (fd == ANT_STDERR_FD) {
247 handle = GetStdHandle(STD_ERROR_HANDLE);
248 } else {
249 intptr_t os_handle = _get_osfhandle(fd);
250 if (os_handle != -1) handle = (HANDLE)os_handle;
251 }
252
253 if (handle != INVALID_HANDLE_VALUE) {
254 CONSOLE_SCREEN_BUFFER_INFO csbi;
255 if (GetConsoleScreenBufferInfo(handle, &csbi)) {
256 int width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
257 int height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
258 if (height > 0) out_rows = height;
259 if (width > 0) out_cols = width;
260 }
261 }
262#else
263 struct winsize ws;
264 if (ioctl(fd, TIOCGWINSZ, &ws) == 0) {
265 if (ws.ws_row > 0) out_rows = (int)ws.ws_row;
266 if (ws.ws_col > 0) out_cols = (int)ws.ws_col;
267 }
268#endif
269
270 if (rows) *rows = out_rows;
271 if (cols) *cols = out_cols;
272}
273
274static bool str_case_eq(const char *a, const char *b) {
275 if (!a || !b) return false;
276 while (*a && *b) {
277 int ca = tolower((unsigned char)*a);
278 int cb = tolower((unsigned char)*b);
279 if (ca != cb) return false;
280 a++;
281 b++;
282 }
283 return *a == '\0' && *b == '\0';
284}
285
286static bool str_case_contains(const char *haystack, const char *needle) {
287 if (!haystack || !needle) return false;
288 size_t needle_len = strlen(needle);
289 if (needle_len == 0) return true;
290
291 size_t hay_len = strlen(haystack);
292 if (needle_len > hay_len) return false;
293
294 for (size_t i = 0; i + needle_len <= hay_len; i++) {
295 bool match = true;
296 for (size_t j = 0; j < needle_len; j++) {
297 int ca = tolower((unsigned char)haystack[i + j]);
298 int cb = tolower((unsigned char)needle[j]);
299 if (ca != cb) {
300 match = false;
301 break;
302 }
303 }
304 if (match) return true;
305 }
306 return false;
307}
308
309static bool read_env_object_value(
310 ant_t *js,
311 ant_value_t env_obj,
312 const char *key,
313 char *buf,
314 size_t buf_len
315) {
316 if (!is_special_object(env_obj) || !key || !buf || buf_len == 0) return false;
317
318 ant_value_t value = js_get(js, env_obj, key);
319 if (vtype(value) == T_UNDEF || vtype(value) == T_NULL) return false;
320
321 ant_value_t str_val = js_tostring_val(js, value);
322 size_t len = 0;
323 char *str = js_getstr(js, str_val, &len);
324 if (!str) return false;
325
326 if (len >= buf_len) len = buf_len - 1;
327 memcpy(buf, str, len);
328 buf[len] = '\0';
329 return true;
330}
331
332static const char *get_env_value(
333 ant_t *js,
334 ant_value_t env_obj,
335 const char *key,
336 char *buf,
337 size_t buf_len
338) {
339 if (read_env_object_value(js, env_obj, key, buf, buf_len)) return buf;
340 return getenv(key);
341}
342
343static int force_color_depth(const char *force_color) {
344 if (!force_color) return 0;
345 if (*force_color == '\0') return 4;
346 if (strcmp(force_color, "0") == 0) return 1;
347 if (strcmp(force_color, "1") == 0) return 4;
348 if (strcmp(force_color, "2") == 0) return 8;
349 if (strcmp(force_color, "3") == 0) return 24;
350 return 4;
351}
352
353static int detect_color_depth(ant_t *js, int fd, ant_value_t env_obj) {
354 char scratch[128];
355 const char *force_color = get_env_value(js, env_obj, "FORCE_COLOR", scratch, sizeof(scratch));
356
357 int forced = force_color_depth(force_color);
358 if (forced > 0) return forced;
359
360 const char *no_color = get_env_value(js, env_obj, "NO_COLOR", scratch, sizeof(scratch));
361 if (no_color && *no_color) return 1;
362 if (!is_tty_fd(fd)) return 1;
363
364 const char *colorterm = get_env_value(js, env_obj, "COLORTERM", scratch, sizeof(scratch));
365 if (colorterm) {
366 if (str_case_contains(colorterm, "truecolor") || str_case_contains(colorterm, "24bit")) return 24;
367 }
368
369#ifdef _WIN32
370 return 24;
371#else
372 const char *term = get_env_value(js, env_obj, "TERM", scratch, sizeof(scratch));
373 if (!term) return 4;
374 if (str_case_eq(term, "dumb")) return 1;
375 if (str_case_contains(term, "256color")) return 8;
376 if (str_case_contains(term, "color")
377 || str_case_contains(term, "xterm")
378 || str_case_contains(term, "screen")
379 || str_case_contains(term, "ansi")
380 || str_case_contains(term, "linux")
381 || str_case_contains(term, "cygwin")
382 || str_case_contains(term, "vt100")
383 ) return 4;
384 return 4;
385#endif
386}
387
388static int palette_size_for_depth(int depth) {
389 switch (depth) {
390 case 1: return 2;
391 case 4: return 16;
392 case 8: return 256;
393 case 24: return 16777216;
394 default: return 2;
395 }
396}
397
398static ant_value_t maybe_callback_or_throw(
399 ant_t *js, ant_value_t this_obj,
400 ant_value_t cb, bool ok,
401 const char *op, int fd
402) {
403 if (is_callable(cb)) {
404 if (ok) invoke_callback_if_needed(js, cb, js_mknull());
405 else {
406 ant_value_t err = make_stream_error(js, op, fd, 0);
407 invoke_callback_if_needed(js, cb, err);
408 }
409 return this_obj;
410 }
411 if (!ok) return make_stream_error(js, op, fd, 0);
412 return this_obj;
413}
414
415#ifndef _WIN32
416static struct {
417 int fd;
418 bool active;
419 struct termios saved;
420} raw_state = { .fd = -1, .active = false };
421
422static int tty_tcsetattr_no_sigtou(int fd, int optional_actions, const struct termios *tio) {
423#ifdef SIGTTOU
424 sigset_t block_set;
425 sigset_t prev_set;
426 if (sigemptyset(&block_set) == 0
427 && sigaddset(&block_set, SIGTTOU) == 0
428 && sigprocmask(SIG_BLOCK, &block_set, &prev_set) == 0) {
429 int rc = tcsetattr(fd, optional_actions, tio);
430 int saved_errno = errno;
431 sigprocmask(SIG_SETMASK, &prev_set, NULL);
432 errno = saved_errno;
433 return rc;
434 }
435#endif
436 return tcsetattr(fd, optional_actions, tio);
437}
438#endif
439
440bool tty_set_raw_mode(int fd, bool enable) {
441#ifdef _WIN32
442 intptr_t os_handle = _get_osfhandle(fd);
443 if (os_handle == -1) return false;
444 HANDLE handle = (HANDLE)os_handle;
445
446 DWORD mode = 0;
447 if (!GetConsoleMode(handle, &mode)) return false;
448
449 if (enable) {
450 mode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);
451 } else {
452 mode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT;
453 }
454
455 return SetConsoleMode(handle, mode) != 0;
456#else
457 if (!is_tty_fd(fd)) return false;
458
459 if (enable) {
460 if (raw_state.active && raw_state.fd == fd) return true;
461
462 struct termios saved;
463 if (tcgetattr(fd, &saved) == -1) return false;
464
465 struct termios raw = saved;
466 raw.c_lflag &= ~(ICANON | ECHO | ISIG);
467 raw.c_iflag &= ~(IXON | ICRNL);
468 raw.c_cc[VMIN] = 1;
469 raw.c_cc[VTIME] = 0;
470#ifdef VDISCARD
471 raw.c_cc[VDISCARD] = _POSIX_VDISABLE;
472#endif
473#ifdef VLNEXT
474 raw.c_cc[VLNEXT] = _POSIX_VDISABLE;
475#endif
476 if (tty_tcsetattr_no_sigtou(fd, TCSANOW, &raw) == -1) return false;
477 raw_state.fd = fd;
478 raw_state.saved = saved;
479 raw_state.active = true;
480 return true;
481 }
482
483 if (!(raw_state.active && raw_state.fd == fd)) return true;
484 if (tty_tcsetattr_no_sigtou(fd, TCSANOW, &raw_state.saved) == -1) return false;
485 raw_state.fd = -1;
486 raw_state.active = false;
487 return true;
488#endif
489}
490
491bool tty_is_raw_mode(int fd) {
492#ifdef _WIN32
493 return false;
494#else
495 return raw_state.active && raw_state.fd == fd;
496#endif
497}
498
499static ant_value_t get_process_stream(ant_t *js, const char *name) {
500 ant_value_t process_obj = js_get(js, js_glob(js), "process");
501 if (!is_special_object(process_obj)) return js_mkundef();
502
503 ant_value_t stream = js_get(js, process_obj, name);
504 if (!is_special_object(stream)) return js_mkundef();
505 return stream;
506}
507
508static void ensure_stream_common_props(ant_t *js, ant_value_t stream, int fd) {
509 if (!is_special_object(stream)) return;
510 js_set(js, stream, "fd", js_mknum((double)fd));
511 js_set(js, stream, "isTTY", js_bool(is_tty_fd(fd)));
512}
513
514static ant_value_t tty_isatty(ant_t *js, ant_value_t *args, int nargs) {
515 if (nargs < 1) return js_false;
516
517 int fd = 0;
518 if (!parse_fd(args[0], &fd)) return js_false;
519 return js_bool(ANT_ISATTY(fd) != 0);
520}
521
522static ant_value_t tty_stream_write(ant_t *js, ant_value_t *args, int nargs) {
523 if (nargs < 1) return js_false;
524
525 size_t len = 0;
526 char *data = js_getstr(js, args[0], &len);
527 if (!data) return js_false;
528
529 ant_value_t cb = js_mkundef();
530 if (nargs > 1 && is_callable(args[1])) cb = args[1];
531
532 int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
533 bool ok = tty_ctrl_write_fd(fd, data, len);
534
535 if (is_callable(cb)) {
536 if (ok) invoke_callback_if_needed(js, cb, js_mknull());
537 else invoke_callback_if_needed(js, cb, make_stream_error(js, "write", fd, 0));
538 }
539
540 if (!ok) return js_false;
541 return js_true;
542}
543
544static ant_value_t tty_write_stream_rows_getter(ant_t *js, ant_value_t *args, int nargs) {
545 int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
546 if (!is_tty_fd(fd)) return js_mkundef();
547
548 int rows = 0;
549 int cols = 0;
550 get_tty_size(fd, &rows, &cols);
551 return js_mknum((double)rows);
552}
553
554static ant_value_t tty_write_stream_columns_getter(ant_t *js, ant_value_t *args, int nargs) {
555 int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
556 if (!is_tty_fd(fd)) return js_mkundef();
557
558 int rows = 0;
559 int cols = 0;
560 get_tty_size(fd, &rows, &cols);
561 return js_mknum((double)cols);
562}
563
564static ant_value_t tty_write_stream_get_window_size(ant_t *js, ant_value_t *args, int nargs) {
565 int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
566 int rows = 0;
567 int cols = 0;
568 get_tty_size(fd, &rows, &cols);
569
570 ant_value_t arr = js_mkarr(js);
571 js_arr_push(js, arr, js_mknum((double)cols));
572 js_arr_push(js, arr, js_mknum((double)rows));
573 return arr;
574}
575
576static ant_value_t tty_write_stream_clear_line(ant_t *js, ant_value_t *args, int nargs) {
577 ant_value_t this_obj = js_getthis(js);
578
579 int dir = 0;
580 if (!tty_ctrl_parse_clear_line_dir(args, nargs, 0, &dir)) {
581 return js_mkerr_typed(js, JS_ERR_TYPE, "clearLine(dir) requires a numeric dir");
582 }
583
584 ant_value_t cb = js_mkundef();
585 if (nargs > 1 && is_callable(args[1])) cb = args[1];
586
587 int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
588 size_t seq_len = 0;
589
590 const char *seq = tty_ctrl_clear_line_seq(dir, &seq_len);
591 bool ok = tty_ctrl_write_fd(fd, seq, seq_len);
592
593 return maybe_callback_or_throw(js, this_obj, cb, ok, "clearLine", fd);
594}
595
596static ant_value_t tty_write_stream_clear_screen_down(ant_t *js, ant_value_t *args, int nargs) {
597 ant_value_t this_obj = js_getthis(js);
598
599 ant_value_t cb = js_mkundef();
600 if (nargs > 0 && is_callable(args[0])) cb = args[0];
601
602 int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
603 size_t seq_len = 0;
604
605 const char *seq = tty_ctrl_clear_screen_down_seq(&seq_len);
606 bool ok = tty_ctrl_write_fd(fd, seq, seq_len);
607
608 return maybe_callback_or_throw(js, this_obj, cb, ok, "clearScreenDown", fd);
609}
610
611static ant_value_t tty_write_stream_cursor_to(ant_t *js, ant_value_t *args, int nargs) {
612 ant_value_t this_obj = js_getthis(js);
613 int x = 0;
614
615 if (nargs < 1 || !tty_ctrl_parse_int_value(args[0], &x)) {
616 return js_mkerr_typed(js, JS_ERR_TYPE, "cursorTo(x[, y][, callback]) requires numeric x");
617 }
618
619 x = tty_ctrl_normalize_coord(x);
620
621 bool has_y = false;
622 int y = 0;
623 ant_value_t cb = js_mkundef();
624
625 if (nargs > 1) {
626 if (is_callable(args[1])) {
627 cb = args[1];
628 } else if (vtype(args[1]) == T_UNDEF) {
629 // no-op
630 } else if (tty_ctrl_parse_int_value(args[1], &y)) {
631 has_y = true;
632 y = tty_ctrl_normalize_coord(y);
633 if (nargs > 2 && is_callable(args[2])) cb = args[2];
634 } else {
635 return js_mkerr_typed(js, JS_ERR_TYPE, "cursorTo y must be a number when provided");
636 }
637 }
638
639 char seq[64];
640 size_t seq_len = 0;
641 if (!tty_ctrl_build_cursor_to(seq, sizeof(seq), x, has_y, y, &seq_len)) {
642 return js_mkerr(js, "Failed to build cursor sequence");
643 }
644
645 int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
646 bool ok = tty_ctrl_write_fd(fd, seq, seq_len);
647 return maybe_callback_or_throw(js, this_obj, cb, ok, "cursorTo", fd);
648}
649
650static ant_value_t tty_write_stream_move_cursor(ant_t *js, ant_value_t *args, int nargs) {
651 ant_value_t this_obj = js_getthis(js);
652 if (nargs < 2) {
653 return js_mkerr_typed(js, JS_ERR_TYPE, "moveCursor(dx, dy[, callback]) requires dx and dy");
654 }
655
656 int dx = 0;
657 int dy = 0;
658 if (!tty_ctrl_parse_move_cursor_args(args, nargs, 0, 1, &dx, &dy)) {
659 return js_mkerr_typed(js, JS_ERR_TYPE, "moveCursor(dx, dy[, callback]) requires numeric dx and dy");
660 }
661
662 ant_value_t cb = js_mkundef();
663 if (nargs > 2 && is_callable(args[2])) cb = args[2];
664
665 int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
666 bool ok = true;
667
668 if (dx != 0) {
669 char seq_x[32];
670 size_t len_x = 0;
671 if (!tty_ctrl_build_move_cursor_axis(seq_x, sizeof(seq_x), dx, true, &len_x)) {
672 return js_mkerr(js, "Failed to build moveCursor sequence");
673 }
674 if (!tty_ctrl_write_fd(fd, seq_x, len_x)) ok = false;
675 }
676
677 if (ok && dy != 0) {
678 char seq_y[32];
679 size_t len_y = 0;
680 if (!tty_ctrl_build_move_cursor_axis(seq_y, sizeof(seq_y), dy, false, &len_y)) {
681 return js_mkerr(js, "Failed to build moveCursor sequence");
682 }
683 if (!tty_ctrl_write_fd(fd, seq_y, len_y)) ok = false;
684 }
685
686 return maybe_callback_or_throw(js, this_obj, cb, ok, "moveCursor", fd);
687}
688
689static ant_value_t tty_write_stream_get_color_depth(ant_t *js, ant_value_t *args, int nargs) {
690 ant_value_t env_obj = js_mkundef();
691 if (nargs > 0 && is_special_object(args[0])) env_obj = args[0];
692
693 int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
694 int depth = detect_color_depth(js, fd, env_obj);
695 return js_mknum((double)depth);
696}
697
698static ant_value_t tty_write_stream_has_colors(ant_t *js, ant_value_t *args, int nargs) {
699 int count = 16;
700 ant_value_t env_obj = js_mkundef();
701
702 if (nargs > 0) {
703 if (vtype(args[0]) == T_NUM) {
704 int parsed_count = 16;
705 if (!tty_ctrl_parse_int_value(args[0], &parsed_count)) {
706 return js_mkerr_typed(js, JS_ERR_TYPE, "hasColors(count[, env]) count must be an integer");
707 }
708 count = parsed_count;
709 } else if (is_special_object(args[0])) {
710 env_obj = args[0];
711 } else if (vtype(args[0]) != T_UNDEF) {
712 return js_mkerr_typed(js, JS_ERR_TYPE, "hasColors(count[, env]) invalid first argument");
713 }
714 }
715
716 if (nargs > 1 && is_special_object(args[1])) env_obj = args[1];
717 if (count < 1) count = 1;
718
719 int fd = stream_fd_from_this(js, ANT_STDOUT_FD);
720 int depth = detect_color_depth(js, fd, env_obj);
721 int max_colors = palette_size_for_depth(depth);
722 return js_bool(max_colors >= count);
723}
724
725static ant_value_t tty_read_stream_set_raw_mode(ant_t *js, ant_value_t *args, int nargs) {
726 ant_value_t this_obj = js_getthis(js);
727 if (!is_special_object(this_obj)) {
728 return js_mkerr_typed(js, JS_ERR_TYPE, "setRawMode() requires a ReadStream receiver");
729 }
730
731 bool enable = nargs > 0 ? js_truthy(js, args[0]) : true;
732 int fd = stream_fd_from_this(js, ANT_STDIN_FD);
733 if (!tty_set_raw_mode(fd, enable)) {
734 return js_mkerr_typed(js, JS_ERR_GENERIC, "Failed to set raw mode for fd %d", fd);
735 }
736 js_set(js, this_obj, "isRaw", js_bool(enable));
737 return this_obj;
738}
739
740static ant_value_t tty_read_stream_constructor(ant_t *js, ant_value_t *args, int nargs) {
741 tty_read_stream_state_t *state = NULL;
742 if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "ReadStream(fd) requires a file descriptor");
743
744 int fd = 0;
745 if (!parse_fd(args[0], &fd)) {
746 return js_mkerr_typed(js, JS_ERR_TYPE, "ReadStream(fd) requires an integer file descriptor");
747 }
748 if (!is_tty_fd(fd)) {
749 return js_mkerr_typed(js, JS_ERR_TYPE, "ReadStream fd %d is not a TTY", fd);
750 }
751
752 if (fd == ANT_STDIN_FD) {
753 ant_value_t stdin_obj = get_process_stream(js, "stdin");
754 if (is_special_object(stdin_obj)) {
755 ensure_stream_common_props(js, stdin_obj, fd);
756 if (vtype(js_get(js, stdin_obj, "isRaw")) == T_UNDEF) js_set(js, stdin_obj, "isRaw", js_false);
757 return stdin_obj;
758 }}
759
760 ant_value_t obj = stream_construct_readable(js, g_tty_readstream_proto, js_mkundef());
761 if (is_err(obj)) return obj;
762
763 state = calloc(1, sizeof(*state));
764 if (!state) return js_mkerr(js, "Out of memory");
765 state->js = js;
766 state->stream_obj = obj;
767 state->fd = fd;
768
769 ensure_stream_common_props(js, obj, fd);
770 stream_set_attached_state(obj, state, tty_read_stream_finalize);
771
772 js_set(js, obj, "isRaw", js_false);
773 js_set(js, obj, "_read", js_mkfun(tty_readstream__read));
774 js_set(js, obj, "_destroy", js_mkfun(tty_readstream__destroy));
775
776 return obj;
777}
778
779static ant_value_t tty_write_stream_constructor(ant_t *js, ant_value_t *args, int nargs) {
780 if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "WriteStream(fd) requires a file descriptor");
781
782 int fd = 0;
783 if (!parse_fd(args[0], &fd)) {
784 return js_mkerr_typed(js, JS_ERR_TYPE, "WriteStream(fd) requires an integer file descriptor");
785 }
786 if (!is_tty_fd(fd)) {
787 return js_mkerr_typed(js, JS_ERR_TYPE, "WriteStream fd %d is not a TTY", fd);
788 }
789
790 if (fd == ANT_STDOUT_FD || fd == ANT_STDERR_FD) {
791 ant_value_t stream = get_process_stream(js, fd == ANT_STDOUT_FD ? "stdout" : "stderr");
792 if (is_special_object(stream)) {
793 ensure_stream_common_props(js, stream, fd);
794 return stream;
795 }
796 }
797
798 ant_value_t obj = stream_construct_writable(js, g_tty_writestream_proto, js_mkundef());
799 if (is_err(obj)) return obj;
800
801 ensure_stream_common_props(js, obj, fd);
802 return obj;
803}
804
805static void setup_readstream_proto(ant_t *js, ant_value_t proto) {
806 if (!is_special_object(proto)) return;
807 js_set(js, proto, "setRawMode", js_mkfun(tty_read_stream_set_raw_mode));
808 js_set_sym(js, proto, get_toStringTag_sym(), js_mkstr(js, "ReadStream", 10));
809}
810
811static void setup_writestream_proto(ant_t *js, ant_value_t proto) {
812 if (!is_special_object(proto)) return;
813
814 js_set(js, proto, "write", js_mkfun(tty_stream_write));
815 js_set(js, proto, "clearLine", js_mkfun(tty_write_stream_clear_line));
816 js_set(js, proto, "clearScreenDown", js_mkfun(tty_write_stream_clear_screen_down));
817 js_set(js, proto, "cursorTo", js_mkfun(tty_write_stream_cursor_to));
818 js_set(js, proto, "moveCursor", js_mkfun(tty_write_stream_move_cursor));
819 js_set(js, proto, "getWindowSize", js_mkfun(tty_write_stream_get_window_size));
820 js_set(js, proto, "getColorDepth", js_mkfun(tty_write_stream_get_color_depth));
821 js_set(js, proto, "hasColors", js_mkfun(tty_write_stream_has_colors));
822 js_set_getter_desc(js, proto, "rows", 4, js_mkfun(tty_write_stream_rows_getter), JS_DESC_E | JS_DESC_C);
823 js_set_getter_desc(js, proto, "columns", 7, js_mkfun(tty_write_stream_columns_getter), JS_DESC_E | JS_DESC_C);
824 js_set_sym(js, proto, get_toStringTag_sym(), js_mkstr(js, "WriteStream", 11));
825}
826
827static void tty_init_stream_constructors(ant_t *js) {
828 if (g_tty_readstream_ctor && g_tty_writestream_ctor) return;
829 stream_init_constructors(js);
830
831 g_tty_readstream_proto = js_mkobj(js);
832 js_set_proto_init(g_tty_readstream_proto, stream_readable_prototype(js));
833 setup_readstream_proto(js, g_tty_readstream_proto);
834
835 g_tty_readstream_ctor = js_make_ctor(js, tty_read_stream_constructor, g_tty_readstream_proto, "ReadStream", 10);
836 js_set_proto_init(g_tty_readstream_ctor, stream_readable_constructor(js));
837
838 gc_register_root(&g_tty_readstream_proto);
839 gc_register_root(&g_tty_readstream_ctor);
840
841 g_tty_writestream_proto = js_mkobj(js);
842 js_set_proto_init(g_tty_writestream_proto, stream_writable_prototype(js));
843 setup_writestream_proto(js, g_tty_writestream_proto);
844
845 g_tty_writestream_ctor = js_make_ctor(js, tty_write_stream_constructor, g_tty_writestream_proto, "WriteStream", 11);
846 js_set_proto_init(g_tty_writestream_ctor, stream_writable_constructor(js));
847
848 gc_register_root(&g_tty_writestream_proto);
849 gc_register_root(&g_tty_writestream_ctor);
850}
851
852void init_tty_module(void) {
853 ant_t *js = rt->js;
854 if (!js) return;
855 tty_init_stream_constructors(js);
856
857 ant_value_t process_obj = js_get(js, js_glob(js), "process");
858 if (!is_special_object(process_obj)) return;
859
860 ant_value_t stdin_obj = js_get(js, process_obj, "stdin");
861 if (is_special_object(stdin_obj)) {
862 ensure_stream_common_props(js, stdin_obj, ANT_STDIN_FD);
863 if (vtype(js_get(js, stdin_obj, "isRaw")) == T_UNDEF) js_set(js, stdin_obj, "isRaw", js_false);
864
865 ant_value_t stdin_proto = js_get_proto(js, stdin_obj);
866 if (is_special_object(stdin_proto)) {
867 js_set_proto_init(stdin_proto, stream_readable_prototype(js));
868 setup_readstream_proto(js, stdin_proto);
869 }
870 stream_init_readable_object(js, stdin_obj, js_mkundef());
871 }
872
873 ant_value_t stdout_obj = js_get(js, process_obj, "stdout");
874 if (is_special_object(stdout_obj)) {
875 ensure_stream_common_props(js, stdout_obj, ANT_STDOUT_FD);
876 js_set_getter_desc(js, stdout_obj, "rows", 4, js_mkfun(tty_write_stream_rows_getter), JS_DESC_E | JS_DESC_C);
877 js_set_getter_desc(js, stdout_obj, "columns", 7, js_mkfun(tty_write_stream_columns_getter), JS_DESC_E | JS_DESC_C);
878
879 ant_value_t stdout_proto = js_get_proto(js, stdout_obj);
880 if (is_special_object(stdout_proto)) js_set_proto_init(stdout_proto, stream_duplex_prototype(js));
881 setup_writestream_proto(js, stdout_proto);
882 stream_init_duplex_object(js, stdout_obj, js_mkundef());
883 js_set(js, stdout_obj, "readable", js_false);
884 }
885
886 ant_value_t stderr_obj = js_get(js, process_obj, "stderr");
887 if (is_special_object(stderr_obj)) {
888 ensure_stream_common_props(js, stderr_obj, ANT_STDERR_FD);
889 js_set_getter_desc(js, stderr_obj, "rows", 4, js_mkfun(tty_write_stream_rows_getter), JS_DESC_E | JS_DESC_C);
890 js_set_getter_desc(js, stderr_obj, "columns", 7, js_mkfun(tty_write_stream_columns_getter), JS_DESC_E | JS_DESC_C);
891
892 ant_value_t stderr_proto = js_get_proto(js, stderr_obj);
893 if (is_special_object(stderr_proto)) js_set_proto_init(stderr_proto, stream_duplex_prototype(js));
894 setup_writestream_proto(js, stderr_proto);
895 stream_init_duplex_object(js, stderr_obj, js_mkundef());
896 js_set(js, stderr_obj, "readable", js_false);
897 }
898}
899
900ant_value_t tty_library(ant_t *js) {
901 ant_value_t lib = js_mkobj(js);
902 tty_init_stream_constructors(js);
903
904 js_set(js, lib, "isatty", js_mkfun(tty_isatty));
905 js_set(js, lib, "ReadStream", g_tty_readstream_ctor);
906 js_set(js, lib, "WriteStream", g_tty_writestream_ctor);
907 js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "tty", 3));
908
909 return lib;
910}