/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { useStdin, useStdout } from 'ink'; import type React from 'react'; import { createContext, useCallback, useContext, useEffect, useRef, } from 'react'; import { TerminalCapabilityManager } from '../utils/terminalCapabilityManager.js'; export type TerminalEventHandler = (event: string) => void; interface TerminalContextValue { subscribe: (handler: TerminalEventHandler) => void; unsubscribe: (handler: TerminalEventHandler) => void; queryTerminalBackground: () => Promise; } const TerminalContext = createContext( undefined, ); export function useTerminalContext() { const context = useContext(TerminalContext); if (!context) { throw new Error( 'useTerminalContext must be used within a TerminalProvider', ); } return context; } export function TerminalProvider({ children }: { children: React.ReactNode }) { const { stdin } = useStdin(); const { stdout } = useStdout(); const subscribers = useRef>(new Set()).current; const bufferRef = useRef(''); const subscribe = useCallback( (handler: TerminalEventHandler) => { subscribers.add(handler); }, [subscribers], ); const unsubscribe = useCallback( (handler: TerminalEventHandler) => { subscribers.delete(handler); }, [subscribers], ); const queryTerminalBackground = useCallback( async () => new Promise((resolve) => { const handler = () => { unsubscribe(handler); resolve(); }; subscribe(handler); TerminalCapabilityManager.queryBackgroundColor(stdout); setTimeout(() => { unsubscribe(handler); resolve(); }, 100); }), [stdout, subscribe, unsubscribe], ); useEffect(() => { const handleData = (data: Buffer | string) => { bufferRef.current += typeof data === 'string' ? data : data.toString('utf-8'); // Check for OSC 11 response const match = bufferRef.current.match( TerminalCapabilityManager.OSC_11_REGEX, ); if (match) { const colorStr = `rgb:${match[1]}/${match[2]}/${match[3]}`; for (const handler of subscribers) { handler(colorStr); } // Safely remove the processed part + match if (match.index !== undefined) { bufferRef.current = bufferRef.current.slice( match.index + match[0].length, ); } } else if (bufferRef.current.length > 4096) { // Safety valve: if buffer gets too large without a match, trim it. // We keep the last 1024 bytes to avoid cutting off a partial sequence. bufferRef.current = bufferRef.current.slice(-1024); } }; stdin.on('data', handleData); return () => { stdin.removeListener('data', handleData); }; }, [stdin, subscribers]); return ( {children} ); }