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.
This commit is contained in:
Sylvie Crowe 2025-01-22 19:09:07 -08:00 committed by GitHub
parent 6612b9c6ba
commit 6d32ae856c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 102 additions and 35 deletions

View File

@ -9,7 +9,7 @@ import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import type { PreviewModel } from "@/app/view/preview/preview";
import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil";
import { fireAndForget, isBlank } from "@/util/util";
import { fireAndForget, isBlank, makeConnRoute, makeNativeLabel } from "@/util/util";
import { offset, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
import {
Column,
@ -508,7 +508,7 @@ function TableBody({
}, [focusIndex]);
const handleFileContextMenu = useCallback(
(e: any, finfo: FileInfo) => {
async (e: any, finfo: FileInfo) => {
e.preventDefault();
e.stopPropagation();
if (finfo == null) {
@ -516,16 +516,14 @@ function TableBody({
}
const normPath = getNormFilePath(finfo);
const fileName = finfo.path.split("/").pop();
let openNativeLabel = "Open File";
if (finfo.isdir) {
openNativeLabel = "Open Directory in File Manager";
if (PLATFORM == "darwin") {
openNativeLabel = "Open Directory in Finder";
} else if (PLATFORM == "win32") {
openNativeLabel = "Open Directory in Explorer";
}
} else {
openNativeLabel = "Open File in Default Application";
let parentFileInfo: FileInfo;
try {
parentFileInfo = await RpcApi.RemoteFileJoinCommand(TabRpcClient, [normPath, ".."], {
route: makeConnRoute(conn),
});
} catch (e) {
console.log("could not get parent file info. using child file info as fallback");
parentFileInfo = finfo;
}
const menu: ContextMenuItem[] = [
{
@ -577,16 +575,6 @@ function TableBody({
{
type: "separator",
},
// TODO: Only show this option for local files, resolve correct host path if connection is WSL
{
label: openNativeLabel,
click: () => {
getApi().openNativePath(normPath);
},
},
{
type: "separator",
},
{
label: "Open Preview in New Block",
click: () =>
@ -595,12 +583,33 @@ function TableBody({
meta: {
view: "preview",
file: finfo.path,
connection: conn,
},
};
await createBlock(blockDef);
}),
},
];
if (!conn) {
menu.push(
{
type: "separator",
},
// TODO: resolve correct host path if connection is WSL
{
label: makeNativeLabel(PLATFORM, finfo.isdir, false),
click: () => {
getApi().openNativePath(normPath);
},
},
{
label: makeNativeLabel(PLATFORM, true, true),
click: () => {
getApi().openNativePath(parentFileInfo.dir);
},
}
);
}
if (finfo.mimetype == "directory") {
menu.push({
label: "Open Terminal in New Block",
@ -611,6 +620,7 @@ function TableBody({
controller: "shell",
view: "term",
"cmd:cwd": await model.formatRemoteUri(finfo.path, globalStore.get),
connection: conn,
},
};
await createBlock(termBlockDef);
@ -858,12 +868,6 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
(e: any) => {
e.preventDefault();
e.stopPropagation();
let openNativeLabel = "Open Directory in File Manager";
if (PLATFORM == "darwin") {
openNativeLabel = "Open Directory in Finder";
} else if (PLATFORM == "win32") {
openNativeLabel = "Open Directory in Explorer";
}
const menu: ContextMenuItem[] = [
{
label: "New File",
@ -880,15 +884,16 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
{
type: "separator",
},
// TODO: Only show this option for local files, resolve correct host path if connection is WSL
{
label: openNativeLabel,
];
if (!conn) {
// TODO: resolve correct host path if connection is WSL
menu.push({
label: makeNativeLabel(PLATFORM, true, true),
click: () => {
console.log(`opening ${dirPath}`);
getApi().openNativePath(dirPath);
},
},
];
});
}
menu.push({
label: "Open Terminal in New Block",
click: async () => {
@ -897,6 +902,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
controller: "shell",
view: "term",
"cmd:cwd": dirPath,
connection: conn,
},
};
await createBlock(termBlockDef);

View File

@ -13,10 +13,12 @@ import { Markdown } from "@/element/markdown";
import {
atoms,
createBlock,
getApi,
getConnStatusAtom,
getOverrideConfigAtom,
getSettingsKeyAtom,
globalStore,
PLATFORM,
refocusNode,
} from "@/store/global";
import * as services from "@/store/services";
@ -24,7 +26,15 @@ import * as WOS from "@/store/wos";
import { getWebServerEndpoint } from "@/util/endpoints";
import { goHistory, goHistoryBack, goHistoryForward } from "@/util/historyutil";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed, keydownWrapper } from "@/util/keyutil";
import { base64ToString, fireAndForget, isBlank, jotaiLoadableValue, makeConnRoute, stringToBase64 } from "@/util/util";
import {
base64ToString,
fireAndForget,
isBlank,
jotaiLoadableValue,
makeConnRoute,
makeNativeLabel,
stringToBase64,
} from "@/util/util";
import { Monaco } from "@monaco-editor/react";
import clsx from "clsx";
import { Atom, atom, Getter, PrimitiveAtom, useAtomValue, useSetAtom, WritableAtom } from "jotai";
@ -139,6 +149,7 @@ export class PreviewModel implements ViewModel {
loadableStatFilePath: Atom<Loadable<string>>;
loadableFileInfo: Atom<Loadable<FileInfo>>;
connection: Atom<Promise<string>>;
connectionImmediate: Atom<string>;
statFile: Atom<Promise<FileInfo>>;
fullFile: Atom<Promise<FileData>>;
fileMimeType: Atom<Promise<string>>;
@ -364,6 +375,9 @@ export class PreviewModel implements ViewModel {
}
return connName;
});
this.connectionImmediate = atom<string>((get) => {
return get(this.blockAtom)?.meta?.connection;
});
this.statFile = atom<Promise<FileInfo>>(async (get) => {
const fileName = get(this.metaFilePath);
if (fileName == null) {
@ -677,17 +691,40 @@ export class PreviewModel implements ViewModel {
label: "Open Terminal in New Block",
click: () =>
fireAndForget(async () => {
const conn = await globalStore.get(this.connection);
const fileInfo = await globalStore.get(this.statFile);
const termBlockDef: BlockDef = {
meta: {
view: "term",
controller: "shell",
"cmd:cwd": fileInfo.dir,
connection: conn,
},
};
await createBlock(termBlockDef);
}),
});
const conn = globalStore.get(this.connectionImmediate);
if (!conn) {
menuItems.push({
label: makeNativeLabel(PLATFORM, true, true),
click: async () => {
const fileInfo = await globalStore.get(this.statFile);
getApi().openNativePath(fileInfo.dir);
},
});
}
} else {
const conn = globalStore.get(this.connectionImmediate);
if (!conn) {
menuItems.push({
label: makeNativeLabel(PLATFORM, false, false),
click: async () => {
const fileInfo = await globalStore.get(this.statFile);
getApi().openNativePath(`${fileInfo.dir}/${fileInfo.name}`);
},
});
}
}
const loadableSV = globalStore.get(this.loadableSpecializedView);
const wordWrapAtom = getOverrideConfigAtom(this.blockId, "editor:wordwrap");

View File

@ -306,6 +306,29 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function makeNativeLabel(platform: string, isDirectory: boolean, isParent: boolean) {
let managerName: string;
if (!isDirectory && !isParent) {
managerName = "Default Application";
} else if (platform == "darwin") {
managerName = "Finder";
} else if (platform == "win32") {
managerName = "Explorer";
} else {
managerName = "File Manager";
}
let fileAction: string;
if (isParent) {
fileAction = "Reveal";
} else if (isDirectory) {
fileAction = "Open Directory";
} else {
fileAction = "Open File";
}
return `${fileAction} in ${managerName}`;
}
export {
atomWithDebounce,
atomWithThrottle,
@ -325,6 +348,7 @@ export {
makeConnRoute,
makeExternLink,
makeIconClass,
makeNativeLabel,
sleep,
stringToBase64,
useAtomValueSafe,