Skip to content

Commit 3fc45c6

Browse files
authored
vdom terminal toolbar (#1263)
1 parent 83f671c commit 3fc45c6

12 files changed

Lines changed: 181 additions & 19 deletions

File tree

frontend/app/block/block.less

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
overflow: hidden;
2727
min-height: 0;
2828
padding: 5px;
29+
30+
&.block-no-padding {
31+
padding: 0;
32+
}
2933
}
3034

3135
.block-focuselem {

frontend/app/block/block.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ import {
2525
} from "@/store/global";
2626
import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos";
2727
import { focusedBlockId, getElemAsStr } from "@/util/focusutil";
28-
import { isBlank } from "@/util/util";
28+
import { isBlank, useAtomValueSafe } from "@/util/util";
2929
import { HelpView, HelpViewModel, makeHelpViewModel } from "@/view/helpview/helpview";
3030
import { QuickTipsView, QuickTipsViewModel } from "@/view/quicktipsview/quicktipsview";
3131
import { TermViewModel, TerminalView, makeTerminalModel } from "@/view/term/term";
3232
import { WaveAi, WaveAiModel, makeWaveAiViewModel } from "@/view/waveai/waveai";
3333
import { WebView, WebViewModel, makeWebViewModel } from "@/view/webview/webview";
34+
import clsx from "clsx";
3435
import { atom, useAtomValue } from "jotai";
3536
import { Suspense, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3637
import "./block.less";
@@ -154,11 +155,12 @@ const BlockSubBlock = memo(({ nodeModel, viewModel }: FullSubBlockProps) => {
154155
() => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel),
155156
[nodeModel.blockId, blockData?.meta?.view, viewModel]
156157
);
158+
const noPadding = useAtomValueSafe(viewModel.noPadding);
157159
if (!blockData) {
158160
return null;
159161
}
160162
return (
161-
<div key="content" className="block-content" ref={contentRef}>
163+
<div key="content" className={clsx("block-content", { "block-no-padding": noPadding })} ref={contentRef}>
162164
<ErrorBoundary>
163165
<Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</Suspense>
164166
</ErrorBoundary>
@@ -176,6 +178,7 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
176178
const isFocused = useAtomValue(nodeModel.isFocused);
177179
const disablePointerEvents = useAtomValue(nodeModel.disablePointerEvents);
178180
const innerRect = useDebouncedNodeInnerRect(nodeModel);
181+
const noPadding = useAtomValueSafe(viewModel.noPadding);
179182

180183
useLayoutEffect(() => {
181184
setBlockClicked(isFocused);
@@ -273,7 +276,12 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
273276
onChange={() => {}}
274277
/>
275278
</div>
276-
<div key="content" className="block-content" ref={contentRef} style={blockContentStyle}>
279+
<div
280+
key="content"
281+
className={clsx("block-content", { "block-no-padding": noPadding })}
282+
ref={contentRef}
283+
style={blockContentStyle}
284+
>
277285
<ErrorBoundary>
278286
<Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</Suspense>
279287
</ErrorBoundary>

frontend/app/view/term/term-wsh.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,35 @@ export class TermWshClient extends WshClient {
4141
magnified: data.target?.magnified,
4242
});
4343
return oref;
44+
} else if (data.target?.toolbar?.toolbar) {
45+
const oldVDomBlockId = globalStore.get(this.model.vdomToolbarBlockId);
46+
console.log("vdom:toolbar", data.target.toolbar);
47+
globalStore.set(this.model.vdomToolbarTarget, data.target.toolbar);
48+
const oref = await RpcApi.CreateSubBlockCommand(this, {
49+
parentblockid: this.blockId,
50+
blockdef: {
51+
meta: {
52+
view: "vdom",
53+
"vdom:route": rh.getSource(),
54+
},
55+
},
56+
});
57+
const [_, newVDomBlockId] = splitORef(oref);
58+
if (!isBlank(oldVDomBlockId)) {
59+
// dispose of the old vdom block
60+
setTimeout(() => {
61+
RpcApi.DeleteSubBlockCommand(this, { blockid: oldVDomBlockId });
62+
}, 500);
63+
}
64+
setTimeout(() => {
65+
RpcApi.SetMetaCommand(this, {
66+
oref: makeORef("block", this.model.blockId),
67+
meta: {
68+
"term:vdomtoolbarblockid": newVDomBlockId,
69+
},
70+
});
71+
}, 50);
72+
return oref;
4473
} else {
4574
// in the terminal
4675
// check if there is a current active vdom block

frontend/app/view/term/term.less

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@
1313

1414
.view-term {
1515
display: flex;
16-
flex-direction: row;
16+
flex-direction: column;
1717
width: 100%;
1818
height: 100%;
1919
overflow: hidden;
20-
padding-left: 4px;
2120
position: relative;
2221

2322
.term-header {
@@ -31,11 +30,19 @@
3130
border-bottom: 1px solid var(--border-color);
3231
}
3332

33+
.term-toolbar {
34+
height: 20px;
35+
border-bottom: 1px solid var(--border-color);
36+
overflow: hidden;
37+
}
38+
3439
.term-connectelem {
3540
flex-grow: 1;
3641
min-height: 0;
3742
overflow: hidden;
3843
line-height: 1;
44+
margin: 5px;
45+
margin-left: 4px;
3946
}
4047

4148
.term-htmlelem {

frontend/app/view/term/term.tsx

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,11 @@ class TermViewModel {
5656
termWshClient: TermWshClient;
5757
shellProcStatusRef: React.MutableRefObject<string>;
5858
vdomBlockId: jotai.Atom<string>;
59+
vdomToolbarBlockId: jotai.Atom<string>;
60+
vdomToolbarTarget: jotai.PrimitiveAtom<VDomTargetToolbar>;
5961
fontSizeAtom: jotai.Atom<number>;
6062
termThemeNameAtom: jotai.Atom<string>;
63+
noPadding: jotai.PrimitiveAtom<boolean>;
6164

6265
constructor(blockId: string, nodeModel: BlockNodeModel) {
6366
this.viewType = "term";
@@ -70,6 +73,11 @@ class TermViewModel {
7073
const blockData = get(this.blockAtom);
7174
return blockData?.meta?.["term:vdomblockid"];
7275
});
76+
this.vdomToolbarBlockId = jotai.atom((get) => {
77+
const blockData = get(this.blockAtom);
78+
return blockData?.meta?.["term:vdomtoolbarblockid"];
79+
});
80+
this.vdomToolbarTarget = jotai.atom<VDomTargetToolbar>(null) as jotai.PrimitiveAtom<VDomTargetToolbar>;
7381
this.termMode = jotai.atom((get) => {
7482
const blockData = get(this.blockAtom);
7583
return blockData?.meta?.["term:mode"] ?? "term";
@@ -167,6 +175,7 @@ class TermViewModel {
167175
return blockData?.meta?.["term:theme"] ?? get(settingsKeyAtom) ?? "default-dark";
168176
});
169177
});
178+
this.noPadding = jotai.atom(true);
170179
}
171180

172181
setTermMode(mode: "term" | "vdom") {
@@ -191,6 +200,18 @@ class TermViewModel {
191200
return bcm.viewModel as VDomModel;
192201
}
193202

203+
getVDomToolbarModel(): VDomModel {
204+
const vdomToolbarBlockId = globalStore.get(this.vdomToolbarBlockId);
205+
if (!vdomToolbarBlockId) {
206+
return null;
207+
}
208+
const bcm = getBlockComponentModel(vdomToolbarBlockId);
209+
if (!bcm) {
210+
return null;
211+
}
212+
return bcm.viewModel as VDomModel;
213+
}
214+
194215
dispose() {
195216
DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId));
196217
}
@@ -347,6 +368,15 @@ class TermViewModel {
347368
prtn.catch((e) => console.log("error controller resync (force restart)", e));
348369
},
349370
});
371+
if (blockData?.meta?.["term:vdomtoolbarblockid"]) {
372+
fullMenu.push({ type: "separator" });
373+
fullMenu.push({
374+
label: "Close Toolbar",
375+
click: () => {
376+
RpcApi.DeleteSubBlockCommand(TabRpcClient, { blockid: blockData.meta["term:vdomtoolbarblockid"] });
377+
},
378+
});
379+
}
350380
return fullMenu;
351381
}
352382
}
@@ -382,6 +412,44 @@ const TermResyncHandler = React.memo(({ blockId, model }: TerminalViewProps) =>
382412
return null;
383413
});
384414

