///|
/// I/O implementation for JavaScript/Node.js target

///|
/// Read a single key from stdin (raw mode)
/// Falls back to /dev/tty if stdin is not a tty (e.g., when used in a pipe)
extern "js" fn js_read_key() -> @js_async.Promise[String] =
  #| async () => {
  #|   if (typeof process === 'undefined') return 'q';
  #|   const fs = await import('node:fs');
  #|
  #|   // Use /dev/tty if stdin is not a tty (piped input)
  #|   let inputStream = process.stdin;
  #|   let ttyFd = null;
  #|
  #|   if (!process.stdin.isTTY) {
  #|     try {
  #|       ttyFd = fs.openSync('/dev/tty', 'r');
  #|       inputStream = fs.createReadStream(null, { fd: ttyFd, encoding: 'utf8' });
  #|     } catch (e) {
  #|       // /dev/tty not available, fall back to stdin
  #|       inputStream = process.stdin;
  #|     }
  #|   }
  #|
  #|   if (inputStream.isTTY && inputStream.setRawMode) {
  #|     inputStream.setRawMode(true);
  #|   }
  #|   inputStream.resume();
  #|   if (inputStream.setEncoding) inputStream.setEncoding('utf8');
  #|
  #|   return new Promise((resolve) => {
  #|     const onData = (data) => {
  #|       inputStream.removeListener('data', onData);
  #|       if (ttyFd !== null) {
  #|         try { fs.closeSync(ttyFd); } catch (e) {}
  #|       }
  #|       resolve(data);
  #|     };
  #|     inputStream.on('data', onData);
  #|   });
  #| }

///|
/// Print to stdout without newline
extern "js" fn js_print_raw(s : String) =
  #| (s) => process.stdout.write(s)

///|
/// Debug print to stderr (won't interfere with TUI)
extern "js" fn js_debug_stderr(s : String) =
  #| (s) => {
  #|   process.stderr.write('[DEBUG] ' + s + '\n');
  #| }

///|
/// Debug print to stderr (public)
pub fn debug_stderr(s : String) -> Unit {
  js_debug_stderr(s)
}

///|
/// Debug stdin state
extern "js" fn js_debug_stdin_state(label : String) =
  #| async (label) => {
  #|   const { stdin } = await import('node:process');
  #|   console.log(`[STDIN ${label}] isTTY=${stdin.isTTY}, isRaw=${stdin.isRaw}, isPaused=${stdin.isPaused()}, readable=${stdin.readable}, listenerCount(data)=${stdin.listenerCount('data')}`);
  #| }

///|
/// Debug stdin state (public)
pub fn debug_stdin_state(label : String) -> Unit {
  js_debug_stdin_state(label)
}

///|
/// Cleanup stdin (turn off raw mode)
/// Also cleans up /dev/tty if it was used
extern "js" fn js_cleanup_stdin() =
  #| async () => {
  #|   if (typeof process === 'undefined') return;
  #|   const fs = await import('node:fs');
  #|
  #|   // Clean up /dev/tty stream if used
  #|   if (globalThis.__tui_tty_stream) {
  #|     const stream = globalThis.__tui_tty_stream;
  #|     if (stream.isTTY && stream.setRawMode) {
  #|       stream.setRawMode(false);
  #|     }
  #|     stream.destroy();
  #|     globalThis.__tui_tty_stream = null;
  #|   }
  #|   if (globalThis.__tui_tty_fd !== undefined && globalThis.__tui_tty_fd !== null) {
  #|     try { fs.closeSync(globalThis.__tui_tty_fd); } catch (e) {}
  #|     globalThis.__tui_tty_fd = null;
  #|   }
  #|
  #|   if (process.stdin.isTTY) {
  #|     process.stdin.setRawMode(false);
  #|     process.stdin.pause();
  #|   }
  #| }

///|
/// Enable raw mode
/// Falls back to /dev/tty if stdin is not a tty
extern "js" fn js_enable_raw_mode() =
  #| async () => {
  #|   if (typeof process === 'undefined') return;
  #|   const fs = await import('node:fs');
  #|   const tty = await import('node:tty');
  #|
  #|   if (process.stdin.isTTY) {
  #|     process.stdin.setRawMode(true);
  #|     process.stdin.resume();
  #|   } else {
  #|     // Try /dev/tty
  #|     try {
  #|       const fd = fs.openSync('/dev/tty', fs.constants.O_RDONLY);
  #|       const stream = new tty.ReadStream(fd);
  #|       stream.setRawMode(true);
  #|       stream.resume();
  #|       globalThis.__tui_tty_fd = fd;
  #|       globalThis.__tui_tty_stream = stream;
  #|     } catch (e) {
  #|       // /dev/tty not available
  #|     }
  #|   }
  #| }

///|
/// Get terminal columns
extern "js" fn js_get_terminal_columns() -> Int =
  #| () => {
  #|   if (typeof process === 'undefined') return 80;
  #|   return process.stdout.columns || 80;
  #| }

///|
/// Get terminal rows
extern "js" fn js_get_terminal_rows() -> Int =
  #| () => {
  #|   if (typeof process === 'undefined') return 24;
  #|   return process.stdout.rows || 24;
  #| }

///|
/// Sleep for milliseconds
extern "js" fn js_sleep(ms : Int) -> @js_async.Promise[Unit] =
  #| (ms) => new Promise(r => setTimeout(r, ms))

///|
/// Get current time in milliseconds
extern "js" fn js_get_time_ms() -> Int =
  #| () => Date.now()

///|
/// Get current time in milliseconds (monotonic)
pub fn get_time_ms() -> Int {
  js_get_time_ms()
}

///|
/// Keep the process alive without blocking (non-blocking)
/// This schedules a timeout but doesn't wait - keeps event loop active
extern "js" fn js_keep_alive_nonblocking(ms : Int) =
  #| (ms) => {
  #|   // Simple timer keeps the process alive
  #|   setTimeout(() => {}, ms);
  #| }

///|
/// Keep process alive without blocking the event loop
pub fn keep_alive_nonblocking(ms : Int) -> Unit {
  js_keep_alive_nonblocking(ms)
}

///|
/// Start a keypress listener (event-driven, non-blocking)
/// Uses raw data events for reliability
/// Falls back to /dev/tty if stdin is not a tty (e.g., when used in a pipe)
extern "js" fn js_start_keypress_listener(callback : (String) -> Unit) =
  #| async (callback) => {
  #|   const { stdin } = await import('node:process');
  #|   const fs = await import('node:fs');
  #|   const tty = await import('node:tty');
  #|
  #|   // Clean up any existing listeners
  #|   stdin.removeAllListeners('keypress');
  #|   stdin.removeAllListeners('data');
  #|
  #|   // Use /dev/tty if stdin is not a tty (piped input)
  #|   let inputStream = stdin;
  #|   let ttyFd = null;
  #|
  #|   if (!stdin.isTTY) {
  #|     try {
  #|       ttyFd = fs.openSync('/dev/tty', fs.constants.O_RDONLY);
  #|       // Create a tty.ReadStream for proper raw mode support
  #|       inputStream = new tty.ReadStream(ttyFd);
  #|       globalThis.__tui_tty_fd = ttyFd;
  #|       globalThis.__tui_tty_stream = inputStream;
  #|     } catch (e) {
  #|       // /dev/tty not available, fall back to stdin
  #|       inputStream = stdin;
  #|     }
  #|   }
  #|
  #|   if (inputStream.isTTY && inputStream.setRawMode) {
  #|     inputStream.setRawMode(true);
  #|   }
  #|   inputStream.resume();
  #|   inputStream.setEncoding('utf8');
  #|
  #|   const onData = (data) => callback(data);
  #|
  #|   inputStream.__dataHandler = onData;
  #|   inputStream.on('data', onData);
  #| }

