earlyInput.ts
utils/earlyInput.ts
No strong subsystem tag
192
Lines
5398
Bytes
6
Exports
1
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 192 lines, 1 detected imports, and 6 detected exports.
Important relationships
Detected exports
startCapturingEarlyInputstopCapturingEarlyInputconsumeEarlyInputhasEarlyInputseedEarlyInputisCapturingEarlyInput
Keywords
stdininputcodeearlyprocessearlyinputbufferrepliscapturingcapturingreadablehandler
Detected imports
./intl.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
/**
* 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
}