Merge branch 'main' into red/tabs-new-design-2

This commit is contained in:
Red J Adaya 2024-12-04 22:01:59 +08:00 committed by GitHub
commit 1b8ac7f8bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 612 additions and 172 deletions

View File

@ -274,6 +274,7 @@ export class WaveBrowserWindow extends BaseWindow {
console.log("switchWorkspace newWs", newWs); console.log("switchWorkspace newWs", newWs);
if (this.allTabViews.size) { if (this.allTabViews.size) {
for (const tab of this.allTabViews.values()) { for (const tab of this.allTabViews.values()) {
this.contentView.removeChildView(tab);
tab?.destroy(); tab?.destroy();
} }
} }

View File

@ -39,6 +39,7 @@ import {
getAllWaveWindows, getAllWaveWindows,
getWaveWindowById, getWaveWindowById,
getWaveWindowByWebContentsId, getWaveWindowByWebContentsId,
getWaveWindowByWorkspaceId,
WaveBrowserWindow, WaveBrowserWindow,
} from "./emain-window"; } from "./emain-window";
import { ElectronWshClient, initElectronWshClient } from "./emain-wsh"; import { ElectronWshClient, initElectronWshClient } from "./emain-wsh";
@ -125,6 +126,14 @@ function handleWSEvent(evtMsg: WSEventType) {
if (ww != null) { if (ww != null) {
ww.destroy(); // bypass the "are you sure?" dialog ww.destroy(); // bypass the "are you sure?" dialog
} }
} else if (evtMsg.eventtype == "electron:updateactivetab") {
const activeTabUpdate: { workspaceid: string; newactivetabid: string } = evtMsg.data;
console.log("electron:updateactivetab", activeTabUpdate);
const ww = getWaveWindowByWorkspaceId(activeTabUpdate.workspaceid);
if (ww == null) {
return;
}
await ww.setActiveTab(activeTabUpdate.newactivetabid, false);
} else { } else {
console.log("unhandled electron ws eventtype", evtMsg.eventtype); console.log("unhandled electron ws eventtype", evtMsg.eventtype);
} }
@ -359,9 +368,11 @@ electron.ipcMain.on("quicklook", (event, filePath: string) => {
electron.ipcMain.on("open-native-path", (event, filePath: string) => { electron.ipcMain.on("open-native-path", (event, filePath: string) => {
console.log("open-native-path", filePath); console.log("open-native-path", filePath);
electron.shell.openPath(filePath).catch((err) => { fireAndForget(async () =>
console.error(`Failed to open path ${filePath}:`, err); electron.shell.openPath(filePath).then((excuse) => {
}); if (excuse) console.error(`Failed to open ${filePath} in native application: ${excuse}`);
})
);
}); });
async function createNewWaveWindow(): Promise<void> { async function createNewWaveWindow(): Promise<void> {

View File

@ -2,15 +2,15 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
.popover-content { .popover-content {
min-width: 100px; min-width: 100px;
min-height: 150px; min-height: 150px;
position: absolute; position: absolute;
z-index: 1000; // TODO: put this in theme.scss z-index: 1000; // TODO: put this in theme.scss
display: flex; display: flex;
padding: 2px; padding: 2px;
gap: 1px; gap: 1px;
border-radius: 4px; border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.15); border: 1px solid rgba(255, 255, 255, 0.15);
background: #212121; background: #212121;
box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3); box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3);
} }

View File

@ -5,6 +5,7 @@ import { Button } from "@/element/button";
import { import {
autoUpdate, autoUpdate,
FloatingPortal, FloatingPortal,
Middleware,
offset as offsetMiddleware, offset as offsetMiddleware,
useClick, useClick,
useDismiss, useDismiss,
@ -34,6 +35,7 @@ interface PopoverProps {
placement?: Placement; placement?: Placement;
offset?: OffsetOptions; offset?: OffsetOptions;
onDismiss?: () => void; onDismiss?: () => void;
middleware?: Middleware[];
} }
const isPopoverButton = ( const isPopoverButton = (

View File

@ -557,6 +557,7 @@ function getConnStatusAtom(conn: string): PrimitiveAtom<ConnStatus> {
status: "connected", status: "connected",
hasconnected: true, hasconnected: true,
activeconnnum: 0, activeconnnum: 0,
wshenabled: false,
}; };
rtn = atom(connStatus); rtn = atom(connStatus);
} else { } else {
@ -567,6 +568,7 @@ function getConnStatusAtom(conn: string): PrimitiveAtom<ConnStatus> {
status: "disconnected", status: "disconnected",
hasconnected: false, hasconnected: false,
activeconnnum: 0, activeconnnum: 0,
wshenabled: false,
}; };
rtn = atom(connStatus); rtn = atom(connStatus);
} }
@ -620,8 +622,13 @@ function removeNotification(id: string) {
}); });
} }
async function createTab(): Promise<void> { function createTab() {
await getApi().createTab(); getApi().createTab();
}
function setActiveTab(tabId: string) {
document.body.classList.add("nohover");
getApi().setActiveTab(tabId);
} }
export { export {
@ -655,6 +662,7 @@ export {
removeFlashError, removeFlashError,
removeNotification, removeNotification,
removeNotificationById, removeNotificationById,
setActiveTab,
setNodeFocus, setNodeFocus,
setPlatform, setPlatform,
subscribeToConnEvents, subscribeToConnEvents,

View File

@ -59,11 +59,17 @@ class FileServiceType {
GetWaveFile(arg1: string, arg2: string): Promise<any> { GetWaveFile(arg1: string, arg2: string): Promise<any> {
return WOS.callBackendService("file", "GetWaveFile", Array.from(arguments)) return WOS.callBackendService("file", "GetWaveFile", Array.from(arguments))
} }
Mkdir(arg1: string, arg2: string): Promise<void> {
return WOS.callBackendService("file", "Mkdir", Array.from(arguments))
}
// read file // read file
ReadFile(connection: string, path: string): Promise<FullFile> { ReadFile(connection: string, path: string): Promise<FullFile> {
return WOS.callBackendService("file", "ReadFile", Array.from(arguments)) return WOS.callBackendService("file", "ReadFile", Array.from(arguments))
} }
Rename(arg1: string, arg2: string, arg3: string): Promise<void> {
return WOS.callBackendService("file", "Rename", Array.from(arguments))
}
// save file // save file
SaveFile(connection: string, path: string, data64: string): Promise<void> { SaveFile(connection: string, path: string, data64: string): Promise<void> {
@ -74,6 +80,9 @@ class FileServiceType {
StatFile(connection: string, path: string): Promise<FileInfo> { StatFile(connection: string, path: string): Promise<FileInfo> {
return WOS.callBackendService("file", "StatFile", Array.from(arguments)) return WOS.callBackendService("file", "StatFile", Array.from(arguments))
} }
TouchFile(arg1: string, arg2: string): Promise<void> {
return WOS.callBackendService("file", "TouchFile", Array.from(arguments))
}
} }
export const FileService = new FileServiceType(); export const FileService = new FileServiceType();

View File

@ -212,6 +212,21 @@ class RpcApiType {
return client.wshRpcCall("remotefilejoin", data, opts); return client.wshRpcCall("remotefilejoin", data, opts);
} }
// command "remotefilerename" [call]
RemoteFileRenameCommand(client: WshClient, data: string[], opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("remotefilerename", data, opts);
}
// command "remotefiletouch" [call]
RemoteFileTouchCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("remotefiletouch", data, opts);
}
// command "remotemkdir" [call]
RemoteMkdirCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("remotemkdir", data, opts);
}
// command "remotestreamcpudata" [responsestream] // command "remotestreamcpudata" [responsestream]
RemoteStreamCpuDataCommand(client: WshClient, opts?: RpcOpts): AsyncGenerator<TimeSeriesData, void, boolean> { RemoteStreamCpuDataCommand(client: WshClient, opts?: RpcOpts): AsyncGenerator<TimeSeriesData, void, boolean> {
return client.wshRpcStream("remotestreamcpudata", null, opts); return client.wshRpcStream("remotestreamcpudata", null, opts);

View File

@ -33,13 +33,6 @@
border-radius: 6px; border-radius: 6px;
} }
&:hover {
.tab-inner {
border-color: transparent;
background: rgb(from var(--main-text-color) r g b / 0.07);
}
}
&.animate { &.animate {
transition: transition:
transform 0.3s ease, transform 0.3s ease,
@ -87,19 +80,20 @@
} }
.close { .close {
visibility: hidden; visibility: hidden;
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 4px; right: 4px;
transform: translate3d(0, -50%, 0); transform: translate3d(0, -50%, 0);
width: 20px; width: 20px;
height: 20px; height: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
z-index: var(--zindex-tab-name); z-index: var(--zindex-tab-name);
padding: 1px 2px; padding: 1px 2px;
transition: none !important;
} }
&:hover .close { &:hover .close {
@ -112,6 +106,20 @@
} }
} }
body:not(.nohover) .tab:hover {
.tab-inner {
border-color: transparent;
background: rgb(from var(--main-text-color) r g b / 0.07);
}
.close {
visibility: visible;
&:hover {
color: var(--main-text-color);
}
}
}
@keyframes expandWidthAndFadeIn { @keyframes expandWidthAndFadeIn {
from { from {
width: var(--initial-tab-width); width: var(--initial-tab-width);

View File

@ -5,7 +5,7 @@ import { Button } from "@/app/element/button";
import { modalsModel } from "@/app/store/modalmodel"; import { modalsModel } from "@/app/store/modalmodel";
import { WindowDrag } from "@/element/windowdrag"; import { WindowDrag } from "@/element/windowdrag";
import { deleteLayoutModelForTab } from "@/layout/index"; import { deleteLayoutModelForTab } from "@/layout/index";
import { atoms, createTab, getApi, isDev, PLATFORM } from "@/store/global"; import { atoms, createTab, getApi, isDev, PLATFORM, setActiveTab } from "@/store/global";
import { fireAndForget } from "@/util/util"; import { fireAndForget } from "@/util/util";
import { useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom } from "jotai";
import { OverlayScrollbars } from "overlayscrollbars"; import { OverlayScrollbars } from "overlayscrollbars";
@ -500,7 +500,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
const handleSelectTab = (tabId: string) => { const handleSelectTab = (tabId: string) => {
if (!draggingTabDataRef.current.dragged) { if (!draggingTabDataRef.current.dragged) {
getApi().setActiveTab(tabId); setActiveTab(tabId);
} }
}; };

View File

@ -207,7 +207,7 @@ const WorkspaceSwitcher = forwardRef<HTMLDivElement, {}>(({}, ref) => {
}; };
return ( return (
<Popover ref={ref} className="workspace-switcher-popover" onDismiss={() => setEditingWorkspace(null)}> <Popover ref={ref} className="workspace-switcher-popover" placement="bottom-start" onDismiss={() => setEditingWorkspace(null)}>
<PopoverButton <PopoverButton
className="workspace-switcher-button grey" className="workspace-switcher-button grey"
as="div" as="div"

View File

@ -230,3 +230,17 @@
background-color: var(--highlight-bg-color); background-color: var(--highlight-bg-color);
} }
} }
.entry-manager-overlay {
display: flex;
flex-direction: column;
max-width: 90%;
max-height: fit-content;
display: flex;
padding: 10px;
gap: 10px;
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);
}

View File

@ -1,15 +1,18 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Input } from "@/app/element/input";
import { ContextMenuModel } from "@/app/store/contextmenu"; import { ContextMenuModel } from "@/app/store/contextmenu";
import { PLATFORM, atoms, createBlock, getApi } from "@/app/store/global"; import { PLATFORM, atoms, createBlock, getApi, globalStore } from "@/app/store/global";
import { FileService } from "@/app/store/services"; import { FileService } from "@/app/store/services";
import type { PreviewModel } from "@/app/view/preview/preview"; import type { PreviewModel } from "@/app/view/preview/preview";
import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil"; import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil";
import { base64ToString, isBlank } from "@/util/util"; import { base64ToString, fireAndForget, isBlank } from "@/util/util";
import { offset, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
import { import {
Column, Column,
Row, Row,
RowData,
Table, Table,
createColumnHelper, createColumnHelper,
flexRender, flexRender,
@ -19,13 +22,21 @@ import {
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import clsx from "clsx"; import clsx from "clsx";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useAtom, useAtomValue } from "jotai"; import { PrimitiveAtom, atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react"; import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import React, { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { quote as shellQuote } from "shell-quote"; import { quote as shellQuote } from "shell-quote";
import { debounce } from "throttle-debounce"; import { debounce } from "throttle-debounce";
import "./directorypreview.scss"; import "./directorypreview.scss";
declare module "@tanstack/react-table" {
interface TableMeta<TData extends RowData> {
updateName: (path: string) => void;
newFile: () => void;
newDirectory: () => void;
}
}
interface DirectoryTableProps { interface DirectoryTableProps {
model: PreviewModel; model: PreviewModel;
data: FileInfo[]; data: FileInfo[];
@ -35,6 +46,9 @@ interface DirectoryTableProps {
setSearch: (_: string) => void; setSearch: (_: string) => void;
setSelectedPath: (_: string) => void; setSelectedPath: (_: string) => void;
setRefreshVersion: React.Dispatch<React.SetStateAction<number>>; setRefreshVersion: React.Dispatch<React.SetStateAction<number>>;
entryManagerOverlayPropsAtom: PrimitiveAtom<EntryManagerOverlayProps>;
newFile: () => void;
newDirectory: () => void;
} }
const columnHelper = createColumnHelper<FileInfo>(); const columnHelper = createColumnHelper<FileInfo>();
@ -121,6 +135,46 @@ function cleanMimetype(input: string): string {
return truncated.trim(); return truncated.trim();
} }
enum EntryManagerType {
NewFile = "New File",
NewDirectory = "New Folder",
EditName = "Rename",
}
type EntryManagerOverlayProps = {
forwardRef?: React.Ref<HTMLDivElement>;
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 (
<div className="entry-manager-overlay" ref={forwardRef} style={style} {...getReferenceProps()}>
<div className="entry-manager-type">{entryManagerType}</div>
<div className="entry-manager-input">
<Input
value={value}
onChange={setValue}
autoFocus={true}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
onSave(value);
}
}}
></Input>
</div>
</div>
);
}
);
function DirectoryTable({ function DirectoryTable({
model, model,
data, data,
@ -130,6 +184,9 @@ function DirectoryTable({
setSearch, setSearch,
setSelectedPath, setSelectedPath,
setRefreshVersion, setRefreshVersion,
entryManagerOverlayPropsAtom,
newFile,
newDirectory,
}: DirectoryTableProps) { }: DirectoryTableProps) {
const fullConfig = useAtomValue(atoms.fullConfigAtom); const fullConfig = useAtomValue(atoms.fullConfigAtom);
const getIconFromMimeType = useCallback( const getIconFromMimeType = useCallback(
@ -205,6 +262,28 @@ function DirectoryTable({
[fullConfig] [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({ const table = useReactTable({
data, data,
columns, columns,
@ -229,6 +308,11 @@ function DirectoryTable({
}, },
enableMultiSort: false, enableMultiSort: false,
enableSortingRemoval: false, enableSortingRemoval: false,
meta: {
updateName,
newFile,
newDirectory,
},
}); });
useEffect(() => { useEffect(() => {
@ -418,6 +502,27 @@ function TableBody({
openNativeLabel = "Open File in Default Application"; openNativeLabel = "Open File in Default Application";
} }
const menu: ContextMenuItem[] = [ 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", label: "Copy File Name",
click: () => navigator.clipboard.writeText(fileName), click: () => navigator.clipboard.writeText(fileName),
@ -446,6 +551,7 @@ function TableBody({
{ {
type: "separator", type: "separator",
}, },
// TODO: Only show this option for local files, resolve correct host path if connection is WSL
{ {
label: openNativeLabel, label: openNativeLabel,
click: async () => { click: async () => {
@ -483,14 +589,18 @@ function TableBody({
}, },
}); });
} }
menu.push({ type: "separator" }); menu.push(
menu.push({ {
label: "Delete File", type: "separator",
click: async () => {
await FileService.DeleteFile(conn, finfo.path).catch((e) => console.log(e));
setRefreshVersion((current) => current + 1);
}, },
}); {
label: "Delete",
click: async () => {
await FileService.DeleteFile(conn, finfo.path).catch((e) => console.log(e));
setRefreshVersion((current) => current + 1);
},
}
);
ContextMenuModel.showContextMenu(menu, e); ContextMenuModel.showContextMenu(menu, e);
}, },
[setRefreshVersion, conn] [setRefreshVersion, conn]
@ -560,12 +670,12 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
const [focusIndex, setFocusIndex] = useState(0); const [focusIndex, setFocusIndex] = useState(0);
const [unfilteredData, setUnfilteredData] = useState<FileInfo[]>([]); const [unfilteredData, setUnfilteredData] = useState<FileInfo[]>([]);
const [filteredData, setFilteredData] = useState<FileInfo[]>([]); const [filteredData, setFilteredData] = useState<FileInfo[]>([]);
const fileName = useAtomValue(model.metaFilePath);
const showHiddenFiles = useAtomValue(model.showHiddenFiles); const showHiddenFiles = useAtomValue(model.showHiddenFiles);
const [selectedPath, setSelectedPath] = useState(""); const [selectedPath, setSelectedPath] = useState("");
const [refreshVersion, setRefreshVersion] = useAtom(model.refreshVersion); const [refreshVersion, setRefreshVersion] = useAtom(model.refreshVersion);
const conn = useAtomValue(model.connection); const conn = useAtomValue(model.connection);
const blockData = useAtomValue(model.blockAtom); const blockData = useAtomValue(model.blockAtom);
const dirPath = useAtomValue(model.normFilePath);
useEffect(() => { useEffect(() => {
model.refreshCallback = () => { model.refreshCallback = () => {
@ -578,13 +688,13 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
useEffect(() => { useEffect(() => {
const getContent = async () => { const getContent = async () => {
const file = await FileService.ReadFile(conn, fileName); const file = await FileService.ReadFile(conn, dirPath);
const serializedContent = base64ToString(file?.data64); const serializedContent = base64ToString(file?.data64);
const content: FileInfo[] = JSON.parse(serializedContent); const content: FileInfo[] = JSON.parse(serializedContent);
setUnfilteredData(content); setUnfilteredData(content);
}; };
getContent(); getContent();
}, [conn, fileName, refreshVersion]); }, [conn, dirPath, refreshVersion]);
useEffect(() => { useEffect(() => {
const filtered = unfilteredData.filter((fileInfo) => { const filtered = unfilteredData.filter((fileInfo) => {
@ -652,26 +762,138 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
} }
}, [filteredData]); }, [filteredData]);
const entryManagerPropsAtom = useState(
atom<EntryManagerOverlayProps>(null) as PrimitiveAtom<EntryManagerOverlayProps>
)[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 ( return (
<div <Fragment>
className="dir-table-container" <div
onChangeCapture={(e) => { ref={refs.setReference}
const event = e as React.ChangeEvent<HTMLInputElement>; className="dir-table-container"
setSearchText(event.target.value.toLowerCase()); onChangeCapture={(e) => {
}} const event = e as React.ChangeEvent<HTMLInputElement>;
// onFocusCapture={() => document.getSelection().collapseToEnd()} if (!entryManagerProps) {
> setSearchText(event.target.value.toLowerCase());
<DirectoryTable }
model={model} }}
data={filteredData} {...getReferenceProps()}
search={searchText} onContextMenu={(e) => handleFileContextMenu(e)}
focusIndex={focusIndex} >
setFocusIndex={setFocusIndex} <DirectoryTable
setSearch={setSearchText} model={model}
setSelectedPath={setSelectedPath} data={filteredData}
setRefreshVersion={setRefreshVersion} search={searchText}
/> focusIndex={focusIndex}
</div> setFocusIndex={setFocusIndex}
setSearch={setSearchText}
setSelectedPath={setSelectedPath}
setRefreshVersion={setRefreshVersion}
entryManagerOverlayPropsAtom={entryManagerPropsAtom}
newFile={newFile}
newDirectory={newDirectory}
/>
</div>
{entryManagerProps && (
<EntryManagerOverlay
{...entryManagerProps}
forwardRef={refs.setFloating}
style={floatingStyles}
getReferenceProps={getFloatingProps}
/>
)}
</Fragment>
); );
} }

View File

@ -87,6 +87,11 @@ async function initWaveWrap(initOpts: WaveInitOpts) {
async function reinitWave() { async function reinitWave() {
console.log("Reinit Wave"); console.log("Reinit Wave");
getApi().sendLog("Reinit Wave"); getApi().sendLog("Reinit Wave");
requestAnimationFrame(() => {
setTimeout(() => {
document.body.classList.remove("nohover");
}, 50);
});
const client = await WOS.reloadWaveObject<Client>(WOS.makeORef("client", savedInitOpts.clientId)); const client = await WOS.reloadWaveObject<Client>(WOS.makeORef("client", savedInitOpts.clientId));
const waveWindow = await WOS.reloadWaveObject<WaveWindow>(WOS.makeORef("window", savedInitOpts.windowId)); const waveWindow = await WOS.reloadWaveObject<WaveWindow>(WOS.makeORef("window", savedInitOpts.windowId));
const ws = await WOS.reloadWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid)); const ws = await WOS.reloadWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid));

View File

@ -13,9 +13,10 @@ import (
) )
const ( const (
WSEvent_ElectronNewWindow = "electron:newwindow" WSEvent_ElectronNewWindow = "electron:newwindow"
WSEvent_ElectronCloseWindow = "electron:closewindow" WSEvent_ElectronCloseWindow = "electron:closewindow"
WSEvent_Rpc = "rpc" WSEvent_ElectronUpdateActiveTab = "electron:updateactivetab"
WSEvent_Rpc = "rpc"
) )
type WSEventType struct { type WSEventType struct {

View File

@ -61,6 +61,33 @@ func (fs *FileService) StatFile(connection string, path string) (*wshrpc.FileInf
return wshclient.RemoteFileInfoCommand(client, path, &wshrpc.RpcOpts{Route: connRoute}) 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 { func (fs *FileService) ReadFile_Meta() tsgenmeta.MethodMeta {
return tsgenmeta.MethodMeta{ return tsgenmeta.MethodMeta{
Desc: "read file", Desc: "read file",

View File

@ -121,7 +121,7 @@ func (svc *ObjectService) DeleteBlock(uiContext waveobj.UIContext, blockId strin
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn() defer cancelFn()
ctx = waveobj.ContextWithUpdates(ctx) ctx = waveobj.ContextWithUpdates(ctx)
err := wcore.DeleteBlock(ctx, blockId) err := wcore.DeleteBlock(ctx, blockId, true)
if err != nil { if err != nil {
return nil, fmt.Errorf("error deleting block: %w", err) return nil, fmt.Errorf("error deleting block: %w", err)
} }

View File

@ -169,21 +169,13 @@ func (svc *WorkspaceService) CloseTab(ctx context.Context, workspaceId string, t
blockcontroller.StopBlockController(blockId) blockcontroller.StopBlockController(blockId)
} }
}() }()
newActiveTabId, err := wcore.DeleteTab(ctx, workspaceId, tabId) newActiveTabId, err := wcore.DeleteTab(ctx, workspaceId, tabId, true)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("error closing tab: %w", err) return nil, nil, fmt.Errorf("error closing tab: %w", err)
} }
rtn := &CloseTabRtnType{} rtn := &CloseTabRtnType{}
if newActiveTabId == "" { if newActiveTabId == "" {
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
if err != nil {
return rtn, nil, fmt.Errorf("unable to find window for workspace id %v: %w", workspaceId, err)
}
rtn.CloseWindow = true rtn.CloseWindow = true
err = wcore.CloseWindow(ctx, windowId, fromElectron)
if err != nil {
return rtn, nil, err
}
} else { } else {
rtn.NewActiveTabId = newActiveTabId rtn.NewActiveTabId = newActiveTabId
} }

View File

@ -159,6 +159,11 @@ type WorkspaceListEntry struct {
type WorkspaceList []*WorkspaceListEntry type WorkspaceList []*WorkspaceListEntry
type ActiveTabUpdate struct {
WorkspaceId string `json:"workspaceid"`
NewActiveTabId string `json:"newactivetabid"`
}
type Workspace struct { type Workspace struct {
OID string `json:"oid"` OID string `json:"oid"`
Version int `json:"version"` Version int `json:"version"`

View File

@ -3,11 +3,14 @@ package wcore
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"time" "time"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/pkg/blockcontroller" "github.com/wavetermdev/waveterm/pkg/blockcontroller"
"github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/telemetry" "github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wps" "github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wshrpc"
@ -21,13 +24,34 @@ func CreateSubBlock(ctx context.Context, blockId string, blockDef *waveobj.Block
if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" { if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" {
return nil, fmt.Errorf("no view provided for new block") return nil, fmt.Errorf("no view provided for new block")
} }
blockData, err := wstore.CreateSubBlock(ctx, blockId, blockDef) blockData, err := createSubBlockObj(ctx, blockId, blockDef)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating sub block: %w", err) return nil, fmt.Errorf("error creating sub block: %w", err)
} }
return blockData, nil return blockData, nil
} }
func createSubBlockObj(ctx context.Context, parentBlockId string, blockDef *waveobj.BlockDef) (*waveobj.Block, error) {
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (*waveobj.Block, error) {
parentBlock, _ := wstore.DBGet[*waveobj.Block](tx.Context(), parentBlockId)
if parentBlock == nil {
return nil, fmt.Errorf("parent block not found: %q", parentBlockId)
}
blockId := uuid.NewString()
blockData := &waveobj.Block{
OID: blockId,
ParentORef: waveobj.MakeORef(waveobj.OType_Block, parentBlockId).String(),
BlockDef: blockDef,
RuntimeOpts: nil,
Meta: blockDef.Meta,
}
wstore.DBInsert(tx.Context(), blockData)
parentBlock.SubBlockIds = append(parentBlock.SubBlockIds, blockId)
wstore.DBUpdate(tx.Context(), parentBlock)
return blockData, nil
})
}
func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) { func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
if blockDef == nil { if blockDef == nil {
return nil, fmt.Errorf("blockDef is nil") return nil, fmt.Errorf("blockDef is nil")
@ -35,7 +59,7 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" { if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" {
return nil, fmt.Errorf("no view provided for new block") return nil, fmt.Errorf("no view provided for new block")
} }
blockData, err := wstore.CreateBlock(ctx, tabId, blockDef, rtOpts) blockData, err := createBlockObj(ctx, tabId, blockDef, rtOpts)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating block: %w", err) return nil, fmt.Errorf("error creating block: %w", err)
} }
@ -54,7 +78,32 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
return blockData, nil return blockData, nil
} }
func DeleteBlock(ctx context.Context, blockId string) error { func createBlockObj(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (*waveobj.Block, error) {
tab, _ := wstore.DBGet[*waveobj.Tab](tx.Context(), tabId)
if tab == nil {
return nil, fmt.Errorf("tab not found: %q", tabId)
}
blockId := uuid.NewString()
blockData := &waveobj.Block{
OID: blockId,
ParentORef: waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
BlockDef: blockDef,
RuntimeOpts: rtOpts,
Meta: blockDef.Meta,
}
wstore.DBInsert(tx.Context(), blockData)
tab.BlockIds = append(tab.BlockIds, blockId)
wstore.DBUpdate(tx.Context(), tab)
return blockData, nil
})
}
// Must delete all blocks individually first.
// Also deletes LayoutState.
// recursive: if true, will recursively close parent tab, window, workspace, if they are empty.
// Returns new active tab id, error.
func DeleteBlock(ctx context.Context, blockId string, recursive bool) error {
block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId) block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
if err != nil { if err != nil {
return fmt.Errorf("error getting block: %w", err) return fmt.Errorf("error getting block: %w", err)
@ -64,21 +113,77 @@ func DeleteBlock(ctx context.Context, blockId string) error {
} }
if len(block.SubBlockIds) > 0 { if len(block.SubBlockIds) > 0 {
for _, subBlockId := range block.SubBlockIds { for _, subBlockId := range block.SubBlockIds {
err := DeleteBlock(ctx, subBlockId) err := DeleteBlock(ctx, subBlockId, recursive)
if err != nil { if err != nil {
return fmt.Errorf("error deleting subblock %s: %w", subBlockId, err) return fmt.Errorf("error deleting subblock %s: %w", subBlockId, err)
} }
} }
} }
err = wstore.DeleteBlock(ctx, blockId) parentBlockCount, err := deleteBlockObj(ctx, blockId)
if err != nil { if err != nil {
return fmt.Errorf("error deleting block: %w", err) return fmt.Errorf("error deleting block: %w", err)
} }
log.Printf("DeleteBlock: parentBlockCount: %d", parentBlockCount)
parentORef := waveobj.ParseORefNoErr(block.ParentORef)
if parentORef.OType == waveobj.OType_Tab {
if parentBlockCount == 0 && recursive {
// if parent tab has no blocks, delete the tab
log.Printf("DeleteBlock: parent tab has no blocks, deleting tab %s", parentORef.OID)
parentWorkspaceId, err := wstore.DBFindWorkspaceForTabId(ctx, parentORef.OID)
if err != nil {
return fmt.Errorf("error finding workspace for tab to delete %s: %w", parentORef.OID, err)
}
newActiveTabId, err := DeleteTab(ctx, parentWorkspaceId, parentORef.OID, true)
if err != nil {
return fmt.Errorf("error deleting tab %s: %w", parentORef.OID, err)
}
SendActiveTabUpdate(ctx, parentWorkspaceId, newActiveTabId)
}
}
go blockcontroller.StopBlockController(blockId) go blockcontroller.StopBlockController(blockId)
sendBlockCloseEvent(blockId) sendBlockCloseEvent(blockId)
return nil return nil
} }
// returns the updated block count for the parent object
func deleteBlockObj(ctx context.Context, blockId string) (int, error) {
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (int, error) {
block, err := wstore.DBGet[*waveobj.Block](tx.Context(), blockId)
if err != nil {
return -1, fmt.Errorf("error getting block: %w", err)
}
if block == nil {
return -1, fmt.Errorf("block not found: %q", blockId)
}
if len(block.SubBlockIds) > 0 {
return -1, fmt.Errorf("block has subblocks, must delete subblocks first")
}
parentORef := waveobj.ParseORefNoErr(block.ParentORef)
parentBlockCount := -1
if parentORef != nil {
if parentORef.OType == waveobj.OType_Tab {
tab, _ := wstore.DBGet[*waveobj.Tab](tx.Context(), parentORef.OID)
if tab != nil {
tab.BlockIds = utilfn.RemoveElemFromSlice(tab.BlockIds, blockId)
wstore.DBUpdate(tx.Context(), tab)
parentBlockCount = len(tab.BlockIds)
}
} else if parentORef.OType == waveobj.OType_Block {
parentBlock, _ := wstore.DBGet[*waveobj.Block](tx.Context(), parentORef.OID)
if parentBlock != nil {
parentBlock.SubBlockIds = utilfn.RemoveElemFromSlice(parentBlock.SubBlockIds, blockId)
wstore.DBUpdate(tx.Context(), parentBlock)
parentBlockCount = len(parentBlock.SubBlockIds)
}
}
}
wstore.DBDelete(tx.Context(), waveobj.OType_Block, blockId)
return parentBlockCount, nil
})
}
func sendBlockCloseEvent(blockId string) { func sendBlockCloseEvent(blockId string) {
waveEvent := wps.WaveEvent{ waveEvent := wps.WaveEvent{
Event: wps.Event_BlockClose, Event: wps.Event_BlockClose,

View File

@ -120,6 +120,8 @@ func CreateWindow(ctx context.Context, winSize *waveobj.WinSize, workspaceId str
return GetWindow(ctx, windowId) return GetWindow(ctx, windowId)
} }
// CloseWindow closes a window and deletes its workspace if it is empty and not named.
// If fromElectron is true, it does not send an event to Electron.
func CloseWindow(ctx context.Context, windowId string, fromElectron bool) error { func CloseWindow(ctx context.Context, windowId string, fromElectron bool) error {
log.Printf("CloseWindow %s\n", windowId) log.Printf("CloseWindow %s\n", windowId)
window, err := GetWindow(ctx, windowId) window, err := GetWindow(ctx, windowId)

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/wavetermdev/waveterm/pkg/eventbus"
"github.com/wavetermdev/waveterm/pkg/telemetry" "github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/waveobj"
@ -27,19 +28,22 @@ func CreateWorkspace(ctx context.Context, name string, icon string, color string
return ws, nil return ws, nil
} }
// If force is true, it will delete even if workspace is named.
// If workspace is empty, it will be deleted, even if it is named.
// Returns true if workspace was deleted, false if it was not deleted.
func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool, error) { func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool, error) {
log.Printf("DeleteWorkspace %s\n", workspaceId) log.Printf("DeleteWorkspace %s\n", workspaceId)
workspace, err := wstore.DBMustGet[*waveobj.Workspace](ctx, workspaceId) workspace, err := wstore.DBMustGet[*waveobj.Workspace](ctx, workspaceId)
if err != nil { if err != nil {
return false, fmt.Errorf("error getting workspace: %w", err) return false, fmt.Errorf("error getting workspace: %w", err)
} }
if workspace.Name != "" && workspace.Icon != "" && !force { if workspace.Name != "" && workspace.Icon != "" && !force && len(workspace.TabIds) > 0 {
log.Printf("Ignoring DeleteWorkspace for workspace %s as it is named\n", workspaceId) log.Printf("Ignoring DeleteWorkspace for workspace %s as it is named\n", workspaceId)
return false, nil return false, nil
} }
for _, tabId := range workspace.TabIds { for _, tabId := range workspace.TabIds {
log.Printf("deleting tab %s\n", tabId) log.Printf("deleting tab %s\n", tabId)
_, err := DeleteTab(ctx, workspaceId, tabId) _, err := DeleteTab(ctx, workspaceId, tabId, false)
if err != nil { if err != nil {
return false, fmt.Errorf("error closing tab: %w", err) return false, fmt.Errorf("error closing tab: %w", err)
} }
@ -103,8 +107,9 @@ func CreateTab(ctx context.Context, workspaceId string, tabName string, activate
// Must delete all blocks individually first. // Must delete all blocks individually first.
// Also deletes LayoutState. // Also deletes LayoutState.
// recursive: if true, will recursively close parent window, workspace, if they are empty.
// Returns new active tab id, error. // Returns new active tab id, error.
func DeleteTab(ctx context.Context, workspaceId string, tabId string) (string, error) { func DeleteTab(ctx context.Context, workspaceId string, tabId string, recursive bool) (string, error) {
ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId) ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
if ws == nil { if ws == nil {
return "", fmt.Errorf("workspace not found: %q", workspaceId) return "", fmt.Errorf("workspace not found: %q", workspaceId)
@ -116,7 +121,7 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string) (string, e
// close blocks (sends events + stops block controllers) // close blocks (sends events + stops block controllers)
for _, blockId := range tab.BlockIds { for _, blockId := range tab.BlockIds {
err := DeleteBlock(ctx, blockId) err := DeleteBlock(ctx, blockId, false)
if err != nil { if err != nil {
return "", fmt.Errorf("error deleting block %s: %w", blockId, err) return "", fmt.Errorf("error deleting block %s: %w", blockId, err)
} }
@ -139,6 +144,17 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string) (string, e
wstore.DBUpdate(ctx, ws) wstore.DBUpdate(ctx, ws)
wstore.DBDelete(ctx, waveobj.OType_Tab, tabId) wstore.DBDelete(ctx, waveobj.OType_Tab, tabId)
wstore.DBDelete(ctx, waveobj.OType_LayoutState, tab.LayoutState) wstore.DBDelete(ctx, waveobj.OType_LayoutState, tab.LayoutState)
if newActiveTabId == "" && recursive {
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
if err != nil {
return newActiveTabId, fmt.Errorf("unable to find window for workspace id %v: %w", workspaceId, err)
}
err = CloseWindow(ctx, windowId, false)
if err != nil {
return newActiveTabId, err
}
}
return newActiveTabId, nil return newActiveTabId, nil
} }
@ -158,6 +174,13 @@ func SetActiveTab(ctx context.Context, workspaceId string, tabId string) error {
return nil return nil
} }
func SendActiveTabUpdate(ctx context.Context, workspaceId string, newActiveTabId string) {
eventbus.SendEventToElectron(eventbus.WSEventType{
EventType: eventbus.WSEvent_ElectronUpdateActiveTab,
Data: &waveobj.ActiveTabUpdate{WorkspaceId: workspaceId, NewActiveTabId: newActiveTabId},
})
}
func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string) error { func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string) error {
ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId) ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
if ws == nil { if ws == nil {

View File

@ -259,6 +259,24 @@ func RemoteFileJoinCommand(w *wshutil.WshRpc, data []string, opts *wshrpc.RpcOpt
return resp, err 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 // command "remotestreamcpudata", wshserver.RemoteStreamCpuDataCommand
func RemoteStreamCpuDataCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[wshrpc.TimeSeriesData] { func RemoteStreamCpuDataCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[wshrpc.TimeSeriesData] {
return sendRpcRequestResponseStreamHelper[wshrpc.TimeSeriesData](w, "remotestreamcpudata", nil, opts) return sendRpcRequestResponseStreamHelper[wshrpc.TimeSeriesData](w, "remotestreamcpudata", nil, opts)

View File

@ -307,6 +307,49 @@ func (impl *ServerImpl) RemoteFileInfoCommand(ctx context.Context, path string)
return impl.fileInfoInternal(path, true) 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 { func (*ServerImpl) RemoteWriteFileCommand(ctx context.Context, data wshrpc.CommandRemoteWriteFileData) error {
path, err := wavebase.ExpandHomeDir(data.Path) path, err := wavebase.ExpandHomeDir(data.Path)
if err != nil { if err != nil {

View File

@ -61,6 +61,7 @@ const (
Command_Test = "test" Command_Test = "test"
Command_RemoteStreamFile = "remotestreamfile" Command_RemoteStreamFile = "remotestreamfile"
Command_RemoteFileInfo = "remotefileinfo" Command_RemoteFileInfo = "remotefileinfo"
Command_RemoteFileTouch = "remotefiletouch"
Command_RemoteWriteFile = "remotewritefile" Command_RemoteWriteFile = "remotewritefile"
Command_RemoteFileDelete = "remotefiledelete" Command_RemoteFileDelete = "remotefiledelete"
Command_RemoteFileJoin = "remotefilejoin" Command_RemoteFileJoin = "remotefilejoin"
@ -69,6 +70,7 @@ const (
Command_Activity = "activity" Command_Activity = "activity"
Command_GetVar = "getvar" Command_GetVar = "getvar"
Command_SetVar = "setvar" Command_SetVar = "setvar"
Command_RemoteMkdir = "remotemkdir"
Command_ConnStatus = "connstatus" Command_ConnStatus = "connstatus"
Command_WslStatus = "wslstatus" Command_WslStatus = "wslstatus"
@ -161,9 +163,12 @@ type WshRpcInterface interface {
// remotes // remotes
RemoteStreamFileCommand(ctx context.Context, data CommandRemoteStreamFileData) chan RespOrErrorUnion[CommandRemoteStreamFileRtnData] RemoteStreamFileCommand(ctx context.Context, data CommandRemoteStreamFileData) chan RespOrErrorUnion[CommandRemoteStreamFileRtnData]
RemoteFileInfoCommand(ctx context.Context, path string) (*FileInfo, error) 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 RemoteFileDeleteCommand(ctx context.Context, path string) error
RemoteWriteFileCommand(ctx context.Context, data CommandRemoteWriteFileData) error RemoteWriteFileCommand(ctx context.Context, data CommandRemoteWriteFileData) error
RemoteFileJoinCommand(ctx context.Context, paths []string) (*FileInfo, error) RemoteFileJoinCommand(ctx context.Context, paths []string) (*FileInfo, error)
RemoteMkdirCommand(ctx context.Context, path string) error
RemoteStreamCpuDataCommand(ctx context.Context) chan RespOrErrorUnion[TimeSeriesData] RemoteStreamCpuDataCommand(ctx context.Context) chan RespOrErrorUnion[TimeSeriesData]
// emain // emain

View File

@ -489,7 +489,7 @@ func (ws *WshServer) FileAppendIJsonCommand(ctx context.Context, data wshrpc.Com
} }
func (ws *WshServer) DeleteSubBlockCommand(ctx context.Context, data wshrpc.CommandDeleteBlockData) error { func (ws *WshServer) DeleteSubBlockCommand(ctx context.Context, data wshrpc.CommandDeleteBlockData) error {
err := wcore.DeleteBlock(ctx, data.BlockId) err := wcore.DeleteBlock(ctx, data.BlockId, false)
if err != nil { if err != nil {
return fmt.Errorf("error deleting block: %w", err) return fmt.Errorf("error deleting block: %w", err)
} }
@ -505,7 +505,7 @@ func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.Command
if tabId == "" { if tabId == "" {
return fmt.Errorf("no tab found for block") return fmt.Errorf("no tab found for block")
} }
err = wcore.DeleteBlock(ctx, data.BlockId) err = wcore.DeleteBlock(ctx, data.BlockId, true)
if err != nil { if err != nil {
return fmt.Errorf("error deleting block: %w", err) return fmt.Errorf("error deleting block: %w", err)
} }

View File

@ -7,7 +7,6 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/waveobj"
) )
@ -32,81 +31,6 @@ func UpdateTabName(ctx context.Context, tabId, name string) error {
}) })
} }
func CreateSubBlock(ctx context.Context, parentBlockId string, blockDef *waveobj.BlockDef) (*waveobj.Block, error) {
return WithTxRtn(ctx, func(tx *TxWrap) (*waveobj.Block, error) {
parentBlock, _ := DBGet[*waveobj.Block](tx.Context(), parentBlockId)
if parentBlock == nil {
return nil, fmt.Errorf("parent block not found: %q", parentBlockId)
}
blockId := uuid.NewString()
blockData := &waveobj.Block{
OID: blockId,
ParentORef: waveobj.MakeORef(waveobj.OType_Block, parentBlockId).String(),
BlockDef: blockDef,
RuntimeOpts: nil,
Meta: blockDef.Meta,
}
DBInsert(tx.Context(), blockData)
parentBlock.SubBlockIds = append(parentBlock.SubBlockIds, blockId)
DBUpdate(tx.Context(), parentBlock)
return blockData, nil
})
}
func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
return WithTxRtn(ctx, func(tx *TxWrap) (*waveobj.Block, error) {
tab, _ := DBGet[*waveobj.Tab](tx.Context(), tabId)
if tab == nil {
return nil, fmt.Errorf("tab not found: %q", tabId)
}
blockId := uuid.NewString()
blockData := &waveobj.Block{
OID: blockId,
ParentORef: waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
BlockDef: blockDef,
RuntimeOpts: rtOpts,
Meta: blockDef.Meta,
}
DBInsert(tx.Context(), blockData)
tab.BlockIds = append(tab.BlockIds, blockId)
DBUpdate(tx.Context(), tab)
return blockData, nil
})
}
func DeleteBlock(ctx context.Context, blockId string) error {
return WithTx(ctx, func(tx *TxWrap) error {
block, err := DBGet[*waveobj.Block](tx.Context(), blockId)
if err != nil {
return fmt.Errorf("error getting block: %w", err)
}
if block == nil {
return nil
}
if len(block.SubBlockIds) > 0 {
return fmt.Errorf("block has subblocks, must delete subblocks first")
}
parentORef := waveobj.ParseORefNoErr(block.ParentORef)
if parentORef != nil {
if parentORef.OType == waveobj.OType_Tab {
tab, _ := DBGet[*waveobj.Tab](tx.Context(), parentORef.OID)
if tab != nil {
tab.BlockIds = utilfn.RemoveElemFromSlice(tab.BlockIds, blockId)
DBUpdate(tx.Context(), tab)
}
} else if parentORef.OType == waveobj.OType_Block {
parentBlock, _ := DBGet[*waveobj.Block](tx.Context(), parentORef.OID)
if parentBlock != nil {
parentBlock.SubBlockIds = utilfn.RemoveElemFromSlice(parentBlock.SubBlockIds, blockId)
DBUpdate(tx.Context(), parentBlock)
}
}
}
DBDelete(tx.Context(), waveobj.OType_Block, blockId)
return nil
})
}
// must delete all blocks individually first // must delete all blocks individually first
// also deletes LayoutState // also deletes LayoutState
func DeleteTab(ctx context.Context, workspaceId string, tabId string) error { func DeleteTab(ctx context.Context, workspaceId string, tabId string) error {