///|
/// Stop the keypress listener
/// Cleans up /dev/tty if it was opened
extern "js" fn js_stop_keypress_listener() =
  #| async () => {
  #|   const { stdin } = await import('node:process');
  #|   const fs = await import('node:fs');
  #|
  #|   // Clean up /dev/tty stream if used
  #|   if (globalThis.__tui_tty_stream) {
  #|     const stream = globalThis.__tui_tty_stream;
  #|     if (stream.__dataHandler) {
  #|       stream.removeListener('data', stream.__dataHandler);
  #|       stream.__dataHandler = null;
  #|     }
  #|     if (stream.isTTY && stream.setRawMode) {
  #|       stream.setRawMode(false);
  #|     }
  #|     stream.destroy();
  #|     globalThis.__tui_tty_stream = null;
  #|   }
  #|   if (globalThis.__tui_tty_fd !== undefined && globalThis.__tui_tty_fd !== null) {
  #|     try { fs.closeSync(globalThis.__tui_tty_fd); } catch (e) {}
  #|     globalThis.__tui_tty_fd = null;
  #|   }
  #|
  #|   // Also clean up stdin handlers
  #|   if (stdin.__dataHandler) {
  #|     stdin.removeListener('data', stdin.__dataHandler);
  #|     stdin.__dataHandler = null;
  #|   }
  #|   if (stdin.isTTY) {
  #|     stdin.setRawMode(false);
  #|   }
  #|   stdin.pause();
  #| }

///|
/// Start listening for keypresses (event-driven)
pub fn start_keypress_listener(callback : (String) -> Unit) -> Unit {
  js_start_keypress_listener(callback)
}

///|
/// Stop listening for keypresses
pub fn stop_keypress_listener() -> Unit {
  js_stop_keypress_listener()
  // Also stop idle interval
  if idle_interval_id.val >= 0 {
    js_stop_idle_interval(idle_interval_id.val)
    idle_interval_id.val = -1
  }
}

///|
/// Start idle interval timer
extern "js" fn js_start_idle_interval(handler : () -> Unit, ms : Int) -> Int =
  #| (handler, ms) => setInterval(() => handler(), ms)

///|
/// Stop idle interval timer
extern "js" fn js_stop_idle_interval(id : Int) =
  #| (id) => clearInterval(id)

///|
/// Idle interval ID storage
let idle_interval_id : Ref[Int] = Ref(-1)

///|
/// Set idle handler (uses setInterval for JS)
pub fn set_idle_handler(handler : () -> Unit) -> Unit {
  // Clear previous interval if any
  if idle_interval_id.val >= 0 {
    js_stop_idle_interval(idle_interval_id.val)
  }
  // Start new interval (10ms polling)
  idle_interval_id.val = js_start_idle_interval(handler, 10)
}

