From cfc875bc2184176923b557968641e43725aa6da6 Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Tue, 30 Jul 2024 12:33:28 -0700 Subject: [PATCH] metadata updates (frontend typing) (#174) --- cmd/wsh/cmd/wshcmd-view.go | 4 +- frontend/app/app.tsx | 4 +- frontend/app/block/block.tsx | 35 +++-- frontend/app/view/preview.tsx | 8 +- frontend/app/view/term/term.tsx | 4 +- frontend/app/view/term/termsticker.tsx | 4 +- frontend/app/view/term/termtheme.ts | 2 +- frontend/app/view/waveai.less | 5 - frontend/app/workspace/workspace.tsx | 10 +- frontend/types/gotypes.d.ts | 59 +++++--- pkg/blockcontroller/blockcontroller.go | 95 ++++--------- pkg/ijson/ijson.go | 40 ++++++ pkg/service/blockservice/blockservice.go | 5 +- pkg/service/objectservice/objectservice.go | 5 +- pkg/service/service.go | 2 +- pkg/tsgen/tsgen.go | 25 ++-- pkg/util/utilfn/utilfn.go | 13 +- pkg/waveobj/metamap.go | 74 ++++++++++ pkg/waveobj/waveobj.go | 15 +- pkg/wconfig/settingsconfig.go | 24 +++- pkg/web/webcmd/webcmd.go | 6 +- pkg/wshrpc/wshclient/wshclient.go | 4 +- pkg/wshrpc/wshrpctypes.go | 8 +- pkg/wshrpc/wshserver/wshserver.go | 33 +---- pkg/wstore/wstore.go | 42 +----- pkg/wstore/wstore_meta.go | 153 +++++++++++++++++++++ pkg/wstore/wstore_types.go | 103 ++++---------- 27 files changed, 482 insertions(+), 300 deletions(-) create mode 100644 pkg/waveobj/metamap.go create mode 100644 pkg/wstore/wstore_meta.go diff --git a/cmd/wsh/cmd/wshcmd-view.go b/cmd/wsh/cmd/wshcmd-view.go index 2e198f6b1..1a5ddc995 100644 --- a/cmd/wsh/cmd/wshcmd-view.go +++ b/cmd/wsh/cmd/wshcmd-view.go @@ -47,9 +47,9 @@ func viewRun(cmd *cobra.Command, args []string) { wshutil.SetTermRawModeAndInstallShutdownHandlers(true) viewWshCmd := &wshrpc.CommandCreateBlockData{ BlockDef: &wstore.BlockDef{ - View: "preview", Meta: map[string]interface{}{ - "file": absFile, + wstore.MetaKey_View: "preview", + wstore.MetaKey_File: absFile, }, }, } diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index 364f130d7..8c7c757ca 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -313,11 +313,11 @@ const AppInner = () => { function handleKeyDown(waveEvent: WaveKeyboardEvent): boolean { // global key handler for now (refactor later) - if (keyutil.checkKeyPressed(waveEvent, "Cmd:]")) { + if (keyutil.checkKeyPressed(waveEvent, "Cmd:]") || keyutil.checkKeyPressed(waveEvent, "Shift:Cmd:]")) { switchTab(1); return true; } - if (keyutil.checkKeyPressed(waveEvent, "Cmd:[")) { + if (keyutil.checkKeyPressed(waveEvent, "Cmd:[") || keyutil.checkKeyPressed(waveEvent, "Shift:Cmd:[")) { switchTab(-1); return true; } diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 8fc09edfe..15c4b364a 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -153,8 +153,8 @@ function getBlockHeaderText(blockIcon: string, blockData: Block, settings: Setti return [blockIconElem, blockData.meta.title + blockIdStr]; } } - let viewString = blockData?.view; - if (blockData.controller == "cmd") { + let viewString = blockData?.meta?.view; + if (blockData?.meta?.controller == "cmd") { viewString = "cmd"; } return [blockIconElem, viewString + blockIdStr]; @@ -259,8 +259,8 @@ const BlockFrame_Default_Component = ({ }); }); let isFocused = jotai.useAtomValue(isFocusedAtom); - const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.view); - const viewName = util.useAtomValueSafe(viewModel.viewName) ?? blockViewToName(blockData?.view); + const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view); + const viewName = util.useAtomValueSafe(viewModel.viewName) ?? blockViewToName(blockData?.meta?.view); const headerTextUnion = util.useAtomValueSafe(viewModel.viewText); const preIconButton = util.useAtomValueSafe(viewModel.preIconButton); const endIconButtons = util.useAtomValueSafe(viewModel.endIconButtons); @@ -377,6 +377,10 @@ const BlockFrame_Default_Component = ({ headerTextElems.push(...renderHeaderElements(headerTextUnion)); } + function handleDoubleClick() { + layoutModel?.onMagnifyToggle(); + } + return (
handleHeaderContextMenu( e, @@ -456,6 +461,9 @@ function blockViewToIcon(view: string): string { } function blockViewToName(view: string): string { + if (util.isBlank(view)) { + return "(No View)"; + } if (view == "term") { return "Terminal"; } @@ -476,12 +484,12 @@ function getViewElemAndModel( blockView: string, blockRef: React.RefObject ): { viewModel: ViewModel; viewElem: JSX.Element } { - if (blockView == null) { - return { viewElem: null, viewModel: null }; - } let viewElem: JSX.Element = null; let viewModel: ViewModel = null; - if (blockView === "term") { + if (util.isBlank(blockView)) { + viewElem = No View; + viewModel = makeDefaultViewModel(blockId); + } else if (blockView === "term") { const termViewModel = makeTerminalModel(blockId); viewElem = ; viewModel = termViewModel; @@ -501,6 +509,7 @@ function getViewElemAndModel( viewModel = waveAiModel; } if (viewModel == null) { + viewElem = Invalid View "{blockView}"; viewModel = makeDefaultViewModel(blockId); } return { viewElem, viewModel }; @@ -511,11 +520,11 @@ function makeDefaultViewModel(blockId: string): ViewModel { let viewModel: ViewModel = { viewIcon: jotai.atom((get) => { const blockData = get(blockDataAtom); - return blockViewToIcon(blockData?.view); + return blockViewToIcon(blockData?.meta?.view); }), viewName: jotai.atom((get) => { const blockData = get(blockDataAtom); - return blockViewToName(blockData?.view); + return blockViewToName(blockData?.meta?.view); }), viewText: jotai.atom((get) => { const blockData = get(blockDataAtom); @@ -532,7 +541,7 @@ const BlockPreview = React.memo(({ blockId, layoutModel }: BlockProps) => { if (!blockData) { return null; } - let { viewModel } = getViewElemAndModel(blockId, blockData?.view, null); + let { viewModel } = getViewElemAndModel(blockId, blockData?.meta?.view, null); return ( { }, []); let { viewElem, viewModel } = React.useMemo( - () => getViewElemAndModel(blockId, blockData?.view, blockRef), - [blockId, blockData?.view, blockRef] + () => getViewElemAndModel(blockId, blockData?.meta?.view, blockRef), + [blockId, blockData?.meta?.view, blockRef] ); const determineFocusedChild = React.useCallback( diff --git a/frontend/app/view/preview.tsx b/frontend/app/view/preview.tsx index 805d4b0ef..5467ca7a0 100644 --- a/frontend/app/view/preview.tsx +++ b/frontend/app/view/preview.tsx @@ -284,10 +284,10 @@ export class PreviewModel implements ViewModel { label: "Open Terminal in New Block", click: async () => { const termBlockDef: BlockDef = { - controller: "shell", - view: "term", meta: { - cwd: globalStore.get(this.fileName), + view: "term", + controller: "shell", + "cmd:cwd": globalStore.get(this.fileName), }, }; await createBlock(termBlockDef); @@ -579,4 +579,4 @@ function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel ); } -export { PreviewView, makePreviewModel }; +export { makePreviewModel, PreviewView }; diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index ef11249d0..40f279ab7 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -143,7 +143,7 @@ class TermViewModel { }); this.viewName = jotai.atom((get) => { const blockData = get(this.blockAtom); - if (blockData.controller == "cmd") { + if (blockData?.meta?.controller == "cmd") { return "Command"; } return "Terminal"; @@ -219,7 +219,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => { } } const settings = globalStore.get(atoms.settingsConfigAtom); - const termTheme = computeTheme(settings, blockData?.meta?.termtheme); + const termTheme = computeTheme(settings, blockData?.meta?.["term:theme"]); const termWrap = new TermWrap( blockId, connectElemRef.current, diff --git a/frontend/app/view/term/termsticker.tsx b/frontend/app/view/term/termsticker.tsx index 1b3511e29..fb9c33f21 100644 --- a/frontend/app/view/term/termsticker.tsx +++ b/frontend/app/view/term/termsticker.tsx @@ -99,7 +99,7 @@ function TermSticker({ sticker, config }: { sticker: StickerType; config: Sticke console.log("clickHandler", sticker.clickcmd, sticker.clickblockdef); if (sticker.clickcmd) { const b64data = btoa(sticker.clickcmd); - WshServer.BlockInputCommand({ blockid: config.blockId, inputdata64: b64data }); + WshServer.ControllerInputCommand({ blockid: config.blockId, inputdata64: b64data }); } if (sticker.clickblockdef) { createBlock(sticker.clickblockdef); @@ -183,7 +183,7 @@ export function TermStickers({ config }: { config: StickerTermConfig }) { imgsrc: "~/Downloads/natureicon.png", opacity: 0.8, pointerevents: true, - clickblockdef: { view: "preview", meta: { file: "~/" } }, + clickblockdef: { meta: { file: "~/", view: "preview" } }, }); stickers.push({ position: "absolute", diff --git a/frontend/app/view/term/termtheme.ts b/frontend/app/view/term/termtheme.ts index cdcb816d5..e39c18fc4 100644 --- a/frontend/app/view/term/termtheme.ts +++ b/frontend/app/view/term/termtheme.ts @@ -16,7 +16,7 @@ const TermThemeUpdater = ({ blockId, termRef }: TermThemeProps) => { const { termthemes } = useAtomValue(atoms.settingsConfigAtom); const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); let defaultThemeName = "default-dark"; - let themeName = blockData.meta?.termtheme ?? "default-dark"; + let themeName = blockData.meta?.["term:theme"] ?? "default-dark"; const defaultTheme: TermThemeType = termthemes?.[defaultThemeName] || ({} as any); const theme: TermThemeType = termthemes?.[themeName] || ({} as any); diff --git a/frontend/app/view/waveai.less b/frontend/app/view/waveai.less index 7d90eceab..4f70e77b8 100644 --- a/frontend/app/view/waveai.less +++ b/frontend/app/view/waveai.less @@ -24,11 +24,6 @@ .filler { flex: 1 1 auto; } - - > * { - cursor: default; - user-select: none; - } } .chat-msg { diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index 38f177f7d..9b2acdae1 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -20,16 +20,18 @@ const Widgets = React.memo(() => { const newWidgetModalVisible = React.useState(false); async function clickTerminal() { const termBlockDef: BlockDef = { - controller: "shell", - view: "term", + meta: { + controller: "shell", + view: "term", + }, }; createBlock(termBlockDef); } async function clickHome() { const editDef: BlockDef = { - view: "preview", meta: { + view: "preview", file: "~", }, }; @@ -37,8 +39,8 @@ const Widgets = React.memo(() => { } async function clickWeb() { const editDef: BlockDef = { - view: "web", meta: { + view: "web", url: "https://waveterm.dev/", }, }; diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 31efef432..f280b9666 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -24,11 +24,8 @@ declare global { // wstore.Block type Block = WaveObj & { blockdef: BlockDef; - controller: string; - view: string; runtimeopts?: RuntimeOpts; stickers?: StickerType[]; - meta: MetaType; }; // blockcontroller.BlockControllerRuntimeStatus @@ -40,8 +37,6 @@ declare global { // wstore.BlockDef type BlockDef = { - controller?: string; - view?: string; files?: {[key: string]: FileDef}; meta?: MetaType; }; @@ -60,9 +55,7 @@ declare global { // wstore.Client type Client = WaveObj & { - mainwindowid: string; windowids: string[]; - meta: MetaType; tosagreed?: number; }; @@ -70,7 +63,7 @@ declare global { type CommandAppendIJsonData = { zoneid: string; filename: string; - data: MetaType; + data: {[key: string]: any}; }; // wshrpc.CommandBlockInputData @@ -144,7 +137,7 @@ declare global { path?: string; url?: string; content?: string; - meta?: MetaType; + meta?: {[key: string]: any}; }; // fileservice.FileInfo @@ -178,10 +171,42 @@ declare global { type LayoutNode = WaveObj & { node?: any; magnifiednodeid?: string; - meta?: MetaType; }; - type MetaType = {[key: string]: any} + // wstore.MetaTSType + type MetaType = { + view?: string; + controller?: string; + title?: string; + file?: string; + url?: string; + connection?: string; + icon?: string; + "icon:color"?: string; + frame?: boolean; + "frame:*"?: boolean; + "frame:bordercolor"?: string; + "frame:bordercolor:focused"?: string; + cmd?: string; + "cmd:*"?: boolean; + "cmd:interactive"?: boolean; + "cmd:login"?: boolean; + "cmd:runonstart"?: boolean; + "cmd:clearonstart"?: boolean; + "cmd:clearonrestart"?: boolean; + "cmd:env"?: {[key: string]: string}; + "cmd:cwd"?: string; + "cmd:nowsh"?: boolean; + bg?: string; + "bg:*"?: boolean; + "bg:opacity"?: number; + "bg:blendmode"?: string; + "term:*"?: boolean; + "term:fontsize"?: number; + "term:fontfamily"?: string; + "term:mode"?: string; + "term:theme"?: string; + }; // tsgenmeta.MethodMeta type MethodMeta = { @@ -284,6 +309,8 @@ declare global { autoupdate: AutoUpdateOpts; termthemes: {[key: string]: TermThemeType}; window: WindowSettingsType; + defaultmeta?: MetaType; + presets?: {[key: string]: MetaType}; }; // wstore.StickerClickOptsType @@ -302,7 +329,7 @@ declare global { // wstore.StickerType type StickerType = { stickertype: string; - style: MetaType; + style: {[key: string]: any}; clickopts?: StickerClickOptsType; display: StickerDisplayOptsType; }; @@ -319,7 +346,6 @@ declare global { name: string; layoutnode: string; blockids: string[]; - meta: MetaType; }; // shellexec.TermSize @@ -392,7 +418,7 @@ declare global { type VDomElem = { id?: string; tag: string; - props?: MetaType; + props?: {[key: string]: any}; children?: VDomElem[]; text?: string; }; @@ -465,7 +491,7 @@ declare global { createdts: number; size: number; modts: number; - meta: MetaType; + meta: {[key: string]: any}; }; // waveobj.WaveObj @@ -473,6 +499,7 @@ declare global { otype: string; oid: string; version: number; + meta: MetaType; }; // wstore.WaveObjUpdate @@ -492,7 +519,6 @@ declare global { pos: Point; winsize: WinSize; lastfocusts: number; - meta: MetaType; }; // service.WebCallType @@ -538,7 +564,6 @@ declare global { type Workspace = WaveObj & { name: string; tabids: string[]; - meta: MetaType; }; // wshrpc.WshRpcCommandOpts diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index 7ebcf752c..c15b5dd83 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -186,7 +186,7 @@ func (bc *BlockController) resetTerminalState() { var shouldTruncate bool blockData, getBlockDataErr := wstore.DBMustGet[*wstore.Block](ctx, bc.BlockId) if getBlockDataErr == nil { - shouldTruncate = getBoolFromMeta(blockData.Meta, wstore.MetaKey_CmdClearOnRestart, false) + shouldTruncate = blockData.Meta.GetBool(wstore.MetaKey_CmdClearOnRestart, false) } if shouldTruncate { err := HandleTruncateBlockFile(bc.BlockId, BlockFile_Main) @@ -208,34 +208,6 @@ func (bc *BlockController) resetTerminalState() { } } -func getMetaBool(meta map[string]any, key string, def bool) bool { - val, found := meta[key] - if !found { - return def - } - if val == nil { - return def - } - if bval, ok := val.(bool); ok { - return bval - } - return def -} - -func getMetaStr(meta map[string]any, key string, def string) string { - val, found := meta[key] - if !found { - return def - } - if val == nil { - return def - } - if sval, ok := val.(string); ok { - return sval - } - return def -} - // every byte is 4-bits of randomness func randomHexString(numHexDigits int) (string, error) { numBytes := (numHexDigits + 1) / 2 // Calculate the number of bytes needed @@ -248,7 +220,7 @@ func randomHexString(numHexDigits int) (string, error) { return hexStr[:numHexDigits], nil // Return the exact number of hex digits } -func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[string]any) error { +func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj.MetaMapType) error { // create a circular blockfile for the output ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) defer cancelFn() @@ -276,7 +248,7 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[str return shellProcErr } var remoteDomainSocketName string - remoteName := getMetaStr(blockMeta, "connection", "") + remoteName := blockMeta.GetString(wstore.MetaKey_Connection, "") isRemote := remoteName != "" if isRemote { randStr, err := randomHexString(16) // 64-bits of randomness @@ -289,7 +261,7 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[str cmdOpts := shellexec.CommandOptsType{ Env: make(map[string]string), } - if !getMetaBool(blockMeta, "nowsh", false) { + if !blockMeta.GetBool(wstore.MetaKey_CmdNoWsh, false) { if isRemote { jwtStr, err := wshutil.MakeClientJWTToken(wshrpc.RpcContext{TabId: bc.TabId, BlockId: bc.BlockId}, remoteDomainSocketName) if err != nil { @@ -307,44 +279,31 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[str if bc.ControllerType == BlockController_Shell { cmdOpts.Interactive = true cmdOpts.Login = true - cmdOpts.Cwd, _ = blockMeta["cmd:cwd"].(string) + cmdOpts.Cwd = blockMeta.GetString(wstore.MetaKey_CmdCwd, "") if cmdOpts.Cwd != "" { cmdOpts.Cwd = wavebase.ExpandHomeDir(cmdOpts.Cwd) } } else if bc.ControllerType == BlockController_Cmd { - if _, ok := blockMeta["cmd"].(string); ok { - cmdStr = blockMeta["cmd"].(string) - } else { + cmdStr = blockMeta.GetString(wstore.MetaKey_Cmd, "") + if cmdStr == "" { return fmt.Errorf("missing cmd in block meta") } - if _, ok := blockMeta["cmd:cwd"].(string); ok { - cmdOpts.Cwd = blockMeta["cmd:cwd"].(string) - if cmdOpts.Cwd != "" { - cmdOpts.Cwd = wavebase.ExpandHomeDir(cmdOpts.Cwd) - } + cmdOpts.Cwd = blockMeta.GetString(wstore.MetaKey_CmdCwd, "") + if cmdOpts.Cwd != "" { + cmdOpts.Cwd = wavebase.ExpandHomeDir(cmdOpts.Cwd) } - if _, ok := blockMeta["cmd:interactive"]; ok { - if blockMeta["cmd:interactive"].(bool) { - cmdOpts.Interactive = true + cmdOpts.Interactive = blockMeta.GetBool(wstore.MetaKey_CmdInteractive, false) + cmdOpts.Login = blockMeta.GetBool(wstore.MetaKey_CmdLogin, false) + cmdEnv := blockMeta.GetMap(wstore.MetaKey_CmdEnv) + for k, v := range cmdEnv { + if v == nil { + continue } - } - if _, ok := blockMeta["cmd:login"]; ok { - if blockMeta["cmd:login"].(bool) { - cmdOpts.Login = true + if _, ok := v.(string); ok { + cmdOpts.Env[k] = v.(string) } - } - if _, ok := blockMeta["cmd:env"].(map[string]any); ok { - cmdEnv := blockMeta["cmd:env"].(map[string]any) - for k, v := range cmdEnv { - if v == nil { - continue - } - if _, ok := v.(string); ok { - cmdOpts.Env[k] = v.(string) - } - if _, ok := v.(float64); ok { - cmdOpts.Env[k] = fmt.Sprintf("%v", v) - } + if _, ok := v.(float64); ok { + cmdOpts.Env[k] = fmt.Sprintf("%v", v) } } } else { @@ -477,8 +436,9 @@ func (bc *BlockController) run(bdata *wstore.Block, blockMeta map[string]any) { bc.Status = Status_Running return true }) - if bdata.Controller != BlockController_Shell && bdata.Controller != BlockController_Cmd { - log.Printf("unknown controller %q\n", bdata.Controller) + controllerName := bdata.Meta.GetString(wstore.MetaKey_Controller, "") + if controllerName != BlockController_Shell && controllerName != BlockController_Cmd { + log.Printf("unknown controller %q\n", controllerName) return } if getBoolFromMeta(blockMeta, wstore.MetaKey_CmdClearOnStart, false) { @@ -527,12 +487,13 @@ func StartBlockController(ctx context.Context, tabId string, blockId string) err if err != nil { return fmt.Errorf("error getting block: %w", err) } - if blockData.Controller == "" { + controllerName := blockData.Meta.GetString(wstore.MetaKey_Controller, "") + if controllerName == "" { // nothing to start return nil } - if blockData.Controller != BlockController_Shell && blockData.Controller != BlockController_Cmd { - return fmt.Errorf("unknown controller %q", blockData.Controller) + if controllerName != BlockController_Shell && controllerName != BlockController_Cmd { + return fmt.Errorf("unknown controller %q", controllerName) } globalLock.Lock() defer globalLock.Unlock() @@ -542,7 +503,7 @@ func StartBlockController(ctx context.Context, tabId string, blockId string) err } bc := &BlockController{ Lock: &sync.Mutex{}, - ControllerType: blockData.Controller, + ControllerType: controllerName, TabId: tabId, BlockId: blockId, Status: Status_Init, diff --git a/pkg/ijson/ijson.go b/pkg/ijson/ijson.go index bb442f3ad..1a21393bb 100644 --- a/pkg/ijson/ijson.go +++ b/pkg/ijson/ijson.go @@ -10,6 +10,7 @@ import ( "fmt" "regexp" "strconv" + "strings" ) // ijson values are built out of standard go building blocks: @@ -56,6 +57,45 @@ func MakeAppendCommand(path Path, value any) Command { } } +var pathPartKeyRe = regexp.MustCompile(`^[a-zA-Z0-9:_#-]+`) + +func ParseSimplePath(input string) ([]any, error) { + var path []any + // Scan the input string character by character + for i := 0; i < len(input); { + if input[i] == '[' { + // Handle the index + end := strings.Index(input[i:], "]") + if end == -1 { + return nil, fmt.Errorf("unmatched bracket at position %d", i) + } + index, err := strconv.Atoi(input[i+1 : i+end]) + if err != nil { + return nil, fmt.Errorf("invalid index at position %d: %v", i, err) + } + path = append(path, index) + i += end + 1 + } else { + // Handle the key + j := i + for j < len(input) && input[j] != '.' && input[j] != '[' { + j++ + } + key := input[i:j] + if !pathPartKeyRe.MatchString(key) { + return nil, fmt.Errorf("invalid key at position %d: %s", i, key) + } + path = append(path, key) + i = j + } + if i < len(input) && input[i] == '.' { + i++ + } + } + + return path, nil +} + type PathError struct { Err string } diff --git a/pkg/service/blockservice/blockservice.go b/pkg/service/blockservice/blockservice.go index bd3f5506d..73b0d896d 100644 --- a/pkg/service/blockservice/blockservice.go +++ b/pkg/service/blockservice/blockservice.go @@ -66,8 +66,9 @@ func (bs *BlockService) SaveWaveAiData(ctx context.Context, blockId string, hist if err != nil { return err } - if block.View != "waveai" { - return fmt.Errorf("invalid view type: %s", block.View) + viewName := block.Meta.GetString(wstore.MetaKey_View, "") + if viewName != "waveai" { + return fmt.Errorf("invalid view type: %s", viewName) } historyBytes, err := json.Marshal(history) if err != nil { diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go index 276a02e78..31a30df64 100644 --- a/pkg/service/objectservice/objectservice.go +++ b/pkg/service/objectservice/objectservice.go @@ -189,7 +189,8 @@ func (svc *ObjectService) CreateBlock(uiContext wstore.UIContext, blockDef *wsto if err != nil { return "", nil, fmt.Errorf("error creating block: %w", err) } - if blockData.Controller != "" { + controllerName := blockData.Meta.GetString(wstore.MetaKey_Controller, "") + if controllerName != "" { err = blockcontroller.StartBlockController(ctx, uiContext.ActiveTabId, blockData.OID) if err != nil { return "", nil, fmt.Errorf("error starting block controller: %w", err) @@ -228,7 +229,7 @@ func (svc *ObjectService) UpdateObjectMeta_Meta() tsgenmeta.MethodMeta { } } -func (svc *ObjectService) UpdateObjectMeta(uiContext wstore.UIContext, orefStr string, meta map[string]any) (wstore.UpdatesRtnType, error) { +func (svc *ObjectService) UpdateObjectMeta(uiContext wstore.UIContext, orefStr string, meta wstore.MetaMapType) (wstore.UpdatesRtnType, error) { ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) defer cancelFn() ctx = wstore.ContextWithUpdates(ctx) diff --git a/pkg/service/service.go b/pkg/service/service.go index 493f93971..566faa30d 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -89,7 +89,7 @@ func convertNumber(argType reflect.Type, jsonArg float64) (any, error) { func convertComplex(argType reflect.Type, jsonArg any) (any, error) { nativeArgVal := reflect.New(argType) - err := utilfn.DoMapStucture(nativeArgVal.Interface(), jsonArg) + err := utilfn.DoMapStructure(nativeArgVal.Interface(), jsonArg) if err != nil { return nil, err } diff --git a/pkg/tsgen/tsgen.go b/pkg/tsgen/tsgen.go index 6f45cc930..fc1ff0b60 100644 --- a/pkg/tsgen/tsgen.go +++ b/pkg/tsgen/tsgen.go @@ -45,6 +45,7 @@ var ExtraTypes = []any{ vdom.Elem{}, vdom.VDomFuncType{}, vdom.VDomRefType{}, + wstore.MetaTSType{}, } // add extra type unions to generate here @@ -55,7 +56,7 @@ var TypeUnions = []tsgenmeta.TypeUnionMeta{ var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem() var errorRType = reflect.TypeOf((*error)(nil)).Elem() var anyRType = reflect.TypeOf((*interface{})(nil)).Elem() -var metaRType = reflect.TypeOf((*map[string]any)(nil)).Elem() +var metaRType = reflect.TypeOf((*wstore.MetaMapType)(nil)).Elem() var uiContextRType = reflect.TypeOf((*wstore.UIContext)(nil)).Elem() var waveObjRType = reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem() var updatesRtnRType = reflect.TypeOf(wstore.UpdatesRtnType{}) @@ -86,6 +87,9 @@ func getTSFieldName(field reflect.StructField) string { if namePart == "-" { return "" } + if strings.Contains(namePart, ":") { + return "\"" + namePart + "\"" + } return namePart } // if namePart is empty, still uses default @@ -155,8 +159,9 @@ func TypeToTSType(t reflect.Type, tsTypesMap map[reflect.Type]string) (string, [ } var tsRenameMap = map[string]string{ - "Window": "WaveWindow", - "Elem": "VDomElem", + "Window": "WaveWindow", + "Elem": "VDomElem", + "MetaTSType": "MetaType", } func generateTSTypeInternal(rtype reflect.Type, tsTypesMap map[reflect.Type]string) (string, []reflect.Type) { @@ -183,7 +188,7 @@ func generateTSTypeInternal(rtype reflect.Type, tsTypesMap map[reflect.Type]stri if fieldName == "" { continue } - if isWaveObj && (fieldName == waveobj.OTypeKeyName || fieldName == waveobj.OIDKeyName || fieldName == waveobj.VersionKeyName) { + if isWaveObj && (fieldName == waveobj.OTypeKeyName || fieldName == waveobj.OIDKeyName || fieldName == waveobj.VersionKeyName || fieldName == waveobj.MetaKeyName) { continue } optMarker := "" @@ -192,6 +197,9 @@ func generateTSTypeInternal(rtype reflect.Type, tsTypesMap map[reflect.Type]stri } tsTypeTag := field.Tag.Get("tstype") if tsTypeTag != "" { + if tsTypeTag == "-" { + continue + } buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsTypeTag)) continue } @@ -216,14 +224,11 @@ func GenerateWaveObjTSType() string { buf.WriteString(" otype: string;\n") buf.WriteString(" oid: string;\n") buf.WriteString(" version: number;\n") + buf.WriteString(" meta: MetaType;\n") buf.WriteString("};\n") return buf.String() } -func GenerateMetaType() string { - return "type MetaType = {[key: string]: any}\n" -} - func GenerateTSTypeUnion(unionMeta tsgenmeta.TypeUnionMeta, tsTypeMap map[reflect.Type]string) { rtn := generateTSTypeUnionInternal(unionMeta) tsTypeMap[unionMeta.BaseType] = rtn @@ -257,10 +262,6 @@ func GenerateTSType(rtype reflect.Type, tsTypesMap map[reflect.Type]string) { if rtype.Kind() == reflect.Chan { rtype = rtype.Elem() } - if rtype == metaRType { - tsTypesMap[metaRType] = GenerateMetaType() - return - } if rtype == contextRType || rtype == errorRType || rtype == anyRType { return } diff --git a/pkg/util/utilfn/utilfn.go b/pkg/util/utilfn/utilfn.go index b3067f51b..7f1aaa9e9 100644 --- a/pkg/util/utilfn/utilfn.go +++ b/pkg/util/utilfn/utilfn.go @@ -747,7 +747,7 @@ func ReUnmarshal(out any, in any) error { } // does a mapstructure using "json" tags -func DoMapStucture(out any, input any) error { +func DoMapStructure(out any, input any) error { dconfig := &mapstructure.DecoderConfig{ Result: out, TagName: "json", @@ -826,3 +826,14 @@ func StarMatchString(pattern string, s string, delimiter string) bool { // Check if both pattern and string are fully matched return pLen == sLen } + +func MergeStrMaps[T any](m1 map[string]T, m2 map[string]T) map[string]T { + rtn := make(map[string]T) + for key, val := range m1 { + rtn[key] = val + } + for key, val := range m2 { + rtn[key] = val + } + return rtn +} diff --git a/pkg/waveobj/metamap.go b/pkg/waveobj/metamap.go new file mode 100644 index 000000000..57a881ea5 --- /dev/null +++ b/pkg/waveobj/metamap.go @@ -0,0 +1,74 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package waveobj + +type MetaMapType map[string]any + +func (m MetaMapType) GetString(key string, def string) string { + if v, ok := m[key]; ok { + if s, ok := v.(string); ok { + return s + } + } + return def +} + +func (m MetaMapType) GetBool(key string, def bool) bool { + if v, ok := m[key]; ok { + if b, ok := v.(bool); ok { + return b + } + } + return def +} + +func (m MetaMapType) GetInt(key string, def int) int { + if v, ok := m[key]; ok { + if fval, ok := v.(float64); ok { + return int(fval) + } + } + return def +} + +func (m MetaMapType) GetFloat(key string, def float64) float64 { + if v, ok := m[key]; ok { + if fval, ok := v.(float64); ok { + return fval + } + } + return def +} + +func (m MetaMapType) GetMap(key string) MetaMapType { + if v, ok := m[key]; ok { + if mval, ok := v.(map[string]any); ok { + return MetaMapType(mval) + } + } + return nil +} + +func (m MetaMapType) GetArray(key string) []any { + if v, ok := m[key]; ok { + if aval, ok := v.([]any); ok { + return aval + } + } + return nil +} + +func (m MetaMapType) GetStringArray(key string) []string { + arr := m.GetArray(key) + if len(arr) == 0 { + return nil + } + rtn := make([]string, 0, len(arr)) + for _, v := range arr { + if s, ok := v.(string); ok { + rtn = append(rtn, s) + } + } + return rtn +} diff --git a/pkg/waveobj/waveobj.go b/pkg/waveobj/waveobj.go index 799c35710..16a8b8852 100644 --- a/pkg/waveobj/waveobj.go +++ b/pkg/waveobj/waveobj.go @@ -106,6 +106,7 @@ type waveObjDesc struct { var waveObjMap = sync.Map{} var waveObjRType = reflect.TypeOf((*WaveObj)(nil)).Elem() +var metaMapRType = reflect.TypeOf(MetaMapType{}) func RegisterType(rtype reflect.Type) { if rtype.Kind() != reflect.Ptr { @@ -143,10 +144,8 @@ func RegisterType(rtype reflect.Type) { if !found { panic(fmt.Sprintf("missing Meta field for %v", rtype)) } - if metaField.Type.Kind() != reflect.Map || - metaField.Type.Elem().Kind() != reflect.Interface || - metaField.Type.Key().Kind() != reflect.String { - panic(fmt.Sprintf("Meta field must be map[string]any for %v", rtype)) + if metaField.Type != metaMapRType { + panic(fmt.Sprintf("Meta field must be MetaMapType for %v", rtype)) } _, found = waveObjMap.Load(otype) if found { @@ -200,12 +199,16 @@ func SetVersion(waveObj WaveObj, version int) { reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.VersionField.Index).SetInt(int64(version)) } -func GetMeta(waveObj WaveObj) map[string]any { +func GetMeta(waveObj WaveObj) MetaMapType { desc := getWaveObjDesc(waveObj.GetOType()) if desc == nil { return nil } - return reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.MetaField.Index).Interface().(map[string]any) + mval := reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.MetaField.Index).Interface() + if mval == nil { + return nil + } + return mval.(MetaMapType) } func SetMeta(waveObj WaveObj, meta map[string]any) { diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 9c7d3fb17..06a90502d 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -8,6 +8,7 @@ import ( "path/filepath" "github.com/wavetermdev/thenextwave/pkg/wavebase" + "github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/wstore" ) @@ -98,6 +99,9 @@ type SettingsConfigType struct { AutoUpdate *AutoUpdateOpts `json:"autoupdate"` TermThemes TermThemesConfigType `json:"termthemes"` WindowSettings WindowSettingsType `json:"window"` + + DefaultMeta *waveobj.MetaMapType `json:"defaultmeta,omitempty"` + Presets map[string]*waveobj.MetaMapType `json:"presets,omitempty"` } var DefaultTermDarkTheme = TermThemeType{ @@ -181,30 +185,38 @@ func applyDefaultSettings(settings *SettingsConfigType) { Icon: "files", Label: "files", BlockDef: wstore.BlockDef{ - View: "preview", - Meta: map[string]any{"file": wavebase.GetHomeDir()}, + Meta: map[string]any{ + wstore.MetaKey_View: "preview", + wstore.MetaKey_File: wavebase.GetHomeDir(), + }, }, }, { Icon: "chart-simple", Label: "chart", BlockDef: wstore.BlockDef{ - View: "plot", + Meta: map[string]any{ + wstore.MetaKey_View: "plot", + }, }, }, { Icon: "globe", Label: "web", BlockDef: wstore.BlockDef{ - View: "web", - Meta: map[string]any{"url": "https://waveterm.dev/"}, + Meta: map[string]any{ + wstore.MetaKey_View: "web", + wstore.MetaKey_Url: "https://waveterm.dev/", + }, }, }, { Icon: "sparkles", Label: "waveai", BlockDef: wstore.BlockDef{ - View: "waveai", + Meta: map[string]any{ + wstore.MetaKey_View: "waveai", + }, }, }, } diff --git a/pkg/web/webcmd/webcmd.go b/pkg/web/webcmd/webcmd.go index 78980f616..7208591db 100644 --- a/pkg/web/webcmd/webcmd.go +++ b/pkg/web/webcmd/webcmd.go @@ -72,21 +72,21 @@ func ParseWSCommandMap(cmdMap map[string]any) (WSCommandType, error) { switch cmdType { case WSCommand_SetBlockTermSize: var cmd SetBlockTermSizeWSCommand - err := utilfn.DoMapStucture(&cmd, cmdMap) + err := utilfn.DoMapStructure(&cmd, cmdMap) if err != nil { return nil, fmt.Errorf("error decoding SetBlockTermSizeWSCommand: %w", err) } return &cmd, nil case WSCommand_BlockInput: var cmd BlockInputWSCommand - err := utilfn.DoMapStucture(&cmd, cmdMap) + err := utilfn.DoMapStructure(&cmd, cmdMap) if err != nil { return nil, fmt.Errorf("error decoding BlockInputWSCommand: %w", err) } return &cmd, nil case WSCommand_Rpc: var cmd WSRpcCommand - err := utilfn.DoMapStucture(&cmd, cmdMap) + err := utilfn.DoMapStructure(&cmd, cmdMap) if err != nil { return nil, fmt.Errorf("error decoding WSRpcCommand: %w", err) } diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index e9cc46f62..4d06eab15 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -96,8 +96,8 @@ func FileWriteCommand(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshr } // command "getmeta", wshserver.GetMetaCommand -func GetMetaCommand(w *wshutil.WshRpc, data wshrpc.CommandGetMetaData, opts *wshrpc.WshRpcCommandOpts) (map[string]interface {}, error) { - resp, err := sendRpcRequestCallHelper[map[string]interface {}](w, "getmeta", data, opts) +func GetMetaCommand(w *wshutil.WshRpc, data wshrpc.CommandGetMetaData, opts *wshrpc.WshRpcCommandOpts) (waveobj.MetaMapType, error) { + resp, err := sendRpcRequestCallHelper[waveobj.MetaMapType](w, "getmeta", data, opts) return resp, err } diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 627ee557a..d45da0775 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -45,8 +45,6 @@ const ( Command_StreamWaveAi = "streamwaveai" ) -type MetaDataType = map[string]any - type RespOrErrorUnion[T any] struct { Response T Error error @@ -55,7 +53,7 @@ type RespOrErrorUnion[T any] struct { type WshRpcInterface interface { AuthenticateCommand(ctx context.Context, data string) error MessageCommand(ctx context.Context, data CommandMessageData) error - GetMetaCommand(ctx context.Context, data CommandGetMetaData) (MetaDataType, error) + GetMetaCommand(ctx context.Context, data CommandGetMetaData) (wstore.MetaMapType, error) SetMetaCommand(ctx context.Context, data CommandSetMetaData) error SetViewCommand(ctx context.Context, data CommandBlockSetViewData) error ControllerInputCommand(ctx context.Context, data CommandBlockInputData) error @@ -130,8 +128,8 @@ type CommandGetMetaData struct { } type CommandSetMetaData struct { - ORef waveobj.ORef `json:"oref" wshcontext:"BlockORef"` - Meta MetaDataType `json:"meta"` + ORef waveobj.ORef `json:"oref" wshcontext:"BlockORef"` + Meta wstore.MetaMapType `json:"meta"` } type CommandResolveIdsData struct { diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 0b9f22124..44876a530 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -67,7 +67,7 @@ func (ws *WshServer) StreamWaveAiCommand(ctx context.Context, request wshrpc.Ope return waveai.RunLocalCompletionStream(ctx, request) } -func (ws *WshServer) GetMetaCommand(ctx context.Context, data wshrpc.CommandGetMetaData) (wshrpc.MetaDataType, error) { +func (ws *WshServer) GetMetaCommand(ctx context.Context, data wshrpc.CommandGetMetaData) (waveobj.MetaMapType, error) { log.Printf("calling meta: %s\n", data.ORef) obj, err := wstore.DBGetORef(ctx, data.ORef) if err != nil { @@ -82,31 +82,9 @@ func (ws *WshServer) GetMetaCommand(ctx context.Context, data wshrpc.CommandGetM func (ws *WshServer) SetMetaCommand(ctx context.Context, data wshrpc.CommandSetMetaData) error { log.Printf("SETMETA: %s | %v\n", data.ORef, data.Meta) oref := data.ORef - if oref.IsEmpty() { - return fmt.Errorf("no oref") - } - obj, err := wstore.DBGetORef(ctx, oref) + err := wstore.UpdateObjectMeta(ctx, oref, data.Meta) if err != nil { - return fmt.Errorf("error getting object: %w", err) - } - if obj == nil { - return nil - } - meta := waveobj.GetMeta(obj) - if meta == nil { - meta = make(map[string]any) - } - for k, v := range data.Meta { - if v == nil { - delete(meta, k) - continue - } - meta[k] = v - } - waveobj.SetMeta(obj, meta) - err = wstore.DBUpdate(ctx, obj) - if err != nil { - return fmt.Errorf("error updating block: %w", err) + return fmt.Errorf("error updating object meta: %w", err) } sendWaveObjUpdate(oref) return nil @@ -177,7 +155,8 @@ func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.Command if err != nil { return nil, fmt.Errorf("error creating block: %w", err) } - if blockData.Controller != "" { + controllerName := blockData.Meta.GetString(wstore.MetaKey_Controller, "") + if controllerName != "" { // TODO err = blockcontroller.StartBlockController(ctx, data.TabId, blockData.OID) if err != nil { @@ -211,7 +190,7 @@ func (ws *WshServer) SetViewCommand(ctx context.Context, data wshrpc.CommandBloc if err != nil { return fmt.Errorf("error getting block: %w", err) } - block.View = data.View + block.Meta[wstore.MetaKey_View] = data.View err = wstore.DBUpdate(ctx, block) if err != nil { return fmt.Errorf("error updating block: %w", err) diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 0efc14417..3988fa7bd 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -236,8 +236,6 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *BlockDef, rtOpts * blockData := &Block{ OID: blockId, BlockDef: blockDef, - Controller: blockDef.Controller, - View: blockDef.View, RuntimeOpts: rtOpts, Meta: blockDef.Meta, } @@ -299,36 +297,21 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string) error { }) } -func UpdateMeta(ctx context.Context, oref waveobj.ORef, meta map[string]any) error { +func UpdateObjectMeta(ctx context.Context, oref waveobj.ORef, meta MetaMapType) error { return WithTx(ctx, func(tx *TxWrap) error { - obj, _ := DBGetORef(tx.Context(), oref) - if obj == nil { - return fmt.Errorf("object not found: %q", oref) + if oref.IsEmpty() { + return fmt.Errorf("empty object reference") } - // obj.SetMeta(meta) - DBUpdate(tx.Context(), obj) - return nil - }) -} - -func UpdateObjectMeta(ctx context.Context, oref waveobj.ORef, meta map[string]any) error { - return WithTx(ctx, func(tx *TxWrap) error { obj, _ := DBGetORef(tx.Context(), oref) if obj == nil { - return fmt.Errorf("object not found: %q", oref) + return ErrNotFound } objMeta := waveobj.GetMeta(obj) if objMeta == nil { objMeta = make(map[string]any) } - for k, v := range meta { - if v == nil { - delete(objMeta, k) - continue - } - objMeta[k] = v - } - waveobj.SetMeta(obj, objMeta) + newMeta := MergeMeta(objMeta, meta) + waveobj.SetMeta(obj, newMeta) DBUpdate(tx.Context(), obj) return nil }) @@ -441,19 +424,6 @@ func EnsureInitialData() error { return fmt.Errorf("error creating client: %w", err) } } - if client.MainWindowId != "" { - // convert to windowIds - client.WindowIds = []string{client.MainWindowId} - client.MainWindowId = "" - err = DBUpdate(ctx, client) - if err != nil { - return fmt.Errorf("error updating client: %w", err) - } - client, err = DBGetSingleton[*Client](ctx) - if err != nil { - return fmt.Errorf("error getting client (after main window update): %w", err) - } - } if len(client.WindowIds) > 0 { return nil } diff --git a/pkg/wstore/wstore_meta.go b/pkg/wstore/wstore_meta.go new file mode 100644 index 000000000..258050f9f --- /dev/null +++ b/pkg/wstore/wstore_meta.go @@ -0,0 +1,153 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package wstore + +import ( + "strings" + + "github.com/wavetermdev/thenextwave/pkg/waveobj" +) + +const Entity_Any = "any" + +type MetaMapType = waveobj.MetaMapType + +// well known meta keys +// to add a new key, add it here and add it to MetaTSType (make sure the keys match) +// TODO: will code generate one side of this so we don't need to add the keys in two places +// will probably drive this off the meta decls so we can add more information and validate the keys/values +const ( + MetaKey_View = "view" + MetaKey_Controller = "controller" + MetaKey_Title = "title" + MetaKey_File = "file" + MetaKey_Url = "url" + MetaKey_Connection = "connection" + + MetaKey_Icon = "icon" + MetaKey_IconColor = "icon:color" + + MetaKey_Frame = "frame" + MetaKey_FrameBorderColor = "frame:bordercolor" + MetaKey_FrameBorderColor_Focused = "frame:bordercolor:focused" + + MetaKey_Cmd = "cmd" + MetaKey_CmdInteractive = "cmd:interactive" + MetaKey_CmdLogin = "cmd:login" + MetaKey_CmdRunOnStart = "cmd:runonstart" + MetaKey_CmdClearOnStart = "cmd:clearonstart" + MetaKey_CmdClearOnRestart = "cmd:clearonrestart" + MetaKey_CmdEnv = "cmd:env" + MetaKey_CmdCwd = "cmd:cwd" + MetaKey_CmdNoWsh = "cmd:nowsh" + + MetaKey_Bg = "bg" + MetaKey_BgOpacity = "bg:opacity" + MetaKey_BgBlendMode = "bg:blendmode" + + MetaKey_TermFontSize = "term:fontsize" + MetaKey_TermFontFamily = "term:fontfamily" + MetaKey_TermMode = "term:mode" + MetaKey_TermTheme = "term:theme" +) + +// for typescript typing +type MetaTSType struct { + // shared + View string `json:"view,omitempty"` + Controller string `json:"controller,omitempty"` + Title string `json:"title,omitempty"` + File string `json:"file,omitempty"` + Url string `json:"url,omitempty"` + Connection string `json:"connection,omitempty"` + + Icon string `json:"icon,omitempty"` + IconColor string `json:"icon:color,omitempty"` + + Frame bool `json:"frame,omitempty"` + FrameClear bool `json:"frame:*,omitempty"` + FrameBorderColor string `json:"frame:bordercolor,omitempty"` + FrameBorderColor_Focused string `json:"frame:bordercolor:focused,omitempty"` + + Cmd string `json:"cmd,omitempty"` + CmdClear bool `json:"cmd:*,omitempty"` + CmdInteractive bool `json:"cmd:interactive,omitempty"` + CmdLogin bool `json:"cmd:login,omitempty"` + CmdRunOnStart bool `json:"cmd:runonstart,omitempty"` + CmdClearOnStart bool `json:"cmd:clearonstart,omitempty"` + CmdClearOnRestart bool `json:"cmd:clearonrestart,omitempty"` + CmdEnv map[string]string `json:"cmd:env,omitempty"` + CmdCwd string `json:"cmd:cwd,omitempty"` + CmdNoWsh bool `json:"cmd:nowsh,omitempty"` + + // for tabs + Bg string `json:"bg,omitempty"` + BgClear bool `json:"bg:*,omitempty"` + BgOpacity float64 `json:"bg:opacity,omitempty"` + BgBlendMode string `json:"bg:blendmode,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"` +} + +type MetaDataDecl struct { + Key string `json:"key"` + Desc string `json:"desc,omitempty"` + Type string `json:"type"` // string, int, float, bool, array, object + Default any `json:"default,omitempty"` + StrOptions []string `json:"stroptions,omitempty"` + NumRange []*int `json:"numrange,omitempty"` // inclusive, null means no limit + Entity []string `json:"entity"` // what entities this applies to, e.g. "block", "tab", "any", etc. + Special []string `json:"special,omitempty"` // special handling. things that need to happen if this gets updated +} + +type MetaPresetDecl struct { + Preset string `json:"preset"` + Desc string `json:"desc,omitempty"` + Keys []string `json:"keys"` + Entity []string `json:"entity"` // what entities this applies to, e.g. "block", "tab", etc. +} + +// returns a clean copy of meta with mergeMeta merged in +func MergeMeta(meta MetaMapType, metaUpdate MetaMapType) MetaMapType { + rtn := make(MetaMapType) + for k, v := range meta { + rtn[k] = v + } + // deal with "section:*" keys + for k := range metaUpdate { + if !strings.HasSuffix(k, ":*") { + continue + } + if !metaUpdate.GetBool(k, false) { + continue + } + prefix := strings.TrimSuffix(k, ":*") + if prefix == "" { + continue + } + // delete "[prefix]" and all keys that start with "[prefix]:" + prefixColon := prefix + ":" + for k2 := range rtn { + if k2 == prefix || strings.HasPrefix(k2, prefixColon) { + delete(rtn, k2) + } + } + } + // now deal with regular keys + for k, v := range metaUpdate { + if strings.HasSuffix(k, ":*") { + continue + } + if v == nil { + delete(rtn, k) + continue + } + rtn[k] = v + } + return rtn +} diff --git a/pkg/wstore/wstore_types.go b/pkg/wstore/wstore_types.go index b9d59befb..99e1c7c11 100644 --- a/pkg/wstore/wstore_types.go +++ b/pkg/wstore/wstore_types.go @@ -12,54 +12,6 @@ import ( "github.com/wavetermdev/thenextwave/pkg/waveobj" ) -// well known meta keys -const ( - MetaKey_Title = "title" - MetaKey_File = "file" - MetaKey_Url = "url" - MetaKey_Icon = "icon" - MetaKey_IconColor = "icon:color" - MetaKey_Frame = "frame" - MetaKey_FrameBorderColor = "frame:bordercolor" - MetaKey_FrameBorderColor_Focused = "frame:bordercolor:focused" - MetaKey_Cmd = "cmd" - MetaKey_CmdInteractive = "cmd:interactive" - MetaKey_CmdLogin = "cmd:login" - MetaKey_CmdRunOnStart = "cmd:runonstart" - MetaKey_CmdClearOnStart = "cmd:clearonstart" - MetaKey_CmdClearOnRestart = "cmd:clearonrestart" - MetaKey_CmdEnv = "cmd:env" - MetaKey_CmdCwd = "cmd:cwd" -) - -type MetaType struct { - View string `json:"view,omitempty"` - Controller string `json:"controller,omitempty"` - Title string `json:"title,omitempty"` - File string `json:"file,omitempty"` - Url string `json:"url,omitempty"` - - Icon string `json:"icon,omitempty"` - IconColor string `json:"icon:color,omitempty"` - - Frame bool `json:"frame,omitempty"` - FrameBorderColor string `json:"frame:bordercolor,omitempty"` - FrameBorderColor_Focused string `json:"frame:bordercolor:focused,omitempty"` - - Cmd string `json:"cmd,omitempty"` - CmdInteractive bool `json:"cmd:interactive,omitempty"` - CmdLogin bool `json:"cmd:login,omitempty"` - CmdRunOnStart bool `json:"cmd:runonstart,omitempty"` - CmdClearOnStart bool `json:"cmd:clearonstart,omitempty"` - CmdClearOnRestart bool `json:"cmd:clearonrestart,omitempty"` - CmdEnv map[string]string `json:"cmd:env,omitempty"` - CmdCwd string `json:"cmd:cwd,omitempty"` - - Bg string `json:"bg,omitempty"` - BgOpacity float64 `json:"bg:opacity,omitempty"` - BgBlendMode string `json:"bg:blendmode,omitempty"` -} - type UIContext struct { WindowId string `json:"windowid"` ActiveTabId string `json:"activetabid"` @@ -161,12 +113,11 @@ func (update *WaveObjUpdate) UnmarshalJSON(data []byte) error { } type Client struct { - OID string `json:"oid"` - Version int `json:"version"` - MainWindowId string `json:"mainwindowid"` // deprecated - WindowIds []string `json:"windowids"` - Meta map[string]any `json:"meta"` - TosAgreed int64 `json:"tosagreed,omitempty"` + OID string `json:"oid"` + Version int `json:"version"` + WindowIds []string `json:"windowids"` + Meta MetaMapType `json:"meta"` + TosAgreed int64 `json:"tosagreed,omitempty"` } func (*Client) GetOType() string { @@ -185,7 +136,7 @@ type Window struct { Pos Point `json:"pos"` WinSize WinSize `json:"winsize"` LastFocusTs int64 `json:"lastfocusts"` - Meta map[string]any `json:"meta"` + Meta MetaMapType `json:"meta"` } func (*Window) GetOType() string { @@ -193,11 +144,11 @@ func (*Window) GetOType() string { } type Workspace struct { - OID string `json:"oid"` - Version int `json:"version"` - Name string `json:"name"` - TabIds []string `json:"tabids"` - Meta map[string]any `json:"meta"` + OID string `json:"oid"` + Version int `json:"version"` + Name string `json:"name"` + TabIds []string `json:"tabids"` + Meta MetaMapType `json:"meta"` } func (*Workspace) GetOType() string { @@ -205,12 +156,12 @@ func (*Workspace) GetOType() string { } type Tab struct { - OID string `json:"oid"` - Version int `json:"version"` - Name string `json:"name"` - LayoutNode string `json:"layoutnode"` - BlockIds []string `json:"blockids"` - Meta map[string]any `json:"meta"` + OID string `json:"oid"` + Version int `json:"version"` + Name string `json:"name"` + LayoutNode string `json:"layoutnode"` + BlockIds []string `json:"blockids"` + Meta MetaMapType `json:"meta"` } func (*Tab) GetOType() string { @@ -226,11 +177,11 @@ func (t *Tab) GetBlockORefs() []waveobj.ORef { } type LayoutNode struct { - OID string `json:"oid"` - Version int `json:"version"` - Node any `json:"node,omitempty"` - MagnifiedNodeId string `json:"magnifiednodeid,omitempty"` - Meta map[string]any `json:"meta,omitempty"` + OID string `json:"oid"` + Version int `json:"version"` + Node any `json:"node,omitempty"` + MagnifiedNodeId string `json:"magnifiednodeid,omitempty"` + Meta MetaMapType `json:"meta,omitempty"` } func (*LayoutNode) GetOType() string { @@ -246,10 +197,8 @@ type FileDef struct { } type BlockDef struct { - Controller string `json:"controller,omitempty"` - View string `json:"view,omitempty"` - Files map[string]*FileDef `json:"files,omitempty"` - Meta map[string]any `json:"meta,omitempty"` + Files map[string]*FileDef `json:"files,omitempty"` + Meta MetaMapType `json:"meta,omitempty"` } type StickerClickOptsType struct { @@ -289,11 +238,9 @@ type Block struct { OID string `json:"oid"` Version int `json:"version"` BlockDef *BlockDef `json:"blockdef"` - Controller string `json:"controller"` - View string `json:"view"` RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"` Stickers []*StickerType `json:"stickers,omitempty"` - Meta map[string]any `json:"meta"` + Meta MetaMapType `json:"meta"` } func (*Block) GetOType() string {