415+
const TermVDomToolbarNode = ({ vdomBlockId, blockId, model }: TerminalViewProps & { vdomBlockId: string }) => {
416+
React.useEffect(() => {
417+
const unsub = waveEventSubscribe({
418+
eventType: "blockclose",
419+
scope: WOS.makeORef("block", vdomBlockId),
420+
handler: (event) => {
421+
RpcApi.SetMetaCommand(TabRpcClient, {
422+
oref: WOS.makeORef("block", blockId),
423+
meta: {
424+
"term:mode": null,
425+
"term:vdomtoolbarblockid": null,
426+
},
427+
});
428+
},
429+
});
430+
return () => {
431+
unsub();
432+
};
433+
}, []);
434+
let vdomNodeModel = {
435+
blockId: vdomBlockId,
436+
isFocused: jotai.atom(false),
437+
focusNode: () => {},
438+
onClose: () => {
439+
if (vdomBlockId != null) {
440+
RpcApi.DeleteSubBlockCommand(TabRpcClient, { blockid: vdomBlockId });
441+
}
442+
},
443+
};
444+
const toolbarTarget = jotai.useAtomValue(model.vdomToolbarTarget);
445+
const heightStr = toolbarTarget?.height ?? "1.5em";
446+
return (
447+
<div key="vdomToolbar" className="term-toolbar" style={{ height: heightStr }}>
448+
<SubBlock key="vdom" nodeModel={vdomNodeModel} />
449+
</div>
450+
);
451+
};
452+
385453
const TermVDomNodeSingleId = ({ vdomBlockId, blockId, model }: TerminalViewProps & { vdomBlockId: string }) => {
386454
React.useEffect(() => {
387455
const unsub = waveEventSubscribe({
@@ -431,6 +499,21 @@ const TermVDomNode = ({ blockId, model }: TerminalViewProps) => {
431499
return <TermVDomNodeSingleId key={vdomBlockId} vdomBlockId={vdomBlockId} blockId={blockId} model={model} />;
432500
};
433501

502+
const TermToolbarVDomNode = ({ blockId, model }: TerminalViewProps) => {
503+
const vdomToolbarBlockId = jotai.useAtomValue(model.vdomToolbarBlockId);
504+
if (vdomToolbarBlockId == null) {
505+
return null;
506+
}
507+
return (
508+
<TermVDomToolbarNode
509+
key={vdomToolbarBlockId}
510+
vdomBlockId={vdomToolbarBlockId}
511+
blockId={blockId}
512+
model={model}
513+
/>
514+
);
515+
};
516+
434517
const TerminalView = ({ blockId, model }: TerminalViewProps) => {
435518
const viewRef = React.useRef<HTMLDivElement>(null);
436519
const connectElemRef = React.useRef<HTMLDivElement>(null);
@@ -547,14 +630,14 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
547630
cols: termRef.current?.terminal.cols ?? 80,
548631
blockId: blockId,
549632
};
550-
551633
return (
552634
<div className={clsx("view-term", "term-mode-" + termMode)} ref={viewRef}>
553635
<TermResyncHandler blockId={blockId} model={model} />
554636
<TermThemeUpdater blockId={blockId} termRef={termRef} />
555637
<TermStickers config={stickerConfig} />
556-
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
638+
<TermToolbarVDomNode key="vdom-toolbar" blockId={blockId} model={model} />
557639
<TermVDomNode key="vdom" blockId={blockId} model={model} />
640+
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
558641
</div>
559642
);
560643
};

frontend/app/view/vdom/vdom-model.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export class VDomModel {
134134
refOutputStore: Map<string, any> = new Map();
135135
globalVersion: jotai.PrimitiveAtom<number> = jotai.atom(0);
136136
hasBackendWork: boolean = false;
137+
noPadding: jotai.PrimitiveAtom<boolean>;
137138

138139
constructor(blockId: string, nodeModel: BlockNodeModel) {
139140
this.viewType = "vdom";
@@ -147,6 +148,7 @@ export class VDomModel {
147148
const blockData = get(WOS.getWaveObjectAtom<Block>(makeORef("block", this.blockId)));
148149
return blockData?.meta?.["vdom:route"];
149150
});
151+
this.noPadding = jotai.atom(true);
150152
this.persist = getBlockMetaKeyAtom(this.blockId, "vdom:persist");
151153
this.wshClient = new VDomWshClient(this);
152154
DefaultRouter.registerRoute(this.wshClient.routeId, this.wshClient);

frontend/types/custom.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ declare global {
229229
endIconButtons?: jotai.Atom<IconButtonDecl[]>;
230230
blockBg?: jotai.Atom<MetaType>;
231231
manageConnection?: jotai.Atom<boolean>;
232+
noPadding?: jotai.Atom<boolean>;
232233

233234
onBack?: () => void;
234235
onForward?: () => void;

frontend/types/gotypes.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ declare global {
365365
"term:localshellopts"?: string[];
366366
"term:scrollback"?: number;
367367
"term:vdomblockid"?: string;
368+
"term:vdomtoolbarblockid"?: string;
368369
"vdom:*"?: boolean;
369370
"vdom:initialized"?: boolean;
370371
"vdom:correlationid"?: string;
@@ -795,6 +796,13 @@ declare global {
795796
type VDomTarget = {
796797
newblock?: boolean;
797798
magnified?: boolean;
799+
toolbar?: VDomTargetToolbar;
800+
};
801+
802+
// vdom.VDomTargetToolbar
803+
type VDomTargetToolbar = {
804+
toolbar: boolean;
805+
height?: string;
798806
};
799807

800808
// vdom.VDomTransferElem

pkg/vdom/vdom_types.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,14 @@ type VDomMessage struct {
205205
// target -- to support new targets in the future, like toolbars, partial blocks, splits, etc.
206206
// default is vdom context inside of a terminal block
207207
type VDomTarget struct {
208-
NewBlock bool `json:"newblock,omitempty"`
209-
Magnified bool `json:"magnified,omitempty"`
208+
NewBlock bool `json:"newblock,omitempty"`
209+
Magnified bool `json:"magnified,omitempty"`
210+
Toolbar *VDomTargetToolbar `json:"toolbar,omitempty"`
211+
}
212+
213+
type VDomTargetToolbar struct {
214+
Toolbar bool `json:"toolbar"`
215+
Height string `json:"height,omitempty"`
210216
}
211217

212218
// matches WaveKeyboardEvent

pkg/vdom/vdomclient/vdomclient.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ type AppOpts struct {
3333
GlobalStyles []byte
3434
RootComponentName string // defaults to "App"
3535
NewBlockFlag string // defaults to "n" (set to "-" to disable)
36+
TargetNewBlock bool
37+
TargetToolbar *vdom.VDomTargetToolbar
3638
}
3739

3840
type Client struct {
@@ -116,7 +118,17 @@ func (client *Client) runMainE() error {
116118
if err != nil {
117119
return err
118120
}
119-
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: client.NewBlockFlag})
121+
target := &vdom.VDomTarget{}
122+
if client.AppOpts.TargetNewBlock || client.NewBlockFlag {
123+
target.NewBlock = client.NewBlockFlag
124+
}
125+
if client.AppOpts.TargetToolbar != nil {
126+
target.Toolbar = client.AppOpts.TargetToolbar
127+
}
128+
if target.NewBlock && target.Toolbar != nil {
129+
return fmt.Errorf("cannot specify both new block and toolbar target")
130+
}
131+
err = client.CreateVDomContext(target)
120132
if err != nil {
121133
return err
122134
}

0 commit comments

Comments
 (0)