///|
/// Start an inline input session with cooked mode (IME enabled)
/// Exits alternate screen temporarily for proper IME support
/// Enter confirms, Escape cancels, Ctrl+C force quits
extern "js" fn js_start_inline_input_cooked(
  field_name : String,
  initial : String,
  confirm_on_shift_enter : Bool,
  esc_cancels : Bool,
  on_confirmed : (String) -> Unit,
  on_cancelled : () -> Unit,
  on_force_quit : () -> Unit,
) -> Unit =
  #| async (fieldName, initial, confirmOnShiftEnter, escCancels, onConfirmed, onCancelled, onForceQuit) => {
  #|   const { stdin, stdout, stderr } = await import('node:process');
  #|   const { execSync } = await import('node:child_process');
  #|   const debug = process.env.TUI_DEBUG_COOKED === '1';
  #|   const reserveRows = Number(process.env.TUI_IME_RESERVE_ROWS || '1');
  #|   const toHex = (text) => Array.from(text)
  #|     .map((ch) => ch.codePointAt(0).toString(16).padStart(4, '0'))
  #|     .join(' ');
  #|   const show = (text) => {
  #|     try {
  #|       return JSON.stringify(text);
  #|     } catch {
  #|       return '[unprintable]';
  #|     }
  #|   };
  #|
  #|   // Show cursor (stay in alt screen)
  #|   stdout.write('\x1b[?25h');
  #|   // Enable modifyOtherKeys to detect Shift+Enter if supported
  #|   stdout.write('\x1b[>4;2m');
  #|
  #|   let sttyState = null;
  #|   if (stdin.isTTY) {
  #|     try {
  #|       sttyState = execSync('stty -g < /dev/tty', { encoding: 'utf8' }).trim();
  #|     } catch {
  #|       sttyState = null;
  #|     }
  #|   }
  #|
  #|   // Clean up any existing handlers
  #|   if (stdin.__dataHandler) {
  #|     stdin.removeListener('data', stdin.__dataHandler);
  #|     stdin.__dataHandler = null;
  #|   }
  #|   if (stdin.__cookedHandler) {
  #|     stdin.removeListener('data', stdin.__cookedHandler);
  #|     stdin.__cookedHandler = null;
  #|   }
  #|
  #|   // Set cooked mode for IME support
  #|   if (stdin.isTTY) {
  #|     stdin.setRawMode(false);
  #|   }
  #|   stdin.resume();
  #|   stdin.setEncoding('utf8');
  #|   if (stdin.isTTY && sttyState) {
  #|     try {
  #|       execSync('stty -echoctl < /dev/tty');
  #|     } catch {
  #|       try {
  #|         execSync('stty -ctlecho < /dev/tty');
  #|       } catch {
  #|         // ignore if unsupported
  #|       }
  #|     }
  #|   }
  #|
  #|   if (stdin.isTTY && sttyState) {
  #|     try {
  #|       execSync('stty -icanon -echo -icrnl < /dev/tty');
  #|     } catch {
  #|       // ignore if unsupported
  #|     }
  #|   }
  #|
  #|   const initialText = initial || '';
  #|   let chars = Array.from(initialText);
  #|   let cursor = chars.length;
  #|   let done = false;
  #|
  #|   const shiftEnterSeqs = [
  #|     '\x1b[13;2u',
  #|     '\x1b[27;2;13~',
  #|     '\x1bOM',
  #|     '\x1b\r',
  #|     '\x1b\n',
  #|   ];
  #|   const shiftEnterAt = (text, index) =>
  #|     shiftEnterSeqs.find((seq) => text.startsWith(seq, index));
  #|   const deleteSeq = '\x1b[3~';
  #|
  #|   const getCharWidth = (char) => {
  #|     const code = char.codePointAt(0);
  #|     if (code >= 0x1100 && (
  #|       code <= 0x115F ||
  #|       code === 0x2329 || code === 0x232A ||
  #|       (code >= 0x2E80 && code <= 0x303E) ||
  #|       (code >= 0x3040 && code <= 0x33FF) ||
  #|       (code >= 0x3400 && code <= 0x4DBF) ||
  #|       (code >= 0x4E00 && code <= 0x9FFF) ||
  #|       (code >= 0xAC00 && code <= 0xD7A3) ||
  #|       (code >= 0xF900 && code <= 0xFAFF) ||
  #|       (code >= 0xFE10 && code <= 0xFE1F) ||
  #|       (code >= 0xFE30 && code <= 0xFE6F) ||
  #|       (code >= 0xFF00 && code <= 0xFF60) ||
  #|       (code >= 0xFFE0 && code <= 0xFFE6) ||
  #|       (code >= 0x20000 && code <= 0x2FFFF)
  #|     )) {
  #|       return 2;
  #|     }
  #|     return 1;
  #|   };
  #|
  #|   const wrapText = (chars) => {
  #|     const lines = [];
  #|     let currentLine = [];
  #|     let lineStartIdx = 0;
  #|     for (let i = 0; i < chars.length; i++) {
  #|       const char = chars[i];
  #|       if (char === '\n') {
  #|         lines.push({ chars: currentLine, startIdx: lineStartIdx });
  #|         currentLine = [];
  #|         lineStartIdx = i + 1;
  #|         continue;
  #|       }
  #|       currentLine.push(char);
  #|     }
  #|     lines.push({ chars: currentLine, startIdx: lineStartIdx });
  #|     return lines;
  #|   };
  #|
  #|   const findCursorPos = (lines, cursor) => {
  #|     for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
  #|       const line = lines[lineIdx];
  #|       const lineEnd = line.startIdx + line.chars.length;
  #|       if (cursor <= lineEnd) {
  #|         const posInLine = cursor - line.startIdx;
  #|         let colOffset = 0;
  #|         for (let i = 0; i < posInLine; i++) {
  #|           colOffset += getCharWidth(line.chars[i]);
  #|         }
  #|         return { lineIdx, colOffset };
  #|       }
  #|     }
  #|     const lastLine = lines[lines.length - 1];
  #|     let colOffset = 0;
  #|     for (const char of lastLine.chars) {
  #|       colOffset += getCharWidth(char);
  #|     }
  #|     return { lineIdx: lines.length - 1, colOffset };
  #|   };
  #|
  #|   const readCursorPos = () => new Promise((resolve) => {
  #|     let buf = '';
  #|     const onResp = (chunk) => {
  #|       buf += chunk;
  #|       const match = /\x1b\[(\d+);(\d+)R/.exec(buf);
  #|       if (match) {
  #|         stdin.removeListener('data', onResp);
  #|         resolve({ row: Number(match[1]), col: Number(match[2]) });
  #|       }
  #|     };
  #|     stdin.on('data', onResp);
  #|     stdout.write('\x1b[6n');
  #|     setTimeout(() => {
  #|       stdin.removeListener('data', onResp);
  #|       resolve(null);
  #|     }, 100);
  #|   });
  #|
  #|   let startRow = null;
  #|   let startCol = 1;
  #|   let canRender = false;
  #|   let prefix = '';
  #|   let renderedLines = Math.max(1, initialText.split('\n').length);
  #|   let maxRows = null;
  #|   let maxCols = null;
  #|   let cursorPending = false;
  #|   let scrollOffset = 0;
  #|
  #|   const updatePrefix = () => {
  #|     prefix = startCol > 1 ? ' '.repeat(startCol - 1) : '';
  #|   };
  #|
  #|   const applyCursorPos = (pos) => {
  #|     if (!pos) return false;
  #|     startRow = pos.row;
  #|     startCol = pos.col;
  #|     canRender = true;
  #|     if (stdout.rows) {
  #|       const available = stdout.rows - startRow + 1 - reserveRows;
  #|       maxRows = Math.max(1, available);
  #|     }
  #|     if (stdout.columns) {
  #|       const availableCols = stdout.columns - startCol + 1;
  #|       maxCols = Math.max(1, availableCols);
  #|     }
  #|     updatePrefix();
  #|     return true;
  #|   };
  #|
  #|   const bufferString = () => chars.join('');
  #|
  #|   const renderAll = () => {
  #|     if (!canRender) return false;
  #|     const lines = wrapText(chars);
  #|     const cursorPos = findCursorPos(lines, cursor);
  #|     const maxVisible = maxRows || lines.length;
  #|
  #|     if (lines.length <= maxVisible) {
  #|       scrollOffset = 0;
  #|     } else {
  #|       if (cursorPos.lineIdx < scrollOffset) {
  #|         scrollOffset = cursorPos.lineIdx;
  #|       } else if (cursorPos.lineIdx >= scrollOffset + maxVisible) {
  #|         scrollOffset = cursorPos.lineIdx - maxVisible + 1;
  #|       }
  #|       scrollOffset = Math.max(0, Math.min(scrollOffset, lines.length - maxVisible));
  #|     }
  #|
  #|     const visible = lines.slice(scrollOffset, scrollOffset + maxVisible);
  #|
  #|     stdout.write(`\x1b[${startRow};${startCol}H`);
  #|     const clearLines = maxRows ? Math.min(renderedLines, maxRows) : renderedLines;
  #|     for (let i = 0; i < clearLines; i++) {
  #|       stdout.write('\x1b[K');
  #|       if (i < clearLines - 1) {
  #|         stdout.write('\x1b[1B');
  #|       }
  #|     }
  #|     stdout.write(`\x1b[${startRow};${startCol}H`);
  #|     for (let i = 0; i < visible.length; i++) {
  #|       if (i > 0) {
  #|         stdout.write('\n' + prefix);
  #|       }
  #|       stdout.write(visible[i].chars.join(''));
  #|     }
  #|
  #|     renderedLines = Math.max(1, visible.length);
  #|
  #|     const cursorLineOffset = cursorPos.lineIdx - scrollOffset;
  #|     const maxRowOffset = maxVisible - 1;
  #|     const displayRow = startRow + Math.max(0, Math.min(cursorLineOffset, maxRowOffset));
  #|     const displayCol = startCol + Math.max(0, cursorPos.colOffset);
  #|     stdout.write(`\x1b[${displayRow};${displayCol}H`);
  #|     stdout.write('\x1b[?25h');
  #|     return true;
  #|   };
  #|
  #|   const lineBounds = (cursor) => {
  #|     let start = 0;
  #|     for (let i = cursor - 1; i >= 0; i--) {
  #|       if (chars[i] === '\n') {
  #|         start = i + 1;
  #|         break;
  #|       }
  #|     }
  #|     let end = chars.length;
  #|     for (let i = cursor; i < chars.length; i++) {
  #|       if (chars[i] === '\n') {
  #|         end = i;
  #|         break;
  #|       }
  #|     }
  #|     return { start, end };
  #|   };
  #|
  #|   const requestCursorPos = () => {
  #|     if (cursorPending || canRender) return;
  #|     cursorPending = true;
  #|     readCursorPos().then((pos) => {
  #|       cursorPending = false;
  #|       if (applyCursorPos(pos)) {
  #|         renderAll();
  #|       }
  #|     });
  #|   };
  #|
  #|   const initialPos = await readCursorPos();
  #|   applyCursorPos(initialPos);
  #|
  #|
  #|   const cleanup = () => {
  #|     if (stdin.__cookedHandler) {
  #|       stdin.removeListener('data', stdin.__cookedHandler);
  #|       stdin.__cookedHandler = null;
  #|     }
  #|     // Disable modifyOtherKeys
  #|     stdout.write('\x1b[>4;m');
  #|     if (sttyState) {
  #|       try {
  #|         execSync(`stty ${sttyState} < /dev/tty`);
  #|       } catch {
  #|         // ignore restore errors
  #|       }
  #|     }
  #|   };
  #|
  #|   const finish = (result) => {
  #|     if (done) return;
  #|     done = true;
  #|     cleanup();
  #|     setImmediate(() => onConfirmed(result));
  #|   };
  #|
  #|   const cancel = () => {
  #|     if (done) return;
  #|     done = true;
  #|     cleanup();
  #|     setImmediate(() => onCancelled());
  #|   };
  #|
  #|   const forceQuit = () => {
  #|     if (done) return;
  #|     done = true;
  #|     cleanup();
  #|     setImmediate(() => onForceQuit());
  #|   };
  #|
  #|   if (!renderAll()) {
  #|     requestCursorPos();
  #|     if (chars.length > 0) {
  #|       stdout.write(bufferString());
  #|     }
  #|   }
  #|
  #|   const onData = (chunk) => {
  #|     if (done) return;
  #|
  #|     if (debug) {
  #|       stderr.write(`[cooked] raw=${show(chunk)} hex=${toHex(chunk)}\n`);
  #|     }
  #|
  #|     let i = 0;
  #|     let needRender = false;
  #|     while (i < chunk.length) {
  #|       if (chunk[i] === '\x1b') {
  #|         const seq = shiftEnterAt(chunk, i);
  #|         if (seq) {
  #|           if (confirmOnShiftEnter) {
  #|             const result = chars.length > 0 ? bufferString() : initialText;
  #|             finish(result);
  #|             return;
  #|           }
  #|           chars.splice(cursor, 0, '\n');
  #|           cursor += 1;
  #|           needRender = true;
  #|           i += seq.length;
  #|           continue;
  #|         }
  #|         if (chunk.startsWith(deleteSeq, i)) {
  #|           if (cursor < chars.length) {
  #|             chars.splice(cursor, 1);
  #|             needRender = true;
  #|           }
  #|           i += deleteSeq.length;
  #|           continue;
  #|         }
  #|         if (chunk.startsWith('\x1b[D', i)) {
  #|           if (cursor > 0) {
  #|             cursor -= 1;
  #|             needRender = true;
  #|           }
  #|           i += 3;
  #|           continue;
  #|         }
  #|         if (chunk.startsWith('\x1b[C', i)) {
  #|           if (cursor < chars.length) {
  #|             cursor += 1;
  #|             needRender = true;
  #|           }
  #|           i += 3;
  #|           continue;
  #|         }
  #|         if (chunk.startsWith('\x1b[H', i)) {
  #|           cursor = 0;
  #|           needRender = true;
  #|           i += 3;
  #|           continue;
  #|         }
  #|         if (chunk.startsWith('\x1b[A', i)) {
  #|           const lines = wrapText(chars);
  #|           const pos = findCursorPos(lines, cursor);
  #|           if (pos.lineIdx > 0) {
  #|             const prevLine = lines[pos.lineIdx - 1];
  #|             let targetCol = pos.colOffset;
  #|             let newCursor = prevLine.startIdx;
  #|             let w = 0;
  #|             for (let j = 0; j < prevLine.chars.length; j++) {
  #|               const cw = getCharWidth(prevLine.chars[j]);
  #|               if (w + cw > targetCol) break;
  #|               w += cw;
  #|               newCursor = prevLine.startIdx + j + 1;
  #|             }
  #|             cursor = Math.min(newCursor, prevLine.startIdx + prevLine.chars.length);
  #|             needRender = true;
  #|           }
  #|           i += 3;
  #|           continue;
  #|         }
  #|         if (chunk.startsWith('\x1b[B', i)) {
  #|           const lines = wrapText(chars);
  #|           const pos = findCursorPos(lines, cursor);
  #|           if (pos.lineIdx < lines.length - 1) {
  #|             const nextLine = lines[pos.lineIdx + 1];
  #|             let targetCol = pos.colOffset;
  #|             let newCursor = nextLine.startIdx;
  #|             let w = 0;
  #|             for (let j = 0; j < nextLine.chars.length; j++) {
  #|               const cw = getCharWidth(nextLine.chars[j]);
  #|               if (w + cw > targetCol) break;
  #|               w += cw;
  #|               newCursor = nextLine.startIdx + j + 1;
  #|             }
  #|             cursor = Math.min(newCursor, nextLine.startIdx + nextLine.chars.length);
  #|             needRender = true;
  #|           }
  #|           i += 3;
  #|           continue;
  #|         }
  #|         if (chunk.startsWith('\x1b[1~', i)) {
  #|           cursor = 0;
  #|           needRender = true;
  #|           i += 4;
  #|           continue;
  #|         }
  #|         if (chunk.startsWith('\x1b[F', i)) {
  #|           cursor = chars.length;
  #|           needRender = true;
  #|           i += 3;
  #|           continue;
  #|         }
  #|         if (chunk.startsWith('\x1b[4~', i)) {
  #|           cursor = chars.length;
  #|           needRender = true;
  #|           i += 4;
  #|           continue;
  #|         }
  #|         if (chunk.startsWith('\x1b[', i) || chunk.startsWith('\x1bO', i)) {
  #|           i += 2;
  #|           while (i < chunk.length) {
  #|             const c = chunk[i];
  #|             i += 1;
  #|             if (c >= '@' && c <= '~') {
  #|               break;
  #|             }
  #|           }
  #|           continue;
  #|         }
  #|         if (chunk.length === 1) {
  #|           if (escCancels) {
  #|             cancel();
  #|             return;
  #|           }
  #|           i += 1;
  #|           continue;
  #|         }
  #|         i += 1;
  #|         continue;
  #|       }
  #|
  #|       const code = chunk.codePointAt(i);
  #|       const ch = String.fromCodePoint(code);
  #|       const size = ch.length;
  #|       i += size;
  #|
  #|       if (ch === '\x03') {
  #|         forceQuit();
  #|         return;
  #|       }
  #|       if (ch === '\x7f' || ch === '\x08') {
  #|         if (cursor > 0) {
  #|           chars.splice(cursor - 1, 1);
  #|           cursor -= 1;
  #|           needRender = true;
  #|         }
  #|         continue;
  #|       }
  #|       if (ch === '\x01') {
  #|         const bounds = lineBounds(cursor);
  #|         cursor = bounds.start;
  #|         needRender = true;
  #|         continue;
  #|       }
  #|       if (ch === '\x05') {
  #|         const bounds = lineBounds(cursor);
  #|         cursor = bounds.end;
  #|         needRender = true;
  #|         continue;
  #|       }
  #|       if (ch === '\r' || ch === '\n') {
  #|         if (confirmOnShiftEnter) {
  #|           chars.splice(cursor, 0, '\n');
  #|           cursor += 1;
  #|           needRender = true;
  #|           continue;
  #|         }
  #|         const result = chars.length > 0 ? bufferString() : initialText;
  #|         finish(result);
  #|         return;
  #|       }
  #|
  #|       chars.splice(cursor, 0, ch);
  #|       cursor += 1;
  #|       needRender = true;
  #|     }
  #|
  #|     if (needRender) {
  #|       if (!renderAll()) {
  #|         requestCursorPos();
  #|         if (chars.length > 0) {
  #|           stdout.write(bufferString());
  #|         }
  #|       }
  #|     }
  #|   };
  #|
  #|   stdin.__cookedHandler = onData;
  #|   stdin.on('data', onData);
  #| }

