Skip to content

Commit 6d32ae8

Browse files
authored
Reveal Parent Directories in External Applications (#1791)
Adds context menu options to the directory preview to open the parent directory in the native file viewer. Additionally, it adds context menu options in the block header to open either a parent directory or a different type of file in an external default application. These context menu items are only available for local directory previews.
1 parent 6612b9c commit 6d32ae8

3 files changed

Lines changed: 102 additions & 35 deletions

File tree

frontend/app/view/preview/directorypreview.tsx

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { RpcApi } from "@/app/store/wshclientapi";
99
import { TabRpcClient } from "@/app/store/wshrpcutil";
1010
import type { PreviewModel } from "@/app/view/preview/preview";
1111
import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil";
12-
import { fireAndForget, isBlank } from "@/util/util";
12+
import { fireAndForget, isBlank, makeConnRoute, makeNativeLabel } from "@/util/util";
1313
import { offset, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
1414
import {
1515
Column,
@@ -508,24 +508,22 @@ function TableBody({
508508
}, [focusIndex]);
509509

510510
const handleFileContextMenu = useCallback(
511-
(e: any, finfo: FileInfo) => {
511+
async (e: any, finfo: FileInfo) => {
512512
e.preventDefault();
513513
e.stopPropagation();
514514
if (finfo == null) {
515515
return;
516516
}
517517
const normPath = getNormFilePath(finfo);
518518
const fileName = finfo.path.split("/").pop();
519-
let openNativeLabel = "Open File";
520-
if (finfo.isdir) {
521-
openNativeLabel = "Open Directory in File Manager";
522-
if (PLATFORM == "darwin") {
523-
openNativeLabel = "Open Directory in Finder";
524-
} else if (PLATFORM == "win32") {
525-
openNativeLabel = "Open Directory in Explorer";
526-
}
527-
} else {
528-
openNativeLabel = "Open File in Default Application";
519+
let parentFileInfo: FileInfo;
520+
try {
521+
parentFileInfo = await RpcApi.RemoteFileJoinCommand(TabRpcClient, [normPath, ".."], {
522+
route: makeConnRoute(conn),
523+
});
524+
} catch (e) {
525+
console.log("could not get parent file info. using child file info as fallback");
526+
parentFileInfo = finfo;
529527
}
530528
const menu: ContextMenuItem[] = [
531529
{
@@ -577,16 +575,6 @@ function TableBody({
577575
{
578576
type: "separator",
579577
},
580-
// TODO: Only show this option for local files, resolve correct host path if connection is WSL
581-
{
582-
label: openNativeLabel,
583-
click: () => {
584-
getApi().openNativePath(normPath);
585-
},
586-
},
587-
{
588-
type: "separator",
589-
},
590578
{
591579
label: "Open Preview in New Block",
592580
click: () =>
@@ -595,12 +583,33 @@ function TableBody({
595583
meta: {
596584
view: "preview",
597585
file: finfo.path,
586+
connection: conn,
598587
},
599588
};
600589
await createBlock(blockDef);
601590
}),
602591
},
603592
];
593+
if (!conn) {
594+
menu.push(
595+
{
596+
type: "separator",
597+
},
598+
// TODO: resolve correct host path if connection is WSL
599+
{
600+
label: makeNativeLabel(PLATFORM, finfo.isdir, false),
601+
click: () => {
602+
getApi().openNativePath(normPath);
603+
},
604+
},
605+
{
606+
label: makeNativeLabel(PLATFORM, true, true),
607+
click: () => {
608+
getApi().openNativePath(parentFileInfo.dir);
609+
},
610+
}
611+
);
612+
}
604613
if (finfo.mimetype == "directory") {
605614
menu.push({
606615
label: "Open Terminal in New Block",
@@ -611,6 +620,7 @@ function TableBody({
611620
controller: "shell",
612621
view: "term",
613622
"cmd:cwd": await model.formatRemoteUri(finfo.path, globalStore.get),
623+
connection: conn,
614624
},
615625
};
616626
await createBlock(termBlockDef);
@@ -858,12 +868,6 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
858868
(e: any) => {
859869
e.preventDefault();
860870
e.stopPropagation();
861-
let openNativeLabel = "Open Directory in File Manager";
862-
if (PLATFORM == "darwin") {
863-
openNativeLabel = "Open Directory in Finder";
864-
} else if (PLATFORM == "win32") {
865-
openNativeLabel = "Open Directory in Explorer";
866-
}
867871
const menu: ContextMenuItem[] = [
868872
{
869873
label: "New File",
@@ -880,15 +884,16 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
880884
{
881885
type: "separator",
882886
},
883-
// TODO: Only show this option for local files, resolve correct host path if connection is WSL
884-
{
885-
label: openNativeLabel,
887+
];
888+
if (!conn) {
889+
// TODO: resolve correct host path if connection is WSL
890+
menu.push({
891+
label: makeNativeLabel(PLATFORM, true, true),
886892
click: () => {
887-
console.log(`opening ${dirPath}`);
888893
getApi().openNativePath(dirPath);
889894
},
890-
},
891-
];
895+
});
896+
}
892897
menu.push({
893898
label: "Open Terminal in New Block",
894899
click: async () => {
@@ -897,6 +902,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
897902
controller: "shell",
898903
view: "term",
899904
"cmd:cwd": dirPath,
905+
connection: conn,
900906
},
901907
};
902908
await createBlock(termBlockDef);

frontend/app/view/preview/preview.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,28 @@ import { Markdown } from "@/element/markdown";
1313
import {
1414
atoms,
1515
createBlock,
16+
getApi,
1617
getConnStatusAtom,
1718
getOverrideConfigAtom,
1819
getSettingsKeyAtom,
1920
globalStore,
21+
PLATFORM,
2022
refocusNode,
2123
} from "@/store/global";
2224
import * as services from "@/store/services";
2325
import * as WOS from "@/store/wos";
2426
import { getWebServerEndpoint } from "@/util/endpoints";
2527
import { goHistory, goHistoryBack, goHistoryForward } from "@/util/historyutil";
2628
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed, keydownWrapper } from "@/util/keyutil";
27-
import { base64ToString, fireAndForget, isBlank, jotaiLoadableValue, makeConnRoute, stringToBase64 } from "@/util/util";
29+
import {
30+
base64ToString,
31+
fireAndForget,
32+
isBlank,
33+
jotaiLoadableValue,
34+
makeConnRoute,
35+
makeNativeLabel,
36+
stringToBase64,
37+
} from "@/util/util";
2838
import { Monaco } from "@monaco-editor/react";
2939
import clsx from "clsx";
3040
import { Atom, atom, Getter, PrimitiveAtom, useAtomValue, useSetAtom, WritableAtom } from "jotai";
@@ -139,6 +149,7 @@ export class PreviewModel implements ViewModel {
139149
loadableStatFilePath: Atom<Loadable<string>>;
140150
loadableFileInfo: Atom<Loadable<FileInfo>>;
141151
connection: Atom<Promise<string>>;
152+
connectionImmediate: Atom<string>;
142153
statFile: Atom<Promise<FileInfo>>;
143154
fullFile: Atom<Promise<FileData>>;
144155
fileMimeType: Atom<Promise<string>>;
@@ -364,6 +375,9 @@ export class PreviewModel implements ViewModel {
364375
}
365376
return connName;
366377
});
378+
this.connectionImmediate = atom<string>((get) => {
379+
return get(this.blockAtom)?.meta?.connection;
380+
});
367381
this.statFile = atom<Promise<FileInfo>>(async (get) => {
368382
const fileName = get(this.metaFilePath);
369383
if (fileName == null) {
@@ -677,17 +691,40 @@ export class PreviewModel implements ViewModel {
677691
label: "Open Terminal in New Block",
678692
click: () =>
679693
fireAndForget(async () => {
694+
const conn = await globalStore.get(this.connection);
680695
const fileInfo = await globalStore.get(this.statFile);
681696
const termBlockDef: BlockDef = {
682697
meta: {
683698
view: "term",
684699
controller: "shell",
685700
"cmd:cwd": fileInfo.dir,
701+
connection: conn,
686702
},
687703
};
688704
await createBlock(termBlockDef);
689705
}),
690706
});
707+
const conn = globalStore.get(this.connectionImmediate);
708+
if (!conn) {
709+
menuItems.push({
710+
label: makeNativeLabel(PLATFORM, true, true),
711+
click: async () => {
712+
const fileInfo = await globalStore.get(this.statFile);
713+
getApi().openNativePath(fileInfo.dir);
714+
},
715+
});
716+
}
717+
} else {
718+
const conn = globalStore.get(this.connectionImmediate);
719+
if (!conn) {
720+
menuItems.push({
721+
label: makeNativeLabel(PLATFORM, false, false),
722+
click: async () => {
723+
const fileInfo = await globalStore.get(this.statFile);
724+
getApi().openNativePath(`${fileInfo.dir}/${fileInfo.name}`);
725+
},
726+
});
727+
}
691728
}
692729
const loadableSV = globalStore.get(this.loadableSpecializedView);
693730
const wordWrapAtom = getOverrideConfigAtom(this.blockId, "editor:wordwrap");

frontend/util/util.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,29 @@ function sleep(ms: number): Promise<void> {
306306
return new Promise((resolve) => setTimeout(resolve, ms));
307307
}
308308

309+
function makeNativeLabel(platform: string, isDirectory: boolean, isParent: boolean) {
310+
let managerName: string;
311+
if (!isDirectory && !isParent) {
312+
managerName = "Default Application";
313+
} else if (platform == "darwin") {
314+
managerName = "Finder";
315+
} else if (platform == "win32") {
316+
managerName = "Explorer";
317+
} else {
318+
managerName = "File Manager";
319+
}
320+
321+
let fileAction: string;
322+
if (isParent) {
323+
fileAction = "Reveal";
324+
} else if (isDirectory) {
325+
fileAction = "Open Directory";
326+
} else {
327+
fileAction = "Open File";
328+
}
329+
return `${fileAction} in ${managerName}`;
330+
}
331+
309332
export {
310333
atomWithDebounce,
311334
atomWithThrottle,
@@ -325,6 +348,7 @@ export {
325348
makeConnRoute,
326349
makeExternLink,
327350
makeIconClass,
351+
makeNativeLabel,
328352
sleep,
329353
stringToBase64,
330354
useAtomValueSafe,

0 commit comments

Comments
 (0)