diff --git a/emain/preload.ts b/emain/preload.ts index 823f99c4cd..8d2b18a308 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", { @@ -70,6 +70,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): string => webUtils.getPathForFile(file), saveTextFile: (fileName: string, content: string) => ipcRenderer.invoke("save-text-file", fileName, content), setIsActive: () => ipcRenderer.invoke("set-is-active"), }); diff --git a/frontend/app/view/term/termutil.ts b/frontend/app/view/term/termutil.ts index e49b8d4b8a..2fea30404a 100644 --- a/frontend/app/view/term/termutil.ts +++ b/frontend/app/view/term/termutil.ts @@ -389,3 +389,7 @@ export function bufferLinesToText(buffer: TermTypes.IBuffer, startIndex: number, return lines; } + +export function quoteForPosixShell(filePath: string): string { + return "'" + filePath.replace(/'/g, "'\\''") + "'"; +} diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index d79ce695cd..e1b129b72d 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -8,6 +8,7 @@ import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { fetchWaveFile, + getApi, getOverrideConfigAtom, getSettingsKeyAtom, globalStore, @@ -35,7 +36,13 @@ import { isClaudeCodeCommand, type ShellIntegrationStatus, } from "./osc-handlers"; -import { bufferLinesToText, createTempFileFromBlob, extractAllClipboardData, normalizeCursorStyle } from "./termutil"; +import { + bufferLinesToText, + createTempFileFromBlob, + extractAllClipboardData, + normalizeCursorStyle, + quoteForPosixShell, +} from "./termutil"; const dlog = debug("wave:termwrap"); @@ -274,6 +281,38 @@ export class TermWrap { this.heldData = []; this.handleResize_debounced = debounce(50, this.handleResize.bind(this)); this.terminal.open(this.connectElem); + + const dragoverHandler = (e: DragEvent) => { + e.preventDefault(); + if (e.dataTransfer) { + e.dataTransfer.dropEffect = "copy"; + } + }; + const dropHandler = (e: DragEvent) => { + e.preventDefault(); + if (!e.dataTransfer || e.dataTransfer.files.length === 0) { + return; + } + const paths: string[] = []; + for (let i = 0; i < e.dataTransfer.files.length; i++) { + const file = e.dataTransfer.files[i]; + const filePath = getApi().getPathForFile(file); + if (filePath) { + paths.push(quoteForPosixShell(filePath)); + } + } + if (paths.length > 0) { + this.terminal.paste(paths.join(" ") + " "); + } + }; + this.connectElem.addEventListener("dragover", dragoverHandler); + this.connectElem.addEventListener("drop", dropHandler); + this.toDispose.push({ + dispose: () => { + this.connectElem.removeEventListener("dragover", dragoverHandler); + this.connectElem.removeEventListener("drop", dropHandler); + }, + }); this.handleResize(); const pasteHandler = this.pasteHandler.bind(this); this.connectElem.addEventListener("paste", pasteHandler, true); diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 9f7cb15ad3..06157e2566 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -133,6 +133,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; // webUtils.getPathForFile saveTextFile: (fileName: string, content: string) => Promise; // save-text-file setIsActive: () => Promise; // set-is-active };