///|
/// Start an inline input session (stays in current screen)
/// Uses raw mode for reliable input handling
/// Enter confirms, Escape cancels, Backspace deletes
/// Supports text wrapping within max_width
extern "js" fn js_start_inline_input(
  label : String,
  initial : String,
  start_row : Int,
  start_col : Int,
  max_width : Int,
  max_height : Int,
  on_confirmed : (String) -> Unit,
  on_cancelled : () -> Unit,
  on_tab_next : (String) -> Unit,
  on_tab_prev : (String) -> Unit,
) -> Unit =
  #| async (label, initial, startRow, startCol, maxWidth, maxHeight, onConfirmed, onCancelled, onTabNext, onTabPrev) => {
  #|   const { stdin, stdout } = await import('node:process');
  #|
  #|   // Helper to get display width of a character (1 for half-width, 2 for full-width)
  #|   const getCharWidth = (char) => {
  #|     const code = char.codePointAt(0);
  #|     if (code >= 0x1100 && (
  #|       code <= 0x115F ||
  #|       code === 0x2329 || code === 0x232A ||
  #|       (code >= 0x2E80 && code <= 0x303E) ||
  #|       (code >= 0x3040 && code <= 0x33FF) ||
  #|       (code >= 0x3400 && code <= 0x4DBF) ||
  #|       (code >= 0x4E00 && code <= 0x9FFF) ||
  #|       (code >= 0xAC00 && code <= 0xD7A3) ||
  #|       (code >= 0xF900 && code <= 0xFAFF) ||
  #|       (code >= 0xFE10 && code <= 0xFE1F) ||
  #|       (code >= 0xFE30 && code <= 0xFE6F) ||
  #|       (code >= 0xFF00 && code <= 0xFF60) ||
  #|       (code >= 0xFFE0 && code <= 0xFFE6) ||
  #|       (code >= 0x20000 && code <= 0x2FFFF)
  #|     )) {
  #|       return 2;
  #|     }
  #|     return 1;
  #|   };
  #|
  #|   // Get total display width of a string
  #|   const getStringWidth = (str) => {
  #|     let width = 0;
  #|     for (const char of str) {
  #|       width += getCharWidth(char);
  #|     }
  #|     return width;
  #|   };
  #|
  #|   // Render buffer with wrapping, returns cursor position
  #|   const renderBuffer = (buf) => {
  #|     let row = startRow;
  #|     let col = startCol;
  #|     let output = '';
  #|
  #|     // Move to start position and clear area
  #|     output += `\x1b[${startRow};${startCol}H`;
  #|     for (let r = 0; r < maxHeight; r++) {
  #|       output += `\x1b[${startRow + r};${startCol}H`;
  #|       output += ' '.repeat(maxWidth);
  #|     }
  #|     output += `\x1b[${startRow};${startCol}H`;
  #|
  #|     // Render text with wrapping
  #|     let lineWidth = 0;
  #|     for (const char of buf) {
  #|       const charWidth = getCharWidth(char);
  #|       if (lineWidth + charWidth > maxWidth) {
  #|         // Wrap to next line
  #|         row++;
  #|         col = startCol;
  #|         lineWidth = 0;
  #|         output += `\x1b[${row};${col}H`;
  #|       }
  #|       output += char;
  #|       lineWidth += charWidth;
  #|       col += charWidth;
  #|     }
  #|
  #|     stdout.write(output);
  #|     return { row, col };
  #|   };
  #|
  #|   // Clear all listeners for clean state
  #|   stdin.removeAllListeners('keypress');
  #|   stdin.removeAllListeners('data');
  #|   stdin.__dataHandler = null;
  #|
  #|   // Use raw mode for character-by-character input
  #|   if (stdin.isTTY) {
  #|     stdin.setRawMode(true);
  #|   }
  #|   stdin.resume();
  #|   stdin.setEncoding('utf8');
  #|
  #|   // Display initial value
  #|   let buffer = initial || '';
  #|   renderBuffer(buffer);
  #|
  #|   let done = false;
  #|
  #|   const cleanup = () => {
  #|     if (done) return;
  #|     done = true;
  #|     stdin.removeListener('data', onData);
  #|   };
  #|
  #|   const onData = (chunk) => {
  #|     if (done) return;
  #|
  #|     // Check for escape sequences first (they come as multi-char chunks)
  #|     if (chunk === '\x1b[Z') {
  #|       // Shift+Tab - confirm and go to previous
  #|       cleanup();
  #|       onTabPrev(buffer);
  #|       return;
  #|     }
  #|     if (chunk === '\x1b' || chunk.startsWith('\x1b[')) {
  #|       // Escape or other escape sequence - cancel
  #|       cleanup();
  #|       onCancelled();
  #|       return;
  #|     }
  #|
  #|     let needsRender = false;
  #|     for (const char of chunk) {
  #|       const code = char.charCodeAt(0);
  #|
  #|       if (code === 9) {
  #|         // Tab - confirm and go to next
  #|         cleanup();
  #|         onTabNext(buffer);
  #|         return;
  #|       } else if (code === 13) {
  #|         // Enter - confirm
  #|         cleanup();
  #|         onConfirmed(buffer);
  #|         return;
  #|       } else if (code === 3) {
  #|         // Ctrl+C - cancel
  #|         cleanup();
  #|         onCancelled();
  #|         return;
  #|       } else if (code === 127 || code === 8) {
  #|         // Backspace - delete last character
  #|         if (buffer.length > 0) {
  #|           const chars = [...buffer];
  #|           chars.pop();
  #|           buffer = chars.join('');
  #|           needsRender = true;
  #|         }
  #|       } else if (code >= 32) {
  #|         // Printable character - check if within bounds
  #|         const newWidth = getStringWidth(buffer + char);
  #|         const maxTotalWidth = maxWidth * maxHeight;
  #|         if (newWidth <= maxTotalWidth) {
  #|           buffer += char;
  #|           needsRender = true;
  #|         }
  #|       }
  #|     }
  #|
  #|     if (needsRender) {
  #|       renderBuffer(buffer);
  #|     }
  #|   };
  #|
  #|   stdin.on('data', onData);
  #| }

