metadata updates (frontend typing) (#174)

This commit is contained in:
Mike Sawka 2024-07-30 12:33:28 -07:00 committed by GitHub
parent 9233b3dbd7
commit cfc875bc21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 482 additions and 300 deletions

View File

@ -47,9 +47,9 @@ func viewRun(cmd *cobra.Command, args []string) {
wshutil.SetTermRawModeAndInstallShutdownHandlers(true) wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
viewWshCmd := &wshrpc.CommandCreateBlockData{ viewWshCmd := &wshrpc.CommandCreateBlockData{
BlockDef: &wstore.BlockDef{ BlockDef: &wstore.BlockDef{
View: "preview",
Meta: map[string]interface{}{ Meta: map[string]interface{}{
"file": absFile, wstore.MetaKey_View: "preview",
wstore.MetaKey_File: absFile,
}, },
}, },
} }

View File

@ -313,11 +313,11 @@ const AppInner = () => {
function handleKeyDown(waveEvent: WaveKeyboardEvent): boolean { function handleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
// global key handler for now (refactor later) // global key handler for now (refactor later)
if (keyutil.checkKeyPressed(waveEvent, "Cmd:]")) { if (keyutil.checkKeyPressed(waveEvent, "Cmd:]") || keyutil.checkKeyPressed(waveEvent, "Shift:Cmd:]")) {
switchTab(1); switchTab(1);
return true; return true;
} }
if (keyutil.checkKeyPressed(waveEvent, "Cmd:[")) { if (keyutil.checkKeyPressed(waveEvent, "Cmd:[") || keyutil.checkKeyPressed(waveEvent, "Shift:Cmd:[")) {
switchTab(-1); switchTab(-1);
return true; return true;
} }

View File

@ -153,8 +153,8 @@ function getBlockHeaderText(blockIcon: string, blockData: Block, settings: Setti
return [blockIconElem, blockData.meta.title + blockIdStr]; return [blockIconElem, blockData.meta.title + blockIdStr];
} }
} }
let viewString = blockData?.view; let viewString = blockData?.meta?.view;
if (blockData.controller == "cmd") { if (blockData?.meta?.controller == "cmd") {
viewString = "cmd"; viewString = "cmd";
} }
return [blockIconElem, viewString + blockIdStr]; return [blockIconElem, viewString + blockIdStr];
@ -259,8 +259,8 @@ const BlockFrame_Default_Component = ({
}); });
}); });
let isFocused = jotai.useAtomValue(isFocusedAtom); let isFocused = jotai.useAtomValue(isFocusedAtom);
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.view); const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
const viewName = util.useAtomValueSafe(viewModel.viewName) ?? blockViewToName(blockData?.view); const viewName = util.useAtomValueSafe(viewModel.viewName) ?? blockViewToName(blockData?.meta?.view);
const headerTextUnion = util.useAtomValueSafe(viewModel.viewText); const headerTextUnion = util.useAtomValueSafe(viewModel.viewText);
const preIconButton = util.useAtomValueSafe(viewModel.preIconButton); const preIconButton = util.useAtomValueSafe(viewModel.preIconButton);
const endIconButtons = util.useAtomValueSafe(viewModel.endIconButtons); const endIconButtons = util.useAtomValueSafe(viewModel.endIconButtons);
@ -377,6 +377,10 @@ const BlockFrame_Default_Component = ({
headerTextElems.push(...renderHeaderElements(headerTextUnion)); headerTextElems.push(...renderHeaderElements(headerTextUnion));
} }
function handleDoubleClick() {
layoutModel?.onMagnifyToggle();
}
return ( return (
<div <div
className={clsx( className={clsx(
@ -397,6 +401,7 @@ const BlockFrame_Default_Component = ({
<div <div
className="block-frame-default-header" className="block-frame-default-header"
ref={layoutModel?.dragHandleRef} ref={layoutModel?.dragHandleRef}
onDoubleClick={handleDoubleClick}
onContextMenu={(e) => onContextMenu={(e) =>
handleHeaderContextMenu( handleHeaderContextMenu(
e, e,
@ -456,6 +461,9 @@ function blockViewToIcon(view: string): string {
} }
function blockViewToName(view: string): string { function blockViewToName(view: string): string {
if (util.isBlank(view)) {
return "(No View)";
}
if (view == "term") { if (view == "term") {
return "Terminal"; return "Terminal";
} }
@ -476,12 +484,12 @@ function getViewElemAndModel(
blockView: string, blockView: string,
blockRef: React.RefObject<HTMLDivElement> blockRef: React.RefObject<HTMLDivElement>
): { viewModel: ViewModel; viewElem: JSX.Element } { ): { viewModel: ViewModel; viewElem: JSX.Element } {
if (blockView == null) {
return { viewElem: null, viewModel: null };
}
let viewElem: JSX.Element = null; let viewElem: JSX.Element = null;
let viewModel: ViewModel = null; let viewModel: ViewModel = null;
if (blockView === "term") { if (util.isBlank(blockView)) {
viewElem = <CenteredDiv>No View</CenteredDiv>;
viewModel = makeDefaultViewModel(blockId);
} else if (blockView === "term") {
const termViewModel = makeTerminalModel(blockId); const termViewModel = makeTerminalModel(blockId);
viewElem = <TerminalView key={blockId} blockId={blockId} model={termViewModel} />; viewElem = <TerminalView key={blockId} blockId={blockId} model={termViewModel} />;
viewModel = termViewModel; viewModel = termViewModel;
@ -501,6 +509,7 @@ function getViewElemAndModel(
viewModel = waveAiModel; viewModel = waveAiModel;
} }
if (viewModel == null) { if (viewModel == null) {
viewElem = <CenteredDiv>Invalid View "{blockView}"</CenteredDiv>;
viewModel = makeDefaultViewModel(blockId); viewModel = makeDefaultViewModel(blockId);
} }
return { viewElem, viewModel }; return { viewElem, viewModel };
@ -511,11 +520,11 @@ function makeDefaultViewModel(blockId: string): ViewModel {
let viewModel: ViewModel = { let viewModel: ViewModel = {
viewIcon: jotai.atom((get) => { viewIcon: jotai.atom((get) => {
const blockData = get(blockDataAtom); const blockData = get(blockDataAtom);
return blockViewToIcon(blockData?.view); return blockViewToIcon(blockData?.meta?.view);
}), }),
viewName: jotai.atom((get) => { viewName: jotai.atom((get) => {
const blockData = get(blockDataAtom); const blockData = get(blockDataAtom);
return blockViewToName(blockData?.view); return blockViewToName(blockData?.meta?.view);
}), }),
viewText: jotai.atom((get) => { viewText: jotai.atom((get) => {
const blockData = get(blockDataAtom); const blockData = get(blockDataAtom);
@ -532,7 +541,7 @@ const BlockPreview = React.memo(({ blockId, layoutModel }: BlockProps) => {
if (!blockData) { if (!blockData) {
return null; return null;
} }
let { viewModel } = getViewElemAndModel(blockId, blockData?.view, null); let { viewModel } = getViewElemAndModel(blockId, blockData?.meta?.view, null);
return ( return (
<BlockFrame <BlockFrame
key={blockId} key={blockId}
@ -588,8 +597,8 @@ const BlockFull = React.memo(({ blockId, layoutModel }: BlockProps) => {
}, []); }, []);
let { viewElem, viewModel } = React.useMemo( let { viewElem, viewModel } = React.useMemo(
() => getViewElemAndModel(blockId, blockData?.view, blockRef), () => getViewElemAndModel(blockId, blockData?.meta?.view, blockRef),
[blockId, blockData?.view, blockRef] [blockId, blockData?.meta?.view, blockRef]
); );
const determineFocusedChild = React.useCallback( const determineFocusedChild = React.useCallback(

View File

@ -284,10 +284,10 @@ export class PreviewModel implements ViewModel {
label: "Open Terminal in New Block", label: "Open Terminal in New Block",
click: async () => { click: async () => {
const termBlockDef: BlockDef = { const termBlockDef: BlockDef = {
controller: "shell",
view: "term",
meta: { meta: {
cwd: globalStore.get(this.fileName), view: "term",
controller: "shell",
"cmd:cwd": globalStore.get(this.fileName),
}, },
}; };
await createBlock(termBlockDef); await createBlock(termBlockDef);
@ -579,4 +579,4 @@ function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel
); );
} }
export { PreviewView, makePreviewModel }; export { makePreviewModel, PreviewView };

View File

@ -143,7 +143,7 @@ class TermViewModel {
}); });
this.viewName = jotai.atom((get) => { this.viewName = jotai.atom((get) => {
const blockData = get(this.blockAtom); const blockData = get(this.blockAtom);
if (blockData.controller == "cmd") { if (blockData?.meta?.controller == "cmd") {
return "Command"; return "Command";
} }
return "Terminal"; return "Terminal";
@ -219,7 +219,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
} }
} }
const settings = globalStore.get(atoms.settingsConfigAtom); 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( const termWrap = new TermWrap(
blockId, blockId,
connectElemRef.current, connectElemRef.current,

View File

@ -99,7 +99,7 @@ function TermSticker({ sticker, config }: { sticker: StickerType; config: Sticke
console.log("clickHandler", sticker.clickcmd, sticker.clickblockdef); console.log("clickHandler", sticker.clickcmd, sticker.clickblockdef);
if (sticker.clickcmd) { if (sticker.clickcmd) {
const b64data = btoa(sticker.clickcmd); const b64data = btoa(sticker.clickcmd);
WshServer.BlockInputCommand({ blockid: config.blockId, inputdata64: b64data }); WshServer.ControllerInputCommand({ blockid: config.blockId, inputdata64: b64data });
} }
if (sticker.clickblockdef) { if (sticker.clickblockdef) {
createBlock(sticker.clickblockdef); createBlock(sticker.clickblockdef);
@ -183,7 +183,7 @@ export function TermStickers({ config }: { config: StickerTermConfig }) {
imgsrc: "~/Downloads/natureicon.png", imgsrc: "~/Downloads/natureicon.png",
opacity: 0.8, opacity: 0.8,
pointerevents: true, pointerevents: true,
clickblockdef: { view: "preview", meta: { file: "~/" } }, clickblockdef: { meta: { file: "~/", view: "preview" } },
}); });
stickers.push({ stickers.push({
position: "absolute", position: "absolute",

View File

@ -16,7 +16,7 @@ const TermThemeUpdater = ({ blockId, termRef }: TermThemeProps) => {
const { termthemes } = useAtomValue(atoms.settingsConfigAtom); const { termthemes } = useAtomValue(atoms.settingsConfigAtom);
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId)); const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
let defaultThemeName = "default-dark"; 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 defaultTheme: TermThemeType = termthemes?.[defaultThemeName] || ({} as any);
const theme: TermThemeType = termthemes?.[themeName] || ({} as any); const theme: TermThemeType = termthemes?.[themeName] || ({} as any);

View File

@ -24,11 +24,6 @@
.filler { .filler {
flex: 1 1 auto; flex: 1 1 auto;
} }
> * {
cursor: default;
user-select: none;
}
} }
.chat-msg { .chat-msg {

View File

@ -20,16 +20,18 @@ const Widgets = React.memo(() => {
const newWidgetModalVisible = React.useState(false); const newWidgetModalVisible = React.useState(false);
async function clickTerminal() { async function clickTerminal() {
const termBlockDef: BlockDef = { const termBlockDef: BlockDef = {
controller: "shell", meta: {
view: "term", controller: "shell",
view: "term",
},
}; };
createBlock(termBlockDef); createBlock(termBlockDef);
} }
async function clickHome() { async function clickHome() {
const editDef: BlockDef = { const editDef: BlockDef = {
view: "preview",
meta: { meta: {
view: "preview",
file: "~", file: "~",
}, },
}; };
@ -37,8 +39,8 @@ const Widgets = React.memo(() => {
} }
async function clickWeb() { async function clickWeb() {
const editDef: BlockDef = { const editDef: BlockDef = {
view: "web",
meta: { meta: {
view: "web",
url: "https://waveterm.dev/", url: "https://waveterm.dev/",
}, },
}; };

View File

@ -24,11 +24,8 @@ declare global {
// wstore.Block // wstore.Block
type Block = WaveObj & { type Block = WaveObj & {
blockdef: BlockDef; blockdef: BlockDef;
controller: string;
view: string;
runtimeopts?: RuntimeOpts; runtimeopts?: RuntimeOpts;
stickers?: StickerType[]; stickers?: StickerType[];
meta: MetaType;
}; };
// blockcontroller.BlockControllerRuntimeStatus // blockcontroller.BlockControllerRuntimeStatus
@ -40,8 +37,6 @@ declare global {
// wstore.BlockDef // wstore.BlockDef
type BlockDef = { type BlockDef = {
controller?: string;
view?: string;
files?: {[key: string]: FileDef}; files?: {[key: string]: FileDef};
meta?: MetaType; meta?: MetaType;
}; };
@ -60,9 +55,7 @@ declare global {
// wstore.Client // wstore.Client
type Client = WaveObj & { type Client = WaveObj & {
mainwindowid: string;
windowids: string[]; windowids: string[];
meta: MetaType;
tosagreed?: number; tosagreed?: number;
}; };
@ -70,7 +63,7 @@ declare global {
type CommandAppendIJsonData = { type CommandAppendIJsonData = {
zoneid: string; zoneid: string;
filename: string; filename: string;
data: MetaType; data: {[key: string]: any};
}; };
// wshrpc.CommandBlockInputData // wshrpc.CommandBlockInputData
@ -144,7 +137,7 @@ declare global {
path?: string; path?: string;
url?: string; url?: string;
content?: string; content?: string;
meta?: MetaType; meta?: {[key: string]: any};
}; };
// fileservice.FileInfo // fileservice.FileInfo
@ -178,10 +171,42 @@ declare global {
type LayoutNode = WaveObj & { type LayoutNode = WaveObj & {
node?: any; node?: any;
magnifiednodeid?: string; 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 // tsgenmeta.MethodMeta
type MethodMeta = { type MethodMeta = {
@ -284,6 +309,8 @@ declare global {
autoupdate: AutoUpdateOpts; autoupdate: AutoUpdateOpts;
termthemes: {[key: string]: TermThemeType}; termthemes: {[key: string]: TermThemeType};
window: WindowSettingsType; window: WindowSettingsType;
defaultmeta?: MetaType;
presets?: {[key: string]: MetaType};
}; };
// wstore.StickerClickOptsType // wstore.StickerClickOptsType
@ -302,7 +329,7 @@ declare global {
// wstore.StickerType // wstore.StickerType
type StickerType = { type StickerType = {
stickertype: string; stickertype: string;
style: MetaType; style: {[key: string]: any};
clickopts?: StickerClickOptsType; clickopts?: StickerClickOptsType;
display: StickerDisplayOptsType; display: StickerDisplayOptsType;
}; };
@ -319,7 +346,6 @@ declare global {
name: string; name: string;
layoutnode: string; layoutnode: string;
blockids: string[]; blockids: string[];
meta: MetaType;
}; };
// shellexec.TermSize // shellexec.TermSize
@ -392,7 +418,7 @@ declare global {
type VDomElem = { type VDomElem = {
id?: string; id?: string;
tag: string; tag: string;
props?: MetaType; props?: {[key: string]: any};
children?: VDomElem[]; children?: VDomElem[];
text?: string; text?: string;
}; };
@ -465,7 +491,7 @@ declare global {
createdts: number; createdts: number;
size: number; size: number;
modts: number; modts: number;
meta: MetaType; meta: {[key: string]: any};
}; };
// waveobj.WaveObj // waveobj.WaveObj
@ -473,6 +499,7 @@ declare global {
otype: string; otype: string;
oid: string; oid: string;
version: number; version: number;
meta: MetaType;
}; };
// wstore.WaveObjUpdate // wstore.WaveObjUpdate
@ -492,7 +519,6 @@ declare global {
pos: Point; pos: Point;
winsize: WinSize; winsize: WinSize;
lastfocusts: number; lastfocusts: number;
meta: MetaType;
}; };
// service.WebCallType // service.WebCallType
@ -538,7 +564,6 @@ declare global {
type Workspace = WaveObj & { type Workspace = WaveObj & {
name: string; name: string;
tabids: string[]; tabids: string[];
meta: MetaType;
}; };
// wshrpc.WshRpcCommandOpts // wshrpc.WshRpcCommandOpts

View File

@ -186,7 +186,7 @@ func (bc *BlockController) resetTerminalState() {
var shouldTruncate bool var shouldTruncate bool
blockData, getBlockDataErr := wstore.DBMustGet[*wstore.Block](ctx, bc.BlockId) blockData, getBlockDataErr := wstore.DBMustGet[*wstore.Block](ctx, bc.BlockId)
if getBlockDataErr == nil { if getBlockDataErr == nil {
shouldTruncate = getBoolFromMeta(blockData.Meta, wstore.MetaKey_CmdClearOnRestart, false) shouldTruncate = blockData.Meta.GetBool(wstore.MetaKey_CmdClearOnRestart, false)
} }
if shouldTruncate { if shouldTruncate {
err := HandleTruncateBlockFile(bc.BlockId, BlockFile_Main) 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 // every byte is 4-bits of randomness
func randomHexString(numHexDigits int) (string, error) { func randomHexString(numHexDigits int) (string, error) {
numBytes := (numHexDigits + 1) / 2 // Calculate the number of bytes needed 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 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 // create a circular blockfile for the output
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn() defer cancelFn()
@ -276,7 +248,7 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[str
return shellProcErr return shellProcErr
} }
var remoteDomainSocketName string var remoteDomainSocketName string
remoteName := getMetaStr(blockMeta, "connection", "") remoteName := blockMeta.GetString(wstore.MetaKey_Connection, "")
isRemote := remoteName != "" isRemote := remoteName != ""
if isRemote { if isRemote {
randStr, err := randomHexString(16) // 64-bits of randomness randStr, err := randomHexString(16) // 64-bits of randomness
@ -289,7 +261,7 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[str
cmdOpts := shellexec.CommandOptsType{ cmdOpts := shellexec.CommandOptsType{
Env: make(map[string]string), Env: make(map[string]string),
} }
if !getMetaBool(blockMeta, "nowsh", false) { if !blockMeta.GetBool(wstore.MetaKey_CmdNoWsh, false) {
if isRemote { if isRemote {
jwtStr, err := wshutil.MakeClientJWTToken(wshrpc.RpcContext{TabId: bc.TabId, BlockId: bc.BlockId}, remoteDomainSocketName) jwtStr, err := wshutil.MakeClientJWTToken(wshrpc.RpcContext{TabId: bc.TabId, BlockId: bc.BlockId}, remoteDomainSocketName)
if err != nil { if err != nil {
@ -307,44 +279,31 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[str
if bc.ControllerType == BlockController_Shell { if bc.ControllerType == BlockController_Shell {
cmdOpts.Interactive = true cmdOpts.Interactive = true
cmdOpts.Login = true cmdOpts.Login = true
cmdOpts.Cwd, _ = blockMeta["cmd:cwd"].(string) cmdOpts.Cwd = blockMeta.GetString(wstore.MetaKey_CmdCwd, "")
if cmdOpts.Cwd != "" { if cmdOpts.Cwd != "" {
cmdOpts.Cwd = wavebase.ExpandHomeDir(cmdOpts.Cwd) cmdOpts.Cwd = wavebase.ExpandHomeDir(cmdOpts.Cwd)
} }
} else if bc.ControllerType == BlockController_Cmd { } else if bc.ControllerType == BlockController_Cmd {
if _, ok := blockMeta["cmd"].(string); ok { cmdStr = blockMeta.GetString(wstore.MetaKey_Cmd, "")
cmdStr = blockMeta["cmd"].(string) if cmdStr == "" {
} else {
return fmt.Errorf("missing cmd in block meta") return fmt.Errorf("missing cmd in block meta")
} }
if _, ok := blockMeta["cmd:cwd"].(string); ok { cmdOpts.Cwd = blockMeta.GetString(wstore.MetaKey_CmdCwd, "")
cmdOpts.Cwd = blockMeta["cmd:cwd"].(string) if cmdOpts.Cwd != "" {
if cmdOpts.Cwd != "" { cmdOpts.Cwd = wavebase.ExpandHomeDir(cmdOpts.Cwd)
cmdOpts.Cwd = wavebase.ExpandHomeDir(cmdOpts.Cwd)
}
} }
if _, ok := blockMeta["cmd:interactive"]; ok { cmdOpts.Interactive = blockMeta.GetBool(wstore.MetaKey_CmdInteractive, false)
if blockMeta["cmd:interactive"].(bool) { cmdOpts.Login = blockMeta.GetBool(wstore.MetaKey_CmdLogin, false)
cmdOpts.Interactive = true cmdEnv := blockMeta.GetMap(wstore.MetaKey_CmdEnv)
for k, v := range cmdEnv {
if v == nil {
continue
} }
} if _, ok := v.(string); ok {
if _, ok := blockMeta["cmd:login"]; ok { cmdOpts.Env[k] = v.(string)
if blockMeta["cmd:login"].(bool) {
cmdOpts.Login = true
} }
} if _, ok := v.(float64); ok {
if _, ok := blockMeta["cmd:env"].(map[string]any); ok { cmdOpts.Env[k] = fmt.Sprintf("%v", v)
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)
}
} }
} }
} else { } else {
@ -477,8 +436,9 @@ func (bc *BlockController) run(bdata *wstore.Block, blockMeta map[string]any) {
bc.Status = Status_Running bc.Status = Status_Running
return true return true
}) })
if bdata.Controller != BlockController_Shell && bdata.Controller != BlockController_Cmd { controllerName := bdata.Meta.GetString(wstore.MetaKey_Controller, "")
log.Printf("unknown controller %q\n", bdata.Controller) if controllerName != BlockController_Shell && controllerName != BlockController_Cmd {
log.Printf("unknown controller %q\n", controllerName)
return return
} }
if getBoolFromMeta(blockMeta, wstore.MetaKey_CmdClearOnStart, false) { if getBoolFromMeta(blockMeta, wstore.MetaKey_CmdClearOnStart, false) {
@ -527,12 +487,13 @@ func StartBlockController(ctx context.Context, tabId string, blockId string) err
if err != nil { if err != nil {
return fmt.Errorf("error getting block: %w", err) return fmt.Errorf("error getting block: %w", err)
} }
if blockData.Controller == "" { controllerName := blockData.Meta.GetString(wstore.MetaKey_Controller, "")
if controllerName == "" {
// nothing to start // nothing to start
return nil return nil
} }
if blockData.Controller != BlockController_Shell && blockData.Controller != BlockController_Cmd { if controllerName != BlockController_Shell && controllerName != BlockController_Cmd {
return fmt.Errorf("unknown controller %q", blockData.Controller) return fmt.Errorf("unknown controller %q", controllerName)
} }
globalLock.Lock() globalLock.Lock()
defer globalLock.Unlock() defer globalLock.Unlock()
@ -542,7 +503,7 @@ func StartBlockController(ctx context.Context, tabId string, blockId string) err
} }
bc := &BlockController{ bc := &BlockController{
Lock: &sync.Mutex{}, Lock: &sync.Mutex{},
ControllerType: blockData.Controller, ControllerType: controllerName,
TabId: tabId, TabId: tabId,
BlockId: blockId, BlockId: blockId,
Status: Status_Init, Status: Status_Init,

View File

@ -10,6 +10,7 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"strconv" "strconv"
"strings"
) )
// ijson values are built out of standard go building blocks: // 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 { type PathError struct {
Err string Err string
} }

View File

@ -66,8 +66,9 @@ func (bs *BlockService) SaveWaveAiData(ctx context.Context, blockId string, hist
if err != nil { if err != nil {
return err return err
} }
if block.View != "waveai" { viewName := block.Meta.GetString(wstore.MetaKey_View, "")
return fmt.Errorf("invalid view type: %s", block.View) if viewName != "waveai" {
return fmt.Errorf("invalid view type: %s", viewName)
} }
historyBytes, err := json.Marshal(history) historyBytes, err := json.Marshal(history)
if err != nil { if err != nil {

View File

@ -189,7 +189,8 @@ func (svc *ObjectService) CreateBlock(uiContext wstore.UIContext, blockDef *wsto
if err != nil { if err != nil {
return "", nil, fmt.Errorf("error creating block: %w", err) 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) err = blockcontroller.StartBlockController(ctx, uiContext.ActiveTabId, blockData.OID)
if err != nil { if err != nil {
return "", nil, fmt.Errorf("error starting block controller: %w", err) 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) ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn() defer cancelFn()
ctx = wstore.ContextWithUpdates(ctx) ctx = wstore.ContextWithUpdates(ctx)

View File

@ -89,7 +89,7 @@ func convertNumber(argType reflect.Type, jsonArg float64) (any, error) {
func convertComplex(argType reflect.Type, jsonArg any) (any, error) { func convertComplex(argType reflect.Type, jsonArg any) (any, error) {
nativeArgVal := reflect.New(argType) nativeArgVal := reflect.New(argType)
err := utilfn.DoMapStucture(nativeArgVal.Interface(), jsonArg) err := utilfn.DoMapStructure(nativeArgVal.Interface(), jsonArg)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -45,6 +45,7 @@ var ExtraTypes = []any{
vdom.Elem{}, vdom.Elem{},
vdom.VDomFuncType{}, vdom.VDomFuncType{},
vdom.VDomRefType{}, vdom.VDomRefType{},
wstore.MetaTSType{},
} }
// add extra type unions to generate here // add extra type unions to generate here
@ -55,7 +56,7 @@ var TypeUnions = []tsgenmeta.TypeUnionMeta{
var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem() var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem()
var errorRType = reflect.TypeOf((*error)(nil)).Elem() var errorRType = reflect.TypeOf((*error)(nil)).Elem()
var anyRType = reflect.TypeOf((*interface{})(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 uiContextRType = reflect.TypeOf((*wstore.UIContext)(nil)).Elem()
var waveObjRType = reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem() var waveObjRType = reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem()
var updatesRtnRType = reflect.TypeOf(wstore.UpdatesRtnType{}) var updatesRtnRType = reflect.TypeOf(wstore.UpdatesRtnType{})
@ -86,6 +87,9 @@ func getTSFieldName(field reflect.StructField) string {
if namePart == "-" { if namePart == "-" {
return "" return ""
} }
if strings.Contains(namePart, ":") {
return "\"" + namePart + "\""
}
return namePart return namePart
} }
// if namePart is empty, still uses default // 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{ var tsRenameMap = map[string]string{
"Window": "WaveWindow", "Window": "WaveWindow",
"Elem": "VDomElem", "Elem": "VDomElem",
"MetaTSType": "MetaType",
} }
func generateTSTypeInternal(rtype reflect.Type, tsTypesMap map[reflect.Type]string) (string, []reflect.Type) { 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 == "" { if fieldName == "" {
continue 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 continue
} }
optMarker := "" optMarker := ""
@ -192,6 +197,9 @@ func generateTSTypeInternal(rtype reflect.Type, tsTypesMap map[reflect.Type]stri
} }
tsTypeTag := field.Tag.Get("tstype") tsTypeTag := field.Tag.Get("tstype")
if tsTypeTag != "" { if tsTypeTag != "" {
if tsTypeTag == "-" {
continue
}
buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsTypeTag)) buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsTypeTag))
continue continue
} }
@ -216,14 +224,11 @@ func GenerateWaveObjTSType() string {
buf.WriteString(" otype: string;\n") buf.WriteString(" otype: string;\n")
buf.WriteString(" oid: string;\n") buf.WriteString(" oid: string;\n")
buf.WriteString(" version: number;\n") buf.WriteString(" version: number;\n")
buf.WriteString(" meta: MetaType;\n")
buf.WriteString("};\n") buf.WriteString("};\n")
return buf.String() return buf.String()
} }
func GenerateMetaType() string {
return "type MetaType = {[key: string]: any}\n"
}
func GenerateTSTypeUnion(unionMeta tsgenmeta.TypeUnionMeta, tsTypeMap map[reflect.Type]string) { func GenerateTSTypeUnion(unionMeta tsgenmeta.TypeUnionMeta, tsTypeMap map[reflect.Type]string) {
rtn := generateTSTypeUnionInternal(unionMeta) rtn := generateTSTypeUnionInternal(unionMeta)
tsTypeMap[unionMeta.BaseType] = rtn tsTypeMap[unionMeta.BaseType] = rtn
@ -257,10 +262,6 @@ func GenerateTSType(rtype reflect.Type, tsTypesMap map[reflect.Type]string) {
if rtype.Kind() == reflect.Chan { if rtype.Kind() == reflect.Chan {
rtype = rtype.Elem() rtype = rtype.Elem()
} }
if rtype == metaRType {
tsTypesMap[metaRType] = GenerateMetaType()
return
}
if rtype == contextRType || rtype == errorRType || rtype == anyRType { if rtype == contextRType || rtype == errorRType || rtype == anyRType {
return return
} }

View File

@ -747,7 +747,7 @@ func ReUnmarshal(out any, in any) error {
} }
// does a mapstructure using "json" tags // does a mapstructure using "json" tags
func DoMapStucture(out any, input any) error { func DoMapStructure(out any, input any) error {
dconfig := &mapstructure.DecoderConfig{ dconfig := &mapstructure.DecoderConfig{
Result: out, Result: out,
TagName: "json", TagName: "json",
@ -826,3 +826,14 @@ func StarMatchString(pattern string, s string, delimiter string) bool {
// Check if both pattern and string are fully matched // Check if both pattern and string are fully matched
return pLen == sLen 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
}

74
pkg/waveobj/metamap.go Normal file
View File

@ -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
}

View File

@ -106,6 +106,7 @@ type waveObjDesc struct {
var waveObjMap = sync.Map{} var waveObjMap = sync.Map{}
var waveObjRType = reflect.TypeOf((*WaveObj)(nil)).Elem() var waveObjRType = reflect.TypeOf((*WaveObj)(nil)).Elem()
var metaMapRType = reflect.TypeOf(MetaMapType{})
func RegisterType(rtype reflect.Type) { func RegisterType(rtype reflect.Type) {
if rtype.Kind() != reflect.Ptr { if rtype.Kind() != reflect.Ptr {
@ -143,10 +144,8 @@ func RegisterType(rtype reflect.Type) {
if !found { if !found {
panic(fmt.Sprintf("missing Meta field for %v", rtype)) panic(fmt.Sprintf("missing Meta field for %v", rtype))
} }
if metaField.Type.Kind() != reflect.Map || if metaField.Type != metaMapRType {
metaField.Type.Elem().Kind() != reflect.Interface || panic(fmt.Sprintf("Meta field must be MetaMapType for %v", rtype))
metaField.Type.Key().Kind() != reflect.String {
panic(fmt.Sprintf("Meta field must be map[string]any for %v", rtype))
} }
_, found = waveObjMap.Load(otype) _, found = waveObjMap.Load(otype)
if found { if found {
@ -200,12 +199,16 @@ func SetVersion(waveObj WaveObj, version int) {
reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.VersionField.Index).SetInt(int64(version)) 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()) desc := getWaveObjDesc(waveObj.GetOType())
if desc == nil { if desc == nil {
return 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) { func SetMeta(waveObj WaveObj, meta map[string]any) {

View File

@ -8,6 +8,7 @@ import (
"path/filepath" "path/filepath"
"github.com/wavetermdev/thenextwave/pkg/wavebase" "github.com/wavetermdev/thenextwave/pkg/wavebase"
"github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wstore" "github.com/wavetermdev/thenextwave/pkg/wstore"
) )
@ -98,6 +99,9 @@ type SettingsConfigType struct {
AutoUpdate *AutoUpdateOpts `json:"autoupdate"` AutoUpdate *AutoUpdateOpts `json:"autoupdate"`
TermThemes TermThemesConfigType `json:"termthemes"` TermThemes TermThemesConfigType `json:"termthemes"`
WindowSettings WindowSettingsType `json:"window"` WindowSettings WindowSettingsType `json:"window"`
DefaultMeta *waveobj.MetaMapType `json:"defaultmeta,omitempty"`
Presets map[string]*waveobj.MetaMapType `json:"presets,omitempty"`
} }
var DefaultTermDarkTheme = TermThemeType{ var DefaultTermDarkTheme = TermThemeType{
@ -181,30 +185,38 @@ func applyDefaultSettings(settings *SettingsConfigType) {
Icon: "files", Icon: "files",
Label: "files", Label: "files",
BlockDef: wstore.BlockDef{ BlockDef: wstore.BlockDef{
View: "preview", Meta: map[string]any{
Meta: map[string]any{"file": wavebase.GetHomeDir()}, wstore.MetaKey_View: "preview",
wstore.MetaKey_File: wavebase.GetHomeDir(),
},
}, },
}, },
{ {
Icon: "chart-simple", Icon: "chart-simple",
Label: "chart", Label: "chart",
BlockDef: wstore.BlockDef{ BlockDef: wstore.BlockDef{
View: "plot", Meta: map[string]any{
wstore.MetaKey_View: "plot",
},
}, },
}, },
{ {
Icon: "globe", Icon: "globe",
Label: "web", Label: "web",
BlockDef: wstore.BlockDef{ BlockDef: wstore.BlockDef{
View: "web", Meta: map[string]any{
Meta: map[string]any{"url": "https://waveterm.dev/"}, wstore.MetaKey_View: "web",
wstore.MetaKey_Url: "https://waveterm.dev/",
},
}, },
}, },
{ {
Icon: "sparkles", Icon: "sparkles",
Label: "waveai", Label: "waveai",
BlockDef: wstore.BlockDef{ BlockDef: wstore.BlockDef{
View: "waveai", Meta: map[string]any{
wstore.MetaKey_View: "waveai",
},
}, },
}, },
} }

View File

@ -72,21 +72,21 @@ func ParseWSCommandMap(cmdMap map[string]any) (WSCommandType, error) {
switch cmdType { switch cmdType {
case WSCommand_SetBlockTermSize: case WSCommand_SetBlockTermSize:
var cmd SetBlockTermSizeWSCommand var cmd SetBlockTermSizeWSCommand
err := utilfn.DoMapStucture(&cmd, cmdMap) err := utilfn.DoMapStructure(&cmd, cmdMap)
if err != nil { if err != nil {
return nil, fmt.Errorf("error decoding SetBlockTermSizeWSCommand: %w", err) return nil, fmt.Errorf("error decoding SetBlockTermSizeWSCommand: %w", err)
} }
return &cmd, nil return &cmd, nil
case WSCommand_BlockInput: case WSCommand_BlockInput:
var cmd BlockInputWSCommand var cmd BlockInputWSCommand
err := utilfn.DoMapStucture(&cmd, cmdMap) err := utilfn.DoMapStructure(&cmd, cmdMap)
if err != nil { if err != nil {
return nil, fmt.Errorf("error decoding BlockInputWSCommand: %w", err) return nil, fmt.Errorf("error decoding BlockInputWSCommand: %w", err)
} }
return &cmd, nil return &cmd, nil
case WSCommand_Rpc: case WSCommand_Rpc:
var cmd WSRpcCommand var cmd WSRpcCommand
err := utilfn.DoMapStucture(&cmd, cmdMap) err := utilfn.DoMapStructure(&cmd, cmdMap)
if err != nil { if err != nil {
return nil, fmt.Errorf("error decoding WSRpcCommand: %w", err) return nil, fmt.Errorf("error decoding WSRpcCommand: %w", err)
} }

View File

@ -96,8 +96,8 @@ func FileWriteCommand(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshr
} }
// command "getmeta", wshserver.GetMetaCommand // command "getmeta", wshserver.GetMetaCommand
func GetMetaCommand(w *wshutil.WshRpc, data wshrpc.CommandGetMetaData, opts *wshrpc.WshRpcCommandOpts) (map[string]interface {}, error) { func GetMetaCommand(w *wshutil.WshRpc, data wshrpc.CommandGetMetaData, opts *wshrpc.WshRpcCommandOpts) (waveobj.MetaMapType, error) {
resp, err := sendRpcRequestCallHelper[map[string]interface {}](w, "getmeta", data, opts) resp, err := sendRpcRequestCallHelper[waveobj.MetaMapType](w, "getmeta", data, opts)
return resp, err return resp, err
} }

View File

@ -45,8 +45,6 @@ const (
Command_StreamWaveAi = "streamwaveai" Command_StreamWaveAi = "streamwaveai"
) )
type MetaDataType = map[string]any
type RespOrErrorUnion[T any] struct { type RespOrErrorUnion[T any] struct {
Response T Response T
Error error Error error
@ -55,7 +53,7 @@ type RespOrErrorUnion[T any] struct {
type WshRpcInterface interface { type WshRpcInterface interface {
AuthenticateCommand(ctx context.Context, data string) error AuthenticateCommand(ctx context.Context, data string) error
MessageCommand(ctx context.Context, data CommandMessageData) 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 SetMetaCommand(ctx context.Context, data CommandSetMetaData) error
SetViewCommand(ctx context.Context, data CommandBlockSetViewData) error SetViewCommand(ctx context.Context, data CommandBlockSetViewData) error
ControllerInputCommand(ctx context.Context, data CommandBlockInputData) error ControllerInputCommand(ctx context.Context, data CommandBlockInputData) error
@ -130,8 +128,8 @@ type CommandGetMetaData struct {
} }
type CommandSetMetaData struct { type CommandSetMetaData struct {
ORef waveobj.ORef `json:"oref" wshcontext:"BlockORef"` ORef waveobj.ORef `json:"oref" wshcontext:"BlockORef"`
Meta MetaDataType `json:"meta"` Meta wstore.MetaMapType `json:"meta"`
} }
type CommandResolveIdsData struct { type CommandResolveIdsData struct {

View File

@ -67,7 +67,7 @@ func (ws *WshServer) StreamWaveAiCommand(ctx context.Context, request wshrpc.Ope
return waveai.RunLocalCompletionStream(ctx, request) 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) log.Printf("calling meta: %s\n", data.ORef)
obj, err := wstore.DBGetORef(ctx, data.ORef) obj, err := wstore.DBGetORef(ctx, data.ORef)
if err != nil { 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 { func (ws *WshServer) SetMetaCommand(ctx context.Context, data wshrpc.CommandSetMetaData) error {
log.Printf("SETMETA: %s | %v\n", data.ORef, data.Meta) log.Printf("SETMETA: %s | %v\n", data.ORef, data.Meta)
oref := data.ORef oref := data.ORef
if oref.IsEmpty() { err := wstore.UpdateObjectMeta(ctx, oref, data.Meta)
return fmt.Errorf("no oref")
}
obj, err := wstore.DBGetORef(ctx, oref)
if err != nil { if err != nil {
return fmt.Errorf("error getting object: %w", err) return fmt.Errorf("error updating object meta: %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)
} }
sendWaveObjUpdate(oref) sendWaveObjUpdate(oref)
return nil return nil
@ -177,7 +155,8 @@ func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.Command
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating block: %w", err) return nil, fmt.Errorf("error creating block: %w", err)
} }
if blockData.Controller != "" { controllerName := blockData.Meta.GetString(wstore.MetaKey_Controller, "")
if controllerName != "" {
// TODO // TODO
err = blockcontroller.StartBlockController(ctx, data.TabId, blockData.OID) err = blockcontroller.StartBlockController(ctx, data.TabId, blockData.OID)
if err != nil { if err != nil {
@ -211,7 +190,7 @@ func (ws *WshServer) SetViewCommand(ctx context.Context, data wshrpc.CommandBloc
if err != nil { if err != nil {
return fmt.Errorf("error getting block: %w", err) return fmt.Errorf("error getting block: %w", err)
} }
block.View = data.View block.Meta[wstore.MetaKey_View] = data.View
err = wstore.DBUpdate(ctx, block) err = wstore.DBUpdate(ctx, block)
if err != nil { if err != nil {
return fmt.Errorf("error updating block: %w", err) return fmt.Errorf("error updating block: %w", err)

View File

@ -236,8 +236,6 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *BlockDef, rtOpts *
blockData := &Block{ blockData := &Block{
OID: blockId, OID: blockId,
BlockDef: blockDef, BlockDef: blockDef,
Controller: blockDef.Controller,
View: blockDef.View,
RuntimeOpts: rtOpts, RuntimeOpts: rtOpts,
Meta: blockDef.Meta, 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 { return WithTx(ctx, func(tx *TxWrap) error {
obj, _ := DBGetORef(tx.Context(), oref) if oref.IsEmpty() {
if obj == nil { return fmt.Errorf("empty object reference")
return fmt.Errorf("object not found: %q", oref)
} }
// 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) obj, _ := DBGetORef(tx.Context(), oref)
if obj == nil { if obj == nil {
return fmt.Errorf("object not found: %q", oref) return ErrNotFound
} }
objMeta := waveobj.GetMeta(obj) objMeta := waveobj.GetMeta(obj)
if objMeta == nil { if objMeta == nil {
objMeta = make(map[string]any) objMeta = make(map[string]any)
} }
for k, v := range meta { newMeta := MergeMeta(objMeta, meta)
if v == nil { waveobj.SetMeta(obj, newMeta)
delete(objMeta, k)
continue
}
objMeta[k] = v
}
waveobj.SetMeta(obj, objMeta)
DBUpdate(tx.Context(), obj) DBUpdate(tx.Context(), obj)
return nil return nil
}) })
@ -441,19 +424,6 @@ func EnsureInitialData() error {
return fmt.Errorf("error creating client: %w", err) 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 { if len(client.WindowIds) > 0 {
return nil return nil
} }

153
pkg/wstore/wstore_meta.go Normal file
View File

@ -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
}

View File

@ -12,54 +12,6 @@ import (
"github.com/wavetermdev/thenextwave/pkg/waveobj" "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 { type UIContext struct {
WindowId string `json:"windowid"` WindowId string `json:"windowid"`
ActiveTabId string `json:"activetabid"` ActiveTabId string `json:"activetabid"`
@ -161,12 +113,11 @@ func (update *WaveObjUpdate) UnmarshalJSON(data []byte) error {
} }
type Client struct { type Client struct {
OID string `json:"oid"` OID string `json:"oid"`
Version int `json:"version"` Version int `json:"version"`
MainWindowId string `json:"mainwindowid"` // deprecated WindowIds []string `json:"windowids"`
WindowIds []string `json:"windowids"` Meta MetaMapType `json:"meta"`
Meta map[string]any `json:"meta"` TosAgreed int64 `json:"tosagreed,omitempty"`
TosAgreed int64 `json:"tosagreed,omitempty"`
} }
func (*Client) GetOType() string { func (*Client) GetOType() string {
@ -185,7 +136,7 @@ type Window struct {
Pos Point `json:"pos"` Pos Point `json:"pos"`
WinSize WinSize `json:"winsize"` WinSize WinSize `json:"winsize"`
LastFocusTs int64 `json:"lastfocusts"` LastFocusTs int64 `json:"lastfocusts"`
Meta map[string]any `json:"meta"` Meta MetaMapType `json:"meta"`
} }
func (*Window) GetOType() string { func (*Window) GetOType() string {
@ -193,11 +144,11 @@ func (*Window) GetOType() string {
} }
type Workspace struct { type Workspace struct {
OID string `json:"oid"` OID string `json:"oid"`
Version int `json:"version"` Version int `json:"version"`
Name string `json:"name"` Name string `json:"name"`
TabIds []string `json:"tabids"` TabIds []string `json:"tabids"`
Meta map[string]any `json:"meta"` Meta MetaMapType `json:"meta"`
} }
func (*Workspace) GetOType() string { func (*Workspace) GetOType() string {
@ -205,12 +156,12 @@ func (*Workspace) GetOType() string {
} }
type Tab struct { type Tab struct {
OID string `json:"oid"` OID string `json:"oid"`
Version int `json:"version"` Version int `json:"version"`
Name string `json:"name"` Name string `json:"name"`
LayoutNode string `json:"layoutnode"` LayoutNode string `json:"layoutnode"`
BlockIds []string `json:"blockids"` BlockIds []string `json:"blockids"`
Meta map[string]any `json:"meta"` Meta MetaMapType `json:"meta"`
} }
func (*Tab) GetOType() string { func (*Tab) GetOType() string {
@ -226,11 +177,11 @@ func (t *Tab) GetBlockORefs() []waveobj.ORef {
} }
type LayoutNode struct { type LayoutNode struct {
OID string `json:"oid"` OID string `json:"oid"`
Version int `json:"version"` Version int `json:"version"`
Node any `json:"node,omitempty"` Node any `json:"node,omitempty"`
MagnifiedNodeId string `json:"magnifiednodeid,omitempty"` MagnifiedNodeId string `json:"magnifiednodeid,omitempty"`
Meta map[string]any `json:"meta,omitempty"` Meta MetaMapType `json:"meta,omitempty"`
} }
func (*LayoutNode) GetOType() string { func (*LayoutNode) GetOType() string {
@ -246,10 +197,8 @@ type FileDef struct {
} }
type BlockDef struct { type BlockDef struct {
Controller string `json:"controller,omitempty"` Files map[string]*FileDef `json:"files,omitempty"`
View string `json:"view,omitempty"` Meta MetaMapType `json:"meta,omitempty"`
Files map[string]*FileDef `json:"files,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
} }
type StickerClickOptsType struct { type StickerClickOptsType struct {
@ -289,11 +238,9 @@ type Block struct {
OID string `json:"oid"` OID string `json:"oid"`
Version int `json:"version"` Version int `json:"version"`
BlockDef *BlockDef `json:"blockdef"` BlockDef *BlockDef `json:"blockdef"`
Controller string `json:"controller"`
View string `json:"view"`
RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"` RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"`
Stickers []*StickerType `json:"stickers,omitempty"` Stickers []*StickerType `json:"stickers,omitempty"`
Meta map[string]any `json:"meta"` Meta MetaMapType `json:"meta"`
} }
func (*Block) GetOType() string { func (*Block) GetOType() string {