import { useRef, useEffect, useState } from "react"; import type { SearchAddon } from "@xterm/addon-search"; import "./SearchBar.css"; // --------------------------------------------------------------------------- // SearchBar — per-pane find-in-scrollback overlay. // // Rendered as an absolutely-positioned sibling of the xterm canvas inside // XtermPane's container div (position: relative). The SearchAddon instance // is owned by XtermPane and passed down as a prop; no IPC or Context needed. // // Toggle state (caseSensitive, regex) uses useState so aria-pressed reflects // the live value on every render — refs alone don't trigger re-renders. // --------------------------------------------------------------------------- interface SearchBarProps { searchAddon: SearchAddon; onClose: () => void; } export default function SearchBar({ searchAddon, onClose }: SearchBarProps) { const inputRef = useRef(null); const queryRef = useRef(""); const [caseSensitive, setCaseSensitive] = useState(false); const [useRegex, setUseRegex] = useState(false); // Keep stable refs to toggle values so findNext/findPrev closures always // see the current value without needing to be recreated on each state change. const caseSensitiveRef = useRef(caseSensitive); const useRegexRef = useRef(useRegex); useEffect(() => { caseSensitiveRef.current = caseSensitive; }, [caseSensitive]); useEffect(() => { useRegexRef.current = useRegex; }, [useRegex]); // Autofocus the input when the bar mounts. useEffect(() => { queueMicrotask(() => inputRef.current?.focus()); }, []); function getOptions() { return { caseSensitive: caseSensitiveRef.current, regex: useRegexRef.current, // Highlight all matches and mark the active one distinctly. decorations: { matchBackground: "#3a3a00", matchBorder: "#888800", matchOverviewRuler: "#888800", activeMatchBackground: "#b5890080", activeMatchBorder: "#e6c000", activeMatchColorOverviewRuler: "#e6c000", }, }; } function findNext() { if (!queryRef.current) return; searchAddon.findNext(queryRef.current, getOptions()); } function findPrev() { if (!queryRef.current) return; searchAddon.findPrevious(queryRef.current, getOptions()); } function handleInput(e: React.ChangeEvent) { queryRef.current = e.target.value; // Live-search: jump to next match as you type. if (queryRef.current) { searchAddon.findNext(queryRef.current, getOptions()); } } function handleKeyDown(e: React.KeyboardEvent) { if (e.key === "Escape") { e.preventDefault(); onClose(); } else if (e.key === "Enter") { e.preventDefault(); if (e.shiftKey) { findPrev(); } else { findNext(); } } } function toggleCase() { setCaseSensitive((v) => { const next = !v; caseSensitiveRef.current = next; // Re-run with the new option so decorations update immediately. if (queryRef.current) { searchAddon.findNext(queryRef.current, { ...getOptions(), caseSensitive: next, }); } return next; }); } function toggleRegex() { setUseRegex((v) => { const next = !v; useRegexRef.current = next; if (queryRef.current) { searchAddon.findNext(queryRef.current, { ...getOptions(), regex: next, }); } return next; }); } return (
); }