///|
/// Start an inline input session with cooked mode (for IME support)
/// Temporarily exits alternate screen for proper IME input
/// field_name: Display name for the field being edited
/// initial: Current value to pre-fill
pub fn start_inline_input_cooked(
  field_name : String,
  initial : String,
  on_result : (InputResult) -> Unit,
  confirm_on_shift_enter? : Bool = false,
  esc_cancels? : Bool = true,
) -> Unit {
  js_start_inline_input_cooked(
    field_name,
    initial,
    confirm_on_shift_enter,
    esc_cancels,
    fn(value) { on_result(InputResult::Confirmed(value)) },
    fn() { on_result(InputResult::Cancelled) },
    fn() { on_result(InputResult::ForceQuit) },
  )
}

///|
/// Start an inplace input session with cooked mode (IME enabled)
/// Stays in alternate screen, edits at specified position
/// row/col are 1-indexed ANSI coordinates
/// width is the max display width for the input
pub fn start_inplace_input(
  row : Int,
  col : Int,
  width : Int,
  height : Int,
  multiline : Bool,
  initial : String,
  on_result : (InputResult) -> Unit,
  confirm_on_shift_enter? : Bool = false,
  esc_cancels? : Bool = true,
  on_lines_change? : ((Int) -> Int)? = None,
  on_completion? : ((String, Int) -> String?)? = None,
) -> Unit {
  // Create controller with configuration
  let config = @input.TextInputConfig::new(
    width,
    height,
    multiline~,
    confirm_on_shift_enter~,
    esc_cancels~,
  )
  let controller = @input.TextInputController::new(config, initial)

  // Current height (can change dynamically)
  let current_height = Ref(height)
  // Base row is the bottom of input area (fixed)
  let base_row = row + height - 1

  // Helper to render and position cursor with scroll
  fn do_render() {
    let state = controller.get_display_state()
    // Calculate current start row (grows upward)
    let old_height = current_height.val
    let old_start_row = base_row - old_height + 1
    // Update height if callback provided
    match on_lines_change {
      Some(callback) => {
        let new_height = callback(state.total_lines)
        if new_height != current_height.val {
          current_height.val = new_height
          controller.resize(width, new_height)
        }
      }
      None => ()
    }
    // Calculate new start row
    let start_row = base_row - current_height.val + 1
    // Clear old area if height changed
    if old_height != current_height.val {
      if current_height.val < old_height {
        // Shrinking: clear above
        for r = old_start_row; r < start_row; r = r + 1 {
          print_raw(
            "\u001b[" +
            r.to_string() +
            ";" +
            col.to_string() +
            "H" +
            " ".repeat(width),
          )
        }
      } else {
        // Growing: clear new area above
        for r = start_row; r < old_start_row; r = r + 1 {
          print_raw(
            "\u001b[" +
            r.to_string() +
            ";" +
            col.to_string() +
            "H" +
            " ".repeat(width),
          )
        }
      }
    }
    // Get updated state after potential resize
    let state = controller.get_display_state()
    // Render with scroll (at calculated start row)
    render_display_state(state, start_row, col, width, current_height.val)
    // Position cursor
    let display_row = start_row + state.cursor_line - state.scroll_offset
    let display_col = col + state.cursor_col
    print_raw(
      "\u001b[" + display_row.to_string() + ";" + display_col.to_string() + "H",
    )
    print_raw(@render.ansi_show_cursor())
  }

  // Initial render
  do_render()

  // Track if done
  let done = Ref(false)

  // Key handler
  let handle_key : (String) -> Unit = fn(key) {
    if done.val {
      return
    }

    // Handle multi-character printable input (IME) before parse_input
    // parse_input only handles single characters, so we need to detect
    // multi-character input and bypass it
    let action = if key.length() > 1 && key[0].to_int() != 0x1b {
      // Not an escape sequence - check if all chars are printable
      let chars = key.to_array()
      let mut all_printable = true
      for c in chars {
        let code = c.to_int()
        if !((code >= 32 && code < 127) || code >= 128) {
          all_printable = false
          break
        }
      }
      if all_printable {
        // Multi-character printable input (IME) - insert entire string
        @input.EditAction::Insert(key)
      } else {
        // Parse normally
        let input_event = @events.parse_input(key)
        match input_event {
          @events.InputEvent::Key(key_event) =>
            @input.interpret_key(key_event, config)
          _ => @input.EditAction::None
        }
      }
    } else {
      // Single character or escape sequence - parse normally
      let input_event = @events.parse_input(key)
      match input_event {
        @events.InputEvent::Key(key_event) =>
          @input.interpret_key(key_event, config)
        _ => @input.EditAction::None
      }
    }

    // Handle special actions that require custom logic
    match action {
      @input.EditAction::Tab => {
        // Tab - try completion first, then confirm and go to next
        let mut handled_by_completion = false
        match on_completion {
          Some(provider) => {
            let text = controller.get_text()
            let completion_result = provider(text, controller.get_cursor_pos())
            match completion_result {
              Some(suffix) =>
                if suffix.length() > 0 {
                  let _ = controller.apply_action(
                    @input.EditAction::Insert(suffix),
                  )
                  do_render()
                  handled_by_completion = true
                }
              None => ()
            }
          }
          None => ()
        }
        if !handled_by_completion {
          done.val = true
          js_cleanup_raw_input_listener()
          on_result(InputResult::TabNext(controller.get_text()))
        }
      }
      @input.EditAction::BackTab => {
        done.val = true
        js_cleanup_raw_input_listener()
        on_result(InputResult::TabPrev(controller.get_text()))
      }
      @input.EditAction::Confirm => {
        done.val = true
        js_cleanup_raw_input_listener()
        on_result(InputResult::Confirmed(controller.get_text()))
      }
      @input.EditAction::Cancel => {
        done.val = true
        js_cleanup_raw_input_listener()
        on_result(InputResult::Cancelled)
      }
      @input.EditAction::ForceQuit => {
        done.val = true
        js_cleanup_raw_input_listener()
        on_result(InputResult::ForceQuit)
      }
      @input.EditAction::None => ()
      _ => {
        // Apply edit action to controller
        let handled = controller.apply_action(action)
        if handled {
          do_render()
        }
      }
    }
  }

  // Cleanup handler
  let cleanup_handler : () -> Unit = fn() {
    print_raw(@render.ansi_hide_cursor())
  }

  // Setup listener
  js_setup_raw_input_listener(handle_key, cleanup_handler)
}

