import { Copy, FileSymlink, Files, Folder, FolderOpen, Monitor, Pencil, Plus, Server, Settings2 } from 'lucide-react';
import React from 'react';
import { useI18n } from '../../application/i18n/I18nProvider';
import { requestOpenDualPaneSftp } from '../../application/state/sftp/sftpDualPaneOpenStore';
import { useSettingsChromeStore } from '../../application/state/settingsChromeStore';
import { sanitizeHost } from '../../domain/host';
import { isPluginHostProtocol } from '../../domain/pluginConnection';
import { canOpenDualPaneSftp } from '../../domain/sftpDualPaneOpen';
import type { Host } from '../../types';
import { ContextMenuContent, ContextMenuItem, ContextMenuShortcut } from '../ui/context-menu';
import { collectOwnedPluginMenus, comparePluginMenus, usePluginContributions } from '../../application/state/usePluginContributions';
import { PluginContributionIcon } from '../plugins/PluginContributionIcon';
export interface HostTreeHostContextMenuHandlers {
onConnect: (host: Host) => void;
onEditHost?: (host: Host) => void;
onRenameHost?: (host: Host) => void;
onDuplicateHost: (host: Host) => void;
onCopyHostname?: (host: Host) => void;
onCopyCredentials: (host: Host) => void;
onDeleteHost: (host: Host) => void;
}
export const OpenDualPaneSftpMenuItem: React.FC<{ host: Host }> = ({ host }) => {
const { t } = useI18n();
const { showSftpTab } = useSettingsChromeStore();
if (!showSftpTab || !canOpenDualPaneSftp(host)) return null;
return (
requestOpenDualPaneSftp(host.id)}>
{t('vault.hosts.openSftp')}
);
};
export const HostTreeHostContextMenuContent: React.FC<
HostTreeHostContextMenuHandlers & { host: Host }
> = ({
host,
onConnect,
onEditHost,
onRenameHost,
onDuplicateHost,
onCopyHostname,
onCopyCredentials,
onDeleteHost,
}) => {
const { t } = useI18n();
const safeHost = sanitizeHost(host);
const pluginContributions = usePluginContributions({
context: {
'netcatty.surface': 'host/context',
'host.id': safeHost.id,
'host.protocol': safeHost.protocol ?? 'ssh',
},
});
const pluginMenus = collectOwnedPluginMenus(pluginContributions.snapshot.plugins)
.filter((menu) => menu.location === 'host/context' && menu.visible)
.sort(comparePluginMenus);
const canCopyHostname = Boolean(onCopyHostname) && !isPluginHostProtocol(safeHost.protocol);
return (
onConnect(safeHost)}>
{t('vault.hosts.connect')}
{onEditHost && (
onEditHost(host)}>
{t('terminal.layer.hostTree.editHost')}
)}
{onRenameHost && (
onRenameHost(host)}>
{t('common.rename')}
)}
onDuplicateHost(host)}>
{t('action.duplicate')}
{canCopyHostname ? (
onCopyHostname?.(host)}>
{t('terminal.statusbar.copyHostname.label')}
) : null}
onCopyCredentials(host)}>
{t('vault.hosts.copyCredentials')}
onDeleteHost(host)}
className="text-destructive focus:text-destructive"
>
{t('action.delete')}
{pluginMenus.map((menu) => (
void pluginContributions.executeCommand(event.altKey && menu.alt ? menu.alt : menu.command, { hostId: safeHost.id }, {
'netcatty.surface': 'host/context',
'host.id': safeHost.id,
'host.protocol': safeHost.protocol ?? 'ssh',
}).catch(() => {})}
>
{menu.title}
{menu.checked && ✓}
{menu.shortcut && {menu.shortcut}}
))}
);
};
export interface HostTreeGroupContextMenuHandlers {
onNewHost?: (groupPath: string) => void;
onNewGroup: (parentPath?: string) => void;
onRenameGroup: (groupPath: string) => void;
onDeleteGroup: (groupPath: string) => void;
onUnmanageGroup?: (groupPath: string) => void;
}
export const HostTreeGroupContextMenuContent: React.FC<
HostTreeGroupContextMenuHandlers & { groupPath: string; isManaged: boolean }
> = ({
groupPath,
isManaged,
onNewHost,
onNewGroup,
onRenameGroup,
onDeleteGroup,
onUnmanageGroup,
}) => {
const { t } = useI18n();
return (
{onNewHost && (
onNewHost(groupPath)}>
{t('terminal.layer.hostTree.newHostInGroup')}
)}
onNewGroup(groupPath)}>
{t('vault.hosts.newGroup')}
onRenameGroup(groupPath)}>
{t('vault.groups.rename')}
onDeleteGroup(groupPath)}
className="text-destructive focus:text-destructive"
>
{t('vault.groups.delete')}
{isManaged && onUnmanageGroup && (
onUnmanageGroup(groupPath)}>
{t('vault.managedSource.unmanage')}
)}
);
};