mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
2f314c3588
* feat(i18n): add translations for group config panel * feat(models): add GroupConfig data model, resolution logic, and encryption Add the GroupConfig interface for group-level default settings that hosts inherit. Includes ancestor-chain resolution (A/B/C merges from A, A/B, A/B/C), host-level application logic, storage key, and secure field encryption/decryption for sensitive GroupConfig fields. Part of #220. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(state): add groupConfigs state management with encryption Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(ui): create GroupDetailsPanel with full config editing Side panel for editing group-level default configuration using AsidePanel. Includes General, SSH, Telnet, Advanced, Mosh, and Appearance sections with sub-panel navigation for Proxy, Chain, EnvVars, and Theme selection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(vault): wire GroupDetailsPanel, replace rename dialog with full config panel Replace all group rename dialog triggers with the new GroupDetailsPanel sidebar. The hover edit button, context menu, and tree view edit callbacks now open the full group configuration panel instead of a simple rename dialog. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(connect): apply group config defaults at connection time When connecting to a host, merge group-level default configuration so hosts inherit their group's settings for auth, protocol, appearance, and other inheritable fields. Connection logs still reference the original host's label/hostname. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(sync): include groupConfigs in sync and export payloads Add groupConfigs to SyncPayload, SyncableVaultData, buildSyncPayload, and applySyncPayload so group connection defaults are preserved during cloud sync and data import/export. Also wire groupConfigs into the vault object in SettingsPage so it flows through to the sync payload builder. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(vault): update group configs on move and delete * feat(host-panel): show inherited group defaults as placeholders When editing a host that belongs to a group with configuration, group default values now appear as placeholder text in username, startup command, and charset fields where the host doesn't have its own value. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: clean up unused imports in GroupDetailsPanel Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(group-panel): add/remove protocol sections, editable parent group - SSH and Telnet sections are now add/remove — click "Add Protocol" to enable, "..." menu to remove. Only enabled protocols override hosts. - Parent Group is now editable via Combobox dropdown for quick group moving. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move SSH-specific fields into SSH protocol section Startup Command, Legacy Algorithms, Proxy, Host Chaining, Environment Variables, and Mosh are all SSH-specific and now only visible when SSH protocol is added. Only Charset remains as a shared field in the Advanced section. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: hide charset and appearance when no protocol is added Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: close Add Protocol dropdown after selection Use controlled open state to explicitly close the dropdown when a protocol is selected, preventing residual content from overlapping the newly rendered section. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: apply group defaults in TerminalLayer sessionHostsMap Terminal component was re-reading the original host from the hosts array by hostId, bypassing the group defaults applied in handleConnectToHost. Now sessionHostsMap applies resolveGroupDefaults + applyGroupDefaults when building the host object for each session, so Terminal sees the merged credentials/settings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move Add Protocol to bottom, fix i18n for protocol/font labels - Add Protocol button moved below Appearance section - Added i18n keys: addProtocol, removeProtocol, fontFamily, fontSize - All hardcoded English strings replaced with t() calls Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: replace font family text input with TerminalFontSelect dropdown Use the same font selector component as settings, showing available terminal fonts with preview. Includes "Use Global" reset button. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(group-panel): match HostDetailsPanel key/certificate selection pattern Replace the simple Combobox key selector with the same credential selection flow used in HostDetailsPanel: a popover with Key/Certificate options, inline combobox per type, and proper badge display with certificate icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(group-panel): add Local Key File option to credential selection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(group-panel): add identityFilePaths to GroupConfig and Local Key File option - Added identityFilePaths to GroupConfig interface and INHERITABLE_KEYS - GroupDetailsPanel now supports Key, Certificate, and Local Key File credential selection, matching HostDetailsPanel's full credential flow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: prevent local key file input from overflowing panel width Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: constrain local key file input width with w-0 flex-1 Native input elements have a large default min-width. Using w-0 with flex-1 forces the input to shrink within the flex container. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add overflow-hidden to SSH Card to contain local key file input Matches HostDetailsPanel's Card which uses overflow-hidden on the credentials section to prevent long file paths from overflowing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add min-w-0 to key file path row for proper text truncation Flex children need min-w-0 for truncate to work correctly, otherwise the text pushes the container wider. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: force key file path text truncation with inline max-width calc Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use fixed 320px max-width on key file path text to force truncation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add overflow-hidden to AsidePanelContent to prevent content overflow The root cause was the inner div of AsidePanelContent only had overflow-x-hidden which was being overridden by ScrollArea's viewport. Changed to full overflow-hidden with w-full box-border. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: override Radix ScrollArea viewport's display:table in AsidePanel Radix ScrollArea Viewport wraps content in a div with display:table and min-width:100%, causing content to expand beyond the panel width. Override this on AsidePanelContent's ScrollArea to use display:block and min-width:0 instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: critical issues — seed new hosts from group defaults, validate group names, fix empty import - HostDetailsPanel: When groupDefaults has values for port/username/charset, new hosts start with undefined/empty so group defaults take effect via applyGroupDefaults() instead of being blocked by hardcoded values - GroupDetailsPanel: Validate group name in handleSubmit to reject '/' and '\' characters, matching the old rename dialog behavior, with visual error - useVaultState: Check groupConfigs !== undefined instead of truthy so that importing an empty array [] properly clears all group configs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: safe prefix replacement, remove dead code, extract shared resolveEffectiveHost - Replace all .replace(oldPath, newPath) / .replace(sourcePath, newPath) with explicit prefix slicing (newPath + str.slice(oldPath.length)) in handleSaveGroupConfig and moveGroup for more robust path renaming - Remove dead c.path === oldPath branch in finalConfigs mapping since updatedConfigs already contains the config with newPath - Extract resolveEffectiveHost helper in App.tsx to deduplicate group defaults resolution in _handleTrayPanelConnect and handleConnectToHost Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: preserve undefined port on save when group has port default form.port || 22 was forcing port to 22 even when intentionally left undefined for group inheritance. Now uses nullish coalescing and only defaults to 22 when no group port default exists. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: SSH-adjacent field detection, chain host defaults, telnet inheritance, theme clear - hasSshFields() now checks proxyConfig, hostChain, startupCommand, legacyAlgorithms, environmentVariables, moshEnabled, moshServerPath, and identityFilePaths so the SSH section auto-opens when editing - Chain hosts in sessionChainHostsMap now get group defaults applied via resolveGroupDefaults + applyGroupDefaults - Added telnetEnabled to GroupConfig interface and INHERITABLE_KEYS; save handler sets telnetEnabled: true when Telnet section is on - Theme/font "Use global" clear now sets override to false instead of undefined, preventing parent group theme from leaking through Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: review round 4 — sync, SFTP, port forwarding, type safety, UX - Scan groupConfigs in encrypted credential guard (P1 security) - Add groupConfigs to auto-sync payload and three-way merge (P1 sync) - Apply group defaults in SFTP connections (P1 SFTP) - Apply group defaults in all port forwarding paths (P1 port forwarding) - Make Host.port optional to fix unsafe type cast (P1 type safety) - Fix port input empty → 0 instead of undefined (P2) - Add port placeholder showing inherited value (P2) - Mutual exclusion of group/host detail panels (P2) - Fix sub-panel width jump 420px → 380px (P2) - Validate duplicate group path on rename/reparent (P2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: review round 5 — null guard, empty array inheritance, memo comparator, form reset - Guard groupConfigs import against null payload (P1 crash) - Validate duplicate path on moveGroup drag-drop (P2 data corruption) - Clear empty environmentVariables to undefined for group inheritance (P1) - Clear empty hostChain to undefined for group inheritance (P2) - Add groupConfigs to SftpView memo comparator (P1 stale defaults) - Add key={editingGroupPath} to GroupDetailsPanel for form reset (P1) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: review round 6 — copy credentials, protocol dialog use effective host - Apply group defaults in handleCopyCredentials (P2) - Apply group defaults in hasMultipleProtocols check (P2) - Pass effective host to ProtocolSelectDialog (P2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: serialize protocol:'ssh' marker to persist SSH section in group config - Add protocol:'ssh' as marker field in handleSubmit SSH block - Detect protocol:'ssh' in hasSshFields() to preserve section on reopen - Clean up protocol field in removeSsh() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
854 lines
33 KiB
TypeScript
Executable File
854 lines
33 KiB
TypeScript
Executable File
// Proxy configuration for SSH connections
|
|
type ProxyType = 'http' | 'socks5';
|
|
// UI locale identifier, stored in settings and used for i18n (e.g., "en", "zh-CN").
|
|
export type UILanguage = string;
|
|
|
|
export interface ProxyConfig {
|
|
type: ProxyType;
|
|
host: string;
|
|
port: number;
|
|
username?: string;
|
|
password?: string;
|
|
}
|
|
|
|
// Host chain configuration for jump host / bastion connections
|
|
export interface HostChainConfig {
|
|
hostIds: string[]; // Array of host IDs in order (first = closest to client)
|
|
}
|
|
|
|
// Environment variable for SSH session
|
|
export interface EnvVar {
|
|
name: string;
|
|
value: string;
|
|
}
|
|
|
|
// Protocol type for connections
|
|
export type HostProtocol = 'ssh' | 'telnet' | 'mosh' | 'local' | 'serial';
|
|
|
|
// Serial port configuration
|
|
export type SerialParity = 'none' | 'even' | 'odd' | 'mark' | 'space';
|
|
export type SerialFlowControl = 'none' | 'xon/xoff' | 'rts/cts';
|
|
|
|
export interface SerialConfig {
|
|
path: string; // Serial port path (e.g., /dev/ttyUSB0, COM1)
|
|
baudRate: number; // Baud rate (e.g., 9600, 115200)
|
|
dataBits?: 5 | 6 | 7 | 8; // Data bits (default: 8)
|
|
stopBits?: 1 | 1.5 | 2; // Stop bits (default: 1)
|
|
parity?: SerialParity; // Parity (default: 'none')
|
|
flowControl?: SerialFlowControl; // Flow control (default: 'none')
|
|
localEcho?: boolean; // Force local echo (default: false, rely on remote echo)
|
|
lineMode?: boolean; // Line mode - buffer input and send on Enter (default: false)
|
|
}
|
|
|
|
// Per-protocol configuration
|
|
interface ProtocolConfig {
|
|
protocol: HostProtocol;
|
|
port: number;
|
|
enabled: boolean;
|
|
// Mosh-specific
|
|
moshServerPath?: string;
|
|
// Protocol-specific theme override
|
|
theme?: string;
|
|
}
|
|
|
|
export interface SftpBookmark {
|
|
id: string;
|
|
path: string;
|
|
label: string;
|
|
global?: boolean;
|
|
}
|
|
|
|
export interface Host {
|
|
id: string;
|
|
label: string;
|
|
hostname: string;
|
|
port?: number;
|
|
username: string;
|
|
// Optional reference to a reusable identity (username + auth) stored in Keychain.
|
|
identityId?: string;
|
|
group?: string;
|
|
tags: string[];
|
|
os: 'linux' | 'windows' | 'macos';
|
|
// Device type: 'general' for standard servers, 'network' for switches/routers/firewalls.
|
|
// Network devices use raw command execution (no shell wrapping) for AI agent compatibility.
|
|
deviceType?: 'general' | 'network';
|
|
identityFileId?: string; // Reference to SSHKey
|
|
protocol?: 'ssh' | 'telnet' | 'local' | 'serial'; // Default/primary protocol
|
|
password?: string;
|
|
savePassword?: boolean; // Whether to save the password (default: true)
|
|
authMethod?: 'password' | 'key' | 'certificate';
|
|
agentForwarding?: boolean;
|
|
createdAt?: number; // Timestamp when host was created
|
|
startupCommand?: string;
|
|
hostChaining?: string; // Deprecated: use hostChain instead
|
|
proxy?: string; // Deprecated: use proxyConfig instead
|
|
proxyConfig?: ProxyConfig; // New structured proxy configuration
|
|
hostChain?: HostChainConfig; // New structured host chain configuration
|
|
envVars?: string; // Deprecated: use environmentVariables instead
|
|
environmentVariables?: EnvVar[]; // Structured environment variables
|
|
charset?: string;
|
|
moshEnabled?: boolean;
|
|
moshServerPath?: string; // Custom mosh-server path (e.g., /usr/local/bin/mosh-server)
|
|
theme?: string;
|
|
themeOverride?: boolean; // Explicitly override the global terminal theme for this host
|
|
fontFamily?: string; // Terminal font family for this host
|
|
fontFamilyOverride?: boolean; // Explicitly override the global terminal font family for this host
|
|
fontSize?: number; // Terminal font size for this host (pt)
|
|
fontSizeOverride?: boolean; // Explicitly override the global terminal font size for this host
|
|
distro?: string; // detected distro id (e.g., ubuntu, debian)
|
|
distroMode?: 'auto' | 'manual'; // whether distro icon comes from detection or manual override
|
|
manualDistro?: string; // manually selected distro id when distroMode='manual'
|
|
// Multi-protocol support
|
|
protocols?: ProtocolConfig[]; // Multiple protocol configurations
|
|
telnetPort?: number; // Telnet-specific port (for quick access)
|
|
telnetEnabled?: boolean; // Is Telnet enabled for this host
|
|
telnetUsername?: string; // Telnet-specific username
|
|
telnetPassword?: string; // Telnet-specific password
|
|
// Serial-specific configuration (for protocol='serial' hosts)
|
|
serialConfig?: SerialConfig;
|
|
// SFTP specific configuration
|
|
sftpSudo?: boolean; // Use sudo for SFTP operations (requires password)
|
|
sftpEncoding?: SftpFilenameEncoding; // Filename encoding for SFTP operations
|
|
sftpBookmarks?: SftpBookmark[]; // Bookmarked SFTP paths for quick navigation
|
|
// 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;
|
|
// Legacy SSH algorithm support for older network equipment (switches, routers)
|
|
legacyAlgorithms?: boolean;
|
|
// Local SSH key file paths (from SSH config IdentityFile or user-added)
|
|
// Resolved at connection time — the app reads the file content when connecting.
|
|
identityFilePaths?: string[];
|
|
// Pin host to top of All hosts view for quick access
|
|
pinned?: boolean;
|
|
// Timestamp of last successful connection, used for Recently Connected section
|
|
lastConnectedAt?: number;
|
|
}
|
|
|
|
export type KeyType = 'RSA' | 'ECDSA' | 'ED25519';
|
|
type KeySource = 'generated' | 'imported';
|
|
export type KeyCategory = 'key' | 'certificate' | 'identity';
|
|
type IdentityAuthMethod = 'password' | 'key' | 'certificate';
|
|
|
|
export interface SSHKey {
|
|
id: string;
|
|
label: string;
|
|
type: KeyType;
|
|
keySize?: number; // RSA: 4096/2048/1024, ECDSA: 521/384/256
|
|
privateKey: string;
|
|
publicKey?: string;
|
|
certificate?: string;
|
|
passphrase?: string; // encrypted or stored securely
|
|
savePassphrase?: boolean;
|
|
source: KeySource;
|
|
category: KeyCategory;
|
|
created: number;
|
|
}
|
|
|
|
// Identity combines username with authentication method
|
|
export interface Identity {
|
|
id: string;
|
|
label: string;
|
|
username: string;
|
|
authMethod: IdentityAuthMethod;
|
|
password?: string; // For password auth
|
|
keyId?: string; // Reference to SSHKey for key/certificate auth
|
|
created: number;
|
|
}
|
|
|
|
export interface Snippet {
|
|
id: string;
|
|
label: string;
|
|
command: string; // Multi-line script
|
|
tags?: string[];
|
|
package?: string; // package path
|
|
targets?: string[]; // host ids
|
|
shortkey?: string; // Keyboard shortcut to send this snippet in terminal (e.g., "F1", "Ctrl + F1")
|
|
noAutoRun?: boolean; // If true, paste command without executing (no trailing Enter)
|
|
}
|
|
|
|
export interface ChatMessage {
|
|
role: 'user' | 'model';
|
|
text: string;
|
|
}
|
|
|
|
export interface GroupNode {
|
|
name: string;
|
|
path: string;
|
|
children: Record<string, GroupNode>;
|
|
hosts: Host[];
|
|
/** Pre-computed total host count including all descendants. Set during tree construction. */
|
|
totalHostCount?: number;
|
|
}
|
|
|
|
/** Default configuration for a group. Hosts in this group inherit these values when not explicitly set. */
|
|
export interface GroupConfig {
|
|
path: string;
|
|
username?: string;
|
|
password?: string;
|
|
savePassword?: boolean;
|
|
authMethod?: 'password' | 'key' | 'certificate';
|
|
identityId?: string;
|
|
identityFileId?: string;
|
|
identityFilePaths?: string[];
|
|
port?: number;
|
|
protocol?: 'ssh' | 'telnet';
|
|
agentForwarding?: boolean;
|
|
proxyConfig?: ProxyConfig;
|
|
hostChain?: HostChainConfig;
|
|
startupCommand?: string;
|
|
legacyAlgorithms?: boolean;
|
|
environmentVariables?: EnvVar[];
|
|
charset?: string;
|
|
moshEnabled?: boolean;
|
|
moshServerPath?: string;
|
|
telnetEnabled?: boolean;
|
|
telnetPort?: number;
|
|
telnetUsername?: string;
|
|
telnetPassword?: string;
|
|
theme?: string;
|
|
themeOverride?: boolean;
|
|
fontFamily?: string;
|
|
fontFamilyOverride?: boolean;
|
|
fontSize?: number;
|
|
fontSizeOverride?: boolean;
|
|
}
|
|
|
|
export interface SyncConfig {
|
|
gistId: string;
|
|
githubToken: string;
|
|
gistToken?: string; // Alias for githubToken (deprecated, use githubToken)
|
|
lastSync?: number;
|
|
}
|
|
|
|
// Keyboard Shortcuts / Hotkeys
|
|
export type HotkeyScheme = 'disabled' | 'mac' | 'pc';
|
|
|
|
export interface KeyBinding {
|
|
id: string;
|
|
action: string;
|
|
label: string;
|
|
mac: string; // e.g., '⌘+1', '⌘+⌥+arrows'
|
|
pc: string; // e.g., 'Ctrl+1', 'Ctrl+Alt+arrows'
|
|
category: 'tabs' | 'terminal' | 'navigation' | 'app' | 'sftp';
|
|
}
|
|
|
|
// User's custom key bindings - only stores overrides from defaults
|
|
export type CustomKeyBindings = Record<string, { mac?: string; pc?: string }>;
|
|
|
|
// Parse a key string like "⌘ + Shift + K" or "Ctrl + Alt + T" into normalized form
|
|
export const parseKeyCombo = (keyStr: string): { modifiers: string[]; key: string } | null => {
|
|
if (!keyStr || keyStr === 'Disabled') return null;
|
|
const parts = keyStr.split('+').map(p => p.trim());
|
|
const key = parts.pop() || '';
|
|
return { modifiers: parts, key };
|
|
};
|
|
|
|
// Convert keyboard event to a key string
|
|
export const keyEventToString = (e: KeyboardEvent, isMac: boolean): string => {
|
|
const parts: string[] = [];
|
|
|
|
if (isMac) {
|
|
if (e.metaKey) parts.push('⌘');
|
|
if (e.ctrlKey) parts.push('⌃');
|
|
if (e.altKey) parts.push('⌥');
|
|
if (e.shiftKey) parts.push('Shift');
|
|
} else {
|
|
if (e.ctrlKey) parts.push('Ctrl');
|
|
if (e.altKey) parts.push('Alt');
|
|
if (e.shiftKey) parts.push('Shift');
|
|
if (e.metaKey) parts.push('Win');
|
|
}
|
|
|
|
// Get the key name
|
|
let keyName = e.key;
|
|
// Normalize special keys
|
|
if (keyName === ' ') keyName = 'Space';
|
|
else if (keyName === 'ArrowUp') keyName = '↑';
|
|
else if (keyName === 'ArrowDown') keyName = '↓';
|
|
else if (keyName === 'ArrowLeft') keyName = '←';
|
|
else if (keyName === 'ArrowRight') keyName = '→';
|
|
else if (keyName === 'Escape') keyName = 'Esc';
|
|
else if (keyName === 'Backspace') keyName = '⌫';
|
|
else if (keyName === 'Delete') keyName = 'Del';
|
|
else if (keyName === 'Enter') keyName = '↵';
|
|
else if (keyName === 'Tab') keyName = '⇥';
|
|
else if (keyName.length === 1) keyName = keyName.toUpperCase();
|
|
|
|
// Don't include modifier keys themselves
|
|
if (['Meta', 'Control', 'Alt', 'Shift'].includes(e.key)) {
|
|
return parts.join(' + ');
|
|
}
|
|
|
|
parts.push(keyName);
|
|
return parts.join(' + ');
|
|
};
|
|
|
|
// Check if a keyboard event matches a key binding string
|
|
export const matchesKeyBinding = (e: KeyboardEvent, keyStr: string, isMac: boolean): boolean => {
|
|
if (!keyStr || keyStr === 'Disabled') return false;
|
|
|
|
// Handle range patterns like "[1...9]"
|
|
if (keyStr.includes('[1...9]')) {
|
|
const basePattern = keyStr.replace('[1...9]', '');
|
|
const key = e.key;
|
|
if (!/^[1-9]$/.test(key)) return false;
|
|
// Check modifiers match the base pattern
|
|
const testStr = basePattern + key;
|
|
return matchesKeyBinding(e, testStr.trim(), isMac);
|
|
}
|
|
|
|
// Handle arrow key patterns like "arrows"
|
|
if (keyStr.includes('arrows')) {
|
|
const basePattern = keyStr.replace('arrows', '');
|
|
const key = e.key;
|
|
// Check if it's an arrow key
|
|
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)) return false;
|
|
// Map arrow key to symbol for matching
|
|
const arrowSymbol = key === 'ArrowUp' ? '↑'
|
|
: key === 'ArrowDown' ? '↓'
|
|
: key === 'ArrowLeft' ? '←'
|
|
: '→';
|
|
// Check modifiers match the base pattern
|
|
const testStr = basePattern + arrowSymbol;
|
|
return matchesKeyBinding(e, testStr.trim(), isMac);
|
|
}
|
|
|
|
const parsed = parseKeyCombo(keyStr);
|
|
if (!parsed) return false;
|
|
|
|
const { modifiers, key } = parsed;
|
|
|
|
const hasMacModifiers = modifiers.some((modifier) => ['⌘', '⌃', '⌥'].includes(modifier));
|
|
const hasPcModifiers = modifiers.some((modifier) => ['Ctrl', 'Alt', 'Win'].includes(modifier));
|
|
if ((!isMac && hasMacModifiers) || (isMac && hasPcModifiers)) {
|
|
return false;
|
|
}
|
|
|
|
// Check modifiers
|
|
if (isMac) {
|
|
const needMeta = modifiers.includes('⌘');
|
|
const needCtrl = modifiers.includes('⌃');
|
|
const needAlt = modifiers.includes('⌥');
|
|
const needShift = modifiers.includes('Shift');
|
|
|
|
if (e.metaKey !== needMeta) return false;
|
|
if (e.ctrlKey !== needCtrl) return false;
|
|
if (e.altKey !== needAlt) return false;
|
|
if (e.shiftKey !== needShift) return false;
|
|
} else {
|
|
const needCtrl = modifiers.includes('Ctrl');
|
|
const needAlt = modifiers.includes('Alt');
|
|
const needShift = modifiers.includes('Shift');
|
|
const needMeta = modifiers.includes('Win');
|
|
|
|
if (e.ctrlKey !== needCtrl) return false;
|
|
if (e.altKey !== needAlt) return false;
|
|
if (e.shiftKey !== needShift) return false;
|
|
if (e.metaKey !== needMeta) return false;
|
|
}
|
|
|
|
const normalizeKey = (rawKey: string): string => {
|
|
let normalizedKey = rawKey;
|
|
if (normalizedKey === ' ') normalizedKey = 'Space';
|
|
else if (normalizedKey === 'ArrowUp') normalizedKey = '↑';
|
|
else if (normalizedKey === 'ArrowDown') normalizedKey = '↓';
|
|
else if (normalizedKey === 'ArrowLeft') normalizedKey = '←';
|
|
else if (normalizedKey === 'ArrowRight') normalizedKey = '→';
|
|
else if (normalizedKey === 'Escape') normalizedKey = 'Esc';
|
|
else if (normalizedKey === 'Backspace') normalizedKey = '⌫';
|
|
else if (normalizedKey === 'Delete') normalizedKey = 'Del';
|
|
else if (normalizedKey === '[') normalizedKey = '[';
|
|
else if (normalizedKey === ']') normalizedKey = ']';
|
|
else if (normalizedKey === 'Del') normalizedKey = 'Del';
|
|
return normalizedKey;
|
|
};
|
|
|
|
const eventKey = normalizeKey(e.key);
|
|
const parsedKey = normalizeKey(key);
|
|
|
|
return eventKey.toLowerCase() === parsedKey.toLowerCase();
|
|
};
|
|
|
|
export const DEFAULT_KEY_BINDINGS: KeyBinding[] = [
|
|
// Tab Management
|
|
{ id: 'switch-tab-1-9', action: 'switchToTab', label: 'Switch to Tab [1...9]', mac: '⌘ + [1...9]', pc: 'Ctrl + [1...9]', category: 'tabs' },
|
|
{ id: 'next-tab', action: 'nextTab', label: 'Next Tab', mac: '⌘ + Shift + ]', pc: 'Ctrl + Tab', category: 'tabs' },
|
|
{ id: 'prev-tab', action: 'prevTab', label: 'Previous Tab', mac: '⌘ + Shift + [', pc: 'Ctrl + Shift + Tab', category: 'tabs' },
|
|
{ id: 'close-tab', action: 'closeTab', label: 'Close Tab', mac: '⌘ + W', pc: 'Ctrl + W', category: 'tabs' },
|
|
{ id: 'new-tab', action: 'newTab', label: 'New Local Tab', mac: '⌘ + T', pc: 'Ctrl + T', category: 'tabs' },
|
|
|
|
// Terminal Operations
|
|
{ id: 'copy', action: 'copy', label: 'Copy from Terminal', mac: '⌘ + C', pc: 'Ctrl + Shift + C', category: 'terminal' },
|
|
{ id: 'paste', action: 'paste', label: 'Paste to Terminal', mac: '⌘ + V', pc: 'Ctrl + Shift + V', category: 'terminal' },
|
|
{ id: 'select-all', action: 'selectAll', label: 'Select All in Terminal', mac: '⌘ + A', pc: 'Ctrl + Shift + A', category: 'terminal' },
|
|
{ id: 'clear-buffer', action: 'clearBuffer', label: 'Clear Terminal Buffer', mac: '⌘ + ⌃ + K', pc: 'Ctrl + Shift + K', category: 'terminal' },
|
|
{ id: 'search-terminal', action: 'searchTerminal', label: 'Open Terminal Search', mac: '⌘ + F', pc: 'Ctrl + Shift + F', category: 'terminal' },
|
|
|
|
// Navigation / Split View
|
|
{ id: 'move-focus', action: 'moveFocus', label: 'Move focus between Split View panes', mac: '⌘ + ⌥ + arrows', pc: 'Ctrl + Alt + arrows', category: 'navigation' },
|
|
{ id: 'split-horizontal', action: 'splitHorizontal', label: 'Split Horizontal', mac: '⌘ + D', pc: 'Ctrl + Shift + D', category: 'navigation' },
|
|
{ id: 'split-vertical', action: 'splitVertical', label: 'Split Vertical', mac: '⌘ + Shift + D', pc: 'Ctrl + Shift + E', category: 'navigation' },
|
|
|
|
// App Features
|
|
{ id: 'open-hosts', action: 'openHosts', label: 'Open Hosts Page', mac: 'Disabled', pc: 'Disabled', category: 'app' },
|
|
{ id: 'open-local', action: 'openLocal', label: 'Open Local Terminal', mac: '⌘ + L', pc: 'Ctrl + L', category: 'app' },
|
|
{ id: 'open-sftp', action: 'openSftp', label: 'Open SFTP', mac: '⌘ + Shift + O', pc: 'Ctrl + Shift + O', category: 'app' },
|
|
{ id: 'port-forwarding', action: 'portForwarding', label: 'Open Port Forwarding', mac: '⌘ + P', pc: 'Ctrl + P', category: 'app' },
|
|
{ id: 'command-palette', action: 'commandPalette', label: 'Open Command Palette', mac: '⌘ + K', pc: 'Ctrl + K', category: 'app' },
|
|
{ id: 'quick-switch', action: 'quickSwitch', label: 'Quick Switch', mac: '⌘ + J', pc: 'Ctrl + J', category: 'app' },
|
|
{ id: 'snippets', action: 'snippets', label: 'Open Snippets', mac: '⌘ + Shift + S', pc: 'Ctrl + Shift + S', category: 'app' },
|
|
{ id: 'broadcast', action: 'broadcast', label: 'Switch the Broadcast Mode', mac: '⌘ + B', pc: 'Ctrl + B', category: 'app' },
|
|
|
|
// SFTP Operations
|
|
{ id: 'sftp-copy', action: 'sftpCopy', label: 'Copy Files', mac: '⌘ + C', pc: 'Ctrl + C', category: 'sftp' },
|
|
{ id: 'sftp-cut', action: 'sftpCut', label: 'Cut Files', mac: '⌘ + X', pc: 'Ctrl + X', category: 'sftp' },
|
|
{ id: 'sftp-paste', action: 'sftpPaste', label: 'Paste Files', mac: '⌘ + V', pc: 'Ctrl + V', category: 'sftp' },
|
|
{ id: 'sftp-select-all', action: 'sftpSelectAll', label: 'Select All Files', mac: '⌘ + A', pc: 'Ctrl + A', category: 'sftp' },
|
|
{ id: 'sftp-rename', action: 'sftpRename', label: 'Rename File', mac: 'F2', pc: 'F2', category: 'sftp' },
|
|
{ id: 'sftp-delete', action: 'sftpDelete', label: 'Delete Files', mac: '⌘ + ⌫', pc: 'Delete', category: 'sftp' },
|
|
{ id: 'sftp-refresh', action: 'sftpRefresh', label: 'Refresh', mac: '⌘ + R', pc: 'F5', category: 'sftp' },
|
|
{ id: 'sftp-new-folder', action: 'sftpNewFolder', label: 'New Folder', mac: '⌘ + Shift + N', pc: 'Ctrl + Shift + N', category: 'sftp' },
|
|
{ id: 'sftp-open', action: 'sftpOpen', label: 'Open File / Enter Directory', mac: 'Enter', pc: 'Enter', category: 'sftp' },
|
|
{ id: 'sftp-go-parent', action: 'sftpGoParent', label: 'Go to Parent Directory', mac: '⌫', pc: 'Backspace', category: 'sftp' },
|
|
{ id: 'sftp-navigate-to', action: 'sftpNavigateTo', label: 'Navigate to Selected Directory', mac: '⌘ + Enter', pc: 'Ctrl + Enter', category: 'sftp' },
|
|
];
|
|
|
|
// Terminal appearance settings
|
|
export type CursorShape = 'block' | 'bar' | 'underline';
|
|
export type RightClickBehavior = 'context-menu' | 'paste' | 'select-word';
|
|
export type LinkModifier = 'none' | 'ctrl' | 'alt' | 'meta';
|
|
export type TerminalEmulationType = 'xterm-256color' | 'xterm-16color' | 'xterm';
|
|
|
|
// Keyword highlighting configuration
|
|
export interface KeywordHighlightRule {
|
|
id: string;
|
|
label: string; // Display name (e.g., "Error", "Warning", "OK")
|
|
patterns: string[]; // Regex patterns to match
|
|
color: string; // Highlight color (hex)
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface TerminalSettings {
|
|
// Rendering
|
|
scrollback: number; // Number of lines kept in buffer
|
|
drawBoldInBrightColors: boolean; // Draw bold text in bright colors
|
|
terminalEmulationType: TerminalEmulationType; // Terminal emulation type (TERM env var)
|
|
|
|
// Font
|
|
fontLigatures: boolean; // Enable font ligatures
|
|
fontWeight: number; // Normal font weight (100-900)
|
|
fontWeightBold: number; // Bold font weight (100-900)
|
|
linePadding: number; // Additional space between lines
|
|
fallbackFont: string; // Fallback font family
|
|
|
|
// Cursor
|
|
cursorShape: CursorShape;
|
|
cursorBlink: boolean;
|
|
|
|
// Accessibility
|
|
minimumContrastRatio: number; // Minimum contrast ratio (1-21)
|
|
|
|
// Keyboard
|
|
altAsMeta: boolean; // Use ⌥ as the Meta key
|
|
scrollOnInput: boolean; // Scroll terminal to bottom on input
|
|
scrollOnOutput: boolean; // Scroll terminal to bottom on output
|
|
scrollOnKeyPress: boolean; // Scroll terminal to bottom on key press
|
|
scrollOnPaste: boolean; // Scroll terminal to bottom on paste
|
|
|
|
smoothScrolling: boolean; // Animate viewport scrolling instead of jumping instantly
|
|
|
|
// Mouse
|
|
rightClickBehavior: RightClickBehavior;
|
|
copyOnSelect: boolean; // Automatically copy selected text
|
|
middleClickPaste: boolean; // Paste on middle-click
|
|
wordSeparators: string; // Characters for word selection
|
|
linkModifier: LinkModifier; // Modifier key to click links
|
|
|
|
// Keyword Highlighting
|
|
keywordHighlightEnabled: boolean;
|
|
keywordHighlightRules: KeywordHighlightRule[];
|
|
|
|
// Local Shell Configuration
|
|
localShell: string; // Path to shell executable (empty = system default)
|
|
localStartDir: string; // Starting directory for local terminal (empty = home directory)
|
|
|
|
// SSH Connection
|
|
keepaliveInterval: number; // Seconds between SSH-level keepalive packets (0 = disabled)
|
|
|
|
// Server Stats Display (Linux only)
|
|
showServerStats: boolean; // Show CPU/Memory/Disk in terminal statusbar
|
|
serverStatsRefreshInterval: number; // Seconds between stats refresh (default: 30)
|
|
|
|
// Paste
|
|
disableBracketedPaste: boolean; // Disable bracketed paste mode (avoid ^[[200~ artifacts)
|
|
|
|
// Clipboard
|
|
osc52Clipboard: 'off' | 'write-only' | 'read-write' | 'prompt'; // OSC-52 clipboard access: off, write-only (default), read-write, or prompt on read
|
|
|
|
// Rendering
|
|
rendererType: 'auto' | 'webgl' | 'canvas'; // Terminal renderer: auto (detect based on hardware), webgl, or canvas
|
|
|
|
// Autocomplete
|
|
autocompleteEnabled: boolean; // Enable terminal command autocomplete
|
|
autocompleteGhostText: boolean; // Show inline ghost text suggestions (like fish shell)
|
|
autocompletePopupMenu: boolean; // Show popup menu with multiple suggestions
|
|
autocompleteDebounceMs: number; // Debounce delay for fetching suggestions (ms)
|
|
autocompleteMinChars: number; // Minimum characters before showing suggestions
|
|
autocompleteMaxSuggestions: number; // Maximum suggestions in popup menu
|
|
}
|
|
|
|
const STRICT_IPV4_OCTET_PATTERN = '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)';
|
|
|
|
const URL_HIGHLIGHT_PATTERN =
|
|
"(?:\\bhttps?:\\/\\/\\[[0-9A-Fa-f:.]+\\](?::\\d+)?(?:[/?#][^\\s<>\"'`]*)?(?<![.,;:!?\\)}])|\\b(?:https?:\\/\\/|www\\.)[^\\s<>\"'`]+(?<![.,;:!?\\])}]))";
|
|
const IPV4_HIGHLIGHT_PATTERN =
|
|
`(?<![\\w.])(?<!\\bver\\s)(?<!\\bversion\\s)(?:${STRICT_IPV4_OCTET_PATTERN}\\.){3}${STRICT_IPV4_OCTET_PATTERN}(?![\\w.])`;
|
|
const MAC_ADDRESS_HIGHLIGHT_PATTERN =
|
|
'\\b([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\\b';
|
|
|
|
export const DEFAULT_KEYWORD_HIGHLIGHT_RULES: KeywordHighlightRule[] = [
|
|
{ id: 'error', label: 'Error', patterns: ['\\[error\\]', '\\[err\\]', '\\berror\\b', '\\bfail(ed)?\\b', '\\bfatal\\b', '\\bcritical\\b', '\\bexception\\b'], color: '#F87171', enabled: true },
|
|
{ id: 'warning', label: 'Warning', patterns: ['\\[warn(ing)?\\]', '\\bwarn(ing)?\\b', '\\bcaution\\b', '\\bdeprecated\\b'], color: '#FBBF24', enabled: true },
|
|
{ id: 'ok', label: 'OK', patterns: ['\\[ok\\]', '\\bok\\b', '\\bsuccess(ful)?\\b', '\\bpassed\\b', '\\bcompleted\\b', '\\bdone\\b'], color: '#34D399', enabled: true },
|
|
{ id: 'info', label: 'Info', patterns: ['\\[info\\]', '\\[notice\\]', '\\[note\\]', '\\bnotice\\b', '\\bnote\\b'], color: '#3B82F6', enabled: true },
|
|
{ id: 'debug', label: 'Debug', patterns: ['\\[debug\\]', '\\[trace\\]', '\\[verbose\\]', '\\bdebug\\b', '\\btrace\\b', '\\bverbose\\b'], color: '#A78BFA', enabled: true },
|
|
{ id: 'ip-mac', label: 'URL, IP & MAC', patterns: [URL_HIGHLIGHT_PATTERN, IPV4_HIGHLIGHT_PATTERN, MAC_ADDRESS_HIGHLIGHT_PATTERN], color: '#EC4899', enabled: true },
|
|
];
|
|
|
|
const cloneKeywordHighlightRule = (rule: KeywordHighlightRule): KeywordHighlightRule => ({
|
|
...rule,
|
|
patterns: [...rule.patterns],
|
|
});
|
|
|
|
const normalizeKeywordHighlightRules = (
|
|
rules?: KeywordHighlightRule[],
|
|
): KeywordHighlightRule[] => {
|
|
if (!rules || rules.length === 0) {
|
|
return DEFAULT_KEYWORD_HIGHLIGHT_RULES.map(cloneKeywordHighlightRule);
|
|
}
|
|
|
|
const defaultRulesById = new Map(
|
|
DEFAULT_KEYWORD_HIGHLIGHT_RULES.map((rule) => [rule.id, rule] as const),
|
|
);
|
|
|
|
const normalizedRules = rules.map((rule) => {
|
|
const defaultRule = defaultRulesById.get(rule.id);
|
|
if (!defaultRule) {
|
|
return cloneKeywordHighlightRule(rule);
|
|
}
|
|
|
|
return {
|
|
...defaultRule,
|
|
color: rule.color,
|
|
enabled: rule.enabled,
|
|
};
|
|
});
|
|
|
|
const existingRuleIds = new Set(normalizedRules.map((rule) => rule.id));
|
|
for (const defaultRule of DEFAULT_KEYWORD_HIGHLIGHT_RULES) {
|
|
if (!existingRuleIds.has(defaultRule.id)) {
|
|
normalizedRules.push(cloneKeywordHighlightRule(defaultRule));
|
|
}
|
|
}
|
|
|
|
return normalizedRules;
|
|
};
|
|
|
|
export const normalizeTerminalSettings = (
|
|
settings?: Partial<TerminalSettings> | null,
|
|
): TerminalSettings => {
|
|
const mergedSettings = {
|
|
...DEFAULT_TERMINAL_SETTINGS,
|
|
...(settings ?? {}),
|
|
};
|
|
|
|
return {
|
|
...mergedSettings,
|
|
autocompleteGhostText: mergedSettings.autocompletePopupMenu
|
|
? false
|
|
: mergedSettings.autocompleteGhostText,
|
|
keywordHighlightRules: normalizeKeywordHighlightRules(
|
|
mergedSettings.keywordHighlightRules,
|
|
),
|
|
};
|
|
};
|
|
|
|
const DEFAULT_TERMINAL_SETTINGS: TerminalSettings = {
|
|
scrollback: 10000,
|
|
drawBoldInBrightColors: true,
|
|
terminalEmulationType: 'xterm-256color',
|
|
fontLigatures: true,
|
|
fontWeight: 400,
|
|
fontWeightBold: 700,
|
|
linePadding: 0,
|
|
fallbackFont: '',
|
|
cursorShape: 'block',
|
|
cursorBlink: true,
|
|
minimumContrastRatio: 1,
|
|
altAsMeta: false,
|
|
scrollOnInput: true,
|
|
scrollOnOutput: false,
|
|
scrollOnKeyPress: false,
|
|
scrollOnPaste: true,
|
|
smoothScrolling: false,
|
|
rightClickBehavior: 'context-menu',
|
|
copyOnSelect: false,
|
|
middleClickPaste: true,
|
|
wordSeparators: ' ()[]{}\'"',
|
|
linkModifier: 'none',
|
|
keywordHighlightEnabled: true,
|
|
keywordHighlightRules: DEFAULT_KEYWORD_HIGHLIGHT_RULES,
|
|
localShell: '', // Empty = use system default
|
|
localStartDir: '', // Empty = use home directory
|
|
keepaliveInterval: 0, // 0 = disabled (use SSH library defaults)
|
|
showServerStats: true, // Show server stats by default
|
|
serverStatsRefreshInterval: 5, // Refresh every 5 seconds
|
|
disableBracketedPaste: false, // Bracketed paste enabled by default
|
|
osc52Clipboard: 'write-only', // OSC-52: allow remote programs to write clipboard by default
|
|
rendererType: 'auto', // Auto-detect best renderer based on hardware
|
|
autocompleteEnabled: true, // Autocomplete enabled by default
|
|
autocompleteGhostText: false, // Mutually exclusive with popup menu
|
|
autocompletePopupMenu: true, // Popup menu enabled by default
|
|
autocompleteDebounceMs: 100, // 100ms debounce
|
|
autocompleteMinChars: 1, // Start suggesting after 1 character
|
|
autocompleteMaxSuggestions: 8, // Show up to 8 suggestions
|
|
};
|
|
|
|
export interface TerminalTheme {
|
|
id: string;
|
|
name: string;
|
|
type: 'dark' | 'light';
|
|
isCustom?: boolean;
|
|
colors: {
|
|
background: string;
|
|
foreground: string;
|
|
cursor: string;
|
|
selection: string;
|
|
black: string;
|
|
red: string;
|
|
green: string;
|
|
yellow: string;
|
|
blue: string;
|
|
magenta: string;
|
|
cyan: string;
|
|
white: string;
|
|
brightBlack: string;
|
|
brightRed: string;
|
|
brightGreen: string;
|
|
brightYellow: string;
|
|
brightBlue: string;
|
|
brightMagenta: string;
|
|
brightCyan: string;
|
|
brightWhite: string;
|
|
}
|
|
}
|
|
|
|
export interface TerminalSession {
|
|
id: string;
|
|
hostId: string;
|
|
hostLabel: string;
|
|
username: string;
|
|
hostname: string;
|
|
status: 'connecting' | 'connected' | 'disconnected';
|
|
workspaceId?: string;
|
|
startupCommand?: string; // Command to run after connection (for snippet runner)
|
|
noAutoRun?: boolean; // If true, paste command without auto-executing
|
|
// Connection-time protocol overrides (used instead of looking up from hosts)
|
|
protocol?: 'ssh' | 'telnet' | 'local' | 'serial';
|
|
port?: number;
|
|
moshEnabled?: boolean;
|
|
shellType?: 'posix' | 'fish' | 'powershell' | 'cmd' | 'unknown';
|
|
charset?: string; // Connection-time charset override (e.g. for quick-connect serial)
|
|
// Serial-specific connection settings
|
|
serialConfig?: SerialConfig;
|
|
}
|
|
|
|
export interface RemoteFile {
|
|
name: string;
|
|
type: 'file' | 'directory' | 'symlink';
|
|
size: string;
|
|
lastModified: string;
|
|
linkTarget?: 'file' | 'directory' | null; // For symlinks: the type of the target, or null if broken
|
|
permissions?: string; // rwx format for owner/group/others e.g. "rwxr-xr-x"
|
|
hidden?: boolean; // Windows hidden attribute (only set for local Windows filesystem)
|
|
}
|
|
|
|
export type WorkspaceNode =
|
|
| {
|
|
id: string;
|
|
type: 'pane';
|
|
sessionId: string;
|
|
}
|
|
| {
|
|
id: string;
|
|
type: 'split';
|
|
direction: 'horizontal' | 'vertical';
|
|
children: WorkspaceNode[];
|
|
sizes?: number[]; // relative sizes for children
|
|
};
|
|
|
|
export type WorkspaceViewMode = 'split' | 'focus';
|
|
|
|
export interface Workspace {
|
|
id: string;
|
|
title: string;
|
|
root: WorkspaceNode;
|
|
viewMode?: WorkspaceViewMode; // 'split' = tiled view (default), 'focus' = left list + single terminal
|
|
focusedSessionId?: string; // Which session is focused when in focus mode
|
|
snippetId?: string; // If this workspace was created from running a snippet
|
|
}
|
|
|
|
// SFTP Types
|
|
export type SftpFilenameEncoding = 'auto' | 'utf-8' | 'gb18030';
|
|
|
|
export interface SftpFileEntry {
|
|
name: string;
|
|
type: 'file' | 'directory' | 'symlink';
|
|
size: number;
|
|
sizeFormatted: string;
|
|
lastModified: number;
|
|
lastModifiedFormatted: string;
|
|
permissions?: string;
|
|
owner?: string;
|
|
group?: string;
|
|
linkTarget?: 'file' | 'directory' | null; // For symlinks: the type of the target, or null if broken
|
|
hidden?: boolean; // Windows hidden attribute (only set for local Windows filesystem)
|
|
}
|
|
|
|
export interface SftpConnection {
|
|
id: string;
|
|
hostId: string;
|
|
hostLabel: string;
|
|
isLocal: boolean;
|
|
status: 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
error?: string;
|
|
currentPath: string;
|
|
homeDir?: string;
|
|
}
|
|
|
|
export type TransferStatus = 'pending' | 'transferring' | 'completed' | 'failed' | 'cancelled';
|
|
export type TransferDirection = 'upload' | 'download' | 'remote-to-remote' | 'local-copy';
|
|
|
|
export interface TransferTask {
|
|
id: string;
|
|
fileName: string;
|
|
originalFileName?: string;
|
|
sourcePath: string;
|
|
targetPath: string;
|
|
sourceConnectionId: string;
|
|
targetConnectionId: string;
|
|
targetHostId?: string;
|
|
/** Full endpoint key (hostId:hostname:port:protocol) for distinguishing
|
|
* same-hostId uploads with different session-time overrides. */
|
|
targetConnectionKey?: string;
|
|
direction: TransferDirection;
|
|
status: TransferStatus;
|
|
totalBytes: number;
|
|
transferredBytes: number;
|
|
speed: number; // bytes per second
|
|
error?: string;
|
|
startTime: number;
|
|
endTime?: number;
|
|
isDirectory: boolean;
|
|
progressMode?: 'bytes' | 'files';
|
|
childTasks?: string[]; // For directory transfers
|
|
parentTaskId?: string;
|
|
sourceLastModified?: number; // Cached from file list to avoid redundant stat
|
|
skipConflictCheck?: boolean; // Skip conflict check for replace operations
|
|
retryable?: boolean; // False for task types that cannot be safely replayed through generic retry
|
|
}
|
|
|
|
export interface FileConflict {
|
|
transferId: string;
|
|
fileName: string;
|
|
sourcePath: string;
|
|
targetPath: string;
|
|
existingSize: number;
|
|
newSize: number;
|
|
existingModified: number;
|
|
newModified: number;
|
|
}
|
|
|
|
// Port Forwarding Types
|
|
export type PortForwardingType = 'local' | 'remote' | 'dynamic';
|
|
type PortForwardingStatus = 'inactive' | 'connecting' | 'active' | 'error';
|
|
|
|
export interface PortForwardingRule {
|
|
id: string;
|
|
label: string;
|
|
type: PortForwardingType;
|
|
// Common fields
|
|
localPort: number;
|
|
bindAddress: string; // e.g., '127.0.0.1', '0.0.0.0'
|
|
// For local and remote forwarding
|
|
remoteHost?: string;
|
|
remotePort?: number;
|
|
// Host to tunnel through
|
|
hostId?: string;
|
|
// Auto-start: if true, this rule will automatically start when the app launches
|
|
autoStart?: boolean;
|
|
// Runtime state
|
|
status: PortForwardingStatus;
|
|
error?: string;
|
|
createdAt: number;
|
|
lastUsedAt?: number;
|
|
}
|
|
|
|
// Known Hosts - discovered from system SSH known_hosts file
|
|
export interface KnownHost {
|
|
id: string;
|
|
hostname: string; // The host pattern from known_hosts
|
|
port: number;
|
|
keyType: string; // ssh-rsa, ssh-ed25519, ecdsa-sha2-nistp256, etc.
|
|
publicKey: string; // The host's public key fingerprint or full key
|
|
discoveredAt: number;
|
|
lastSeen?: number;
|
|
convertedToHostId?: string; // If converted to managed host
|
|
}
|
|
|
|
// Shell History - records real commands executed in terminal sessions
|
|
export interface ShellHistoryEntry {
|
|
id: string;
|
|
command: string;
|
|
hostId: string; // ID of the host where command was executed
|
|
hostLabel: string; // Label for display
|
|
sessionId: string;
|
|
timestamp: number;
|
|
}
|
|
|
|
// Connection Log - records connection history
|
|
export interface ConnectionLog {
|
|
id: string;
|
|
sessionId?: string; // Terminal session ID for matching during capture
|
|
hostId: string; // Host ID (can be empty for local terminal)
|
|
hostLabel: string; // Display label (e.g., 'Local Terminal' or host label)
|
|
hostname: string; // Target hostname or 'localhost'
|
|
username: string; // SSH username or system username
|
|
protocol: 'ssh' | 'telnet' | 'local' | 'mosh' | 'serial';
|
|
startTime: number; // Connection start timestamp
|
|
endTime?: number; // Connection end timestamp (undefined if still active)
|
|
localUsername: string; // System username of the local user
|
|
localHostname: string; // Local machine hostname
|
|
saved: boolean; // Whether this log is bookmarked/saved
|
|
terminalData?: string; // Captured terminal output data for replay
|
|
themeId?: string; // Terminal theme ID for this log view
|
|
fontSize?: number; // Terminal font size for this log view
|
|
}
|
|
|
|
// Session Logs Settings - for auto-saving terminal logs to local filesystem
|
|
export type SessionLogFormat = 'txt' | 'raw' | 'html';
|
|
|
|
// Managed Source - external file that manages a group of hosts (e.g., ~/.ssh/config)
|
|
type ManagedSourceType = 'ssh_config';
|
|
|
|
export interface ManagedSource {
|
|
id: string;
|
|
type: ManagedSourceType;
|
|
filePath: string;
|
|
groupName: string;
|
|
lastSyncedAt: number;
|
|
lastFileHash?: string;
|
|
}
|