From 04c4f0a203805e5d5cd93a0d8a6c4a420f9c2ff2 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 3 Dec 2024 01:23:44 -0500 Subject: [PATCH] Create and rename files and dirs in dirpreview (#1156) New context menu options are available in the directory preview to create and rename files and directories It's missing three pieces of functionality, none of which are a regression: - Editing or creating an entry does not update the focused index. Focus index right now is pretty dumb, it doesn't factor in the column sorting so if you change that, the selected item will change to whatever is now at that index. We should update this so we use the actual file name to determine which element to focus and let the table determine which index to then highlight given the current sorting algo - Open in native preview should not be an option on remote connections with the exception of WSL, where it should resolve the file in the Windows filesystem, rather than the WSL one - We should catch CRUD errors in the dir preview and display a popup --- emain/emain.ts | 8 +- frontend/app/element/popover.scss | 22 +- frontend/app/element/popover.tsx | 97 +++--- frontend/app/store/services.ts | 9 + frontend/app/store/wshclientapi.ts | 15 + frontend/app/tab/workspaceswitcher.tsx | 6 +- .../app/view/preview/directorypreview.scss | 14 + .../app/view/preview/directorypreview.tsx | 288 ++++++++++++++++-- pkg/service/fileservice/fileservice.go | 27 ++ pkg/wshrpc/wshclient/wshclient.go | 18 ++ pkg/wshrpc/wshremote/wshremote.go | 43 +++ pkg/wshrpc/wshrpctypes.go | 5 + 12 files changed, 461 insertions(+), 91 deletions(-) diff --git a/emain/emain.ts b/emain/emain.ts index 5535b8864..77cbc8f54 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -359,9 +359,11 @@ electron.ipcMain.on("quicklook", (event, filePath: string) => { electron.ipcMain.on("open-native-path", (event, filePath: string) => { console.log("open-native-path", filePath); - electron.shell.openPath(filePath).catch((err) => { - console.error(`Failed to open path ${filePath}:`, err); - }); + fireAndForget(async () => + electron.shell.openPath(filePath).then((excuse) => { + if (excuse) console.error(`Failed to open ${filePath} in native application: ${excuse}`); + }) + ); }); async function createNewWaveWindow(): Promise { diff --git a/frontend/app/element/popover.scss b/frontend/app/element/popover.scss index 32af28ef3..bd4217653 100644 --- a/frontend/app/element/popover.scss +++ b/frontend/app/element/popover.scss @@ -2,15 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 .popover-content { - min-width: 100px; - min-height: 150px; - position: absolute; - z-index: 1000; // TODO: put this in theme.scss - display: flex; - padding: 2px; - gap: 1px; - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.15); - background: #212121; - box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3); + min-width: 100px; + min-height: 150px; + position: absolute; + z-index: 1000; // TODO: put this in theme.scss + display: flex; + padding: 2px; + gap: 1px; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.15); + background: #212121; + box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3); } diff --git a/frontend/app/element/popover.tsx b/frontend/app/element/popover.tsx index cac445981..587ade0aa 100644 --- a/frontend/app/element/popover.tsx +++ b/frontend/app/element/popover.tsx @@ -5,6 +5,7 @@ import { Button } from "@/element/button"; import { autoUpdate, FloatingPortal, + Middleware, offset as offsetMiddleware, useClick, useDismiss, @@ -34,6 +35,7 @@ interface PopoverProps { placement?: Placement; offset?: OffsetOptions; onDismiss?: () => void; + middleware?: Middleware[]; } const isPopoverButton = ( @@ -48,54 +50,63 @@ const isPopoverContent = ( return element.type === PopoverContent; }; -const Popover = memo(({ children, className, placement = "bottom-start", offset = 3, onDismiss }: PopoverProps) => { - const [isOpen, setIsOpen] = useState(false); +const Popover = memo( + ({ children, className, placement = "bottom-start", offset = 3, onDismiss, middleware }: PopoverProps) => { + const [isOpen, setIsOpen] = useState(false); - const handleOpenChange = (open: boolean) => { - setIsOpen(open); - if (!open && onDismiss) { - onDismiss(); - } - }; - - const { refs, floatingStyles, context } = useFloating({ - placement, - open: isOpen, - onOpenChange: handleOpenChange, - middleware: [offsetMiddleware(offset)], - whileElementsMounted: autoUpdate, - }); - - const click = useClick(context); - const dismiss = useDismiss(context); - const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]); - - const renderChildren = Children.map(children, (child) => { - if (isValidElement(child)) { - if (isPopoverButton(child)) { - return cloneElement(child as any, { - isActive: isOpen, - ref: refs.setReference, - getReferenceProps, - // Do not overwrite onClick - }); + const handleOpenChange = (open: boolean) => { + setIsOpen(open); + if (!open && onDismiss) { + onDismiss(); } + }; - if (isPopoverContent(child)) { - return isOpen - ? cloneElement(child as any, { - ref: refs.setFloating, - style: floatingStyles, - getFloatingProps, - }) - : null; - } + if (offset === undefined) { + offset = 3; } - return child; - }); - return
{renderChildren}
; -}); + middleware ??= []; + middleware.push(offsetMiddleware(offset)); + + const { refs, floatingStyles, context } = useFloating({ + placement, + open: isOpen, + onOpenChange: handleOpenChange, + middleware: middleware, + whileElementsMounted: autoUpdate, + }); + + const click = useClick(context); + const dismiss = useDismiss(context); + const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]); + + const renderChildren = Children.map(children, (child) => { + if (isValidElement(child)) { + if (isPopoverButton(child)) { + return cloneElement(child as any, { + isActive: isOpen, + ref: refs.setReference, + getReferenceProps, + // Do not overwrite onClick + }); + } + + if (isPopoverContent(child)) { + return isOpen + ? cloneElement(child as any, { + ref: refs.setFloating, + style: floatingStyles, + getFloatingProps, + }) + : null; + } + } + return child; + }); + + return
{renderChildren}
; + } +); interface PopoverButtonProps extends React.ButtonHTMLAttributes { isActive?: boolean; diff --git a/frontend/app/store/services.ts b/frontend/app/store/services.ts index 4b7714701..5abbaef9d 100644 --- a/frontend/app/store/services.ts +++ b/frontend/app/store/services.ts @@ -59,11 +59,17 @@ class FileServiceType { GetWaveFile(arg1: string, arg2: string): Promise { return WOS.callBackendService("file", "GetWaveFile", Array.from(arguments)) } + Mkdir(arg1: string, arg2: string): Promise { + return WOS.callBackendService("file", "Mkdir", Array.from(arguments)) + } // read file ReadFile(connection: string, path: string): Promise { return WOS.callBackendService("file", "ReadFile", Array.from(arguments)) } + Rename(arg1: string, arg2: string, arg3: string): Promise { + return WOS.callBackendService("file", "Rename", Array.from(arguments)) + } // save file SaveFile(connection: string, path: string, data64: string): Promise { @@ -74,6 +80,9 @@ class FileServiceType { StatFile(connection: string, path: string): Promise { return WOS.callBackendService("file", "StatFile", Array.from(arguments)) } + TouchFile(arg1: string, arg2: string): Promise { + return WOS.callBackendService("file", "TouchFile", Array.from(arguments)) + } } export const FileService = new FileServiceType(); diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index 244349289..e6142aeee 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -212,6 +212,21 @@ class RpcApiType { return client.wshRpcCall("remotefilejoin", data, opts); } + // command "remotefilerename" [call] + RemoteFileRenameCommand(client: WshClient, data: string[], opts?: RpcOpts): Promise { + return client.wshRpcCall("remotefilerename", data, opts); + } + + // command "remotefiletouch" [call] + RemoteFileTouchCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { + return client.wshRpcCall("remotefiletouch", data, opts); + } + + // command "remotemkdir" [call] + RemoteMkdirCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { + return client.wshRpcCall("remotemkdir", data, opts); + } + // command "remotestreamcpudata" [responsestream] RemoteStreamCpuDataCommand(client: WshClient, opts?: RpcOpts): AsyncGenerator { return client.wshRpcStream("remotestreamcpudata", null, opts); diff --git a/frontend/app/tab/workspaceswitcher.tsx b/frontend/app/tab/workspaceswitcher.tsx index 606436dbe..fc513f91c 100644 --- a/frontend/app/tab/workspaceswitcher.tsx +++ b/frontend/app/tab/workspaceswitcher.tsx @@ -207,7 +207,11 @@ const WorkspaceSwitcher = () => { }; return ( - setEditingWorkspace(null)}> + setEditingWorkspace(null)} + > { + updateName: (path: string) => void; + newFile: () => void; + newDirectory: () => void; + } +} + interface DirectoryTableProps { model: PreviewModel; data: FileInfo[]; @@ -35,6 +46,9 @@ interface DirectoryTableProps { setSearch: (_: string) => void; setSelectedPath: (_: string) => void; setRefreshVersion: React.Dispatch>; + entryManagerOverlayPropsAtom: PrimitiveAtom; + newFile: () => void; + newDirectory: () => void; } const columnHelper = createColumnHelper(); @@ -121,6 +135,46 @@ function cleanMimetype(input: string): string { return truncated.trim(); } +enum EntryManagerType { + NewFile = "New File", + NewDirectory = "New Folder", + EditName = "Rename", +} + +type EntryManagerOverlayProps = { + forwardRef?: React.Ref; + entryManagerType: EntryManagerType; + startingValue?: string; + onSave: (newValue: string) => void; + style?: React.CSSProperties; + getReferenceProps?: () => any; +}; + +const EntryManagerOverlay = memo( + ({ entryManagerType, startingValue, onSave, forwardRef, style, getReferenceProps }: EntryManagerOverlayProps) => { + const [value, setValue] = useState(startingValue); + return ( +
+
{entryManagerType}
+
+ { + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + onSave(value); + } + }} + > +
+
+ ); + } +); + function DirectoryTable({ model, data, @@ -130,6 +184,9 @@ function DirectoryTable({ setSearch, setSelectedPath, setRefreshVersion, + entryManagerOverlayPropsAtom, + newFile, + newDirectory, }: DirectoryTableProps) { const fullConfig = useAtomValue(atoms.fullConfigAtom); const getIconFromMimeType = useCallback( @@ -205,6 +262,28 @@ function DirectoryTable({ [fullConfig] ); + const setEntryManagerProps = useSetAtom(entryManagerOverlayPropsAtom); + + const updateName = useCallback((path: string) => { + const fileName = path.split("/").at(-1); + setEntryManagerProps({ + entryManagerType: EntryManagerType.EditName, + startingValue: fileName, + onSave: (newName: string) => { + let newPath: string; + if (newName !== fileName) { + newPath = path.replace(fileName, newName); + console.log(`replacing ${fileName} with ${newName}: ${path}`); + fireAndForget(async () => { + await FileService.Rename(globalStore.get(model.connection), path, newPath); + model.refreshCallback(); + }); + } + setEntryManagerProps(undefined); + }, + }); + }, []); + const table = useReactTable({ data, columns, @@ -229,6 +308,11 @@ function DirectoryTable({ }, enableMultiSort: false, enableSortingRemoval: false, + meta: { + updateName, + newFile, + newDirectory, + }, }); useEffect(() => { @@ -418,6 +502,27 @@ function TableBody({ openNativeLabel = "Open File in Default Application"; } const menu: ContextMenuItem[] = [ + { + label: "New File", + click: () => { + table.options.meta.newFile(); + }, + }, + { + label: "New Folder", + click: () => { + table.options.meta.newDirectory(); + }, + }, + { + label: "Rename", + click: () => { + table.options.meta.updateName(finfo.path); + }, + }, + { + type: "separator", + }, { label: "Copy File Name", click: () => navigator.clipboard.writeText(fileName), @@ -446,6 +551,7 @@ function TableBody({ { type: "separator", }, + // TODO: Only show this option for local files, resolve correct host path if connection is WSL { label: openNativeLabel, click: async () => { @@ -483,14 +589,18 @@ function TableBody({ }, }); } - menu.push({ type: "separator" }); - menu.push({ - label: "Delete File", - click: async () => { - await FileService.DeleteFile(conn, finfo.path).catch((e) => console.log(e)); - setRefreshVersion((current) => current + 1); + menu.push( + { + type: "separator", }, - }); + { + label: "Delete", + click: async () => { + await FileService.DeleteFile(conn, finfo.path).catch((e) => console.log(e)); + setRefreshVersion((current) => current + 1); + }, + } + ); ContextMenuModel.showContextMenu(menu, e); }, [setRefreshVersion, conn] @@ -560,12 +670,12 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { const [focusIndex, setFocusIndex] = useState(0); const [unfilteredData, setUnfilteredData] = useState([]); const [filteredData, setFilteredData] = useState([]); - const fileName = useAtomValue(model.metaFilePath); const showHiddenFiles = useAtomValue(model.showHiddenFiles); const [selectedPath, setSelectedPath] = useState(""); const [refreshVersion, setRefreshVersion] = useAtom(model.refreshVersion); const conn = useAtomValue(model.connection); const blockData = useAtomValue(model.blockAtom); + const dirPath = useAtomValue(model.normFilePath); useEffect(() => { model.refreshCallback = () => { @@ -578,13 +688,13 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { useEffect(() => { const getContent = async () => { - const file = await FileService.ReadFile(conn, fileName); + const file = await FileService.ReadFile(conn, dirPath); const serializedContent = base64ToString(file?.data64); const content: FileInfo[] = JSON.parse(serializedContent); setUnfilteredData(content); }; getContent(); - }, [conn, fileName, refreshVersion]); + }, [conn, dirPath, refreshVersion]); useEffect(() => { const filtered = unfilteredData.filter((fileInfo) => { @@ -652,26 +762,138 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { } }, [filteredData]); + const entryManagerPropsAtom = useState( + atom(null) as PrimitiveAtom + )[0]; + const [entryManagerProps, setEntryManagerProps] = useAtom(entryManagerPropsAtom); + + const { refs, floatingStyles, context } = useFloating({ + open: !!entryManagerProps, + onOpenChange: () => setEntryManagerProps(undefined), + middleware: [offset(({ rects }) => -rects.reference.height / 2 - rects.floating.height / 2)], + }); + + const dismiss = useDismiss(context); + const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]); + + const newFile = useCallback(() => { + setEntryManagerProps({ + entryManagerType: EntryManagerType.NewFile, + onSave: (newName: string) => { + console.log(`newFile: ${newName}`); + fireAndForget(async () => { + await FileService.TouchFile(globalStore.get(model.connection), `${dirPath}/${newName}`); + model.refreshCallback(); + }); + setEntryManagerProps(undefined); + }, + }); + }, [dirPath]); + const newDirectory = useCallback(() => { + setEntryManagerProps({ + entryManagerType: EntryManagerType.NewDirectory, + onSave: (newName: string) => { + console.log(`newDirectory: ${newName}`); + fireAndForget(async () => { + await FileService.Mkdir(globalStore.get(model.connection), `${dirPath}/${newName}`); + model.refreshCallback(); + }); + setEntryManagerProps(undefined); + }, + }); + }, [dirPath]); + + const handleFileContextMenu = useCallback( + (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", + click: () => { + newFile(); + }, + }, + { + label: "New Folder", + click: () => { + newDirectory(); + }, + }, + { + type: "separator", + }, + // TODO: Only show this option for local files, resolve correct host path if connection is WSL + { + label: openNativeLabel, + click: () => { + console.log(`opening ${dirPath}`); + getApi().openNativePath(dirPath); + }, + }, + ]; + menu.push({ + label: "Open Terminal in New Block", + click: async () => { + const termBlockDef: BlockDef = { + meta: { + controller: "shell", + view: "term", + "cmd:cwd": dirPath, + }, + }; + await createBlock(termBlockDef); + }, + }); + + ContextMenuModel.showContextMenu(menu, e); + }, + [setRefreshVersion, conn, newFile, newDirectory, dirPath] + ); + return ( -
{ - const event = e as React.ChangeEvent; - setSearchText(event.target.value.toLowerCase()); - }} - // onFocusCapture={() => document.getSelection().collapseToEnd()} - > - -
+ +
{ + const event = e as React.ChangeEvent; + if (!entryManagerProps) { + setSearchText(event.target.value.toLowerCase()); + } + }} + {...getReferenceProps()} + onContextMenu={(e) => handleFileContextMenu(e)} + > + +
+ {entryManagerProps && ( + + )} +
); } diff --git a/pkg/service/fileservice/fileservice.go b/pkg/service/fileservice/fileservice.go index b9c1cf662..68490a2f4 100644 --- a/pkg/service/fileservice/fileservice.go +++ b/pkg/service/fileservice/fileservice.go @@ -61,6 +61,33 @@ func (fs *FileService) StatFile(connection string, path string) (*wshrpc.FileInf return wshclient.RemoteFileInfoCommand(client, path, &wshrpc.RpcOpts{Route: connRoute}) } +func (fs *FileService) Mkdir(connection string, path string) error { + if connection == "" { + connection = wshrpc.LocalConnName + } + connRoute := wshutil.MakeConnectionRouteId(connection) + client := wshserver.GetMainRpcClient() + return wshclient.RemoteMkdirCommand(client, path, &wshrpc.RpcOpts{Route: connRoute}) +} + +func (fs *FileService) TouchFile(connection string, path string) error { + if connection == "" { + connection = wshrpc.LocalConnName + } + connRoute := wshutil.MakeConnectionRouteId(connection) + client := wshserver.GetMainRpcClient() + return wshclient.RemoteFileTouchCommand(client, path, &wshrpc.RpcOpts{Route: connRoute}) +} + +func (fs *FileService) Rename(connection string, path string, newPath string) error { + if connection == "" { + connection = wshrpc.LocalConnName + } + connRoute := wshutil.MakeConnectionRouteId(connection) + client := wshserver.GetMainRpcClient() + return wshclient.RemoteFileRenameCommand(client, [2]string{path, newPath}, &wshrpc.RpcOpts{Route: connRoute}) +} + func (fs *FileService) ReadFile_Meta() tsgenmeta.MethodMeta { return tsgenmeta.MethodMeta{ Desc: "read file", diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index 88635130f..30f81fe8c 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -259,6 +259,24 @@ func RemoteFileJoinCommand(w *wshutil.WshRpc, data []string, opts *wshrpc.RpcOpt return resp, err } +// command "remotefilerename", wshserver.RemoteFileRenameCommand +func RemoteFileRenameCommand(w *wshutil.WshRpc, data [2]string, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "remotefilerename", data, opts) + return err +} + +// command "remotefiletouch", wshserver.RemoteFileTouchCommand +func RemoteFileTouchCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "remotefiletouch", data, opts) + return err +} + +// command "remotemkdir", wshserver.RemoteMkdirCommand +func RemoteMkdirCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "remotemkdir", data, opts) + return err +} + // command "remotestreamcpudata", wshserver.RemoteStreamCpuDataCommand func RemoteStreamCpuDataCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[wshrpc.TimeSeriesData] { return sendRpcRequestResponseStreamHelper[wshrpc.TimeSeriesData](w, "remotestreamcpudata", nil, opts) diff --git a/pkg/wshrpc/wshremote/wshremote.go b/pkg/wshrpc/wshremote/wshremote.go index 0cd6b637c..72df33852 100644 --- a/pkg/wshrpc/wshremote/wshremote.go +++ b/pkg/wshrpc/wshremote/wshremote.go @@ -307,6 +307,49 @@ func (impl *ServerImpl) RemoteFileInfoCommand(ctx context.Context, path string) return impl.fileInfoInternal(path, true) } +func (impl *ServerImpl) RemoteFileTouchCommand(ctx context.Context, path string) error { + cleanedPath := filepath.Clean(wavebase.ExpandHomeDirSafe(path)) + if _, err := os.Stat(cleanedPath); err == nil { + return fmt.Errorf("file %q already exists", path) + } + if err := os.MkdirAll(filepath.Dir(cleanedPath), 0755); err != nil { + return fmt.Errorf("cannot create directory %q: %w", filepath.Dir(cleanedPath), err) + } + if err := os.WriteFile(cleanedPath, []byte{}, 0644); err != nil { + return fmt.Errorf("cannot create file %q: %w", cleanedPath, err) + } + return nil +} + +func (impl *ServerImpl) RemoteFileRenameCommand(ctx context.Context, pathTuple [2]string) error { + path := pathTuple[0] + newPath := pathTuple[1] + cleanedPath := filepath.Clean(wavebase.ExpandHomeDirSafe(path)) + cleanedNewPath := filepath.Clean(wavebase.ExpandHomeDirSafe(newPath)) + if _, err := os.Stat(cleanedNewPath); err == nil { + return fmt.Errorf("destination file path %q already exists", path) + } + if err := os.Rename(cleanedPath, cleanedNewPath); err != nil { + return fmt.Errorf("cannot rename file %q to %q: %w", cleanedPath, cleanedNewPath, err) + } + return nil +} + +func (impl *ServerImpl) RemoteMkdirCommand(ctx context.Context, path string) error { + cleanedPath := filepath.Clean(wavebase.ExpandHomeDirSafe(path)) + if stat, err := os.Stat(cleanedPath); err == nil { + if stat.IsDir() { + return fmt.Errorf("directory %q already exists", path) + } else { + return fmt.Errorf("cannot create directory %q, file exists at path", path) + } + } + if err := os.MkdirAll(cleanedPath, 0755); err != nil { + return fmt.Errorf("cannot create directory %q: %w", cleanedPath, err) + } + return nil +} + func (*ServerImpl) RemoteWriteFileCommand(ctx context.Context, data wshrpc.CommandRemoteWriteFileData) error { path, err := wavebase.ExpandHomeDir(data.Path) if err != nil { diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 4f0992f11..517ef33e0 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -61,6 +61,7 @@ const ( Command_Test = "test" Command_RemoteStreamFile = "remotestreamfile" Command_RemoteFileInfo = "remotefileinfo" + Command_RemoteFileTouch = "remotefiletouch" Command_RemoteWriteFile = "remotewritefile" Command_RemoteFileDelete = "remotefiledelete" Command_RemoteFileJoin = "remotefilejoin" @@ -69,6 +70,7 @@ const ( Command_Activity = "activity" Command_GetVar = "getvar" Command_SetVar = "setvar" + Command_RemoteMkdir = "remotemkdir" Command_ConnStatus = "connstatus" Command_WslStatus = "wslstatus" @@ -161,9 +163,12 @@ type WshRpcInterface interface { // remotes RemoteStreamFileCommand(ctx context.Context, data CommandRemoteStreamFileData) chan RespOrErrorUnion[CommandRemoteStreamFileRtnData] RemoteFileInfoCommand(ctx context.Context, path string) (*FileInfo, error) + RemoteFileTouchCommand(ctx context.Context, path string) error + RemoteFileRenameCommand(ctx context.Context, pathTuple [2]string) error RemoteFileDeleteCommand(ctx context.Context, path string) error RemoteWriteFileCommand(ctx context.Context, data CommandRemoteWriteFileData) error RemoteFileJoinCommand(ctx context.Context, paths []string) (*FileInfo, error) + RemoteMkdirCommand(ctx context.Context, path string) error RemoteStreamCpuDataCommand(ctx context.Context) chan RespOrErrorUnion[TimeSeriesData] // emain