mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
vdom terminal toolbar (#1263)
This commit is contained in:
parent
83f671c7a9
commit
3fc45c63f3
@ -26,6 +26,10 @@
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
padding: 5px;
|
||||
|
||||
&.block-no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.block-focuselem {
|
||||
|
@ -25,12 +25,13 @@ import {
|
||||
} from "@/store/global";
|
||||
import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos";
|
||||
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 { QuickTipsView, QuickTipsViewModel } from "@/view/quicktipsview/quicktipsview";
|
||||
import { TermViewModel, TerminalView, makeTerminalModel } from "@/view/term/term";
|
||||
import { WaveAi, WaveAiModel, makeWaveAiViewModel } from "@/view/waveai/waveai";
|
||||
import { WebView, WebViewModel, makeWebViewModel } from "@/view/webview/webview";
|
||||
import clsx from "clsx";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
import { Suspense, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import "./block.less";
|
||||
@ -154,11 +155,12 @@ const BlockSubBlock = memo(({ nodeModel, viewModel }: FullSubBlockProps) => {
|
||||
() => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel),
|
||||
[nodeModel.blockId, blockData?.meta?.view, viewModel]
|
||||
);
|
||||
const noPadding = useAtomValueSafe(viewModel.noPadding);
|
||||
if (!blockData) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div key="content" className="block-content" ref={contentRef}>
|
||||
<div key="content" className={clsx("block-content", { "block-no-padding": noPadding })} ref={contentRef}>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</Suspense>
|
||||
</ErrorBoundary>
|
||||
@ -176,6 +178,7 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
|
||||
const isFocused = useAtomValue(nodeModel.isFocused);
|
||||
const disablePointerEvents = useAtomValue(nodeModel.disablePointerEvents);
|
||||
const innerRect = useDebouncedNodeInnerRect(nodeModel);
|
||||
const noPadding = useAtomValueSafe(viewModel.noPadding);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setBlockClicked(isFocused);
|
||||
@ -273,7 +276,12 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</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>
|
||||
<Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</Suspense>
|
||||
</ErrorBoundary>
|
||||
|
@ -41,6 +41,35 @@ export class TermWshClient extends WshClient {
|
||||
magnified: data.target?.magnified,
|
||||
});
|
||||
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 {
|
||||
// in the terminal
|
||||
// check if there is a current active vdom block
|
||||
|
@ -13,11 +13,10 @@
|
||||
|
||||
.view-term {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding-left: 4px;
|
||||
position: relative;
|
||||
|
||||
.term-header {
|
||||
@ -31,11 +30,19 @@
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.term-toolbar {
|
||||
height: 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.term-connectelem {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
margin: 5px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.term-htmlelem {
|
||||
|
@ -56,8 +56,11 @@ class TermViewModel {
|
||||
termWshClient: TermWshClient;
|
||||
shellProcStatusRef: React.MutableRefObject<string>;
|
||||
vdomBlockId: jotai.Atom<string>;
|
||||
vdomToolbarBlockId: jotai.Atom<string>;
|
||||
vdomToolbarTarget: jotai.PrimitiveAtom<VDomTargetToolbar>;
|
||||
fontSizeAtom: jotai.Atom<number>;
|
||||
termThemeNameAtom: jotai.Atom<string>;
|
||||
noPadding: jotai.PrimitiveAtom<boolean>;
|
||||
|
||||
constructor(blockId: string, nodeModel: BlockNodeModel) {
|
||||
this.viewType = "term";
|
||||
@ -70,6 +73,11 @@ class TermViewModel {
|
||||
const blockData = get(this.blockAtom);
|
||||
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) => {
|
||||
const blockData = get(this.blockAtom);
|
||||
return blockData?.meta?.["term:mode"] ?? "term";
|
||||
@ -167,6 +175,7 @@ class TermViewModel {
|
||||
return blockData?.meta?.["term:theme"] ?? get(settingsKeyAtom) ?? "default-dark";
|
||||
});
|
||||
});
|
||||
this.noPadding = jotai.atom(true);
|
||||
}
|
||||
|
||||
setTermMode(mode: "term" | "vdom") {
|
||||
@ -191,6 +200,18 @@ class TermViewModel {
|
||||
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() {
|
||||
DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId));
|
||||
}
|
||||
@ -347,6 +368,15 @@ class TermViewModel {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -382,6 +412,44 @@ const TermResyncHandler = React.memo(({ blockId, model }: TerminalViewProps) =>
|
||||
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 }) => {
|
||||
React.useEffect(() => {
|
||||
const unsub = waveEventSubscribe({
|
||||
@ -431,6 +499,21 @@ const TermVDomNode = ({ blockId, model }: TerminalViewProps) => {
|
||||
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 viewRef = 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,
|
||||
blockId: blockId,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx("view-term", "term-mode-" + termMode)} ref={viewRef}>
|
||||
<TermResyncHandler blockId={blockId} model={model} />
|
||||
<TermThemeUpdater blockId={blockId} termRef={termRef} />
|
||||
<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} />
|
||||
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -134,6 +134,7 @@ export class VDomModel {
|
||||
refOutputStore: Map<string, any> = new Map();
|
||||
globalVersion: jotai.PrimitiveAtom<number> = jotai.atom(0);
|
||||
hasBackendWork: boolean = false;
|
||||
noPadding: jotai.PrimitiveAtom<boolean>;
|
||||
|
||||
constructor(blockId: string, nodeModel: BlockNodeModel) {
|
||||
this.viewType = "vdom";
|
||||
@ -147,6 +148,7 @@ export class VDomModel {
|
||||
const blockData = get(WOS.getWaveObjectAtom<Block>(makeORef("block", this.blockId)));
|
||||
return blockData?.meta?.["vdom:route"];
|
||||
});
|
||||
this.noPadding = jotai.atom(true);
|
||||
this.persist = getBlockMetaKeyAtom(this.blockId, "vdom:persist");
|
||||
this.wshClient = new VDomWshClient(this);
|
||||
DefaultRouter.registerRoute(this.wshClient.routeId, this.wshClient);
|
||||
|
1
frontend/types/custom.d.ts
vendored
1
frontend/types/custom.d.ts
vendored
@ -229,6 +229,7 @@ declare global {
|
||||
endIconButtons?: jotai.Atom<IconButtonDecl[]>;
|
||||
blockBg?: jotai.Atom<MetaType>;
|
||||
manageConnection?: jotai.Atom<boolean>;
|
||||
noPadding?: jotai.Atom<boolean>;
|
||||
|
||||
onBack?: () => void;
|
||||
onForward?: () => void;
|
||||
|
8
frontend/types/gotypes.d.ts
vendored
8
frontend/types/gotypes.d.ts
vendored
@ -365,6 +365,7 @@ declare global {
|
||||
"term:localshellopts"?: string[];
|
||||
"term:scrollback"?: number;
|
||||
"term:vdomblockid"?: string;
|
||||
"term:vdomtoolbarblockid"?: string;
|
||||
"vdom:*"?: boolean;
|
||||
"vdom:initialized"?: boolean;
|
||||
"vdom:correlationid"?: string;
|
||||
@ -795,6 +796,13 @@ declare global {
|
||||
type VDomTarget = {
|
||||
newblock?: boolean;
|
||||
magnified?: boolean;
|
||||
toolbar?: VDomTargetToolbar;
|
||||
};
|
||||
|
||||
// vdom.VDomTargetToolbar
|
||||
type VDomTargetToolbar = {
|
||||
toolbar: boolean;
|
||||
height?: string;
|
||||
};
|
||||
|
||||
// vdom.VDomTransferElem
|
||||
|
@ -205,8 +205,14 @@ type VDomMessage struct {
|
||||
// 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"`
|
||||
NewBlock bool `json:"newblock,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
|
||||
|
@ -33,6 +33,8 @@ type AppOpts struct {
|
||||
GlobalStyles []byte
|
||||
RootComponentName string // defaults to "App"
|
||||
NewBlockFlag string // defaults to "n" (set to "-" to disable)
|
||||
TargetNewBlock bool
|
||||
TargetToolbar *vdom.VDomTargetToolbar
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
@ -116,7 +118,17 @@ func (client *Client) runMainE() error {
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -85,6 +85,7 @@ const (
|
||||
MetaKey_TermLocalShellOpts = "term:localshellopts"
|
||||
MetaKey_TermScrollback = "term:scrollback"
|
||||
MetaKey_TermVDomSubBlockId = "term:vdomblockid"
|
||||
MetaKey_TermVDomToolbarBlockId = "term:vdomtoolbarblockid"
|
||||
|
||||
MetaKey_VDomClear = "vdom:*"
|
||||
MetaKey_VDomInitialized = "vdom:initialized"
|
||||
|
@ -77,15 +77,16 @@ type MetaTSType struct {
|
||||
BgBorderColor string `json:"bg:bordercolor,omitempty"` // frame:bordercolor
|
||||
BgActiveBorderColor string `json:"bg:activebordercolor,omitempty"` // frame:activebordercolor
|
||||
|
||||
TermClear bool `json:"term:*,omitempty"`
|
||||
TermFontSize int `json:"term:fontsize,omitempty"`
|
||||
TermFontFamily string `json:"term:fontfamily,omitempty"`
|
||||
TermMode string `json:"term:mode,omitempty"`
|
||||
TermTheme string `json:"term:theme,omitempty"`
|
||||
TermLocalShellPath string `json:"term:localshellpath,omitempty"` // matches settings
|
||||
TermLocalShellOpts []string `json:"term:localshellopts,omitempty"` // matches settings
|
||||
TermScrollback *int `json:"term:scrollback,omitempty"`
|
||||
TermVDomSubBlockId string `json:"term:vdomblockid,omitempty"`
|
||||
TermClear bool `json:"term:*,omitempty"`
|
||||
TermFontSize int `json:"term:fontsize,omitempty"`
|
||||
TermFontFamily string `json:"term:fontfamily,omitempty"`
|
||||
TermMode string `json:"term:mode,omitempty"`
|
||||
TermTheme string `json:"term:theme,omitempty"`
|
||||
TermLocalShellPath string `json:"term:localshellpath,omitempty"` // matches settings
|
||||
TermLocalShellOpts []string `json:"term:localshellopts,omitempty"` // matches settings
|
||||
TermScrollback *int `json:"term:scrollback,omitempty"`
|
||||
TermVDomSubBlockId string `json:"term:vdomblockid,omitempty"`
|
||||
TermVDomToolbarBlockId string `json:"term:vdomtoolbarblockid,omitempty"`
|
||||
|
||||
VDomClear bool `json:"vdom:*,omitempty"`
|
||||
VDomInitialized bool `json:"vdom:initialized,omitempty"`
|
||||
|
Loading…
Reference in New Issue
Block a user