From 35533d3aeff11c84df2389d9243b6465bdecd348 Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Wed, 22 Jan 2025 00:03:50 -0800 Subject: [PATCH 1/9] feat: add ability to open parent file externally This only adds the options to the individual rows and the frame. It still needs to be added to the header. --- .../app/view/preview/directorypreview.tsx | 24 +++++++----------- frontend/util/util.ts | 25 +++++++++++++++++++ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index c1c13e019b..6251072873 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -8,7 +8,7 @@ import { PLATFORM, atoms, createBlock, getApi, globalStore } from "@/app/store/g import { FileService } from "@/app/store/services"; import type { PreviewModel } from "@/app/view/preview/preview"; import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil"; -import { base64ToString, fireAndForget, isBlank } from "@/util/util"; +import { base64ToString, fireAndForget, isBlank, makeNativeLabel } from "@/util/util"; import { offset, useDismiss, useFloating, useInteractions } from "@floating-ui/react"; import { Column, @@ -509,17 +509,6 @@ 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"; - } const menu: ContextMenuItem[] = [ { label: "New File", @@ -572,11 +561,17 @@ function TableBody({ }, // TODO: Only show this option for local files, resolve correct host path if connection is WSL { - label: openNativeLabel, + label: makeNativeLabel(finfo.isdir, false), click: () => { getApi().openNativePath(normPath); }, }, + { + label: makeNativeLabel(true, true), + click: () => { + getApi().openNativePath(finfo.dir); + }, + }, { type: "separator", }, @@ -856,9 +851,8 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { }, // TODO: Only show this option for local files, resolve correct host path if connection is WSL { - label: openNativeLabel, + label: makeNativeLabel(true, true), click: () => { - console.log(`opening ${dirPath}`); getApi().openNativePath(dirPath); }, }, diff --git a/frontend/util/util.ts b/frontend/util/util.ts index 7bd20981ff..e5a7ab4cb9 100644 --- a/frontend/util/util.ts +++ b/frontend/util/util.ts @@ -1,6 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0s +import { PLATFORM } from "@/app/store/global"; import base64 from "base64-js"; import clsx from "clsx"; import { Atom, atom, Getter, SetStateAction, Setter, useAtomValue } from "jotai"; @@ -302,6 +303,29 @@ function makeConnRoute(conn: string): string { return "conn:" + conn; } +function makeNativeLabel(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 fileType: string; + if (isParent) { + fileType = "Parent Directory"; + } else if (isDirectory) { + fileType = "Directory"; + } else { + fileType = "File"; + } + return `Reveal ${fileType} in ${managerName}`; +} + export { atomWithDebounce, atomWithThrottle, @@ -321,6 +345,7 @@ export { makeConnRoute, makeExternLink, makeIconClass, + makeNativeLabel, stringToBase64, useAtomValueSafe, }; From 1e1d9853efc6ce4b5ef2db3a516b16c48e1b7509 Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Wed, 22 Jan 2025 00:10:35 -0800 Subject: [PATCH 2/9] fix: remove weird import issue Importing PLATFORM in util.ts broke the app. I am now passing it as an argument to avoid this. --- frontend/app/view/preview/directorypreview.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index 6251072873..3e2c20ef95 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -561,13 +561,13 @@ function TableBody({ }, // TODO: Only show this option for local files, resolve correct host path if connection is WSL { - label: makeNativeLabel(finfo.isdir, false), + label: makeNativeLabel(PLATFORM, finfo.isdir, false), click: () => { getApi().openNativePath(normPath); }, }, { - label: makeNativeLabel(true, true), + label: makeNativeLabel(PLATFORM, true, true), click: () => { getApi().openNativePath(finfo.dir); }, @@ -851,7 +851,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { }, // TODO: Only show this option for local files, resolve correct host path if connection is WSL { - label: makeNativeLabel(true, true), + label: makeNativeLabel(PLATFORM, true, true), click: () => { getApi().openNativePath(dirPath); }, From 2ff0eae388b3a84f5b1a6b2a2c128d69b18d5816 Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Wed, 22 Jan 2025 00:10:50 -0800 Subject: [PATCH 3/9] fix: remove weird import issue Importing PLATFORM in util.ts broke the app. I am now passing it as an argument to avoid this. --- frontend/util/util.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/util/util.ts b/frontend/util/util.ts index e5a7ab4cb9..7bf8f3e37c 100644 --- a/frontend/util/util.ts +++ b/frontend/util/util.ts @@ -1,7 +1,6 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0s -import { PLATFORM } from "@/app/store/global"; import base64 from "base64-js"; import clsx from "clsx"; import { Atom, atom, Getter, SetStateAction, Setter, useAtomValue } from "jotai"; @@ -303,13 +302,13 @@ function makeConnRoute(conn: string): string { return "conn:" + conn; } -function makeNativeLabel(isDirectory: boolean, isParent: boolean) { +function makeNativeLabel(platform: string, isDirectory: boolean, isParent: boolean) { let managerName: string; if (!isDirectory && !isParent) { managerName = "Default Application"; - } else if (PLATFORM == "darwin") { + } else if (platform == "darwin") { managerName = "Finder"; - } else if (PLATFORM == "win32") { + } else if (platform == "win32") { managerName = "Explorer"; } else { managerName = "File Manager"; From 5346b62fc1237b6d7865ffed5dcca75785325115 Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Wed, 22 Jan 2025 00:39:32 -0800 Subject: [PATCH 4/9] feat: add external open to preview block header This allows revealing directories and other files with external applications. --- frontend/app/view/preview/preview.tsx | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index 7add7e3ded..4b69c4766b 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -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"; @@ -674,6 +684,21 @@ export class PreviewModel implements ViewModel { await createBlock(termBlockDef); }), }); + menuItems.push({ + label: makeNativeLabel(PLATFORM, true, true), + click: async () => { + const fileInfo = await globalStore.get(this.statFile); + getApi().openNativePath(fileInfo.dir); + }, + }); + } else { + 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"); From cd30f9980c77b1ea4fd76bce8b167537a3e7c828 Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Wed, 22 Jan 2025 14:32:13 -0800 Subject: [PATCH 5/9] fix: use reveal for parent directory label --- frontend/util/util.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/util/util.ts b/frontend/util/util.ts index 7bf8f3e37c..07195b7321 100644 --- a/frontend/util/util.ts +++ b/frontend/util/util.ts @@ -314,15 +314,15 @@ function makeNativeLabel(platform: string, isDirectory: boolean, isParent: boole managerName = "File Manager"; } - let fileType: string; + let fileAction: string; if (isParent) { - fileType = "Parent Directory"; + fileAction = "Reveal"; } else if (isDirectory) { - fileType = "Directory"; + fileAction = "Open Directory"; } else { - fileType = "File"; + fileAction = "Open File"; } - return `Reveal ${fileType} in ${managerName}`; + return `${fileAction} in ${managerName}`; } export { From 39562f5ae17effbf8f2c96405991bf0a1c6d8822 Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Wed, 22 Jan 2025 14:33:25 -0800 Subject: [PATCH 6/9] fix: resolve parent directory for directory items While parent is trivial to compute for non-directories, it is slightly more involved for children that are also directories. This resolves that case using the RemoteFileJoinCommand --- frontend/app/view/preview/directorypreview.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index 3e2c20ef95..02833dff0c 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -6,9 +6,11 @@ import { Input } from "@/app/element/input"; import { ContextMenuModel } from "@/app/store/contextmenu"; import { PLATFORM, atoms, createBlock, getApi, globalStore } from "@/app/store/global"; import { FileService } from "@/app/store/services"; +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 { base64ToString, fireAndForget, isBlank, makeNativeLabel } from "@/util/util"; +import { base64ToString, fireAndForget, isBlank, makeConnRoute, makeNativeLabel } from "@/util/util"; import { offset, useDismiss, useFloating, useInteractions } from "@floating-ui/react"; import { Column, @@ -501,7 +503,7 @@ function TableBody({ }, [focusIndex]); const handleFileContextMenu = useCallback( - (e: any, finfo: FileInfo) => { + async (e: any, finfo: FileInfo) => { e.preventDefault(); e.stopPropagation(); if (finfo == null) { @@ -509,6 +511,15 @@ function TableBody({ } const normPath = getNormFilePath(finfo); const fileName = finfo.path.split("/").pop(); + 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[] = [ { label: "New File", @@ -569,7 +580,7 @@ function TableBody({ { label: makeNativeLabel(PLATFORM, true, true), click: () => { - getApi().openNativePath(finfo.dir); + getApi().openNativePath(parentFileInfo.dir); }, }, { From 4e708369a80b3c3512c4b45e1bb24256570915cc Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Wed, 22 Jan 2025 17:39:59 -0800 Subject: [PATCH 7/9] fix: missing import in merge --- frontend/app/view/preview/directorypreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index 360d791a7d..c662c1e320 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -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, From dd0d952cf9ef30486cae13a252f6a7d6157562f2 Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Wed, 22 Jan 2025 18:12:03 -0800 Subject: [PATCH 8/9] fix: correct connections in context menus --- frontend/app/view/preview/directorypreview.tsx | 3 +++ frontend/app/view/preview/preview.tsx | 2 ++ 2 files changed, 5 insertions(+) diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index 99239bae5f..8bb1c4bddd 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -599,6 +599,7 @@ function TableBody({ meta: { view: "preview", file: finfo.path, + connection: conn, }, }; await createBlock(blockDef); @@ -615,6 +616,7 @@ function TableBody({ controller: "shell", view: "term", "cmd:cwd": await model.formatRemoteUri(finfo.path, globalStore.get), + connection: conn, }, }; await createBlock(termBlockDef); @@ -900,6 +902,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { controller: "shell", view: "term", "cmd:cwd": dirPath, + connection: conn, }, }; await createBlock(termBlockDef); diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index 1bf7d1fb2e..9fd91d6869 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -687,12 +687,14 @@ 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); From 474305c102f90d26ff6640729d7d09bd631e4dc1 Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Wed, 22 Jan 2025 18:48:23 -0800 Subject: [PATCH 9/9] fix: exclude external open on remotes --- .../app/view/preview/directorypreview.tsx | 52 +++++++++---------- frontend/app/view/preview/preview.tsx | 38 +++++++++----- 2 files changed, 50 insertions(+), 40 deletions(-) diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index 8bb1c4bddd..a58a77328f 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -575,22 +575,6 @@ function TableBody({ { type: "separator", }, - // TODO: Only show this option for local files, 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); - }, - }, - { - type: "separator", - }, { label: "Open Preview in New Block", click: () => @@ -606,6 +590,26 @@ function TableBody({ }), }, ]; + 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", @@ -864,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", @@ -886,14 +884,16 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { { type: "separator", }, - // TODO: Only show this option for local files, resolve correct host path if connection is WSL - { + ]; + if (!conn) { + // TODO: resolve correct host path if connection is WSL + menu.push({ label: makeNativeLabel(PLATFORM, true, true), click: () => { getApi().openNativePath(dirPath); }, - }, - ]; + }); + } menu.push({ label: "Open Terminal in New Block", click: async () => { diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index 9fd91d6869..f51c707aaf 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -149,6 +149,7 @@ export class PreviewModel implements ViewModel { loadableStatFilePath: Atom>; loadableFileInfo: Atom>; connection: Atom>; + connectionImmediate: Atom; statFile: Atom>; fullFile: Atom>; fileMimeType: Atom>; @@ -374,6 +375,9 @@ export class PreviewModel implements ViewModel { } return connName; }); + this.connectionImmediate = atom((get) => { + return get(this.blockAtom)?.meta?.connection; + }); this.statFile = atom>(async (get) => { const fileName = get(this.metaFilePath); if (fileName == null) { @@ -700,21 +704,27 @@ export class PreviewModel implements ViewModel { await createBlock(termBlockDef); }), }); - menuItems.push({ - label: makeNativeLabel(PLATFORM, true, true), - click: async () => { - const fileInfo = await globalStore.get(this.statFile); - getApi().openNativePath(fileInfo.dir); - }, - }); + 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 { - menuItems.push({ - label: makeNativeLabel(PLATFORM, false, false), - click: async () => { - const fileInfo = await globalStore.get(this.statFile); - getApi().openNativePath(`${fileInfo.dir}/${fileInfo.name}`); - }, - }); + 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");