π File detail
utils/earlyInput.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes startCapturingEarlyInput, stopCapturingEarlyInput, consumeEarlyInput, hasEarlyInput, and seedEarlyInput (and more) β mainly functions, hooks, or classes. It composes internal code from intl (relative imports). What the file header says: Early Input Capture This module captures terminal input that is typed before the REPL is fully initialized. Users often type `claude` and immediately start typing their prompt, but those early keystrokes would otherwise be lost during startup. Usage: 1. Call startCapturingEarlyIn.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
Early Input Capture This module captures terminal input that is typed before the REPL is fully initialized. Users often type `claude` and immediately start typing their prompt, but those early keystrokes would otherwise be lost during startup. Usage: 1. Call startCapturingEarlyInput() as early as possible in cli.tsx 2. When REPL is ready, call consumeEarlyInput() to get any buffered text 3. stopCapturingEarlyInput() is called automatically when input is consumed
π€ Exports (heuristic)
startCapturingEarlyInputstopCapturingEarlyInputconsumeEarlyInputhasEarlyInputseedEarlyInputisCapturingEarlyInput
π₯οΈ Source preview
/**
* Early Input Capture
*
* This module captures terminal input that is typed before the REPL is fully
* initialized. Users often type `claude` and immediately start typing their
* prompt, but those early keystrokes would otherwise be lost during startup.
*
* Usage:
* 1. Call startCapturingEarlyInput() as early as possible in cli.tsx
* 2. When REPL is ready, call consumeEarlyInput() to get any buffered text
* 3. stopCapturingEarlyInput() is called automatically when input is consumed
*/
import { lastGrapheme } from './intl.js'
// Buffer for early input characters
let earlyInputBuffer = ''
// Flag to track if we're currently capturing
let isCapturing = false
// Reference to the readable handler so we can remove it later
let readableHandler: (() => void) | null = null
/**
* Start capturing stdin data early, before the REPL is initialized.
* Should be called as early as possible in the startup sequence.
*
* Only captures if stdin is a TTY (interactive terminal).
*/
export function startCapturingEarlyInput(): void {
// Only capture in interactive mode: stdin must be a TTY, and we must not
// be in print mode. Raw mode disables ISIG (terminal Ctrl+C β SIGINT),
// which would make -p uninterruptible.
if (
!process.stdin.isTTY ||
isCapturing ||
process.argv.includes('-p') ||
process.argv.includes('--print')
) {
return
}
isCapturing = true
earlyInputBuffer = ''
// Set stdin to raw mode and use 'readable' event like Ink does
// This ensures compatibility with how the REPL will handle stdin later
try {
process.stdin.setEncoding('utf8')
process.stdin.setRawMode(true)
process.stdin.ref()
readableHandler = () => {
let chunk = process.stdin.read()
while (chunk !== null) {
if (typeof chunk === 'string') {
processChunk(chunk)
}
chunk = process.stdin.read()
}
}
process.stdin.on('readable', readableHandler)
} catch {
// If we can't set raw mode, just silently continue without early capture
isCapturing = false
}
}
/**
* Process a chunk of input data
*/
function processChunk(str: string): void {
let i = 0
while (i < str.length) {
const char = str[i]!
const code = char.charCodeAt(0)
// Ctrl+C (code 3) - stop capturing and exit immediately.
// We use process.exit here instead of gracefulShutdown because at this
// early stage of startup, the shutdown machinery isn't initialized yet.
if (code === 3) {
stopCapturingEarlyInput()
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(130) // Standard exit code for Ctrl+C
return
}
// Ctrl+D (code 4) - EOF, stop capturing
if (code === 4) {
stopCapturingEarlyInput()
return
}
// Backspace (code 127 or 8) - remove last grapheme cluster
if (code === 127 || code === 8) {
if (earlyInputBuffer.length > 0) {
const last = lastGrapheme(earlyInputBuffer)
earlyInputBuffer = earlyInputBuffer.slice(0, -(last.length || 1))
}
i++
continue
}
// Skip escape sequences (arrow keys, function keys, focus events, etc.)
// All escape sequences start with ESC (0x1B) and end with a byte in 0x40-0x7E
if (code === 27) {
i++ // Skip the ESC character
// Skip until the terminating byte (@ to ~) or end of string
while (
i < str.length &&
!(str.charCodeAt(i) >= 64 && str.charCodeAt(i) <= 126)
) {
i++
}
if (i < str.length) i++ // Skip the terminating byte
continue
}
// Skip other control characters (except tab and newline)
if (code < 32 && code !== 9 && code !== 10 && code !== 13) {
i++
continue
}
// Convert carriage return to newline
if (code === 13) {
earlyInputBuffer += '\n'
i++
continue
}
// Add printable characters and allowed control chars to buffer
earlyInputBuffer += char
i++
}
}
/**
* Stop capturing early input.
* Called automatically when input is consumed, or can be called manually.
*/
export function stopCapturingEarlyInput(): void {
if (!isCapturing) {
return
}
isCapturing = false
if (readableHandler) {
process.stdin.removeListener('readable', readableHandler)
readableHandler = null
}
// Don't reset stdin state - the REPL's Ink App will manage stdin state.
// If we call setRawMode(false) here, it can interfere with the REPL's
// own stdin setup which happens around the same time.
}
/**
* Consume any early input that was captured.
* Returns the captured input and clears the buffer.
* Automatically stops capturing when called.
*/
export function consumeEarlyInput(): string {
stopCapturingEarlyInput()
const input = earlyInputBuffer.trim()
earlyInputBuffer = ''
return input
}
/**
* Check if there is any early input available without consuming it.
*/
export function hasEarlyInput(): boolean {
return earlyInputBuffer.trim().length > 0
}
/**
* Seed the early input buffer with text that will appear pre-filled
* in the prompt input when the REPL renders. Does not auto-submit.
*/
export function seedEarlyInput(text: string): void {
earlyInputBuffer = text
}
/**
* Check if early input capture is currently active.
*/
export function isCapturingEarlyInput(): boolean {
return isCapturing
}