source dump of claude code
at main 191 lines 5.4 kB view raw
1/** 2 * Early Input Capture 3 * 4 * This module captures terminal input that is typed before the REPL is fully 5 * initialized. Users often type `claude` and immediately start typing their 6 * prompt, but those early keystrokes would otherwise be lost during startup. 7 * 8 * Usage: 9 * 1. Call startCapturingEarlyInput() as early as possible in cli.tsx 10 * 2. When REPL is ready, call consumeEarlyInput() to get any buffered text 11 * 3. stopCapturingEarlyInput() is called automatically when input is consumed 12 */ 13 14import { lastGrapheme } from './intl.js' 15 16// Buffer for early input characters 17let earlyInputBuffer = '' 18// Flag to track if we're currently capturing 19let isCapturing = false 20// Reference to the readable handler so we can remove it later 21let readableHandler: (() => void) | null = null 22 23/** 24 * Start capturing stdin data early, before the REPL is initialized. 25 * Should be called as early as possible in the startup sequence. 26 * 27 * Only captures if stdin is a TTY (interactive terminal). 28 */ 29export function startCapturingEarlyInput(): void { 30 // Only capture in interactive mode: stdin must be a TTY, and we must not 31 // be in print mode. Raw mode disables ISIG (terminal Ctrl+C → SIGINT), 32 // which would make -p uninterruptible. 33 if ( 34 !process.stdin.isTTY || 35 isCapturing || 36 process.argv.includes('-p') || 37 process.argv.includes('--print') 38 ) { 39 return 40 } 41 42 isCapturing = true 43 earlyInputBuffer = '' 44 45 // Set stdin to raw mode and use 'readable' event like Ink does 46 // This ensures compatibility with how the REPL will handle stdin later 47 try { 48 process.stdin.setEncoding('utf8') 49 process.stdin.setRawMode(true) 50 process.stdin.ref() 51 52 readableHandler = () => { 53 let chunk = process.stdin.read() 54 while (chunk !== null) { 55 if (typeof chunk === 'string') { 56 processChunk(chunk) 57 } 58 chunk = process.stdin.read() 59 } 60 } 61 62 process.stdin.on('readable', readableHandler) 63 } catch { 64 // If we can't set raw mode, just silently continue without early capture 65 isCapturing = false 66 } 67} 68 69/** 70 * Process a chunk of input data 71 */ 72function processChunk(str: string): void { 73 let i = 0 74 while (i < str.length) { 75 const char = str[i]! 76 const code = char.charCodeAt(0) 77 78 // Ctrl+C (code 3) - stop capturing and exit immediately. 79 // We use process.exit here instead of gracefulShutdown because at this 80 // early stage of startup, the shutdown machinery isn't initialized yet. 81 if (code === 3) { 82 stopCapturingEarlyInput() 83 // eslint-disable-next-line custom-rules/no-process-exit 84 process.exit(130) // Standard exit code for Ctrl+C 85 return 86 } 87 88 // Ctrl+D (code 4) - EOF, stop capturing 89 if (code === 4) { 90 stopCapturingEarlyInput() 91 return 92 } 93 94 // Backspace (code 127 or 8) - remove last grapheme cluster 95 if (code === 127 || code === 8) { 96 if (earlyInputBuffer.length > 0) { 97 const last = lastGrapheme(earlyInputBuffer) 98 earlyInputBuffer = earlyInputBuffer.slice(0, -(last.length || 1)) 99 } 100 i++ 101 continue 102 } 103 104 // Skip escape sequences (arrow keys, function keys, focus events, etc.) 105 // All escape sequences start with ESC (0x1B) and end with a byte in 0x40-0x7E 106 if (code === 27) { 107 i++ // Skip the ESC character 108 // Skip until the terminating byte (@ to ~) or end of string 109 while ( 110 i < str.length && 111 !(str.charCodeAt(i) >= 64 && str.charCodeAt(i) <= 126) 112 ) { 113 i++ 114 } 115 if (i < str.length) i++ // Skip the terminating byte 116 continue 117 } 118 119 // Skip other control characters (except tab and newline) 120 if (code < 32 && code !== 9 && code !== 10 && code !== 13) { 121 i++ 122 continue 123 } 124 125 // Convert carriage return to newline 126 if (code === 13) { 127 earlyInputBuffer += '\n' 128 i++ 129 continue 130 } 131 132 // Add printable characters and allowed control chars to buffer 133 earlyInputBuffer += char 134 i++ 135 } 136} 137 138/** 139 * Stop capturing early input. 140 * Called automatically when input is consumed, or can be called manually. 141 */ 142export function stopCapturingEarlyInput(): void { 143 if (!isCapturing) { 144 return 145 } 146 147 isCapturing = false 148 149 if (readableHandler) { 150 process.stdin.removeListener('readable', readableHandler) 151 readableHandler = null 152 } 153 154 // Don't reset stdin state - the REPL's Ink App will manage stdin state. 155 // If we call setRawMode(false) here, it can interfere with the REPL's 156 // own stdin setup which happens around the same time. 157} 158 159/** 160 * Consume any early input that was captured. 161 * Returns the captured input and clears the buffer. 162 * Automatically stops capturing when called. 163 */ 164export function consumeEarlyInput(): string { 165 stopCapturingEarlyInput() 166 const input = earlyInputBuffer.trim() 167 earlyInputBuffer = '' 168 return input 169} 170 171/** 172 * Check if there is any early input available without consuming it. 173 */ 174export function hasEarlyInput(): boolean { 175 return earlyInputBuffer.trim().length > 0 176} 177 178/** 179 * Seed the early input buffer with text that will appear pre-filled 180 * in the prompt input when the REPL renders. Does not auto-submit. 181 */ 182export function seedEarlyInput(text: string): void { 183 earlyInputBuffer = text 184} 185 186/** 187 * Check if early input capture is currently active. 188 */ 189export function isCapturingEarlyInput(): boolean { 190 return isCapturing 191}