///|
/// Start an inline input session (for editing in place)
/// row/col are 1-indexed ANSI coordinates
/// max_width/max_height define the input area for text wrapping
pub fn start_inline_input(
  label : String,
  initial : String,
  row : Int,
  col : Int,
  max_width : Int,
  max_height : Int,
  on_result : (InputResult) -> Unit,
) -> Unit {
  js_start_inline_input(
    label,
    initial,
    row,
    col,
    max_width,
    max_height,
    fn(value) { on_result(InputResult::Confirmed(value)) },
    fn() { on_result(InputResult::Cancelled) },
    fn(value) { on_result(InputResult::TabNext(value)) },
    fn(value) { on_result(InputResult::TabPrev(value)) },
  )
}

///|
/// Read a key asynchronously
pub async fn read_key() -> String {
  @js_async.Promise::wait(js_read_key())
}

///|
/// Print string to stdout (no newline)
pub fn print_raw(s : String) -> Unit {
  js_print_raw(s)
}

///|
/// Cleanup stdin state
pub fn cleanup_stdin() -> Unit {
  js_cleanup_stdin()
  raw_mode_flag.val = false
}

///|
/// Enable raw mode
pub fn enable_raw_mode() -> Unit {
  js_enable_raw_mode()
  raw_mode_flag.val = true
}

