vdom terminal toolbar (#1263)

This commit is contained in:
Mike Sawka 2024-11-11 13:11:09 -08:00 committed by GitHub
parent 83f671c7a9
commit 3fc45c63f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 181 additions and 19 deletions

View File

@ -26,6 +26,10 @@
overflow: hidden; overflow: hidden;
min-height: 0; min-height: 0;
padding: 5px; padding: 5px;
&.block-no-padding {
padding: 0;
}
} }
.block-focuselem { .block-focuselem {

View File

@ -25,12 +25,13 @@ import {
} from "@/store/global"; } from "@/store/global";
import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos"; import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos";
import { focusedBlockId, getElemAsStr } from "@/util/focusutil"; import { focusedBlockId, getElemAsStr } from "@/util/focusutil";
import { isBlank } from "@/util/util"; import { isBlank, useAtomValueSafe } from "@/util/util";
import { HelpView, HelpViewModel, makeHelpViewModel } from "@/view/helpview/helpview"; import { HelpView, HelpViewModel, makeHelpViewModel } from "@/view/helpview/helpview";
import { QuickTipsView, QuickTipsViewModel } from "@/view/quicktipsview/quicktipsview"; import { QuickTipsView, QuickTipsViewModel } from "@/view/quicktipsview/quicktipsview";
import { TermViewModel, TerminalView, makeTerminalModel } from "@/view/term/term"; import { TermViewModel, TerminalView, makeTerminalModel } from "@/view/term/term";
import { WaveAi, WaveAiModel, makeWaveAiViewModel } from "@/view/waveai/waveai"; import { WaveAi, WaveAiModel, makeWaveAiViewModel } from "@/view/waveai/waveai";
import { WebView, WebViewModel, makeWebViewModel } from "@/view/webview/webview"; import { WebView, WebViewModel, makeWebViewModel } from "@/view/webview/webview";
import clsx from "clsx";
import { atom, useAtomValue } from "jotai"; import { atom, useAtomValue } from "jotai";
import { Suspense, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { Suspense, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import "./block.less"; import "./block.less";
@ -154,11 +155,12 @@ const BlockSubBlock = memo(({ nodeModel, viewModel }: FullSubBlockProps) => {
() => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel), () => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel),
[nodeModel.blockId, blockData?.meta?.view, viewModel] [nodeModel.blockId, blockData?.meta?.view, viewModel]
); );
const noPadding = useAtomValueSafe(viewModel.noPadding);
if (!blockData) { if (!blockData) {
return null; return null;
} }
return ( return (
<div key="content" className="block-content" ref={contentRef}> <div key="content" className={clsx("block-content", { "block-no-padding": noPadding })} ref={contentRef}>
<ErrorBoundary> <ErrorBoundary>
<Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</Suspense> <Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</Suspense>
</ErrorBoundary> </ErrorBoundary>
@ -176,6 +178,7 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
const isFocused = useAtomValue(nodeModel.isFocused); const isFocused = useAtomValue(nodeModel.isFocused);
const disablePointerEvents = useAtomValue(nodeModel.disablePointerEvents); const disablePointerEvents = useAtomValue(nodeModel.disablePointerEvents);
const innerRect = useDebouncedNodeInnerRect(nodeModel); const innerRect = useDebouncedNodeInnerRect(nodeModel);
const noPadding = useAtomValueSafe(viewModel.noPadding);
useLayoutEffect(() => { useLayoutEffect(() => {
setBlockClicked(isFocused); setBlockClicked(isFocused);
@ -273,7 +276,12 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
onChange={() => {}} onChange={() => {}}
/> />
</div> </div>
<div key="content" className="block-content" ref={contentRef} style={blockContentStyle}> <div
key="content"
className={clsx("block-content", { "block-no-padding": noPadding })}
ref={contentRef}
style={blockContentStyle}
>
<ErrorBoundary> <ErrorBoundary>
<Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</Suspense> <Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</Suspense>
</ErrorBoundary> </ErrorBoundary>

View File

@ -41,6 +41,35 @@ export class TermWshClient extends WshClient {
magnified: data.target?.magnified, magnified: data.target?.magnified,
}); });
return oref; return oref;
} else if (data.target?.toolbar?.toolbar) {
const oldVDomBlockId = globalStore.get(this.model.vdomToolbarBlockId);
console.log("vdom:toolbar", data.target.toolbar);
globalStore.set(this.model.vdomToolbarTarget, data.target.toolbar);
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:vdomtoolbarblockid": newVDomBlockId,
},
});
}, 50);
return oref;
} else { } else {
// in the terminal // in the terminal
// check if there is a current active vdom block // check if there is a current active vdom block

View File

@ -13,11 +13,10 @@
.view-term { .view-term {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
padding-left: 4px;
position: relative; position: relative;
.term-header { .term-header {
@ -31,11 +30,19 @@
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
.term-toolbar {
height: 20px;
border-bottom: 1px solid var(--border-color);
overflow: hidden;
}
.term-connectelem { .term-connectelem {
flex-grow: 1; flex-grow: 1;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
line-height: 1; line-height: 1;
margin: 5px;
margin-left: 4px;
} }
.term-htmlelem { .term-htmlelem {

View File

@ -56,8 +56,11 @@ class TermViewModel {
termWshClient: TermWshClient; termWshClient: TermWshClient;
shellProcStatusRef: React.MutableRefObject<string>; shellProcStatusRef: React.MutableRefObject<string>;
vdomBlockId: jotai.Atom<string>; vdomBlockId: jotai.Atom<string>;
vdomToolbarBlockId: jotai.Atom<string>;
vdomToolbarTarget: jotai.PrimitiveAtom<VDomTargetToolbar>;
fontSizeAtom: jotai.Atom<number>; fontSizeAtom: jotai.Atom<number>;
termThemeNameAtom: jotai.Atom<string>; termThemeNameAtom: jotai.Atom<string>;
noPadding: jotai.PrimitiveAtom<boolean>;
constructor(blockId: string, nodeModel: BlockNodeModel) { constructor(blockId: string, nodeModel: BlockNodeModel) {
this.viewType = "term"; this.viewType = "term";
@ -70,6 +73,11 @@ class TermViewModel {
const blockData = get(this.blockAtom); const blockData = get(this.blockAtom);
return blockData?.meta?.["term:vdomblockid"]; return blockData?.meta?.["term:vdomblockid"];
}); });
this.vdomToolbarBlockId = jotai.atom((get) => {
const blockData = get(this.blockAtom);
return blockData?.meta?.["term:vdomtoolbarblockid"];
});
this.vdomToolbarTarget = jotai.atom<VDomTargetToolbar>(null) as jotai.PrimitiveAtom<VDomTargetToolbar>;
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";
@ -167,6 +175,7 @@ class TermViewModel {
return blockData?.meta?.["term:theme"] ?? get(settingsKeyAtom) ?? "default-dark"; return blockData?.meta?.["term:theme"] ?? get(settingsKeyAtom) ?? "default-dark";
}); });
}); });
this.noPadding = jotai.atom(true);
} }
setTermMode(mode: "term" | "vdom") { setTermMode(mode: "term" | "vdom") {
@ -191,6 +200,18 @@ class TermViewModel {
return bcm.viewModel as VDomModel; return bcm.viewModel as VDomModel;
} }
getVDomToolbarModel(): VDomModel {
const vdomToolbarBlockId = globalStore.get(this.vdomToolbarBlockId);
if (!vdomToolbarBlockId) {
return null;
}
const bcm = getBlockComponentModel(vdomToolbarBlockId);
if (!bcm) {
return null;
}
return bcm.viewModel as VDomModel;
}
dispose() { dispose() {
DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId)); DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId));
} }
@ -347,6 +368,15 @@ class TermViewModel {
prtn.catch((e) => console.log("error controller resync (force restart)", e)); prtn.catch((e) => console.log("error controller resync (force restart)", e));
}, },
}); });
if (blockData?.meta?.["term:vdomtoolbarblockid"]) {
fullMenu.push({ type: "separator" });
fullMenu.push({
label: "Close Toolbar",
click: () => {
RpcApi.DeleteSubBlockCommand(TabRpcClient, { blockid: blockData.meta["term:vdomtoolbarblockid"] });
},
});
}
return fullMenu; return fullMenu;
} }
} }
@ -382,6 +412,44 @@ const TermResyncHandler = React.memo(({ blockId, model }: TerminalViewProps) =>
return null; return null;
}); });
const TermVDomToolbarNode = ({ 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:vdomtoolbarblockid": null,
},
});
},
});
return () => {
unsub();
};
}, []);
let vdomNodeModel = {
blockId: vdomBlockId,
isFocused: jotai.atom(false),
focusNode: () => {},
onClose: () => {
if (vdomBlockId != null) {
RpcApi.DeleteSubBlockCommand(TabRpcClient, { blockid: vdomBlockId });
}
},
};
const toolbarTarget = jotai.useAtomValue(model.vdomToolbarTarget);
const heightStr = toolbarTarget?.height ?? "1.5em";
return (
<div key="vdomToolbar" className="term-toolbar" style={{ height: heightStr }}>
<SubBlock key="vdom" nodeModel={vdomNodeModel} />
</div>
);
};
const TermVDomNodeSingleId = ({ vdomBlockId, blockId, model }: TerminalViewProps & { vdomBlockId: string }) => { const TermVDomNodeSingleId = ({ vdomBlockId, blockId, model }: TerminalViewProps & { vdomBlockId: string }) => {
React.useEffect(() => { React.useEffect(() => {
const unsub = waveEventSubscribe({ const unsub = waveEventSubscribe({
@ -431,6 +499,21 @@ const TermVDomNode = ({ blockId, model }: TerminalViewProps) => {
return <TermVDomNodeSingleId key={vdomBlockId} vdomBlockId={vdomBlockId} blockId={blockId} model={model} />; return <TermVDomNodeSingleId key={vdomBlockId} vdomBlockId={vdomBlockId} blockId={blockId} model={model} />;
}; };
const TermToolbarVDomNode = ({ blockId, model }: TerminalViewProps) => {
const vdomToolbarBlockId = jotai.useAtomValue(model.vdomToolbarBlockId);
if (vdomToolbarBlockId == null) {
return null;
}
return (
<TermVDomToolbarNode
key={vdomToolbarBlockId}
vdomBlockId={vdomToolbarBlockId}
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);
@ -547,14 +630,14 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
cols: termRef.current?.terminal.cols ?? 80, cols: termRef.current?.terminal.cols ?? 80,
blockId: blockId, blockId: blockId,
}; };
return ( return (
<div className={clsx("view-term", "term-mode-" + termMode)} ref={viewRef}> <div className={clsx("view-term", "term-mode-" + termMode)} ref={viewRef}>
<TermResyncHandler blockId={blockId} model={model} /> <TermResyncHandler blockId={blockId} model={model} />
<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> <TermToolbarVDomNode key="vdom-toolbar" blockId={blockId} model={model} />
<TermVDomNode key="vdom" blockId={blockId} model={model} /> <TermVDomNode key="vdom" blockId={blockId} model={model} />
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
</div> </div>
); );
}; };

View File

@ -134,6 +134,7 @@ export class VDomModel {
refOutputStore: Map<string, any> = new Map(); refOutputStore: Map<string, any> = new Map();
globalVersion: jotai.PrimitiveAtom<number> = jotai.atom(0); globalVersion: jotai.PrimitiveAtom<number> = jotai.atom(0);
hasBackendWork: boolean = false; hasBackendWork: boolean = false;
noPadding: jotai.PrimitiveAtom<boolean>;
constructor(blockId: string, nodeModel: BlockNodeModel) { constructor(blockId: string, nodeModel: BlockNodeModel) {
this.viewType = "vdom"; this.viewType = "vdom";
@ -147,6 +148,7 @@ export class VDomModel {
const blockData = get(WOS.getWaveObjectAtom<Block>(makeORef("block", this.blockId))); const blockData = get(WOS.getWaveObjectAtom<Block>(makeORef("block", this.blockId)));
return blockData?.meta?.["vdom:route"]; return blockData?.meta?.["vdom:route"];
}); });
this.noPadding = jotai.atom(true);
this.persist = getBlockMetaKeyAtom(this.blockId, "vdom:persist"); this.persist = getBlockMetaKeyAtom(this.blockId, "vdom:persist");
this.wshClient = new VDomWshClient(this); this.wshClient = new VDomWshClient(this);
DefaultRouter.registerRoute(this.wshClient.routeId, this.wshClient); DefaultRouter.registerRoute(this.wshClient.routeId, this.wshClient);

View File

@ -229,6 +229,7 @@ declare global {
endIconButtons?: jotai.Atom<IconButtonDecl[]>; endIconButtons?: jotai.Atom<IconButtonDecl[]>;
blockBg?: jotai.Atom<MetaType>; blockBg?: jotai.Atom<MetaType>;
manageConnection?: jotai.Atom<boolean>; manageConnection?: jotai.Atom<boolean>;
noPadding?: jotai.Atom<boolean>;
onBack?: () => void; onBack?: () => void;
onForward?: () => void; onForward?: () => void;

View File

@ -365,6 +365,7 @@ declare global {
"term:localshellopts"?: string[]; "term:localshellopts"?: string[];
"term:scrollback"?: number; "term:scrollback"?: number;
"term:vdomblockid"?: string; "term:vdomblockid"?: string;
"term:vdomtoolbarblockid"?: string;
"vdom:*"?: boolean; "vdom:*"?: boolean;
"vdom:initialized"?: boolean; "vdom:initialized"?: boolean;
"vdom:correlationid"?: string; "vdom:correlationid"?: string;
@ -795,6 +796,13 @@ declare global {
type VDomTarget = { type VDomTarget = {
newblock?: boolean; newblock?: boolean;
magnified?: boolean; magnified?: boolean;
toolbar?: VDomTargetToolbar;
};
// vdom.VDomTargetToolbar
type VDomTargetToolbar = {
toolbar: boolean;
height?: string;
}; };
// vdom.VDomTransferElem // vdom.VDomTransferElem

View File

@ -205,8 +205,14 @@ type VDomMessage struct {
// target -- to support new targets in the future, like toolbars, partial blocks, splits, etc. // target -- to support new targets in the future, like toolbars, partial blocks, splits, etc.
// default is vdom context inside of a terminal block // default is vdom context inside of a terminal block
type VDomTarget struct { type VDomTarget struct {
NewBlock bool `json:"newblock,omitempty"` NewBlock bool `json:"newblock,omitempty"`
Magnified bool `json:"magnified,omitempty"` Magnified bool `json:"magnified,omitempty"`
Toolbar *VDomTargetToolbar `json:"toolbar,omitempty"`
}
type VDomTargetToolbar struct {
Toolbar bool `json:"toolbar"`
Height string `json:"height,omitempty"`
} }
// matches WaveKeyboardEvent // matches WaveKeyboardEvent

View File

@ -33,6 +33,8 @@ type AppOpts struct {
GlobalStyles []byte GlobalStyles []byte
RootComponentName string // defaults to "App" RootComponentName string // defaults to "App"
NewBlockFlag string // defaults to "n" (set to "-" to disable) NewBlockFlag string // defaults to "n" (set to "-" to disable)
TargetNewBlock bool
TargetToolbar *vdom.VDomTargetToolbar
} }
type Client struct { type Client struct {
@ -116,7 +118,17 @@ func (client *Client) runMainE() error {
if err != nil { if err != nil {
return err return err
} }
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: client.NewBlockFlag}) target := &vdom.VDomTarget{}
if client.AppOpts.TargetNewBlock || client.NewBlockFlag {
target.NewBlock = client.NewBlockFlag
}
if client.AppOpts.TargetToolbar != nil {
target.Toolbar = client.AppOpts.TargetToolbar
}
if target.NewBlock && target.Toolbar != nil {
return fmt.Errorf("cannot specify both new block and toolbar target")
}
err = client.CreateVDomContext(target)
if err != nil { if err != nil {
return err return err
} }

View File

@ -85,6 +85,7 @@ const (
MetaKey_TermLocalShellOpts = "term:localshellopts" MetaKey_TermLocalShellOpts = "term:localshellopts"
MetaKey_TermScrollback = "term:scrollback" MetaKey_TermScrollback = "term:scrollback"
MetaKey_TermVDomSubBlockId = "term:vdomblockid" MetaKey_TermVDomSubBlockId = "term:vdomblockid"
MetaKey_TermVDomToolbarBlockId = "term:vdomtoolbarblockid"
MetaKey_VDomClear = "vdom:*" MetaKey_VDomClear = "vdom:*"
MetaKey_VDomInitialized = "vdom:initialized" MetaKey_VDomInitialized = "vdom:initialized"

View File

@ -77,15 +77,16 @@ type MetaTSType struct {
BgBorderColor string `json:"bg:bordercolor,omitempty"` // frame:bordercolor BgBorderColor string `json:"bg:bordercolor,omitempty"` // frame:bordercolor
BgActiveBorderColor string `json:"bg:activebordercolor,omitempty"` // frame:activebordercolor BgActiveBorderColor string `json:"bg:activebordercolor,omitempty"` // frame:activebordercolor
TermClear bool `json:"term:*,omitempty"` TermClear bool `json:"term:*,omitempty"`
TermFontSize int `json:"term:fontsize,omitempty"` TermFontSize int `json:"term:fontsize,omitempty"`
TermFontFamily string `json:"term:fontfamily,omitempty"` TermFontFamily string `json:"term:fontfamily,omitempty"`
TermMode string `json:"term:mode,omitempty"` TermMode string `json:"term:mode,omitempty"`
TermTheme string `json:"term:theme,omitempty"` TermTheme string `json:"term:theme,omitempty"`
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"` TermVDomSubBlockId string `json:"term:vdomblockid,omitempty"`
TermVDomToolbarBlockId string `json:"term:vdomtoolbarblockid,omitempty"`
VDomClear bool `json:"vdom:*,omitempty"` VDomClear bool `json:"vdom:*,omitempty"`
VDomInitialized bool `json:"vdom:initialized,omitempty"` VDomInitialized bool `json:"vdom:initialized,omitempty"`