MIRROR: javascript for ๐Ÿœ's, a tiny runtime with big ambitions
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add process.stdin support to readline module

+599 -69
+203
examples/tui/button.js
··· 1 + const ESC = '\x1b'; 2 + const CSI = `${ESC}[`; 3 + 4 + const codes = { 5 + altScreenOn: `${CSI}?1049h`, 6 + altScreenOff: `${CSI}?1049l`, 7 + hideCursor: `${CSI}?25l`, 8 + showCursor: `${CSI}?25h`, 9 + clear: `${CSI}2J`, 10 + home: `${CSI}H`, 11 + reset: `${CSI}0m`, 12 + mouseOn: `${CSI}?1000h${CSI}?1003h${CSI}?1006h`, 13 + mouseOff: `${CSI}?1000l${CSI}?1003l${CSI}?1006l`, 14 + invert: `${CSI}7m`, 15 + fg: n => `${CSI}38;5;${n}m`, 16 + bg: n => `${CSI}48;5;${n}m` 17 + }; 18 + 19 + const state = { 20 + hover: false, 21 + pressed: false, 22 + message: 'Hover or click the button' 23 + }; 24 + 25 + const button = { 26 + label: 'Click Me', 27 + x: 0, 28 + y: 0, 29 + width: 0, 30 + height: 1 31 + }; 32 + 33 + const styles = { 34 + normal: '', 35 + hover: `${codes.bg(46)}${codes.fg(16)}`, 36 + active: `${codes.bg(196)}${codes.fg(15)}` 37 + }; 38 + 39 + function write(text) { 40 + process.stdout.write(text); 41 + } 42 + 43 + function centerButton(width, height) { 44 + button.width = button.label.length + 4; 45 + button.x = Math.max(0, Math.floor((width - button.width) / 2)); 46 + button.y = Math.max(0, Math.floor((height - button.height) / 2)); 47 + } 48 + 49 + function createGrid(width, height) { 50 + const chars = Array(height).fill(null).map(() => Array(width).fill(' ')); 51 + const stylesGrid = Array(height).fill(null).map(() => Array(width).fill('')); 52 + return { chars, styles: stylesGrid }; 53 + } 54 + 55 + function setCell(grid, x, y, ch, style) { 56 + if (y < 0 || y >= grid.chars.length) return; 57 + if (x < 0 || x >= grid.chars[y].length) return; 58 + grid.chars[y][x] = ch; 59 + grid.styles[y][x] = style; 60 + } 61 + 62 + function setText(grid, x, y, text, style) { 63 + for (let i = 0; i < text.length; i++) { 64 + setCell(grid, x + i, y, text[i], style); 65 + } 66 + } 67 + 68 + function drawButton(grid) { 69 + const fillStyle = state.pressed 70 + ? styles.active 71 + : state.hover 72 + ? styles.hover 73 + : styles.normal; 74 + for (let row = 0; row < button.height; row++) { 75 + for (let col = 0; col < button.width; col++) { 76 + setCell(grid, button.x + col, button.y + row, ' ', fillStyle); 77 + } 78 + } 79 + 80 + const padTotal = Math.max(0, button.width - button.label.length); 81 + const padLeft = Math.floor(padTotal / 2); 82 + const labelX = button.x + padLeft; 83 + const labelY = button.y + Math.floor(button.height / 2); 84 + setText(grid, labelX, labelY, button.label, fillStyle); 85 + } 86 + 87 + function render() { 88 + const width = process.stdout.columns || 80; 89 + const height = process.stdout.rows || 24; 90 + centerButton(width, height); 91 + 92 + const grid = createGrid(width, height); 93 + const header = ' Simple Button TUI (q to quit)'; 94 + setText(grid, 0, 0, header.slice(0, width), ''); 95 + 96 + const message = state.message; 97 + const messageLine = Math.min(height - 2, button.y + button.height + 1); 98 + if (messageLine >= 0 && messageLine < height) { 99 + setText(grid, 1, messageLine, message.slice(0, width - 2), ''); 100 + } 101 + 102 + drawButton(grid); 103 + const rows = grid.chars.map((row, y) => { 104 + let line = ''; 105 + let currentStyle = ''; 106 + for (let x = 0; x < row.length; x++) { 107 + const nextStyle = grid.styles[y][x]; 108 + if (nextStyle !== currentStyle) { 109 + line += nextStyle || codes.reset; 110 + currentStyle = nextStyle; 111 + } 112 + line += row[x]; 113 + } 114 + return line + codes.reset; 115 + }); 116 + write(codes.home + codes.clear + rows.join('\n') + codes.reset); 117 + } 118 + 119 + function isInsideButton(x, y) { 120 + return ( 121 + x >= button.x && 122 + x < button.x + button.width && 123 + y >= button.y && 124 + y < button.y + button.height 125 + ); 126 + } 127 + 128 + function parseMouseEvent(seq) { 129 + const match = seq.match(/^\x1b\[<([0-9]+);([0-9]+);([0-9]+)([mM])$/); 130 + if (!match) return false; 131 + 132 + const [, codeStr, xStr, yStr, action] = match; 133 + const code = Number(codeStr); 134 + const x = Number(xStr) - 1; 135 + const y = Number(yStr) - 1; 136 + const buttonCode = code & 3; 137 + 138 + const inside = isInsideButton(x, y); 139 + const prevHover = state.hover; 140 + state.hover = inside; 141 + 142 + if (action === 'M' && buttonCode === 0 && inside) { 143 + state.pressed = true; 144 + state.message = 'Button pressed! Release to click.'; 145 + } else if (action === 'm') { 146 + if (state.pressed && inside) { 147 + state.message = 'Button clicked!'; 148 + } 149 + state.pressed = false; 150 + } 151 + 152 + if (inside && !prevHover && action === 'M' && (code & 32)) { 153 + state.message = 'Hovering on the button.'; 154 + } 155 + 156 + if (!inside && prevHover && action === 'M' && (code & 32)) { 157 + state.message = 'Hover or click the button'; 158 + } 159 + 160 + return true; 161 + } 162 + 163 + function handleInput(chunk) { 164 + const str = chunk.toString(); 165 + if (str === 'q' || str === '\x03') { 166 + cleanup(); 167 + return; 168 + } 169 + 170 + if (str.startsWith('\x1b[<')) { 171 + if (parseMouseEvent(str)) { 172 + render(); 173 + } 174 + } 175 + } 176 + 177 + function cleanup() { 178 + if (process.stdin.isTTY) { 179 + process.stdin.setRawMode(false); 180 + } 181 + process.stdin.pause(); 182 + process.stdin.removeListener('data', handleInput); 183 + process.stdout.removeListener('resize', render); 184 + write(codes.mouseOff + codes.showCursor + codes.altScreenOff); 185 + process.exit(0); 186 + } 187 + 188 + function start() { 189 + if (process.stdin.isTTY) { 190 + process.stdin.setRawMode(true); 191 + } 192 + process.stdin.resume(); 193 + process.stdin.on('data', handleInput); 194 + process.stdout.on('resize', render); 195 + 196 + write(codes.altScreenOn + codes.hideCursor + codes.mouseOn); 197 + render(); 198 + } 199 + 200 + process.on('SIGINT', cleanup); 201 + process.on('SIGTERM', cleanup); 202 + 203 + start();
+122
examples/tui/mouse.js
··· 1 + const ESC = '\x1b'; 2 + const CSI = `${ESC}[`; 3 + 4 + const codes = { 5 + altScreenOn: `${CSI}?1049h`, 6 + altScreenOff: `${CSI}?1049l`, 7 + hideCursor: `${CSI}?25l`, 8 + showCursor: `${CSI}?25h`, 9 + clear: `${CSI}2J`, 10 + home: `${CSI}H`, 11 + reset: `${CSI}0m`, 12 + mouseOn: `${CSI}?1000h${CSI}?1006h`, 13 + mouseOff: `${CSI}?1000l${CSI}?1006l` 14 + }; 15 + 16 + const state = { 17 + x: 0, 18 + y: 0, 19 + button: 'none', 20 + action: 'move', 21 + lastEvent: '' 22 + }; 23 + 24 + function write(text) { 25 + process.stdout.write(text); 26 + } 27 + 28 + function render() { 29 + const width = process.stdout.columns || 80; 30 + const height = process.stdout.rows || 24; 31 + const lines = []; 32 + 33 + lines.push(' Simple Mouse TUI'); 34 + lines.push(''); 35 + lines.push(` Position: ${state.x}, ${state.y}`); 36 + lines.push(` Button: ${state.button}`); 37 + lines.push(` Action: ${state.action}`); 38 + lines.push(''); 39 + lines.push(' Last event:'); 40 + lines.push(` ${state.lastEvent || 'none'}`); 41 + lines.push(''); 42 + lines.push(' Move or click in the terminal. Press q or Ctrl-C to exit.'); 43 + 44 + while (lines.length < height) { 45 + lines.push(''); 46 + } 47 + 48 + const padded = lines.map(line => { 49 + if (line.length >= width) return line.slice(0, width); 50 + return line + ' '.repeat(width - line.length); 51 + }); 52 + 53 + write(codes.home + codes.clear + padded.join('\n') + codes.reset); 54 + } 55 + 56 + function parseMouseEvent(seq) { 57 + const match = seq.match(/^\x1b\[<([0-9]+);([0-9]+);([0-9]+)([mM])$/); 58 + if (!match) return false; 59 + 60 + const [, codeStr, xStr, yStr, action] = match; 61 + const code = Number(codeStr); 62 + const x = Number(xStr); 63 + const y = Number(yStr); 64 + const buttonCode = code & 3; 65 + 66 + let button = 'none'; 67 + if (buttonCode === 0) button = 'left'; 68 + if (buttonCode === 1) button = 'middle'; 69 + if (buttonCode === 2) button = 'right'; 70 + 71 + let actionLabel = 'move'; 72 + if (action === 'M' && (code & 32) === 0) actionLabel = 'press'; 73 + if (action === 'm') actionLabel = 'release'; 74 + 75 + state.x = x; 76 + state.y = y; 77 + state.button = button; 78 + state.action = actionLabel; 79 + state.lastEvent = `code=${code} x=${x} y=${y} action=${action}`; 80 + return true; 81 + } 82 + 83 + function handleInput(chunk) { 84 + const str = chunk.toString(); 85 + if (str === 'q' || str === '\x03') { 86 + cleanup(); 87 + return; 88 + } 89 + 90 + if (str.startsWith('\x1b[<')) { 91 + if (parseMouseEvent(str)) { 92 + render(); 93 + return; 94 + } 95 + } 96 + } 97 + 98 + function cleanup() { 99 + process.stdin.setRawMode(false); 100 + process.stdin.pause(); 101 + process.stdin.removeListener('data', handleInput); 102 + process.stdout.removeListener('resize', render); 103 + write(codes.mouseOff + codes.showCursor + codes.altScreenOff); 104 + process.exit(0); 105 + } 106 + 107 + function start() { 108 + if (process.stdin.isTTY) { 109 + process.stdin.setRawMode(true); 110 + } 111 + process.stdin.resume(); 112 + process.stdin.on('data', handleInput); 113 + process.stdout.on('resize', render); 114 + 115 + write(codes.altScreenOn + codes.hideCursor + codes.mouseOn); 116 + render(); 117 + } 118 + 119 + process.on('SIGINT', cleanup); 120 + process.on('SIGTERM', cleanup); 121 + 122 + start();
+10 -49
examples/tui/tuey.js
··· 1 + import * as readline from 'node:readline'; 2 + 1 3 const ESC = '\x1b'; 2 4 const CSI = `${ESC}[`; 3 5 ··· 307 309 this._buffer = new Buffer(this._width, this._height); 308 310 this._prevBuffer = null; 309 311 this._running = false; 310 - this._inputBuf = ''; 311 312 this._keyHandlers = []; 312 313 this._resizeHandlers = []; 313 314 this._modalStack = []; 314 - this._escapeTimer = null; 315 315 316 - this._onData = this._onData.bind(this); 316 + this._onKeypress = this._onKeypress.bind(this); 317 317 this._onResize = this._onResize.bind(this); 318 318 this._cleanup = this._cleanup.bind(this); 319 319 } ··· 330 330 this.stdin.setRawMode(true); 331 331 } 332 332 this.stdin.resume(); 333 - this.stdin.on('data', this._onData); 333 + readline.emitKeypressEvents(this.stdin); 334 + this.stdin.on('keypress', this._onKeypress); 334 335 this.stdout.on('resize', this._onResize); 335 336 process.on('SIGINT', this._cleanup); 336 337 process.on('SIGTERM', this._cleanup); ··· 350 351 if (!this._running) return; 351 352 this._running = false; 352 353 353 - this.stdin.removeListener('data', this._onData); 354 + this.stdin.removeListener('keypress', this._onKeypress); 354 355 this.stdout.removeListener('resize', this._onResize); 355 356 process.removeListener('SIGINT', this._cleanup); 356 357 process.removeListener('SIGTERM', this._cleanup); ··· 376 377 } 377 378 } 378 379 379 - _onData(chunk) { 380 - const str = chunk.toString(); 381 - 382 - if (this._escapeTimer) { 383 - clearTimeout(this._escapeTimer); 384 - this._escapeTimer = null; 385 - } 386 - 387 - if (this._modalStack.length > 0 && str === '\x1b') { 388 - this._emitKey('\x1b'); 389 - return; 390 - } 391 - 392 - for (const ch of str) { 393 - if (this._inputBuf.length > 0) { 394 - this._inputBuf += ch; 395 - if (this._inputBuf.length >= 2) { 396 - if (this._inputBuf.length >= 3 && /[A-Za-z~]/.test(ch)) { 397 - this._emitKey(this._inputBuf); 398 - this._inputBuf = ''; 399 - } else if (this._inputBuf.length === 2 && /[A-Z]/.test(ch)) { 400 - this._emitKey(this._inputBuf); 401 - this._inputBuf = ''; 402 - } else if (this._inputBuf.length > 6) { 403 - this._emitKey(this._inputBuf); 404 - this._inputBuf = ''; 405 - } 406 - } 407 - } else if (ch === '\x1b') { 408 - this._inputBuf = ch; 409 - } else { 410 - this._emitKey(ch); 411 - } 412 - } 413 - 414 - if (this._inputBuf === '\x1b') { 415 - this._escapeTimer = setTimeout(() => { 416 - if (this._inputBuf === '\x1b') { 417 - this._emitKey('\x1b'); 418 - this._inputBuf = ''; 419 - } 420 - this._escapeTimer = null; 421 - }, 50); 422 - } 380 + _onKeypress(str, key) { 381 + const sequence = key && key.sequence ? key.sequence : str; 382 + if (!sequence) return; 383 + this._emitKey(sequence); 423 384 } 424 385 425 386 _emitKey(key) {
+2 -1
include/modules/process.h
··· 3 3 4 4 #include "ant.h" 5 5 6 - void init_process_module(void); 7 6 void process_gc_update_roots(GC_FWD_ARGS); 7 + void process_enable_keypress_events(void); 8 8 9 + void init_process_module(void); 9 10 bool has_active_stdin(void); 10 11 11 12 #endif
+1 -1
meson/version/meson.build
··· 4 4 timestamp_opt = get_option('build_timestamp') 5 5 timestamp = timestamp_opt != '' ? timestamp_opt : run_command('date', '+%s', check: true).stdout().strip() 6 6 7 - ant_version = '0.4.2.' + timestamp + '-g' + git_hash 7 + ant_version = '0.5.0.' + timestamp + '-g' + git_hash 8 8 cmd_cc = meson.get_compiler('c') 9 9 10 10 target_triple = run_command(cmd_cc.cmd_array(), '-dumpmachine', check: true).stdout().strip()
+229 -17
src/modules/process.c
··· 63 63 static ProcessEventType *stdin_events = NULL; 64 64 static ProcessEventType *stdout_events = NULL; 65 65 static ProcessEventType *stderr_events = NULL; 66 - static uv_tty_t stdin_tty; 67 - static bool stdin_tty_initialized = false; 68 - static bool stdin_reading = false; 66 + 67 + typedef struct { 68 + uv_tty_t tty; 69 + bool tty_initialized; 70 + bool reading; 71 + bool keypress_enabled; 72 + int escape_state; 73 + int escape_len; 74 + char escape_buf[16]; 75 + } stdin_state_t; 76 + 77 + static stdin_state_t stdin_state = {0}; 69 78 static uint64_t process_start_time = 0; 70 79 71 80 #ifndef _WIN32 ··· 74 83 static uv_signal_t sigwinch_handle; 75 84 static bool sigwinch_initialized = false; 76 85 #endif 77 - 78 86 79 87 typedef struct { 80 88 const char *name; ··· 309 317 } 310 318 } 311 319 320 + static const char *stdin_escape_name(const char *seq, int len) { 321 + if (len < 2) return NULL; 322 + 323 + if (seq[0] == '[') { 324 + if (seq[1] >= '0' && seq[1] <= '9') { 325 + int num = 0; 326 + int idx = 1; 327 + 328 + while (idx < len && seq[idx] >= '0' && seq[idx] <= '9') { 329 + num = num * 10 + (seq[idx] - '0'); 330 + idx++; 331 + } 332 + 333 + if (idx < len && seq[idx] == '~') { 334 + typedef struct { 335 + int code; 336 + const char *name; 337 + } esc_num_map_t; 338 + 339 + static const esc_num_map_t esc_num_map[] = { 340 + { 1, "home" }, 341 + { 2, "insert" }, 342 + { 3, "delete" }, 343 + { 4, "end" }, 344 + { 5, "pageup" }, 345 + { 6, "pagedown" }, 346 + { 7, "home" }, 347 + { 8, "end" }, 348 + { 15, "f5" }, 349 + { 17, "f6" }, 350 + { 18, "f7" }, 351 + { 19, "f8" }, 352 + { 20, "f9" }, 353 + { 21, "f10" }, 354 + { 23, "f11" }, 355 + { 24, "f12" }, 356 + }; 357 + 358 + for (size_t i = 0; i < sizeof(esc_num_map) / sizeof(esc_num_map[0]); i++) { 359 + if (esc_num_map[i].code == num) return esc_num_map[i].name; 360 + } 361 + } 362 + return NULL; 363 + } 364 + 365 + typedef struct { 366 + char code; 367 + bool needs_tilde; 368 + const char *name; 369 + } esc_map_t; 370 + 371 + static const esc_map_t esc_map[] = { 372 + { 'A', false, "up" }, 373 + { 'B', false, "down" }, 374 + { 'C', false, "right" }, 375 + { 'D', false, "left" }, 376 + { 'H', false, "home" }, 377 + { 'F', false, "end" }, 378 + { 'Z', false, "tab" }, 379 + { '2', true, "insert" }, 380 + { '3', true, "delete" }, 381 + { '5', true, "pageup" }, 382 + { '6', true, "pagedown" }, 383 + }; 384 + 385 + for (size_t i = 0; i < sizeof(esc_map) / sizeof(esc_map[0]); i++) { 386 + if (seq[1] != esc_map[i].code) continue; 387 + if (esc_map[i].needs_tilde) { 388 + return (len >= 3 && seq[2] == '~') ? esc_map[i].name : NULL; 389 + } 390 + return esc_map[i].name; 391 + } 392 + return NULL; 393 + } 394 + 395 + if (seq[0] == 'O') { 396 + switch (seq[1]) { 397 + case 'P': return "f1"; 398 + case 'Q': return "f2"; 399 + case 'R': return "f3"; 400 + case 'S': return "f4"; 401 + default: return NULL; 402 + } 403 + } 404 + 405 + return NULL; 406 + } 407 + 408 + static void emit_keypress_event( 409 + struct js *js, 410 + const char *str, 411 + size_t str_len, 412 + const char *name, 413 + bool ctrl, 414 + bool meta, 415 + bool shift, 416 + const char *sequence, 417 + size_t sequence_len 418 + ) { 419 + jsval_t str_val = js_mkstr(js, str ? str : "", str ? str_len : 0); 420 + jsval_t key_obj = js_mkobj(js); 421 + 422 + if (name) { 423 + js_set(js, key_obj, "name", js_mkstr(js, name, strlen(name))); 424 + } else { 425 + js_set(js, key_obj, "name", js_mkundef()); 426 + } 427 + 428 + js_set(js, key_obj, "ctrl", ctrl ? js_mktrue() : js_mkfalse()); 429 + js_set(js, key_obj, "meta", meta ? js_mktrue() : js_mkfalse()); 430 + js_set(js, key_obj, "shift", shift ? js_mktrue() : js_mkfalse()); 431 + 432 + if (sequence) { 433 + js_set(js, key_obj, "sequence", js_mkstr(js, sequence, sequence_len)); 434 + } 435 + 436 + jsval_t args[2] = { str_val, key_obj }; 437 + emit_stdio_event(stdin_events, "keypress", args, 2); 438 + } 439 + 440 + static void process_keypress_data(struct js *js, const char *data, size_t len) { 441 + for (size_t i = 0; i < len; i++) { 442 + unsigned char c = (unsigned char)data[i]; 443 + 444 + if (stdin_state.escape_state == 1) { 445 + stdin_state.escape_buf[stdin_state.escape_len++] = (char)c; 446 + if (c == '[' || c == 'O') { 447 + stdin_state.escape_state = 2; 448 + continue; 449 + } 450 + 451 + emit_keypress_event(js, "\x1b", 1, "escape", false, false, false, "\x1b", 1); 452 + stdin_state.escape_state = 0; 453 + stdin_state.escape_len = 0; 454 + } 455 + 456 + if (stdin_state.escape_state == 2) { 457 + stdin_state.escape_buf[stdin_state.escape_len++] = (char)c; 458 + if ((c >= 'A' && c <= 'Z') || c == '~' || stdin_state.escape_len >= 15) { 459 + char sequence[18]; 460 + size_t seq_len = 0; 461 + sequence[seq_len++] = '\x1b'; 462 + memcpy(sequence + seq_len, stdin_state.escape_buf, (size_t)stdin_state.escape_len); 463 + seq_len += (size_t)stdin_state.escape_len; 464 + 465 + const char *name = stdin_escape_name(stdin_state.escape_buf, stdin_state.escape_len); 466 + if (!name) name = "escape"; 467 + 468 + emit_keypress_event(js, "", 0, name, false, false, false, sequence, seq_len); 469 + stdin_state.escape_state = 0; 470 + stdin_state.escape_len = 0; 471 + } 472 + continue; 473 + } 474 + 475 + if (c == 27) { 476 + stdin_state.escape_state = 1; 477 + stdin_state.escape_len = 0; 478 + continue; 479 + } 480 + 481 + if (c == '\r' || c == '\n') { 482 + emit_keypress_event(js, "\n", 1, "return", false, false, false, "\n", 1); 483 + continue; 484 + } 485 + 486 + if (c == 127 || c == 8) { 487 + emit_keypress_event(js, "", 0, "backspace", false, false, false, NULL, 0); 488 + continue; 489 + } 490 + 491 + if (c == '\t') { 492 + emit_keypress_event(js, "\t", 1, "tab", false, false, false, "\t", 1); 493 + continue; 494 + } 495 + 496 + if (c < 32) { 497 + char name_buf[2] = { (char)('a' + c - 1), '\0' }; 498 + char seq = (char)c; 499 + emit_keypress_event(js, &seq, 1, name_buf, true, false, false, &seq, 1); 500 + continue; 501 + } 502 + 503 + char ch = (char)c; 504 + char name_buf[2] = { ch, '\0' }; 505 + emit_keypress_event(js, &ch, 1, name_buf, false, false, false, &ch, 1); 506 + } 507 + 508 + if (stdin_state.escape_state == 1) { 509 + emit_keypress_event(js, "\x1b", 1, "escape", false, false, false, "\x1b", 1); 510 + stdin_state.escape_state = 0; 511 + stdin_state.escape_len = 0; 512 + } 513 + } 514 + 312 515 static bool remove_listener_from_events(ProcessEventType *events, const char *event, jsval_t listener) { 313 516 ProcessEventType *evt = NULL; 314 517 HASH_FIND_STR(events, event, evt); ··· 396 599 if (nread > 0 && rt->js) { 397 600 jsval_t data_val = js_mkstr(rt->js, buf->base, (size_t)nread); 398 601 emit_stdio_event(stdin_events, "data", &data_val, 1); 602 + if (stdin_state.keypress_enabled) process_keypress_data(rt->js, buf->base, (size_t)nread); 399 603 } 400 604 if (buf->base) free(buf->base); 401 605 } 402 606 403 607 static void stdin_start_reading(void) { 404 - if (stdin_reading) return; 405 - if (!stdin_tty_initialized) { 608 + if (stdin_state.reading) return; 609 + if (!stdin_state.tty_initialized) { 406 610 uv_loop_t *loop = uv_default_loop(); 407 - if (uv_tty_init(loop, &stdin_tty, STDIN_FILENO, 1) != 0) return; 611 + if (uv_tty_init(loop, &stdin_state.tty, STDIN_FILENO, 1) != 0) return; 408 612 #ifndef _WIN32 409 - uv_tty_set_mode(&stdin_tty, stdin_raw_mode ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); 613 + uv_tty_set_mode(&stdin_state.tty, stdin_raw_mode ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); 410 614 #endif 411 - stdin_tty.data = NULL; 412 - stdin_tty_initialized = true; 615 + stdin_state.tty.data = NULL; 616 + stdin_state.tty_initialized = true; 413 617 } else { 414 618 #ifndef _WIN32 415 - uv_tty_set_mode(&stdin_tty, stdin_raw_mode ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); 619 + uv_tty_set_mode(&stdin_state.tty, stdin_raw_mode ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); 416 620 #endif 417 621 } 418 - stdin_reading = true; 419 - uv_read_start((uv_stream_t *)&stdin_tty, stdin_alloc_buffer, on_stdin_read); 622 + stdin_state.reading = true; 623 + uv_read_start((uv_stream_t *)&stdin_state.tty, stdin_alloc_buffer, on_stdin_read); 420 624 } 421 625 422 626 static void stdin_stop_reading(void) { 423 - if (!stdin_reading) return; 424 - uv_read_stop((uv_stream_t *)&stdin_tty); 425 - stdin_reading = false; 627 + if (!stdin_state.reading) return; 628 + uv_read_stop((uv_stream_t *)&stdin_state.tty); 629 + stdin_state.reading = false; 426 630 } 427 631 428 632 #ifndef _WIN32 ··· 1489 1693 1490 1694 #undef GC_FWD_EVENTS 1491 1695 1492 - bool has_active_stdin(void) { return stdin_reading; } 1696 + bool has_active_stdin(void) { 1697 + return stdin_state.reading; 1698 + } 1699 + 1700 + void process_enable_keypress_events(void) { 1701 + stdin_state.keypress_enabled = true; 1702 + stdin_state.escape_state = 0; 1703 + stdin_state.escape_len = 0; 1704 + }
+8 -1
src/modules/readline.c
··· 27 27 #include "internal.h" 28 28 29 29 #include "modules/readline.h" 30 + #include "modules/process.h" 30 31 #include "modules/symbol.h" 31 32 32 33 #define MAX_LINE_LENGTH 4096 ··· 1239 1240 } 1240 1241 1241 1242 static jsval_t rl_emit_keypress_events(struct js *js, jsval_t *args, int nargs) { 1242 - (void)args; (void)nargs; 1243 + if (nargs > 0) { 1244 + jsval_t stdin_obj = js_get(js, js_get(js, js_glob(js), "process"), "stdin"); 1245 + if (stdin_obj != args[0]) { 1246 + return js_mkerr(js, "emitKeypressEvents only supports process.stdin"); 1247 + } 1248 + } 1249 + process_enable_keypress_events(); 1243 1250 return js_mkundef(); 1244 1251 } 1245 1252
+24
tests/test_readline_keypress.cjs
··· 1 + import * as readline from 'node:readline'; 2 + 3 + if (!process.stdin.isTTY) { 4 + console.log('Not a TTY, skipping interactive test'); 5 + process.exit(0); 6 + } 7 + 8 + console.log('Keypress test - press keys to see parsed output'); 9 + console.log('Press Ctrl+C to exit\n'); 10 + 11 + readline.emitKeypressEvents(process.stdin); 12 + process.stdin.setRawMode(true); 13 + process.stdin.resume(); 14 + 15 + process.stdin.on('keypress', (str, key) => { 16 + console.log('keypress:', JSON.stringify({ str, key })); 17 + 18 + if (key.ctrl && key.name === 'c') { 19 + console.log('\nExiting...'); 20 + process.stdin.setRawMode(false); 21 + process.stdin.pause(); 22 + process.exit(0); 23 + } 24 + });