///|
/// Get terminal size (columns, rows)
pub fn get_terminal_size() -> (Int, Int) {
  (js_get_terminal_columns(), js_get_terminal_rows())
}

///|
/// Sleep for milliseconds
pub async fn sleep(ms : Int) -> Unit {
  @js_async.Promise::wait(js_sleep(ms))
}

// --- Input Session for IME support ---

///|
/// Start an input session with callback (non-blocking)
extern "js" fn js_start_input_session_callback(
  initial : String,
  on_confirmed : (String) -> Unit,
  on_cancelled : () -> Unit,
) -> Unit =
  #| async (initial, onConfirmed, onCancelled) => {
  #|   const rl = await import('node:readline/promises');
  #|   const { stdin, stdout } = await import('node:process');
  #|
  #|   // Show cursor
  #|   stdout.write('\x1b[?25h');
  #|
  #|   // Reset stdin state for readline
  #|   stdin.removeAllListeners('keypress');
  #|   if (stdin.isTTY) {
  #|     stdin.setRawMode(false);
  #|   }
  #|
  #|   const readline = rl.createInterface({
  #|     input: stdin,
  #|     output: stdout,
  #|   });
  #|
  #|   try {
  #|     // Pre-fill and get answer
  #|     const answer = await readline.question('> ' + initial);
  #|     readline.close();
  #|     onConfirmed(answer);
  #|   } catch (err) {
  #|     readline.close();
  #|     onCancelled();
  #|   }
  #| }

///|
/// Legacy Promise-based version (kept for reference, may not work properly)
extern "js" fn js_start_input_session(
  x : Int,
  y : Int,
  initial : String,
) -> @js_async.Promise[String] =
  #| async (x, y, initial) => {
  #|   return new Promise((resolve) => {
  #|     // This version may block the event loop - use callback version instead
  #|     resolve('cancelled:');
  #|   });
  #| }

///|
/// Abort the current input session
extern "js" fn js_abort_input_session() =
  #| () => {
  #|   if (globalThis.__inputSession && globalThis.__inputSession.active) {
  #|     globalThis.__inputSession.active = false;
  #|     if (globalThis.__inputSession.rl) {
  #|       globalThis.__inputSession.rl.close();
  #|     }
  #|     if (globalThis.__inputSession.resolve) {
  #|       globalThis.__inputSession.resolve('cancelled:');
  #|     }
  #|   }
  #| }

///|
/// Start a render ticker (setInterval)
extern "js" fn js_start_render_ticker(ms : Int, callback : () -> Unit) -> Int =
  #| (ms, cb) => setInterval(cb, ms)

///|
/// Stop a render ticker
extern "js" fn js_stop_render_ticker(ticker_id : Int) =
  #| (id) => clearInterval(id)

///|
/// Start an input session (callback-based, non-blocking)
/// This is the recommended way to read input - it doesn't block the event loop
pub fn start_input_session_cb(
  initial : String,
  on_result : (InputResult) -> Unit,
) -> Unit {
  js_start_input_session_callback(
    initial,
    fn(value) { on_result(InputResult::Confirmed(value)) },
    fn() { on_result(InputResult::Cancelled) },
  )
}

///|
/// Start an input session that returns a Promise (for async/await)
/// This wraps the callback-based API in a Promise created on the JS side
extern "js" fn js_start_input_session_promise(
  initial : String,
) -> @js_async.Promise[InputResult] =
  #| async (initial) => {
  #|   const rl = await import('node:readline/promises');
  #|   const { stdin, stdout } = await import('node:process');
  #|
  #|   // Show cursor
  #|   stdout.write('\x1b[?25h');
  #|
  #|   // Reset stdin state for readline
  #|   stdin.removeAllListeners('keypress');
  #|   if (stdin.isTTY) {
  #|     stdin.setRawMode(false);
  #|   }
  #|
  #|   const readline = rl.createInterface({
  #|     input: stdin,
  #|     output: stdout,
  #|   });
  #|
  #|   try {
  #|     const answer = await readline.question('> ' + initial);
  #|     readline.close();
  #|     return { $tag: "Confirmed", _0: answer };
  #|   } catch (err) {
  #|     readline.close();
  #|     return { $tag: "Cancelled" };
  #|   }
  #| }

///|
/// Start an input session that returns a Promise (for async/await)
/// Use with @js_async.Promise::wait() in async functions
pub fn start_input_session_promise(
  initial : String,
) -> @js_async.Promise[InputResult] {
  js_start_input_session_promise(initial)
}

///|
/// Start an input session (async version - may not work with raw mode)
/// WARNING: This uses Promise::wait which can block the Node.js event loop
/// Use start_input_session_cb instead for reliable operation
pub async fn start_input_session(
  x : Int,
  y : Int,
  initial : String,
) -> InputResult {
  // This version doesn't work properly after raw mode - returns cancelled immediately
  let result = @js_async.Promise::wait(js_start_input_session(x, y, initial))
  if result.has_prefix("confirmed:") {
    InputResult::Confirmed(result[10:].to_owned())
  } else {
    InputResult::Cancelled
  }
}

///|
/// Abort the current input session
pub fn abort_input_session() -> Unit {
  js_abort_input_session()
}

///|
/// Start a render ticker that calls the callback every `ms` milliseconds
pub fn start_render_ticker(ms : Int, callback : () -> Unit) -> Int {
  js_start_render_ticker(ms, callback)
}

///|
/// Stop a render ticker
pub fn stop_render_ticker(ticker_id : Int) -> Unit {
  js_stop_render_ticker(ticker_id)
}

// --- Functions for native compatibility ---

///|
/// Check if raw mode is enabled (always true after enable_raw_mode in JS)
let raw_mode_flag : Ref[Bool] = { val: false }

///|
pub fn is_raw_mode() -> Bool {
  raw_mode_flag.val
}

///|
/// Check if a TTY is available for input (stdin or /dev/tty)
extern "js" fn js_is_tty() -> Bool =
  #| () => {
  #|   if (typeof process === 'undefined') return false;
  #|   if (process.stdin.isTTY) return true;
  #|   // Check if /dev/tty is available
  #|   try {
  #|     const fs = require('node:fs');
  #|     const fd = fs.openSync('/dev/tty', fs.constants.O_RDONLY);
  #|     fs.closeSync(fd);
  #|     return true;
  #|   } catch (e) {
  #|     return false;
  #|   }
  #| }

///|
/// Check if a TTY is available for input (stdin or /dev/tty)
pub fn is_tty() -> Bool {
  js_is_tty()
}

///|
/// Poll for keypress (not applicable in JS - use start_keypress_listener instead)
/// Returns false as JS uses event-driven model
pub fn poll_keypress() -> Bool {
  // JS uses event-driven model, not polling
  false
}

// --- Resize Event Listener ---

///|
/// Start a resize listener (event-driven)
/// Callback receives (width, height) when terminal is resized
extern "js" fn js_start_resize_listener(callback : (Int, Int) -> Unit) =
  #| async (callback) => {
  #|   const { stdout } = await import('node:process');
  #|
  #|   const onResize = () => {
  #|     const cols = stdout.columns || 80;
  #|     const rows = stdout.rows || 24;
  #|     callback(cols, rows);
  #|   };
  #|
  #|   stdout.__resizeHandler = onResize;
  #|   stdout.on('resize', onResize);
  #| }

