From f37c34cdb5a320c7e2bc6ede3ac456aa10db56fa Mon Sep 17 00:00:00 2001 From: theMackabu Date: Tue, 27 Jan 2026 18:40:42 -0800 Subject: [PATCH] add process.stdin support to readline module --- examples/tui/button.js | 203 +++++++++++++++++++++++++ examples/tui/mouse.js | 122 +++++++++++++++ examples/tui/tuey.js | 59 ++------ include/modules/process.h | 3 +- meson/version/meson.build | 2 +- src/modules/process.c | 246 ++++++++++++++++++++++++++++--- src/modules/readline.c | 9 +- tests/test_readline_keypress.cjs | 24 +++ 8 files changed, 599 insertions(+), 69 deletions(-) create mode 100644 examples/tui/button.js create mode 100644 examples/tui/mouse.js create mode 100644 tests/test_readline_keypress.cjs diff --git a/examples/tui/button.js b/examples/tui/button.js new file mode 100644 index 0000000..990e3e6 --- /dev/null +++ b/examples/tui/button.js @@ -0,0 +1,203 @@ +const ESC = '\x1b'; +const CSI = `${ESC}[`; + +const codes = { + altScreenOn: `${CSI}?1049h`, + altScreenOff: `${CSI}?1049l`, + hideCursor: `${CSI}?25l`, + showCursor: `${CSI}?25h`, + clear: `${CSI}2J`, + home: `${CSI}H`, + reset: `${CSI}0m`, + mouseOn: `${CSI}?1000h${CSI}?1003h${CSI}?1006h`, + mouseOff: `${CSI}?1000l${CSI}?1003l${CSI}?1006l`, + invert: `${CSI}7m`, + fg: n => `${CSI}38;5;${n}m`, + bg: n => `${CSI}48;5;${n}m` +}; + +const state = { + hover: false, + pressed: false, + message: 'Hover or click the button' +}; + +const button = { + label: 'Click Me', + x: 0, + y: 0, + width: 0, + height: 1 +}; + +const styles = { + normal: '', + hover: `${codes.bg(46)}${codes.fg(16)}`, + active: `${codes.bg(196)}${codes.fg(15)}` +}; + +function write(text) { + process.stdout.write(text); +} + +function centerButton(width, height) { + button.width = button.label.length + 4; + button.x = Math.max(0, Math.floor((width - button.width) / 2)); + button.y = Math.max(0, Math.floor((height - button.height) / 2)); +} + +function createGrid(width, height) { + const chars = Array(height).fill(null).map(() => Array(width).fill(' ')); + const stylesGrid = Array(height).fill(null).map(() => Array(width).fill('')); + return { chars, styles: stylesGrid }; +} + +function setCell(grid, x, y, ch, style) { + if (y < 0 || y >= grid.chars.length) return; + if (x < 0 || x >= grid.chars[y].length) return; + grid.chars[y][x] = ch; + grid.styles[y][x] = style; +} + +function setText(grid, x, y, text, style) { + for (let i = 0; i < text.length; i++) { + setCell(grid, x + i, y, text[i], style); + } +} + +function drawButton(grid) { + const fillStyle = state.pressed + ? styles.active + : state.hover + ? styles.hover + : styles.normal; + for (let row = 0; row < button.height; row++) { + for (let col = 0; col < button.width; col++) { + setCell(grid, button.x + col, button.y + row, ' ', fillStyle); + } + } + + const padTotal = Math.max(0, button.width - button.label.length); + const padLeft = Math.floor(padTotal / 2); + const labelX = button.x + padLeft; + const labelY = button.y + Math.floor(button.height / 2); + setText(grid, labelX, labelY, button.label, fillStyle); +} + +function render() { + const width = process.stdout.columns || 80; + const height = process.stdout.rows || 24; + centerButton(width, height); + + const grid = createGrid(width, height); + const header = ' Simple Button TUI (q to quit)'; + setText(grid, 0, 0, header.slice(0, width), ''); + + const message = state.message; + const messageLine = Math.min(height - 2, button.y + button.height + 1); + if (messageLine >= 0 && messageLine < height) { + setText(grid, 1, messageLine, message.slice(0, width - 2), ''); + } + + drawButton(grid); + const rows = grid.chars.map((row, y) => { + let line = ''; + let currentStyle = ''; + for (let x = 0; x < row.length; x++) { + const nextStyle = grid.styles[y][x]; + if (nextStyle !== currentStyle) { + line += nextStyle || codes.reset; + currentStyle = nextStyle; + } + line += row[x]; + } + return line + codes.reset; + }); + write(codes.home + codes.clear + rows.join('\n') + codes.reset); +} + +function isInsideButton(x, y) { + return ( + x >= button.x && + x < button.x + button.width && + y >= button.y && + y < button.y + button.height + ); +} + +function parseMouseEvent(seq) { + const match = seq.match(/^\x1b\[<([0-9]+);([0-9]+);([0-9]+)([mM])$/); + if (!match) return false; + + const [, codeStr, xStr, yStr, action] = match; + const code = Number(codeStr); + const x = Number(xStr) - 1; + const y = Number(yStr) - 1; + const buttonCode = code & 3; + + const inside = isInsideButton(x, y); + const prevHover = state.hover; + state.hover = inside; + + if (action === 'M' && buttonCode === 0 && inside) { + state.pressed = true; + state.message = 'Button pressed! Release to click.'; + } else if (action === 'm') { + if (state.pressed && inside) { + state.message = 'Button clicked!'; + } + state.pressed = false; + } + + if (inside && !prevHover && action === 'M' && (code & 32)) { + state.message = 'Hovering on the button.'; + } + + if (!inside && prevHover && action === 'M' && (code & 32)) { + state.message = 'Hover or click the button'; + } + + return true; +} + +function handleInput(chunk) { + const str = chunk.toString(); + if (str === 'q' || str === '\x03') { + cleanup(); + return; + } + + if (str.startsWith('\x1b[<')) { + if (parseMouseEvent(str)) { + render(); + } + } +} + +function cleanup() { + if (process.stdin.isTTY) { + process.stdin.setRawMode(false); + } + process.stdin.pause(); + process.stdin.removeListener('data', handleInput); + process.stdout.removeListener('resize', render); + write(codes.mouseOff + codes.showCursor + codes.altScreenOff); + process.exit(0); +} + +function start() { + if (process.stdin.isTTY) { + process.stdin.setRawMode(true); + } + process.stdin.resume(); + process.stdin.on('data', handleInput); + process.stdout.on('resize', render); + + write(codes.altScreenOn + codes.hideCursor + codes.mouseOn); + render(); +} + +process.on('SIGINT', cleanup); +process.on('SIGTERM', cleanup); + +start(); diff --git a/examples/tui/mouse.js b/examples/tui/mouse.js new file mode 100644 index 0000000..c64d7c6 --- /dev/null +++ b/examples/tui/mouse.js @@ -0,0 +1,122 @@ +const ESC = '\x1b'; +const CSI = `${ESC}[`; + +const codes = { + altScreenOn: `${CSI}?1049h`, + altScreenOff: `${CSI}?1049l`, + hideCursor: `${CSI}?25l`, + showCursor: `${CSI}?25h`, + clear: `${CSI}2J`, + home: `${CSI}H`, + reset: `${CSI}0m`, + mouseOn: `${CSI}?1000h${CSI}?1006h`, + mouseOff: `${CSI}?1000l${CSI}?1006l` +}; + +const state = { + x: 0, + y: 0, + button: 'none', + action: 'move', + lastEvent: '' +}; + +function write(text) { + process.stdout.write(text); +} + +function render() { + const width = process.stdout.columns || 80; + const height = process.stdout.rows || 24; + const lines = []; + + lines.push(' Simple Mouse TUI'); + lines.push(''); + lines.push(` Position: ${state.x}, ${state.y}`); + lines.push(` Button: ${state.button}`); + lines.push(` Action: ${state.action}`); + lines.push(''); + lines.push(' Last event:'); + lines.push(` ${state.lastEvent || 'none'}`); + lines.push(''); + lines.push(' Move or click in the terminal. Press q or Ctrl-C to exit.'); + + while (lines.length < height) { + lines.push(''); + } + + const padded = lines.map(line => { + if (line.length >= width) return line.slice(0, width); + return line + ' '.repeat(width - line.length); + }); + + write(codes.home + codes.clear + padded.join('\n') + codes.reset); +} + +function parseMouseEvent(seq) { + const match = seq.match(/^\x1b\[<([0-9]+);([0-9]+);([0-9]+)([mM])$/); + if (!match) return false; + + const [, codeStr, xStr, yStr, action] = match; + const code = Number(codeStr); + const x = Number(xStr); + const y = Number(yStr); + const buttonCode = code & 3; + + let button = 'none'; + if (buttonCode === 0) button = 'left'; + if (buttonCode === 1) button = 'middle'; + if (buttonCode === 2) button = 'right'; + + let actionLabel = 'move'; + if (action === 'M' && (code & 32) === 0) actionLabel = 'press'; + if (action === 'm') actionLabel = 'release'; + + state.x = x; + state.y = y; + state.button = button; + state.action = actionLabel; + state.lastEvent = `code=${code} x=${x} y=${y} action=${action}`; + return true; +} + +function handleInput(chunk) { + const str = chunk.toString(); + if (str === 'q' || str === '\x03') { + cleanup(); + return; + } + + if (str.startsWith('\x1b[<')) { + if (parseMouseEvent(str)) { + render(); + return; + } + } +} + +function cleanup() { + process.stdin.setRawMode(false); + process.stdin.pause(); + process.stdin.removeListener('data', handleInput); + process.stdout.removeListener('resize', render); + write(codes.mouseOff + codes.showCursor + codes.altScreenOff); + process.exit(0); +} + +function start() { + if (process.stdin.isTTY) { + process.stdin.setRawMode(true); + } + process.stdin.resume(); + process.stdin.on('data', handleInput); + process.stdout.on('resize', render); + + write(codes.altScreenOn + codes.hideCursor + codes.mouseOn); + render(); +} + +process.on('SIGINT', cleanup); +process.on('SIGTERM', cleanup); + +start(); diff --git a/examples/tui/tuey.js b/examples/tui/tuey.js index 293cae7..1359caf 100644 --- a/examples/tui/tuey.js +++ b/examples/tui/tuey.js @@ -1,3 +1,5 @@ +import * as readline from 'node:readline'; + const ESC = '\x1b'; const CSI = `${ESC}[`; @@ -307,13 +309,11 @@ export class Screen { this._buffer = new Buffer(this._width, this._height); this._prevBuffer = null; this._running = false; - this._inputBuf = ''; this._keyHandlers = []; this._resizeHandlers = []; this._modalStack = []; - this._escapeTimer = null; - this._onData = this._onData.bind(this); + this._onKeypress = this._onKeypress.bind(this); this._onResize = this._onResize.bind(this); this._cleanup = this._cleanup.bind(this); } @@ -330,7 +330,8 @@ export class Screen { this.stdin.setRawMode(true); } this.stdin.resume(); - this.stdin.on('data', this._onData); + readline.emitKeypressEvents(this.stdin); + this.stdin.on('keypress', this._onKeypress); this.stdout.on('resize', this._onResize); process.on('SIGINT', this._cleanup); process.on('SIGTERM', this._cleanup); @@ -350,7 +351,7 @@ export class Screen { if (!this._running) return; this._running = false; - this.stdin.removeListener('data', this._onData); + this.stdin.removeListener('keypress', this._onKeypress); this.stdout.removeListener('resize', this._onResize); process.removeListener('SIGINT', this._cleanup); process.removeListener('SIGTERM', this._cleanup); @@ -376,50 +377,10 @@ export class Screen { } } - _onData(chunk) { - const str = chunk.toString(); - - if (this._escapeTimer) { - clearTimeout(this._escapeTimer); - this._escapeTimer = null; - } - - if (this._modalStack.length > 0 && str === '\x1b') { - this._emitKey('\x1b'); - return; - } - - for (const ch of str) { - if (this._inputBuf.length > 0) { - this._inputBuf += ch; - if (this._inputBuf.length >= 2) { - if (this._inputBuf.length >= 3 && /[A-Za-z~]/.test(ch)) { - this._emitKey(this._inputBuf); - this._inputBuf = ''; - } else if (this._inputBuf.length === 2 && /[A-Z]/.test(ch)) { - this._emitKey(this._inputBuf); - this._inputBuf = ''; - } else if (this._inputBuf.length > 6) { - this._emitKey(this._inputBuf); - this._inputBuf = ''; - } - } - } else if (ch === '\x1b') { - this._inputBuf = ch; - } else { - this._emitKey(ch); - } - } - - if (this._inputBuf === '\x1b') { - this._escapeTimer = setTimeout(() => { - if (this._inputBuf === '\x1b') { - this._emitKey('\x1b'); - this._inputBuf = ''; - } - this._escapeTimer = null; - }, 50); - } + _onKeypress(str, key) { + const sequence = key && key.sequence ? key.sequence : str; + if (!sequence) return; + this._emitKey(sequence); } _emitKey(key) { diff --git a/include/modules/process.h b/include/modules/process.h index bfc49e5..1d49ac4 100644 --- a/include/modules/process.h +++ b/include/modules/process.h @@ -3,9 +3,10 @@ #include "ant.h" -void init_process_module(void); void process_gc_update_roots(GC_FWD_ARGS); +void process_enable_keypress_events(void); +void init_process_module(void); bool has_active_stdin(void); #endif diff --git a/meson/version/meson.build b/meson/version/meson.build index 5b0abd4..c54f80a 100644 --- a/meson/version/meson.build +++ b/meson/version/meson.build @@ -4,7 +4,7 @@ git_hash = git_hash != '' ? git_hash : 'unknown' timestamp_opt = get_option('build_timestamp') timestamp = timestamp_opt != '' ? timestamp_opt : run_command('date', '+%s', check: true).stdout().strip() -ant_version = '0.4.2.' + timestamp + '-g' + git_hash +ant_version = '0.5.0.' + timestamp + '-g' + git_hash cmd_cc = meson.get_compiler('c') target_triple = run_command(cmd_cc.cmd_array(), '-dumpmachine', check: true).stdout().strip() diff --git a/src/modules/process.c b/src/modules/process.c index e04e695..0204bc7 100644 --- a/src/modules/process.c +++ b/src/modules/process.c @@ -63,9 +63,18 @@ static ProcessEventType *process_events = NULL; static ProcessEventType *stdin_events = NULL; static ProcessEventType *stdout_events = NULL; static ProcessEventType *stderr_events = NULL; -static uv_tty_t stdin_tty; -static bool stdin_tty_initialized = false; -static bool stdin_reading = false; + +typedef struct { + uv_tty_t tty; + bool tty_initialized; + bool reading; + bool keypress_enabled; + int escape_state; + int escape_len; + char escape_buf[16]; +} stdin_state_t; + +static stdin_state_t stdin_state = {0}; static uint64_t process_start_time = 0; #ifndef _WIN32 @@ -75,7 +84,6 @@ static uv_signal_t sigwinch_handle; static bool sigwinch_initialized = false; #endif - typedef struct { const char *name; int signum; @@ -309,6 +317,201 @@ static void emit_stdio_event(ProcessEventType *events, const char *event_type, j } } +static const char *stdin_escape_name(const char *seq, int len) { + if (len < 2) return NULL; + + if (seq[0] == '[') { + if (seq[1] >= '0' && seq[1] <= '9') { + int num = 0; + int idx = 1; + + while (idx < len && seq[idx] >= '0' && seq[idx] <= '9') { + num = num * 10 + (seq[idx] - '0'); + idx++; + } + + if (idx < len && seq[idx] == '~') { + typedef struct { + int code; + const char *name; + } esc_num_map_t; + + static const esc_num_map_t esc_num_map[] = { + { 1, "home" }, + { 2, "insert" }, + { 3, "delete" }, + { 4, "end" }, + { 5, "pageup" }, + { 6, "pagedown" }, + { 7, "home" }, + { 8, "end" }, + { 15, "f5" }, + { 17, "f6" }, + { 18, "f7" }, + { 19, "f8" }, + { 20, "f9" }, + { 21, "f10" }, + { 23, "f11" }, + { 24, "f12" }, + }; + + for (size_t i = 0; i < sizeof(esc_num_map) / sizeof(esc_num_map[0]); i++) { + if (esc_num_map[i].code == num) return esc_num_map[i].name; + } + } + return NULL; + } + + typedef struct { + char code; + bool needs_tilde; + const char *name; + } esc_map_t; + + static const esc_map_t esc_map[] = { + { 'A', false, "up" }, + { 'B', false, "down" }, + { 'C', false, "right" }, + { 'D', false, "left" }, + { 'H', false, "home" }, + { 'F', false, "end" }, + { 'Z', false, "tab" }, + { '2', true, "insert" }, + { '3', true, "delete" }, + { '5', true, "pageup" }, + { '6', true, "pagedown" }, + }; + + for (size_t i = 0; i < sizeof(esc_map) / sizeof(esc_map[0]); i++) { + if (seq[1] != esc_map[i].code) continue; + if (esc_map[i].needs_tilde) { + return (len >= 3 && seq[2] == '~') ? esc_map[i].name : NULL; + } + return esc_map[i].name; + } + return NULL; + } + + if (seq[0] == 'O') { + switch (seq[1]) { + case 'P': return "f1"; + case 'Q': return "f2"; + case 'R': return "f3"; + case 'S': return "f4"; + default: return NULL; + } + } + + return NULL; +} + +static void emit_keypress_event( + struct js *js, + const char *str, + size_t str_len, + const char *name, + bool ctrl, + bool meta, + bool shift, + const char *sequence, + size_t sequence_len +) { + jsval_t str_val = js_mkstr(js, str ? str : "", str ? str_len : 0); + jsval_t key_obj = js_mkobj(js); + + if (name) { + js_set(js, key_obj, "name", js_mkstr(js, name, strlen(name))); + } else { + js_set(js, key_obj, "name", js_mkundef()); + } + + js_set(js, key_obj, "ctrl", ctrl ? js_mktrue() : js_mkfalse()); + js_set(js, key_obj, "meta", meta ? js_mktrue() : js_mkfalse()); + js_set(js, key_obj, "shift", shift ? js_mktrue() : js_mkfalse()); + + if (sequence) { + js_set(js, key_obj, "sequence", js_mkstr(js, sequence, sequence_len)); + } + + jsval_t args[2] = { str_val, key_obj }; + emit_stdio_event(stdin_events, "keypress", args, 2); +} + +static void process_keypress_data(struct js *js, const char *data, size_t len) { + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)data[i]; + + if (stdin_state.escape_state == 1) { + stdin_state.escape_buf[stdin_state.escape_len++] = (char)c; + if (c == '[' || c == 'O') { + stdin_state.escape_state = 2; + continue; + } + + emit_keypress_event(js, "\x1b", 1, "escape", false, false, false, "\x1b", 1); + stdin_state.escape_state = 0; + stdin_state.escape_len = 0; + } + + if (stdin_state.escape_state == 2) { + stdin_state.escape_buf[stdin_state.escape_len++] = (char)c; + if ((c >= 'A' && c <= 'Z') || c == '~' || stdin_state.escape_len >= 15) { + char sequence[18]; + size_t seq_len = 0; + sequence[seq_len++] = '\x1b'; + memcpy(sequence + seq_len, stdin_state.escape_buf, (size_t)stdin_state.escape_len); + seq_len += (size_t)stdin_state.escape_len; + + const char *name = stdin_escape_name(stdin_state.escape_buf, stdin_state.escape_len); + if (!name) name = "escape"; + + emit_keypress_event(js, "", 0, name, false, false, false, sequence, seq_len); + stdin_state.escape_state = 0; + stdin_state.escape_len = 0; + } + continue; + } + + if (c == 27) { + stdin_state.escape_state = 1; + stdin_state.escape_len = 0; + continue; + } + + if (c == '\r' || c == '\n') { + emit_keypress_event(js, "\n", 1, "return", false, false, false, "\n", 1); + continue; + } + + if (c == 127 || c == 8) { + emit_keypress_event(js, "", 0, "backspace", false, false, false, NULL, 0); + continue; + } + + if (c == '\t') { + emit_keypress_event(js, "\t", 1, "tab", false, false, false, "\t", 1); + continue; + } + + if (c < 32) { + char name_buf[2] = { (char)('a' + c - 1), '\0' }; + char seq = (char)c; + emit_keypress_event(js, &seq, 1, name_buf, true, false, false, &seq, 1); + continue; + } + + char ch = (char)c; + char name_buf[2] = { ch, '\0' }; + emit_keypress_event(js, &ch, 1, name_buf, false, false, false, &ch, 1); + } + + if (stdin_state.escape_state == 1) { + emit_keypress_event(js, "\x1b", 1, "escape", false, false, false, "\x1b", 1); + stdin_state.escape_state = 0; + stdin_state.escape_len = 0; + } +} + static bool remove_listener_from_events(ProcessEventType *events, const char *event, jsval_t listener) { ProcessEventType *evt = NULL; HASH_FIND_STR(events, event, evt); @@ -396,33 +599,34 @@ static void on_stdin_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *bu if (nread > 0 && rt->js) { jsval_t data_val = js_mkstr(rt->js, buf->base, (size_t)nread); emit_stdio_event(stdin_events, "data", &data_val, 1); + if (stdin_state.keypress_enabled) process_keypress_data(rt->js, buf->base, (size_t)nread); } if (buf->base) free(buf->base); } static void stdin_start_reading(void) { - if (stdin_reading) return; - if (!stdin_tty_initialized) { + if (stdin_state.reading) return; + if (!stdin_state.tty_initialized) { uv_loop_t *loop = uv_default_loop(); - if (uv_tty_init(loop, &stdin_tty, STDIN_FILENO, 1) != 0) return; + if (uv_tty_init(loop, &stdin_state.tty, STDIN_FILENO, 1) != 0) return; #ifndef _WIN32 - uv_tty_set_mode(&stdin_tty, stdin_raw_mode ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); + uv_tty_set_mode(&stdin_state.tty, stdin_raw_mode ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); #endif - stdin_tty.data = NULL; - stdin_tty_initialized = true; + stdin_state.tty.data = NULL; + stdin_state.tty_initialized = true; } else { #ifndef _WIN32 - uv_tty_set_mode(&stdin_tty, stdin_raw_mode ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); + uv_tty_set_mode(&stdin_state.tty, stdin_raw_mode ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); #endif } - stdin_reading = true; - uv_read_start((uv_stream_t *)&stdin_tty, stdin_alloc_buffer, on_stdin_read); + stdin_state.reading = true; + uv_read_start((uv_stream_t *)&stdin_state.tty, stdin_alloc_buffer, on_stdin_read); } static void stdin_stop_reading(void) { - if (!stdin_reading) return; - uv_read_stop((uv_stream_t *)&stdin_tty); - stdin_reading = false; + if (!stdin_state.reading) return; + uv_read_stop((uv_stream_t *)&stdin_state.tty); + stdin_state.reading = false; } #ifndef _WIN32 @@ -1489,4 +1693,12 @@ void process_gc_update_roots(GC_FWD_ARGS) { #undef GC_FWD_EVENTS -bool has_active_stdin(void) { return stdin_reading; } +bool has_active_stdin(void) { + return stdin_state.reading; +} + +void process_enable_keypress_events(void) { + stdin_state.keypress_enabled = true; + stdin_state.escape_state = 0; + stdin_state.escape_len = 0; +} diff --git a/src/modules/readline.c b/src/modules/readline.c index e795d1e..958971c 100644 --- a/src/modules/readline.c +++ b/src/modules/readline.c @@ -27,6 +27,7 @@ #include "internal.h" #include "modules/readline.h" +#include "modules/process.h" #include "modules/symbol.h" #define MAX_LINE_LENGTH 4096 @@ -1239,7 +1240,13 @@ static jsval_t rl_move_cursor(struct js *js, jsval_t *args, int nargs) { } static jsval_t rl_emit_keypress_events(struct js *js, jsval_t *args, int nargs) { - (void)args; (void)nargs; + if (nargs > 0) { + jsval_t stdin_obj = js_get(js, js_get(js, js_glob(js), "process"), "stdin"); + if (stdin_obj != args[0]) { + return js_mkerr(js, "emitKeypressEvents only supports process.stdin"); + } + } + process_enable_keypress_events(); return js_mkundef(); } diff --git a/tests/test_readline_keypress.cjs b/tests/test_readline_keypress.cjs new file mode 100644 index 0000000..e969466 --- /dev/null +++ b/tests/test_readline_keypress.cjs @@ -0,0 +1,24 @@ +import * as readline from 'node:readline'; + +if (!process.stdin.isTTY) { + console.log('Not a TTY, skipping interactive test'); + process.exit(0); +} + +console.log('Keypress test - press keys to see parsed output'); +console.log('Press Ctrl+C to exit\n'); + +readline.emitKeypressEvents(process.stdin); +process.stdin.setRawMode(true); +process.stdin.resume(); + +process.stdin.on('keypress', (str, key) => { + console.log('keypress:', JSON.stringify({ str, key })); + + if (key.ctrl && key.name === 'c') { + console.log('\nExiting...'); + process.stdin.setRawMode(false); + process.stdin.pause(); + process.exit(0); + } +}); -- 2.51.2