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 return
} }
doneCh := make(chan bool) 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()) { if event.HasScope(blockRef.String()) {
close(doneCh) 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 <-doneCh
} }

View File

@ -13,7 +13,10 @@ import (
"github.com/wavetermdev/waveterm/pkg/wshutil" "github.com/wavetermdev/waveterm/pkg/wshutil"
) )
var htmlCmdNewBlock bool
func init() { func init() {
htmlCmd.Flags().BoolVarP(&htmlCmdNewBlock, "newblock", "n", false, "create a new block")
rootCmd.AddCommand(htmlCmd) 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> <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><bind key="$.text"/> | num[<bind key="$.num"/>]</div>
<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>
</div> </div>
` `
@ -39,7 +45,7 @@ func MakeVDom() *vdom.VDomElem {
} }
func GlobalEventHandler(client *vdomclient.Client, event vdom.VDomEvent) { func GlobalEventHandler(client *vdomclient.Client, event vdom.VDomEvent) {
if event.PropName == "clickinc" { if event.EventType == "clickinc" {
client.SetAtomVal("num", client.GetAtomVal("num").(int)+1) client.SetAtomVal("num", client.GetAtomVal("num").(int)+1)
return return
} }
@ -58,7 +64,7 @@ func htmlRun(cmd *cobra.Command, args []string) error {
client.SetAtomVal("text", "initial text") client.SetAtomVal("text", "initial text")
client.SetAtomVal("num", 0) client.SetAtomVal("num", 0)
client.SetRootElem(MakeVDom()) client.SetRootElem(MakeVDom())
err = client.CreateVDomContext() err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: htmlCmdNewBlock})
if err != nil { if err != nil {
return err return err
} }
@ -70,8 +76,12 @@ func htmlRun(cmd *cobra.Command, args []string) error {
log.Printf("created vdom context\n") log.Printf("created vdom context\n")
go func() { go func() {
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
log.Printf("updating text\n")
client.SetAtomVal("text", "updated text") client.SetAtomVal("text", "updated text")
client.SendAsyncInitiation() err := client.SendAsyncInitiation()
if err != nil {
log.Printf("error sending async initiation: %v\n", err)
}
}() }()
<-client.DoneCh <-client.DoneCh
return nil return nil

View File

@ -71,6 +71,22 @@ func preRunSetupRpcClient(cmd *cobra.Command, args []string) error {
return nil 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) // returns the wrapped stdin and a new rpc client (that wraps the stdin input and stdout output)
func setupRpcClient(serverImpl wshutil.ServerImpl) error { func setupRpcClient(serverImpl wshutil.ServerImpl) error {
jwtToken := os.Getenv(wshutil.WaveJwtTokenVarName) jwtToken := os.Getenv(wshutil.WaveJwtTokenVarName)
@ -101,7 +117,7 @@ func setupRpcClient(serverImpl wshutil.ServerImpl) error {
func setTermHtmlMode() { func setTermHtmlMode() {
wshutil.SetExtraShutdownFunc(extraShutdownFn) wshutil.SetExtraShutdownFunc(extraShutdownFn)
cmd := &wshrpc.CommandSetMetaData{ 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) err := RpcClient.SendCommand(wshrpc.Command_SetMeta, cmd, nil)
if err != nil { if err != nil {

View File

@ -28,7 +28,7 @@ var webOpenCmd = &cobra.Command{
} }
var webGetCmd = &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", Short: "get the html for a css selector",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Hidden: true, Hidden: true,
@ -67,7 +67,7 @@ func webGetRun(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return fmt.Errorf("getting block info: %w", err) 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) return fmt.Errorf("block %s is not a web block", fullORef.OID)
} }
data := wshrpc.CommandWebSelectorData{ 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 { PlotView } from "@/app/view/plotview/plotview";
import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview"; import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview";
import { SysinfoView, SysinfoViewModel, makeSysinfoViewModel } from "@/app/view/sysinfo/sysinfo"; 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 { ErrorBoundary } from "@/element/errorboundary";
import { CenteredDiv } from "@/element/quickelems"; import { CenteredDiv } from "@/element/quickelems";
import { NodeModel, useDebouncedNodeInnerRect } from "@/layout/index"; import { NodeModel, useDebouncedNodeInnerRect } from "@/layout/index";
@ -29,6 +31,7 @@ import { BlockFrame } from "./blockframe";
import { blockViewToIcon, blockViewToName } from "./blockutil"; import { blockViewToIcon, blockViewToName } from "./blockutil";
type FullBlockProps = { type FullBlockProps = {
isSubBlock?: boolean;
preview: boolean; preview: boolean;
nodeModel: NodeModel; nodeModel: NodeModel;
viewModel: ViewModel; viewModel: ViewModel;
@ -51,6 +54,9 @@ function makeViewModel(blockId: string, blockView: string, nodeModel: NodeModel)
// "cpuplot" is for backwards compatibility with already-opened widgets // "cpuplot" is for backwards compatibility with already-opened widgets
return makeSysinfoViewModel(blockId, blockView); return makeSysinfoViewModel(blockId, blockView);
} }
if (blockView == "vdom") {
return makeVDomModel(blockId, nodeModel);
}
if (blockView === "help") { if (blockView === "help") {
return makeHelpViewModel(blockId, nodeModel); return makeHelpViewModel(blockId, nodeModel);
} }
@ -100,6 +106,9 @@ function getViewElem(
if (blockView == "tips") { if (blockView == "tips") {
return <QuickTipsView key={blockId} model={viewModel as QuickTipsViewModel} />; 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>; 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) => { const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
counterInc("render-BlockFull"); counterInc("render-BlockFull");
const focusElemRef = useRef<HTMLInputElement>(null); const focusElemRef = useRef<HTMLInputElement>(null);
@ -275,6 +304,9 @@ const Block = memo((props: BlockProps) => {
if (props.preview) { if (props.preview) {
return <BlockPreview {...props} viewModel={viewModel} />; return <BlockPreview {...props} viewModel={viewModel} />;
} }
if (props.isSubBlock) {
return <BlockSubBlock {...props} viewModel={viewModel} />;
}
return <BlockFull {...props} viewModel={viewModel} />; return <BlockFull {...props} viewModel={viewModel} />;
}); });

View File

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

View File

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

View File

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

View File

@ -67,11 +67,21 @@ class RpcApiType {
return client.wshRpcCall("createblock", data, opts); 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] // command "deleteblock" [call]
DeleteBlockCommand(client: WshClient, data: CommandDeleteBlockData, opts?: RpcOpts): Promise<void> { DeleteBlockCommand(client: WshClient, data: CommandDeleteBlockData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("deleteblock", data, opts); 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] // command "dispose" [call]
DisposeCommand(client: WshClient, data: CommandDisposeData, opts?: RpcOpts): Promise<void> { DisposeCommand(client: WshClient, data: CommandDisposeData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("dispose", data, opts); return client.wshRpcCall("dispose", data, opts);
@ -228,7 +238,7 @@ class RpcApiType {
} }
// command "vdomcreatecontext" [call] // 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); return client.wshRpcCall("vdomcreatecontext", data, opts);
} }
@ -237,6 +247,11 @@ class RpcApiType {
return client.wshRpcCall("vdomrender", data, opts); 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] // command "webselector" [call]
WebSelectorCommand(client: WshClient, data: CommandWebSelectorData, opts?: RpcOpts): Promise<string[]> { WebSelectorCommand(client: WshClient, data: CommandWebSelectorData, opts?: RpcOpts): Promise<string[]> {
return client.wshRpcCall("webselector", data, opts); return client.wshRpcCall("webselector", data, opts);

View File

@ -1,12 +1,13 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { WOS } from "@/app/store/global"; import { atoms, globalStore } from "@/app/store/global";
import { waveEventSubscribe } from "@/app/store/wps"; import { makeORef, splitORef } from "@/app/store/wos";
import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
import { RpcApi } from "@/app/store/wshclientapi"; import { RpcApi } from "@/app/store/wshclientapi";
import { makeFeBlockRouteId } from "@/app/store/wshrouter"; import { makeFeBlockRouteId } from "@/app/store/wshrouter";
import { TermViewModel } from "@/app/view/term/term"; import { TermViewModel } from "@/app/view/term/term";
import { isBlank } from "@/util/util";
import debug from "debug"; import debug from "debug";
const dlog = debug("wave:vdom"); const dlog = debug("wave:vdom");
@ -21,32 +22,55 @@ export class TermWshClient extends WshClient {
this.model = model; this.model = model;
} }
handle_vdomcreatecontext(rh: RpcResponseHelper, data: VDomCreateContext) { async handle_vdomcreatecontext(rh: RpcResponseHelper, data: VDomCreateContext) {
console.log("vdom-create", rh.getSource(), data); const source = rh.getSource();
this.model.vdomModel.reset(); if (isBlank(source)) {
this.model.vdomModel.backendRoute = rh.getSource(); throw new Error("source cannot be blank");
if (!data.persist) { }
const unsubFn = waveEventSubscribe({ console.log("vdom-create", source, data);
eventType: "route:gone", const tabId = globalStore.get(atoms.staticTabId);
scope: rh.getSource(), if (data.target?.newblock) {
handler: () => { const oref = await RpcApi.CreateBlockCommand(this, {
RpcApi.SetMetaCommand(this, { tabid: tabId,
oref: WOS.makeORef("block", this.blockId), blockdef: {
meta: { "term:mode": null }, meta: {
}); view: "vdom",
unsubFn(); "vdom:route": rh.getSource(),
},
},
magnified: data.target?.magnified,
});
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: makeORef("block", this.model.blockId),
meta: {
"term:mode": "vdom",
"term:vdomblockid": newVDomBlockId,
},
});
}, 50);
return oref;
} }
RpcApi.SetMetaCommand(this, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:mode": "html" },
});
this.model.vdomModel.queueUpdate(true);
}
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 { .term-connectelem {
display: none; display: none;
} }

View File

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

View File

@ -1,11 +1,13 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // 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 { 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 { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil"; import { makeFeBlockRouteId } from "@/app/store/wshrouter";
import { TermWshClient } from "@/app/view/term/term-wsh"; import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
import { NodeModel } from "@/layout/index"; import { NodeModel } from "@/layout/index";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import debug from "debug"; import debug from "debug";
@ -61,22 +63,37 @@ function convertEvent(e: React.SyntheticEvent, fromProp: string): any {
return { type: "unknown" }; 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 { export class VDomModel {
blockId: string; blockId: string;
nodeModel: NodeModel; 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(); vdomRoot: jotai.PrimitiveAtom<VDomElem> = jotai.atom();
atoms: Map<string, AtomContainer> = new Map(); // key is atomname atoms: Map<string, AtomContainer> = new Map(); // key is atomname
refs: Map<string, RefContainer> = new Map(); // key is refid refs: Map<string, RefContainer> = new Map(); // key is refid
batchedEvents: VDomEvent[] = []; batchedEvents: VDomEvent[] = [];
messages: VDomMessage[] = []; messages: VDomMessage[] = [];
needsInitialization: boolean = true;
needsResync: boolean = true; needsResync: boolean = true;
vdomNodeVersion: WeakMap<VDomElem, jotai.PrimitiveAtom<number>> = new WeakMap(); vdomNodeVersion: WeakMap<VDomElem, jotai.PrimitiveAtom<number>> = new WeakMap();
compoundAtoms: Map<string, jotai.PrimitiveAtom<{ [key: string]: any }>> = new Map(); compoundAtoms: Map<string, jotai.PrimitiveAtom<{ [key: string]: any }>> = new Map();
rootRefId: string = crypto.randomUUID(); rootRefId: string = crypto.randomUUID();
termWshClient: TermWshClient; backendRoute: jotai.Atom<string>;
backendRoute: string;
backendOpts: VDomBackendOpts; backendOpts: VDomBackendOpts;
shouldDispose: boolean; shouldDispose: boolean;
disposed: boolean; disposed: boolean;
@ -86,18 +103,61 @@ export class VDomModel {
needsImmediateUpdate: boolean; needsImmediateUpdate: boolean;
lastUpdateTs: number = 0; lastUpdateTs: number = 0;
queuedUpdate: { timeoutId: any; ts: number; quick: boolean }; queuedUpdate: { timeoutId: any; ts: number; quick: boolean };
contextActive: jotai.PrimitiveAtom<boolean>;
wshClient: VDomWshClient;
persist: jotai.Atom<boolean>;
routeGoneUnsub: () => void;
routeConfirmed: boolean = false;
constructor( constructor(blockId: string, nodeModel: NodeModel) {
blockId: string, this.viewType = "vdom";
nodeModel: NodeModel,
viewRef: React.RefObject<HTMLDivElement>,
termWshClient: TermWshClient
) {
this.blockId = blockId; this.blockId = blockId;
this.nodeModel = nodeModel; this.nodeModel = nodeModel;
this.viewRef = viewRef; this.contextActive = jotai.atom(false);
this.termWshClient = termWshClient;
this.reset(); 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() { reset() {
@ -107,11 +167,9 @@ export class VDomModel {
this.batchedEvents = []; this.batchedEvents = [];
this.messages = []; this.messages = [];
this.needsResync = true; this.needsResync = true;
this.needsInitialization = true;
this.vdomNodeVersion = new WeakMap(); this.vdomNodeVersion = new WeakMap();
this.compoundAtoms.clear(); this.compoundAtoms.clear();
this.rootRefId = crypto.randomUUID(); this.rootRefId = crypto.randomUUID();
this.backendRoute = null;
this.backendOpts = {}; this.backendOpts = {};
this.shouldDispose = false; this.shouldDispose = false;
this.disposed = false; this.disposed = false;
@ -121,9 +179,15 @@ export class VDomModel {
this.needsImmediateUpdate = false; this.needsImmediateUpdate = false;
this.lastUpdateTs = 0; this.lastUpdateTs = 0;
this.queuedUpdate = null; 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")) { if (this.backendOpts?.closeonctrlc && checkKeyPressed(e, "Ctrl:c")) {
this.shouldDispose = true; this.shouldDispose = true;
this.queueUpdate(true); this.queueUpdate(true);
@ -135,7 +199,7 @@ export class VDomModel {
} }
this.batchedEvents.push({ this.batchedEvents.push({
waveid: null, waveid: null,
propname: "onKeyDown", eventtype: "onKeyDown",
eventdata: e, eventdata: e,
}); });
this.queueUpdate(); this.queueUpdate();
@ -179,6 +243,9 @@ export class VDomModel {
} }
queueUpdate(quick: boolean = false, delay: number = 10) { queueUpdate(quick: boolean = false, delay: number = 10) {
if (this.disposed) {
return;
}
this.needsUpdate = true; this.needsUpdate = true;
let nowTs = Date.now(); let nowTs = Date.now();
if (delay > this.maxNormalUpdateIntervalMs) { if (delay > this.maxNormalUpdateIntervalMs) {
@ -220,7 +287,7 @@ export class VDomModel {
async _sendRenderRequest(force: boolean) { async _sendRenderRequest(force: boolean) {
this.queuedUpdate = null; this.queuedUpdate = null;
if (this.disposed) { if (this.disposed || !this.routeConfirmed) {
return; return;
} }
if (this.hasPendingRequest) { if (this.hasPendingRequest) {
@ -232,7 +299,8 @@ export class VDomModel {
if (!force && !this.needsUpdate) { if (!force && !this.needsUpdate) {
return; return;
} }
if (this.backendRoute == null) { const backendRoute = globalStore.get(this.backendRoute);
if (backendRoute == null) {
console.log("vdom-model", "no backend route"); console.log("vdom-model", "no backend route");
return; return;
} }
@ -241,7 +309,7 @@ export class VDomModel {
try { try {
const feUpdate = this.createFeUpdate(); const feUpdate = this.createFeUpdate();
dlog("fe-update", feUpdate); 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); this.handleBackendUpdate(beUpdate);
} finally { } finally {
this.lastUpdateTs = Date.now(); this.lastUpdateTs = Date.now();
@ -454,6 +522,7 @@ export class VDomModel {
if (update == null) { if (update == null) {
return; return;
} }
globalStore.set(this.contextActive, true);
const idMap = new Map<string, VDomElem>(); const idMap = new Map<string, VDomElem>();
const vdomRoot = globalStore.get(this.vdomRoot); const vdomRoot = globalStore.get(this.vdomRoot);
if (update.opts != null) { if (update.opts != null) {
@ -478,14 +547,14 @@ export class VDomModel {
if (fnDecl.globalevent) { if (fnDecl.globalevent) {
const waveEvent: VDomEvent = { const waveEvent: VDomEvent = {
waveid: null, waveid: null,
propname: fnDecl.globalevent, eventtype: fnDecl.globalevent,
eventdata: eventData, eventdata: eventData,
}; };
this.batchedEvents.push(waveEvent); this.batchedEvents.push(waveEvent);
} else { } else {
const vdomEvent: VDomEvent = { const vdomEvent: VDomEvent = {
waveid: compId, waveid: compId,
propname: propName, eventtype: propName,
eventdata: eventData, eventdata: eventData,
}; };
this.batchedEvents.push(vdomEvent); this.batchedEvents.push(vdomEvent);
@ -510,7 +579,6 @@ export class VDomModel {
type: "frontendupdate", type: "frontendupdate",
ts: Date.now(), ts: Date.now(),
blockid: this.blockId, blockid: this.blockId,
initialize: this.needsInitialization,
rendercontext: renderContext, rendercontext: renderContext,
dispose: this.shouldDispose, dispose: this.shouldDispose,
resync: this.needsResync, resync: this.needsResync,
@ -518,7 +586,6 @@ export class VDomModel {
refupdates: this.getRefUpdates(), refupdates: this.getRefUpdates(),
}; };
this.needsResync = false; this.needsResync = false;
this.needsInitialization = false;
this.batchedEvents = []; this.batchedEvents = [];
if (this.shouldDispose) { if (this.shouldDispose) {
this.disposed = true; this.disposed = true;

View File

@ -1,10 +1,9 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Markdown } from "@/app/element/markdown";
import { VDomModel } from "@/app/view/term/vdom-model"; import { VDomModel } from "@/app/view/term/vdom-model";
import { NodeModel } from "@/layout/index";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import { useAtomValueSafe } from "@/util/util";
import debug from "debug"; import debug from "debug";
import * as jotai from "jotai"; import * as jotai from "jotai";
import * as React from "react"; import * as React from "react";
@ -20,6 +19,12 @@ const VDomObjType_Func = "func";
const dlog = debug("wave:vdom"); 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 } = { const AllowedTags: { [tagName: string]: boolean } = {
div: true, div: true,
b: true, b: true,
@ -191,7 +196,7 @@ function stringSetsEqual(set1: Set<string>, set2: Set<string>): boolean {
return true; 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 version = jotai.useAtomValue(model.getVDomNodeVersionAtom(elem));
const [oldAtomKeys, setOldAtomKeys] = React.useState<Set<string>>(new Set()); const [oldAtomKeys, setOldAtomKeys] = React.useState<Set<string>>(new Set());
let [props, atomKeys] = convertProps(elem, model); let [props, atomKeys] = convertProps(elem, model);
@ -208,18 +213,32 @@ function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
model.tagUnuseAtoms(elem.waveid, oldAtomKeys); 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) { if (elem.tag == WaveNullTag) {
return null; return null;
} }
if (elem.tag == WaveTextTag) { if (elem.tag == WaveTextTag) {
return props.text; return props.text;
} }
const waveTag = WaveTagMap[elem.tag];
if (waveTag) {
return waveTag({ elem, model });
}
if (!AllowedTags[elem.tag]) { if (!AllowedTags[elem.tag]) {
return <div>{"Invalid Tag <" + elem.tag + ">"}</div>; return <div>{"Invalid Tag <" + elem.tag + ">"}</div>;
} }
let childrenComps = convertChildren(elem, model); let childrenComps = convertChildren(elem, model);
dlog("children", childrenComps);
if (elem.tag == FragmentTag) { if (elem.tag == FragmentTag) {
return childrenComps; return childrenComps;
} }
@ -251,25 +270,14 @@ const testVDom: VDomElem = {
], ],
}; };
function VDomView({ function VDomRoot({ model }: { model: VDomModel }) {
blockId, let rootNode = jotai.useAtomValue(model.vdomRoot);
nodeModel, if (model.viewRef.current == null || rootNode == null) {
viewRef,
model,
}: {
blockId: string;
nodeModel: NodeModel;
viewRef: React.RefObject<HTMLDivElement>;
model: VDomModel;
}) {
let rootNode = useAtomValueSafe(model?.vdomRoot);
if (!model || viewRef.current == null || rootNode == null) {
return null; return null;
} }
dlog("render", rootNode); dlog("render", rootNode);
model.viewRef = viewRef;
let rtn = convertElemToTag(rootNode, model); let rtn = convertElemToTag(rootNode, model);
return <div className="vdom">{rtn}</div>; 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 // waveobj.Block
type Block = WaveObj & { type Block = WaveObj & {
parentoref?: string;
blockdef: BlockDef; blockdef: BlockDef;
runtimeopts?: RuntimeOpts; runtimeopts?: RuntimeOpts;
stickers?: StickerType[]; stickers?: StickerType[];
subblockids?: string[];
}; };
// blockcontroller.BlockControllerRuntimeStatus // blockcontroller.BlockControllerRuntimeStatus
@ -30,7 +32,7 @@ declare global {
blockid: string; blockid: string;
tabid: string; tabid: string;
windowid: string; windowid: string;
meta: MetaType; block: Block;
}; };
// webcmd.BlockInputWSCommand // webcmd.BlockInputWSCommand
@ -96,6 +98,12 @@ declare global {
magnified?: boolean; magnified?: boolean;
}; };
// wshrpc.CommandCreateSubBlockData
type CommandCreateSubBlockData = {
parentblockid: string;
blockdef: BlockDef;
};
// wshrpc.CommandDeleteBlockData // wshrpc.CommandDeleteBlockData
type CommandDeleteBlockData = { type CommandDeleteBlockData = {
blockid: string; blockid: string;
@ -167,6 +175,12 @@ declare global {
meta: MetaType; meta: MetaType;
}; };
// wshrpc.CommandWaitForRouteData
type CommandWaitForRouteData = {
routeid: string;
waitms: number;
};
// wshrpc.CommandWebSelectorData // wshrpc.CommandWebSelectorData
type CommandWebSelectorData = { type CommandWebSelectorData = {
windowid: string; windowid: string;
@ -341,9 +355,12 @@ declare global {
"term:localshellpath"?: string; "term:localshellpath"?: string;
"term:localshellopts"?: string[]; "term:localshellopts"?: string[];
"term:scrollback"?: number; "term:scrollback"?: number;
"term:vdomblockid"?: string;
"vdom:*"?: boolean; "vdom:*"?: boolean;
"vdom:initialized"?: boolean; "vdom:initialized"?: boolean;
"vdom:correlationid"?: string; "vdom:correlationid"?: string;
"vdom:route"?: string;
"vdom:persist"?: boolean;
count?: number; count?: number;
}; };
@ -645,7 +662,7 @@ declare global {
type: "createcontext"; type: "createcontext";
ts: number; ts: number;
meta?: MetaType; meta?: MetaType;
newblock?: boolean; target?: VDomTarget;
persist?: boolean; persist?: boolean;
}; };
@ -661,7 +678,7 @@ declare global {
// vdom.VDomEvent // vdom.VDomEvent
type VDomEvent = { type VDomEvent = {
waveid: string; waveid: string;
propname: string; eventtype: string;
eventdata: any; eventdata: any;
}; };
@ -671,7 +688,6 @@ declare global {
ts: number; ts: number;
blockid: string; blockid: string;
correlationid?: string; correlationid?: string;
initialize?: boolean;
dispose?: boolean; dispose?: boolean;
resync?: boolean; resync?: boolean;
rendercontext?: VDomRenderContext; rendercontext?: VDomRenderContext;
@ -755,6 +771,12 @@ declare global {
value: any; value: any;
}; };
// vdom.VDomTarget
type VDomTarget = {
newblock?: boolean;
magnified?: boolean;
};
type WSCommandType = { type WSCommandType = {
wscommand: string; wscommand: string;
} & ( SetBlockTermSizeWSCommand | BlockInputWSCommand | WSRpcCommand ); } & ( SetBlockTermSizeWSCommand | BlockInputWSCommand | WSRpcCommand );

View File

@ -36,7 +36,7 @@ const (
const ( const (
BlockFile_Term = "term" // used for main pty output 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 ( const (

View File

@ -201,7 +201,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, uiContext.ActiveTabId, blockId) err := wcore.DeleteBlock(ctx, blockId)
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

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

View File

@ -34,11 +34,11 @@ type VDomElem struct {
//// protocol messages //// protocol messages
type VDomCreateContext struct { type VDomCreateContext struct {
Type string `json:"type" tstype:"\"createcontext\""` Type string `json:"type" tstype:"\"createcontext\""`
Ts int64 `json:"ts"` Ts int64 `json:"ts"`
Meta waveobj.MetaMapType `json:"meta,omitempty"` Meta waveobj.MetaMapType `json:"meta,omitempty"`
NewBlock bool `json:"newblock,omitempty"` Target *VDomTarget `json:"target,omitempty"`
Persist bool `json:"persist,omitempty"` Persist bool `json:"persist,omitempty"`
} }
type VDomAsyncInitiationRequest struct { type VDomAsyncInitiationRequest struct {
@ -60,9 +60,8 @@ type VDomFrontendUpdate struct {
Ts int64 `json:"ts"` Ts int64 `json:"ts"`
BlockId string `json:"blockid"` BlockId string `json:"blockid"`
CorrelationId string `json:"correlationid,omitempty"` CorrelationId string `json:"correlationid,omitempty"`
Initialize bool `json:"initialize,omitempty"` // initialize the app Dispose bool `json:"dispose,omitempty"` // the vdom context was closed
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
Resync bool `json:"resync,omitempty"` // resync (send all backend data). useful when the FE reloads
RenderContext VDomRenderContext `json:"rendercontext,omitempty"` RenderContext VDomRenderContext `json:"rendercontext,omitempty"`
Events []VDomEvent `json:"events,omitempty"` Events []VDomEvent `json:"events,omitempty"`
StateSync []VDomStateSync `json:"statesync,omitempty"` StateSync []VDomStateSync `json:"statesync,omitempty"`
@ -129,8 +128,8 @@ type VDomRefPosition struct {
///// subbordinate protocol types ///// subbordinate protocol types
type VDomEvent struct { type VDomEvent struct {
WaveId string `json:"waveid"` WaveId string `json:"waveid"` // empty for global events
PropName string `json:"propname"` EventType string `json:"eventtype"`
EventData any `json:"eventdata"` EventData any `json:"eventdata"`
} }
@ -179,6 +178,13 @@ type VDomMessage struct {
Params []any `json:"params,omitempty"` 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 // matches WaveKeyboardEvent
type VDomKeyboardEvent struct { type VDomKeyboardEvent struct {
Type string `json:"type"` Type string `json:"type"`

View File

@ -13,7 +13,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/wavetermdev/waveterm/pkg/vdom" "github.com/wavetermdev/waveterm/pkg/vdom"
"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"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
@ -21,6 +20,7 @@ import (
) )
type Client struct { type Client struct {
Lock *sync.Mutex
Root *vdom.RootElem Root *vdom.RootElem
RootElem *vdom.VDomElem RootElem *vdom.VDomElem
RpcClient *wshutil.WshRpc RpcClient *wshutil.WshRpc
@ -28,8 +28,8 @@ type Client struct {
ServerImpl *VDomServerImpl ServerImpl *VDomServerImpl
IsDone bool IsDone bool
RouteId string RouteId string
VDomContextBlockId string
DoneReason string DoneReason string
DoneOnce *sync.Once
DoneCh chan struct{} DoneCh chan struct{}
Opts vdom.VDomBackendOpts Opts vdom.VDomBackendOpts
GlobalEventHandler func(client *Client, event vdom.VDomEvent) 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") impl.Client.doShutdown("got dispose from frontend")
return nil, nil return nil, nil
} }
if impl.Client.IsDone { if impl.Client.GetIsDone() {
return nil, nil return nil, nil
} }
// set atoms // set atoms
@ -62,21 +62,30 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
impl.Client.GlobalEventHandler(impl.Client, event) impl.Client.GlobalEventHandler(impl.Client, event)
} }
} else { } 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.fullRender()
} }
return impl.Client.incrementalRender() 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) { func (c *Client) doShutdown(reason string) {
c.DoneOnce.Do(func() { c.Lock.Lock()
c.DoneReason = reason defer c.Lock.Unlock()
c.IsDone = true if c.IsDone {
close(c.DoneCh) return
}) }
c.DoneReason = reason
c.IsDone = true
close(c.DoneCh)
} }
func (c *Client) SetGlobalEventHandler(handler func(client *Client, event vdom.VDomEvent)) { 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) { func MakeClient(opts *vdom.VDomBackendOpts) (*Client, error) {
client := &Client{ client := &Client{
Root: vdom.MakeRoot(), Lock: &sync.Mutex{},
DoneCh: make(chan struct{}), Root: vdom.MakeRoot(),
DoneOnce: &sync.Once{}, DoneCh: make(chan struct{}),
} }
if opts != nil { if opts != nil {
client.Opts = *opts client.Opts = *opts
@ -126,13 +135,29 @@ func (c *Client) SetRootElem(elem *vdom.VDomElem) {
c.RootElem = elem c.RootElem = elem
} }
func (c *Client) CreateVDomContext() error { func (c *Client) CreateVDomContext(target *vdom.VDomTarget) error {
err := wshclient.VDomCreateContextCommand(c.RpcClient, vdom.VDomCreateContext{}, &wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.RpcContext.BlockId)}) blockORef, err := wshclient.VDomCreateContextCommand(
c.RpcClient,
vdom.VDomCreateContext{Target: target},
&wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.RpcContext.BlockId)},
)
if err != nil { if err != nil {
return err return err
} }
wshclient.EventSubCommand(c.RpcClient, wps.SubscriptionRequest{Event: "blockclose", Scopes: []string{ c.VDomContextBlockId = blockORef.OID
waveobj.MakeORef("block", c.RpcContext.BlockId).String(), 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) }}, nil)
c.RpcClient.EventListener.On("blockclose", func(event *wps.WaveEvent) { c.RpcClient.EventListener.On("blockclose", func(event *wps.WaveEvent) {
c.doShutdown("got blockclose event") c.doShutdown("got blockclose event")
@ -140,8 +165,18 @@ func (c *Client) CreateVDomContext() error {
return nil return nil
} }
func (c *Client) SendAsyncInitiation() { func (c *Client) SendAsyncInitiation() error {
wshclient.VDomAsyncInitiationCommand(c.RpcClient, vdom.MakeAsyncInitiationRequest(c.RpcContext.BlockId), &wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.RpcContext.BlockId)}) 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) { func (c *Client) SetAtomVals(m map[string]any) {

View File

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

View File

@ -94,6 +94,14 @@ func ParseORef(orefStr string) (ORef, error) {
return ORef{OType: otype, OID: oid}, nil 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 { type WaveObj interface {
GetOType() string // should not depend on object state (should work with nil value) 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 { type Block struct {
OID string `json:"oid"` OID string `json:"oid"`
ParentORef string `json:"parentoref,omitempty"`
Version int `json:"version"` Version int `json:"version"`
BlockDef *BlockDef `json:"blockdef"` BlockDef *BlockDef `json:"blockdef"`
RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"` RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"`
Stickers []*StickerType `json:"stickers,omitempty"` Stickers []*StickerType `json:"stickers,omitempty"`
Meta MetaMapType `json:"meta"` Meta MetaMapType `json:"meta"`
SubBlockIds []string `json:"subblockids,omitempty"`
} }
func (*Block) GetOType() string { func (*Block) GetOType() string {

View File

@ -80,10 +80,13 @@ type MetaTSType struct {
TermLocalShellPath string `json:"term:localshellpath,omitempty"` // matches settings TermLocalShellPath string `json:"term:localshellpath,omitempty"` // matches settings
TermLocalShellOpts []string `json:"term:localshellopts,omitempty"` // matches settings TermLocalShellOpts []string `json:"term:localshellopts,omitempty"` // matches settings
TermScrollback *int `json:"term:scrollback,omitempty"` TermScrollback *int `json:"term:scrollback,omitempty"`
TermVDomSubBlockId string `json:"term:vdomblockid,omitempty"`
VDomClear bool `json:"vdom:*,omitempty"` VDomClear bool `json:"vdom:*,omitempty"`
VDomInitialized bool `json:"vdom:initialized,omitempty"` VDomInitialized bool `json:"vdom:initialized,omitempty"`
VDomCorrelationId string `json:"vdom:correlationid,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 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 DefaultTimeout = 2 * time.Second
const DefaultActivateBlockTimeout = 60 * time.Second const DefaultActivateBlockTimeout = 60 * time.Second
func DeleteBlock(ctx context.Context, tabId string, blockId string) error { func DeleteBlock(ctx context.Context, blockId string) error {
err := wstore.DeleteBlock(ctx, tabId, blockId) 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 { if err != nil {
return fmt.Errorf("error deleting block: %w", err) return fmt.Errorf("error deleting block: %w", err)
} }
go blockcontroller.StopBlockController(blockId) go blockcontroller.StopBlockController(blockId)
sendBlockCloseEvent(tabId, blockId) sendBlockCloseEvent(blockId)
return nil return nil
} }
func sendBlockCloseEvent(tabId string, blockId string) { func sendBlockCloseEvent(blockId string) {
waveEvent := wps.WaveEvent{ waveEvent := wps.WaveEvent{
Event: wps.Event_BlockClose, Event: wps.Event_BlockClose,
Scopes: []string{ Scopes: []string{
waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
waveobj.MakeORef(waveobj.OType_Block, blockId).String(), waveobj.MakeORef(waveobj.OType_Block, blockId).String(),
}, },
Data: blockId, Data: blockId,
@ -58,7 +72,7 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string) error {
} }
// close blocks (sends events + stops block controllers) // close blocks (sends events + stops block controllers)
for _, blockId := range tabData.BlockIds { for _, blockId := range tabData.BlockIds {
err := DeleteBlock(ctx, tabId, blockId) err := DeleteBlock(ctx, blockId)
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)
} }
@ -205,6 +219,20 @@ func CreateClient(ctx context.Context) (*waveobj.Client, error) {
return client, nil 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) { 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")

View File

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

View File

@ -103,7 +103,10 @@ type WshRpcInterface interface {
FileAppendIJsonCommand(ctx context.Context, data CommandAppendIJsonData) error FileAppendIJsonCommand(ctx context.Context, data CommandAppendIJsonData) error
ResolveIdsCommand(ctx context.Context, data CommandResolveIdsData) (CommandResolveIdsRtnData, error) ResolveIdsCommand(ctx context.Context, data CommandResolveIdsData) (CommandResolveIdsRtnData, error)
CreateBlockCommand(ctx context.Context, data CommandCreateBlockData) (waveobj.ORef, 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 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 FileWriteCommand(ctx context.Context, data CommandFileData) error
FileReadCommand(ctx context.Context, data CommandFileData) (string, error) FileReadCommand(ctx context.Context, data CommandFileData) (string, error)
EventPublishCommand(ctx context.Context, data wps.WaveEvent) error EventPublishCommand(ctx context.Context, data wps.WaveEvent) error
@ -145,7 +148,7 @@ type WshRpcInterface interface {
NotifyCommand(ctx context.Context, notificationOptions WaveNotificationOptions) error NotifyCommand(ctx context.Context, notificationOptions WaveNotificationOptions) error
// terminal // 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 VDomAsyncInitiationCommand(ctx context.Context, data vdom.VDomAsyncInitiationRequest) error
// proc // proc
@ -248,6 +251,11 @@ type CommandCreateBlockData struct {
Magnified bool `json:"magnified,omitempty"` Magnified bool `json:"magnified,omitempty"`
} }
type CommandCreateSubBlockData struct {
ParentBlockId string `json:"parentblockid"`
BlockDef *waveobj.BlockDef `json:"blockdef"`
}
type CommandBlockSetViewData struct { type CommandBlockSetViewData struct {
BlockId string `json:"blockid" wshcontext:"BlockId"` BlockId string `json:"blockid" wshcontext:"BlockId"`
View string `json:"view"` View string `json:"view"`
@ -279,6 +287,11 @@ type CommandAppendIJsonData struct {
Data ijson.Command `json:"data"` Data ijson.Command `json:"data"`
} }
type CommandWaitForRouteData struct {
RouteId string `json:"routeid"`
WaitMs int `json:"waitms"`
}
type CommandDeleteBlockData struct { type CommandDeleteBlockData struct {
BlockId string `json:"blockid" wshcontext:"BlockId"` BlockId string `json:"blockid" wshcontext:"BlockId"`
} }
@ -402,10 +415,10 @@ type CommandWebSelectorData struct {
} }
type BlockInfoData struct { type BlockInfoData struct {
BlockId string `json:"blockid"` BlockId string `json:"blockid"`
TabId string `json:"tabid"` TabId string `json:"tabid"`
WindowId string `json:"windowid"` WindowId string `json:"windowid"`
Meta waveobj.MetaMapType `json:"meta"` Block *waveobj.Block `json:"block"`
} }
type WaveNotificationOptions struct { 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 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 { func (ws *WshServer) SetViewCommand(ctx context.Context, data wshrpc.CommandBlockSetViewData) error {
log.Printf("SETVIEW: %s | %q\n", data.BlockId, data.View) log.Printf("SETVIEW: %s | %q\n", data.BlockId, data.View)
ctx = waveobj.ContextWithUpdates(ctx) 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 { func (ws *WshServer) FileAppendIJsonCommand(ctx context.Context, data wshrpc.CommandAppendIJsonData) error {
tryCreate := true 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}) err := filestore.WFS.MakeFile(ctx, data.ZoneId, data.FileName, nil, filestore.FileOptsType{MaxSize: blockcontroller.DefaultHtmlMaxFileSize, IJson: true})
if err != nil && err != fs.ErrExist { 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) 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 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 { func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.CommandDeleteBlockData) error {
ctx = waveobj.ContextWithUpdates(ctx) ctx = waveobj.ContextWithUpdates(ctx)
tabId, err := wstore.DBFindTabForBlockId(ctx, data.BlockId) tabId, err := wstore.DBFindTabForBlockId(ctx, data.BlockId)
@ -395,7 +413,7 @@ func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.Command
if windowId == "" { if windowId == "" {
return fmt.Errorf("no window found for tab") return fmt.Errorf("no window found for tab")
} }
err = wcore.DeleteBlock(ctx, tabId, data.BlockId) err = wcore.DeleteBlock(ctx, data.BlockId)
if err != nil { if err != nil {
return fmt.Errorf("error deleting block: %w", err) return fmt.Errorf("error deleting block: %w", err)
} }
@ -408,6 +426,13 @@ func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.Command
return nil 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 { func (ws *WshServer) EventRecvCommand(ctx context.Context, data wps.WaveEvent) error {
return nil return nil
} }
@ -587,6 +612,6 @@ func (ws *WshServer) BlockInfoCommand(ctx context.Context, blockId string) (*wsh
BlockId: blockId, BlockId: blockId,
TabId: tabId, TabId: tabId,
WindowId: windowId, WindowId: windowId,
Meta: blockData.Meta, Block: blockData,
}, nil }, nil
} }

View File

@ -268,6 +268,9 @@ func (router *WshRouter) WaitForRegister(ctx context.Context, routeId string) er
if router.GetRpc(routeId) != nil { if router.GetRpc(routeId) != nil {
return nil return nil
} }
if router.getAnnouncedRoute(routeId) != "" {
return nil
}
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() 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) { 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) { return WithTxRtn(ctx, func(tx *TxWrap) (*waveobj.Block, error) {
tab, _ := DBGet[*waveobj.Tab](tx.Context(), tabId) tab, _ := DBGet[*waveobj.Tab](tx.Context(), tabId)
@ -104,6 +125,7 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
blockId := uuid.NewString() blockId := uuid.NewString()
blockData := &waveobj.Block{ blockData := &waveobj.Block{
OID: blockId, OID: blockId,
ParentORef: waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
BlockDef: blockDef, BlockDef: blockDef,
RuntimeOpts: rtOpts, RuntimeOpts: rtOpts,
Meta: blockDef.Meta, Meta: blockDef.Meta,
@ -124,18 +146,34 @@ func findStringInSlice(slice []string, val string) int {
return -1 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 { return WithTx(ctx, func(tx *TxWrap) error {
tab, _ := DBGet[*waveobj.Tab](tx.Context(), tabId) block, err := DBGet[*waveobj.Block](tx.Context(), blockId)
if tab == nil { if err != nil {
return fmt.Errorf("tab not found: %q", tabId) return fmt.Errorf("error getting block: %w", err)
} }
blockIdx := findStringInSlice(tab.BlockIds, blockId) if block == nil {
if blockIdx == -1 {
return nil return nil
} }
tab.BlockIds = append(tab.BlockIds[:blockIdx], tab.BlockIds[blockIdx+1:]...) if len(block.SubBlockIds) > 0 {
DBUpdate(tx.Context(), tab) 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) DBDelete(tx.Context(), waveobj.OType_Block, blockId)
return nil return nil
}) })
@ -145,23 +183,18 @@ func DeleteBlock(ctx context.Context, tabId string, blockId string) error {
// 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 {
return WithTx(ctx, func(tx *TxWrap) 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) tab, _ := DBGet[*waveobj.Tab](tx.Context(), tabId)
if tab == nil { if tab == nil {
return fmt.Errorf("tab not found: %q", tabId) return nil
} }
if len(tab.BlockIds) != 0 { if len(tab.BlockIds) != 0 {
return fmt.Errorf("tab has blocks, must delete blocks first") return fmt.Errorf("tab has blocks, must delete blocks first")
} }
tabIdx := findStringInSlice(ws.TabIds, tabId) ws, _ := DBGet[*waveobj.Workspace](tx.Context(), workspaceId)
if tabIdx == -1 { if ws != nil {
return nil ws.TabIds = utilfn.RemoveElemFromSlice(ws.TabIds, tabId)
DBUpdate(tx.Context(), ws)
} }
ws.TabIds = append(ws.TabIds[:tabIdx], ws.TabIds[tabIdx+1:]...)
DBUpdate(tx.Context(), ws)
DBDelete(tx.Context(), waveobj.OType_Tab, tabId) DBDelete(tx.Context(), waveobj.OType_Tab, tabId)
DBDelete(tx.Context(), waveobj.OType_LayoutState, tab.LayoutState) DBDelete(tx.Context(), waveobj.OType_LayoutState, tab.LayoutState)
return nil 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 { func MoveBlockToTab(ctx context.Context, currentTabId string, newTabId string, blockId string) error {
return WithTx(ctx, func(tx *TxWrap) 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) currentTab, _ := DBGet[*waveobj.Tab](tx.Context(), currentTabId)
if currentTab == nil { if currentTab == nil {
return fmt.Errorf("current tab not found: %q", currentTabId) 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) currentTab.BlockIds = utilfn.RemoveElemFromSlice(currentTab.BlockIds, blockId)
newTab.BlockIds = append(newTab.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(), currentTab)
DBUpdate(tx.Context(), newTab) DBUpdate(tx.Context(), newTab)
return nil return nil