///|
/// Stop the resize listener
extern "js" fn js_stop_resize_listener() =
  #| async () => {
  #|   const { stdout } = await import('node:process');
  #|
  #|   if (stdout.__resizeHandler) {
  #|     stdout.removeListener('resize', stdout.__resizeHandler);
  #|     delete stdout.__resizeHandler;
  #|   }
  #| }

///|
/// Start listening for terminal resize events
/// Callback receives (width, height) when terminal is resized
pub fn start_resize_listener(callback : (Int, Int) -> Unit) -> Unit {
  js_start_resize_listener(callback)
}

///|
/// Stop listening for resize events
pub fn stop_resize_listener() -> Unit {
  js_stop_resize_listener()
}

// --- Environment and Arguments ---

///|
/// Get command line arguments (excluding node and script path)
extern "js" fn js_get_args() -> Array[String] =
  #| () => {
  #|   if (typeof process === 'undefined') return [];
  #|   return process.argv.slice(2);
  #| }

///|
/// Get an environment variable value (empty string if not set)
extern "js" fn js_get_env(name : String) -> String =
  #| (name) => {
  #|   if (typeof process === 'undefined') return '';
  #|   return process.env[name] || '';
  #| }

///|
/// Get command line arguments
pub fn get_args() -> Array[String] {
  js_get_args()
}

///|
/// Get environment variable value (empty string if not set)
pub fn get_env(name : String) -> String {
  js_get_env(name)
}

///|
/// Check if running in headless mode (TUI_HEADLESS=1 or --headless argument)
pub fn is_headless() -> Bool {
  if js_get_env("TUI_HEADLESS") == "1" {
    return true
  }
  let args = js_get_args()
  for arg in args {
    if arg == "--headless" {
      return true
    }
  }
  false
}

// ===== TextInputController-based implementation =====

///|
/// Render display state with scroll support (shared with native)
fn render_display_state(
  state : @input.DisplayState,
  start_row : Int,
  start_col : Int,
  max_width : Int,
  max_height : Int,
) -> Unit {
  let mut output = ""
  // Clear area
  for r = 0; r < max_height; r = r + 1 {
    output = output +
      "\u001b[" +
      (start_row + r).to_string() +
      ";" +
      start_col.to_string() +
      "H"
    for w = 0; w < max_width; w = w + 1 {
      output = output + " "
    }
  }
  // Draw visible lines
  for i = 0; i < max_height; i = i + 1 {
    let line_idx = state.scroll_offset + i
    if line_idx >= state.lines.length() {
      break
    }
    let line = state.lines[line_idx]
    output = output +
      "\u001b[" +
      (start_row + i).to_string() +
      ";" +
      start_col.to_string() +
      "H"
    // Show ellipsis for overflow
    if i == 0 && state.has_more_above {
      output = output + "…"
      let mut w = 1
      for c in line.chars {
        let cw = @core.char_display_width(c)
        if w + cw > max_width {
          break
        }
        output = output + c.to_string()
        w = w + cw
      }
    } else if i == max_height - 1 && state.has_more_below {
      let mut w = 0
      for c in line.chars {
        let cw = @core.char_display_width(c)
        if w + cw > max_width - 1 {
          break
        }
        output = output + c.to_string()
        w = w + cw
      }
      output = output + "…"
    } else {
      for c in line.chars {
        output = output + c.to_string()
      }
    }
  }
  print_raw(output)
}

///|
/// Setup raw input listener that calls onKey callback for each keystroke
/// Falls back to /dev/tty if stdin is not a tty
extern "js" fn js_setup_raw_input_listener(
  on_key : (String) -> Unit,
  on_cleanup : () -> Unit,
) -> Unit =
  #| async (onKey, onCleanup) => {
  #|   const { stdin, stdout } = await import('node:process');
  #|   const fs = await import('node:fs');
  #|   const tty = await import('node:tty');
  #|
  #|   // Clean up existing handlers
  #|   if (stdin.__rawInputHandler) {
  #|     stdin.removeListener('data', stdin.__rawInputHandler);
  #|     stdin.__rawInputHandler = null;
  #|   }
  #|
  #|   // Use /dev/tty if stdin is not a tty (piped input)
  #|   let inputStream = stdin;
  #|   let ttyFd = null;
  #|
  #|   if (!stdin.isTTY) {
  #|     try {
  #|       ttyFd = fs.openSync('/dev/tty', fs.constants.O_RDONLY);
  #|       inputStream = new tty.ReadStream(ttyFd);
  #|       globalThis.__tui_raw_tty_fd = ttyFd;
  #|       globalThis.__tui_raw_tty_stream = inputStream;
  #|     } catch (e) {
  #|       // /dev/tty not available, fall back to stdin
  #|       inputStream = stdin;
  #|     }
  #|   }
  #|
  #|   if (inputStream.isTTY && inputStream.setRawMode) {
  #|     inputStream.setRawMode(true);
  #|   }
  #|   inputStream.resume();
  #|   inputStream.setEncoding('utf8');
  #|
  #|   const handler = (data) => {
  #|     onKey(data);
  #|   };
  #|
  #|   inputStream.__rawInputHandler = handler;
  #|   inputStream.__rawInputCleanup = onCleanup;
  #|   inputStream.on('data', handler);
  #| }

///|
/// Cleanup raw input listener
/// Also cleans up /dev/tty if it was used
extern "js" fn js_cleanup_raw_input_listener() =
  #| async () => {
  #|   const { stdin } = await import('node:process');
  #|   const fs = await import('node:fs');
  #|
  #|   // Clean up /dev/tty stream if used for raw input
  #|   if (globalThis.__tui_raw_tty_stream) {
  #|     const stream = globalThis.__tui_raw_tty_stream;
  #|     if (stream.__rawInputHandler) {
  #|       stream.removeListener('data', stream.__rawInputHandler);
  #|       stream.__rawInputHandler = null;
  #|     }
  #|     if (stream.__rawInputCleanup) {
  #|       stream.__rawInputCleanup();
  #|       stream.__rawInputCleanup = null;
  #|     }
  #|     if (stream.isTTY && stream.setRawMode) {
  #|       stream.setRawMode(false);
  #|     }
  #|     stream.destroy();
  #|     globalThis.__tui_raw_tty_stream = null;
  #|   }
  #|   if (globalThis.__tui_raw_tty_fd !== undefined && globalThis.__tui_raw_tty_fd !== null) {
  #|     try { fs.closeSync(globalThis.__tui_raw_tty_fd); } catch (e) {}
  #|     globalThis.__tui_raw_tty_fd = null;
  #|   }
  #|
  #|   // Also clean up stdin handlers
  #|   if (stdin.__rawInputHandler) {
  #|     stdin.removeListener('data', stdin.__rawInputHandler);
  #|     stdin.__rawInputHandler = null;
  #|   }
  #|   if (stdin.__rawInputCleanup) {
  #|     stdin.__rawInputCleanup();
  #|     stdin.__rawInputCleanup = null;
  #|   }
  #|   if (stdin.isTTY) {
  #|     stdin.setRawMode(false);
  #|   }
  #| }