From 8630e2323948d1fc253603b9277cd97c0173d44d Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Tue, 27 Aug 2024 18:49:49 -0700 Subject: [PATCH] new config system (#283) --- Taskfile.yml | 10 +- cmd/generatego/main-generatego.go | 77 +++ .../main-generatets.go} | 0 .../main-generatewshclient.go | 86 --- emain/emain.ts | 23 +- emain/platform.ts | 4 + emain/preload.ts | 1 + frontend/app/app.tsx | 15 +- frontend/app/block/blockframe.tsx | 9 +- frontend/app/block/blockutil.tsx | 25 - frontend/app/store/global.ts | 57 +- frontend/app/store/services.ts | 11 +- frontend/app/tab/tab.tsx | 16 +- .../app/view/preview/directorypreview.tsx | 12 +- frontend/app/view/term/term.tsx | 20 +- frontend/app/view/term/termtheme.ts | 3 +- frontend/app/view/term/termutil.ts | 6 +- frontend/app/view/waveai/waveai.tsx | 19 +- frontend/app/workspace/workspace.tsx | 36 +- frontend/types/custom.d.ts | 6 +- frontend/types/gotypes.d.ts | 122 ++-- frontend/util/util.ts | 14 + frontend/wave.ts | 6 +- pkg/gogen/gogen.go | 107 ++++ pkg/service/fileservice/fileservice.go | 14 +- pkg/telemetry/telemetry.go | 7 +- pkg/tsgen/tsgen.go | 3 +- pkg/waveobj/metaconsts.go | 59 ++ pkg/waveobj/wtypemeta.go | 53 +- pkg/wconfig/defaultconfig/defaultconfig.go | 9 + pkg/wconfig/defaultconfig/defaultwidgets.json | 55 ++ pkg/wconfig/defaultconfig/mimetypes.json | 57 ++ pkg/wconfig/defaultconfig/presets.json | 32 + pkg/wconfig/defaultconfig/settings.json | 8 + pkg/wconfig/defaultconfig/termthemes.json | 74 +++ pkg/wconfig/filewatcher.go | 208 +------ pkg/wconfig/metaconsts.go | 46 ++ pkg/wconfig/settingsconfig.go | 577 +++++++++--------- pkg/wshrpc/wshclient/wshclient.go | 108 ++-- pkg/wstore/wstore.go | 2 +- 40 files changed, 1102 insertions(+), 895 deletions(-) create mode 100644 cmd/generatego/main-generatego.go rename cmd/{generate/main-generate.go => generatets/main-generatets.go} (100%) delete mode 100644 cmd/generatewshclient/main-generatewshclient.go create mode 100644 pkg/gogen/gogen.go create mode 100644 pkg/waveobj/metaconsts.go create mode 100644 pkg/wconfig/defaultconfig/defaultconfig.go create mode 100644 pkg/wconfig/defaultconfig/defaultwidgets.json create mode 100644 pkg/wconfig/defaultconfig/mimetypes.json create mode 100644 pkg/wconfig/defaultconfig/presets.json create mode 100644 pkg/wconfig/defaultconfig/settings.json create mode 100644 pkg/wconfig/defaultconfig/termthemes.json create mode 100644 pkg/wconfig/metaconsts.go diff --git a/Taskfile.yml b/Taskfile.yml index 2f5a9d014..e7cf9843f 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -164,17 +164,19 @@ tasks: generate: desc: Generate Typescript bindings for the Go backend. cmds: - - go run cmd/generate/main-generate.go - - go run cmd/generatewshclient/main-generatewshclient.go + - go run cmd/generatets/main-generatets.go + - go run cmd/generatego/main-generatego.go sources: - - "cmd/generate/*.go" - - "cmd/generatewshclient/*.go" + - "cmd/generatego/*.go" + - "cmd/generatets/*.go" - "pkg/service/**/*.go" - "pkg/waveobj/wtype.go" - "pkg/wconfig/**/*.go" - "pkg/wstore/*.go" - "pkg/wshrpc/**/*.go" - "pkg/tsgen/**/*.go" + - "pkg/gogen/**/*.go" + - "pkg/wconfig/**/*.go" - "pkg/eventbus/eventbus.go" generates: - frontend/types/gotypes.d.ts diff --git a/cmd/generatego/main-generatego.go b/cmd/generatego/main-generatego.go new file mode 100644 index 000000000..a91e0e72d --- /dev/null +++ b/cmd/generatego/main-generatego.go @@ -0,0 +1,77 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "os" + "reflect" + "strings" + + "github.com/wavetermdev/thenextwave/pkg/gogen" + "github.com/wavetermdev/thenextwave/pkg/util/utilfn" + "github.com/wavetermdev/thenextwave/pkg/waveobj" + "github.com/wavetermdev/thenextwave/pkg/wconfig" + "github.com/wavetermdev/thenextwave/pkg/wshrpc" +) + +const WshClientFileName = "pkg/wshrpc/wshclient/wshclient.go" +const WaveObjMetaConstsFileName = "pkg/waveobj/metaconsts.go" +const SettingsMetaConstsFileName = "pkg/wconfig/metaconsts.go" + +func GenerateWshClient() { + fmt.Fprintf(os.Stderr, "generating wshclient file to %s\n", WshClientFileName) + var buf strings.Builder + gogen.GenerateBoilerplate(&buf, "wshclient", []string{ + "github.com/wavetermdev/thenextwave/pkg/wshutil", + "github.com/wavetermdev/thenextwave/pkg/wshrpc", + "github.com/wavetermdev/thenextwave/pkg/waveobj", + }) + wshDeclMap := wshrpc.GenerateWshCommandDeclMap() + for _, key := range utilfn.GetOrderedMapKeys(wshDeclMap) { + methodDecl := wshDeclMap[key] + if methodDecl.CommandType == wshrpc.RpcType_ResponseStream { + gogen.GenMethod_ResponseStream(&buf, methodDecl) + } else if methodDecl.CommandType == wshrpc.RpcType_Call { + gogen.GenMethod_Call(&buf, methodDecl) + } else { + panic("unsupported command type " + methodDecl.CommandType) + } + } + buf.WriteString("\n") + err := os.WriteFile(WshClientFileName, []byte(buf.String()), 0644) + if err != nil { + panic(err) + } +} + +func GenerateWaveObjMetaConsts() { + fmt.Fprintf(os.Stderr, "generating waveobj meta consts file to %s\n", WaveObjMetaConstsFileName) + var buf strings.Builder + gogen.GenerateBoilerplate(&buf, "waveobj", []string{}) + gogen.GenerateMetaMapConsts(&buf, "MetaKey_", reflect.TypeOf(waveobj.MetaTSType{})) + buf.WriteString("\n") + err := os.WriteFile(WaveObjMetaConstsFileName, []byte(buf.String()), 0644) + if err != nil { + panic(err) + } +} + +func GenerateSettingsMetaConsts() { + fmt.Fprintf(os.Stderr, "generating settings meta consts file to %s\n", SettingsMetaConstsFileName) + var buf strings.Builder + gogen.GenerateBoilerplate(&buf, "wconfig", []string{}) + gogen.GenerateMetaMapConsts(&buf, "ConfigKey_", reflect.TypeOf(wconfig.SettingsType{})) + buf.WriteString("\n") + err := os.WriteFile(SettingsMetaConstsFileName, []byte(buf.String()), 0644) + if err != nil { + panic(err) + } +} + +func main() { + GenerateWshClient() + GenerateWaveObjMetaConsts() + GenerateSettingsMetaConsts() +} diff --git a/cmd/generate/main-generate.go b/cmd/generatets/main-generatets.go similarity index 100% rename from cmd/generate/main-generate.go rename to cmd/generatets/main-generatets.go diff --git a/cmd/generatewshclient/main-generatewshclient.go b/cmd/generatewshclient/main-generatewshclient.go deleted file mode 100644 index 358ef90c2..000000000 --- a/cmd/generatewshclient/main-generatewshclient.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "fmt" - "os" - - "github.com/wavetermdev/thenextwave/pkg/util/utilfn" - "github.com/wavetermdev/thenextwave/pkg/wshrpc" -) - -func genMethod_ResponseStream(fd *os.File, methodDecl *wshrpc.WshRpcMethodDecl) { - fmt.Fprintf(fd, "// command %q, wshserver.%s\n", methodDecl.Command, methodDecl.MethodName) - var dataType string - dataVarName := "nil" - if methodDecl.CommandDataType != nil { - dataType = ", data " + methodDecl.CommandDataType.String() - dataVarName = "data" - } - respType := "any" - if methodDecl.DefaultResponseDataType != nil { - respType = methodDecl.DefaultResponseDataType.String() - } - fmt.Fprintf(fd, "func %s(w *wshutil.WshRpc%s, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[%s] {\n", methodDecl.MethodName, dataType, respType) - fmt.Fprintf(fd, " return sendRpcRequestResponseStreamHelper[%s](w, %q, %s, opts)\n", respType, methodDecl.Command, dataVarName) - fmt.Fprintf(fd, "}\n\n") -} - -func genMethod_Call(fd *os.File, methodDecl *wshrpc.WshRpcMethodDecl) { - fmt.Fprintf(fd, "// command %q, wshserver.%s\n", methodDecl.Command, methodDecl.MethodName) - var dataType string - dataVarName := "nil" - if methodDecl.CommandDataType != nil { - dataType = ", data " + methodDecl.CommandDataType.String() - dataVarName = "data" - } - returnType := "error" - respName := "_" - tParamVal := "any" - if methodDecl.DefaultResponseDataType != nil { - returnType = "(" + methodDecl.DefaultResponseDataType.String() + ", error)" - respName = "resp" - tParamVal = methodDecl.DefaultResponseDataType.String() - } - fmt.Fprintf(fd, "func %s(w *wshutil.WshRpc%s, opts *wshrpc.RpcOpts) %s {\n", methodDecl.MethodName, dataType, returnType) - fmt.Fprintf(fd, " %s, err := sendRpcRequestCallHelper[%s](w, %q, %s, opts)\n", respName, tParamVal, methodDecl.Command, dataVarName) - if methodDecl.DefaultResponseDataType != nil { - fmt.Fprintf(fd, " return resp, err\n") - } else { - fmt.Fprintf(fd, " return err\n") - } - fmt.Fprintf(fd, "}\n\n") -} - -func main() { - fd, err := os.Create("pkg/wshrpc/wshclient/wshclient.go") - if err != nil { - panic(err) - } - defer fd.Close() - fmt.Fprintf(os.Stderr, "generating wshclient file to %s\n", fd.Name()) - fmt.Fprintf(fd, "// Copyright 2024, Command Line Inc.\n") - fmt.Fprintf(fd, "// SPDX-License-Identifier: Apache-2.0\n\n") - fmt.Fprintf(fd, "// generated by cmd/generatewshclient/main-generatewshclient.go\n\n") - fmt.Fprintf(fd, "package wshclient\n\n") - fmt.Fprintf(fd, "import (\n") - fmt.Fprintf(fd, " \"github.com/wavetermdev/thenextwave/pkg/wshutil\"\n") - fmt.Fprintf(fd, " \"github.com/wavetermdev/thenextwave/pkg/wshrpc\"\n") - fmt.Fprintf(fd, " \"github.com/wavetermdev/thenextwave/pkg/waveobj\"\n") - fmt.Fprintf(fd, ")\n\n") - - wshDeclMap := wshrpc.GenerateWshCommandDeclMap() - for _, key := range utilfn.GetOrderedMapKeys(wshDeclMap) { - methodDecl := wshDeclMap[key] - if methodDecl.CommandType == wshrpc.RpcType_ResponseStream { - genMethod_ResponseStream(fd, methodDecl) - } else if methodDecl.CommandType == wshrpc.RpcType_Call { - genMethod_Call(fd, methodDecl) - } else { - panic("unsupported command type " + methodDecl.CommandType) - } - } - fmt.Fprintf(fd, "\n") -} diff --git a/emain/emain.ts b/emain/emain.ts index 5b08c1265..7766666b9 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -191,8 +191,8 @@ async function handleWSEvent(evtMsg: WSEventType) { return; } const clientData = await services.ClientService.GetClientData(); - const settings = await services.FileService.GetSettingsConfig(); - const newWin = createBrowserWindow(clientData.oid, windowData, settings); + const fullConfig = await services.FileService.GetFullConfig(); + const newWin = createBrowserWindow(clientData.oid, windowData, fullConfig); await newWin.readyPromise; newWin.show(); } else if (evtMsg.eventtype == "electron:closewindow") { @@ -268,11 +268,7 @@ function shFrameNavHandler(event: Electron.Event { const clientData = await services.ClientService.GetClientData(); const newWindow = await services.ClientService.MakeWindow(); - const settings = await services.FileService.GetSettingsConfig(); - const newBrowserWindow = createBrowserWindow(clientData.oid, newWindow, settings); + const fullConfig = await services.FileService.GetFullConfig(); + const newBrowserWindow = createBrowserWindow(clientData.oid, newWindow, fullConfig); newBrowserWindow.show(); } @@ -700,7 +697,7 @@ async function relaunchBrowserWindows(): Promise { globalIsRelaunching = false; const clientData = await services.ClientService.GetClientData(); - const settings = await services.FileService.GetSettingsConfig(); + const fullConfig = await services.FileService.GetFullConfig(); const wins: WaveBrowserWindow[] = []; for (const windowId of clientData.windowids.slice().reverse()) { const windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow; @@ -710,7 +707,7 @@ async function relaunchBrowserWindows(): Promise { }); continue; } - const win = createBrowserWindow(clientData.oid, windowData, settings); + const win = createBrowserWindow(clientData.oid, windowData, fullConfig); wins.push(win); } for (const win of wins) { diff --git a/emain/platform.ts b/emain/platform.ts index d9a2e8893..cd70004a4 100644 --- a/emain/platform.ts +++ b/emain/platform.ts @@ -27,6 +27,10 @@ ipcMain.on("get-is-dev", (event) => { ipcMain.on("get-platform", (event, url) => { event.returnValue = unamePlatform; }); +ipcMain.on("get-user-name", (event) => { + const userInfo = os.userInfo(); + event.returnValue = userInfo.username; +}); // must match golang function getWaveHomeDir() { diff --git a/emain/preload.ts b/emain/preload.ts index 19d33848a..e86d52a2e 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -8,6 +8,7 @@ contextBridge.exposeInMainWorld("api", { getIsDev: () => ipcRenderer.sendSync("get-is-dev"), getPlatform: () => ipcRenderer.sendSync("get-platform"), getCursorPoint: () => ipcRenderer.sendSync("get-cursor-point"), + getUserName: () => ipcRenderer.sendSync("get-user-name"), openNewWindow: () => ipcRenderer.send("open-new-window"), showContextMenu: (menu, position) => ipcRenderer.send("contextmenu-show", menu, position), onContextMenuClick: (callback) => ipcRenderer.on("contextmenu-click", (_event, id) => callback(id)), diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index 8925d5185..846e862c9 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -5,7 +5,7 @@ import { appHandleKeyDown, appHandleKeyUp } from "@/app/appkey"; import { useWaveObjectValue } from "@/app/store/wos"; import { Workspace } from "@/app/workspace/workspace"; import { ContextMenuModel } from "@/store/contextmenu"; -import { PLATFORM, WOS, atoms, getApi, globalStore } from "@/store/global"; +import { PLATFORM, WOS, atoms, getApi, globalStore, useSettingsPrefixAtom } from "@/store/global"; import { getWebServerEndpoint } from "@/util/endpoints"; import * as keyutil from "@/util/keyutil"; import * as util from "@/util/util"; @@ -80,12 +80,13 @@ function handleContextMenu(e: React.MouseEvent) { } function AppSettingsUpdater() { - const settings = jotai.useAtomValue(atoms.settingsConfigAtom); + const windowSettings = useSettingsPrefixAtom("window"); React.useEffect(() => { - const isTransparentOrBlur = (settings?.window?.transparent || settings?.window?.blur) ?? false; - const opacity = util.boundNumber(settings?.window?.opacity ?? 0.8, 0, 1); - let baseBgColor = settings?.window?.bgcolor; - console.log("window settings", settings.window); + const isTransparentOrBlur = + (windowSettings?.["window:transparent"] || windowSettings?.["window:blur"]) ?? false; + const opacity = util.boundNumber(windowSettings?.["window:opacity"] ?? 0.8, 0, 1); + let baseBgColor = windowSettings?.["window:bgcolor"]; + console.log("window settings", windowSettings); if (isTransparentOrBlur) { document.body.classList.add("is-transparent"); const rootStyles = getComputedStyle(document.documentElement); @@ -99,7 +100,7 @@ function AppSettingsUpdater() { document.body.classList.remove("is-transparent"); document.body.style.opacity = null; } - }, [settings?.window]); + }, [windowSettings]); return null; } diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 8c1c87adc..9d2b81000 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -12,7 +12,7 @@ import { import { Button } from "@/app/element/button"; import { TypeAheadModal } from "@/app/modals/typeaheadmodal"; import { ContextMenuModel } from "@/app/store/contextmenu"; -import { atoms, globalStore, useBlockAtom, WOS } from "@/app/store/global"; +import { atoms, globalStore, useBlockAtom, useSettingsKeyAtom, WOS } from "@/app/store/global"; import * as services from "@/app/store/services"; import { WshServer } from "@/app/store/wshserver"; import { MagnifyIcon } from "@/element/magnify"; @@ -132,7 +132,8 @@ const BlockFrame_Header = ({ }: BlockFrameProps & { changeConnModalAtom: jotai.PrimitiveAtom }) => { const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", nodeModel.blockId)); const viewName = util.useAtomValueSafe(viewModel.viewName) ?? blockViewToName(blockData?.meta?.view); - const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom); + const showBlockIds = jotai.useAtomValue(useSettingsKeyAtom("blockheader:showblockids")); + const settingsConfig = jotai.useAtomValue(atoms.settingsAtom); const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view); const preIconButton = util.useAtomValueSafe(viewModel.preIconButton); const headerTextUnion = util.useAtomValueSafe(viewModel.viewText); @@ -190,9 +191,7 @@ const BlockFrame_Header = ({
{viewIconElem}
{viewName}
- {settingsConfig?.blockheader?.showblockids && ( -
[{nodeModel.blockId.substring(0, 8)}]
- )} + {showBlockIds &&
[{nodeModel.blockId.substring(0, 8)}]
}
{headerTextElems}
diff --git a/frontend/app/block/blockutil.tsx b/frontend/app/block/blockutil.tsx index 8c0257167..182cd880a 100644 --- a/frontend/app/block/blockutil.tsx +++ b/frontend/app/block/blockutil.tsx @@ -135,31 +135,6 @@ export function getBlockHeaderIcon(blockIcon: string, blockData: Block): React.R return blockIconElem; } -export function getBlockHeaderText(blockIcon: string, blockData: Block, settings: SettingsConfigType): React.ReactNode { - if (!blockData) { - return "no block data"; - } - let blockIdStr = ""; - if (settings?.blockheader?.showblockids) { - blockIdStr = ` [${blockData.oid.substring(0, 8)}]`; - } - let blockIconElem = getBlockHeaderIcon(blockIcon, blockData); - if (!util.isBlank(blockData?.meta?.title)) { - try { - const rtn = processTitleString(blockData.meta.title) ?? []; - return [blockIconElem, ...rtn, blockIdStr == "" ? null : blockIdStr]; - } catch (e) { - console.error("error processing title", blockData.meta.title, e); - return [blockIconElem, blockData.meta.title + blockIdStr]; - } - } - let viewString = blockData?.meta?.view; - if (blockData?.meta?.controller == "cmd") { - viewString = "cmd"; - } - return [blockIconElem, viewString + blockIdStr]; -} - export const IconButton = React.memo(({ decl, className }: { decl: HeaderIconButton; className?: string }) => { const buttonRef = React.useRef(null); useLongClick(buttonRef, decl.click, decl.longClick); diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 5a0faa579..24bbdd578 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -96,7 +96,10 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { } return WOS.getObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get); }); - const settingsConfigAtom = jotai.atom(null) as jotai.PrimitiveAtom; + const fullConfigAtom = jotai.atom(null) as jotai.PrimitiveAtom; + const settingsAtom = jotai.atom((get) => { + return get(fullConfigAtom)?.settings ?? {}; + }) as jotai.Atom; const tabAtom: jotai.Atom = jotai.atom((get) => { const windowData = get(windowDataAtom); if (windowData == null) { @@ -121,7 +124,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { } catch (_) { // do nothing } - const reducedMotionPreferenceAtom = jotai.atom((get) => get(settingsConfigAtom).window.reducedmotion); + const reducedMotionPreferenceAtom = jotai.atom((get) => get(settingsAtom)?.["window:reducedmotion"]); const typeAheadModalAtom = jotai.atom({}); atoms = { // initialized in wave.ts (will not be null inside of application) @@ -131,7 +134,8 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { client: clientAtom, waveWindow: windowDataAtom, workspace: workspaceAtom, - settingsConfigAtom, + fullConfigAtom, + settingsAtom, tabAtom, activeTabId: activeTabIdAtom, isFullScreen: isFullScreenAtom, @@ -271,19 +275,35 @@ function useBlockCache(blockId: string, name: string, makeFn: () => T): T { const settingsAtomCache = new Map>(); -function useSettingsAtom(name: string, settingsFn: (settings: SettingsConfigType) => T): jotai.Atom { - let atom = settingsAtomCache.get(name); +function useSettingsKeyAtom(key: T): jotai.Atom { + let atom = settingsAtomCache.get(key) as jotai.Atom; if (atom == null) { atom = jotai.atom((get) => { - const settings = get(atoms.settingsConfigAtom); + const settings = get(atoms.settingsAtom); if (settings == null) { return null; } - return settingsFn(settings); - }) as jotai.Atom; - settingsAtomCache.set(name, atom); + return settings[key]; + }); + settingsAtomCache.set(key, atom); } - return atom as jotai.Atom; + return atom; +} + +function useSettingsPrefixAtom(prefix: string): jotai.Atom { + // TODO: use a shallow equal here to make this more efficient + let atom = settingsAtomCache.get(prefix + ":"); + if (atom == null) { + atom = jotai.atom((get) => { + const settings = get(atoms.settingsAtom); + if (settings == null) { + return {}; + } + return util.getPrefixedSettings(settings, prefix); + }); + settingsAtomCache.set(prefix + ":", atom); + } + return atom; } const blockAtomCache = new Map>>(); @@ -337,7 +357,7 @@ function handleWSEventMessage(msg: WSEventType) { return; } if (msg.eventtype == "config") { - globalStore.set(atoms.settingsConfigAtom, msg.data.settings); + globalStore.set(atoms.fullConfigAtom, (msg.data as WatcherUpdate).fullconfig); return; } if (msg.eventtype == "userinput") { @@ -474,8 +494,17 @@ function isDev() { return cachedIsDev; } +let cachedUserName: string = null; + +function getUserName(): string { + if (cachedUserName == null) { + cachedUserName = getApi().getUserName(); + } + return cachedUserName; +} + async function openLink(uri: string) { - if (globalStore.get(atoms.settingsConfigAtom)?.web?.openlinksinternally) { + if (globalStore.get(atoms.settingsAtom)?.["web:openlinksinternally"]) { const blockDef: BlockDef = { meta: { view: "web", @@ -563,6 +592,7 @@ export { getEventSubject, getFileSubject, getObjectId, + getUserName, getViewModel, globalStore, globalWS, @@ -581,7 +611,8 @@ export { useBlockAtom, useBlockCache, useBlockDataLoaded, - useSettingsAtom, + useSettingsKeyAtom, + useSettingsPrefixAtom, waveEventSubscribe, waveEventUnsubscribe, WOS, diff --git a/frontend/app/store/services.ts b/frontend/app/store/services.ts index 060d1bd76..78e07d166 100644 --- a/frontend/app/store/services.ts +++ b/frontend/app/store/services.ts @@ -53,16 +53,12 @@ export const ClientService = new ClientServiceType(); // fileservice.FileService (file) class FileServiceType { - AddWidget(arg1: WidgetsConfigType): Promise { - return WOS.callBackendService("file", "AddWidget", Array.from(arguments)) - } - // delete file DeleteFile(connection: string, path: string): Promise { return WOS.callBackendService("file", "DeleteFile", Array.from(arguments)) } - GetSettingsConfig(): Promise { - return WOS.callBackendService("file", "GetSettingsConfig", Array.from(arguments)) + GetFullConfig(): Promise { + return WOS.callBackendService("file", "GetFullConfig", Array.from(arguments)) } GetWaveFile(arg1: string, arg2: string): Promise { return WOS.callBackendService("file", "GetWaveFile", Array.from(arguments)) @@ -72,9 +68,6 @@ class FileServiceType { ReadFile(connection: string, path: string): Promise { return WOS.callBackendService("file", "ReadFile", Array.from(arguments)) } - RemoveWidget(arg1: number): Promise { - return WOS.callBackendService("file", "RemoveWidget", Array.from(arguments)) - } // save file SaveFile(connection: string, path: string, data64: string): Promise { diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index edf31f15e..7c0ceadf8 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -139,33 +139,33 @@ const Tab = React.memo( function handleContextMenu(e: React.MouseEvent) { e.preventDefault(); let menu: ContextMenuItem[] = []; - const settings = globalStore.get(atoms.settingsConfigAtom); - console.log("settings", settings); + const fullConfig = globalStore.get(atoms.fullConfigAtom); const bgPresets: string[] = []; - for (const key in settings?.presets ?? {}) { + for (const key in fullConfig?.presets ?? {}) { if (key.startsWith("bg@")) { bgPresets.push(key); } } bgPresets.sort((a, b) => { - const aOrder = settings.presets[a]["display:order"] ?? 0; - const bOrder = settings.presets[b]["display:order"] ?? 0; + const aOrder = fullConfig.presets[a]["display:order"] ?? 0; + const bOrder = fullConfig.presets[b]["display:order"] ?? 0; return aOrder - bOrder; }); - console.log("bgPresets", bgPresets); menu.push({ label: "Copy TabId", click: () => navigator.clipboard.writeText(id) }); menu.push({ type: "separator" }); if (bgPresets.length > 0) { const submenu: ContextMenuItem[] = []; const oref = WOS.makeORef("tab", id); for (const presetName of bgPresets) { - const preset = settings.presets[presetName]; + const preset = fullConfig.presets[presetName]; if (preset == null) { continue; } submenu.push({ label: preset["display:name"] ?? presetName, - click: () => services.ObjectService.UpdateObjectMeta(oref, preset), + click: () => { + services.ObjectService.UpdateObjectMeta(oref, preset); + }, }); } menu.push({ label: "Backgrounds", type: "submenu", submenu }); diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index 6473644fa..2d492ae40 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -134,11 +134,11 @@ function DirectoryTable({ setSelectedPath, setRefreshVersion, }: DirectoryTableProps) { - const settings = jotai.useAtomValue(atoms.settingsConfigAtom); + const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom); const getIconFromMimeType = useCallback( (mimeType: string): string => { while (mimeType.length > 0) { - let icon = settings.mimetypes?.[mimeType]?.icon ?? null; + let icon = fullConfig.mimetypes?.[mimeType]?.icon ?? null; if (isIconValid(icon)) { return `fa fa-solid fa-${icon} fa-fw`; } @@ -146,14 +146,14 @@ function DirectoryTable({ } return "fa fa-solid fa-file fa-fw"; }, - [settings.mimetypes] + [fullConfig.mimetypes] ); const getIconColor = useCallback( (mimeType: string): string => { - let iconColor = settings.mimetypes?.[mimeType]?.color ?? "inherit"; + let iconColor = fullConfig.mimetypes?.[mimeType]?.color ?? "inherit"; return iconColor; }, - [settings.mimetypes] + [fullConfig.mimetypes] ); const columns = useMemo( () => [ @@ -208,7 +208,7 @@ function DirectoryTable({ }), columnHelper.accessor("path", {}), ], - [settings] + [fullConfig] ); const table = useReactTable({ diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index b0036d66c..4c2d1d043 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -3,7 +3,7 @@ import { WshServer } from "@/app/store/wshserver"; import { VDomView } from "@/app/view/term/vdom"; -import { WOS, atoms, getEventORefSubject, globalStore, useBlockAtom, useSettingsAtom } from "@/store/global"; +import { WOS, atoms, getEventORefSubject, globalStore, useBlockAtom, useSettingsPrefixAtom } from "@/store/global"; import * as services from "@/store/services"; import * as keyutil from "@/util/keyutil"; import * as util from "@/util/util"; @@ -135,8 +135,8 @@ class TermViewModel { }); this.blockBg = jotai.atom((get) => { const blockData = get(this.blockAtom); - const settings = globalStore.get(atoms.settingsConfigAtom); - const theme = computeTheme(settings, blockData?.meta?.["term:theme"]); + const fullConfig = get(atoms.fullConfigAtom); + const theme = computeTheme(fullConfig, blockData?.meta?.["term:theme"]); if (theme != null && theme.background != null) { return { bg: theme.background }; } @@ -203,9 +203,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => { const htmlElemFocusRef = React.useRef(null); model.htmlElemFocusRef = htmlElemFocusRef; const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); - const termSettingsAtom = useSettingsAtom("term", (settings: SettingsConfigType) => { - return settings?.term; - }); + const termSettingsAtom = useSettingsPrefixAtom("term"); const termSettings = jotai.useAtomValue(termSettingsAtom); React.useEffect(() => { @@ -240,8 +238,8 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => { } return true; } - const settings = globalStore.get(atoms.settingsConfigAtom); - const termTheme = computeTheme(settings, blockData?.meta?.["term:theme"]); + const fullConfig = globalStore.get(atoms.fullConfigAtom); + const termTheme = computeTheme(fullConfig, blockData?.meta?.["term:theme"]); const themeCopy = { ...termTheme }; themeCopy.background = "#00000000"; const termWrap = new TermWrap( @@ -249,8 +247,8 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => { connectElemRef.current, { theme: themeCopy, - fontSize: termSettings?.fontsize ?? 12, - fontFamily: termSettings?.fontfamily ?? "Hack", + fontSize: termSettings?.["term:fontsize"] ?? 12, + fontFamily: termSettings?.["term:fontfamily"] ?? "Hack", drawBoldTextInBrightColors: false, fontWeight: "normal", fontWeightBold: "bold", @@ -258,7 +256,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => { }, { keydownHandler: handleTerminalKeydown, - useWebGl: !termSettings?.disablewebgl, + useWebGl: !termSettings?.["term:disablewebgl"], } ); (window as any).term = termWrap; diff --git a/frontend/app/view/term/termtheme.ts b/frontend/app/view/term/termtheme.ts index 5f8b22ce6..c18543b71 100644 --- a/frontend/app/view/term/termtheme.ts +++ b/frontend/app/view/term/termtheme.ts @@ -13,7 +13,8 @@ interface TermThemeProps { } const TermThemeUpdater = ({ blockId, termRef }: TermThemeProps) => { - const { termthemes } = useAtomValue(atoms.settingsConfigAtom); + const fullConfig = useAtomValue(atoms.fullConfigAtom); + const termthemes = fullConfig?.termthemes; const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); let defaultThemeName = "default-dark"; let themeName = blockData.meta?.["term:theme"] ?? "default-dark"; diff --git a/frontend/app/view/term/termutil.ts b/frontend/app/view/term/termutil.ts index 574864048..395ebc53e 100644 --- a/frontend/app/view/term/termutil.ts +++ b/frontend/app/view/term/termutil.ts @@ -3,11 +3,11 @@ import * as util from "@/util/util"; -function computeTheme(settings: SettingsConfigType, themeName: string): TermThemeType { +function computeTheme(fullConfig: FullConfigType, themeName: string): TermThemeType { let defaultThemeName = "default-dark"; themeName = themeName ?? "default-dark"; - const defaultTheme: TermThemeType = settings?.termthemes?.[defaultThemeName] || ({} as any); - const theme: TermThemeType = settings?.termthemes?.[themeName] || ({} as any); + const defaultTheme: TermThemeType = fullConfig?.termthemes?.[defaultThemeName] || ({} as any); + const theme: TermThemeType = fullConfig?.termthemes?.[themeName] || ({} as any); const combinedTheme = { ...defaultTheme }; for (const key in theme) { if (!util.isBlank(theme[key])) { diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index 3ffe28904..e9c9fdf1f 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -3,7 +3,7 @@ import { Markdown } from "@/app/element/markdown"; import { TypingIndicator } from "@/app/element/typingindicator"; -import { WOS, atoms, fetchWaveFile, globalStore } from "@/store/global"; +import { WOS, atoms, fetchWaveFile, getUserName, globalStore } from "@/store/global"; import * as services from "@/store/services"; import { WshServer } from "@/store/wshserver"; import * as jotai from "jotai"; @@ -107,7 +107,7 @@ export class WaveAiModel implements ViewModel { const viewTextChildren: HeaderElem[] = [ { elemtype: "text", - text: get(atoms.settingsConfigAtom).ai?.model ?? "gpt-3.5-turbo", + text: get(atoms.settingsAtom)["ai:model"] ?? "gpt-3.5-turbo", }, ]; return viewTextChildren; @@ -152,18 +152,21 @@ export class WaveAiModel implements ViewModel { }; addMessage(newMessage); // send message to backend and get response - const settings = globalStore.get(atoms.settingsConfigAtom); + const settings = globalStore.get(atoms.settingsAtom); const opts: OpenAIOptsType = { - model: settings.ai.model, - apitoken: settings.ai.apitoken, - maxtokens: settings.ai.maxtokens, - timeout: settings.ai.timeoutms / 1000, - baseurl: settings.ai.baseurl, + model: settings["ai:model"], + apitoken: settings["ai:apitoken"], + maxtokens: settings["ai:maxtokens"], + timeout: settings["ai:timeoutms"] / 1000, + baseurl: settings["ai:baseurl"], }; const newPrompt: OpenAIPromptMessageType = { role: "user", content: text, }; + if (newPrompt.name == "*username") { + newPrompt.name = getUserName(); + } let temp = async () => { const history = await this.fetchAiData(); const beMsg: OpenAiStreamRequest = { diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index ad9c9f9d9..9231b8354 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -14,10 +14,28 @@ import "./workspace.less"; const iconRegex = /^[a-z0-9-]+$/; +function keyLen(obj: Object): number { + if (obj == null) { + return 0; + } + return Object.keys(obj).length; +} + +function sortByDisplayOrder(wmap: { [key: string]: WidgetConfigType }): WidgetConfigType[] { + if (wmap == null) { + return []; + } + const wlist = Object.values(wmap); + wlist.sort((a, b) => { + return a["display:order"] - b["display:order"]; + }); + return wlist; +} + const Widgets = React.memo(() => { - const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom); + const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom); const newWidgetModalVisible = React.useState(false); - const helpWidget: WidgetsConfigType = { + const helpWidget: WidgetConfigType = { icon: "circle-question", label: "help", blockdef: { @@ -26,13 +44,17 @@ const Widgets = React.memo(() => { }, }, }; - const showHelp = settingsConfig?.["widget:showhelp"] ?? true; - const showDivider = settingsConfig?.defaultwidgets?.length > 0 && settingsConfig?.widgets?.length > 0; + const showHelp = fullConfig?.settings?.["widget:showhelp"] ?? true; + const showDivider = keyLen(fullConfig?.defaultwidgets) > 0 && keyLen(fullConfig?.widgets) > 0; + const defaultWidgets = sortByDisplayOrder(fullConfig?.defaultwidgets); + const widgets = sortByDisplayOrder(fullConfig?.widgets); return (
- {settingsConfig?.defaultwidgets?.map((data, idx) => )} + {defaultWidgets.map((data, idx) => ( + + ))} {showDivider ?
: null} - {settingsConfig?.widgets?.map((data, idx) => )} + {widgets?.map((data, idx) => )} {showHelp ? ( <>
@@ -61,7 +83,7 @@ function getIconClass(icon: string): string { return `fa fa-solid fa-${icon} fa-fw`; } -const Widget = React.memo(({ widget }: { widget: WidgetsConfigType }) => { +const Widget = React.memo(({ widget }: { widget: WidgetConfigType }) => { return (
; // driven from windowId, activetabid, etc. waveWindow: jotai.Atom; // driven from WOS workspace: jotai.Atom; // driven from WOS - settingsConfigAtom: jotai.PrimitiveAtom; // driven from WOS, settings -- updated via WebSocket + fullConfigAtom: jotai.PrimitiveAtom; // driven from WOS, settings -- updated via WebSocket + settingsAtom: jotai.Atom; // derrived from fullConfig tabAtom: jotai.Atom; // driven from WOS activeTabId: jotai.Atom; // derrived from windowDataAtom isFullScreen: jotai.PrimitiveAtom; @@ -49,10 +50,9 @@ declare global { getAuthKey(): string; getIsDev(): boolean; getCursorPoint: () => Electron.Point; - getPlatform: () => NodeJS.Platform; getEnv: (varName: string) => string; - + getUserName: () => string; showContextMenu: (menu?: ElectronContextMenuItem[]) => void; onContextMenuClick: (callback: (id: string) => void) => void; onNavigate: (callback: (url: string) => void) => void; diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 351a675ee..4e618eab3 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -5,22 +5,6 @@ declare global { - // wconfig.AiConfigType - type AiConfigType = { - baseurl: string; - apitoken: string; - model: string; - maxtokens: number; - timeoutms: number; - }; - - // wconfig.AutoUpdateOpts - type AutoUpdateOpts = { - enabled: boolean; - intervalms: number; - installonquit: boolean; - }; - // waveobj.Block type Block = WaveObj & { blockdef: BlockDef; @@ -41,11 +25,6 @@ declare global { meta?: MetaType; }; - // wconfig.BlockHeaderOpts - type BlockHeaderOpts = { - showblockids: boolean; - }; - // webcmd.BlockInputWSCommand type BlockInputWSCommand = { wscommand: "blockinput"; @@ -157,6 +136,12 @@ declare global { meta: MetaType; }; + // wconfig.ConfigError + type ConfigError = { + file: string; + err: string; + }; + // wshrpc.ConnStatus type ConnStatus = { status: string; @@ -201,6 +186,17 @@ declare global { ijsonbudget?: number; }; + // wconfig.FullConfigType + type FullConfigType = { + settings: SettingsType; + mimetypes: {[key: string]: MimeTypeConfigType}; + defaultwidgets: {[key: string]: WidgetConfigType}; + widgets: {[key: string]: WidgetConfigType}; + presets: {[key: string]: MetaType}; + termthemes: {[key: string]: TermThemeType}; + configerrors: ConfigError[]; + }; + // fileservice.FullFile type FullFile = { info: FileInfo; @@ -235,6 +231,8 @@ declare global { connection?: string; history?: string[]; "history:forward"?: string[]; + "display:name"?: string; + "display:order"?: number; icon?: string; "icon:color"?: string; frame?: boolean; @@ -363,21 +361,37 @@ declare global { termsize: TermSize; }; - // wconfig.SettingsConfigType - type SettingsConfigType = { - mimetypes: {[key: string]: MimeTypeConfigType}; - term: TerminalConfigType; - ai: AiConfigType; - defaultwidgets: WidgetsConfigType[]; - widgets: WidgetsConfigType[]; - "widget:showhelp": boolean; - blockheader: BlockHeaderOpts; - autoupdate: AutoUpdateOpts; - termthemes: {[key: string]: TermThemeType}; - window: WindowSettingsType; - web: WebConfigType; - telemetry: TelemetrySettingsType; - presets?: {[key: string]: MetaType}; + // wconfig.SettingsType + type SettingsType = { + "ai:*"?: boolean; + "ai:baseurl"?: string; + "ai:apitoken"?: string; + "ai:name"?: string; + "ai:model"?: string; + "ai:maxtokens"?: number; + "ai:timeoutms"?: number; + "term:*"?: boolean; + "term:fontsize"?: number; + "term:fontfamily"?: string; + "term:disablewebgl"?: boolean; + "web:*"?: boolean; + "web:openlinksinternally"?: boolean; + "blockheader:*"?: boolean; + "blockheader:showblockids"?: boolean; + "autoupdate:*"?: boolean; + "autoupdate:enabled"?: boolean; + "autoupdate:intervalms"?: number; + "autoupdate:installonquit"?: boolean; + "widget:*"?: boolean; + "widget:showhelp"?: boolean; + "window:*"?: boolean; + "window:transparent"?: boolean; + "window:blur"?: boolean; + "window:opacity"?: number; + "window:bgcolor"?: string; + "window:reducedmotion"?: boolean; + "telemetry:*"?: boolean; + "telemetry:enabled"?: boolean; }; // waveobj.StickerClickOptsType @@ -415,11 +429,6 @@ declare global { blockids: string[]; }; - // wconfig.TelemetrySettingsType - type TelemetrySettingsType = { - enabled: boolean; - }; - // waveobj.TermSize type TermSize = { rows: number; @@ -452,13 +461,6 @@ declare global { cursorAccent: string; }; - // wconfig.TerminalConfigType - type TerminalConfigType = { - fontsize?: number; - fontfamily?: string; - disablewebgl: boolean; - }; - // wshrpc.TimeSeriesData type TimeSeriesData = { ts: number; @@ -543,8 +545,7 @@ declare global { // wconfig.WatcherUpdate type WatcherUpdate = { - settings: SettingsConfigType; - error: string; + fullconfig: FullConfigType; }; // wshrpc.WaveEvent @@ -599,11 +600,6 @@ declare global { args: any[]; }; - // wconfig.WebConfigType - type WebConfigType = { - openlinksinternally: boolean; - }; - // service.WebReturnType type WebReturnType = { success?: boolean; @@ -612,9 +608,10 @@ declare global { updates?: WaveObjUpdate[]; }; - // wconfig.WidgetsConfigType - type WidgetsConfigType = { - icon: string; + // wconfig.WidgetConfigType + type WidgetConfigType = { + "display:order"?: number; + icon?: string; color?: string; label?: string; description?: string; @@ -627,15 +624,6 @@ declare global { height: number; }; - // wconfig.WindowSettingsType - type WindowSettingsType = { - transparent: boolean; - blur: boolean; - opacity: number; - bgcolor: string; - reducedmotion: boolean; - }; - // waveobj.Workspace type Workspace = WaveObj & { name: string; diff --git a/frontend/util/util.ts b/frontend/util/util.ts index a330ce549..359b15692 100644 --- a/frontend/util/util.ts +++ b/frontend/util/util.ts @@ -242,6 +242,19 @@ function atomWithDebounce(initialValue: T, delayMilliseconds = 500): AtomWith }; } +function getPrefixedSettings(settings: SettingsType, prefix: string): SettingsType { + const rtn: SettingsType = {}; + if (settings == null || isBlank(prefix)) { + return rtn; + } + for (const key in settings) { + if (key == prefix || key.startsWith(prefix + ":")) { + rtn[key] = settings[key]; + } + } + return rtn; +} + export { atomWithDebounce, atomWithThrottle, @@ -249,6 +262,7 @@ export { base64ToString, boundNumber, fireAndForget, + getPrefixedSettings, getPromiseState, getPromiseValue, isBlank, diff --git a/frontend/wave.ts b/frontend/wave.ts index e87468b58..35c06ecd7 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -57,9 +57,9 @@ document.addEventListener("DOMContentLoaded", async () => { initWS(); await loadConnStatus(); subscribeToConnEvents(); - const settings = await services.FileService.GetSettingsConfig(); - console.log("settings", settings); - globalStore.set(atoms.settingsConfigAtom, settings); + const fullConfig = await services.FileService.GetFullConfig(); + console.log("fullconfig", fullConfig); + globalStore.set(atoms.fullConfigAtom, fullConfig); services.ObjectService.SetActiveTab(waveWindow.activetabid); // no need to wait const reactElem = React.createElement(App, null, null); const elem = document.getElementById("main"); diff --git a/pkg/gogen/gogen.go b/pkg/gogen/gogen.go new file mode 100644 index 000000000..99f45c96d --- /dev/null +++ b/pkg/gogen/gogen.go @@ -0,0 +1,107 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package gogen + +import ( + "fmt" + "reflect" + "strings" + + "github.com/wavetermdev/thenextwave/pkg/wshrpc" +) + +func GenerateBoilerplate(buf *strings.Builder, pkgName string, imports []string) { + buf.WriteString("// Copyright 2024, Command Line Inc.\n") + buf.WriteString("// SPDX-License-Identifier: Apache-2.0\n") + buf.WriteString("\n// Generated Code. DO NOT EDIT.\n\n") + buf.WriteString(fmt.Sprintf("package %s\n\n", pkgName)) + if len(imports) > 0 { + buf.WriteString("import (\n") + for _, imp := range imports { + buf.WriteString(fmt.Sprintf("\t%q\n", imp)) + } + buf.WriteString(")\n\n") + } +} + +func getBeforeColonPart(s string) string { + if colonIdx := strings.Index(s, ":"); colonIdx != -1 { + return s[:colonIdx] + } + return s +} + +func GenerateMetaMapConsts(buf *strings.Builder, constPrefix string, rtype reflect.Type) { + buf.WriteString("const (\n") + var lastBeforeColon = "" + isFirst := true + for idx := 0; idx < rtype.NumField(); idx++ { + field := rtype.Field(idx) + if field.PkgPath != "" { + continue + } + fieldName := field.Name + jsonTag := field.Tag.Get("json") + if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 { + jsonTag = jsonTag[:commaIdx] + } + if jsonTag == "" { + jsonTag = fieldName + } + beforeColon := getBeforeColonPart(jsonTag) + if beforeColon != lastBeforeColon { + if !isFirst { + buf.WriteString("\n") + } + lastBeforeColon = beforeColon + } + cname := constPrefix + fieldName + buf.WriteString(fmt.Sprintf("\t%-40s = %q\n", cname, jsonTag)) + isFirst = false + } + buf.WriteString(")\n") +} + +func GenMethod_Call(buf *strings.Builder, methodDecl *wshrpc.WshRpcMethodDecl) { + fmt.Fprintf(buf, "// command %q, wshserver.%s\n", methodDecl.Command, methodDecl.MethodName) + var dataType string + dataVarName := "nil" + if methodDecl.CommandDataType != nil { + dataType = ", data " + methodDecl.CommandDataType.String() + dataVarName = "data" + } + returnType := "error" + respName := "_" + tParamVal := "any" + if methodDecl.DefaultResponseDataType != nil { + returnType = "(" + methodDecl.DefaultResponseDataType.String() + ", error)" + respName = "resp" + tParamVal = methodDecl.DefaultResponseDataType.String() + } + fmt.Fprintf(buf, "func %s(w *wshutil.WshRpc%s, opts *wshrpc.RpcOpts) %s {\n", methodDecl.MethodName, dataType, returnType) + fmt.Fprintf(buf, "\t%s, err := sendRpcRequestCallHelper[%s](w, %q, %s, opts)\n", respName, tParamVal, methodDecl.Command, dataVarName) + if methodDecl.DefaultResponseDataType != nil { + fmt.Fprintf(buf, "\treturn resp, err\n") + } else { + fmt.Fprintf(buf, "\treturn err\n") + } + fmt.Fprintf(buf, "}\n\n") +} + +func GenMethod_ResponseStream(buf *strings.Builder, methodDecl *wshrpc.WshRpcMethodDecl) { + fmt.Fprintf(buf, "// command %q, wshserver.%s\n", methodDecl.Command, methodDecl.MethodName) + var dataType string + dataVarName := "nil" + if methodDecl.CommandDataType != nil { + dataType = ", data " + methodDecl.CommandDataType.String() + dataVarName = "data" + } + respType := "any" + if methodDecl.DefaultResponseDataType != nil { + respType = methodDecl.DefaultResponseDataType.String() + } + fmt.Fprintf(buf, "func %s(w *wshutil.WshRpc%s, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[%s] {\n", methodDecl.MethodName, dataType, respType) + fmt.Fprintf(buf, "\treturn sendRpcRequestResponseStreamHelper[%s](w, %q, %s, opts)\n", respType, methodDecl.Command, dataVarName) + fmt.Fprintf(buf, "}\n\n") +} diff --git a/pkg/service/fileservice/fileservice.go b/pkg/service/fileservice/fileservice.go index 4e8d05a8f..4172a90f8 100644 --- a/pkg/service/fileservice/fileservice.go +++ b/pkg/service/fileservice/fileservice.go @@ -153,17 +153,7 @@ func (fs *FileService) DeleteFile(connection string, path string) error { return wshclient.RemoteFileDeleteCommand(client, path, &wshrpc.RpcOpts{Route: connRoute}) } -func (fs *FileService) GetSettingsConfig() wconfig.SettingsConfigType { +func (fs *FileService) GetFullConfig() wconfig.FullConfigType { watcher := wconfig.GetWatcher() - return watcher.GetSettingsConfig() -} - -func (fs *FileService) AddWidget(newWidget wconfig.WidgetsConfigType) error { - watcher := wconfig.GetWatcher() - return watcher.AddWidget(newWidget) -} - -func (fs *FileService) RemoveWidget(idx uint) error { - watcher := wconfig.GetWatcher() - return watcher.RmWidget(idx) + return watcher.GetFullConfig() } diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go index a1f62f3ce..ae5e7ed7f 100644 --- a/pkg/telemetry/telemetry.go +++ b/pkg/telemetry/telemetry.go @@ -74,11 +74,8 @@ func (tdata *TelemetryData) Scan(val interface{}) error { } func IsTelemetryEnabled() bool { - settings := wconfig.GetWatcher().GetSettingsConfig() - if settings.Telemetry == nil || settings.Telemetry.Enabled == nil { - return true - } - return *settings.Telemetry.Enabled + settings := wconfig.GetWatcher().GetFullConfig() + return settings.Settings.TelemetryEnabled } func IsAllowedRenderer(renderer string) bool { diff --git a/pkg/tsgen/tsgen.go b/pkg/tsgen/tsgen.go index 6fc4e1d61..a8cf05e56 100644 --- a/pkg/tsgen/tsgen.go +++ b/pkg/tsgen/tsgen.go @@ -35,8 +35,7 @@ var ExtraTypes = []any{ eventbus.WSFileEventData{}, waveobj.LayoutActionData{}, filestore.WaveFile{}, - wconfig.SettingsConfigType{}, - wconfig.TermThemesConfigType{}, + wconfig.FullConfigType{}, wconfig.WatcherUpdate{}, wshutil.RpcMessage{}, wshrpc.WshServerCommandMeta{}, diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go new file mode 100644 index 000000000..a9d23d7ae --- /dev/null +++ b/pkg/waveobj/metaconsts.go @@ -0,0 +1,59 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Generated Code. DO NOT EDIT. + +package waveobj + +const ( + MetaKey_View = "view" + + MetaKey_Controller = "controller" + + MetaKey_Title = "title" + + MetaKey_File = "file" + + MetaKey_Url = "url" + + MetaKey_Connection = "connection" + + MetaKey_History = "history" + MetaKey_HistoryForward = "history:forward" + + MetaKey_DisplayName = "display:name" + MetaKey_DisplayOrder = "display:order" + + MetaKey_Icon = "icon" + MetaKey_IconColor = "icon:color" + + MetaKey_Frame = "frame" + MetaKey_FrameClear = "frame:*" + MetaKey_FrameBorderColor = "frame:bordercolor" + MetaKey_FrameBorderColor_Focused = "frame:bordercolor:focused" + + MetaKey_Cmd = "cmd" + MetaKey_CmdClear = "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_BgClear = "bg:*" + MetaKey_BgOpacity = "bg:opacity" + MetaKey_BgBlendMode = "bg:blendmode" + + MetaKey_TermClear = "term:*" + MetaKey_TermFontSize = "term:fontsize" + MetaKey_TermFontFamily = "term:fontfamily" + MetaKey_TermMode = "term:mode" + MetaKey_TermTheme = "term:theme" + + MetaKey_Count = "count" +) + diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go index 12c716151..f6056e9dc 100644 --- a/pkg/waveobj/wtypemeta.go +++ b/pkg/waveobj/wtypemeta.go @@ -9,51 +9,6 @@ import ( const Entity_Any = "any" -// 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_DisplayName = "display:name" // special, does not get merged - MetaKey_DisplayOrder = "display:order" // special, does not get merged - - MetaKey_View = "view" - MetaKey_Controller = "controller" - MetaKey_Title = "title" - MetaKey_File = "file" - MetaKey_Url = "url" - MetaKey_Connection = "connection" - MetaKey_History = "history" // stores an array of history items specific to the block - - 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_BgClear = "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" - MetaKey_Count = "count" // temp for cpu plot. will remove later -) - // for typescript typing type MetaTSType struct { // shared @@ -66,6 +21,9 @@ type MetaTSType struct { History []string `json:"history,omitempty"` HistoryForward []string `json:"history:forward,omitempty"` + DisplayName string `json:"display:name,omitempty"` + DisplayOrder float64 `json:"display:order,omitempty"` + Icon string `json:"icon,omitempty"` IconColor string `json:"icon:color,omitempty"` @@ -118,7 +76,8 @@ type MetaPresetDecl struct { } // returns a clean copy of meta with mergeMeta merged in -func MergeMeta(meta MetaMapType, metaUpdate MetaMapType) MetaMapType { +// if mergeSpecial is false, then special keys will not be merged (like display:*) +func MergeMeta(meta MetaMapType, metaUpdate MetaMapType, mergeSpecial bool) MetaMapType { rtn := make(MetaMapType) for k, v := range meta { rtn[k] = v @@ -145,7 +104,7 @@ func MergeMeta(meta MetaMapType, metaUpdate MetaMapType) MetaMapType { } // now deal with regular keys for k, v := range metaUpdate { - if strings.HasPrefix(k, "display:") { + if !mergeSpecial && strings.HasPrefix(k, "display:") { continue } if strings.HasSuffix(k, ":*") { diff --git a/pkg/wconfig/defaultconfig/defaultconfig.go b/pkg/wconfig/defaultconfig/defaultconfig.go new file mode 100644 index 000000000..bc28a9557 --- /dev/null +++ b/pkg/wconfig/defaultconfig/defaultconfig.go @@ -0,0 +1,9 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package defaultconfig + +import "embed" + +//go:embed *.json +var ConfigFS embed.FS diff --git a/pkg/wconfig/defaultconfig/defaultwidgets.json b/pkg/wconfig/defaultconfig/defaultwidgets.json new file mode 100644 index 000000000..3f6ebc863 --- /dev/null +++ b/pkg/wconfig/defaultconfig/defaultwidgets.json @@ -0,0 +1,55 @@ +{ + "defwidget@terminal": { + "display:order": 1, + "icon": "square-terminal", + "label": "terminal", + "blockdef": { + "meta": { + "view": "term", + "controller": "shell" + } + } + }, + "defwidget@files": { + "display:order": 2, + "icon": "folder", + "label": "files", + "blockdef": { + "meta": { + "view": "preview", + "file": "~" + } + } + }, + "defwidget@web": { + "display:order": 3, + "icon": "globe", + "label": "web", + "blockdef": { + "meta": { + "view": "web", + "url": "https://waveterm.dev/" + } + } + }, + "defwidget@ai": { + "display:order": 4, + "icon": "sparkles", + "label": "ai", + "blockdef": { + "meta": { + "view": "waveai" + } + } + }, + "defwidget@cpuplot": { + "display:order": 5, + "icon": "chart-line", + "label": "cpu", + "blockdef": { + "meta": { + "view": "cpuplot" + } + } + } +} diff --git a/pkg/wconfig/defaultconfig/mimetypes.json b/pkg/wconfig/defaultconfig/mimetypes.json new file mode 100644 index 000000000..c4acb003b --- /dev/null +++ b/pkg/wconfig/defaultconfig/mimetypes.json @@ -0,0 +1,57 @@ +{ + "audio": { + "icon": "file-audio" + }, + "application/pdf": { + "icon": "file-pdf" + }, + "application/json": { + "icon": "file-lines" + }, + "directory": { + "icon": "folder", + "color": "var(--term-bright-blue)" + }, + "font": { + "icon": "book-font" + }, + "image": { + "icon": "file-image" + }, + "text": { + "icon": "file-lines" + }, + "text/css": { + "icon": "css3-alt fa-brands" + }, + "text/javascript": { + "icon": "js fa-brands" + }, + "text/typescript": { + "icon": "js fa-brands" + }, + "text/golang": { + "icon": "golang fa-brands" + }, + "text/html": { + "icon": "html5 fa-brands" + }, + "text/less": { + "icon": "less fa-brands" + }, + "text/markdown": { + "icon": "markdown fa-brands" + }, + "text/rust": { + "icon": "rust fa-brands" + }, + "text/scss": { + "icon": "sass fa-brands" + }, + "video": { + "icon": "file-video" + }, + "text/csv": { + "icon": "file-csv" + } +} diff --git a/pkg/wconfig/defaultconfig/presets.json b/pkg/wconfig/defaultconfig/presets.json new file mode 100644 index 000000000..9e0ee9b92 --- /dev/null +++ b/pkg/wconfig/defaultconfig/presets.json @@ -0,0 +1,32 @@ +{ + "bg@default": { + "display:name": "Default", + "display:order": -1, + "bg:*": true + }, + "bg@rainbow": { + "display:name": "Rainbow", + "display:order": 1, + "bg:*": true, + "bg": "linear-gradient( 226.4deg, rgba(255,26,1,1) 28.9%, rgba(254,155,1,1) 33%, rgba(255,241,0,1) 48.6%, rgba(34,218,1,1) 65.3%, rgba(0,141,254,1) 80.6%, rgba(113,63,254,1) 100.1% )", + "bg:opacity": 0.3 + }, + "bg@green": { + "display:name": "Green", + "bg:*": true, + "bg": "green", + "bg:opacity": 0.3 + }, + "bg@blue": { + "display:name": "Blue", + "bg:*": true, + "bg": "blue", + "bg:opacity": 0.3 + }, + "bg@red": { + "display:name": "Red", + "bg:*": true, + "bg": "red", + "bg:opacity": 0.3 + } +} diff --git a/pkg/wconfig/defaultconfig/settings.json b/pkg/wconfig/defaultconfig/settings.json new file mode 100644 index 000000000..d5813dbef --- /dev/null +++ b/pkg/wconfig/defaultconfig/settings.json @@ -0,0 +1,8 @@ +{ + "ai:model": "gpt-3.5-turbo", + "ai:maxtokens": 1000, + "ai:timeoutms": 10000, + "autoupdate:enabled": true, + "autoupdate:installonquit": true, + "autoupdate:intervalms": 3600000 +} diff --git a/pkg/wconfig/defaultconfig/termthemes.json b/pkg/wconfig/defaultconfig/termthemes.json new file mode 100644 index 000000000..a92e6833b --- /dev/null +++ b/pkg/wconfig/defaultconfig/termthemes.json @@ -0,0 +1,74 @@ +{ + "default-dark": { + "black": "#757575", + "red": "#cc685c", + "green": "#76c266", + "yellow": "#cbca9b", + "blue": "#85aacb", + "magenta": "#cc72ca", + "cyan": "#74a7cb", + "white": "#c1c1c1", + "brightBlack": "#727272", + "brightRed": "#cc9d97", + "brightGreen": "#a3dd97", + "brightYellow": "#cbcaaa", + "brightBlue": "#9ab6cb", + "brightMagenta": "#cc8ecb", + "brightCyan": "#b7b8cb", + "brightWhite": "#f0f0f0", + "gray": "#8b918a", + "cmdtext": "#f0f0f0", + "foreground": "#c1c1c1", + "selectionBackground": "", + "background": "#00000077", + "cursorAccent": "" + }, + "dracula": { + "black": "#21222C", + "red": "#FF5555", + "green": "#50FA7B", + "yellow": "#F1FA8C", + "blue": "#BD93F9", + "magenta": "#FF79C6", + "cyan": "#8BE9FD", + "white": "#F8F8F2", + "brightBlack": "#6272A4", + "brightRed": "#FF6E6E", + "brightGreen": "#69FF94", + "brightYellow": "#FFFFA5", + "brightBlue": "#D6ACFF", + "brightMagenta": "#FF92DF", + "brightCyan": "#A4FFFF", + "brightWhite": "#FFFFFF", + "gray": "#6272A4", + "cmdtext": "#F8F8F2", + "foreground": "#F8F8F2", + "selectionBackground": "#44475a", + "background": "#282a36", + "cursorAccent": "#f8f8f2" + }, + "campbell": { + "black": "#0C0C0C", + "red": "#C50F1F", + "green": "#13A10E", + "yellow": "#C19C00", + "blue": "#0037DA", + "magenta": "#881798", + "cyan": "#3A96DD", + "white": "#CCCCCC", + "brightBlack": "#767676", + "brightRed": "#E74856", + "brightGreen": "#16C60C", + "brightYellow": "#F9F1A5", + "brightBlue": "#3B78FF", + "brightMagenta": "#B4009E", + "brightCyan": "#61D6D6", + "brightWhite": "#F2F2F2", + "gray": "#767676", + "cmdtext": "#CCCCCC", + "foreground": "#CCCCCC", + "selectionBackground": "#3A96DD", + "background": "#0C0C0C", + "cursorAccent": "#CCCCCC" + } +} diff --git a/pkg/wconfig/filewatcher.go b/pkg/wconfig/filewatcher.go index 4dea4fdbb..8eca08bfa 100644 --- a/pkg/wconfig/filewatcher.go +++ b/pkg/wconfig/filewatcher.go @@ -4,14 +4,9 @@ package wconfig import ( - "encoding/json" - "errors" - "fmt" "log" - "os" "path/filepath" "regexp" - "strings" "sync" "github.com/fsnotify/fsnotify" @@ -22,97 +17,19 @@ import ( const configDir = "config" var configDirAbsPath = filepath.Join(wavebase.GetWaveHomeDir(), configDir) -var termThemesDirAbsPath = filepath.Join(configDirAbsPath, termThemesDir) var instance *Watcher var once sync.Once type Watcher struct { - initialized bool - watcher *fsnotify.Watcher - mutex sync.Mutex - settingsData SettingsConfigType + initialized bool + watcher *fsnotify.Watcher + mutex sync.Mutex + fullConfig FullConfigType } type WatcherUpdate struct { - Settings SettingsConfigType `json:"settings"` - Error string `json:"error"` -} - -func LoadFullSettings() (*SettingsConfigType, error) { - // first load settings.json - // then load themes - // then apply defaults - settings, err := readFileContents[SettingsConfigType](settingsAbsPath, false) - if err != nil { - return nil, err - } - themes, err := readThemes() - if err != nil { - return nil, err - } - if settings.TermThemes == nil { - settings.TermThemes = make(map[string]TermThemeType) - } - for k, v := range themes { - settings.TermThemes[k] = v - } - applyDefaultSettings(settings) - return settings, nil -} - -func readThemes() (map[string]TermThemeType, error) { - files, err := os.ReadDir(termThemesDirAbsPath) - if errors.Is(err, os.ErrNotExist) { - return nil, nil - } - if err != nil { - return nil, fmt.Errorf("error reading themes directory: %v", err) - } - themes := make(map[string]TermThemeType) - for _, file := range files { - if !file.IsDir() && filepath.Ext(file.Name()) == ".json" { - log.Printf("reading theme file %s\n", file.Name()) - theme, err := readFileContents[TermThemeType](filepath.Join(termThemesDirAbsPath, file.Name()), true) - if err != nil { - log.Printf("error reading theme file %s: %v", file.Name(), err) - continue - } - if theme == nil { - continue - } - themeName := getThemeName(file.Name()) - themes[themeName] = *theme - } - } - return themes, nil - -} - -func readFileContents[T any](filePath string, nilOnNotExist bool) (*T, error) { - var content T - data, err := os.ReadFile(filePath) - if errors.Is(err, os.ErrNotExist) { - if nilOnNotExist { - return nil, nil - } else { - return &content, nil - } - } - if err != nil { - log.Printf("could not read file %s: %v", filePath, err) - return nil, err - } - if err := json.Unmarshal(data, &content); err != nil { - log.Printf("could not unmarshal file %s: %v", filePath, err) - return nil, err - } - return &content, nil -} - -func isInDirectory(fileName, directory string) bool { - rel, err := filepath.Rel(directory, fileName) - return err == nil && !strings.HasPrefix(rel, "..") + FullConfig FullConfigType `json:"fullconfig"` } // GetWatcher returns the singleton instance of the Watcher @@ -124,52 +41,14 @@ func GetWatcher() *Watcher { return } instance = &Watcher{watcher: watcher} - if err := instance.addSettingsFile(settingsAbsPath); err != nil { - log.Printf("failed to add path %s to watcher: %v", settingsAbsPath, err) - return - } - if err := instance.addTermThemesDir(termThemesDirAbsPath); err != nil { - log.Printf("failed to add terminal themes path %s to watcher: %v", termThemesDirAbsPath, err) - return + err = instance.watcher.Add(configDirAbsPath) + if err != nil { + log.Printf("failed to add path %s to watcher: %v", configDirAbsPath, err) } }) return instance } -func (w *Watcher) addSettingsFile(filePath string) error { - w.mutex.Lock() - defer w.mutex.Unlock() - - dir := filepath.Dir(filePath) - err := os.MkdirAll(dir, 0751) - if err != nil { - return fmt.Errorf("error creating config directory: %v", err) - } - - w.watcher.Add(filePath) - log.Printf("started config watcher: %v\n", filePath) - return nil -} - -func (w *Watcher) addTermThemesDir(dir string) error { - w.mutex.Lock() - defer w.mutex.Unlock() - - _, err := os.Stat(dir) - if os.IsNotExist(err) { - if err := os.MkdirAll(dir, 0751); err != nil { - return fmt.Errorf("error creating themes directory: %v", err) - } - } else if err != nil { - return fmt.Errorf("error accessing themes directory: %v", err) - } - if err := w.watcher.Add(dir); err != nil { - return fmt.Errorf("error adding themes directory to watcher: %v", err) - } - log.Printf("started termthemes watcher: %v\n", dir) - return nil -} - func (w *Watcher) Start() { w.mutex.Lock() defer w.mutex.Unlock() @@ -198,13 +77,9 @@ func (w *Watcher) Start() { // for initial values, exit on first error func (w *Watcher) sendInitialValues() error { - settings, err := LoadFullSettings() - if err != nil { - return err - } - w.settingsData = *settings + w.fullConfig = ReadFullConfig() message := WatcherUpdate{ - Settings: w.settingsData, + FullConfig: w.fullConfig, } w.broadcast(message) return nil @@ -226,17 +101,12 @@ func (w *Watcher) broadcast(message WatcherUpdate) { EventType: eventbus.WSEvent_Config, Data: message, }) - - if message.Error != "" { - log.Printf("watcher: error processing update: %v. error: %s", message.Settings, message.Error) - } } -func (w *Watcher) GetSettingsConfig() SettingsConfigType { +func (w *Watcher) GetFullConfig() FullConfigType { w.mutex.Lock() defer w.mutex.Unlock() - - return w.settingsData + return w.fullConfig } func (w *Watcher) handleEvent(event fsnotify.Event) { @@ -250,11 +120,7 @@ func (w *Watcher) handleEvent(event fsnotify.Event) { if !isValidSubSettingsFileName(fileName) { return } - if isInDirectory(fileName, termThemesDirAbsPath) { - w.handleTermThemesEvent(event, fileName) - } else if filepath.Base(fileName) == filepath.Base(settingsAbsPath) { - w.handleSettingsFileEvent(event, fileName) - } + w.handleSettingsFileEvent(event, fileName) } var validFileRe = regexp.MustCompile(`^[a-zA-Z0-9_@.-]+\.json$`) @@ -267,50 +133,8 @@ func isValidSubSettingsFileName(fileName string) bool { return validFileRe.MatchString(baseName) } -func (w *Watcher) handleTermThemesEvent(event fsnotify.Event, fileName string) { - settings, err := LoadFullSettings() - if err != nil { - log.Printf("error loading settings after term-themes event: %v", err) - return - } - w.settingsData = *settings - w.broadcast(WatcherUpdate{Settings: w.settingsData}) -} - func (w *Watcher) handleSettingsFileEvent(event fsnotify.Event, fileName string) { - settings, err := LoadFullSettings() - if err != nil { - log.Printf("error loading settings after settings file event: %v", err) - return - } - w.settingsData = *settings - w.broadcast(WatcherUpdate{Settings: w.settingsData}) -} - -func getThemeName(fileName string) string { - return strings.TrimSuffix(filepath.Base(fileName), filepath.Ext(fileName)) -} - -func (w *Watcher) AddWidget(newWidget WidgetsConfigType) error { - current := w.GetSettingsConfig() - current.Widgets = append(current.Widgets, newWidget) - update, err := json.Marshal(current) - if err != nil { - return err - } - - os.MkdirAll(filepath.Dir(settingsFile), 0751) - return os.WriteFile(settingsFile, update, 0644) -} - -func (w *Watcher) RmWidget(idx uint) error { - current := w.GetSettingsConfig().Widgets - truncated := append(current[:idx], current[idx+1:]...) - update, err := json.Marshal(truncated) - if err != nil { - return err - } - - os.MkdirAll(filepath.Dir(settingsFile), 0751) - return os.WriteFile(settingsFile, update, 0644) + fullConfig := ReadFullConfig() + w.fullConfig = fullConfig + w.broadcast(WatcherUpdate{FullConfig: w.fullConfig}) } diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go new file mode 100644 index 000000000..688f5f347 --- /dev/null +++ b/pkg/wconfig/metaconsts.go @@ -0,0 +1,46 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Generated Code. DO NOT EDIT. + +package wconfig + +const ( + ConfigKey_AiClear = "ai:*" + ConfigKey_AiBaseURL = "ai:baseurl" + ConfigKey_AiApiToken = "ai:apitoken" + ConfigKey_AiName = "ai:name" + ConfigKey_AiModel = "ai:model" + ConfigKey_AiMaxTokens = "ai:maxtokens" + ConfigKey_AiTimeoutMs = "ai:timeoutms" + + ConfigKey_TermClear = "term:*" + ConfigKey_TermFontSize = "term:fontsize" + ConfigKey_TermFontFamily = "term:fontfamily" + ConfigKey_TermDisableWebGl = "term:disablewebgl" + + ConfigKey_WebClear = "web:*" + ConfigKey_WebOpenLinksInternally = "web:openlinksinternally" + + ConfigKey_BlockHeaderClear = "blockheader:*" + ConfigKey_BlockHeaderShowBlockIds = "blockheader:showblockids" + + ConfigKey_AutoUpdateClear = "autoupdate:*" + ConfigKey_AutoUpdateEnabled = "autoupdate:enabled" + ConfigKey_AutoUpdateIntervalMs = "autoupdate:intervalms" + ConfigKey_AutoUpdateInstallOnQuit = "autoupdate:installonquit" + + ConfigKey_WidgetClear = "widget:*" + ConfigKey_WidgetShowHelp = "widget:showhelp" + + ConfigKey_WindowClear = "window:*" + ConfigKey_WindowTransparent = "window:transparent" + ConfigKey_WindowBlur = "window:blur" + ConfigKey_WindowOpacity = "window:opacity" + ConfigKey_WindowBgColor = "window:bgcolor" + ConfigKey_WindowReducedMotion = "window:reducedmotion" + + ConfigKey_TelemetryClear = "telemetry:*" + ConfigKey_TelemetryEnabled = "telemetry:enabled" +) + diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index f05d649fd..93b11327e 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -4,40 +4,293 @@ package wconfig import ( + "bytes" + "encoding/json" + "fmt" + "os" "path/filepath" + "reflect" + "sort" + "strings" + "github.com/wavetermdev/thenextwave/pkg/util/utilfn" "github.com/wavetermdev/thenextwave/pkg/waveobj" + "github.com/wavetermdev/thenextwave/pkg/wconfig/defaultconfig" ) -const termThemesDir = "terminal-themes" -const settingsFile = "settings.json" +const SettingsFile = "settings.json" -var settingsAbsPath = filepath.Join(configDirAbsPath, settingsFile) +type SettingsType struct { + AiClear bool `json:"ai:*,omitempty"` + AiBaseURL string `json:"ai:baseurl,omitempty"` + AiApiToken string `json:"ai:apitoken,omitempty"` + AiName string `json:"ai:name,omitempty"` + AiModel string `json:"ai:model,omitempty"` + AiMaxTokens int `json:"ai:maxtokens,omitempty"` + AiTimeoutMs int `json:"ai:timeoutms,omitempty"` -type WidgetsConfigType struct { - Icon string `json:"icon"` - Color string `json:"color,omitempty"` - Label string `json:"label,omitempty"` - Description string `json:"description,omitempty"` - BlockDef waveobj.BlockDef `json:"blockdef"` + TermClear bool `json:"term:*,omitempty"` + TermFontSize int `json:"term:fontsize,omitempty"` + TermFontFamily string `json:"term:fontfamily,omitempty"` + TermDisableWebGl bool `json:"term:disablewebgl,omitempty"` + + WebClear bool `json:"web:*,omitempty"` + WebOpenLinksInternally bool `json:"web:openlinksinternally,omitempty"` + + BlockHeaderClear bool `json:"blockheader:*,omitempty"` + BlockHeaderShowBlockIds bool `json:"blockheader:showblockids,omitempty"` + + AutoUpdateClear bool `json:"autoupdate:*,omitempty"` + AutoUpdateEnabled bool `json:"autoupdate:enabled,omitempty"` + AutoUpdateIntervalMs int `json:"autoupdate:intervalms,omitempty"` + AutoUpdateInstallOnQuit bool `json:"autoupdate:installonquit,omitempty"` + + WidgetClear bool `json:"widget:*,omitempty"` + WidgetShowHelp bool `json:"widget:showhelp,omitempty"` + + WindowClear bool `json:"window:*,omitempty"` + WindowTransparent bool `json:"window:transparent,omitempty"` + WindowBlur bool `json:"window:blur,omitempty"` + WindowOpacity float64 `json:"window:opacity,omitempty"` + WindowBgColor string `json:"window:bgcolor,omitempty"` + WindowReducedMotion bool `json:"window:reducedmotion,omitempty"` + + TelemetryClear bool `json:"telemetry:*,omitempty"` + TelemetryEnabled bool `json:"telemetry:enabled,omitempty"` } -type TerminalConfigType struct { - FontSize int `json:"fontsize,omitempty"` - FontFamily string `json:"fontfamily,omitempty"` - DisableWebGl bool `json:"disablewebgl"` +type ConfigError struct { + File string `json:"file"` + Err string `json:"err"` } -type WebConfigType struct { - OpenLinksInternally bool `json:"openlinksinternally"` +type FullConfigType struct { + Settings SettingsType `json:"settings" merge:"meta"` + MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"` + DefaultWidgets map[string]WidgetConfigType `json:"defaultwidgets"` + Widgets map[string]WidgetConfigType `json:"widgets"` + Presets map[string]waveobj.MetaMapType `json:"presets"` + TermThemes map[string]TermThemeType `json:"termthemes"` + ConfigErrors []ConfigError `json:"configerrors" configfile:"-"` } -type AiConfigType struct { - BaseURL string `json:"baseurl"` - ApiToken string `json:"apitoken"` - Model string `json:"model"` - MaxTokens uint32 `json:"maxtokens"` - TimeoutMs uint32 `json:"timeoutms"` +var settingsAbsPath = filepath.Join(configDirAbsPath, SettingsFile) + +func readConfigHelper(fileName string, barr []byte, readErr error) (waveobj.MetaMapType, []ConfigError) { + var cerrs []ConfigError + if readErr != nil && !os.IsNotExist(readErr) { + cerrs = append(cerrs, ConfigError{File: "defaults:" + fileName, Err: readErr.Error()}) + } + if len(barr) == 0 { + return nil, cerrs + } + var rtn waveobj.MetaMapType + err := json.Unmarshal(barr, &rtn) + if err != nil { + cerrs = append(cerrs, ConfigError{File: "defaults:" + fileName, Err: err.Error()}) + } + return rtn, cerrs +} + +func ReadDefaultsConfigFile(fileName string) (waveobj.MetaMapType, []ConfigError) { + barr, readErr := defaultconfig.ConfigFS.ReadFile(fileName) + return readConfigHelper("defaults:"+fileName, barr, readErr) +} + +func ReadWaveHomeConfigFile(fileName string) (waveobj.MetaMapType, []ConfigError) { + fullFileName := filepath.Join(configDirAbsPath, fileName) + barr, err := os.ReadFile(fullFileName) + return readConfigHelper(fullFileName, barr, err) +} + +func WriteWaveHomeConfigFile(fileName string, m waveobj.MetaMapType) error { + fullFileName := filepath.Join(configDirAbsPath, fileName) + barr, err := jsonMarshalConfigInOrder(m) + if err != nil { + return err + } + return os.WriteFile(fullFileName, barr, 0644) +} + +// simple merge that overwrites +func mergeMetaMapSimple(m waveobj.MetaMapType, toMerge waveobj.MetaMapType) waveobj.MetaMapType { + if m == nil { + return toMerge + } + if toMerge == nil { + return m + } + for k, v := range toMerge { + if v == nil { + delete(m, k) + continue + } + m[k] = v + } + if len(m) == 0 { + return nil + } + return m +} + +func ReadConfigPart(partName string, simpleMerge bool) (waveobj.MetaMapType, []ConfigError) { + defConfig, cerrs1 := ReadDefaultsConfigFile(partName) + userConfig, cerrs2 := ReadWaveHomeConfigFile(partName) + allErrs := append(cerrs1, cerrs2...) + if simpleMerge { + return mergeMetaMapSimple(defConfig, userConfig), allErrs + } else { + return waveobj.MergeMeta(defConfig, userConfig, true), allErrs + } +} + +func ReadFullConfig() FullConfigType { + var fullConfig FullConfigType + configRType := reflect.TypeOf(fullConfig) + configRVal := reflect.ValueOf(&fullConfig).Elem() + for fieldIdx := 0; fieldIdx < configRType.NumField(); fieldIdx++ { + field := configRType.Field(fieldIdx) + if field.PkgPath != "" { + continue + } + configFile := field.Tag.Get("configfile") + if configFile == "-" { + continue + } + jsonTag := field.Tag.Get("json") + if jsonTag == "-" || jsonTag == "" { + continue + } + simpleMerge := field.Tag.Get("merge") == "" + fileName := field.Tag.Get("json") + ".json" + configPart, cerrs := ReadConfigPart(fileName, simpleMerge) + fullConfig.ConfigErrors = append(fullConfig.ConfigErrors, cerrs...) + if configPart != nil { + fieldPtr := configRVal.Field(fieldIdx).Addr().Interface() + utilfn.ReUnmarshal(fieldPtr, configPart) + } + } + return fullConfig +} + +func getConfigKeyType(configKey string) reflect.Type { + ctype := reflect.TypeOf(SettingsType{}) + for i := 0; i < ctype.NumField(); i++ { + field := ctype.Field(i) + if field.Tag.Get("json") == configKey { + return field.Type + } + } + return nil +} + +func getConfigKeyNamespace(key string) string { + colonIdx := strings.Index(key, ":") + if colonIdx == -1 { + return "" + } + return key[:colonIdx] +} + +func orderConfigKeys(m waveobj.MetaMapType) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { + k1 := keys[i] + k2 := keys[j] + k1ns := getConfigKeyNamespace(k1) + k2ns := getConfigKeyNamespace(k2) + if k1ns != k2ns { + return k1ns < k2ns + } + return k1 < k2 + }) + return keys +} + +func reindentJson(barr []byte, indentStr string) []byte { + if len(barr) < 2 { + return barr + } + if barr[0] != '{' && barr[0] != '[' { + return barr + } + if bytes.Index(barr, []byte("\n")) == -1 { + return barr + } + outputLines := bytes.Split(barr, []byte("\n")) + for i, line := range outputLines { + if i == 0 || i == len(outputLines)-1 { + continue + } + outputLines[i] = append([]byte(indentStr), line...) + } + return bytes.Join(outputLines, []byte("\n")) +} + +func jsonMarshalConfigInOrder(m waveobj.MetaMapType) ([]byte, error) { + if len(m) == 0 { + return []byte("{}"), nil + } + var buf bytes.Buffer + orderedKeys := orderConfigKeys(m) + buf.WriteString("{\n") + for idx, key := range orderedKeys { + val := m[key] + keyBarr, err := json.Marshal(key) + if err != nil { + return nil, err + } + valBarr, err := json.MarshalIndent(val, "", " ") + if err != nil { + return nil, err + } + valBarr = reindentJson(valBarr, " ") + buf.WriteString(" ") + buf.Write(keyBarr) + buf.WriteString(": ") + buf.Write(valBarr) + if idx < len(orderedKeys)-1 { + buf.WriteString(",") + } + buf.WriteString("\n") + } + buf.WriteString("}") + return buf.Bytes(), nil +} + +func SetBaseConfigValue(configKey string, val any) error { + ctype := getConfigKeyType(configKey) + if ctype == nil { + return fmt.Errorf("invalid config key: %s", configKey) + } + m, cerrs := ReadWaveHomeConfigFile(SettingsFile) + if len(cerrs) > 0 { + return fmt.Errorf("error reading config file: %v", cerrs[0]) + } + if m == nil { + m = make(waveobj.MetaMapType) + } + if val == nil { + delete(m, configKey) + } else { + if reflect.TypeOf(val) != ctype { + return fmt.Errorf("invalid value type for %s: %T", configKey, val) + } + m[configKey] = val + } + return WriteWaveHomeConfigFile(SettingsFile, m) +} + +type WidgetConfigType struct { + DisplayOrder float64 `json:"display:order,omitempty"` + Icon string `json:"icon,omitempty"` + Color string `json:"color,omitempty"` + Label string `json:"label,omitempty"` + Description string `json:"description,omitempty"` + BlockDef waveobj.BlockDef `json:"blockdef"` } type MimeTypeConfigType struct { @@ -45,16 +298,6 @@ type MimeTypeConfigType struct { Color string `json:"color"` } -type BlockHeaderOpts struct { - ShowBlockIds bool `json:"showblockids"` -} - -type AutoUpdateOpts struct { - Enabled bool `json:"enabled"` - IntervalMs uint32 `json:"intervalms"` - InstallOnQuit bool `json:"installonquit"` -} - type TermThemeType struct { Black string `json:"black"` Red string `json:"red"` @@ -79,275 +322,3 @@ type TermThemeType struct { Background string `json:"background"` CursorAccent string `json:"cursorAccent"` } - -type TermThemesConfigType map[string]TermThemeType - -// TODO add default term theme settings - -// note we pointers so we preserve nulls -type WindowSettingsType struct { - Transparent *bool `json:"transparent"` - Blur *bool `json:"blur"` - Opacity *float64 `json:"opacity"` - BgColor *string `json:"bgcolor"` - ReducedMotion *bool `json:"reducedmotion"` -} - -type TelemetrySettingsType struct { - Enabled *bool `json:"enabled"` -} - -type SettingsConfigType struct { - MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"` - Term TerminalConfigType `json:"term"` - Ai *AiConfigType `json:"ai"` - DefaultWidgets []WidgetsConfigType `json:"defaultwidgets"` - Widgets []WidgetsConfigType `json:"widgets"` - WidgetShowHelp *bool `json:"widget:showhelp"` - BlockHeader BlockHeaderOpts `json:"blockheader"` - AutoUpdate *AutoUpdateOpts `json:"autoupdate"` - TermThemes TermThemesConfigType `json:"termthemes"` - WindowSettings WindowSettingsType `json:"window"` - Web WebConfigType `json:"web"` - Telemetry *TelemetrySettingsType `json:"telemetry"` - Presets map[string]*waveobj.MetaMapType `json:"presets,omitempty"` -} - -var DefaultTermDarkTheme = TermThemeType{ - Black: "#757575", - Red: "#cc685c", - Green: "#76c266", - Yellow: "#cbca9b", - Blue: "#85aacb", - Magenta: "#cc72ca", - Cyan: "#74a7cb", - White: "#c1c1c1", - BrightBlack: "#727272", - BrightRed: "#cc9d97", - BrightGreen: "#a3dd97", - BrightYellow: "#cbcaaa", - BrightBlue: "#9ab6cb", - BrightMagenta: "#cc8ecb", - BrightCyan: "#b7b8cb", - BrightWhite: "#f0f0f0", - Gray: "#8b918a", - CmdText: "#f0f0f0", - Foreground: "#c1c1c1", - SelectionBackground: "", - Background: "#00000077", - CursorAccent: "", -} - -var DraculaTheme = TermThemeType{ - Black: "#21222C", // AnsiBlack - Red: "#FF5555", // AnsiRed - Green: "#50FA7B", // AnsiGreen - Yellow: "#F1FA8C", // AnsiYellow - Blue: "#BD93F9", // AnsiBlue - Magenta: "#FF79C6", // AnsiMagenta - Cyan: "#8BE9FD", // AnsiCyan - White: "#F8F8F2", // AnsiWhite - BrightBlack: "#6272A4", // AnsiBrightBlack - BrightRed: "#FF6E6E", // AnsiBrightRed - BrightGreen: "#69FF94", // AnsiBrightGreen - BrightYellow: "#FFFFA5", // AnsiBrightYellow - BrightBlue: "#D6ACFF", // AnsiBrightBlue - BrightMagenta: "#FF92DF", // AnsiBrightMagenta - BrightCyan: "#A4FFFF", // AnsiBrightCyan - BrightWhite: "#FFFFFF", // AnsiBrightWhite - Gray: "#6272A4", // Comment or closest approximation - CmdText: "#F8F8F2", // Foreground - Foreground: "#F8F8F2", // Foreground - SelectionBackground: "#44475a", // Selection - Background: "#282a36", // Background - CursorAccent: "#f8f8f2", // Foreground (used for cursor accent) -} - -var CampbellTheme = TermThemeType{ - Black: "#0C0C0C", // Black - Red: "#C50F1F", // Red - Green: "#13A10E", // Green - Yellow: "#C19C00", // Yellow - Blue: "#0037DA", // Blue - Magenta: "#881798", // Purple (used as Magenta) - Cyan: "#3A96DD", // Cyan - White: "#CCCCCC", // White - BrightBlack: "#767676", // BrightBlack - BrightRed: "#E74856", // BrightRed - BrightGreen: "#16C60C", // BrightGreen - BrightYellow: "#F9F1A5", // BrightYellow - BrightBlue: "#3B78FF", // BrightBlue - BrightMagenta: "#B4009E", // BrightPurple (used as BrightMagenta) - BrightCyan: "#61D6D6", // BrightCyan - BrightWhite: "#F2F2F2", // BrightWhite - Gray: "#767676", // BrightBlack or closest approximation - CmdText: "#CCCCCC", // Foreground - Foreground: "#CCCCCC", // Foreground - SelectionBackground: "#3A96DD", // Cyan (chosen for selection background) - Background: "#0C0C0C", // Background - CursorAccent: "#CCCCCC", // Foreground (used for cursor accent) -} - -var BgDefaultPreset = waveobj.MetaMapType{ - waveobj.MetaKey_DisplayName: "Default", - waveobj.MetaKey_DisplayOrder: -1, - waveobj.MetaKey_BgClear: true, -} - -var BgRainbowPreset = waveobj.MetaMapType{ - waveobj.MetaKey_DisplayName: "Rainbow", - waveobj.MetaKey_DisplayOrder: 1, - waveobj.MetaKey_BgClear: true, - waveobj.MetaKey_Bg: "linear-gradient( 226.4deg, rgba(255,26,1,1) 28.9%, rgba(254,155,1,1) 33%, rgba(255,241,0,1) 48.6%, rgba(34,218,1,1) 65.3%, rgba(0,141,254,1) 80.6%, rgba(113,63,254,1) 100.1% );", - waveobj.MetaKey_BgOpacity: 0.3, -} - -var BgGreenPreset = waveobj.MetaMapType{ - waveobj.MetaKey_DisplayName: "Green", - waveobj.MetaKey_BgClear: true, - waveobj.MetaKey_Bg: "green", - waveobj.MetaKey_BgOpacity: 0.3, -} - -var BgBluePreset = waveobj.MetaMapType{ - waveobj.MetaKey_DisplayName: "Blue", - waveobj.MetaKey_BgClear: true, - waveobj.MetaKey_Bg: "blue", - waveobj.MetaKey_BgOpacity: 0.3, -} - -var BgRedPreset = waveobj.MetaMapType{ - waveobj.MetaKey_DisplayName: "Red", - waveobj.MetaKey_BgClear: true, - waveobj.MetaKey_Bg: "red", - waveobj.MetaKey_BgOpacity: 0.3, -} - -func applyDefaultSettings(settings *SettingsConfigType) { - defaultMimeTypes := map[string]MimeTypeConfigType{ - "audio": {Icon: "file-audio"}, - "application/pdf": {Icon: "file-pdf"}, - "application/json": {Icon: "file-lines"}, - "directory": {Icon: "folder", Color: "var(--term-bright-blue)"}, - "font": {Icon: "book-font"}, - "image": {Icon: "file-image"}, - "text": {Icon: "file-lines"}, - "text/css": {Icon: "css3-alt fa-brands"}, - "text/javascript": {Icon: "js fa-brands"}, - "text/typescript": {Icon: "js fa-brands"}, - "text/golang": {Icon: "golang fa-brands"}, - "text/html": {Icon: "html5 fa-brands"}, - "text/less": {Icon: "less fa-brands"}, - "text/markdown": {Icon: "markdown fa-brands"}, - "text/rust": {Icon: "rust fa-brands"}, - "text/scss": {Icon: "sass fa-brands"}, - "video": {Icon: "file-video"}, - "text/csv": {Icon: "file-csv"}, - } - if settings.MimeTypes == nil { - settings.MimeTypes = defaultMimeTypes - } else { - for k, v := range defaultMimeTypes { - if _, found := settings.MimeTypes[k]; !found { - settings.MimeTypes[k] = v - } - } - } - if settings.AutoUpdate == nil { - settings.AutoUpdate = &AutoUpdateOpts{ - Enabled: true, - InstallOnQuit: true, - IntervalMs: 3600000, - } - } - if settings.Ai == nil { - settings.Ai = &AiConfigType{ - Model: "gpt-3.5-turbo", - MaxTokens: 1000, - TimeoutMs: 10 * 1000, - } - } - defaultWidgets := []WidgetsConfigType{ - { - Icon: "square-terminal", - Label: "terminal", - BlockDef: waveobj.BlockDef{ - Meta: map[string]any{ - waveobj.MetaKey_View: "term", - waveobj.MetaKey_Controller: "shell", - }, - }, - }, - { - Icon: "folder", - Label: "files", - BlockDef: waveobj.BlockDef{ - Meta: map[string]any{ - waveobj.MetaKey_View: "preview", - waveobj.MetaKey_File: "~", - }, - }, - }, - { - Icon: "globe", - Label: "web", - BlockDef: waveobj.BlockDef{ - Meta: map[string]any{ - waveobj.MetaKey_View: "web", - waveobj.MetaKey_Url: "https://waveterm.dev/", - }, - }, - }, - { - Icon: "sparkles", - Label: "waveai", - BlockDef: waveobj.BlockDef{ - Meta: map[string]any{ - waveobj.MetaKey_View: "waveai", - }, - }, - }, - { - Icon: "chart-line", - Label: "cpu", - BlockDef: waveobj.BlockDef{ - Meta: map[string]any{ - waveobj.MetaKey_View: "cpuplot", - }, - }, - }, - } - if settings.DefaultWidgets == nil { - settings.DefaultWidgets = defaultWidgets - } - if settings.TermThemes == nil { - settings.TermThemes = make(map[string]TermThemeType) - } - if _, found := settings.TermThemes["default-dark"]; !found { - settings.TermThemes["default-dark"] = DefaultTermDarkTheme - } - if _, found := settings.TermThemes["dracula"]; !found { - settings.TermThemes["dracula"] = DraculaTheme - } - if _, found := settings.TermThemes["campbell"]; !found { - settings.TermThemes["campbell"] = CampbellTheme - } - if settings.Presets == nil { - settings.Presets = make(map[string]*waveobj.MetaMapType) - } - if _, found := settings.Presets["bg@default"]; !found { - settings.Presets["bg@default"] = &BgDefaultPreset - } - if _, found := settings.Presets["bg@rainbow"]; !found { - settings.Presets["bg@rainbow"] = &BgRainbowPreset - } - if _, found := settings.Presets["bg@green"]; !found { - settings.Presets["bg@green"] = &BgGreenPreset - } - if _, found := settings.Presets["bg@blue"]; !found { - settings.Presets["bg@blue"] = &BgBluePreset - } - if _, found := settings.Presets["bg@red"]; !found { - settings.Presets["bg@red"] = &BgRedPreset - } -} diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index 1c06237c7..f2e9c8be3 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -1,7 +1,7 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -// generated by cmd/generatewshclient/main-generatewshclient.go +// Generated Code. DO NOT EDIT. package wshclient @@ -13,171 +13,171 @@ import ( // command "announce", wshserver.AnnounceCommand func AnnounceCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "announce", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "announce", data, opts) + return err } // command "authenticate", wshserver.AuthenticateCommand func AuthenticateCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) (wshrpc.CommandAuthenticateRtnData, error) { - resp, err := sendRpcRequestCallHelper[wshrpc.CommandAuthenticateRtnData](w, "authenticate", data, opts) - return resp, err + resp, err := sendRpcRequestCallHelper[wshrpc.CommandAuthenticateRtnData](w, "authenticate", data, opts) + return resp, err } // command "controllerinput", wshserver.ControllerInputCommand func ControllerInputCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockInputData, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "controllerinput", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "controllerinput", data, opts) + return err } // command "controllerrestart", wshserver.ControllerRestartCommand func ControllerRestartCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockRestartData, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "controllerrestart", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "controllerrestart", data, opts) + return err } // command "createblock", wshserver.CreateBlockCommand func CreateBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandCreateBlockData, opts *wshrpc.RpcOpts) (waveobj.ORef, error) { - resp, err := sendRpcRequestCallHelper[waveobj.ORef](w, "createblock", data, opts) - return resp, err + resp, err := sendRpcRequestCallHelper[waveobj.ORef](w, "createblock", data, opts) + return resp, err } // command "deleteblock", wshserver.DeleteBlockCommand func DeleteBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandDeleteBlockData, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "deleteblock", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "deleteblock", data, opts) + return err } // command "eventpublish", wshserver.EventPublishCommand func EventPublishCommand(w *wshutil.WshRpc, data wshrpc.WaveEvent, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "eventpublish", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "eventpublish", data, opts) + return err } // command "eventrecv", wshserver.EventRecvCommand func EventRecvCommand(w *wshutil.WshRpc, data wshrpc.WaveEvent, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "eventrecv", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "eventrecv", data, opts) + return err } // command "eventsub", wshserver.EventSubCommand func EventSubCommand(w *wshutil.WshRpc, data wshrpc.SubscriptionRequest, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "eventsub", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "eventsub", data, opts) + return err } // command "eventunsub", wshserver.EventUnsubCommand func EventUnsubCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "eventunsub", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "eventunsub", data, opts) + return err } // command "eventunsuball", wshserver.EventUnsubAllCommand func EventUnsubAllCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "eventunsuball", nil, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "eventunsuball", nil, opts) + return err } // command "fileappend", wshserver.FileAppendCommand func FileAppendCommand(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "fileappend", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "fileappend", data, opts) + return err } // command "fileappendijson", wshserver.FileAppendIJsonCommand func FileAppendIJsonCommand(w *wshutil.WshRpc, data wshrpc.CommandAppendIJsonData, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "fileappendijson", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "fileappendijson", data, opts) + return err } // command "fileread", wshserver.FileReadCommand func FileReadCommand(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.RpcOpts) (string, error) { - resp, err := sendRpcRequestCallHelper[string](w, "fileread", data, opts) - return resp, err + resp, err := sendRpcRequestCallHelper[string](w, "fileread", data, opts) + return resp, err } // command "filewrite", wshserver.FileWriteCommand func FileWriteCommand(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "filewrite", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "filewrite", data, opts) + return err } // command "getmeta", wshserver.GetMetaCommand func GetMetaCommand(w *wshutil.WshRpc, data wshrpc.CommandGetMetaData, opts *wshrpc.RpcOpts) (waveobj.MetaMapType, error) { - resp, err := sendRpcRequestCallHelper[waveobj.MetaMapType](w, "getmeta", data, opts) - return resp, err + resp, err := sendRpcRequestCallHelper[waveobj.MetaMapType](w, "getmeta", data, opts) + return resp, err } // command "message", wshserver.MessageCommand func MessageCommand(w *wshutil.WshRpc, data wshrpc.CommandMessageData, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "message", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "message", data, opts) + return err } // command "remotefiledelete", wshserver.RemoteFileDeleteCommand func RemoteFileDeleteCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "remotefiledelete", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "remotefiledelete", data, opts) + return err } // command "remotefileinfo", wshserver.RemoteFileInfoCommand func RemoteFileInfoCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) (*wshrpc.FileInfo, error) { - resp, err := sendRpcRequestCallHelper[*wshrpc.FileInfo](w, "remotefileinfo", data, opts) - return resp, err + resp, err := sendRpcRequestCallHelper[*wshrpc.FileInfo](w, "remotefileinfo", data, opts) + return resp, err } // command "remotestreamcpudata", wshserver.RemoteStreamCpuDataCommand func RemoteStreamCpuDataCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[wshrpc.TimeSeriesData] { - return sendRpcRequestResponseStreamHelper[wshrpc.TimeSeriesData](w, "remotestreamcpudata", nil, opts) + return sendRpcRequestResponseStreamHelper[wshrpc.TimeSeriesData](w, "remotestreamcpudata", nil, opts) } // command "remotestreamfile", wshserver.RemoteStreamFileCommand func RemoteStreamFileCommand(w *wshutil.WshRpc, data wshrpc.CommandRemoteStreamFileData, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[wshrpc.CommandRemoteStreamFileRtnData] { - return sendRpcRequestResponseStreamHelper[wshrpc.CommandRemoteStreamFileRtnData](w, "remotestreamfile", data, opts) + return sendRpcRequestResponseStreamHelper[wshrpc.CommandRemoteStreamFileRtnData](w, "remotestreamfile", data, opts) } // command "remotewritefile", wshserver.RemoteWriteFileCommand func RemoteWriteFileCommand(w *wshutil.WshRpc, data wshrpc.CommandRemoteWriteFileData, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "remotewritefile", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "remotewritefile", data, opts) + return err } // command "resolveids", wshserver.ResolveIdsCommand func ResolveIdsCommand(w *wshutil.WshRpc, data wshrpc.CommandResolveIdsData, opts *wshrpc.RpcOpts) (wshrpc.CommandResolveIdsRtnData, error) { - resp, err := sendRpcRequestCallHelper[wshrpc.CommandResolveIdsRtnData](w, "resolveids", data, opts) - return resp, err + resp, err := sendRpcRequestCallHelper[wshrpc.CommandResolveIdsRtnData](w, "resolveids", data, opts) + return resp, err } // command "setmeta", wshserver.SetMetaCommand func SetMetaCommand(w *wshutil.WshRpc, data wshrpc.CommandSetMetaData, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "setmeta", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "setmeta", data, opts) + return err } // command "setview", wshserver.SetViewCommand func SetViewCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockSetViewData, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "setview", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "setview", data, opts) + return err } // command "streamcpudata", wshserver.StreamCpuDataCommand func StreamCpuDataCommand(w *wshutil.WshRpc, data wshrpc.CpuDataRequest, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[wshrpc.TimeSeriesData] { - return sendRpcRequestResponseStreamHelper[wshrpc.TimeSeriesData](w, "streamcpudata", data, opts) + return sendRpcRequestResponseStreamHelper[wshrpc.TimeSeriesData](w, "streamcpudata", data, opts) } // command "streamtest", wshserver.StreamTestCommand func StreamTestCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[int] { - return sendRpcRequestResponseStreamHelper[int](w, "streamtest", nil, opts) + return sendRpcRequestResponseStreamHelper[int](w, "streamtest", nil, opts) } // command "streamwaveai", wshserver.StreamWaveAiCommand func StreamWaveAiCommand(w *wshutil.WshRpc, data wshrpc.OpenAiStreamRequest, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType] { - return sendRpcRequestResponseStreamHelper[wshrpc.OpenAIPacketType](w, "streamwaveai", data, opts) + return sendRpcRequestResponseStreamHelper[wshrpc.OpenAIPacketType](w, "streamwaveai", data, opts) } // command "test", wshserver.TestCommand func TestCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { - _, err := sendRpcRequestCallHelper[any](w, "test", data, opts) - return err + _, err := sendRpcRequestCallHelper[any](w, "test", data, opts) + return err } diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index e46c24edf..c767fb53f 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -181,7 +181,7 @@ func UpdateObjectMeta(ctx context.Context, oref waveobj.ORef, meta waveobj.MetaM if objMeta == nil { objMeta = make(map[string]any) } - newMeta := waveobj.MergeMeta(objMeta, meta) + newMeta := waveobj.MergeMeta(objMeta, meta, false) waveobj.SetMeta(obj, newMeta) DBUpdate(tx.Context(), obj) return nil