/** * Host Keyword Highlight Popover * Allows users to manage host-specific keyword highlighting rules in the terminal statusbar */ import { Highlighter, Plus, Trash2, RotateCcw } from 'lucide-react'; import React, { useState, useCallback, useMemo } from 'react'; import { useI18n } from '../../application/i18n/I18nProvider'; import { Host, KeywordHighlightRule } from '../../types'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { ScrollArea } from '../ui/scroll-area'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; export interface HostKeywordHighlightPopoverProps { host?: Host; onUpdateHost?: (host: Host) => void; isOpen: boolean; setIsOpen: (open: boolean) => void; buttonClassName?: string; } const DEFAULT_NEW_RULE_COLOR = '#F87171'; export function addHostKeywordHighlightRule(host: Host, rule: KeywordHighlightRule): Host { return { ...host, keywordHighlightRules: [...(host.keywordHighlightRules ?? []), rule], keywordHighlightEnabled: true, }; } export const HostKeywordHighlightPopover: React.FC = ({ host, onUpdateHost, isOpen, setIsOpen, buttonClassName = '', }) => { const { t } = useI18n(); const [newRuleLabel, setNewRuleLabel] = useState(''); const [newRulePattern, setNewRulePattern] = useState(''); const [newRuleColor, setNewRuleColor] = useState(DEFAULT_NEW_RULE_COLOR); const [patternError, setPatternError] = useState(null); const rules = useMemo(() => host?.keywordHighlightRules ?? [], [host?.keywordHighlightRules]); const enabled = host?.keywordHighlightEnabled ?? false; const updateRules = useCallback((newRules: KeywordHighlightRule[]) => { if (!host || !onUpdateHost) return; onUpdateHost({ ...host, keywordHighlightRules: newRules }); }, [host, onUpdateHost]); const toggleEnabled = useCallback(() => { if (!host || !onUpdateHost) return; onUpdateHost({ ...host, keywordHighlightEnabled: !enabled }); }, [host, onUpdateHost, enabled]); const validatePattern = (pattern: string): boolean => { try { new RegExp(pattern, 'gi'); return true; } catch { return false; } }; const handleAddRule = useCallback(() => { if (!newRuleLabel.trim() || !newRulePattern.trim()) { return; } if (!validatePattern(newRulePattern)) { setPatternError(t('terminal.toolbar.hostHighlight.invalidPattern')); return; } const newRule: KeywordHighlightRule = { id: crypto.randomUUID(), label: newRuleLabel.trim(), patterns: [newRulePattern.trim()], color: newRuleColor, enabled: true, }; if (host && onUpdateHost) { onUpdateHost(addHostKeywordHighlightRule(host, newRule)); } // Reset form setNewRuleLabel(''); setNewRulePattern(''); setNewRuleColor(DEFAULT_NEW_RULE_COLOR); setPatternError(null); }, [newRuleLabel, newRulePattern, newRuleColor, host, onUpdateHost, t]); const handleDeleteRule = useCallback((ruleId: string) => { updateRules(rules.filter((r) => r.id !== ruleId)); }, [rules, updateRules]); const handleColorChange = useCallback((ruleId: string, color: string) => { updateRules(rules.map((r) => (r.id === ruleId ? { ...r, color } : r))); }, [rules, updateRules]); const handleToggleRule = useCallback((ruleId: string) => { updateRules(rules.map((r) => (r.id === ruleId ? { ...r, enabled: !r.enabled } : r))); }, [rules, updateRules]); const handleClearAll = useCallback(() => { if (!host || !onUpdateHost) return; onUpdateHost({ ...host, keywordHighlightRules: [], keywordHighlightEnabled: false }); }, [host, onUpdateHost]); const handlePatternChange = (value: string) => { setNewRulePattern(value); if (patternError && validatePattern(value)) { setPatternError(null); } }; // Disable if no host (local/serial terminal sessions) const isLocalTerminal = host?.protocol === 'local' || host?.id?.startsWith('local-'); const isSerialTerminal = host?.protocol === 'serial' || host?.id?.startsWith('serial-'); const isDisabled = !host || !onUpdateHost || isLocalTerminal || isSerialTerminal; return ( {/* Force-close tooltip while the panel is open so the blue label does not sit on top of the trigger/panel (especially in the compact top-right cluster when the host info bar is hidden). */} {/* Toolbar sits at the top of the terminal pane; open below the trigger so the label is not clipped by the window/title edge (especially in compact top-right action cluster when the host info bar is hidden). */} {t('terminal.toolbar.hostHighlight.title')}
{t('terminal.toolbar.hostHighlight.title')}
{rules.length === 0 ? (
{t('terminal.toolbar.hostHighlight.noRules')}
) : ( rules.map((rule) => (
)) )}
{/* Add new rule form */}
{t('terminal.toolbar.hostHighlight.addRule')}
setNewRuleLabel(e.target.value)} className="h-7 text-xs flex-1" />
handlePatternChange(e.target.value)} className={`h-7 text-xs font-mono flex-1 ${patternError ? 'border-destructive' : ''}`} />
{patternError && (
{patternError}
)}
{/* Footer actions */} {rules.length > 0 && (
)}
); }; export default HostKeywordHighlightPopover;