[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
192
components/terminal/TerminalSearchBar.tsx
Normal file
192
components/terminal/TerminalSearchBar.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* 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<TerminalSearchBarProps> = ({
|
||||
isOpen,
|
||||
focusToken,
|
||||
onClose,
|
||||
onSearch,
|
||||
onFindNext,
|
||||
onFindPrevious,
|
||||
matchCount,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>(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 (
|
||||
<div
|
||||
className="flex items-center gap-1.5 px-2 pt-0 pb-2 bg-black/50 backdrop-blur-sm"
|
||||
style={{
|
||||
backgroundColor: 'color-mix(in srgb, var(--terminal-ui-bg, #000000) 86%, transparent)',
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Search input */}
|
||||
<div className="relative flex-1">
|
||||
<Search
|
||||
size={12}
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2"
|
||||
style={{ color: 'color-mix(in srgb, var(--terminal-ui-fg, #ffffff) 40%, transparent)' }}
|
||||
/>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => 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)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Match count indicator - only show when no results */}
|
||||
{searchTerm.length > 0 && matchCount?.total === 0 && (
|
||||
<span
|
||||
className="text-[10px] flex-shrink-0"
|
||||
style={{ color: 'color-mix(in srgb, var(--terminal-ui-fg, #ffffff) 50%, transparent)' }}
|
||||
>
|
||||
{t("terminal.search.noResults")}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex items-center gap-0.5 flex-shrink-0">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 disabled:opacity-30"
|
||||
style={{
|
||||
color: 'color-mix(in srgb, var(--terminal-ui-fg, #ffffff) 60%, transparent)',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFindPrevious();
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
disabled={!searchTerm}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ChevronUp size={14} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("terminal.search.prevMatch")}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 disabled:opacity-30"
|
||||
style={{
|
||||
color: 'color-mix(in srgb, var(--terminal-ui-fg, #ffffff) 60%, transparent)',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFindNext();
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
disabled={!searchTerm}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ChevronDown size={14} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("terminal.search.nextMatch")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user