diff --git a/emain/preload.ts b/emain/preload.ts index c6bdf1498..ee60c1360 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -1,7 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { contextBridge, ipcRenderer, Rectangle, WebviewTag } from "electron"; +import { contextBridge, ipcRenderer, Rectangle, webUtils, WebviewTag } from "electron"; // update type in custom.d.ts (ElectronApi type) contextBridge.exposeInMainWorld("api", { @@ -68,6 +68,7 @@ contextBridge.exposeInMainWorld("api", { openBuilder: (appId?: string) => ipcRenderer.send("open-builder", appId), setBuilderWindowAppId: (appId: string) => ipcRenderer.send("set-builder-window-appid", appId), doRefresh: () => ipcRenderer.send("do-refresh"), + getPathForFile: (file: File) => webUtils.getPathForFile(file), }); // Custom event for "new-window" diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 387155752..e34ca6fdf 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -7,7 +7,7 @@ import { waveEventSubscribe } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import type { TermViewModel } from "@/app/view/term/term-model"; -import { atoms, getOverrideConfigAtom, getSettingsPrefixAtom, globalStore, WOS } from "@/store/global"; +import { atoms, getApi, getOverrideConfigAtom, getSettingsPrefixAtom, globalStore, WOS } from "@/store/global"; import { fireAndForget, useAtomValueSafe } from "@/util/util"; import { computeBgStyleFromMeta } from "@/util/waveutil"; import { ISearchOptions } from "@xterm/addon-search"; @@ -349,8 +349,70 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => const termBg = computeBgStyleFromMeta(blockData?.meta); + // Handle drag and drop + // Helper to check if drag event contains files + const isFileDrop = (e: React.DragEvent): boolean => { + return e.dataTransfer?.types?.includes("Files") ?? false; + }; + + const handleDragOver = React.useCallback((e: React.DragEvent) => { + if (!isFileDrop(e)) return; + e.preventDefault(); + e.stopPropagation(); + e.dataTransfer.dropEffect = "copy"; + }, []); + + const handleDrop = React.useCallback((e: React.DragEvent) => { + if (!isFileDrop(e)) return; + e.preventDefault(); + e.stopPropagation(); + + const files = Array.from(e.dataTransfer.files); + if (files.length === 0) return; + + // Get file paths using Electron API + const paths = files.map((file: File) => { + try { + const fullPath = getApi().getPathForFile(file); + // Quote paths with spaces or special shell characters + if (/[\s'"]/.test(fullPath)) { + return `"${fullPath}"`; + } + return fullPath; + } catch (err) { + console.error("Could not get path for file:", file.name, err); + return file.name; + } + }); + + // Send space-separated paths to terminal + const pathString = paths.join(" "); + if (model.termRef.current && pathString) { + model.sendDataToController(pathString); + } + }, [model]); + + const handleDragEnter = React.useCallback((e: React.DragEvent) => { + if (!isFileDrop(e)) return; + e.preventDefault(); + e.stopPropagation(); + }, []); + + const handleDragLeave = React.useCallback((e: React.DragEvent) => { + if (!isFileDrop(e)) return; + e.preventDefault(); + e.stopPropagation(); + }, []); + return ( -
+
{termBg &&
} diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 3476fe539..b43f31c8c 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -130,6 +130,7 @@ declare global { openBuilder: (appId?: string) => void; // open-builder setBuilderWindowAppId: (appId: string) => void; // set-builder-window-appid doRefresh: () => void; // do-refresh + getPathForFile: (file: File) => string; // get-path-for-file }; type ElectronContextMenuItem = {