import type { SftpFileEntry } from "../../types"; import type { DropEntry } from "../../lib/sftpFileUtils"; import type { KeyBinding } from "../../domain/models"; import { joinPath } from "../../application/state/sftp/utils"; import { isNavigableDirectory } from "./utils"; export interface ClipboardLocalFile { path: string; name: string; isDirectory: boolean; size?: number; } export interface SftpClipboardUploadTreeSelection { name: string; path: string; isDirectory: boolean; } export interface ResolveSftpClipboardUploadTargetParams { currentPath: string; selectedFileNames: string[]; files: SftpFileEntry[]; treeSelection: SftpClipboardUploadTreeSelection[]; } export interface GetSftpClipboardSystemTextPathsParams { currentPath: string; selectedFileNames: string[]; treeSelection: SftpClipboardUploadTreeSelection[]; } export function resolveSftpClipboardUploadTarget({ currentPath, selectedFileNames, files, treeSelection, }: ResolveSftpClipboardUploadTargetParams): string { const selectedTreeFolders = treeSelection.filter((entry) => entry.isDirectory && entry.name !== ".."); if (selectedTreeFolders.length === 1) { return selectedTreeFolders[0].path; } if (selectedFileNames.length === 1) { const filesByName = new Map(files.map((entry) => [entry.name, entry])); const selectedEntry = filesByName.get(selectedFileNames[0]); if (selectedEntry && isNavigableDirectory(selectedEntry)) { return joinPath(currentPath, selectedEntry.name); } } return currentPath; } export function getSftpClipboardSystemTextPaths({ currentPath, selectedFileNames, treeSelection, }: GetSftpClipboardSystemTextPathsParams): string[] { if (treeSelection.length > 0) { return treeSelection.map((entry) => entry.path); } return selectedFileNames.map((name) => joinPath(currentPath, name)); } export function createDropEntriesFromClipboardFiles(files: ClipboardLocalFile[]): DropEntry[] { return files.map((file) => ({ file: null, localPath: file.path, relativePath: file.name, isDirectory: file.isDirectory, size: file.size, })); } export function getSupportedClipboardUploadFiles(files: ClipboardLocalFile[]): ClipboardLocalFile[] { return files; } export function shouldLetNativePasteEventHandleSftpPaste( action: string, key: string | undefined, ): boolean { if (action !== "sftpPaste" || !key) return false; const normalized = key.toLowerCase().replace(/\s+/g, ""); return [ "ctrl+v", "⌘+v", "cmd+v", "command+v", ].includes(normalized); } export function isSftpNativeClipboardPasteEnabled( hotkeyScheme: "disabled" | "mac" | "pc", keyBindings: KeyBinding[], ): boolean { if (hotkeyScheme === "disabled") return false; const pasteBinding = keyBindings.find((binding) => ( binding.category === "sftp" && binding.action === "sftpPaste" )); if (!pasteBinding) return false; const key = hotkeyScheme === "mac" ? pasteBinding.mac : pasteBinding.pc; return shouldLetNativePasteEventHandleSftpPaste("sftpPaste", key); } export interface SftpClipboardUploadRequest { scopeId: string; side: "left" | "right"; targetPath: string; files: ClipboardLocalFile[]; onConfirm: () => Promise; } type ClipboardUploadListener = () => void; let clipboardUploadRequest: SftpClipboardUploadRequest | null = null; const clipboardUploadListeners = new Set(); const notifyClipboardUploadListeners = () => { clipboardUploadListeners.forEach((listener) => listener()); }; export const sftpClipboardUploadStore = { trigger: (request: SftpClipboardUploadRequest) => { clipboardUploadRequest = request; notifyClipboardUploadListeners(); }, clear: (request?: SftpClipboardUploadRequest | null) => { if (request && clipboardUploadRequest !== request) return; clipboardUploadRequest = null; notifyClipboardUploadListeners(); }, getSnapshot: () => clipboardUploadRequest, subscribe: (listener: ClipboardUploadListener) => { clipboardUploadListeners.add(listener); return () => clipboardUploadListeners.delete(listener); }, }; /** * Hand off a confirmed clipboard upload without holding the modal open. * Clears the active request first, then runs the transfer (issue #2478). */ export async function confirmSftpClipboardUpload(params: { request: SftpClipboardUploadRequest; clear?: (request: SftpClipboardUploadRequest) => void; onUploaded?: (targetPath: string) => void; }): Promise { const { request, onUploaded } = params; const clear = params.clear ?? sftpClipboardUploadStore.clear; clear(request); await request.onConfirm(); onUploaded?.(request.targetPath); } /** True when Upload should start for this request (blocks same-request double-click only). */ export function shouldStartClipboardUploadConfirm( request: SftpClipboardUploadRequest | null | undefined, alreadyStartedFor: SftpClipboardUploadRequest | null | undefined, ): boolean { return !!request && alreadyStartedFor !== request; }