/** * Terminal Search Bar * Provides search functionality within terminal scrollback buffer */ import { ChevronUp, ChevronDown, Search } from 'lucide-react'; import React, { useState, useRef, useEffect, useCallback } from 'react'; import { useI18n } from '../../application/i18n/I18nProvider'; import { Button } from '../ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; export interface TerminalSearchBarProps { isOpen: boolean; /** * Incremented each time the search hotkey fires while the bar is already * open. Watched by the focus effect so Cmd/Ctrl+F re-grabs focus when it * has moved elsewhere (issue #1789). Ignored while `isOpen` is false. */ focusToken?: number; onClose: () => void; onSearch: (term: string) => boolean; onFindNext: () => boolean; onFindPrevious: () => boolean; matchCount?: { current: number; total: number } | null; } export const notifyTerminalSearchTermChange = ( searchTerm: string, previousSearchTerm: string, onSearch: (term: string) => boolean, ): string => { if (searchTerm === previousSearchTerm) return previousSearchTerm; onSearch(searchTerm); return searchTerm; }; export const TerminalSearchBar: React.FC = ({ isOpen, focusToken, onClose, onSearch, onFindNext, onFindPrevious, matchCount, }) => { const { t } = useI18n(); const [searchTerm, setSearchTerm] = useState(''); const inputRef = useRef(null); const prevSearchTermRef = useRef(''); // Focus input when opened, or when the search hotkey re-fires while open // (focusToken bumps) so focus returns to the input after it moved elsewhere. useEffect(() => { if (isOpen && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } }, [isOpen, focusToken]); // Trigger search when term changes. When the term is cleared we still call // onSearch('') so the underlying search addon clears its highlights; // otherwise the last match decorations linger after emptying the input. useEffect(() => { prevSearchTermRef.current = notifyTerminalSearchTermChange( searchTerm, prevSearchTermRef.current, onSearch, ); }, [searchTerm, onSearch]); const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); onClose(); } else if (e.key === 'Enter') { e.preventDefault(); if (e.shiftKey) { onFindPrevious(); } else { onFindNext(); } } else if (e.key === 'F3' || (e.key === 'g' && (e.ctrlKey || e.metaKey))) { e.preventDefault(); if (e.shiftKey) { onFindPrevious(); } else { onFindNext(); } } }, [onClose, onFindNext, onFindPrevious]); if (!isOpen) return null; return (
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} > {/* Search input */}
setSearchTerm(e.target.value)} onKeyDown={handleKeyDown} onClick={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} data-terminal-search-input="" placeholder={t("terminal.search.placeholder")} className="w-full h-6 pl-7 pr-2 text-[11px] border-none rounded placeholder:opacity-40 focus:outline-none" style={{ backgroundColor: 'color-mix(in srgb, var(--terminal-ui-fg, #ffffff) 5%, transparent)', color: 'var(--terminal-ui-fg, #ffffff)', }} />
{/* Match count indicator - only show when no results */} {searchTerm.length > 0 && matchCount?.total === 0 && ( {t("terminal.search.noResults")} )} {/* Navigation buttons */}
{t("terminal.search.prevMatch")} {t("terminal.search.nextMatch")}
); };