Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b067a9aae | ||
|
|
2d4a3a5602 | ||
|
|
6c57ce7b28 | ||
|
|
6a2bd0a6a1 |
@@ -871,6 +871,15 @@ const en: Messages = {
|
||||
'terminal.toolbar.focus': 'Focus',
|
||||
'terminal.toolbar.focusMode': 'Focus Mode',
|
||||
'terminal.toolbar.closeSession': 'Close session',
|
||||
'terminal.toolbar.hostHighlight.title': 'Host Keyword Highlighting',
|
||||
'terminal.toolbar.hostHighlight.noRules': 'No custom highlight rules defined for this host',
|
||||
'terminal.toolbar.hostHighlight.addRule': 'Add New Rule',
|
||||
'terminal.toolbar.hostHighlight.labelPlaceholder': 'Label (e.g., Error)',
|
||||
'terminal.toolbar.hostHighlight.patternPlaceholder': 'Regex pattern (e.g., \\bfailed\\b)',
|
||||
'terminal.toolbar.hostHighlight.invalidPattern': 'Invalid regex pattern',
|
||||
'terminal.toolbar.hostHighlight.clearAll': 'Clear All',
|
||||
'terminal.toolbar.hostHighlight.changeColor': 'Change highlight color for',
|
||||
'terminal.toolbar.hostHighlight.selectColor': 'Select color for new rule',
|
||||
'terminal.serverStats.cpu': 'CPU Usage',
|
||||
'terminal.serverStats.cpuCores': 'CPU Core Usage',
|
||||
'terminal.serverStats.memory': 'Memory Usage',
|
||||
|
||||
@@ -557,6 +557,15 @@ const zhCN: Messages = {
|
||||
'terminal.toolbar.focus': '聚焦',
|
||||
'terminal.toolbar.focusMode': '聚焦模式',
|
||||
'terminal.toolbar.closeSession': '关闭会话',
|
||||
'terminal.toolbar.hostHighlight.title': '主机关键字高亮',
|
||||
'terminal.toolbar.hostHighlight.noRules': '此主机未定义自定义高亮规则',
|
||||
'terminal.toolbar.hostHighlight.addRule': '添加新规则',
|
||||
'terminal.toolbar.hostHighlight.labelPlaceholder': '标签(例如:错误)',
|
||||
'terminal.toolbar.hostHighlight.patternPlaceholder': '正则表达式(例如:\\bfailed\\b)',
|
||||
'terminal.toolbar.hostHighlight.invalidPattern': '无效的正则表达式',
|
||||
'terminal.toolbar.hostHighlight.clearAll': '清除全部',
|
||||
'terminal.toolbar.hostHighlight.changeColor': '更改高亮颜色',
|
||||
'terminal.toolbar.hostHighlight.selectColor': '选择新规则的颜色',
|
||||
'terminal.serverStats.cpu': 'CPU 使用率',
|
||||
'terminal.serverStats.cpuCores': 'CPU 核心使用率',
|
||||
'terminal.serverStats.memory': '内存使用',
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* - components/sftp/SftpHostPicker.tsx - Host selection dialog
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import React, { memo, useCallback, useLayoutEffect, useMemo, useRef } from "react";
|
||||
import { useI18n } from "../application/i18n/I18nProvider";
|
||||
import { useIsSftpActive } from "../application/state/activeTabStore";
|
||||
import { useSftpState } from "../application/state/useSftpState";
|
||||
|
||||
@@ -216,12 +216,32 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (xtermRuntimeRef.current) {
|
||||
xtermRuntimeRef.current.keywordHighlighter.setRules(
|
||||
terminalSettings?.keywordHighlightRules ?? [],
|
||||
terminalSettings?.keywordHighlightEnabled ?? false
|
||||
);
|
||||
// Merge global rules with host-level rules
|
||||
// Host-level rules are appended to global rules, allowing hosts to add custom highlighting
|
||||
const globalRules = terminalSettings?.keywordHighlightRules ?? [];
|
||||
const hostRules = host?.keywordHighlightRules ?? [];
|
||||
|
||||
// Check if highlighting is enabled at either global or host level
|
||||
const globalEnabled = terminalSettings?.keywordHighlightEnabled ?? false;
|
||||
const hostEnabled = host?.keywordHighlightEnabled ?? false;
|
||||
|
||||
// Merge rules: include only rules from enabled sources
|
||||
const mergedRules = [
|
||||
...(globalEnabled ? globalRules : []),
|
||||
...(hostEnabled ? hostRules : [])
|
||||
];
|
||||
|
||||
// Enable highlighting if either global or host-level is enabled
|
||||
const isEnabled = globalEnabled || hostEnabled;
|
||||
|
||||
xtermRuntimeRef.current.keywordHighlighter.setRules(mergedRules, isEnabled);
|
||||
}
|
||||
}, [terminalSettings?.keywordHighlightEnabled, terminalSettings?.keywordHighlightRules]);
|
||||
}, [
|
||||
terminalSettings?.keywordHighlightEnabled,
|
||||
terminalSettings?.keywordHighlightRules,
|
||||
host?.keywordHighlightEnabled,
|
||||
host?.keywordHighlightRules
|
||||
]);
|
||||
|
||||
const hotkeySchemeRef = useRef(hotkeyScheme);
|
||||
const keyBindingsRef = useRef(keyBindings);
|
||||
@@ -447,6 +467,20 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
serializeAddonRef.current = runtime.serializeAddon;
|
||||
searchAddonRef.current = runtime.searchAddon;
|
||||
|
||||
// Apply merged keyword highlight rules immediately after runtime creation
|
||||
// This fixes a timing issue where the useEffect for keyword highlighting
|
||||
// runs before the runtime is created, causing host-level rules to be missed
|
||||
const globalRules = terminalSettingsRef.current?.keywordHighlightRules ?? [];
|
||||
const hostRules = host?.keywordHighlightRules ?? [];
|
||||
const globalEnabled = terminalSettingsRef.current?.keywordHighlightEnabled ?? false;
|
||||
const hostEnabled = host?.keywordHighlightEnabled ?? false;
|
||||
const mergedRules = [
|
||||
...(globalEnabled ? globalRules : []),
|
||||
...(hostEnabled ? hostRules : [])
|
||||
];
|
||||
const isEnabled = globalEnabled || hostEnabled;
|
||||
runtime.keywordHighlighter.setRules(mergedRules, isEnabled);
|
||||
|
||||
const term = runtime.term;
|
||||
|
||||
if (host.protocol === "serial") {
|
||||
@@ -996,7 +1030,7 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
|
||||
try {
|
||||
const dropEntries = await extractDropEntries(e.dataTransfer);
|
||||
|
||||
|
||||
if (dropEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -1087,7 +1121,7 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
onSplitVertical={onSplitVertical}
|
||||
onClose={inWorkspace ? () => onCloseSession?.(sessionId) : undefined}
|
||||
>
|
||||
<div
|
||||
<div
|
||||
className="relative h-full w-full flex overflow-hidden bg-gradient-to-br from-[#050910] via-[#06101a] to-[#0b1220]"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
@@ -1100,13 +1134,13 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
<div className="bg-background/90 backdrop-blur-md rounded-lg shadow-lg p-6 border border-border">
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-semibold mb-2">
|
||||
{isLocalConnection
|
||||
{isLocalConnection
|
||||
? t("terminal.dragDrop.localTitle")
|
||||
: t("terminal.dragDrop.remoteTitle")
|
||||
}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{isLocalConnection
|
||||
{isLocalConnection
|
||||
? t("terminal.dragDrop.localMessage")
|
||||
: t("terminal.dragDrop.remoteMessage")
|
||||
}
|
||||
@@ -1483,46 +1517,46 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
{status !== "connected" && !needsHostKeyVerification && !(
|
||||
(isLocalConnection || isSerialConnection) && status === "connecting"
|
||||
) && (
|
||||
<TerminalConnectionDialog
|
||||
host={host}
|
||||
status={status}
|
||||
error={error}
|
||||
progressValue={progressValue}
|
||||
chainProgress={chainProgress}
|
||||
needsAuth={auth.needsAuth}
|
||||
showLogs={showLogs}
|
||||
_setShowLogs={setShowLogs}
|
||||
keys={keys}
|
||||
authProps={{
|
||||
authMethod: auth.authMethod,
|
||||
setAuthMethod: auth.setAuthMethod,
|
||||
authUsername: auth.authUsername,
|
||||
setAuthUsername: auth.setAuthUsername,
|
||||
authPassword: auth.authPassword,
|
||||
setAuthPassword: auth.setAuthPassword,
|
||||
authKeyId: auth.authKeyId,
|
||||
setAuthKeyId: auth.setAuthKeyId,
|
||||
authPassphrase: auth.authPassphrase,
|
||||
setAuthPassphrase: auth.setAuthPassphrase,
|
||||
showAuthPassphrase: auth.showAuthPassphrase,
|
||||
setShowAuthPassphrase: auth.setShowAuthPassphrase,
|
||||
showAuthPassword: auth.showAuthPassword,
|
||||
setShowAuthPassword: auth.setShowAuthPassword,
|
||||
authRetryMessage: auth.authRetryMessage,
|
||||
onSubmit: () => auth.submit(),
|
||||
onSubmitWithoutSave: () => auth.submit({ saveToHost: false }),
|
||||
onCancel: handleCancelConnect,
|
||||
isValid: auth.isValid,
|
||||
}}
|
||||
progressProps={{
|
||||
timeLeft,
|
||||
isCancelling,
|
||||
progressLogs,
|
||||
onCancel: handleCancelConnect,
|
||||
onRetry: handleRetry,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<TerminalConnectionDialog
|
||||
host={host}
|
||||
status={status}
|
||||
error={error}
|
||||
progressValue={progressValue}
|
||||
chainProgress={chainProgress}
|
||||
needsAuth={auth.needsAuth}
|
||||
showLogs={showLogs}
|
||||
_setShowLogs={setShowLogs}
|
||||
keys={keys}
|
||||
authProps={{
|
||||
authMethod: auth.authMethod,
|
||||
setAuthMethod: auth.setAuthMethod,
|
||||
authUsername: auth.authUsername,
|
||||
setAuthUsername: auth.setAuthUsername,
|
||||
authPassword: auth.authPassword,
|
||||
setAuthPassword: auth.setAuthPassword,
|
||||
authKeyId: auth.authKeyId,
|
||||
setAuthKeyId: auth.setAuthKeyId,
|
||||
authPassphrase: auth.authPassphrase,
|
||||
setAuthPassphrase: auth.setAuthPassphrase,
|
||||
showAuthPassphrase: auth.showAuthPassphrase,
|
||||
setShowAuthPassphrase: auth.setShowAuthPassphrase,
|
||||
showAuthPassword: auth.showAuthPassword,
|
||||
setShowAuthPassword: auth.setShowAuthPassword,
|
||||
authRetryMessage: auth.authRetryMessage,
|
||||
onSubmit: () => auth.submit(),
|
||||
onSubmitWithoutSave: () => auth.submit({ saveToHost: false }),
|
||||
onCancel: handleCancelConnect,
|
||||
isValid: auth.isValid,
|
||||
}}
|
||||
progressProps={{
|
||||
timeLeft,
|
||||
isCancelling,
|
||||
progressLogs,
|
||||
onCancel: handleCancelConnect,
|
||||
onRetry: handleRetry,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SFTPModal
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* This store allows keyboard shortcuts to trigger dialogs in the appropriate pane.
|
||||
*/
|
||||
|
||||
import { useSyncExternalStore, useCallback, useEffect } from "react";
|
||||
import { useSyncExternalStore, useEffect } from "react";
|
||||
import { sftpFocusStore, SftpFocusedSide } from "./useSftpFocusedPane";
|
||||
|
||||
export type SftpDialogActionType = "rename" | "delete" | "newFolder" | "newFile" | null;
|
||||
|
||||
@@ -224,7 +224,7 @@ export const useSftpKeyboardShortcuts = ({
|
||||
sourceConnectionId: clipboard.sourceConnectionId,
|
||||
onTransferComplete: handleTransferComplete,
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("Paste failed. Please try again.", "SFTP");
|
||||
}
|
||||
} else {
|
||||
|
||||
295
components/terminal/HostKeywordHighlightPopover.tsx
Normal file
295
components/terminal/HostKeywordHighlightPopover.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* 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 { v4 as uuidv4 } from 'uuid';
|
||||
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';
|
||||
|
||||
export interface HostKeywordHighlightPopoverProps {
|
||||
host?: Host;
|
||||
onUpdateHost?: (host: Host) => void;
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
buttonClassName?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_NEW_RULE_COLOR = '#F87171';
|
||||
|
||||
export const HostKeywordHighlightPopover: React.FC<HostKeywordHighlightPopoverProps> = ({
|
||||
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<string | null>(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: uuidv4(),
|
||||
label: newRuleLabel.trim(),
|
||||
patterns: [newRulePattern.trim()],
|
||||
color: newRuleColor,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
updateRules([...rules, newRule]);
|
||||
|
||||
// Reset form
|
||||
setNewRuleLabel('');
|
||||
setNewRulePattern('');
|
||||
setNewRuleColor(DEFAULT_NEW_RULE_COLOR);
|
||||
setPatternError(null);
|
||||
|
||||
// Auto-enable if adding the first rule and not enabled
|
||||
if (rules.length === 0 && !enabled && host && onUpdateHost) {
|
||||
onUpdateHost({ ...host, keywordHighlightRules: [newRule], keywordHighlightEnabled: true });
|
||||
}
|
||||
}, [newRuleLabel, newRulePattern, newRuleColor, rules, updateRules, enabled, 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 (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className={buttonClassName}
|
||||
title={t('terminal.toolbar.hostHighlight.title')}
|
||||
aria-label={t('terminal.toolbar.hostHighlight.title')}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<Highlighter size={12} />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-0" align="start" side="top">
|
||||
<div className="px-3 py-2 border-b bg-muted/30 flex items-center justify-between">
|
||||
<span className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
{t('terminal.toolbar.hostHighlight.title')}
|
||||
</span>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{enabled ? t('common.enabled') : t('common.disabled')}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
onClick={toggleEnabled}
|
||||
className={`
|
||||
relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
|
||||
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2
|
||||
${enabled ? 'bg-primary' : 'bg-muted-foreground/30'}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0
|
||||
transition duration-200 ease-in-out
|
||||
${enabled ? 'translate-x-4' : 'translate-x-0'}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="max-h-64">
|
||||
<div className="p-2 space-y-1.5">
|
||||
{rules.length === 0 ? (
|
||||
<div className="px-2 py-4 text-xs text-muted-foreground text-center italic">
|
||||
{t('terminal.toolbar.hostHighlight.noRules')}
|
||||
</div>
|
||||
) : (
|
||||
rules.map((rule) => (
|
||||
<div
|
||||
key={rule.id}
|
||||
className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-accent/50 group"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleToggleRule(rule.id)}
|
||||
className={`
|
||||
flex-shrink-0 w-3 h-3 rounded-sm border transition-colors
|
||||
${rule.enabled
|
||||
? 'bg-primary border-primary'
|
||||
: 'bg-transparent border-muted-foreground/50'
|
||||
}
|
||||
`}
|
||||
title={rule.enabled ? t('common.enabled') : t('common.disabled')}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className="text-xs font-medium truncate"
|
||||
style={{ color: rule.enabled ? rule.color : 'inherit' }}
|
||||
>
|
||||
{rule.label}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground font-mono truncate">
|
||||
{rule.patterns.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
<label className="relative flex-shrink-0">
|
||||
<input
|
||||
type="color"
|
||||
value={rule.color}
|
||||
onChange={(e) => handleColorChange(rule.id, e.target.value)}
|
||||
className="sr-only"
|
||||
aria-label={`${t('terminal.toolbar.hostHighlight.changeColor')} ${rule.label}`}
|
||||
/>
|
||||
<span
|
||||
className="block w-6 h-4 rounded cursor-pointer border border-border/50 hover:border-border"
|
||||
style={{ backgroundColor: rule.color }}
|
||||
/>
|
||||
</label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5 opacity-0 group-hover:opacity-100 transition-opacity text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={() => handleDeleteRule(rule.id)}
|
||||
>
|
||||
<Trash2 size={10} />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Add new rule form */}
|
||||
<div className="p-2 border-t bg-muted/20 space-y-2">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-1">
|
||||
{t('terminal.toolbar.hostHighlight.addRule')}
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<Input
|
||||
placeholder={t('terminal.toolbar.hostHighlight.labelPlaceholder')}
|
||||
value={newRuleLabel}
|
||||
onChange={(e) => setNewRuleLabel(e.target.value)}
|
||||
className="h-7 text-xs flex-1"
|
||||
/>
|
||||
<label className="relative flex-shrink-0">
|
||||
<input
|
||||
type="color"
|
||||
value={newRuleColor}
|
||||
onChange={(e) => setNewRuleColor(e.target.value)}
|
||||
className="sr-only"
|
||||
aria-label={t('terminal.toolbar.hostHighlight.selectColor')}
|
||||
/>
|
||||
<span
|
||||
className="block w-7 h-7 rounded cursor-pointer border border-border/50 hover:border-border"
|
||||
style={{ backgroundColor: newRuleColor }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<Input
|
||||
placeholder={t('terminal.toolbar.hostHighlight.patternPlaceholder')}
|
||||
value={newRulePattern}
|
||||
onChange={(e) => handlePatternChange(e.target.value)}
|
||||
className={`h-7 text-xs font-mono flex-1 ${patternError ? 'border-destructive' : ''}`}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="h-7 w-7 flex-shrink-0"
|
||||
onClick={handleAddRule}
|
||||
disabled={!newRuleLabel.trim() || !newRulePattern.trim()}
|
||||
>
|
||||
<Plus size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
{patternError && (
|
||||
<div className="text-[10px] text-destructive">{patternError}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer actions */}
|
||||
{rules.length > 0 && (
|
||||
<div className="p-2 border-t flex justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 text-xs text-muted-foreground hover:text-destructive"
|
||||
onClick={handleClearAll}
|
||||
>
|
||||
<RotateCcw size={10} className="mr-1" />
|
||||
{t('terminal.toolbar.hostHighlight.clearAll')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default HostKeywordHighlightPopover;
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Terminal Toolbar
|
||||
* Displays SFTP, Scripts, Theme, Search buttons and close button in terminal status bar
|
||||
* Displays SFTP, Scripts, Theme, Highlight, Search buttons and close button in terminal status bar
|
||||
*/
|
||||
import { FolderInput, X, Zap, Palette, Search } from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
@@ -10,6 +10,7 @@ import { Button } from '../ui/button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import ThemeCustomizeModal from './ThemeCustomizeModal';
|
||||
import HostKeywordHighlightPopover from './HostKeywordHighlightPopover';
|
||||
|
||||
export interface TerminalToolbarProps {
|
||||
status: 'connecting' | 'connected' | 'disconnected';
|
||||
@@ -55,6 +56,7 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [themeModalOpen, setThemeModalOpen] = useState(false);
|
||||
const [highlightPopoverOpen, setHighlightPopoverOpen] = useState(false);
|
||||
const buttonBase = "h-6 w-6 p-0 shadow-none border-none text-[color:var(--terminal-toolbar-fg)] bg-transparent hover:bg-transparent";
|
||||
|
||||
const isLocalTerminal = host?.protocol === 'local' || host?.id?.startsWith('local-');
|
||||
@@ -163,6 +165,14 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
|
||||
<Palette size={12} />
|
||||
</Button>
|
||||
|
||||
<HostKeywordHighlightPopover
|
||||
host={host}
|
||||
onUpdateHost={onUpdateHost}
|
||||
isOpen={highlightPopoverOpen}
|
||||
setIsOpen={setHighlightPopoverOpen}
|
||||
buttonClassName={buttonBase}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
|
||||
@@ -12,6 +12,9 @@ export type { TerminalConnectionProgressProps } from './TerminalConnectionProgre
|
||||
export { TerminalToolbar } from './TerminalToolbar';
|
||||
export type { TerminalToolbarProps } from './TerminalToolbar';
|
||||
|
||||
export { HostKeywordHighlightPopover } from './HostKeywordHighlightPopover';
|
||||
export type { HostKeywordHighlightPopoverProps } from './HostKeywordHighlightPopover';
|
||||
|
||||
export { TerminalConnectionDialog } from './TerminalConnectionDialog';
|
||||
export type { ChainProgress,TerminalConnectionDialogProps } from './TerminalConnectionDialog';
|
||||
|
||||
|
||||
@@ -96,6 +96,9 @@ export interface Host {
|
||||
sftpEncoding?: SftpFilenameEncoding; // Filename encoding for SFTP operations
|
||||
// Managed source: if this host is managed by an external file (e.g., ~/.ssh/config)
|
||||
managedSourceId?: string; // Reference to ManagedSource.id
|
||||
// Host-level keyword highlighting (overrides/extends global settings)
|
||||
keywordHighlightRules?: KeywordHighlightRule[];
|
||||
keywordHighlightEnabled?: boolean;
|
||||
}
|
||||
|
||||
export type KeyType = 'RSA' | 'ECDSA' | 'ED25519';
|
||||
|
||||
@@ -311,7 +311,6 @@ async function connectThroughChainForSftp(event, options, jumpHosts, targetHost,
|
||||
// Prioritize fastest ciphers (GCM modes are hardware-accelerated)
|
||||
cipher: [
|
||||
'aes128-gcm@openssh.com', 'aes256-gcm@openssh.com',
|
||||
'chacha20-poly1305@openssh.com',
|
||||
'aes128-ctr', 'aes192-ctr', 'aes256-ctr',
|
||||
],
|
||||
// Prioritize modern key exchange algorithms for broad compatibility
|
||||
|
||||
32
package-lock.json
generated
32
package-lock.json
generated
@@ -1008,7 +1008,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -1654,6 +1653,7 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cross-dirname": "^0.1.0",
|
||||
"debug": "^4.3.4",
|
||||
@@ -1675,6 +1675,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
@@ -1691,6 +1692,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
@@ -1705,6 +1707,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
@@ -5670,7 +5673,6 @@
|
||||
"integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
@@ -5700,7 +5702,6 @@
|
||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
@@ -5979,8 +5980,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@yarnpkg/lockfile": {
|
||||
"version": "1.1.0",
|
||||
@@ -6012,7 +6012,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -6045,7 +6044,6 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -6518,7 +6516,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -7126,7 +7123,8 @@
|
||||
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "10.1.0",
|
||||
@@ -7691,6 +7689,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.1",
|
||||
"debug": "^4.1.1",
|
||||
@@ -7711,6 +7710,7 @@
|
||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
@@ -7928,7 +7928,6 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -10168,6 +10167,7 @@
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
@@ -10180,7 +10180,6 @@
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dompurify": "3.2.7",
|
||||
"marked": "14.0.0"
|
||||
@@ -10827,6 +10826,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"commander": "^9.4.0"
|
||||
},
|
||||
@@ -10844,6 +10844,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
@@ -10956,7 +10957,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -10966,7 +10966,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -11834,6 +11833,7 @@
|
||||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mkdirp": "^0.5.1",
|
||||
"rimraf": "~2.6.2"
|
||||
@@ -11898,6 +11898,7 @@
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
@@ -11966,7 +11967,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -12091,7 +12091,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -12294,7 +12293,6 @@
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -12388,7 +12386,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -12681,7 +12678,6 @@
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user