Skip to content

Commit 02b9e5f

Browse files
feat: drag and drop file paths into terminal (#2857)
Fixes #746, fixes #2813 Drag a file from Finder into a terminal and it pastes the quoted path. Uses `webUtils.getPathForFile()` through a preload bridge since Electron 32 killed `File.path`. Handles spaces in filenames. Needs app restart after install (preload change).
1 parent 0ade6ee commit 02b9e5f

File tree

4 files changed

+47
-2
lines changed

4 files changed

+47
-2
lines changed

emain/preload.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { contextBridge, ipcRenderer, Rectangle, WebviewTag } from "electron";
4+
import { contextBridge, ipcRenderer, Rectangle, webUtils, WebviewTag } from "electron";
55

66
// update type in custom.d.ts (ElectronApi type)
77
contextBridge.exposeInMainWorld("api", {
@@ -70,6 +70,7 @@ contextBridge.exposeInMainWorld("api", {
7070
openBuilder: (appId?: string) => ipcRenderer.send("open-builder", appId),
7171
setBuilderWindowAppId: (appId: string) => ipcRenderer.send("set-builder-window-appid", appId),
7272
doRefresh: () => ipcRenderer.send("do-refresh"),
73+
getPathForFile: (file: File): string => webUtils.getPathForFile(file),
7374
saveTextFile: (fileName: string, content: string) => ipcRenderer.invoke("save-text-file", fileName, content),
7475
setIsActive: () => ipcRenderer.invoke("set-is-active"),
7576
});

frontend/app/view/term/termutil.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,7 @@ export function bufferLinesToText(buffer: TermTypes.IBuffer, startIndex: number,
389389

390390
return lines;
391391
}
392+
393+
export function quoteForPosixShell(filePath: string): string {
394+
return "'" + filePath.replace(/'/g, "'\\''") + "'";
395+
}

frontend/app/view/term/termwrap.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { RpcApi } from "@/app/store/wshclientapi";
88
import { TabRpcClient } from "@/app/store/wshrpcutil";
99
import {
1010
fetchWaveFile,
11+
getApi,
1112
getOverrideConfigAtom,
1213
getSettingsKeyAtom,
1314
globalStore,
@@ -35,7 +36,13 @@ import {
3536
isClaudeCodeCommand,
3637
type ShellIntegrationStatus,
3738
} from "./osc-handlers";
38-
import { bufferLinesToText, createTempFileFromBlob, extractAllClipboardData, normalizeCursorStyle } from "./termutil";
39+
import {
40+
bufferLinesToText,
41+
createTempFileFromBlob,
42+
extractAllClipboardData,
43+
normalizeCursorStyle,
44+
quoteForPosixShell,
45+
} from "./termutil";
3946

4047
const dlog = debug("wave:termwrap");
4148

@@ -274,6 +281,38 @@ export class TermWrap {
274281
this.heldData = [];
275282
this.handleResize_debounced = debounce(50, this.handleResize.bind(this));
276283
this.terminal.open(this.connectElem);
284+
285+
const dragoverHandler = (e: DragEvent) => {
286+
e.preventDefault();
287+
if (e.dataTransfer) {
288+
e.dataTransfer.dropEffect = "copy";
289+
}
290+
};
291+
const dropHandler = (e: DragEvent) => {
292+
e.preventDefault();
293+
if (!e.dataTransfer || e.dataTransfer.files.length === 0) {
294+
return;
295+
}
296+
const paths: string[] = [];
297+
for (let i = 0; i < e.dataTransfer.files.length; i++) {
298+
const file = e.dataTransfer.files[i];
299+
const filePath = getApi().getPathForFile(file);
300+
if (filePath) {
301+
paths.push(quoteForPosixShell(filePath));
302+
}
303+
}
304+
if (paths.length > 0) {
305+
this.terminal.paste(paths.join(" ") + " ");
306+
}
307+
};
308+
this.connectElem.addEventListener("dragover", dragoverHandler);
309+
this.connectElem.addEventListener("drop", dropHandler);
310+
this.toDispose.push({
311+
dispose: () => {
312+
this.connectElem.removeEventListener("dragover", dragoverHandler);
313+
this.connectElem.removeEventListener("drop", dropHandler);
314+
},
315+
});
277316
this.handleResize();
278317
const pasteHandler = this.pasteHandler.bind(this);
279318
this.connectElem.addEventListener("paste", pasteHandler, true);

frontend/types/custom.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ declare global {
133133
openBuilder: (appId?: string) => void; // open-builder
134134
setBuilderWindowAppId: (appId: string) => void; // set-builder-window-appid
135135
doRefresh: () => void; // do-refresh
136+
getPathForFile: (file: File) => string; // webUtils.getPathForFile
136137
saveTextFile: (fileName: string, content: string) => Promise<boolean>; // save-text-file
137138
setIsActive: () => Promise<void>; // set-is-active
138139
};

0 commit comments

Comments
 (0)