This commit is contained in:
Mike Sawka 2024-10-23 22:47:29 -07:00 committed by GitHub
parent 8248637e00
commit 701d93884d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 787 additions and 190 deletions

View File

@ -0,0 +1,47 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"encoding/json"
"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
)
var debugCmd = &cobra.Command{
Use: "debug",
Short: "debug commands",
PersistentPreRunE: preRunSetupRpcClient,
Hidden: true,
}
var debugBlockIdsCmd = &cobra.Command{
Use: "block",
Short: "list sub-blockids for block",
RunE: debugBlockIdsRun,
Hidden: true,
}
func init() {
debugCmd.AddCommand(debugBlockIdsCmd)
rootCmd.AddCommand(debugCmd)
}
func debugBlockIdsRun(cmd *cobra.Command, args []string) error {
oref, err := resolveBlockArg()
if err != nil {
return err
}
blockInfo, err := wshclient.BlockInfoCommand(RpcClient, oref.OID, nil)
if err != nil {
return err
}
barr, err := json.MarshalIndent(blockInfo, "", " ")
if err != nil {
return err
}
WriteStdout("%s\n", string(barr))
return nil
}

View File

@ -65,11 +65,11 @@ func editorRun(cmd *cobra.Command, args []string) {
return
}
doneCh := make(chan bool)
RpcClient.EventListener.On("blockclose", func(event *wps.WaveEvent) {
RpcClient.EventListener.On(wps.Event_BlockClose, func(event *wps.WaveEvent) {
if event.HasScope(blockRef.String()) {
close(doneCh)
}
})
wshclient.EventSubCommand(RpcClient, wps.SubscriptionRequest{Event: "blockclose", Scopes: []string{blockRef.String()}}, nil)
wshclient.EventSubCommand(RpcClient, wps.SubscriptionRequest{Event: wps.Event_BlockClose, Scopes: []string{blockRef.String()}}, nil)
<-doneCh
}

View File

@ -13,7 +13,10 @@ import (
"github.com/wavetermdev/waveterm/pkg/wshutil"
)
var htmlCmdNewBlock bool
func init() {
htmlCmd.Flags().BoolVarP(&htmlCmdNewBlock, "newblock", "n", false, "create a new block")
rootCmd.AddCommand(htmlCmd)
}
@ -30,7 +33,10 @@ func MakeVDom() *vdom.VDomElem {
<h1 style="color:red; background-color: #bind:$.bgcolor; border-radius: 4px; padding: 5px;">hello vdom world</h1>
<div><bind key="$.text"/> | num[<bind key="$.num"/>]</div>
<div>
<button onClick="#globalevent:clickinc">increment</button>
<button data-text="hello" onClick='#globalevent:clickinc'>increment</button>
</div>
<div>
<wave:markdown text="*hello from markdown*"/>
</div>
</div>
`
@ -39,7 +45,7 @@ func MakeVDom() *vdom.VDomElem {
}
func GlobalEventHandler(client *vdomclient.Client, event vdom.VDomEvent) {
if event.PropName == "clickinc" {
if event.EventType == "clickinc" {
client.SetAtomVal("num", client.GetAtomVal("num").(int)+1)
return
}
@ -58,7 +64,7 @@ func htmlRun(cmd *cobra.Command, args []string) error {
client.SetAtomVal("text", "initial text")
client.SetAtomVal("num", 0)
client.SetRootElem(MakeVDom())
err = client.CreateVDomContext()
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: htmlCmdNewBlock})
if err != nil {
return err
}
@ -70,8 +76,12 @@ func htmlRun(cmd *cobra.Command, args []string) error {
log.Printf("created vdom context\n")
go func() {
time.Sleep(5 * time.Second)
log.Printf("updating text\n")
client.SetAtomVal("text", "updated text")
client.SendAsyncInitiation()
err := client.SendAsyncInitiation()
if err != nil {
log.Printf("error sending async initiation: %v\n", err)
}
}()
<-client.DoneCh
return nil

View File

@ -71,6 +71,22 @@ func preRunSetupRpcClient(cmd *cobra.Command, args []string) error {
return nil
}
func resolveBlockArg() (*waveobj.ORef, error) {
oref := blockArg
if oref == "" {
return nil, fmt.Errorf("blockid is required")
}
err := validateEasyORef(oref)
if err != nil {
return nil, err
}
fullORef, err := resolveSimpleId(oref)
if err != nil {
return nil, fmt.Errorf("resolving blockid: %w", err)
}
return fullORef, nil
}
// returns the wrapped stdin and a new rpc client (that wraps the stdin input and stdout output)
func setupRpcClient(serverImpl wshutil.ServerImpl) error {
jwtToken := os.Getenv(wshutil.WaveJwtTokenVarName)
@ -101,7 +117,7 @@ func setupRpcClient(serverImpl wshutil.ServerImpl) error {
func setTermHtmlMode() {
wshutil.SetExtraShutdownFunc(extraShutdownFn)
cmd := &wshrpc.CommandSetMetaData{
Meta: map[string]any{"term:mode": "html"},
Meta: map[string]any{"term:mode": "vdom"},
}
err := RpcClient.SendCommand(wshrpc.Command_SetMeta, cmd, nil)
if err != nil {

View File

@ -28,7 +28,7 @@ var webOpenCmd = &cobra.Command{
}
var webGetCmd = &cobra.Command{
Use: "get [--inner] [--all] [--json] blockid css-selector",
Use: "get [--inner] [--all] [--json] css-selector",
Short: "get the html for a css selector",
Args: cobra.ExactArgs(1),
Hidden: true,
@ -67,7 +67,7 @@ func webGetRun(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("getting block info: %w", err)
}
if blockInfo.Meta.GetString(waveobj.MetaKey_View, "") != "web" {
if blockInfo.Block.Meta.GetString(waveobj.MetaKey_View, "") != "web" {
return fmt.Errorf("block %s is not a web block", fullORef.OID)
}
data := wshrpc.CommandWebSelectorData{

View File

@ -0,0 +1 @@
-- we don't need to remove parentoref

View File

@ -0,0 +1,4 @@
UPDATE db_block
SET data = json_set(db_block.data, '$.parentoref', 'tab:' || db_tab.oid)
FROM db_tab
WHERE db_block.oid IN (SELECT value FROM json_each(db_tab.data, '$.blockids'));

View File

@ -5,6 +5,8 @@ import { BlockComponentModel2, BlockProps } from "@/app/block/blocktypes";
import { PlotView } from "@/app/view/plotview/plotview";
import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview";
import { SysinfoView, SysinfoViewModel, makeSysinfoViewModel } from "@/app/view/sysinfo/sysinfo";
import { VDomModel } from "@/app/view/term/vdom-model";
import { VDomView, makeVDomModel } from "@/app/view/vdom/vdom-view";
import { ErrorBoundary } from "@/element/errorboundary";
import { CenteredDiv } from "@/element/quickelems";
import { NodeModel, useDebouncedNodeInnerRect } from "@/layout/index";
@ -29,6 +31,7 @@ import { BlockFrame } from "./blockframe";
import { blockViewToIcon, blockViewToName } from "./blockutil";
type FullBlockProps = {
isSubBlock?: boolean;
preview: boolean;
nodeModel: NodeModel;
viewModel: ViewModel;
@ -51,6 +54,9 @@ function makeViewModel(blockId: string, blockView: string, nodeModel: NodeModel)
// "cpuplot" is for backwards compatibility with already-opened widgets
return makeSysinfoViewModel(blockId, blockView);
}
if (blockView == "vdom") {
return makeVDomModel(blockId, nodeModel);
}
if (blockView === "help") {
return makeHelpViewModel(blockId, nodeModel);
}
@ -100,6 +106,9 @@ function getViewElem(
if (blockView == "tips") {
return <QuickTipsView key={blockId} model={viewModel as QuickTipsViewModel} />;
}
if (blockView == "vdom") {
return <VDomView key={blockId} blockId={blockId} model={viewModel as VDomModel} />;
}
return <CenteredDiv>Invalid View "{blockView}"</CenteredDiv>;
}
@ -137,6 +146,26 @@ const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => {
);
});
const BlockSubBlock = memo(({ nodeModel, viewModel }: FullBlockProps) => {
const [blockData] = useWaveObjectValue<Block>(makeORef("block", nodeModel.blockId));
const blockRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const viewElem = useMemo(
() => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel),
[nodeModel.blockId, blockData?.meta?.view, viewModel]
);
if (!blockData) {
return null;
}
return (
<div key="content" className="block-content" ref={contentRef}>
<ErrorBoundary>
<Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</Suspense>
</ErrorBoundary>
</div>
);
});
const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
counterInc("render-BlockFull");
const focusElemRef = useRef<HTMLInputElement>(null);
@ -275,6 +304,9 @@ const Block = memo((props: BlockProps) => {
if (props.preview) {
return <BlockPreview {...props} viewModel={viewModel} />;
}
if (props.isSubBlock) {
return <BlockSubBlock {...props} viewModel={viewModel} />;
}
return <BlockFull {...props} viewModel={viewModel} />;
});

View File

@ -26,7 +26,6 @@ import {
useBlockAtom,
WOS,
} from "@/app/store/global";
import * as services from "@/app/store/services";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { ErrorBoundary } from "@/element/errorboundary";
@ -60,17 +59,17 @@ function handleHeaderContextMenu(
onMagnifyToggle();
},
},
{
label: "Move to New Window",
click: () => {
const currentTabId = globalStore.get(atoms.staticTabId);
try {
services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid);
} catch (e) {
console.error("error moving block to new window", e);
}
},
},
// {
// label: "Move to New Window",
// click: () => {
// const currentTabId = globalStore.get(atoms.staticTabId);
// try {
// services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid);
// } catch (e) {
// console.error("error moving block to new window", e);
// }
// },
// },
{ type: "separator" },
{
label: "Copy BlockId",

View File

@ -3,6 +3,7 @@
import { NodeModel } from "@/layout/index";
export interface BlockProps {
isSubBlock?: boolean;
preview: boolean;
nodeModel: NodeModel;
}

View File

@ -316,6 +316,7 @@ export {
makeORef,
reloadWaveObject,
setObjectValue,
splitORef,
updateWaveObject,
updateWaveObjects,
useWaveObjectValue,

View File

@ -67,11 +67,21 @@ class RpcApiType {
return client.wshRpcCall("createblock", data, opts);
}
// command "createsubblock" [call]
CreateSubBlockCommand(client: WshClient, data: CommandCreateSubBlockData, opts?: RpcOpts): Promise<ORef> {
return client.wshRpcCall("createsubblock", data, opts);
}
// command "deleteblock" [call]
DeleteBlockCommand(client: WshClient, data: CommandDeleteBlockData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("deleteblock", data, opts);
}
// command "deletesubblock" [call]
DeleteSubBlockCommand(client: WshClient, data: CommandDeleteBlockData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("deletesubblock", data, opts);
}
// command "dispose" [call]
DisposeCommand(client: WshClient, data: CommandDisposeData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("dispose", data, opts);
@ -228,7 +238,7 @@ class RpcApiType {
}
// command "vdomcreatecontext" [call]
VDomCreateContextCommand(client: WshClient, data: VDomCreateContext, opts?: RpcOpts): Promise<void> {
VDomCreateContextCommand(client: WshClient, data: VDomCreateContext, opts?: RpcOpts): Promise<ORef> {
return client.wshRpcCall("vdomcreatecontext", data, opts);
}
@ -237,6 +247,11 @@ class RpcApiType {
return client.wshRpcCall("vdomrender", data, opts);
}
// command "waitforroute" [call]
WaitForRouteCommand(client: WshClient, data: CommandWaitForRouteData, opts?: RpcOpts): Promise<boolean> {
return client.wshRpcCall("waitforroute", data, opts);
}
// command "webselector" [call]
WebSelectorCommand(client: WshClient, data: CommandWebSelectorData, opts?: RpcOpts): Promise<string[]> {
return client.wshRpcCall("webselector", data, opts);

View File

@ -1,12 +1,13 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { WOS } from "@/app/store/global";
import { waveEventSubscribe } from "@/app/store/wps";
import { atoms, globalStore } from "@/app/store/global";
import { makeORef, splitORef } from "@/app/store/wos";
import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
import { RpcApi } from "@/app/store/wshclientapi";
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
import { TermViewModel } from "@/app/view/term/term";
import { isBlank } from "@/util/util";
import debug from "debug";
const dlog = debug("wave:vdom");
@ -21,32 +22,55 @@ export class TermWshClient extends WshClient {
this.model = model;
}
handle_vdomcreatecontext(rh: RpcResponseHelper, data: VDomCreateContext) {
console.log("vdom-create", rh.getSource(), data);
this.model.vdomModel.reset();
this.model.vdomModel.backendRoute = rh.getSource();
if (!data.persist) {
const unsubFn = waveEventSubscribe({
eventType: "route:gone",
scope: rh.getSource(),
handler: () => {
RpcApi.SetMetaCommand(this, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:mode": null },
async handle_vdomcreatecontext(rh: RpcResponseHelper, data: VDomCreateContext) {
const source = rh.getSource();
if (isBlank(source)) {
throw new Error("source cannot be blank");
}
console.log("vdom-create", source, data);
const tabId = globalStore.get(atoms.staticTabId);
if (data.target?.newblock) {
const oref = await RpcApi.CreateBlockCommand(this, {
tabid: tabId,
blockdef: {
meta: {
view: "vdom",
"vdom:route": rh.getSource(),
},
},
magnified: data.target?.magnified,
});
unsubFn();
return oref;
} else {
// in the terminal
// check if there is a current active vdom block
const oldVDomBlockId = globalStore.get(this.model.vdomBlockId);
const oref = await RpcApi.CreateSubBlockCommand(this, {
parentblockid: this.blockId,
blockdef: {
meta: {
view: "vdom",
"vdom:route": rh.getSource(),
},
},
});
const [_, newVDomBlockId] = splitORef(oref);
if (!isBlank(oldVDomBlockId)) {
// dispose of the old vdom block
setTimeout(() => {
RpcApi.DeleteSubBlockCommand(this, { blockid: oldVDomBlockId });
}, 500);
}
setTimeout(() => {
RpcApi.SetMetaCommand(this, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:mode": "html" },
oref: makeORef("block", this.model.blockId),
meta: {
"term:mode": "vdom",
"term:vdomblockid": newVDomBlockId,
},
});
this.model.vdomModel.queueUpdate(true);
}, 50);
return oref;
}
handle_vdomasyncinitiation(rh: RpcResponseHelper, data: VDomAsyncInitiationRequest) {
console.log("async-initiation", rh.getSource(), data);
this.model.vdomModel.queueUpdate(true);
}
}

View File

@ -76,7 +76,7 @@
}
}
&.term-mode-html {
&.term-mode-vdom {
.term-connectelem {
display: none;
}

View File

@ -1,19 +1,28 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Block } from "@/app/block/block";
import { getAllGlobalKeyBindings } from "@/app/store/keymodel";
import { waveEventSubscribe } from "@/app/store/wps";
import { RpcApi } from "@/app/store/wshclientapi";
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
import { TermWshClient } from "@/app/view/term/term-wsh";
import { VDomView } from "@/app/view/term/vdom";
import { VDomModel } from "@/app/view/term/vdom-model";
import { NodeModel } from "@/layout/index";
import { WOS, atoms, getConnStatusAtom, getSettingsKeyAtom, globalStore, useSettingsPrefixAtom } from "@/store/global";
import {
WOS,
atoms,
getBlockComponentModel,
getConnStatusAtom,
getSettingsKeyAtom,
globalStore,
useSettingsPrefixAtom,
} from "@/store/global";
import * as services from "@/store/services";
import * as keyutil from "@/util/keyutil";
import clsx from "clsx";
import debug from "debug";
import * as jotai from "jotai";
import * as React from "react";
import { TermStickers } from "./termsticker";
@ -22,6 +31,8 @@ import { computeTheme } from "./termutil";
import { TermWrap } from "./termwrap";
import "./xterm.css";
const dlog = debug("wave:term");
type InitialLoadDataType = {
loaded: boolean;
heldData: Uint8Array[];
@ -37,12 +48,13 @@ class TermViewModel {
blockId: string;
viewIcon: jotai.Atom<string>;
viewName: jotai.Atom<string>;
viewText: jotai.Atom<HeaderElem[]>;
blockBg: jotai.Atom<MetaType>;
manageConnection: jotai.Atom<boolean>;
connStatus: jotai.Atom<ConnStatus>;
termWshClient: TermWshClient;
shellProcStatusRef: React.MutableRefObject<string>;
vdomModel: VDomModel;
vdomBlockId: jotai.Atom<string>;
constructor(blockId: string, nodeModel: NodeModel) {
this.viewType = "term";
@ -50,23 +62,70 @@ class TermViewModel {
this.termWshClient = new TermWshClient(blockId, this);
DefaultRouter.registerRoute(makeFeBlockRouteId(blockId), this.termWshClient);
this.nodeModel = nodeModel;
this.vdomModel = new VDomModel(blockId, nodeModel, null, this.termWshClient);
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
this.vdomBlockId = jotai.atom((get) => {
const blockData = get(this.blockAtom);
return blockData?.meta?.["term:vdomblockid"];
});
this.termMode = jotai.atom((get) => {
const blockData = get(this.blockAtom);
return blockData?.meta?.["term:mode"] ?? "term";
});
this.viewIcon = jotai.atom((get) => {
const termMode = get(this.termMode);
if (termMode == "vdom") {
return "bolt";
}
return "terminal";
});
this.viewName = jotai.atom((get) => {
const blockData = get(this.blockAtom);
const termMode = get(this.termMode);
if (termMode == "vdom") {
return "Wave App";
}
if (blockData?.meta?.controller == "cmd") {
return "Command";
}
return "Terminal";
});
this.manageConnection = jotai.atom(true);
this.viewText = jotai.atom((get) => {
const termMode = get(this.termMode);
if (termMode == "vdom") {
return [
{
elemtype: "iconbutton",
icon: "square-terminal",
title: "Switch back to Terminal",
click: () => {
this.setTermMode("term");
},
},
];
} else {
const vdomBlockId = get(this.vdomBlockId);
if (vdomBlockId) {
return [
{
elemtype: "iconbutton",
icon: "bolt",
title: "Switch to Wave App",
click: () => {
this.setTermMode("vdom");
},
},
];
}
}
return null;
});
this.manageConnection = jotai.atom((get) => {
const termMode = get(this.termMode);
if (termMode == "vdom") {
return false;
}
return true;
});
this.blockBg = jotai.atom((get) => {
const blockData = get(this.blockAtom);
const fullConfig = get(atoms.fullConfigAtom);
@ -88,6 +147,28 @@ class TermViewModel {
});
}
setTermMode(mode: "term" | "vdom") {
if (mode == "term") {
mode = null;
}
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:mode": mode },
});
}
getVDomModel(): VDomModel {
const vdomBlockId = globalStore.get(this.vdomBlockId);
if (!vdomBlockId) {
return null;
}
const bcm = getBlockComponentModel(vdomBlockId);
if (!bcm) {
return null;
}
return bcm.viewModel as VDomModel;
}
dispose() {
DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId));
}
@ -107,16 +188,18 @@ class TermViewModel {
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
const blockAtom = WOS.getWaveObjectAtom<Block>(`block:${this.blockId}`);
const blockData = globalStore.get(blockAtom);
const newTermMode = blockData?.meta?.["term:mode"] == "html" ? null : "html";
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:mode": newTermMode },
});
const newTermMode = blockData?.meta?.["term:mode"] == "vdom" ? null : "vdom";
const vdomBlockId = globalStore.get(this.vdomBlockId);
if (newTermMode == "vdom" && !vdomBlockId) {
return;
}
this.setTermMode(newTermMode);
return true;
}
const blockData = globalStore.get(this.blockAtom);
if (blockData.meta?.["term:mode"] == "html") {
return this.vdomModel?.globalKeydownHandler(waveEvent);
if (blockData.meta?.["term:mode"] == "vdom") {
const vdomModel = this.getVDomModel();
return vdomModel?.keyDownHandler(waveEvent);
}
return false;
}
@ -241,6 +324,52 @@ const TermResyncHandler = React.memo(({ blockId, model }: TerminalViewProps) =>
return null;
});
const TermVDomNodeSingleId = ({ vdomBlockId, blockId, model }: TerminalViewProps & { vdomBlockId: string }) => {
React.useEffect(() => {
const unsub = waveEventSubscribe({
eventType: "blockclose",
scope: WOS.makeORef("block", vdomBlockId),
handler: (event) => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", blockId),
meta: {
"term:mode": null,
"term:vdomblockid": null,
},
});
},
});
return () => {
unsub();
};
}, []);
const isFocusedAtom = jotai.atom((get) => {
return get(model.nodeModel.isFocused) && get(model.termMode) == "vdom";
});
let vdomNodeModel = {
blockId: vdomBlockId,
isFocused: isFocusedAtom,
onClose: () => {
if (vdomBlockId != null) {
RpcApi.DeleteSubBlockCommand(TabRpcClient, { blockid: vdomBlockId });
}
},
};
return (
<div key="htmlElem" className="term-htmlelem">
<Block key="vdom" isSubBlock={true} preview={false} nodeModel={vdomNodeModel} />
</div>
);
};
const TermVDomNode = ({ blockId, model }: TerminalViewProps) => {
const vdomBlockId = jotai.useAtomValue(model.vdomBlockId);
if (vdomBlockId == null) {
return null;
}
return <TermVDomNodeSingleId key={vdomBlockId} vdomBlockId={vdomBlockId} blockId={blockId} model={model} />;
};
const TerminalView = ({ blockId, model }: TerminalViewProps) => {
const viewRef = React.useRef<HTMLDivElement>(null);
const connectElemRef = React.useRef<HTMLDivElement>(null);
@ -252,7 +381,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
const termSettingsAtom = useSettingsPrefixAtom("term");
const termSettings = jotai.useAtomValue(termSettingsAtom);
let termMode = blockData?.meta?.["term:mode"] ?? "term";
if (termMode != "term" && termMode != "html") {
if (termMode != "term" && termMode != "vdom") {
termMode = "term";
}
const termModeRef = React.useRef(termMode);
@ -307,7 +436,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
}, [blockId, termSettings]);
React.useEffect(() => {
if (termModeRef.current == "html" && termMode == "term") {
if (termModeRef.current == "vdom" && termMode == "term") {
// focus the terminal
model.giveFocus();
}
@ -356,11 +485,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
<TermThemeUpdater blockId={blockId} termRef={termRef} />
<TermStickers config={stickerConfig} />
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
<div key="htmlElem" className="term-htmlelem">
<div key="htmlElemContent" className="term-htmlelem-content">
<VDomView blockId={blockId} nodeModel={model.nodeModel} viewRef={viewRef} model={model.vdomModel} />
</div>
</div>
<TermVDomNode key="vdom" blockId={blockId} model={model} />
</div>
);
};

View File

@ -1,11 +1,13 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { globalStore, WOS } from "@/app/store/global";
import { getBlockMetaKeyAtom, globalStore, WOS } from "@/app/store/global";
import { makeORef } from "@/app/store/wos";
import { waveEventSubscribe } from "@/app/store/wps";
import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { TermWshClient } from "@/app/view/term/term-wsh";
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
import { NodeModel } from "@/layout/index";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import debug from "debug";
@ -61,22 +63,37 @@ function convertEvent(e: React.SyntheticEvent, fromProp: string): any {
return { type: "unknown" };
}
class VDomWshClient extends WshClient {
model: VDomModel;
constructor(model: VDomModel) {
super(makeFeBlockRouteId(model.blockId));
this.model = model;
}
handle_vdomasyncinitiation(rh: RpcResponseHelper, data: VDomAsyncInitiationRequest) {
console.log("async-initiation", rh.getSource(), data);
this.model.queueUpdate(true);
}
}
export class VDomModel {
blockId: string;
nodeModel: NodeModel;
viewRef: React.RefObject<HTMLDivElement>;
viewType: string;
viewIcon: jotai.Atom<string>;
viewName: jotai.Atom<string>;
viewRef: React.RefObject<HTMLDivElement> = { current: null };
vdomRoot: jotai.PrimitiveAtom<VDomElem> = jotai.atom();
atoms: Map<string, AtomContainer> = new Map(); // key is atomname
refs: Map<string, RefContainer> = new Map(); // key is refid
batchedEvents: VDomEvent[] = [];
messages: VDomMessage[] = [];
needsInitialization: boolean = true;
needsResync: boolean = true;
vdomNodeVersion: WeakMap<VDomElem, jotai.PrimitiveAtom<number>> = new WeakMap();
compoundAtoms: Map<string, jotai.PrimitiveAtom<{ [key: string]: any }>> = new Map();
rootRefId: string = crypto.randomUUID();
termWshClient: TermWshClient;
backendRoute: string;
backendRoute: jotai.Atom<string>;
backendOpts: VDomBackendOpts;
shouldDispose: boolean;
disposed: boolean;
@ -86,18 +103,61 @@ export class VDomModel {
needsImmediateUpdate: boolean;
lastUpdateTs: number = 0;
queuedUpdate: { timeoutId: any; ts: number; quick: boolean };
contextActive: jotai.PrimitiveAtom<boolean>;
wshClient: VDomWshClient;
persist: jotai.Atom<boolean>;
routeGoneUnsub: () => void;
routeConfirmed: boolean = false;
constructor(
blockId: string,
nodeModel: NodeModel,
viewRef: React.RefObject<HTMLDivElement>,
termWshClient: TermWshClient
) {
constructor(blockId: string, nodeModel: NodeModel) {
this.viewType = "vdom";
this.blockId = blockId;
this.nodeModel = nodeModel;
this.viewRef = viewRef;
this.termWshClient = termWshClient;
this.contextActive = jotai.atom(false);
this.reset();
this.viewIcon = jotai.atom("bolt");
this.viewName = jotai.atom("Wave App");
this.backendRoute = jotai.atom((get) => {
const blockData = get(WOS.getWaveObjectAtom<Block>(makeORef("block", this.blockId)));
return blockData?.meta?.["vdom:route"];
});
this.persist = getBlockMetaKeyAtom(this.blockId, "vdom:persist");
this.wshClient = new VDomWshClient(this);
DefaultRouter.registerRoute(this.wshClient.routeId, this.wshClient);
const curBackendRoute = globalStore.get(this.backendRoute);
if (curBackendRoute) {
this.queueUpdate(true);
}
this.routeGoneUnsub = waveEventSubscribe({
eventType: "route:gone",
scope: curBackendRoute,
handler: (event: WaveEvent) => {
this.disposed = true;
const shouldPersist = globalStore.get(this.persist);
if (!shouldPersist) {
this.nodeModel?.onClose?.();
}
},
});
RpcApi.WaitForRouteCommand(TabRpcClient, { routeid: curBackendRoute, waitms: 4000 }, { timeout: 5000 }).then(
(routeOk: boolean) => {
if (routeOk) {
this.routeConfirmed = true;
this.queueUpdate(true);
} else {
this.disposed = true;
const shouldPersist = globalStore.get(this.persist);
if (!shouldPersist) {
this.nodeModel?.onClose?.();
}
}
}
);
}
dispose() {
DefaultRouter.unregisterRoute(this.wshClient.routeId);
this.routeGoneUnsub?.();
}
reset() {
@ -107,11 +167,9 @@ export class VDomModel {
this.batchedEvents = [];
this.messages = [];
this.needsResync = true;
this.needsInitialization = true;
this.vdomNodeVersion = new WeakMap();
this.compoundAtoms.clear();
this.rootRefId = crypto.randomUUID();
this.backendRoute = null;
this.backendOpts = {};
this.shouldDispose = false;
this.disposed = false;
@ -121,9 +179,15 @@ export class VDomModel {
this.needsImmediateUpdate = false;
this.lastUpdateTs = 0;
this.queuedUpdate = null;
globalStore.set(this.contextActive, false);
}
globalKeydownHandler(e: WaveKeyboardEvent): boolean {
getBackendRoute(): string {
const blockData = globalStore.get(WOS.getWaveObjectAtom<Block>(makeORef("block", this.blockId)));
return blockData?.meta?.["vdom:route"];
}
keyDownHandler(e: WaveKeyboardEvent): boolean {
if (this.backendOpts?.closeonctrlc && checkKeyPressed(e, "Ctrl:c")) {
this.shouldDispose = true;
this.queueUpdate(true);
@ -135,7 +199,7 @@ export class VDomModel {
}
this.batchedEvents.push({
waveid: null,
propname: "onKeyDown",
eventtype: "onKeyDown",
eventdata: e,
});
this.queueUpdate();
@ -179,6 +243,9 @@ export class VDomModel {
}
queueUpdate(quick: boolean = false, delay: number = 10) {
if (this.disposed) {
return;
}
this.needsUpdate = true;
let nowTs = Date.now();
if (delay > this.maxNormalUpdateIntervalMs) {
@ -220,7 +287,7 @@ export class VDomModel {
async _sendRenderRequest(force: boolean) {
this.queuedUpdate = null;
if (this.disposed) {
if (this.disposed || !this.routeConfirmed) {
return;
}
if (this.hasPendingRequest) {
@ -232,7 +299,8 @@ export class VDomModel {
if (!force && !this.needsUpdate) {
return;
}
if (this.backendRoute == null) {
const backendRoute = globalStore.get(this.backendRoute);
if (backendRoute == null) {
console.log("vdom-model", "no backend route");
return;
}
@ -241,7 +309,7 @@ export class VDomModel {
try {
const feUpdate = this.createFeUpdate();
dlog("fe-update", feUpdate);
const beUpdate = await RpcApi.VDomRenderCommand(TabRpcClient, feUpdate, { route: this.backendRoute });
const beUpdate = await RpcApi.VDomRenderCommand(TabRpcClient, feUpdate, { route: backendRoute });
this.handleBackendUpdate(beUpdate);
} finally {
this.lastUpdateTs = Date.now();
@ -454,6 +522,7 @@ export class VDomModel {
if (update == null) {
return;
}
globalStore.set(this.contextActive, true);
const idMap = new Map<string, VDomElem>();
const vdomRoot = globalStore.get(this.vdomRoot);
if (update.opts != null) {
@ -478,14 +547,14 @@ export class VDomModel {
if (fnDecl.globalevent) {
const waveEvent: VDomEvent = {
waveid: null,
propname: fnDecl.globalevent,
eventtype: fnDecl.globalevent,
eventdata: eventData,
};
this.batchedEvents.push(waveEvent);
} else {
const vdomEvent: VDomEvent = {
waveid: compId,
propname: propName,
eventtype: propName,
eventdata: eventData,
};
this.batchedEvents.push(vdomEvent);
@ -510,7 +579,6 @@ export class VDomModel {
type: "frontendupdate",
ts: Date.now(),
blockid: this.blockId,
initialize: this.needsInitialization,
rendercontext: renderContext,
dispose: this.shouldDispose,
resync: this.needsResync,
@ -518,7 +586,6 @@ export class VDomModel {
refupdates: this.getRefUpdates(),
};
this.needsResync = false;
this.needsInitialization = false;
this.batchedEvents = [];
if (this.shouldDispose) {
this.disposed = true;

View File

@ -1,10 +1,9 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Markdown } from "@/app/element/markdown";
import { VDomModel } from "@/app/view/term/vdom-model";
import { NodeModel } from "@/layout/index";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import { useAtomValueSafe } from "@/util/util";
import debug from "debug";
import * as jotai from "jotai";
import * as React from "react";
@ -20,6 +19,12 @@ const VDomObjType_Func = "func";
const dlog = debug("wave:vdom");
type VDomReactTagType = (props: { elem: VDomElem; model: VDomModel }) => JSX.Element;
const WaveTagMap: Record<string, VDomReactTagType> = {
"wave:markdown": WaveMarkdown,
};
const AllowedTags: { [tagName: string]: boolean } = {
div: true,
b: true,
@ -191,7 +196,7 @@ function stringSetsEqual(set1: Set<string>, set2: Set<string>): boolean {
return true;
}
function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
function useVDom(model: VDomModel, elem: VDomElem): GenericPropsType {
const version = jotai.useAtomValue(model.getVDomNodeVersionAtom(elem));
const [oldAtomKeys, setOldAtomKeys] = React.useState<Set<string>>(new Set());
let [props, atomKeys] = convertProps(elem, model);
@ -208,18 +213,32 @@ function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
model.tagUnuseAtoms(elem.waveid, oldAtomKeys);
};
}, []);
return props;
}
function WaveMarkdown({ elem, model }: { elem: VDomElem; model: VDomModel }) {
const props = useVDom(model, elem);
return (
<Markdown text={props?.text} style={props?.style} className={props?.className} scrollable={props?.scrollable} />
);
}
function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
const props = useVDom(model, elem);
if (elem.tag == WaveNullTag) {
return null;
}
if (elem.tag == WaveTextTag) {
return props.text;
}
const waveTag = WaveTagMap[elem.tag];
if (waveTag) {
return waveTag({ elem, model });
}
if (!AllowedTags[elem.tag]) {
return <div>{"Invalid Tag <" + elem.tag + ">"}</div>;
}
let childrenComps = convertChildren(elem, model);
dlog("children", childrenComps);
if (elem.tag == FragmentTag) {
return childrenComps;
}
@ -251,25 +270,14 @@ const testVDom: VDomElem = {
],
};
function VDomView({
blockId,
nodeModel,
viewRef,
model,
}: {
blockId: string;
nodeModel: NodeModel;
viewRef: React.RefObject<HTMLDivElement>;
model: VDomModel;
}) {
let rootNode = useAtomValueSafe(model?.vdomRoot);
if (!model || viewRef.current == null || rootNode == null) {
function VDomRoot({ model }: { model: VDomModel }) {
let rootNode = jotai.useAtomValue(model.vdomRoot);
if (model.viewRef.current == null || rootNode == null) {
return null;
}
dlog("render", rootNode);
model.viewRef = viewRef;
let rtn = convertElemToTag(rootNode, model);
return <div className="vdom">{rtn}</div>;
}
export { VDomView };
export { VDomRoot };

View File

@ -0,0 +1,28 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { VDomRoot } from "@/app/view/term/vdom";
import { VDomModel } from "@/app/view/term/vdom-model";
import { NodeModel } from "@/layout/index";
import { useRef } from "react";
function makeVDomModel(blockId: string, nodeModel: NodeModel): VDomModel {
return new VDomModel(blockId, nodeModel);
}
type VDomViewProps = {
model: VDomModel;
blockId: string;
};
function VDomView({ blockId, model }: VDomViewProps) {
let viewRef = useRef(null);
model.viewRef = viewRef;
return (
<div className="vdom-view" ref={viewRef}>
<VDomRoot model={model} />
</div>
);
}
export { makeVDomModel, VDomView };

View File

@ -7,9 +7,11 @@ declare global {
// waveobj.Block
type Block = WaveObj & {
parentoref?: string;
blockdef: BlockDef;
runtimeopts?: RuntimeOpts;
stickers?: StickerType[];
subblockids?: string[];
};
// blockcontroller.BlockControllerRuntimeStatus
@ -30,7 +32,7 @@ declare global {
blockid: string;
tabid: string;
windowid: string;
meta: MetaType;
block: Block;
};
// webcmd.BlockInputWSCommand
@ -96,6 +98,12 @@ declare global {
magnified?: boolean;
};
// wshrpc.CommandCreateSubBlockData
type CommandCreateSubBlockData = {
parentblockid: string;
blockdef: BlockDef;
};
// wshrpc.CommandDeleteBlockData
type CommandDeleteBlockData = {
blockid: string;
@ -167,6 +175,12 @@ declare global {
meta: MetaType;
};
// wshrpc.CommandWaitForRouteData
type CommandWaitForRouteData = {
routeid: string;
waitms: number;
};
// wshrpc.CommandWebSelectorData
type CommandWebSelectorData = {
windowid: string;
@ -341,9 +355,12 @@ declare global {
"term:localshellpath"?: string;
"term:localshellopts"?: string[];
"term:scrollback"?: number;
"term:vdomblockid"?: string;
"vdom:*"?: boolean;
"vdom:initialized"?: boolean;
"vdom:correlationid"?: string;
"vdom:route"?: string;
"vdom:persist"?: boolean;
count?: number;
};
@ -645,7 +662,7 @@ declare global {
type: "createcontext";
ts: number;
meta?: MetaType;
newblock?: boolean;
target?: VDomTarget;
persist?: boolean;
};
@ -661,7 +678,7 @@ declare global {
// vdom.VDomEvent
type VDomEvent = {
waveid: string;
propname: string;
eventtype: string;
eventdata: any;
};
@ -671,7 +688,6 @@ declare global {
ts: number;
blockid: string;
correlationid?: string;
initialize?: boolean;
dispose?: boolean;
resync?: boolean;
rendercontext?: VDomRenderContext;
@ -755,6 +771,12 @@ declare global {
value: any;
};
// vdom.VDomTarget
type VDomTarget = {
newblock?: boolean;
magnified?: boolean;
};
type WSCommandType = {
wscommand: string;
} & ( SetBlockTermSizeWSCommand | BlockInputWSCommand | WSRpcCommand );

View File

@ -36,7 +36,7 @@ const (
const (
BlockFile_Term = "term" // used for main pty output
BlockFile_Html = "html" // used for alt html layout
BlockFile_VDom = "vdom" // used for alt html layout
)
const (

View File

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

View File

@ -4,6 +4,7 @@
package vdom
import (
"encoding/json"
"errors"
"fmt"
"io"
@ -72,7 +73,20 @@ func finalizeStack(stack []*VDomElem) *VDomElem {
return rtnElem
}
func getAttr(token htmltoken.Token, key string) string {
func attrVal(attr htmltoken.Attribute) (any, error) {
// if !attr.IsJson {
// return attr.Val, nil
// }
var val any
err := json.Unmarshal([]byte(attr.Val), &val)
if err != nil {
return nil, fmt.Errorf("error parsing json attr %q: %v", attr.Key, err)
}
return val, nil
}
// returns value, isjson
func getAttrString(token htmltoken.Token, key string) string {
for _, attr := range token.Attr {
if attr.Key == key {
return attr.Val
@ -81,7 +95,7 @@ func getAttr(token htmltoken.Token, key string) string {
return ""
}
func attrToProp(attrVal string, params map[string]any) any {
func attrToProp(attrVal string, isJson bool, params map[string]any) any {
if strings.HasPrefix(attrVal, Html_ParamPrefix) {
bindKey := attrVal[len(Html_ParamPrefix):]
bindVal, ok := params[bindKey]
@ -120,7 +134,7 @@ func tokenToElem(token htmltoken.Token, params map[string]any) *VDomElem {
if attr.Key == "" || attr.Val == "" {
continue
}
propVal := attrToProp(attr.Val, params)
propVal := attrToProp(attr.Val, false, params)
elem.Props[attr.Key] = propVal
}
return elem
@ -253,7 +267,7 @@ func convertStyleToReactStyles(styleMap map[string]string, params map[string]any
}
rtn := make(map[string]any)
for key, val := range styleMap {
rtn[toReactName(key)] = attrToProp(val, params)
rtn[toReactName(key)] = attrToProp(val, false, params)
}
return rtn
}
@ -330,7 +344,7 @@ outer:
elemStack = popElemStack(elemStack)
case htmltoken.SelfClosingTagToken:
if token.Data == Html_BindParamTagName {
keyAttr := getAttr(token, "key")
keyAttr := getAttrString(token, "key")
dataVal := params[keyAttr]
elemList := partToElems(dataVal)
for _, elem := range elemList {
@ -339,7 +353,7 @@ outer:
continue
}
if token.Data == Html_BindTagName {
keyAttr := getAttr(token, "key")
keyAttr := getAttrString(token, "key")
binding := &VDomBinding{Type: ObjectType_Binding, Bind: keyAttr}
appendChildToStack(elemStack, &VDomElem{Tag: WaveTextTag, Props: map[string]any{"text": binding}})
continue

View File

@ -37,7 +37,7 @@ type VDomCreateContext struct {
Type string `json:"type" tstype:"\"createcontext\""`
Ts int64 `json:"ts"`
Meta waveobj.MetaMapType `json:"meta,omitempty"`
NewBlock bool `json:"newblock,omitempty"`
Target *VDomTarget `json:"target,omitempty"`
Persist bool `json:"persist,omitempty"`
}
@ -60,7 +60,6 @@ type VDomFrontendUpdate struct {
Ts int64 `json:"ts"`
BlockId string `json:"blockid"`
CorrelationId string `json:"correlationid,omitempty"`
Initialize bool `json:"initialize,omitempty"` // initialize the app
Dispose bool `json:"dispose,omitempty"` // the vdom context was closed
Resync bool `json:"resync,omitempty"` // resync (send all backend data). useful when the FE reloads
RenderContext VDomRenderContext `json:"rendercontext,omitempty"`
@ -129,8 +128,8 @@ type VDomRefPosition struct {
///// subbordinate protocol types
type VDomEvent struct {
WaveId string `json:"waveid"`
PropName string `json:"propname"`
WaveId string `json:"waveid"` // empty for global events
EventType string `json:"eventtype"`
EventData any `json:"eventdata"`
}
@ -179,6 +178,13 @@ type VDomMessage struct {
Params []any `json:"params,omitempty"`
}
// target -- to support new targets in the future, like toolbars, partial blocks, splits, etc.
// default is vdom context inside of a terminal block
type VDomTarget struct {
NewBlock bool `json:"newblock,omitempty"`
Magnified bool `json:"magnified,omitempty"`
}
// matches WaveKeyboardEvent
type VDomKeyboardEvent struct {
Type string `json:"type"`

View File

@ -13,7 +13,6 @@ import (
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/pkg/vdom"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
@ -21,6 +20,7 @@ import (
)
type Client struct {
Lock *sync.Mutex
Root *vdom.RootElem
RootElem *vdom.VDomElem
RpcClient *wshutil.WshRpc
@ -28,8 +28,8 @@ type Client struct {
ServerImpl *VDomServerImpl
IsDone bool
RouteId string
VDomContextBlockId string
DoneReason string
DoneOnce *sync.Once
DoneCh chan struct{}
Opts vdom.VDomBackendOpts
GlobalEventHandler func(client *Client, event vdom.VDomEvent)
@ -48,7 +48,7 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
impl.Client.doShutdown("got dispose from frontend")
return nil, nil
}
if impl.Client.IsDone {
if impl.Client.GetIsDone() {
return nil, nil
}
// set atoms
@ -62,21 +62,30 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
impl.Client.GlobalEventHandler(impl.Client, event)
}
} else {
impl.Client.Root.Event(event.WaveId, event.PropName, event.EventData)
impl.Client.Root.Event(event.WaveId, event.EventType, event.EventData)
}
}
if feUpdate.Initialize || feUpdate.Resync {
if feUpdate.Resync {
return impl.Client.fullRender()
}
return impl.Client.incrementalRender()
}
func (c *Client) GetIsDone() bool {
c.Lock.Lock()
defer c.Lock.Unlock()
return c.IsDone
}
func (c *Client) doShutdown(reason string) {
c.DoneOnce.Do(func() {
c.Lock.Lock()
defer c.Lock.Unlock()
if c.IsDone {
return
}
c.DoneReason = reason
c.IsDone = true
close(c.DoneCh)
})
}
func (c *Client) SetGlobalEventHandler(handler func(client *Client, event vdom.VDomEvent)) {
@ -85,9 +94,9 @@ func (c *Client) SetGlobalEventHandler(handler func(client *Client, event vdom.V
func MakeClient(opts *vdom.VDomBackendOpts) (*Client, error) {
client := &Client{
Lock: &sync.Mutex{},
Root: vdom.MakeRoot(),
DoneCh: make(chan struct{}),
DoneOnce: &sync.Once{},
}
if opts != nil {
client.Opts = *opts
@ -126,13 +135,29 @@ func (c *Client) SetRootElem(elem *vdom.VDomElem) {
c.RootElem = elem
}
func (c *Client) CreateVDomContext() error {
err := wshclient.VDomCreateContextCommand(c.RpcClient, vdom.VDomCreateContext{}, &wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.RpcContext.BlockId)})
func (c *Client) CreateVDomContext(target *vdom.VDomTarget) error {
blockORef, err := wshclient.VDomCreateContextCommand(
c.RpcClient,
vdom.VDomCreateContext{Target: target},
&wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.RpcContext.BlockId)},
)
if err != nil {
return err
}
wshclient.EventSubCommand(c.RpcClient, wps.SubscriptionRequest{Event: "blockclose", Scopes: []string{
waveobj.MakeORef("block", c.RpcContext.BlockId).String(),
c.VDomContextBlockId = blockORef.OID
log.Printf("created vdom context: %v\n", blockORef)
gotRoute, err := wshclient.WaitForRouteCommand(c.RpcClient, wshrpc.CommandWaitForRouteData{
RouteId: wshutil.MakeFeBlockRouteId(blockORef.OID),
WaitMs: 4000,
}, &wshrpc.RpcOpts{Timeout: 5000})
if err != nil {
return fmt.Errorf("error waiting for vdom context route: %v", err)
}
if !gotRoute {
return fmt.Errorf("vdom context route could not be established")
}
wshclient.EventSubCommand(c.RpcClient, wps.SubscriptionRequest{Event: wps.Event_BlockClose, Scopes: []string{
blockORef.String(),
}}, nil)
c.RpcClient.EventListener.On("blockclose", func(event *wps.WaveEvent) {
c.doShutdown("got blockclose event")
@ -140,8 +165,18 @@ func (c *Client) CreateVDomContext() error {
return nil
}
func (c *Client) SendAsyncInitiation() {
wshclient.VDomAsyncInitiationCommand(c.RpcClient, vdom.MakeAsyncInitiationRequest(c.RpcContext.BlockId), &wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.RpcContext.BlockId)})
func (c *Client) SendAsyncInitiation() error {
if c.VDomContextBlockId == "" {
return fmt.Errorf("no vdom context block id")
}
if c.GetIsDone() {
return fmt.Errorf("client is done")
}
return wshclient.VDomAsyncInitiationCommand(
c.RpcClient,
vdom.MakeAsyncInitiationRequest(c.RpcContext.BlockId),
&wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.VDomContextBlockId)},
)
}
func (c *Client) SetAtomVals(m map[string]any) {

View File

@ -79,10 +79,13 @@ const (
MetaKey_TermLocalShellPath = "term:localshellpath"
MetaKey_TermLocalShellOpts = "term:localshellopts"
MetaKey_TermScrollback = "term:scrollback"
MetaKey_TermVDomSubBlockId = "term:vdomblockid"
MetaKey_VDomClear = "vdom:*"
MetaKey_VDomInitialized = "vdom:initialized"
MetaKey_VDomCorrelationId = "vdom:correlationid"
MetaKey_VDomRoute = "vdom:route"
MetaKey_VDomPersist = "vdom:persist"
MetaKey_Count = "count"
)

View File

@ -94,6 +94,14 @@ func ParseORef(orefStr string) (ORef, error) {
return ORef{OType: otype, OID: oid}, nil
}
func ParseORefNoErr(orefStr string) *ORef {
oref, err := ParseORef(orefStr)
if err != nil {
return nil
}
return &oref
}
type WaveObj interface {
GetOType() string // should not depend on object state (should work with nil value)
}

View File

@ -252,11 +252,13 @@ type WinSize struct {
type Block struct {
OID string `json:"oid"`
ParentORef string `json:"parentoref,omitempty"`
Version int `json:"version"`
BlockDef *BlockDef `json:"blockdef"`
RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"`
Stickers []*StickerType `json:"stickers,omitempty"`
Meta MetaMapType `json:"meta"`
SubBlockIds []string `json:"subblockids,omitempty"`
}
func (*Block) GetOType() string {

View File

@ -80,10 +80,13 @@ type MetaTSType struct {
TermLocalShellPath string `json:"term:localshellpath,omitempty"` // matches settings
TermLocalShellOpts []string `json:"term:localshellopts,omitempty"` // matches settings
TermScrollback *int `json:"term:scrollback,omitempty"`
TermVDomSubBlockId string `json:"term:vdomblockid,omitempty"`
VDomClear bool `json:"vdom:*,omitempty"`
VDomInitialized bool `json:"vdom:initialized,omitempty"`
VDomCorrelationId string `json:"vdom:correlationid,omitempty"`
VDomRoute string `json:"vdom:route,omitempty"`
VDomPersist bool `json:"vdom:persist,omitempty"`
Count int `json:"count,omitempty"` // temp for cpu plot. will remove later
}

View File

@ -26,21 +26,35 @@ import (
const DefaultTimeout = 2 * time.Second
const DefaultActivateBlockTimeout = 60 * time.Second
func DeleteBlock(ctx context.Context, tabId string, blockId string) error {
err := wstore.DeleteBlock(ctx, tabId, blockId)
func DeleteBlock(ctx context.Context, blockId string) error {
block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
if err != nil {
return fmt.Errorf("error getting block: %w", err)
}
if block == nil {
return nil
}
if len(block.SubBlockIds) > 0 {
for _, subBlockId := range block.SubBlockIds {
err := DeleteBlock(ctx, subBlockId)
if err != nil {
return fmt.Errorf("error deleting subblock %s: %w", subBlockId, err)
}
}
}
err = wstore.DeleteBlock(ctx, blockId)
if err != nil {
return fmt.Errorf("error deleting block: %w", err)
}
go blockcontroller.StopBlockController(blockId)
sendBlockCloseEvent(tabId, blockId)
sendBlockCloseEvent(blockId)
return nil
}
func sendBlockCloseEvent(tabId string, blockId string) {
func sendBlockCloseEvent(blockId string) {
waveEvent := wps.WaveEvent{
Event: wps.Event_BlockClose,
Scopes: []string{
waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
waveobj.MakeORef(waveobj.OType_Block, blockId).String(),
},
Data: blockId,
@ -58,7 +72,7 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string) error {
}
// close blocks (sends events + stops block controllers)
for _, blockId := range tabData.BlockIds {
err := DeleteBlock(ctx, tabId, blockId)
err := DeleteBlock(ctx, blockId)
if err != nil {
return fmt.Errorf("error deleting block %s: %w", blockId, err)
}
@ -205,6 +219,20 @@ func CreateClient(ctx context.Context) (*waveobj.Client, error) {
return client, nil
}
func CreateSubBlock(ctx context.Context, blockId string, blockDef *waveobj.BlockDef) (*waveobj.Block, error) {
if blockDef == nil {
return nil, fmt.Errorf("blockDef is nil")
}
if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" {
return nil, fmt.Errorf("no view provided for new block")
}
blockData, err := wstore.CreateSubBlock(ctx, blockId, blockDef)
if err != nil {
return nil, fmt.Errorf("error creating sub block: %w", err)
}
return blockData, nil
}
func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
if blockDef == nil {
return nil, fmt.Errorf("blockDef is nil")

View File

@ -86,12 +86,24 @@ func CreateBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandCreateBlockData, o
return resp, err
}
// command "createsubblock", wshserver.CreateSubBlockCommand
func CreateSubBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandCreateSubBlockData, opts *wshrpc.RpcOpts) (waveobj.ORef, error) {
resp, err := sendRpcRequestCallHelper[waveobj.ORef](w, "createsubblock", data, opts)
return resp, err
}
// command "deleteblock", wshserver.DeleteBlockCommand
func DeleteBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandDeleteBlockData, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "deleteblock", data, opts)
return err
}
// command "deletesubblock", wshserver.DeleteSubBlockCommand
func DeleteSubBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandDeleteBlockData, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "deletesubblock", data, opts)
return err
}
// command "dispose", wshserver.DisposeCommand
func DisposeCommand(w *wshutil.WshRpc, data wshrpc.CommandDisposeData, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "dispose", data, opts)
@ -274,9 +286,9 @@ func VDomAsyncInitiationCommand(w *wshutil.WshRpc, data vdom.VDomAsyncInitiation
}
// command "vdomcreatecontext", wshserver.VDomCreateContextCommand
func VDomCreateContextCommand(w *wshutil.WshRpc, data vdom.VDomCreateContext, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "vdomcreatecontext", data, opts)
return err
func VDomCreateContextCommand(w *wshutil.WshRpc, data vdom.VDomCreateContext, opts *wshrpc.RpcOpts) (*waveobj.ORef, error) {
resp, err := sendRpcRequestCallHelper[*waveobj.ORef](w, "vdomcreatecontext", data, opts)
return resp, err
}
// command "vdomrender", wshserver.VDomRenderCommand
@ -285,6 +297,12 @@ func VDomRenderCommand(w *wshutil.WshRpc, data vdom.VDomFrontendUpdate, opts *ws
return resp, err
}
// command "waitforroute", wshserver.WaitForRouteCommand
func WaitForRouteCommand(w *wshutil.WshRpc, data wshrpc.CommandWaitForRouteData, opts *wshrpc.RpcOpts) (bool, error) {
resp, err := sendRpcRequestCallHelper[bool](w, "waitforroute", data, opts)
return resp, err
}
// command "webselector", wshserver.WebSelectorCommand
func WebSelectorCommand(w *wshutil.WshRpc, data wshrpc.CommandWebSelectorData, opts *wshrpc.RpcOpts) ([]string, error) {
resp, err := sendRpcRequestCallHelper[[]string](w, "webselector", data, opts)

View File

@ -103,7 +103,10 @@ type WshRpcInterface interface {
FileAppendIJsonCommand(ctx context.Context, data CommandAppendIJsonData) error
ResolveIdsCommand(ctx context.Context, data CommandResolveIdsData) (CommandResolveIdsRtnData, error)
CreateBlockCommand(ctx context.Context, data CommandCreateBlockData) (waveobj.ORef, error)
CreateSubBlockCommand(ctx context.Context, data CommandCreateSubBlockData) (waveobj.ORef, error)
DeleteBlockCommand(ctx context.Context, data CommandDeleteBlockData) error
DeleteSubBlockCommand(ctx context.Context, data CommandDeleteBlockData) error
WaitForRouteCommand(ctx context.Context, data CommandWaitForRouteData) (bool, error)
FileWriteCommand(ctx context.Context, data CommandFileData) error
FileReadCommand(ctx context.Context, data CommandFileData) (string, error)
EventPublishCommand(ctx context.Context, data wps.WaveEvent) error
@ -145,7 +148,7 @@ type WshRpcInterface interface {
NotifyCommand(ctx context.Context, notificationOptions WaveNotificationOptions) error
// terminal
VDomCreateContextCommand(ctx context.Context, data vdom.VDomCreateContext) error
VDomCreateContextCommand(ctx context.Context, data vdom.VDomCreateContext) (*waveobj.ORef, error)
VDomAsyncInitiationCommand(ctx context.Context, data vdom.VDomAsyncInitiationRequest) error
// proc
@ -248,6 +251,11 @@ type CommandCreateBlockData struct {
Magnified bool `json:"magnified,omitempty"`
}
type CommandCreateSubBlockData struct {
ParentBlockId string `json:"parentblockid"`
BlockDef *waveobj.BlockDef `json:"blockdef"`
}
type CommandBlockSetViewData struct {
BlockId string `json:"blockid" wshcontext:"BlockId"`
View string `json:"view"`
@ -279,6 +287,11 @@ type CommandAppendIJsonData struct {
Data ijson.Command `json:"data"`
}
type CommandWaitForRouteData struct {
RouteId string `json:"routeid"`
WaitMs int `json:"waitms"`
}
type CommandDeleteBlockData struct {
BlockId string `json:"blockid" wshcontext:"BlockId"`
}
@ -405,7 +418,7 @@ type BlockInfoData struct {
BlockId string `json:"blockid"`
TabId string `json:"tabid"`
WindowId string `json:"windowid"`
Meta waveobj.MetaMapType `json:"meta"`
Block *waveobj.Block `json:"block"`
}
type WaveNotificationOptions struct {

View File

@ -250,6 +250,16 @@ func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.Command
return &waveobj.ORef{OType: waveobj.OType_Block, OID: blockRef.OID}, nil
}
func (ws *WshServer) CreateSubBlockCommand(ctx context.Context, data wshrpc.CommandCreateSubBlockData) (*waveobj.ORef, error) {
parentBlockId := data.ParentBlockId
blockData, err := wcore.CreateSubBlock(ctx, parentBlockId, data.BlockDef)
if err != nil {
return nil, fmt.Errorf("error creating block: %w", err)
}
blockRef := &waveobj.ORef{OType: waveobj.OType_Block, OID: blockData.OID}
return blockRef, nil
}
func (ws *WshServer) SetViewCommand(ctx context.Context, data wshrpc.CommandBlockSetViewData) error {
log.Printf("SETVIEW: %s | %q\n", data.BlockId, data.View)
ctx = waveobj.ContextWithUpdates(ctx)
@ -356,10 +366,10 @@ func (ws *WshServer) FileAppendCommand(ctx context.Context, data wshrpc.CommandF
func (ws *WshServer) FileAppendIJsonCommand(ctx context.Context, data wshrpc.CommandAppendIJsonData) error {
tryCreate := true
if data.FileName == blockcontroller.BlockFile_Html && tryCreate {
if data.FileName == blockcontroller.BlockFile_VDom && tryCreate {
err := filestore.WFS.MakeFile(ctx, data.ZoneId, data.FileName, nil, filestore.FileOptsType{MaxSize: blockcontroller.DefaultHtmlMaxFileSize, IJson: true})
if err != nil && err != fs.ErrExist {
return fmt.Errorf("error creating blockfile[html]: %w", err)
return fmt.Errorf("error creating blockfile[vdom]: %w", err)
}
}
err := filestore.WFS.AppendIJson(ctx, data.ZoneId, data.FileName, data.Data)
@ -379,6 +389,14 @@ func (ws *WshServer) FileAppendIJsonCommand(ctx context.Context, data wshrpc.Com
return nil
}
func (ws *WshServer) DeleteSubBlockCommand(ctx context.Context, data wshrpc.CommandDeleteBlockData) error {
err := wcore.DeleteBlock(ctx, data.BlockId)
if err != nil {
return fmt.Errorf("error deleting block: %w", err)
}
return nil
}
func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.CommandDeleteBlockData) error {
ctx = waveobj.ContextWithUpdates(ctx)
tabId, err := wstore.DBFindTabForBlockId(ctx, data.BlockId)
@ -395,7 +413,7 @@ func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.Command
if windowId == "" {
return fmt.Errorf("no window found for tab")
}
err = wcore.DeleteBlock(ctx, tabId, data.BlockId)
err = wcore.DeleteBlock(ctx, data.BlockId)
if err != nil {
return fmt.Errorf("error deleting block: %w", err)
}
@ -408,6 +426,13 @@ func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.Command
return nil
}
func (ws *WshServer) WaitForRouteCommand(ctx context.Context, data wshrpc.CommandWaitForRouteData) (bool, error) {
waitCtx, cancelFn := context.WithTimeout(ctx, time.Duration(data.WaitMs)*time.Millisecond)
defer cancelFn()
err := wshutil.DefaultRouter.WaitForRegister(waitCtx, data.RouteId)
return err == nil, nil
}
func (ws *WshServer) EventRecvCommand(ctx context.Context, data wps.WaveEvent) error {
return nil
}
@ -587,6 +612,6 @@ func (ws *WshServer) BlockInfoCommand(ctx context.Context, blockId string) (*wsh
BlockId: blockId,
TabId: tabId,
WindowId: windowId,
Meta: blockData.Meta,
Block: blockData,
}, nil
}

View File

@ -268,6 +268,9 @@ func (router *WshRouter) WaitForRegister(ctx context.Context, routeId string) er
if router.GetRpc(routeId) != nil {
return nil
}
if router.getAnnouncedRoute(routeId) != "" {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()

View File

@ -95,6 +95,27 @@ 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)
@ -104,6 +125,7 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
blockId := uuid.NewString()
blockData := &waveobj.Block{
OID: blockId,
ParentORef: waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
BlockDef: blockDef,
RuntimeOpts: rtOpts,
Meta: blockDef.Meta,
@ -124,18 +146,34 @@ func findStringInSlice(slice []string, val string) int {
return -1
}
func DeleteBlock(ctx context.Context, tabId string, blockId string) error {
func DeleteBlock(ctx context.Context, blockId string) error {
return WithTx(ctx, func(tx *TxWrap) error {
tab, _ := DBGet[*waveobj.Tab](tx.Context(), tabId)
if tab == nil {
return fmt.Errorf("tab not found: %q", tabId)
block, err := DBGet[*waveobj.Block](tx.Context(), blockId)
if err != nil {
return fmt.Errorf("error getting block: %w", err)
}
blockIdx := findStringInSlice(tab.BlockIds, blockId)
if blockIdx == -1 {
if block == nil {
return nil
}
tab.BlockIds = append(tab.BlockIds[:blockIdx], tab.BlockIds[blockIdx+1:]...)
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
})
@ -145,23 +183,18 @@ func DeleteBlock(ctx context.Context, tabId string, blockId string) error {
// also deletes LayoutState
func DeleteTab(ctx context.Context, workspaceId string, tabId string) error {
return WithTx(ctx, func(tx *TxWrap) error {
ws, _ := DBGet[*waveobj.Workspace](tx.Context(), workspaceId)
if ws == nil {
return fmt.Errorf("workspace not found: %q", workspaceId)
}
tab, _ := DBGet[*waveobj.Tab](tx.Context(), tabId)
if tab == nil {
return fmt.Errorf("tab not found: %q", tabId)
return nil
}
if len(tab.BlockIds) != 0 {
return fmt.Errorf("tab has blocks, must delete blocks first")
}
tabIdx := findStringInSlice(ws.TabIds, tabId)
if tabIdx == -1 {
return nil
}
ws.TabIds = append(ws.TabIds[:tabIdx], ws.TabIds[tabIdx+1:]...)
ws, _ := DBGet[*waveobj.Workspace](tx.Context(), workspaceId)
if ws != nil {
ws.TabIds = utilfn.RemoveElemFromSlice(ws.TabIds, tabId)
DBUpdate(tx.Context(), ws)
}
DBDelete(tx.Context(), waveobj.OType_Tab, tabId)
DBDelete(tx.Context(), waveobj.OType_LayoutState, tab.LayoutState)
return nil
@ -190,6 +223,10 @@ func UpdateObjectMeta(ctx context.Context, oref waveobj.ORef, meta waveobj.MetaM
func MoveBlockToTab(ctx context.Context, currentTabId string, newTabId string, blockId string) error {
return WithTx(ctx, func(tx *TxWrap) error {
block, _ := DBGet[*waveobj.Block](tx.Context(), blockId)
if block == nil {
return fmt.Errorf("block not found: %q", blockId)
}
currentTab, _ := DBGet[*waveobj.Tab](tx.Context(), currentTabId)
if currentTab == nil {
return fmt.Errorf("current tab not found: %q", currentTabId)
@ -204,6 +241,8 @@ func MoveBlockToTab(ctx context.Context, currentTabId string, newTabId string, b
}
currentTab.BlockIds = utilfn.RemoveElemFromSlice(currentTab.BlockIds, blockId)
newTab.BlockIds = append(newTab.BlockIds, blockId)
block.ParentORef = waveobj.MakeORef(waveobj.OType_Tab, newTabId).String()
DBUpdate(tx.Context(), block)
DBUpdate(tx.Context(), currentTab)
DBUpdate(tx.Context(), newTab)
return nil