chore: initialize recovered claude workspace
This commit is contained in:
191
src/utils/earlyInput.ts
Normal file
191
src/utils/earlyInput.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user