diff --git a/cmd/generatego/main-generatego.go b/cmd/generatego/main-generatego.go index f127b70ba..885a34093 100644 --- a/cmd/generatego/main-generatego.go +++ b/cmd/generatego/main-generatego.go @@ -27,10 +27,8 @@ func GenerateWshClient() error { "github.com/wavetermdev/waveterm/pkg/wshutil", "github.com/wavetermdev/waveterm/pkg/wshrpc", "github.com/wavetermdev/waveterm/pkg/waveobj", - "github.com/wavetermdev/waveterm/pkg/wconfig", "github.com/wavetermdev/waveterm/pkg/wps", "github.com/wavetermdev/waveterm/pkg/vdom", - "github.com/wavetermdev/waveterm/pkg/telemetry", }) wshDeclMap := wshrpc.GenerateWshCommandDeclMap() for _, key := range utilfn.GetOrderedMapKeys(wshDeclMap) { diff --git a/cmd/server/main-server.go b/cmd/server/main-server.go index f5a5442d9..f433c6015 100644 --- a/cmd/server/main-server.go +++ b/cmd/server/main-server.go @@ -29,7 +29,6 @@ import ( "github.com/wavetermdev/waveterm/pkg/wconfig" "github.com/wavetermdev/waveterm/pkg/wcore" "github.com/wavetermdev/waveterm/pkg/web" - "github.com/wavetermdev/waveterm/pkg/wlayout" "github.com/wavetermdev/waveterm/pkg/wps" "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshremote" @@ -113,7 +112,7 @@ func telemetryLoop() { } func panicTelemetryHandler() { - activity := telemetry.ActivityUpdate{NumPanics: 1} + activity := wshrpc.ActivityUpdate{NumPanics: 1} err := telemetry.UpdateActivity(context.Background(), activity) if err != nil { log.Printf("error updating activity (panicTelemetryHandler): %v\n", err) @@ -137,7 +136,7 @@ func sendTelemetryWrapper() { } func beforeSendActivityUpdate(ctx context.Context) { - activity := telemetry.ActivityUpdate{} + activity := wshrpc.ActivityUpdate{} activity.NumTabs, _ = wstore.DBGetCount[*waveobj.Tab](ctx) activity.NumBlocks, _ = wstore.DBGetCount[*waveobj.Block](ctx) activity.Blocks, _ = wstore.DBGetBlockViewCounts(ctx) @@ -153,7 +152,7 @@ func beforeSendActivityUpdate(ctx context.Context) { func startupActivityUpdate() { ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - activity := telemetry.ActivityUpdate{Startup: 1} + activity := wshrpc.ActivityUpdate{Startup: 1} err := telemetry.UpdateActivity(ctx, activity) // set at least one record into activity (don't use go routine wrap here) if err != nil { log.Printf("error updating startup activity: %v\n", err) @@ -163,7 +162,7 @@ func startupActivityUpdate() { func shutdownActivityUpdate() { ctx, cancelFn := context.WithTimeout(context.Background(), 1*time.Second) defer cancelFn() - activity := telemetry.ActivityUpdate{Shutdown: 1} + activity := wshrpc.ActivityUpdate{Shutdown: 1} err := telemetry.UpdateActivity(ctx, activity) // do NOT use the go routine wrap here (this needs to be synchronous) if err != nil { log.Printf("error updating shutdown activity: %v\n", err) @@ -276,7 +275,7 @@ func main() { log.Printf("error initializing wsh and shell-integration files: %v\n", err) } }() - window, firstRun, err := wcore.EnsureInitialData() + err = wcore.EnsureInitialData() if err != nil { log.Printf("error ensuring initial data: %v\n", err) return @@ -286,23 +285,7 @@ func main() { log.Printf("error clearing temp files: %v\n", err) return } - if firstRun { - migrateErr := wstore.TryMigrateOldHistory() - if migrateErr != nil { - log.Printf("error migrating old history: %v\n", migrateErr) - } - } - if window != nil { - ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) - defer cancelFn() - if !firstRun { - err = wlayout.BootstrapNewWindowLayout(ctx, window) - if err != nil { - log.Panicf("error applying new window layout: %v\n", err) - return - } - } - } + createMainWshClient() installShutdownSignalHandlers() startupActivityUpdate() diff --git a/cmd/wsh/cmd/wshcmd-conn.go b/cmd/wsh/cmd/wshcmd-conn.go index bdfebcb55..52d76ef67 100644 --- a/cmd/wsh/cmd/wshcmd-conn.go +++ b/cmd/wsh/cmd/wshcmd-conn.go @@ -167,7 +167,7 @@ func connConnectRun(cmd *cobra.Command, args []string) error { if err := validateConnectionName(connName); err != nil { return err } - err := wshclient.ConnConnectCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000}) + err := wshclient.ConnConnectCommand(RpcClient, wshrpc.ConnRequest{Host: connName}, &wshrpc.RpcOpts{Timeout: 60000}) if err != nil { return fmt.Errorf("connecting connection: %w", err) } diff --git a/cmd/wsh/cmd/wshcmd-getvar.go b/cmd/wsh/cmd/wshcmd-getvar.go index 36740704c..dfbb428b8 100644 --- a/cmd/wsh/cmd/wshcmd-getvar.go +++ b/cmd/wsh/cmd/wshcmd-getvar.go @@ -30,6 +30,8 @@ var ( getVarAllVars bool getVarNullTerminate bool getVarLocal bool + getVarFlagNL bool + getVarFlagNoNL bool ) func init() { @@ -38,6 +40,19 @@ func init() { getVarCmd.Flags().BoolVar(&getVarAllVars, "all", false, "get all variables") getVarCmd.Flags().BoolVarP(&getVarNullTerminate, "null", "0", false, "use null terminators in output") getVarCmd.Flags().BoolVarP(&getVarLocal, "local", "l", false, "get variables local to block") + getVarCmd.Flags().BoolVarP(&getVarFlagNL, "newline", "n", false, "print newline after output") + getVarCmd.Flags().BoolVarP(&getVarFlagNoNL, "no-newline", "N", false, "do not print newline after output") +} + +func shouldPrintNewline() bool { + isTty := getIsTty() + if getVarFlagNL { + return true + } + if getVarFlagNoNL { + return false + } + return isTty } func getVarRun(cmd *cobra.Command, args []string) error { @@ -87,7 +102,11 @@ func getVarRun(cmd *cobra.Command, args []string) error { return nil } - WriteStdout("%s\n", resp.Val) + WriteStdout("%s", resp.Val) + if shouldPrintNewline() { + WriteStdout("\n") + } + return nil } diff --git a/cmd/wsh/cmd/wshcmd-root.go b/cmd/wsh/cmd/wshcmd-root.go index 8ba14b556..eaa56111c 100644 --- a/cmd/wsh/cmd/wshcmd-root.go +++ b/cmd/wsh/cmd/wshcmd-root.go @@ -57,6 +57,13 @@ func preRunSetupRpcClient(cmd *cobra.Command, args []string) error { return nil } +func getIsTty() bool { + if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 { + return true + } + return false +} + type RunEFnType = func(*cobra.Command, []string) error func activityWrap(activityStr string, origRunE RunEFnType) RunEFnType { diff --git a/cmd/wsh/cmd/wshcmd-setconfig.go b/cmd/wsh/cmd/wshcmd-setconfig.go index b076d89bb..77fd84e70 100644 --- a/cmd/wsh/cmd/wshcmd-setconfig.go +++ b/cmd/wsh/cmd/wshcmd-setconfig.go @@ -7,7 +7,6 @@ import ( "fmt" "github.com/spf13/cobra" - "github.com/wavetermdev/waveterm/pkg/wconfig" "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" ) @@ -34,7 +33,7 @@ func setConfigRun(cmd *cobra.Command, args []string) (rtnErr error) { if err != nil { return err } - commandData := wconfig.MetaSettingsType{MetaMapType: meta} + commandData := wshrpc.MetaSettingsType{MetaMapType: meta} err = wshclient.SetConfigCommand(RpcClient, commandData, &wshrpc.RpcOpts{Timeout: 2000}) if err != nil { return fmt.Errorf("setting config: %w", err) diff --git a/cmd/wsh/cmd/wshcmd-ssh.go b/cmd/wsh/cmd/wshcmd-ssh.go index daa7cdf84..51833fc2a 100644 --- a/cmd/wsh/cmd/wshcmd-ssh.go +++ b/cmd/wsh/cmd/wshcmd-ssh.go @@ -12,6 +12,8 @@ import ( "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" ) +var identityFiles []string + var sshCmd = &cobra.Command{ Use: "ssh", Short: "connect this terminal to a remote host", @@ -21,6 +23,7 @@ var sshCmd = &cobra.Command{ } func init() { + sshCmd.Flags().StringArrayVarP(&identityFiles, "identityfile", "i", []string{}, "add an identity file for publickey authentication") rootCmd.AddCommand(sshCmd) } @@ -34,6 +37,16 @@ func sshRun(cmd *cobra.Command, args []string) (rtnErr error) { if blockId == "" { return fmt.Errorf("cannot determine blockid (not in JWT)") } + // first, make a connection independent of the block + connOpts := wshrpc.ConnRequest{ + Host: sshArg, + Keywords: wshrpc.ConnKeywords{ + SshIdentityFile: identityFiles, + }, + } + wshclient.ConnConnectCommand(RpcClient, connOpts, nil) + + // now, with that made, it will be straightforward to connect data := wshrpc.CommandSetMetaData{ ORef: waveobj.MakeORef(waveobj.OType_Block, blockId), Meta: map[string]any{ diff --git a/cmd/wsh/cmd/wshcmd-web.go b/cmd/wsh/cmd/wshcmd-web.go index a825a0d46..c3d108dbc 100644 --- a/cmd/wsh/cmd/wshcmd-web.go +++ b/cmd/wsh/cmd/wshcmd-web.go @@ -63,10 +63,10 @@ func webGetRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("block %s is not a web block", fullORef.OID) } data := wshrpc.CommandWebSelectorData{ - WindowId: blockInfo.WindowId, - BlockId: fullORef.OID, - TabId: blockInfo.TabId, - Selector: args[0], + WorkspaceId: blockInfo.WorkspaceId, + BlockId: fullORef.OID, + TabId: blockInfo.TabId, + Selector: args[0], Opts: &wshrpc.WebSelectorOpts{ Inner: webGetInner, All: webGetAll, diff --git a/cmd/wsh/cmd/wshcmd-workspace.go b/cmd/wsh/cmd/wshcmd-workspace.go new file mode 100644 index 000000000..6bdcf63fd --- /dev/null +++ b/cmd/wsh/cmd/wshcmd-workspace.go @@ -0,0 +1,51 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/wavetermdev/waveterm/pkg/wshrpc" + "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" +) + +var workspaceCommand = &cobra.Command{ + Use: "workspace", + Short: "Manage workspaces", + // Args: cobra.MinimumNArgs(1), +} + +func init() { + workspaceCommand.AddCommand(workspaceListCommand) + rootCmd.AddCommand(workspaceCommand) +} + +var workspaceListCommand = &cobra.Command{ + Use: "list", + Short: "List workspaces", + Run: workspaceListRun, + PreRunE: preRunSetupRpcClient, +} + +func workspaceListRun(cmd *cobra.Command, args []string) { + workspaces, err := wshclient.WorkspaceListCommand(RpcClient, &wshrpc.RpcOpts{Timeout: 2000}) + if err != nil { + WriteStderr("Unable to list workspaces: %v\n", err) + return + } + + WriteStdout("[\n") + for i, w := range workspaces { + WriteStdout(" {\n \"windowId\": \"%s\",\n", w.WindowId) + WriteStderr(" \"workspaceId\": \"%s\",\n", w.WorkspaceData.OID) + WriteStdout(" \"name\": \"%s\",\n", w.WorkspaceData.Name) + WriteStdout(" \"icon\": \"%s\",\n", w.WorkspaceData.Icon) + WriteStdout(" \"color\": \"%s\"\n", w.WorkspaceData.Color) + if i < len(workspaces)-1 { + WriteStdout(" },\n") + } else { + WriteStdout(" }\n") + } + } + WriteStdout("]\n") +} diff --git a/db/migrations-wstore/000006_workspace.down.sql b/db/migrations-wstore/000006_workspace.down.sql new file mode 100644 index 000000000..25991a930 --- /dev/null +++ b/db/migrations-wstore/000006_workspace.down.sql @@ -0,0 +1,20 @@ +-- Step 1: Restore the $.activetabid field to db_window.data +UPDATE db_window +SET data = json_set( + db_window.data, + '$.activetabid', + (SELECT json_extract(db_workspace.data, '$.activetabid') + FROM db_workspace + WHERE db_workspace.oid = json_extract(db_window.data, '$.workspaceid')) +) +WHERE json_extract(data, '$.workspaceid') IN ( + SELECT oid FROM db_workspace +); + +-- Step 2: Remove the $.activetabid field from db_workspace.data +UPDATE db_workspace +SET data = json_remove(data, '$.activetabid') +WHERE oid IN ( + SELECT json_extract(db_window.data, '$.workspaceid') + FROM db_window +); diff --git a/db/migrations-wstore/000006_workspace.up.sql b/db/migrations-wstore/000006_workspace.up.sql new file mode 100644 index 000000000..a8f5f3314 --- /dev/null +++ b/db/migrations-wstore/000006_workspace.up.sql @@ -0,0 +1,18 @@ +-- Step 1: Update db_workspace.data to set the $.activetabid field +UPDATE db_workspace +SET data = json_set( + db_workspace.data, + '$.activetabid', + (SELECT json_extract(db_window.data, '$.activetabid')) +) +FROM db_window +WHERE db_workspace.oid IN ( + SELECT json_extract(db_window.data, '$.workspaceid') +); + +-- Step 2: Remove the $.activetabid field from db_window.data +UPDATE db_window +SET data = json_remove(data, '$.activetabid') +WHERE json_extract(data, '$.workspaceid') IN ( + SELECT oid FROM db_workspace +); diff --git a/docs/docs/config.mdx b/docs/docs/config.mdx index 62900ab17..83b9e625a 100644 --- a/docs/docs/config.mdx +++ b/docs/docs/config.mdx @@ -42,6 +42,7 @@ wsh editconfig | term:localshellpath | string | set to override the default shell path for local terminals | | term:localshellopts | string[] | set to pass additional parameters to the term:localshellpath | | term:copyonselect | bool | set to false to disable terminal copy-on-select | +| term:scrollback | int | size of terminal scrollback buffer, max is 10000 | | editor:minimapenabled | bool | set to false to disable editor minimap | | editor:stickscrollenabled | bool | | | web:openlinksinternally | bool | set to false to open web links in external browser | diff --git a/docs/package.json b/docs/package.json index 079027a3e..45320cbd3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -44,7 +44,7 @@ "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-mdx": "^3.1.5", - "prettier": "^3.3.3", + "prettier": "^3.4.1", "prettier-plugin-jsdoc": "^1.3.0", "prettier-plugin-organize-imports": "^4.1.0", "remark-cli": "^12.0.1", @@ -53,7 +53,7 @@ "remark-preset-lint-consistent": "^6.0.0", "remark-preset-lint-recommended": "^7.0.0", "typescript": "^5.7.2", - "typescript-eslint": "^8.15.0" + "typescript-eslint": "^8.16.0" }, "resolutions": { "path-to-regexp@npm:2.2.1": "^3", diff --git a/emain/emain-tabview.ts b/emain/emain-tabview.ts new file mode 100644 index 000000000..543276d92 --- /dev/null +++ b/emain/emain-tabview.ts @@ -0,0 +1,236 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { adaptFromElectronKeyEvent } from "@/util/keyutil"; +import { Rectangle, shell, WebContentsView } from "electron"; +import path from "path"; +import { configureAuthKeyRequestInjection } from "./authkey"; +import { setWasActive } from "./emain-activity"; +import { handleCtrlShiftFocus, handleCtrlShiftState, shFrameNavHandler, shNavHandler } from "./emain-util"; +import { waveWindowMap } from "./emain-window"; +import { getElectronAppBasePath, isDevVite } from "./platform"; + +function computeBgColor(fullConfig: FullConfigType): string { + const settings = fullConfig?.settings; + const isTransparent = settings?.["window:transparent"] ?? false; + const isBlur = !isTransparent && (settings?.["window:blur"] ?? false); + if (isTransparent) { + return "#00000000"; + } else if (isBlur) { + return "#00000000"; + } else { + return "#222222"; + } +} + +const wcIdToWaveTabMap = new Map(); + +export function getWaveTabViewByWebContentsId(webContentsId: number): WaveTabView { + return wcIdToWaveTabMap.get(webContentsId); +} + +export class WaveTabView extends WebContentsView { + isActiveTab: boolean; + waveWindowId: string; // set when showing in an active window + waveTabId: string; // always set, WaveTabViews are unique per tab + lastUsedTs: number; // ts milliseconds + createdTs: number; // ts milliseconds + initPromise: Promise; + savedInitOpts: WaveInitOpts; + waveReadyPromise: Promise; + initResolve: () => void; + waveReadyResolve: () => void; + + constructor(fullConfig: FullConfigType) { + console.log("createBareTabView"); + super({ + webPreferences: { + preload: path.join(getElectronAppBasePath(), "preload", "index.cjs"), + webviewTag: true, + }, + }); + this.createdTs = Date.now(); + this.savedInitOpts = null; + this.initPromise = new Promise((resolve, _) => { + this.initResolve = resolve; + }); + this.initPromise.then(() => { + console.log("tabview init", Date.now() - this.createdTs + "ms"); + }); + this.waveReadyPromise = new Promise((resolve, _) => { + this.waveReadyResolve = resolve; + }); + wcIdToWaveTabMap.set(this.webContents.id, this); + if (isDevVite) { + this.webContents.loadURL(`${process.env.ELECTRON_RENDERER_URL}/index.html}`); + } else { + this.webContents.loadFile(path.join(getElectronAppBasePath(), "frontend", "index.html")); + } + this.webContents.on("destroyed", () => { + wcIdToWaveTabMap.delete(this.webContents.id); + removeWaveTabView(this.waveTabId); + }); + this.setBackgroundColor(computeBgColor(fullConfig)); + } + + positionTabOnScreen(winBounds: Rectangle) { + const curBounds = this.getBounds(); + if ( + curBounds.width == winBounds.width && + curBounds.height == winBounds.height && + curBounds.x == 0 && + curBounds.y == 0 + ) { + return; + } + this.setBounds({ x: 0, y: 0, width: winBounds.width, height: winBounds.height }); + } + + positionTabOffScreen(winBounds: Rectangle) { + this.setBounds({ + x: -15000, + y: -15000, + width: winBounds.width, + height: winBounds.height, + }); + } + + isOnScreen() { + const bounds = this.getBounds(); + return bounds.x == 0 && bounds.y == 0; + } + + destroy() { + console.log("destroy tab", this.waveTabId); + this.webContents.close(); + removeWaveTabView(this.waveTabId); + + // TODO: circuitous + const waveWindow = waveWindowMap.get(this.waveWindowId); + if (waveWindow) { + waveWindow.allTabViews.delete(this.waveTabId); + } + } +} + +let MaxCacheSize = 10; +const wcvCache = new Map(); + +export function setMaxTabCacheSize(size: number) { + console.log("setMaxTabCacheSize", size); + MaxCacheSize = size; +} + +export function getWaveTabView(waveTabId: string): WaveTabView | undefined { + const rtn = wcvCache.get(waveTabId); + if (rtn) { + rtn.lastUsedTs = Date.now(); + } + return rtn; +} + +function checkAndEvictCache(): void { + if (wcvCache.size <= MaxCacheSize) { + return; + } + const sorted = Array.from(wcvCache.values()).sort((a, b) => { + // Prioritize entries which are active + if (a.isActiveTab && !b.isActiveTab) { + return -1; + } + // Otherwise, sort by lastUsedTs + return a.lastUsedTs - b.lastUsedTs; + }); + for (let i = 0; i < sorted.length - MaxCacheSize; i++) { + if (sorted[i].isActiveTab) { + // don't evict WaveTabViews that are currently showing in a window + continue; + } + const tabView = sorted[i]; + tabView?.destroy(); + } +} + +export function clearTabCache() { + const wcVals = Array.from(wcvCache.values()); + for (let i = 0; i < wcVals.length; i++) { + const tabView = wcVals[i]; + if (tabView.isActiveTab) { + continue; + } + tabView?.destroy(); + } +} + +// returns [tabview, initialized] +export function getOrCreateWebViewForTab(fullConfig: FullConfigType, tabId: string): [WaveTabView, boolean] { + let tabView = getWaveTabView(tabId); + if (tabView) { + return [tabView, true]; + } + tabView = getSpareTab(fullConfig); + tabView.lastUsedTs = Date.now(); + tabView.waveTabId = tabId; + setWaveTabView(tabId, tabView); + tabView.webContents.on("will-navigate", shNavHandler); + tabView.webContents.on("will-frame-navigate", shFrameNavHandler); + tabView.webContents.on("did-attach-webview", (event, wc) => { + wc.setWindowOpenHandler((details) => { + tabView.webContents.send("webview-new-window", wc.id, details); + return { action: "deny" }; + }); + }); + tabView.webContents.on("before-input-event", (e, input) => { + const waveEvent = adaptFromElectronKeyEvent(input); + // console.log("WIN bie", tabView.waveTabId.substring(0, 8), waveEvent.type, waveEvent.code); + handleCtrlShiftState(tabView.webContents, waveEvent); + setWasActive(true); + }); + tabView.webContents.on("zoom-changed", (e) => { + tabView.webContents.send("zoom-changed"); + }); + tabView.webContents.setWindowOpenHandler(({ url, frameName }) => { + if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) { + console.log("openExternal fallback", url); + shell.openExternal(url); + } + console.log("window-open denied", url); + return { action: "deny" }; + }); + tabView.webContents.on("blur", () => { + handleCtrlShiftFocus(tabView.webContents, false); + }); + configureAuthKeyRequestInjection(tabView.webContents.session); + return [tabView, false]; +} + +export function setWaveTabView(waveTabId: string, wcv: WaveTabView): void { + wcvCache.set(waveTabId, wcv); + checkAndEvictCache(); +} + +function removeWaveTabView(waveTabId: string): void { + wcvCache.delete(waveTabId); +} + +let HotSpareTab: WaveTabView = null; + +export function ensureHotSpareTab(fullConfig: FullConfigType) { + console.log("ensureHotSpareTab"); + if (HotSpareTab == null) { + HotSpareTab = new WaveTabView(fullConfig); + } +} + +export function getSpareTab(fullConfig: FullConfigType): WaveTabView { + setTimeout(ensureHotSpareTab, 500); + if (HotSpareTab != null) { + const rtn = HotSpareTab; + HotSpareTab = null; + console.log("getSpareTab: returning hotspare"); + return rtn; + } else { + console.log("getSpareTab: creating new tab"); + return new WaveTabView(fullConfig); + } +} diff --git a/emain/emain-viewmgr.ts b/emain/emain-viewmgr.ts deleted file mode 100644 index ef47f7cca..000000000 --- a/emain/emain-viewmgr.ts +++ /dev/null @@ -1,636 +0,0 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import * as electron from "electron"; -import * as path from "path"; -import { debounce } from "throttle-debounce"; -import { ClientService, FileService, ObjectService, WindowService } from "../frontend/app/store/services"; -import * as keyutil from "../frontend/util/keyutil"; -import { configureAuthKeyRequestInjection } from "./authkey"; -import { getGlobalIsQuitting, getGlobalIsRelaunching, setWasActive, setWasInFg } from "./emain-activity"; -import { - delay, - ensureBoundsAreVisible, - handleCtrlShiftFocus, - handleCtrlShiftState, - shFrameNavHandler, - shNavHandler, -} from "./emain-util"; -import { getElectronAppBasePath, isDevVite } from "./platform"; -import { updater } from "./updater"; - -let MaxCacheSize = 10; -let HotSpareTab: WaveTabView = null; - -const waveWindowMap = new Map(); // waveWindowId -> WaveBrowserWindow -let focusedWaveWindow = null; // on blur we do not set this to null (but on destroy we do) -const wcvCache = new Map(); -const wcIdToWaveTabMap = new Map(); -let tabSwitchQueue: { bwin: WaveBrowserWindow; tabView: WaveTabView; tabInitialized: boolean }[] = []; - -export function setMaxTabCacheSize(size: number) { - console.log("setMaxTabCacheSize", size); - MaxCacheSize = size; -} - -function computeBgColor(fullConfig: FullConfigType): string { - const settings = fullConfig?.settings; - const isTransparent = settings?.["window:transparent"] ?? false; - const isBlur = !isTransparent && (settings?.["window:blur"] ?? false); - if (isTransparent) { - return "#00000000"; - } else if (isBlur) { - return "#00000000"; - } else { - return "#222222"; - } -} - -function createBareTabView(fullConfig: FullConfigType): WaveTabView { - console.log("createBareTabView"); - const tabView = new electron.WebContentsView({ - webPreferences: { - preload: path.join(getElectronAppBasePath(), "preload", "index.cjs"), - webviewTag: true, - }, - }) as WaveTabView; - tabView.createdTs = Date.now(); - tabView.savedInitOpts = null; - tabView.initPromise = new Promise((resolve, _) => { - tabView.initResolve = resolve; - }); - tabView.initPromise.then(() => { - console.log("tabview init", Date.now() - tabView.createdTs + "ms"); - }); - tabView.waveReadyPromise = new Promise((resolve, _) => { - tabView.waveReadyResolve = resolve; - }); - wcIdToWaveTabMap.set(tabView.webContents.id, tabView); - if (isDevVite) { - tabView.webContents.loadURL(`${process.env.ELECTRON_RENDERER_URL}/index.html}`); - } else { - tabView.webContents.loadFile(path.join(getElectronAppBasePath(), "frontend", "index.html")); - } - tabView.webContents.on("destroyed", () => { - wcIdToWaveTabMap.delete(tabView.webContents.id); - removeWaveTabView(tabView.waveWindowId, tabView.waveTabId); - }); - tabView.setBackgroundColor(computeBgColor(fullConfig)); - return tabView; -} - -function positionTabOffScreen(tabView: WaveTabView, winBounds: Electron.Rectangle) { - if (tabView == null) { - return; - } - tabView.setBounds({ - x: -15000, - y: -15000, - width: winBounds.width, - height: winBounds.height, - }); -} - -async function repositionTabsSlowly(waveWindow: WaveBrowserWindow, delayMs: number) { - const activeTabView = waveWindow.activeTabView; - const winBounds = waveWindow.getContentBounds(); - if (activeTabView == null) { - return; - } - if (isOnScreen(activeTabView)) { - activeTabView.setBounds({ - x: 0, - y: 0, - width: winBounds.width, - height: winBounds.height, - }); - } else { - activeTabView.setBounds({ - x: winBounds.width - 10, - y: winBounds.height - 10, - width: winBounds.width, - height: winBounds.height, - }); - } - await delay(delayMs); - if (waveWindow.activeTabView != activeTabView) { - // another tab view has been set, do not finalize this layout - return; - } - finalizePositioning(waveWindow); -} - -function isOnScreen(tabView: WaveTabView) { - const bounds = tabView.getBounds(); - return bounds.x == 0 && bounds.y == 0; -} - -function finalizePositioning(waveWindow: WaveBrowserWindow) { - if (waveWindow.isDestroyed()) { - return; - } - const curBounds = waveWindow.getContentBounds(); - positionTabOnScreen(waveWindow.activeTabView, curBounds); - for (const tabView of waveWindow.allTabViews.values()) { - if (tabView == waveWindow.activeTabView) { - continue; - } - positionTabOffScreen(tabView, curBounds); - } -} - -function positionTabOnScreen(tabView: WaveTabView, winBounds: Electron.Rectangle) { - if (tabView == null) { - return; - } - const curBounds = tabView.getBounds(); - if ( - curBounds.width == winBounds.width && - curBounds.height == winBounds.height && - curBounds.x == 0 && - curBounds.y == 0 - ) { - return; - } - tabView.setBounds({ x: 0, y: 0, width: winBounds.width, height: winBounds.height }); -} - -export function getWaveTabViewByWebContentsId(webContentsId: number): WaveTabView { - return wcIdToWaveTabMap.get(webContentsId); -} - -export function getWaveWindowByWebContentsId(webContentsId: number): WaveBrowserWindow { - const tabView = wcIdToWaveTabMap.get(webContentsId); - if (tabView == null) { - return null; - } - return waveWindowMap.get(tabView.waveWindowId); -} - -export function getWaveWindowById(windowId: string): WaveBrowserWindow { - return waveWindowMap.get(windowId); -} - -export function getAllWaveWindows(): WaveBrowserWindow[] { - return Array.from(waveWindowMap.values()); -} - -export function getFocusedWaveWindow(): WaveBrowserWindow { - return focusedWaveWindow; -} - -export function ensureHotSpareTab(fullConfig: FullConfigType) { - console.log("ensureHotSpareTab"); - if (HotSpareTab == null) { - HotSpareTab = createBareTabView(fullConfig); - } -} - -export function destroyWindow(waveWindow: WaveBrowserWindow) { - if (waveWindow == null) { - return; - } - console.log("destroy win", waveWindow.waveWindowId); - for (const tabView of waveWindow.allTabViews.values()) { - destroyTab(tabView); - } - waveWindowMap.delete(waveWindow.waveWindowId); -} - -export function destroyTab(tabView: WaveTabView) { - if (tabView == null) { - return; - } - console.log("destroy tab", tabView.waveTabId); - tabView.webContents.close(); - wcIdToWaveTabMap.delete(tabView.webContents.id); - removeWaveTabView(tabView.waveWindowId, tabView.waveTabId); - const waveWindow = waveWindowMap.get(tabView.waveWindowId); - if (waveWindow) { - waveWindow.allTabViews.delete(tabView.waveTabId); - } -} - -function getSpareTab(fullConfig: FullConfigType): WaveTabView { - setTimeout(ensureHotSpareTab, 500); - if (HotSpareTab != null) { - const rtn = HotSpareTab; - HotSpareTab = null; - console.log("getSpareTab: returning hotspare"); - return rtn; - } else { - console.log("getSpareTab: creating new tab"); - return createBareTabView(fullConfig); - } -} - -function getWaveTabView(waveWindowId: string, waveTabId: string): WaveTabView | undefined { - const cacheKey = waveWindowId + "|" + waveTabId; - const rtn = wcvCache.get(cacheKey); - if (rtn) { - rtn.lastUsedTs = Date.now(); - } - return rtn; -} - -function setWaveTabView(waveWindowId: string, waveTabId: string, wcv: WaveTabView): void { - const cacheKey = waveWindowId + "|" + waveTabId; - wcvCache.set(cacheKey, wcv); - checkAndEvictCache(); -} - -function removeWaveTabView(waveWindowId: string, waveTabId: string): void { - const cacheKey = waveWindowId + "|" + waveTabId; - wcvCache.delete(cacheKey); -} - -function forceRemoveAllTabsForWindow(waveWindowId: string): void { - const keys = Array.from(wcvCache.keys()); - for (const key of keys) { - if (key.startsWith(waveWindowId)) { - wcvCache.delete(key); - } - } -} - -function checkAndEvictCache(): void { - if (wcvCache.size <= MaxCacheSize) { - return; - } - const sorted = Array.from(wcvCache.values()).sort((a, b) => { - // Prioritize entries which are active - if (a.isActiveTab && !b.isActiveTab) { - return -1; - } - // Otherwise, sort by lastUsedTs - return a.lastUsedTs - b.lastUsedTs; - }); - for (let i = 0; i < sorted.length - MaxCacheSize; i++) { - if (sorted[i].isActiveTab) { - // don't evict WaveTabViews that are currently showing in a window - continue; - } - const tabView = sorted[i]; - destroyTab(tabView); - } -} - -export function clearTabCache() { - const wcVals = Array.from(wcvCache.values()); - for (let i = 0; i < wcVals.length; i++) { - const tabView = wcVals[i]; - if (tabView.isActiveTab) { - continue; - } - destroyTab(tabView); - } -} - -// returns [tabview, initialized] -function getOrCreateWebViewForTab(fullConfig: FullConfigType, windowId: string, tabId: string): [WaveTabView, boolean] { - let tabView = getWaveTabView(windowId, tabId); - if (tabView) { - return [tabView, true]; - } - tabView = getSpareTab(fullConfig); - tabView.lastUsedTs = Date.now(); - tabView.waveTabId = tabId; - tabView.waveWindowId = windowId; - setWaveTabView(windowId, tabId, tabView); - tabView.webContents.on("will-navigate", shNavHandler); - tabView.webContents.on("will-frame-navigate", shFrameNavHandler); - tabView.webContents.on("did-attach-webview", (event, wc) => { - wc.setWindowOpenHandler((details) => { - tabView.webContents.send("webview-new-window", wc.id, details); - return { action: "deny" }; - }); - }); - tabView.webContents.on("before-input-event", (e, input) => { - const waveEvent = keyutil.adaptFromElectronKeyEvent(input); - // console.log("WIN bie", tabView.waveTabId.substring(0, 8), waveEvent.type, waveEvent.code); - handleCtrlShiftState(tabView.webContents, waveEvent); - setWasActive(true); - }); - tabView.webContents.on("zoom-changed", (e) => { - tabView.webContents.send("zoom-changed"); - }); - tabView.webContents.setWindowOpenHandler(({ url, frameName }) => { - if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) { - console.log("openExternal fallback", url); - electron.shell.openExternal(url); - } - console.log("window-open denied", url); - return { action: "deny" }; - }); - tabView.webContents.on("blur", () => { - handleCtrlShiftFocus(tabView.webContents, false); - }); - configureAuthKeyRequestInjection(tabView.webContents.session); - return [tabView, false]; -} - -async function mainResizeHandler(_: any, windowId: string, win: WaveBrowserWindow) { - if (win == null || win.isDestroyed() || win.fullScreen) { - return; - } - const bounds = win.getBounds(); - try { - await WindowService.SetWindowPosAndSize( - windowId, - { x: bounds.x, y: bounds.y }, - { width: bounds.width, height: bounds.height } - ); - } catch (e) { - console.log("error sending new window bounds to backend", e); - } -} - -type WindowOpts = { - unamePlatform: string; -}; - -function createBaseWaveBrowserWindow( - waveWindow: WaveWindow, - fullConfig: FullConfigType, - opts: WindowOpts -): WaveBrowserWindow { - console.log("create win", waveWindow.oid); - let winWidth = waveWindow?.winsize?.width; - let winHeight = waveWindow?.winsize?.height; - let winPosX = waveWindow.pos.x; - let winPosY = waveWindow.pos.y; - if (winWidth == null || winWidth == 0) { - const primaryDisplay = electron.screen.getPrimaryDisplay(); - const { width } = primaryDisplay.workAreaSize; - winWidth = width - winPosX - 100; - if (winWidth > 2000) { - winWidth = 2000; - } - } - if (winHeight == null || winHeight == 0) { - const primaryDisplay = electron.screen.getPrimaryDisplay(); - const { height } = primaryDisplay.workAreaSize; - winHeight = height - winPosY - 100; - if (winHeight > 1200) { - winHeight = 1200; - } - } - let winBounds = { - x: winPosX, - y: winPosY, - width: winWidth, - height: winHeight, - }; - winBounds = ensureBoundsAreVisible(winBounds); - const settings = fullConfig?.settings; - const winOpts: Electron.BaseWindowConstructorOptions = { - titleBarStyle: - opts.unamePlatform === "darwin" ? "hiddenInset" : settings["window:nativetitlebar"] ? "default" : "hidden", - titleBarOverlay: - opts.unamePlatform !== "darwin" - ? { - symbolColor: "white", - color: "#00000000", - } - : false, - x: winBounds.x, - y: winBounds.y, - width: winBounds.width, - height: winBounds.height, - minWidth: 400, - minHeight: 300, - icon: - opts.unamePlatform == "linux" - ? path.join(getElectronAppBasePath(), "public/logos/wave-logo-dark.png") - : undefined, - show: false, - autoHideMenuBar: !settings?.["window:showmenubar"], - }; - const isTransparent = settings?.["window:transparent"] ?? false; - const isBlur = !isTransparent && (settings?.["window:blur"] ?? false); - if (isTransparent) { - winOpts.transparent = true; - } else if (isBlur) { - switch (opts.unamePlatform) { - case "win32": { - winOpts.backgroundMaterial = "acrylic"; - break; - } - case "darwin": { - winOpts.vibrancy = "fullscreen-ui"; - break; - } - } - } else { - winOpts.backgroundColor = "#222222"; - } - const bwin = new electron.BaseWindow(winOpts); - const win: WaveBrowserWindow = bwin as WaveBrowserWindow; - win.waveWindowId = waveWindow.oid; - win.alreadyClosed = false; - win.allTabViews = new Map(); - const winBoundsPoller = setInterval(() => { - if (win.isDestroyed()) { - clearInterval(winBoundsPoller); - return; - } - if (tabSwitchQueue.length > 0) { - return; - } - finalizePositioning(win); - }, 1000); - win.on( - // @ts-expect-error - "resize", - debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win)) - ); - win.on("resize", () => { - if (win.isDestroyed()) { - return; - } - positionTabOnScreen(win.activeTabView, win.getContentBounds()); - }); - win.on( - // @ts-expect-error - "move", - debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win)) - ); - win.on("enter-full-screen", async () => { - console.log("enter-full-screen event", win.getContentBounds()); - const tabView = win.activeTabView; - if (tabView) { - tabView.webContents.send("fullscreen-change", true); - } - positionTabOnScreen(win.activeTabView, win.getContentBounds()); - }); - win.on("leave-full-screen", async () => { - const tabView = win.activeTabView; - if (tabView) { - tabView.webContents.send("fullscreen-change", false); - } - positionTabOnScreen(win.activeTabView, win.getContentBounds()); - }); - win.on("focus", () => { - if (getGlobalIsRelaunching()) { - return; - } - focusedWaveWindow = win; - console.log("focus win", win.waveWindowId); - ClientService.FocusWindow(win.waveWindowId); - setWasInFg(true); - setWasActive(true); - }); - win.on("blur", () => { - if (focusedWaveWindow == win) { - focusedWaveWindow = null; - } - }); - win.on("close", (e) => { - console.log("win 'close' handler fired", win.waveWindowId); - if (getGlobalIsQuitting() || updater?.status == "installing" || getGlobalIsRelaunching()) { - return; - } - const numWindows = waveWindowMap.size; - if (numWindows == 1) { - return; - } - const choice = electron.dialog.showMessageBoxSync(win, { - type: "question", - buttons: ["Cancel", "Yes"], - title: "Confirm", - message: "Are you sure you want to close this window (all tabs and blocks will be deleted)?", - }); - if (choice === 0) { - e.preventDefault(); - } else { - win.deleteAllowed = true; - } - }); - win.on("closed", () => { - console.log("win 'closed' handler fired", win.waveWindowId); - if (getGlobalIsQuitting() || updater?.status == "installing") { - return; - } - if (getGlobalIsRelaunching()) { - destroyWindow(win); - return; - } - const numWindows = waveWindowMap.size; - if (numWindows == 0) { - return; - } - if (!win.alreadyClosed && win.deleteAllowed) { - console.log("win removing window from backend DB", win.waveWindowId); - WindowService.CloseWindow(waveWindow.oid, true); - } - destroyWindow(win); - }); - waveWindowMap.set(waveWindow.oid, win); - return win; -} - -export function getLastFocusedWaveWindow(): WaveBrowserWindow { - return focusedWaveWindow; -} - -// note, this does not *show* the window. -// to show, await win.readyPromise and then win.show() -export function createBrowserWindow( - clientId: string, - waveWindow: WaveWindow, - fullConfig: FullConfigType, - opts: WindowOpts -): WaveBrowserWindow { - const bwin = createBaseWaveBrowserWindow(waveWindow, fullConfig, opts); - // TODO fix null activetabid if it exists - if (waveWindow.activetabid != null) { - setActiveTab(bwin, waveWindow.activetabid); - } - return bwin; -} - -export async function setActiveTab(waveWindow: WaveBrowserWindow, tabId: string) { - const windowId = waveWindow.waveWindowId; - await ObjectService.SetActiveTab(waveWindow.waveWindowId, tabId); - const fullConfig = await FileService.GetFullConfig(); - const [tabView, tabInitialized] = getOrCreateWebViewForTab(fullConfig, windowId, tabId); - queueTabSwitch(waveWindow, tabView, tabInitialized); -} - -export function queueTabSwitch(bwin: WaveBrowserWindow, tabView: WaveTabView, tabInitialized: boolean) { - if (tabSwitchQueue.length == 2) { - tabSwitchQueue[1] = { bwin, tabView, tabInitialized }; - return; - } - tabSwitchQueue.push({ bwin, tabView, tabInitialized }); - if (tabSwitchQueue.length == 1) { - processTabSwitchQueue(); - } -} - -async function processTabSwitchQueue() { - if (tabSwitchQueue.length == 0) { - tabSwitchQueue = []; - return; - } - try { - const { bwin, tabView, tabInitialized } = tabSwitchQueue[0]; - await setTabViewIntoWindow(bwin, tabView, tabInitialized); - } finally { - tabSwitchQueue.shift(); - processTabSwitchQueue(); - } -} - -async function setTabViewIntoWindow(bwin: WaveBrowserWindow, tabView: WaveTabView, tabInitialized: boolean) { - const clientData = await ClientService.GetClientData(); - if (bwin.activeTabView == tabView) { - return; - } - const oldActiveView = bwin.activeTabView; - tabView.isActiveTab = true; - if (oldActiveView != null) { - oldActiveView.isActiveTab = false; - } - bwin.activeTabView = tabView; - bwin.allTabViews.set(tabView.waveTabId, tabView); - if (!tabInitialized) { - console.log("initializing a new tab"); - await tabView.initPromise; - bwin.contentView.addChildView(tabView); - const initOpts = { - tabId: tabView.waveTabId, - clientId: clientData.oid, - windowId: bwin.waveWindowId, - activate: true, - }; - tabView.savedInitOpts = { ...initOpts }; - tabView.savedInitOpts.activate = false; - let startTime = Date.now(); - tabView.webContents.send("wave-init", initOpts); - console.log("before wave ready"); - await tabView.waveReadyPromise; - // positionTabOnScreen(tabView, bwin.getContentBounds()); - console.log("wave-ready init time", Date.now() - startTime + "ms"); - // positionTabOffScreen(oldActiveView, bwin.getContentBounds()); - await repositionTabsSlowly(bwin, 100); - } else { - console.log("reusing an existing tab"); - const p1 = repositionTabsSlowly(bwin, 35); - const p2 = tabView.webContents.send("wave-init", tabView.savedInitOpts); // reinit - await Promise.all([p1, p2]); - } - - // something is causing the new tab to lose focus so it requires manual refocusing - tabView.webContents.focus(); - setTimeout(() => { - if (bwin.activeTabView == tabView && !tabView.webContents.isFocused()) { - tabView.webContents.focus(); - } - }, 10); - setTimeout(() => { - if (bwin.activeTabView == tabView && !tabView.webContents.isFocused()) { - tabView.webContents.focus(); - } - }, 30); -} diff --git a/emain/emain-web.ts b/emain/emain-web.ts index f1fbd9aeb..9d16c4022 100644 --- a/emain/emain-web.ts +++ b/emain/emain-web.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { ipcMain, webContents, WebContents } from "electron"; +import { WaveBrowserWindow } from "./emain-window"; export function getWebContentsByBlockId(ww: WaveBrowserWindow, tabId: string, blockId: string): Promise { const prtn = new Promise((resolve, reject) => { diff --git a/emain/emain-window.ts b/emain/emain-window.ts new file mode 100644 index 000000000..ead2bfaff --- /dev/null +++ b/emain/emain-window.ts @@ -0,0 +1,569 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { ClientService, FileService, WindowService, WorkspaceService } from "@/app/store/services"; +import { fireAndForget } from "@/util/util"; +import { BaseWindow, BaseWindowConstructorOptions, dialog, ipcMain, screen } from "electron"; +import path from "path"; +import { debounce } from "throttle-debounce"; +import { getGlobalIsQuitting, getGlobalIsRelaunching, setWasActive, setWasInFg } from "./emain-activity"; +import { getOrCreateWebViewForTab, getWaveTabViewByWebContentsId, WaveTabView } from "./emain-tabview"; +import { delay, ensureBoundsAreVisible } from "./emain-util"; +import { getElectronAppBasePath, unamePlatform } from "./platform"; +import { updater } from "./updater"; +export type WindowOpts = { + unamePlatform: string; +}; + +export const waveWindowMap = new Map(); // waveWindowId -> WaveBrowserWindow +export let focusedWaveWindow = null; // on blur we do not set this to null (but on destroy we do) + +export class WaveBrowserWindow extends BaseWindow { + waveWindowId: string; + workspaceId: string; + waveReadyPromise: Promise; + allTabViews: Map; + activeTabView: WaveTabView; + private canClose: boolean; + private deleteAllowed: boolean; + private tabSwitchQueue: { tabView: WaveTabView; tabInitialized: boolean }[]; + + constructor(waveWindow: WaveWindow, fullConfig: FullConfigType, opts: WindowOpts) { + console.log("create win", waveWindow.oid); + let winWidth = waveWindow?.winsize?.width; + let winHeight = waveWindow?.winsize?.height; + let winPosX = waveWindow.pos.x; + let winPosY = waveWindow.pos.y; + if (winWidth == null || winWidth == 0) { + const primaryDisplay = screen.getPrimaryDisplay(); + const { width } = primaryDisplay.workAreaSize; + winWidth = width - winPosX - 100; + if (winWidth > 2000) { + winWidth = 2000; + } + } + if (winHeight == null || winHeight == 0) { + const primaryDisplay = screen.getPrimaryDisplay(); + const { height } = primaryDisplay.workAreaSize; + winHeight = height - winPosY - 100; + if (winHeight > 1200) { + winHeight = 1200; + } + } + let winBounds = { + x: winPosX, + y: winPosY, + width: winWidth, + height: winHeight, + }; + winBounds = ensureBoundsAreVisible(winBounds); + const settings = fullConfig?.settings; + const winOpts: BaseWindowConstructorOptions = { + titleBarStyle: + opts.unamePlatform === "darwin" + ? "hiddenInset" + : settings["window:nativetitlebar"] + ? "default" + : "hidden", + titleBarOverlay: + opts.unamePlatform !== "darwin" + ? { + symbolColor: "white", + color: "#00000000", + } + : false, + x: winBounds.x, + y: winBounds.y, + width: winBounds.width, + height: winBounds.height, + minWidth: 400, + minHeight: 300, + icon: + opts.unamePlatform == "linux" + ? path.join(getElectronAppBasePath(), "public/logos/wave-logo-dark.png") + : undefined, + show: false, + autoHideMenuBar: !settings?.["window:showmenubar"], + }; + const isTransparent = settings?.["window:transparent"] ?? false; + const isBlur = !isTransparent && (settings?.["window:blur"] ?? false); + if (isTransparent) { + winOpts.transparent = true; + } else if (isBlur) { + switch (opts.unamePlatform) { + case "win32": { + winOpts.backgroundMaterial = "acrylic"; + break; + } + case "darwin": { + winOpts.vibrancy = "fullscreen-ui"; + break; + } + } + } else { + winOpts.backgroundColor = "#222222"; + } + + super(winOpts); + this.tabSwitchQueue = []; + this.waveWindowId = waveWindow.oid; + this.workspaceId = waveWindow.workspaceid; + this.allTabViews = new Map(); + const winBoundsPoller = setInterval(() => { + if (this.isDestroyed()) { + clearInterval(winBoundsPoller); + return; + } + if (this.tabSwitchQueue.length > 0) { + return; + } + this.finalizePositioning(); + }, 1000); + this.on( + // @ts-expect-error + "resize", + debounce(400, (e) => this.mainResizeHandler(e)) + ); + this.on("resize", () => { + if (this.isDestroyed()) { + return; + } + this.activeTabView?.positionTabOnScreen(this.getContentBounds()); + }); + this.on( + // @ts-expect-error + "move", + debounce(400, (e) => this.mainResizeHandler(e)) + ); + this.on("enter-full-screen", async () => { + if (this.isDestroyed()) { + return; + } + console.log("enter-full-screen event", this.getContentBounds()); + const tabView = this.activeTabView; + if (tabView) { + tabView.webContents.send("fullscreen-change", true); + } + this.activeTabView?.positionTabOnScreen(this.getContentBounds()); + }); + this.on("leave-full-screen", async () => { + if (this.isDestroyed()) { + return; + } + const tabView = this.activeTabView; + if (tabView) { + tabView.webContents.send("fullscreen-change", false); + } + this.activeTabView?.positionTabOnScreen(this.getContentBounds()); + }); + this.on("focus", () => { + if (this.isDestroyed()) { + return; + } + if (getGlobalIsRelaunching()) { + return; + } + focusedWaveWindow = this; + console.log("focus win", this.waveWindowId); + fireAndForget(async () => await ClientService.FocusWindow(this.waveWindowId)); + setWasInFg(true); + setWasActive(true); + }); + this.on("blur", () => { + if (this.isDestroyed()) { + return; + } + if (focusedWaveWindow == this) { + focusedWaveWindow = null; + } + }); + this.on("close", (e) => { + if (this.canClose) { + return; + } + if (this.isDestroyed()) { + return; + } + console.log("win 'close' handler fired", this.waveWindowId); + if (getGlobalIsQuitting() || updater?.status == "installing" || getGlobalIsRelaunching()) { + return; + } + e.preventDefault(); + fireAndForget(async () => { + const numWindows = waveWindowMap.size; + if (numWindows > 1) { + console.log("numWindows > 1", numWindows); + const workspace = await WorkspaceService.GetWorkspace(this.workspaceId); + console.log("workspace", workspace); + if (!workspace.name && !workspace.icon && workspace.tabids.length > 1) { + console.log("workspace has no name, icon, and multiple tabs", workspace); + const choice = dialog.showMessageBoxSync(this, { + type: "question", + buttons: ["Cancel", "Yes"], + title: "Confirm", + message: + "Are you sure you want to close this window (all tabs and blocks will be deleted)?", + }); + if (choice === 0) { + console.log("user cancelled close window", this.waveWindowId); + return; + } + } + console.log("deleteAllowed = true", this.waveWindowId); + this.deleteAllowed = true; + } + console.log("canClose = true", this.waveWindowId); + this.canClose = true; + this.close(); + }); + }); + this.on("closed", () => { + console.log("win 'closed' handler fired", this.waveWindowId); + if (getGlobalIsQuitting() || updater?.status == "installing") { + console.log("win quitting or updating", this.waveWindowId); + return; + } + if (getGlobalIsRelaunching()) { + console.log("win relaunching", this.waveWindowId); + this.destroy(); + return; + } + const numWindows = waveWindowMap.size; + if (numWindows == 0) { + console.log("win no windows left", this.waveWindowId); + return; + } + if (this.deleteAllowed) { + console.log("win removing window from backend DB", this.waveWindowId); + fireAndForget(async () => await WindowService.CloseWindow(this.waveWindowId, true)); + } + this.destroy(); + }); + waveWindowMap.set(waveWindow.oid, this); + } + + async switchWorkspace(workspaceId: string) { + console.log("switchWorkspace", workspaceId, this.waveWindowId); + const curWorkspace = await WorkspaceService.GetWorkspace(this.workspaceId); + if (curWorkspace.tabids.length > 1 && (!curWorkspace.name || !curWorkspace.icon)) { + const choice = dialog.showMessageBoxSync(this, { + type: "question", + buttons: ["Cancel", "Open in New Window", "Yes"], + title: "Confirm", + message: + "This window has unsaved tabs, switching workspaces will delete the existing tabs. Would you like to continue?", + }); + if (choice === 0) { + console.log("user cancelled switch workspace", this.waveWindowId); + return; + } else if (choice === 1) { + console.log("user chose open in new window", this.waveWindowId); + const newWin = await WindowService.CreateWindow(null, workspaceId); + if (!newWin) { + console.log("error creating new window", this.waveWindowId); + } + const newBwin = await createBrowserWindow(newWin, await FileService.GetFullConfig(), { unamePlatform }); + newBwin.show(); + return; + } + } + const newWs = await WindowService.SwitchWorkspace(this.waveWindowId, workspaceId); + if (!newWs) { + return; + } + console.log("switchWorkspace newWs", newWs); + if (this.allTabViews.size) { + for (const tab of this.allTabViews.values()) { + tab?.destroy(); + } + } + console.log("destroyed all tabs", this.waveWindowId); + this.workspaceId = workspaceId; + this.allTabViews = new Map(); + await this.setActiveTab(newWs.activetabid, false); + } + + async setActiveTab(tabId: string, setInBackend: boolean) { + console.log("setActiveTab", tabId, this.waveWindowId, this.workspaceId, setInBackend); + if (setInBackend) { + await WorkspaceService.SetActiveTab(this.workspaceId, tabId); + } + const fullConfig = await FileService.GetFullConfig(); + const [tabView, tabInitialized] = getOrCreateWebViewForTab(fullConfig, tabId); + await this.queueTabSwitch(tabView, tabInitialized); + } + + async createTab() { + const tabId = await WorkspaceService.CreateTab(this.workspaceId, null, true); + await this.setActiveTab(tabId, false); + } + + async closeTab(tabId: string) { + console.log("closeTab", tabId, this.waveWindowId, this.workspaceId); + const tabView = this.allTabViews.get(tabId); + if (tabView) { + const rtn = await WorkspaceService.CloseTab(this.workspaceId, tabId, true); + if (rtn?.closewindow) { + this.close(); + } else if (rtn?.newactivetabid) { + await this.setActiveTab(rtn.newactivetabid, false); + } + this.allTabViews.delete(tabId); + } + } + + forceClose() { + console.log("forceClose window", this.waveWindowId); + this.canClose = true; + this.deleteAllowed = true; + this.close(); + } + + async setTabViewIntoWindow(tabView: WaveTabView, tabInitialized: boolean) { + const clientData = await ClientService.GetClientData(); + if (this.activeTabView == tabView) { + return; + } + const oldActiveView = this.activeTabView; + tabView.isActiveTab = true; + if (oldActiveView != null) { + oldActiveView.isActiveTab = false; + } + this.activeTabView = tabView; + this.allTabViews.set(tabView.waveTabId, tabView); + if (!tabInitialized) { + console.log("initializing a new tab"); + await tabView.initPromise; + this.contentView.addChildView(tabView); + const initOpts = { + tabId: tabView.waveTabId, + clientId: clientData.oid, + windowId: this.waveWindowId, + activate: true, + }; + tabView.savedInitOpts = { ...initOpts }; + tabView.savedInitOpts.activate = false; + let startTime = Date.now(); + tabView.webContents.send("wave-init", initOpts); + console.log("before wave ready"); + await tabView.waveReadyPromise; + // positionTabOnScreen(tabView, this.getContentBounds()); + console.log("wave-ready init time", Date.now() - startTime + "ms"); + // positionTabOffScreen(oldActiveView, this.getContentBounds()); + await this.repositionTabsSlowly(100); + } else { + console.log("reusing an existing tab"); + const p1 = this.repositionTabsSlowly(35); + const p2 = tabView.webContents.send("wave-init", tabView.savedInitOpts); // reinit + await Promise.all([p1, p2]); + } + + // something is causing the new tab to lose focus so it requires manual refocusing + tabView.webContents.focus(); + setTimeout(() => { + if (this.activeTabView == tabView && !tabView.webContents.isFocused()) { + tabView.webContents.focus(); + } + }, 10); + setTimeout(() => { + if (this.activeTabView == tabView && !tabView.webContents.isFocused()) { + tabView.webContents.focus(); + } + }, 30); + } + + async repositionTabsSlowly(delayMs: number) { + const activeTabView = this.activeTabView; + const winBounds = this.getContentBounds(); + if (activeTabView == null) { + return; + } + if (activeTabView.isOnScreen()) { + activeTabView.setBounds({ + x: 0, + y: 0, + width: winBounds.width, + height: winBounds.height, + }); + } else { + activeTabView.setBounds({ + x: winBounds.width - 10, + y: winBounds.height - 10, + width: winBounds.width, + height: winBounds.height, + }); + } + await delay(delayMs); + if (this.activeTabView != activeTabView) { + // another tab view has been set, do not finalize this layout + return; + } + this.finalizePositioning(); + } + + finalizePositioning() { + if (this.isDestroyed()) { + return; + } + const curBounds = this.getContentBounds(); + this.activeTabView?.positionTabOnScreen(curBounds); + for (const tabView of this.allTabViews.values()) { + if (tabView == this.activeTabView) { + continue; + } + tabView?.positionTabOffScreen(curBounds); + } + } + + async queueTabSwitch(tabView: WaveTabView, tabInitialized: boolean) { + if (this.tabSwitchQueue.length == 2) { + this.tabSwitchQueue[1] = { tabView, tabInitialized }; + return; + } + this.tabSwitchQueue.push({ tabView, tabInitialized }); + if (this.tabSwitchQueue.length == 1) { + await this.processTabSwitchQueue(); + } + } + + async processTabSwitchQueue() { + if (this.tabSwitchQueue.length == 0) { + this.tabSwitchQueue = []; + return; + } + try { + const { tabView, tabInitialized } = this.tabSwitchQueue[0]; + await this.setTabViewIntoWindow(tabView, tabInitialized); + } finally { + this.tabSwitchQueue.shift(); + await this.processTabSwitchQueue(); + } + } + + async mainResizeHandler(_: any) { + if (this == null || this.isDestroyed() || this.fullScreen) { + return; + } + const bounds = this.getBounds(); + try { + await WindowService.SetWindowPosAndSize( + this.waveWindowId, + { x: bounds.x, y: bounds.y }, + { width: bounds.width, height: bounds.height } + ); + } catch (e) { + console.log("error sending new window bounds to backend", e); + } + } + + destroy() { + console.log("destroy win", this.waveWindowId); + for (const tabView of this.allTabViews.values()) { + tabView?.destroy(); + } + waveWindowMap.delete(this.waveWindowId); + if (focusedWaveWindow == this) { + focusedWaveWindow = null; + } + super.destroy(); + } +} + +export function getWaveWindowByTabId(tabId: string): WaveBrowserWindow { + for (const ww of waveWindowMap.values()) { + if (ww.allTabViews.has(tabId)) { + return ww; + } + } +} + +export function getWaveWindowByWebContentsId(webContentsId: number): WaveBrowserWindow { + const tabView = getWaveTabViewByWebContentsId(webContentsId); + if (tabView == null) { + return null; + } + return getWaveWindowByTabId(tabView.waveTabId); +} + +export function getWaveWindowById(windowId: string): WaveBrowserWindow { + return waveWindowMap.get(windowId); +} + +export function getWaveWindowByWorkspaceId(workspaceId: string): WaveBrowserWindow { + for (const waveWindow of waveWindowMap.values()) { + if (waveWindow.workspaceId === workspaceId) { + return waveWindow; + } + } +} + +export function getAllWaveWindows(): WaveBrowserWindow[] { + return Array.from(waveWindowMap.values()); +} + +// note, this does not *show* the window. +// to show, await win.readyPromise and then win.show() +export async function createBrowserWindow( + waveWindow: WaveWindow, + fullConfig: FullConfigType, + opts: WindowOpts +): Promise { + if (!waveWindow) { + console.log("createBrowserWindow: no waveWindow"); + waveWindow = await WindowService.CreateWindow(null, ""); + } + let workspace = await WorkspaceService.GetWorkspace(waveWindow.workspaceid); + if (!workspace) { + console.log("createBrowserWindow: no workspace, creating new window"); + await WindowService.CloseWindow(waveWindow.oid, true); + waveWindow = await WindowService.CreateWindow(null, ""); + workspace = await WorkspaceService.GetWorkspace(waveWindow.workspaceid); + } + console.log("createBrowserWindow", waveWindow.oid, workspace.oid, workspace); + const bwin = new WaveBrowserWindow(waveWindow, fullConfig, opts); + if (workspace.activetabid) { + await bwin.setActiveTab(workspace.activetabid, false); + } + return bwin; +} + +ipcMain.on("set-active-tab", async (event, tabId) => { + const ww = getWaveWindowByWebContentsId(event.sender.id); + console.log("set-active-tab", tabId, ww?.waveWindowId); + await ww?.setActiveTab(tabId, true); +}); + +ipcMain.on("create-tab", async (event, opts) => { + const senderWc = event.sender; + const ww = getWaveWindowByWebContentsId(senderWc.id); + if (!ww) { + return; + } + await ww.createTab(); + event.returnValue = true; + return null; +}); + +ipcMain.on("close-tab", async (event, tabId) => { + const ww = getWaveWindowByTabId(tabId); + await ww.closeTab(tabId); + event.returnValue = true; + return null; +}); + +ipcMain.on("switch-workspace", async (event, workspaceId) => { + const ww = getWaveWindowByWebContentsId(event.sender.id); + console.log("switch-workspace", workspaceId, ww?.waveWindowId); + await ww?.switchWorkspace(workspaceId); +}); + +ipcMain.on("delete-workspace", async (event, workspaceId) => { + const ww = getWaveWindowByWebContentsId(event.sender.id); + console.log("delete-workspace", workspaceId, ww?.waveWindowId); + await WorkspaceService.DeleteWorkspace(workspaceId); + console.log("delete-workspace done", workspaceId, ww?.waveWindowId); + if (ww?.workspaceId == workspaceId) { + console.log("delete-workspace closing window", workspaceId, ww?.waveWindowId); + ww.forceClose(); + } +}); diff --git a/emain/emain-wsh.ts b/emain/emain-wsh.ts index 467d85cd8..b27d63f56 100644 --- a/emain/emain-wsh.ts +++ b/emain/emain-wsh.ts @@ -1,11 +1,13 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { FileService, WindowService } from "@/app/store/services"; import { Notification } from "electron"; import { getResolvedUpdateChannel } from "emain/updater"; import { RpcResponseHelper, WshClient } from "../frontend/app/store/wshclient"; -import { getWaveWindowById } from "./emain-viewmgr"; import { getWebContentsByBlockId, webGetSelector } from "./emain-web"; +import { createBrowserWindow, getWaveWindowById, getWaveWindowByWorkspaceId } from "./emain-window"; +import { unamePlatform } from "./platform"; export class ElectronWshClientType extends WshClient { constructor() { @@ -13,12 +15,12 @@ export class ElectronWshClientType extends WshClient { } async handle_webselector(rh: RpcResponseHelper, data: CommandWebSelectorData): Promise { - if (!data.tabid || !data.blockid || !data.windowid) { + if (!data.tabid || !data.blockid || !data.workspaceid) { throw new Error("tabid and blockid are required"); } - const ww = getWaveWindowById(data.windowid); + const ww = getWaveWindowByWorkspaceId(data.workspaceid); if (ww == null) { - throw new Error(`no window found with id ${data.windowid}`); + throw new Error(`no window found with workspace ${data.workspaceid}`); } const wc = await getWebContentsByBlockId(ww, data.tabid, data.blockid); if (wc == null) { @@ -39,6 +41,20 @@ export class ElectronWshClientType extends WshClient { async handle_getupdatechannel(rh: RpcResponseHelper): Promise { return getResolvedUpdateChannel(); } + + async handle_focuswindow(rh: RpcResponseHelper, windowId: string) { + console.log(`focuswindow ${windowId}`); + const fullConfig = await FileService.GetFullConfig(); + let ww = getWaveWindowById(windowId); + if (ww == null) { + const window = await WindowService.GetWindow(windowId); + if (window == null) { + throw new Error(`window ${windowId} not found`); + } + ww = await createBrowserWindow(window, fullConfig, { unamePlatform }); + } + ww.focus(); + } } export let ElectronWshClient: ElectronWshClientType; diff --git a/emain/emain.ts b/emain/emain.ts index c5ad4f55f..5535b8864 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -30,20 +30,17 @@ import { setWasActive, setWasInFg, } from "./emain-activity"; +import { ensureHotSpareTab, getWaveTabViewByWebContentsId, setMaxTabCacheSize } from "./emain-tabview"; import { handleCtrlShiftState } from "./emain-util"; +import { getIsWaveSrvDead, getWaveSrvProc, getWaveSrvReady, getWaveVersion, runWaveSrv } from "./emain-wavesrv"; import { createBrowserWindow, - ensureHotSpareTab, + focusedWaveWindow, getAllWaveWindows, - getFocusedWaveWindow, - getLastFocusedWaveWindow, - getWaveTabViewByWebContentsId, getWaveWindowById, getWaveWindowByWebContentsId, - setActiveTab, - setMaxTabCacheSize, -} from "./emain-viewmgr"; -import { getIsWaveSrvDead, getWaveSrvProc, getWaveSrvReady, getWaveVersion, runWaveSrv } from "./emain-wavesrv"; + WaveBrowserWindow, +} from "./emain-window"; import { ElectronWshClient, initElectronWshClient } from "./emain-wsh"; import { getLaunchSettings } from "./launchsettings"; import { getAppMenu } from "./menu"; @@ -95,6 +92,7 @@ console.log = log; console.log( sprintf( "waveterm-app starting, data_dir=%s, config_dir=%s electronpath=%s gopath=%s arch=%s/%s", + waveDataDir, waveConfigDir, getElectronAppBasePath(), getElectronAppUnpackedBasePath(), @@ -106,29 +104,31 @@ if (isDev) { console.log("waveterm-app WAVETERM_DEV set"); } -async function handleWSEvent(evtMsg: WSEventType) { - console.log("handleWSEvent", evtMsg?.eventtype); - if (evtMsg.eventtype == "electron:newwindow") { - const windowId: string = evtMsg.data; - const windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow; - if (windowData == null) { - return; +function handleWSEvent(evtMsg: WSEventType) { + fireAndForget(async () => { + console.log("handleWSEvent", evtMsg?.eventtype); + if (evtMsg.eventtype == "electron:newwindow") { + console.log("electron:newwindow", evtMsg.data); + const windowId: string = evtMsg.data; + const windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow; + if (windowData == null) { + return; + } + const fullConfig = await services.FileService.GetFullConfig(); + const newWin = await createBrowserWindow(windowData, fullConfig, { unamePlatform }); + await newWin.waveReadyPromise; + newWin.show(); + } else if (evtMsg.eventtype == "electron:closewindow") { + console.log("electron:closewindow", evtMsg.data); + if (evtMsg.data === undefined) return; + const ww = getWaveWindowById(evtMsg.data); + if (ww != null) { + ww.destroy(); // bypass the "are you sure?" dialog + } + } else { + console.log("unhandled electron ws eventtype", evtMsg.eventtype); } - const clientData = await services.ClientService.GetClientData(); - const fullConfig = await services.FileService.GetFullConfig(); - const newWin = createBrowserWindow(clientData.oid, windowData, fullConfig, { unamePlatform }); - await newWin.waveReadyPromise; - newWin.show(); - } else if (evtMsg.eventtype == "electron:closewindow") { - if (evtMsg.data === undefined) return; - const ww = getWaveWindowById(evtMsg.data); - if (ww != null) { - ww.alreadyClosed = true; - ww.destroy(); // bypass the "are you sure?" dialog - } - } else { - console.log("unhandled electron ws eventtype", evtMsg.eventtype); - } + }); } // Listen for the open-external event from the renderer process @@ -251,50 +251,6 @@ electron.ipcMain.on("download", (event, payload) => { event.sender.downloadURL(streamingUrl); }); -electron.ipcMain.on("set-active-tab", async (event, tabId) => { - const ww = getWaveWindowByWebContentsId(event.sender.id); - console.log("set-active-tab", tabId, ww?.waveWindowId); - await setActiveTab(ww, tabId); -}); - -electron.ipcMain.on("create-tab", async (event, opts) => { - const senderWc = event.sender; - const tabView = getWaveTabViewByWebContentsId(senderWc.id); - if (tabView == null) { - return; - } - const waveWindowId = tabView.waveWindowId; - const waveWindow = (await services.ObjectService.GetObject("window:" + waveWindowId)) as WaveWindow; - if (waveWindow == null) { - return; - } - const newTabId = await services.ObjectService.AddTabToWorkspace(waveWindowId, null, true); - const ww = getWaveWindowById(waveWindowId); - if (ww == null) { - return; - } - await setActiveTab(ww, newTabId); - event.returnValue = true; - return null; -}); - -electron.ipcMain.on("close-tab", async (event, tabId) => { - const tabView = getWaveTabViewByWebContentsId(event.sender.id); - if (tabView == null) { - return; - } - const rtn = await services.WindowService.CloseTab(tabView.waveWindowId, tabId, true); - if (rtn?.closewindow) { - const ww = getWaveWindowById(tabView.waveWindowId); - ww.alreadyClosed = true; - ww?.destroy(); // bypass the "are you sure?" dialog - } else if (rtn?.newactivetabid) { - setActiveTab(getWaveWindowById(tabView.waveWindowId), rtn.newactivetabid); - } - event.returnValue = true; - return null; -}); - electron.ipcMain.on("get-cursor-point", (event) => { const tabView = getWaveTabViewByWebContentsId(event.sender.id); if (tabView == null) { @@ -409,30 +365,34 @@ electron.ipcMain.on("open-native-path", (event, filePath: string) => { }); async function createNewWaveWindow(): Promise { + log("createNewWaveWindow"); const clientData = await services.ClientService.GetClientData(); const fullConfig = await services.FileService.GetFullConfig(); let recreatedWindow = false; const allWindows = getAllWaveWindows(); if (allWindows.length === 0 && clientData?.windowids?.length >= 1) { + console.log("no windows, but clientData has windowids, recreating first window"); // reopen the first window const existingWindowId = clientData.windowids[0]; const existingWindowData = (await services.ObjectService.GetObject("window:" + existingWindowId)) as WaveWindow; if (existingWindowData != null) { - const win = createBrowserWindow(clientData.oid, existingWindowData, fullConfig, { unamePlatform }); + const win = await createBrowserWindow(existingWindowData, fullConfig, { unamePlatform }); await win.waveReadyPromise; win.show(); recreatedWindow = true; } } if (recreatedWindow) { + console.log("recreated window, returning"); return; } - const newWindow = await services.ClientService.MakeWindow(); - const newBrowserWindow = createBrowserWindow(clientData.oid, newWindow, fullConfig, { unamePlatform }); + console.log("creating new window"); + const newBrowserWindow = await createBrowserWindow(null, fullConfig, { unamePlatform }); await newBrowserWindow.waveReadyPromise; newBrowserWindow.show(); } +// Here's where init is not getting fired electron.ipcMain.on("set-window-init-status", (event, status: "ready" | "wave-ready") => { const tabView = getWaveTabViewByWebContentsId(event.sender.id); if (tabView == null || tabView.initResolve == null) { @@ -442,7 +402,10 @@ electron.ipcMain.on("set-window-init-status", (event, status: "ready" | "wave-re console.log("initResolve"); tabView.initResolve(); if (tabView.savedInitOpts) { + console.log("savedInitOpts"); tabView.webContents.send("wave-init", tabView.savedInitOpts); + } else { + console.log("no-savedInitOpts"); } } else if (status === "wave-ready") { console.log("waveReadyResolve"); @@ -458,7 +421,7 @@ function saveImageFileWithNativeDialog(defaultFileName: string, mimeType: string if (defaultFileName == null || defaultFileName == "") { defaultFileName = "image"; } - const ww = getFocusedWaveWindow(); + const ww = focusedWaveWindow; const mimeToExtension: { [key: string]: string } = { "image/png": "png", "image/jpeg": "jpg", @@ -539,26 +502,28 @@ function getActivityDisplays(): ActivityDisplayType[] { return rtn; } -async function logActiveState() { - const astate = getActivityState(); - const activity: ActivityUpdate = { openminutes: 1 }; - if (astate.wasInFg) { - activity.fgminutes = 1; - } - if (astate.wasActive) { - activity.activeminutes = 1; - } - activity.displays = getActivityDisplays(); - try { - RpcApi.ActivityCommand(ElectronWshClient, activity, { noresponse: true }); - } catch (e) { - console.log("error logging active state", e); - } finally { - // for next iteration - const ww = getFocusedWaveWindow(); - setWasInFg(ww?.isFocused() ?? false); - setWasActive(false); - } +function logActiveState() { + fireAndForget(async () => { + const astate = getActivityState(); + const activity: ActivityUpdate = { openminutes: 1 }; + if (astate.wasInFg) { + activity.fgminutes = 1; + } + if (astate.wasActive) { + activity.activeminutes = 1; + } + activity.displays = getActivityDisplays(); + try { + await RpcApi.ActivityCommand(ElectronWshClient, activity, { noresponse: true }); + } catch (e) { + console.log("error logging active state", e); + } finally { + // for next iteration + const ww = focusedWaveWindow; + setWasInFg(ww?.isFocused() ?? false); + setWasActive(false); + } + }); } // this isn't perfect, but gets the job done without being complicated @@ -593,7 +558,6 @@ function instantiateAppMenu(): electron.Menu { return getAppMenu({ createNewWaveWindow, relaunchBrowserWindows, - getLastFocusedWaveWindow: getLastFocusedWaveWindow, }); } @@ -679,6 +643,7 @@ process.on("uncaughtException", (error) => { }); async function relaunchBrowserWindows(): Promise { + console.log("relaunchBrowserWindows"); setGlobalIsRelaunching(true); const windows = getAllWaveWindows(); for (const window of windows) { @@ -691,14 +656,14 @@ async function relaunchBrowserWindows(): Promise { 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; + const windowData: WaveWindow = await services.WindowService.GetWindow(windowId); if (windowData == null) { - services.WindowService.CloseWindow(windowId, true).catch((e) => { - /* ignore */ - }); + console.log("relaunch -- window data not found, closing window", windowId); + await services.WindowService.CloseWindow(windowId, true); continue; } - const win = createBrowserWindow(clientData.oid, windowData, fullConfig, { unamePlatform }); + console.log("relaunch -- creating window", windowId, windowData); + const win = await createBrowserWindow(windowData, fullConfig, { unamePlatform }); wins.push(win); } for (const win of wins) { @@ -749,10 +714,10 @@ async function appMain() { setMaxTabCacheSize(fullConfig.settings["window:maxtabcachesize"]); } - electronApp.on("activate", async () => { + electronApp.on("activate", () => { const allWindows = getAllWaveWindows(); if (allWindows.length === 0) { - await createNewWaveWindow(); + fireAndForget(createNewWaveWindow); } }); } diff --git a/emain/menu.ts b/emain/menu.ts index 0c451bdc9..bc55424d1 100644 --- a/emain/menu.ts +++ b/emain/menu.ts @@ -3,14 +3,14 @@ import * as electron from "electron"; import { fireAndForget } from "../frontend/util/util"; -import { clearTabCache, getFocusedWaveWindow } from "./emain-viewmgr"; +import { clearTabCache } from "./emain-tabview"; +import { focusedWaveWindow, WaveBrowserWindow } from "./emain-window"; import { unamePlatform } from "./platform"; import { updater } from "./updater"; type AppMenuCallbacks = { createNewWaveWindow: () => Promise; relaunchBrowserWindows: () => Promise; - getLastFocusedWaveWindow: () => WaveBrowserWindow; }; function getWindowWebContents(window: electron.BaseWindow): electron.WebContents { @@ -38,7 +38,7 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu { role: "close", accelerator: "", // clear the accelerator click: () => { - getFocusedWaveWindow()?.close(); + focusedWaveWindow?.close(); }, }, ]; diff --git a/emain/preload.ts b/emain/preload.ts index 966b121b2..86ecdadeb 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -40,6 +40,8 @@ contextBridge.exposeInMainWorld("api", { registerGlobalWebviewKeys: (keys) => ipcRenderer.send("register-global-webview-keys", keys), onControlShiftStateUpdate: (callback) => ipcRenderer.on("control-shift-state-update", (_event, state) => callback(state)), + switchWorkspace: (workspaceId) => ipcRenderer.send("switch-workspace", workspaceId), + deleteWorkspace: (workspaceId) => ipcRenderer.send("delete-workspace", workspaceId), setActiveTab: (tabId) => ipcRenderer.send("set-active-tab", tabId), createTab: () => ipcRenderer.send("create-tab"), closeTab: (tabId) => ipcRenderer.send("close-tab", tabId), diff --git a/emain/updater.ts b/emain/updater.ts index 11fb4ac29..240020b77 100644 --- a/emain/updater.ts +++ b/emain/updater.ts @@ -11,7 +11,7 @@ import { RpcApi } from "../frontend/app/store/wshclientapi"; import { isDev } from "../frontend/util/isdev"; import { fireAndForget } from "../frontend/util/util"; import { delay } from "./emain-util"; -import { getAllWaveWindows, getFocusedWaveWindow } from "./emain-viewmgr"; +import { focusedWaveWindow, getAllWaveWindows } from "./emain-window"; import { ElectronWshClient } from "./emain-wsh"; export let updater: Updater; @@ -164,7 +164,7 @@ export class Updater { type: "info", message: "There are currently no updates available.", }; - dialog.showMessageBox(getFocusedWaveWindow(), dialogOpts); + dialog.showMessageBox(focusedWaveWindow, dialogOpts); } // Only update the last check time if this is an automatic check. This ensures the interval remains consistent. @@ -186,8 +186,7 @@ export class Updater { const allWindows = getAllWaveWindows(); if (allWindows.length > 0) { - const focusedWindow = getFocusedWaveWindow(); - await dialog.showMessageBox(focusedWindow ?? allWindows[0], dialogOpts).then(({ response }) => { + await dialog.showMessageBox(focusedWaveWindow ?? allWindows[0], dialogOpts).then(({ response }) => { if (response === 0) { fireAndForget(async () => this.installUpdate()); } diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 16c59db14..b3e93d22f 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -341,7 +341,7 @@ const ConnStatusOverlay = React.memo( }, [width, connStatus, setShowError]); const handleTryReconnect = React.useCallback(() => { - const prtn = RpcApi.ConnConnectCommand(TabRpcClient, connName, { timeout: 60000 }); + const prtn = RpcApi.ConnConnectCommand(TabRpcClient, { host: connName }, { timeout: 60000 }); prtn.catch((e) => console.log("error reconnecting", connName, e)); }, [connName]); @@ -673,7 +673,11 @@ const ChangeConnectionBlockModal = React.memo( label: `Reconnect to ${connStatus.connection}`, value: "", onSelect: async (_: string) => { - const prtn = RpcApi.ConnConnectCommand(TabRpcClient, connStatus.connection, { timeout: 60000 }); + const prtn = RpcApi.ConnConnectCommand( + TabRpcClient, + { host: connStatus.connection }, + { timeout: 60000 } + ); prtn.catch((e) => console.log("error reconnecting", connStatus.connection, e)); }, }; diff --git a/frontend/app/modals/userinputmodal.scss b/frontend/app/modals/userinputmodal.scss index 6cf2f5595..6ec38c048 100644 --- a/frontend/app/modals/userinputmodal.scss +++ b/frontend/app/modals/userinputmodal.scss @@ -45,11 +45,17 @@ .userinput-checkbox-container { display: flex; - align-items: center; + flex-direction: column; gap: 6px; - .userinput-checkbox { - accent-color: var(--accent-color); + .userinput-checkbox-row { + display: flex; + align-items: center; + gap: 6px; + + .userinput-checkbox { + accent-color: var(--accent-color); + } } } } diff --git a/frontend/app/modals/userinputmodal.tsx b/frontend/app/modals/userinputmodal.tsx index 5721d78de..daeec9755 100644 --- a/frontend/app/modals/userinputmodal.tsx +++ b/frontend/app/modals/userinputmodal.tsx @@ -15,7 +15,7 @@ const UserInputModal = (userInputRequest: UserInputRequest) => { const [countdown, setCountdown] = useState(Math.floor(userInputRequest.timeoutms / 1000)); const checkboxRef = useRef(); - const handleSendCancel = useCallback(() => { + const handleSendErrResponse = useCallback(() => { UserInputService.SendUserInputResponse({ type: "userinputresp", requestid: userInputRequest.requestid, @@ -29,20 +29,24 @@ const UserInputModal = (userInputRequest: UserInputRequest) => { type: "userinputresp", requestid: userInputRequest.requestid, text: responseText, - checkboxstat: checkboxRef.current?.checked ?? false, + checkboxstat: checkboxRef?.current?.checked ?? false, }); modalsModel.popModal(); }, [responseText, userInputRequest]); + console.log("bar"); - const handleSendConfirm = useCallback(() => { - UserInputService.SendUserInputResponse({ - type: "userinputresp", - requestid: userInputRequest.requestid, - confirm: true, - checkboxstat: checkboxRef.current?.checked ?? false, - }); - modalsModel.popModal(); - }, [userInputRequest]); + const handleSendConfirm = useCallback( + (response: boolean) => { + UserInputService.SendUserInputResponse({ + type: "userinputresp", + requestid: userInputRequest.requestid, + confirm: response, + checkboxstat: checkboxRef?.current?.checked ?? false, + }); + modalsModel.popModal(); + }, + [userInputRequest] + ); const handleSubmit = useCallback(() => { switch (userInputRequest.responsetype) { @@ -50,15 +54,16 @@ const UserInputModal = (userInputRequest: UserInputRequest) => { handleSendText(); break; case "confirm": - handleSendConfirm(); + handleSendConfirm(true); break; } }, [handleSendConfirm, handleSendText, userInputRequest.responsetype]); + console.log("baz"); const handleKeyDown = useCallback( (waveEvent: WaveKeyboardEvent): boolean => { if (keyutil.checkKeyPressed(waveEvent, "Escape")) { - handleSendCancel(); + handleSendErrResponse(); return; } if (keyutil.checkKeyPressed(waveEvent, "Enter")) { @@ -66,7 +71,7 @@ const UserInputModal = (userInputRequest: UserInputRequest) => { return true; } }, - [handleSendCancel, handleSubmit] + [handleSendErrResponse, handleSubmit] ); const queryText = useMemo(() => { @@ -75,6 +80,7 @@ const UserInputModal = (userInputRequest: UserInputRequest) => { } return {userInputRequest.querytext}; }, [userInputRequest.markdown, userInputRequest.querytext]); + console.log("foobarbaz"); const inputBox = useMemo(() => { if (userInputRequest.responsetype === "confirm") { @@ -92,6 +98,7 @@ const UserInputModal = (userInputRequest: UserInputRequest) => { /> ); }, [userInputRequest.responsetype, userInputRequest.publictext, responseText, handleKeyDown, setResponseText]); + console.log("mem1"); const optionalCheckbox = useMemo(() => { if (userInputRequest.checkboxmsg == "") { @@ -99,22 +106,25 @@ const UserInputModal = (userInputRequest: UserInputRequest) => { } return (
- - +
+ + +
); }, []); + console.log("mem2"); useEffect(() => { let timeout: ReturnType; if (countdown <= 0) { timeout = setTimeout(() => { - handleSendCancel(); + handleSendErrResponse(); }, 300); } else { timeout = setTimeout(() => { @@ -123,9 +133,28 @@ const UserInputModal = (userInputRequest: UserInputRequest) => { } return () => clearTimeout(timeout); }, [countdown]); + console.log("count"); + + const handleNegativeResponse = useCallback(() => { + switch (userInputRequest.responsetype) { + case "text": + handleSendErrResponse(); + break; + case "confirm": + handleSendConfirm(false); + break; + } + }, [userInputRequest.responsetype, handleSendErrResponse, handleSendConfirm]); + console.log("before end"); return ( - handleSubmit()} onCancel={() => handleSendCancel()} onClose={() => handleSendCancel()}> + handleSubmit()} + onCancel={() => handleNegativeResponse()} + onClose={() => handleSendErrResponse()} + okLabel={userInputRequest.oklabel} + cancelLabel={userInputRequest.cancellabel} + >
{userInputRequest.title + ` (${countdown}s)`}
{queryText} diff --git a/frontend/app/store/services.ts b/frontend/app/store/services.ts index 19d762189..4b7714701 100644 --- a/frontend/app/store/services.ts +++ b/frontend/app/store/services.ts @@ -40,15 +40,6 @@ class ClientServiceType { GetTab(arg1: string): Promise { return WOS.callBackendService("client", "GetTab", Array.from(arguments)) } - GetWindow(arg1: string): Promise { - return WOS.callBackendService("client", "GetWindow", Array.from(arguments)) - } - GetWorkspace(arg1: string): Promise { - return WOS.callBackendService("client", "GetWorkspace", Array.from(arguments)) - } - MakeWindow(): Promise { - return WOS.callBackendService("client", "MakeWindow", Array.from(arguments)) - } TelemetryUpdate(arg2: boolean): Promise { return WOS.callBackendService("client", "TelemetryUpdate", Array.from(arguments)) } @@ -89,11 +80,6 @@ export const FileService = new FileServiceType(); // objectservice.ObjectService (object) class ObjectServiceType { - // @returns tabId (and object updates) - AddTabToWorkspace(windowId: string, tabName: string, activateTab: boolean): Promise { - return WOS.callBackendService("object", "AddTabToWorkspace", Array.from(arguments)) - } - // @returns blockId (and object updates) CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise { return WOS.callBackendService("object", "CreateBlock", Array.from(arguments)) @@ -114,11 +100,6 @@ class ObjectServiceType { return WOS.callBackendService("object", "GetObjects", Array.from(arguments)) } - // @returns object updates - SetActiveTab(uiContext: string, tabId: string): Promise { - return WOS.callBackendService("object", "SetActiveTab", Array.from(arguments)) - } - // @returns object updates UpdateObject(waveObj: WaveObj, returnUpdates: boolean): Promise { return WOS.callBackendService("object", "UpdateObject", Array.from(arguments)) @@ -133,11 +114,6 @@ class ObjectServiceType { UpdateTabName(tabId: string, name: string): Promise { return WOS.callBackendService("object", "UpdateTabName", Array.from(arguments)) } - - // @returns object updates - UpdateWorkspaceTabIds(workspaceId: string, tabIds: string[]): Promise { - return WOS.callBackendService("object", "UpdateWorkspaceTabIds", Array.from(arguments)) - } } export const ObjectService = new ObjectServiceType(); @@ -153,13 +129,15 @@ export const UserInputService = new UserInputServiceType(); // windowservice.WindowService (window) class WindowServiceType { - // @returns object updates - CloseTab(arg2: string, arg3: string, arg4: boolean): Promise { - return WOS.callBackendService("window", "CloseTab", Array.from(arguments)) - } - CloseWindow(arg2: string, arg3: boolean): Promise { + CloseWindow(windowId: string, fromElectron: boolean): Promise { return WOS.callBackendService("window", "CloseWindow", Array.from(arguments)) } + CreateWindow(winSize: WinSize, workspaceId: string): Promise { + return WOS.callBackendService("window", "CreateWindow", Array.from(arguments)) + } + GetWindow(windowId: string): Promise { + return WOS.callBackendService("window", "GetWindow", Array.from(arguments)) + } // move block to new window // @returns object updates @@ -167,11 +145,51 @@ class WindowServiceType { return WOS.callBackendService("window", "MoveBlockToNewWindow", Array.from(arguments)) } + // set window position and size // @returns object updates - SetWindowPosAndSize(arg2: string, arg3: Point, arg4: WinSize): Promise { + SetWindowPosAndSize(windowId: string, pos: Point, size: WinSize): Promise { return WOS.callBackendService("window", "SetWindowPosAndSize", Array.from(arguments)) } + SwitchWorkspace(windowId: string, workspaceId: string): Promise { + return WOS.callBackendService("window", "SwitchWorkspace", Array.from(arguments)) + } } export const WindowService = new WindowServiceType(); +// workspaceservice.WorkspaceService (workspace) +class WorkspaceServiceType { + // @returns object updates + CloseTab(workspaceId: string, tabId: string, fromElectron: boolean): Promise { + return WOS.callBackendService("workspace", "CloseTab", Array.from(arguments)) + } + + // @returns tabId (and object updates) + CreateTab(workspaceId: string, tabName: string, activateTab: boolean): Promise { + return WOS.callBackendService("workspace", "CreateTab", Array.from(arguments)) + } + + // @returns object updates + DeleteWorkspace(workspaceId: string): Promise { + return WOS.callBackendService("workspace", "DeleteWorkspace", Array.from(arguments)) + } + GetWorkspace(workspaceId: string): Promise { + return WOS.callBackendService("workspace", "GetWorkspace", Array.from(arguments)) + } + ListWorkspaces(): Promise { + return WOS.callBackendService("workspace", "ListWorkspaces", Array.from(arguments)) + } + + // @returns object updates + SetActiveTab(workspaceId: string, tabId: string): Promise { + return WOS.callBackendService("workspace", "SetActiveTab", Array.from(arguments)) + } + + // @returns object updates + UpdateTabIds(workspaceId: string, tabIds: string[]): Promise { + return WOS.callBackendService("workspace", "UpdateTabIds", Array.from(arguments)) + } +} + +export const WorkspaceService = new WorkspaceServiceType(); + diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index 7c8b693fc..244349289 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -28,7 +28,7 @@ class RpcApiType { } // command "connconnect" [call] - ConnConnectCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { + ConnConnectCommand(client: WshClient, data: ConnRequest, opts?: RpcOpts): Promise { return client.wshRpcCall("connconnect", data, opts); } @@ -167,6 +167,11 @@ class RpcApiType { return client.wshRpcCall("filewrite", data, opts); } + // command "focuswindow" [call] + FocusWindowCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { + return client.wshRpcCall("focuswindow", data, opts); + } + // command "getmeta" [call] GetMetaCommand(client: WshClient, data: CommandGetMetaData, opts?: RpcOpts): Promise { return client.wshRpcCall("getmeta", data, opts); @@ -312,6 +317,11 @@ class RpcApiType { return client.wshRpcCall("webselector", data, opts); } + // command "workspacelist" [call] + WorkspaceListCommand(client: WshClient, opts?: RpcOpts): Promise { + return client.wshRpcCall("workspacelist", null, opts); + } + // command "wshactivity" [call] WshActivityCommand(client: WshClient, data: {[key: string]: number}, opts?: RpcOpts): Promise { return client.wshRpcCall("wshactivity", data, opts); diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index 665e87374..ac32fbdfb 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -6,11 +6,12 @@ import { modalsModel } from "@/app/store/modalmodel"; import { WindowDrag } from "@/element/windowdrag"; import { deleteLayoutModelForTab } from "@/layout/index"; import { atoms, createTab, getApi, isDev, PLATFORM } from "@/store/global"; -import * as services from "@/store/services"; +import { fireAndForget } from "@/util/util"; import { useAtomValue } from "jotai"; import { OverlayScrollbars } from "overlayscrollbars"; -import React, { createRef, useCallback, useEffect, useRef, useState } from "react"; +import { createRef, memo, useCallback, useEffect, useRef, useState } from "react"; import { debounce } from "throttle-debounce"; +import { WorkspaceService } from "../store/services"; import { Tab } from "./tab"; import "./tabbar.scss"; import { UpdateStatusBanner } from "./updatebanner"; @@ -98,7 +99,7 @@ const ConfigErrorIcon = ({ buttonRef }: { buttonRef: React.RefObject { +const TabBar = memo(({ workspace }: TabBarProps) => { const [tabIds, setTabIds] = useState([]); const [dragStartPositions, setDragStartPositions] = useState([]); const [draggingTab, setDraggingTab] = useState(); @@ -440,7 +441,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { // Reset dragging state setDraggingTab(null); // Update workspace tab ids - services.ObjectService.UpdateWorkspaceTabIds(workspace.oid, tabIds); + fireAndForget(async () => await WorkspaceService.UpdateTabIds(workspace.oid, tabIds)); })(); } else { // Reset styles @@ -545,7 +546,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { {appMenuButton} {devLabel} - {isDev() ? : null} +
{tabIds.map((tabId, index) => { diff --git a/frontend/app/tab/workspaceswitcher.scss b/frontend/app/tab/workspaceswitcher.scss index 4a21a398b..e3dbd4cce 100644 --- a/frontend/app/tab/workspaceswitcher.scss +++ b/frontend/app/tab/workspaceswitcher.scss @@ -2,210 +2,224 @@ // SPDX-License-Identifier: Apache-2.0 .workspace-switcher-button { - display: flex; - height: 26px; - padding: 0px 12px; - justify-content: flex-end; - align-items: center; - gap: 12px; - border-radius: 6px; - background: rgba(255, 255, 255, 0.07); - margin-top: 6px; - margin-right: 13px; - box-sizing: border-box; + display: flex; + height: 26px; + padding: 0px 12px; + justify-content: flex-end; + align-items: center; + gap: 12px; + border-radius: 6px; + background: var(--modal-bg-color); + margin-top: 6px; + margin-right: 13px; + box-sizing: border-box; - .workspace-icon { - width: 15px; - height: 15px; - display: flex; - align-items: center; - justify-content: center; - } + .workspace-icon { + width: 15px; + height: 15px; + display: flex; + align-items: center; + justify-content: center; + } } .icon-left, .icon-right { - display: flex; - align-items: center; - justify-content: center; - font-size: 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; } .divider { - width: 1px; - height: 20px; - background: rgba(255, 255, 255, 0.08); + width: 1px; + height: 20px; + background: rgba(255, 255, 255, 0.08); } .scrollable { - max-height: 400px; - width: 100%; + max-height: 400px; + width: 100%; } .workspace-switcher-content { - min-height: auto; - display: flex; - width: 256px; - padding: 0; - flex-direction: column; - align-items: center; - border-radius: 8px; - border: 0.5px solid rgba(255, 255, 255, 0.1); - background-color: rgb(35, 35, 35); - box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.8); + min-height: auto; + display: flex; + width: 256px; + padding: 0; + flex-direction: column; + align-items: center; + border-radius: 8px; + box-shadow: 0px 8px 24px 0px var(--modal-shadow-color); - .title { - color: #fff; - font-size: 12px; - line-height: 19px; - font-weight: 600; - margin-bottom: 5px; - width: 100%; - padding: 6px 8px 0px; + .title { + font-size: 12px; + line-height: 19px; + font-weight: 600; + margin-bottom: 5px; + width: 100%; + padding: 6px 8px 0px; + } + + .expandable-menu { + gap: 5px; + } + + .expandable-menu-item { + margin: 3px 8px; + } + + .expandable-menu-item-group { + margin: 0 8px; + + &:last-child { + margin-bottom: 4px; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; } .expandable-menu-item { - margin: 3px 8px; + margin: 0; + } + } + + .expandable-menu-item-group { + border: 1px solid transparent; + border-radius: 4px; + + --workspace-color: var(--main-bg-color); + + .menu-group-title-wrapper { + display: flex; + width: 100%; + padding: 5px 8px; + border-radius: 4px; + .icons { + display: flex; + flex-direction: row; + gap: 5px; + } + + .iconbutton.edit { + visibility: hidden; + } + + .iconbutton.window { + cursor: default; + opacity: 1 !important; + } } - .expandable-menu-item-group { - margin: 0 8px; - - &:last-child { - margin-bottom: 4px; - border-bottom-left-radius: 8px; - border-bottom-right-radius: 8px; - } - - .expandable-menu-item { - margin: 0; - } + &:hover .iconbutton.edit { + visibility: visible; } - .expandable-menu-item-group { - border: 1px solid transparent; - border-radius: 4px; - - .menu-group-title-wrapper { - display: flex; - width: 100%; - padding: 5px 8px; - border-radius: 4px; - } - - &.open:not(:first-child) { - background-color: rgb(30, 30, 30); - border: 1px solid rgb(41, 41, 41); - } - - &.is-active { - .expandable-menu-item-group-title:hover { - background-color: transparent; - } - } + &.open { + background-color: var(--modal-bg-color); + border: 1px solid var(--modal-border-color); } - .expandable-menu-item, - .expandable-menu-item-group-title { - color: #fff; - font-size: 12px; - line-height: 19px; - padding: 5px 8px; + &.is-current .menu-group-title-wrapper { + background-color: rgb(from var(--workspace-color) r g b / 0.1); + } + } - .content { - width: 100%; - } + .expandable-menu-item, + .expandable-menu-item-group-title { + font-size: 12px; + line-height: 19px; + padding: 5px 8px; + + .content { + width: 100%; } - .expandable-menu-item-group-title { - height: 29px; - padding: 0; + &:hover { + background-color: transparent; + } + } - .left-icon { - font-size: 14px; - } + .expandable-menu-item-group-title { + height: 29px; + padding: 0; + + .left-icon { + font-size: 14px; + } + } + + .color-icon-selector { + width: 100%; + .input { + margin: 5px 0 10px; } - .color-icon-selector { - .input { - margin: 5px 0 10px; + .color-selector { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(15px, 15px)); // Ensures each color circle has a fixed 14px size + grid-gap: 18.5px; // Space between items + justify-content: center; + align-items: center; + margin-top: 5px; + + .color-circle { + width: 15px; + height: 15px; + border-radius: 50%; + cursor: pointer; + position: relative; + + // Border offset outward + &:before { + content: ""; + position: absolute; + top: -3px; + left: -3px; + right: -3px; + bottom: -3px; + border-radius: 50%; + border: 1px solid transparent; } - .color-selector { - display: grid; - grid-template-columns: repeat( - auto-fit, - minmax(15px, 15px) - ); // Ensures each color circle has a fixed 14px size - grid-gap: 18.5px; // Space between items - justify-content: center; - align-items: center; - margin-top: 5px; - - .color-circle { - width: 15px; - height: 15px; - border-radius: 50%; - cursor: pointer; - position: relative; - - // Border offset outward - &:before { - content: ""; - position: absolute; - top: -3px; - left: -3px; - right: -3px; - bottom: -3px; - border-radius: 50%; - border: 1px solid transparent; - } - - &.selected:before { - border-color: white; // Highlight for the selected circle - } - } - } - - .icon-selector { - display: grid; - grid-template-columns: repeat( - auto-fit, - minmax(16px, 16px) - ); // Ensures each color circle has a fixed 14px size - grid-column-gap: 17.5px; // Space between items - grid-row-gap: 13px; // Space between items - justify-content: center; - align-items: center; - margin-top: 15px; - - .icon-item { - font-size: 15px; - color: #666; - cursor: pointer; - transition: color 0.3s ease; - - &.selected { - color: white; - } - - &:hover { - color: #fff; - } - } - } - - .delete-ws-btn-wrapper { - display: flex; - align-items: center; - justify-content: center; - margin-top: 10px; + &.selected:before { + border-color: var(--main-text-color); // Highlight for the selected circle } + } } - .actions { - width: 100%; - padding: 3px 0; - border-top: 1px solid rgba(255, 255, 255, 0.1); + .icon-selector { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(16px, 16px)); // Ensures each color circle has a fixed 14px size + grid-column-gap: 17.5px; // Space between items + grid-row-gap: 13px; // Space between items + justify-content: center; + align-items: center; + margin-top: 15px; + + .icon-item { + font-size: 15px; + color: oklch(from var(--modal-bg-color) calc(l * 1.5) c h); + cursor: pointer; + transition: color 0.3s ease; + + &.selected, + &:hover { + color: var(--main-text-color); + } + } } + + .delete-ws-btn-wrapper { + display: flex; + align-items: center; + justify-content: center; + margin-top: 10px; + } + } + + .actions { + width: 100%; + padding: 3px 0; + border-top: 1px solid var(--modal-border-color); + } } diff --git a/frontend/app/tab/workspaceswitcher.tsx b/frontend/app/tab/workspaceswitcher.tsx index 3f4343f35..a59294f1a 100644 --- a/frontend/app/tab/workspaceswitcher.tsx +++ b/frontend/app/tab/workspaceswitcher.tsx @@ -5,23 +5,24 @@ import { Button } from "@/element/button"; import { ExpandableMenu, ExpandableMenuItem, - ExpandableMenuItemData, ExpandableMenuItemGroup, ExpandableMenuItemGroupTitle, - ExpandableMenuItemGroupTitleType, ExpandableMenuItemLeftElement, ExpandableMenuItemRightElement, } from "@/element/expandablemenu"; import { Input } from "@/element/input"; import { Popover, PopoverButton, PopoverContent } from "@/element/popover"; -import { makeIconClass } from "@/util/util"; +import { fireAndForget, makeIconClass, useAtomValueSafe } from "@/util/util"; import clsx from "clsx"; -import { colord } from "colord"; -import { atom, useAtom } from "jotai"; +import { atom, PrimitiveAtom, useAtom, useAtomValue, useSetAtom } from "jotai"; +import { splitAtom } from "jotai/utils"; import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; -import { forwardRef, memo, useEffect, useRef } from "react"; +import { CSSProperties, forwardRef, memo, useCallback, useEffect, useRef } from "react"; import WorkspaceSVG from "../asset/workspace.svg"; - +import { IconButton } from "../element/iconbutton"; +import { atoms, getApi } from "../store/global"; +import { WorkspaceService } from "../store/services"; +import { getObjectValue, makeORef, setObjectValue } from "../store/wos"; import "./workspaceswitcher.scss"; interface ColorSelectorProps { @@ -136,7 +137,7 @@ const ColorAndIconSelector = memo( onSelect={onIconChange} />
-
@@ -145,287 +146,198 @@ const ColorAndIconSelector = memo( } ); -interface WorkspaceDataType { - id: string; - icon: string; - label: string; - color: string; - isActive: boolean; -} - -// Define the global Jotai atom for menuData -const workspaceData: WorkspaceDataType[] = [ - { - id: "596e76eb-d87d-425e-9f6e-1519069ee446", - icon: "", - label: "Default", - color: "", - isActive: false, - }, - { - id: "596e76eb-d87d-425e-9f6e-1519069ee447", - icon: "shield-cat", - label: "Cat Space", - color: "#e91e63", - isActive: true, - }, - { - id: "596e76eb-d87d-425e-9f6e-1519069ee448", - icon: "paw-simple", - label: "Bear Space", - color: "#ffc107", - isActive: false, - }, -]; - -export const menuDataAtom = atom(workspaceData); +type WorkspaceListEntry = { + windowId: string; + workspace: Workspace; +}; +type WorkspaceList = WorkspaceListEntry[]; +const workspaceMapAtom = atom([]); +const workspaceSplitAtom = splitAtom(workspaceMapAtom); +const editingWorkspaceAtom = atom(); const WorkspaceSwitcher = forwardRef(({}, ref) => { - const [menuData, setMenuData] = useAtom(menuDataAtom); + const setWorkspaceList = useSetAtom(workspaceMapAtom); + const activeWorkspace = useAtomValueSafe(atoms.workspace); + const workspaceList = useAtomValue(workspaceSplitAtom); + const setEditingWorkspace = useSetAtom(editingWorkspaceAtom); - const handleTitleChange = (id: string, newTitle: string) => { - // This should call a service - setMenuData((prevMenuData) => - prevMenuData.map((item) => { - if (item.id === id) { - return { - ...item, - label: newTitle, - }; - } - return item; - }) - ); - }; - - const handleColorChange = (id: string, newColor: string) => { - // This should call a service - setMenuData((prevMenuData) => - prevMenuData.map((item) => { - if (item.id === id) { - return { - ...item, - color: newColor, - }; - } - return item; - }) - ); - }; - - const handleIconChange = (id: string, newIcon: string) => { - // This should call a service - setMenuData((prevMenuData) => - prevMenuData.map((item) => { - if (item.id === id) { - return { - ...item, - icon: newIcon, - }; - } - return item; - }) - ); - }; - - const setActiveWorkspace = (id: string) => { - // This should call a service - setMenuData((prevMenuData) => - prevMenuData.map((item) => { - if (item.id === id) { - return { - ...item, - isActive: true, - }; - } - return { - ...item, - isActive: false, - }; - }) - ); - }; - - const handleAddNewWorkspace = () => { - // This should call a service - const id = `group-${Math.random().toString(36).substr(2, 9)}`; - setMenuData((prevMenuData) => { - const updatedMenuData = prevMenuData.map((item) => ({ - ...item, - isActive: false, - })); - - const newWorkspace = { - id, - icon: "circle", - label: "New Workspace", - color: "#8bc34a", - isActive: true, - }; - - return [...updatedMenuData, newWorkspace]; - }); - }; - - const handleDeleteWorkspace = (id: string) => { - console.log("got here!!!"); - // This should call a service - setMenuData((prevMenuData) => { - const updatedMenuData = prevMenuData.filter((item) => item.id !== id); - console.log("updatedMenuData", updatedMenuData); - - const isAnyActive = updatedMenuData.some((item) => item.isActive); - - if (!isAnyActive && updatedMenuData.length > 0) { - updatedMenuData[0] = { ...updatedMenuData[0], isActive: true }; - } - - return updatedMenuData; - }); - }; - - const activeWorkspace = menuData.find((workspace) => workspace.isActive); - - const data = menuData.map((item): ExpandableMenuItemData => { - const { id, icon, label, color, isActive } = item; - const title: ExpandableMenuItemGroupTitleType = { label }; - const leftElement = icon ? ( - - ) : null; - title.leftElement = leftElement; - title.rightElement = isActive ? : null; - - if (label === "Default") { - return { - id, - type: "group", - title: { - leftElement: , - label: "Default", - rightElement: isActive ? : null, - }, - }; + const updateWorkspaceList = useCallback(async () => { + const workspaceList = await WorkspaceService.ListWorkspaces(); + if (!workspaceList) { + return; } - return { - id, - type: "group", - title, - isOpen: isActive, - children: [ - { - type: "item", - content: ({ isOpen }: { isOpen: boolean }) => ( - handleTitleChange(id, title)} - onColorChange={(color) => handleColorChange(id, color)} - onIconChange={(icon) => handleIconChange(id, icon)} - onDeleteWorkspace={() => handleDeleteWorkspace(id)} - /> - ), - }, - ], - }; - }); + const newList: WorkspaceList = []; + for (const entry of workspaceList) { + // This just ensures that the atom exists for easier setting of the object + getObjectValue(makeORef("workspace", entry.workspaceid)); + newList.push({ + windowId: entry.windowid, + workspace: await WorkspaceService.GetWorkspace(entry.workspaceid), + }); + } + setWorkspaceList(newList); + }, []); - const modWorkspaceColor = - activeWorkspace.label === "Default" - ? "rgba(0, 0, 0, .2)" - : colord(activeWorkspace.color).alpha(0.1).toRgbString(); + useEffect(() => { + fireAndForget(updateWorkspaceList); + }, []); - const renderExpandableMenu = (menuItems: ExpandableMenuItemData[], parentIsOpen?: boolean) => { - return menuItems.map((item, index) => { - if (item.type === "item") { - let contentElement; - if (typeof item.content === "function") { - contentElement = item.content({ isOpen: parentIsOpen }); - } else { - contentElement = item.content; - } - return ( - - {item.leftElement && ( - {item.leftElement} - )} -
{contentElement}
- {item.rightElement && ( - {item.rightElement} - )} -
- ); - } else if (item.type === "group") { - return ( - - setActiveWorkspace(item.id)}> -
- {item.title.leftElement && ( - - {item.title.leftElement} - - )} -
{item.title.label}
- {item.title.rightElement && ( - - {item.title.rightElement} - - )} -
-
- {item.children && item.children.length > 0 && renderExpandableMenu(item.children, item.isOpen)} -
- ); - } - return null; + const onDeleteWorkspace = useCallback((workspaceId: string) => { + fireAndForget(async () => { + getApi().deleteWorkspace(workspaceId); + setTimeout(() => { + fireAndForget(updateWorkspaceList); + }, 10); }); - }; + }, []); - let workspaceIcon = ( + const isActiveWorkspaceSaved = !!(activeWorkspace.name && activeWorkspace.icon); + + const workspaceIcon = isActiveWorkspaceSaved ? ( + ) : ( + ); - if (activeWorkspace.label == "Default") { - workspaceIcon = ; - } + + const saveWorkspace = () => { + setObjectValue({ ...activeWorkspace, name: "New Workspace", icon: "circle", color: "green" }, undefined, true); + setTimeout(() => { + fireAndForget(updateWorkspaceList); + }, 10); + }; return ( - - + setEditingWorkspace(null)}> + { + fireAndForget(updateWorkspaceList); + }} + > {workspaceIcon} - {/* - - - */} -
Switch workspace
+
{isActiveWorkspaceSaved ? "Switch workspace" : "Open workspace"}
- {renderExpandableMenu(data)} + {workspaceList.map((entry, i) => ( + + ))} -
- handleAddNewWorkspace()}> - - - -
New workspace
-
-
+ {!isActiveWorkspaceSaved && ( +
+ saveWorkspace()}> + + + +
Save workspace
+
+
+ )}
); }); +const WorkspaceSwitcherItem = ({ + entryAtom, + onDeleteWorkspace, +}: { + entryAtom: PrimitiveAtom; + onDeleteWorkspace: (workspaceId: string) => void; +}) => { + const activeWorkspace = useAtomValueSafe(atoms.workspace); + const [workspaceEntry, setWorkspaceEntry] = useAtom(entryAtom); + const [editingWorkspace, setEditingWorkspace] = useAtom(editingWorkspaceAtom); + + const workspace = workspaceEntry.workspace; + const isCurrentWorkspace = activeWorkspace.oid === workspace.oid; + + const setWorkspace = useCallback((newWorkspace: Workspace) => { + fireAndForget(async () => { + setObjectValue({ ...newWorkspace, otype: "workspace" }, undefined, true); + setWorkspaceEntry({ ...workspaceEntry, workspace: newWorkspace }); + }); + }, []); + + const isActive = !!workspaceEntry.windowId; + const editIconDecl: IconButtonDecl = { + elemtype: "iconbutton", + className: "edit", + icon: "pencil", + title: "Edit workspace", + click: (e) => { + e.stopPropagation(); + if (editingWorkspace === workspace.oid) { + setEditingWorkspace(null); + } else { + setEditingWorkspace(workspace.oid); + } + }, + }; + const windowIconDecl: IconButtonDecl = { + elemtype: "iconbutton", + className: "window", + disabled: true, + icon: isCurrentWorkspace ? "check" : "window", + title: isCurrentWorkspace ? "This is your current workspace" : "This workspace is open", + }; + + const isEditing = editingWorkspace === workspace.oid; + + return ( + + { + getApi().switchWorkspace(workspace.oid); + // Create a fake escape key event to close the popover + document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })); + }} + > +
+ + + +
{workspace.name}
+ +
+ + {isActive && } +
+
+
+
+ + setWorkspace({ ...workspace, name: title })} + onColorChange={(color) => setWorkspace({ ...workspace, color })} + onIconChange={(icon) => setWorkspace({ ...workspace, icon })} + onDeleteWorkspace={() => onDeleteWorkspace(workspace.oid)} + /> + +
+ ); +}; + export { WorkspaceSwitcher }; diff --git a/frontend/app/theme.scss b/frontend/app/theme.scss index d79da9467..efd389e29 100644 --- a/frontend/app/theme.scss +++ b/frontend/app/theme.scss @@ -5,156 +5,157 @@ @import url("../../node_modules/highlight.js/styles/github-dark-dimmed.min.css"); :root { - --main-text-color: #f7f7f7; - --title-font-size: 18px; - --window-opacity: 1; - --secondary-text-color: rgb(195, 200, 194); - --grey-text-color: #666; - --main-bg-color: rgb(34, 34, 34); - --border-color: rgba(255, 255, 255, 0.16); - --base-font: normal 14px / normal "Inter", sans-serif; - --fixed-font: normal 12px / normal "Hack", monospace; - --accent-color: rgb(88, 193, 66); - --panel-bg-color: rgba(31, 33, 31, 0.5); - --highlight-bg-color: rgba(255, 255, 255, 0.2); - --markdown-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji"; - --error-color: rgb(229, 77, 46); - --warning-color: rgb(224, 185, 86); - --success-color: rgb(78, 154, 6); - --hover-bg-color: rgba(255, 255, 255, 0.1); - --block-bg-color: rgba(0, 0, 0, 0.5); - --block-bg-solid-color: rgb(0, 0, 0); - --block-border-radius: 8px; + --main-text-color: #f7f7f7; + --title-font-size: 18px; + --window-opacity: 1; + --secondary-text-color: rgb(195, 200, 194); + --grey-text-color: #666; + --main-bg-color: rgb(34, 34, 34); + --border-color: rgba(255, 255, 255, 0.16); + --base-font: normal 14px / normal "Inter", sans-serif; + --fixed-font: normal 12px / normal "Hack", monospace; + --accent-color: rgb(88, 193, 66); + --panel-bg-color: rgba(31, 33, 31, 0.5); + --highlight-bg-color: rgba(255, 255, 255, 0.2); + --markdown-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji"; + --error-color: rgb(229, 77, 46); + --warning-color: rgb(224, 185, 86); + --success-color: rgb(78, 154, 6); + --hover-bg-color: rgba(255, 255, 255, 0.1); + --block-bg-color: rgba(0, 0, 0, 0.5); + --block-bg-solid-color: rgb(0, 0, 0); + --block-border-radius: 8px; - --keybinding-color: #e0e0e0; - --keybinding-bg-color: #333; - --keybinding-border-color: #444; + --keybinding-color: #e0e0e0; + --keybinding-bg-color: #333; + --keybinding-border-color: #444; - /* scrollbar colors */ - --scrollbar-background-color: transparent; - --scrollbar-thumb-color: rgba(255, 255, 255, 0.15); - --scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.5); - --scrollbar-thumb-active-color: rgba(255, 255, 255, 0.6); + /* scrollbar colors */ + --scrollbar-background-color: transparent; + --scrollbar-thumb-color: rgba(255, 255, 255, 0.15); + --scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.5); + --scrollbar-thumb-active-color: rgba(255, 255, 255, 0.6); - --header-font: 700 11px / normal "Inter", sans-serif; - --header-icon-size: 14px; - --header-icon-width: 16px; - --header-height: 30px; + --header-font: 700 11px / normal "Inter", sans-serif; + --header-icon-size: 14px; + --header-icon-width: 16px; + --header-height: 30px; - --tab-green: rgb(88, 193, 66); + --tab-green: rgb(88, 193, 66); - /* z-index values */ - --zindex-header-hover: 100; - --zindex-termstickers: 20; - --zindex-modal: 2; - --zindex-modal-wrapper: 500; - --zindex-modal-backdrop: 1; - --zindex-typeahead-modal: 100; - --zindex-typeahead-modal-backdrop: 90; - --zindex-elem-modal: 100; - --zindex-window-drag: 100; - --zindex-tab-name: 3; - --zindex-layout-display-container: 0; - --zindex-layout-last-magnified-node: 1; - --zindex-layout-last-ephemeral-node: 2; - --zindex-layout-resize-handle: 3; - --zindex-layout-placeholder-container: 4; - --zindex-layout-overlay-container: 5; - --zindex-layout-magnified-node-backdrop: 6; - --zindex-layout-magnified-node: 7; - --zindex-layout-ephemeral-node-backdrop: 8; - --zindex-layout-ephemeral-node: 9; - --zindex-block-mask-inner: 10; - --zindex-flash-error-container: 550; - --zindex-app-background: -1; + /* z-index values */ + --zindex-header-hover: 100; + --zindex-termstickers: 20; + --zindex-modal: 2; + --zindex-modal-wrapper: 500; + --zindex-modal-backdrop: 1; + --zindex-typeahead-modal: 100; + --zindex-typeahead-modal-backdrop: 90; + --zindex-elem-modal: 100; + --zindex-window-drag: 100; + --zindex-tab-name: 3; + --zindex-layout-display-container: 0; + --zindex-layout-last-magnified-node: 1; + --zindex-layout-last-ephemeral-node: 2; + --zindex-layout-resize-handle: 3; + --zindex-layout-placeholder-container: 4; + --zindex-layout-overlay-container: 5; + --zindex-layout-magnified-node-backdrop: 6; + --zindex-layout-magnified-node: 7; + --zindex-layout-ephemeral-node-backdrop: 8; + --zindex-layout-ephemeral-node: 9; + --zindex-block-mask-inner: 10; + --zindex-flash-error-container: 550; + --zindex-app-background: -1; - // z-indexes in xterm.css - // xterm-helpers: 5 - // xterm-helper-textarea: -5 - // composition-view: 1 - // xterm-message: 10 - // xterm-decoration: 6 - // xterm-decoration-top-layer: 7 - // xterm-decoration-overview-ruler: 8 - // xterm-decoration-top: 2 - --zindex-xterm-viewport-overlay: 5; // Viewport contains the scrollbar + // z-indexes in xterm.css + // xterm-helpers: 5 + // xterm-helper-textarea: -5 + // composition-view: 1 + // xterm-message: 10 + // xterm-decoration: 6 + // xterm-decoration-top-layer: 7 + // xterm-decoration-overview-ruler: 8 + // xterm-decoration-top: 2 + --zindex-xterm-viewport-overlay: 5; // Viewport contains the scrollbar - // modal colors - --modal-bg-color: #232323; - --modal-header-bottom-border-color: rgba(241, 246, 243, 0.15); - --modal-border-color: rgba(255, 255, 255, 0.12); /* toggle colors */ - --toggle-bg-color: var(--border-color); + // modal colors + --modal-bg-color: #232323; + --modal-header-bottom-border-color: rgba(241, 246, 243, 0.15); + --modal-border-color: rgba(255, 255, 255, 0.12); /* toggle colors */ + --toggle-bg-color: var(--border-color); + --modal-shadow-color: rgba(0, 0, 0, 0.8); - --toggle-thumb-color: var(--main-text-color); - --toggle-checked-bg-color: var(--accent-color); + --toggle-thumb-color: var(--main-text-color); + --toggle-checked-bg-color: var(--accent-color); - // link color - --link-color: #58c142; + // link color + --link-color: #58c142; - // form colors - --form-element-border-color: rgba(241, 246, 243, 0.15); - --form-element-bg-color: var(--main-bg-color); - --form-element-text-color: var(--main-text-color); - --form-element-primary-text-color: var(--main-text-color); - --form-element-primary-color: var(--accent-color); - --form-element-secondary-color: rgba(255, 255, 255, 0.2); - --form-element-error-color: var(--error-color); + // form colors + --form-element-border-color: rgba(241, 246, 243, 0.15); + --form-element-bg-color: var(--main-bg-color); + --form-element-text-color: var(--main-text-color); + --form-element-primary-text-color: var(--main-text-color); + --form-element-primary-color: var(--accent-color); + --form-element-secondary-color: rgba(255, 255, 255, 0.2); + --form-element-error-color: var(--error-color); - --conn-icon-color: #53b4ea; - --conn-icon-color-1: #53b4ea; - --conn-icon-color-2: #aa67ff; - --conn-icon-color-3: #fda7fd; - --conn-icon-color-4: #ef476f; - --conn-icon-color-5: #497bf8; - --conn-icon-color-6: #ffa24e; - --conn-icon-color-7: #dbde52; - --conn-icon-color-8: #58c142; - --conn-status-overlay-bg-color: rgba(230, 186, 30, 0.2); + --conn-icon-color: #53b4ea; + --conn-icon-color-1: #53b4ea; + --conn-icon-color-2: #aa67ff; + --conn-icon-color-3: #fda7fd; + --conn-icon-color-4: #ef476f; + --conn-icon-color-5: #497bf8; + --conn-icon-color-6: #ffa24e; + --conn-icon-color-7: #dbde52; + --conn-icon-color-8: #58c142; + --conn-status-overlay-bg-color: rgba(230, 186, 30, 0.2); - --sysinfo-cpu-color: #58c142; - --sysinfo-mem-color: #53b4ea; + --sysinfo-cpu-color: #58c142; + --sysinfo-mem-color: #53b4ea; - --bulb-color: rgb(255, 221, 51); + --bulb-color: rgb(255, 221, 51); - // term colors (16 + 6) form the base terminal theme - // for consistency these colors should be used by plugins/applications - --term-black: #000000; - --term-red: #cc0000; - --term-green: #4e9a06; - --term-yellow: #c4a000; - --term-blue: #3465a4; - --term-magenta: #bc3fbc; - --term-cyan: #06989a; - --term-white: #d0d0d0; - --term-bright-black: #555753; - --term-bright-red: #ef2929; - --term-bright-green: #58c142; - --term-bright-yellow: #fce94f; - --term-bright-blue: #32afff; - --term-bright-magenta: #ad7fa8; - --term-bright-cyan: #34e2e2; - --term-bright-white: #e7e7e7; + // term colors (16 + 6) form the base terminal theme + // for consistency these colors should be used by plugins/applications + --term-black: #000000; + --term-red: #cc0000; + --term-green: #4e9a06; + --term-yellow: #c4a000; + --term-blue: #3465a4; + --term-magenta: #bc3fbc; + --term-cyan: #06989a; + --term-white: #d0d0d0; + --term-bright-black: #555753; + --term-bright-red: #ef2929; + --term-bright-green: #58c142; + --term-bright-yellow: #fce94f; + --term-bright-blue: #32afff; + --term-bright-magenta: #ad7fa8; + --term-bright-cyan: #34e2e2; + --term-bright-white: #e7e7e7; - --term-gray: #8b918a; // not an official terminal color - --term-cmdtext: #ffffff; - --term-foreground: #d3d7cf; - --term-background: #000000; - --term-selection-background: #ffffff60; - --term-cursor-accent: #000000; + --term-gray: #8b918a; // not an official terminal color + --term-cmdtext: #ffffff; + --term-foreground: #d3d7cf; + --term-background: #000000; + --term-selection-background: #ffffff60; + --term-cursor-accent: #000000; - // button colors - --button-text-color: #000000; - --button-green-bg: var(--term-green); - --button-green-border-color: #29f200; - --button-grey-bg: rgba(255, 255, 255, 0.04); - --button-grey-hover-bg: rgba(255, 255, 255, 0.09); - --button-grey-border-color: rgba(255, 255, 255, 0.1); - --button-grey-outlined-color: rgba(255, 255, 255, 0.6); - --button-red-bg: #cc0000; - --button-red-hover-bg: #f93939; - --button-red-border-color: #fc3131; - --button-red-outlined-color: #ff3c3c; - --button-yellow-bg: #c4a000; - --button-yellow-hover-bg: #fce94f; + // button colors + --button-text-color: #000000; + --button-green-bg: var(--term-green); + --button-green-border-color: #29f200; + --button-grey-bg: rgba(255, 255, 255, 0.04); + --button-grey-hover-bg: rgba(255, 255, 255, 0.09); + --button-grey-border-color: rgba(255, 255, 255, 0.1); + --button-grey-outlined-color: rgba(255, 255, 255, 0.6); + --button-red-bg: #cc0000; + --button-red-hover-bg: #f93939; + --button-red-border-color: #fc3131; + --button-red-outlined-color: #ff3c3c; + --button-yellow-bg: #c4a000; + --button-yellow-hover-bg: #fce94f; } diff --git a/frontend/layout/lib/layoutModelHooks.ts b/frontend/layout/lib/layoutModelHooks.ts index 42597a492..12b840c33 100644 --- a/frontend/layout/lib/layoutModelHooks.ts +++ b/frontend/layout/lib/layoutModelHooks.ts @@ -5,8 +5,7 @@ import { useOnResize } from "@/app/hook/useDimensions"; import { atoms, globalStore, WOS } from "@/app/store/global"; import { fireAndForget } from "@/util/util"; import { Atom, useAtomValue } from "jotai"; -import { CSSProperties, useCallback, useEffect, useLayoutEffect, useState } from "react"; -import { debounce } from "throttle-debounce"; +import { CSSProperties, useCallback, useEffect, useState } from "react"; import { withLayoutTreeStateAtomFromTab } from "./layoutAtom"; import { LayoutModel } from "./layoutModel"; import { LayoutNode, NodeModel, TileLayoutContents } from "./types"; @@ -74,16 +73,29 @@ export function useDebouncedNodeInnerRect(nodeModel: NodeModel): CSSProperties { const isResizing = useAtomValue(nodeModel.isResizing); const prefersReducedMotion = useAtomValue(atoms.prefersReducedMotionAtom); const [innerRect, setInnerRect] = useState(); + const [innerRectDebounceTimeout, setInnerRectDebounceTimeout] = useState(); const setInnerRectDebounced = useCallback( - debounce(animationTimeS * 1000, (nodeInnerRect) => { - setInnerRect(nodeInnerRect); - }), + (nodeInnerRect: CSSProperties) => { + clearInnerRectDebounce(); + setInnerRectDebounceTimeout( + setTimeout(() => { + setInnerRect(nodeInnerRect); + }, animationTimeS * 1000) + ); + }, [animationTimeS] ); + const clearInnerRectDebounce = useCallback(() => { + if (innerRectDebounceTimeout) { + clearTimeout(innerRectDebounceTimeout); + setInnerRectDebounceTimeout(undefined); + } + }, [innerRectDebounceTimeout]); - useLayoutEffect(() => { + useEffect(() => { if (prefersReducedMotion || isMagnified || isResizing) { + clearInnerRectDebounce(); setInnerRect(nodeInnerRect); } else { setInnerRectDebounced(nodeInnerRect); diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 809f4b3d6..19e822a52 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -89,6 +89,8 @@ declare global { setWebviewFocus: (focusedId: number) => void; // focusedId si the getWebContentsId of the webview registerGlobalWebviewKeys: (keys: string[]) => void; onControlShiftStateUpdate: (callback: (state: boolean) => void) => void; + switchWorkspace: (workspaceId: string) => void; + deleteWorkspace: (workspaceId: string) => void; setActiveTab: (tabId: string) => void; createTab: () => void; closeTab: (tabId: string) => void; @@ -334,28 +336,6 @@ declare global { msgFn: (msg: RpcMessage) => void; }; - type WaveBrowserWindow = Electron.BaseWindow & { - waveWindowId: string; - waveReadyPromise: Promise; - allTabViews: Map; - activeTabView: WaveTabView; - alreadyClosed: boolean; - deleteAllowed: boolean; - }; - - type WaveTabView = Electron.WebContentsView & { - isActiveTab: boolean; - waveWindowId: string; // set when showing in an active window - waveTabId: string; // always set, WaveTabViews are unique per tab - lastUsedTs: number; // ts milliseconds - createdTs: number; // ts milliseconds - initPromise: Promise; - savedInitOpts: WaveInitOpts; - waveReadyPromise: Promise; - initResolve: () => void; - waveReadyResolve: () => void; - }; - type TimeSeriesMeta = { name?: string; color?: string; diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 1dca3f680..d68fd93d6 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -5,7 +5,7 @@ declare global { - // telemetry.ActivityDisplayType + // wshrpc.ActivityDisplayType type ActivityDisplayType = { width: number; height: number; @@ -13,7 +13,7 @@ declare global { internal?: boolean; }; - // telemetry.ActivityUpdate + // wshrpc.ActivityUpdate type ActivityUpdate = { fgminutes?: number; activeminutes?: number; @@ -68,7 +68,7 @@ declare global { type BlockInfoData = { blockid: string; tabid: string; - windowid: string; + workspaceid: string; block: Block; }; @@ -84,11 +84,10 @@ declare global { windowids: string[]; tosagreed?: number; hasoldhistory?: boolean; - nexttabid?: number; tempoid?: string; }; - // windowservice.CloseTabRtnType + // workspaceservice.CloseTabRtnType type CloseTabRtnType = { closewindow?: boolean; newactivetabid?: string; @@ -262,7 +261,7 @@ declare global { // wshrpc.CommandWebSelectorData type CommandWebSelectorData = { - windowid: string; + workspaceid: string; blockid: string; tabid: string; selector: string; @@ -275,9 +274,36 @@ declare global { err: string; }; + // wshrpc.ConnKeywords + type ConnKeywords = { + wshenabled?: boolean; + askbeforewshinstall?: boolean; + "ssh:user"?: string; + "ssh:hostname"?: string; + "ssh:port"?: string; + "ssh:identityfile"?: string[]; + "ssh:batchmode"?: boolean; + "ssh:pubkeyauthentication"?: boolean; + "ssh:passwordauthentication"?: boolean; + "ssh:kbdinteractiveauthentication"?: boolean; + "ssh:preferredauthentications"?: string[]; + "ssh:addkeystoagent"?: boolean; + "ssh:identityagent"?: string; + "ssh:proxyjump"?: string[]; + "ssh:userknownhostsfile"?: string[]; + "ssh:globalknownhostsfile"?: string[]; + }; + + // wshrpc.ConnRequest + type ConnRequest = { + host: string; + keywords?: ConnKeywords; + }; + // wshrpc.ConnStatus type ConnStatus = { status: string; + wshenabled: boolean; connection: string; connected: boolean; hasconnected: boolean; @@ -337,9 +363,11 @@ declare global { type FullConfigType = { settings: SettingsType; mimetypes: {[key: string]: MimeTypeConfigType}; + defaultwidgets: {[key: string]: WidgetConfigType}; widgets: {[key: string]: WidgetConfigType}; presets: {[key: string]: MetaType}; termthemes: {[key: string]: TermThemeType}; + connections: {[key: string]: ConnKeywords}; configerrors: ConfigError[]; }; @@ -608,6 +636,7 @@ declare global { "telemetry:enabled"?: boolean; "conn:*"?: boolean; "conn:askbeforewshinstall"?: boolean; + "conn:wshenabled"?: boolean; }; // waveobj.StickerClickOptsType @@ -701,6 +730,8 @@ declare global { timeoutms: number; checkboxmsg: string; publictext: boolean; + oklabel?: string; + cancellabel?: string; }; // userinput.UserInputResponse @@ -1038,7 +1069,6 @@ declare global { // waveobj.Window type WaveWindow = WaveObj & { workspaceid: string; - activetabid: string; isnew?: boolean; pos: Point; winsize: WinSize; @@ -1086,7 +1116,22 @@ declare global { // waveobj.Workspace type Workspace = WaveObj & { name: string; + icon: string; + color: string; tabids: string[]; + activetabid: string; + }; + + // wshrpc.WorkspaceInfoData + type WorkspaceInfoData = { + windowid: string; + workspacedata: Workspace; + }; + + // waveobj.WorkspaceListEntry + type WorkspaceListEntry = { + workspaceid: string; + windowid: string; }; // wshrpc.WshServerCommandMeta diff --git a/package.json b/package.json index 86ee06d42..feb280be7 100644 --- a/package.json +++ b/package.json @@ -30,19 +30,19 @@ "@chromatic-com/storybook": "^3.2.2", "@eslint/js": "^9.15.0", "@rollup/plugin-node-resolve": "^15.3.0", - "@storybook/addon-essentials": "^8.4.5", - "@storybook/addon-interactions": "^8.4.5", - "@storybook/addon-links": "^8.4.5", - "@storybook/blocks": "^8.4.5", - "@storybook/react": "^8.4.5", - "@storybook/react-vite": "^8.4.5", - "@storybook/test": "^8.4.5", - "@storybook/theming": "^8.4.5", + "@storybook/addon-essentials": "^8.4.6", + "@storybook/addon-interactions": "^8.4.6", + "@storybook/addon-links": "^8.4.6", + "@storybook/blocks": "^8.4.6", + "@storybook/react": "^8.4.6", + "@storybook/react-vite": "^8.4.6", + "@storybook/test": "^8.4.6", + "@storybook/theming": "^8.4.6", "@types/color": "^4.2.0", "@types/css-tree": "^2", "@types/debug": "^4", "@types/electron": "^1.6.12", - "@types/node": "^22.9.1", + "@types/node": "^22.10.1", "@types/papaparse": "^5", "@types/pngjs": "^6.0.5", "@types/prop-types": "^15", @@ -55,32 +55,32 @@ "@types/tinycolor2": "^1", "@types/uuid": "^10.0.0", "@types/ws": "^8", - "@vitejs/plugin-react-swc": "^3.7.1", - "@vitest/coverage-istanbul": "^2.1.5", + "@vitejs/plugin-react-swc": "^3.7.2", + "@vitest/coverage-istanbul": "^2.1.6", "electron": "^33.2.0", "electron-builder": "^25.1.8", "electron-vite": "^2.3.0", "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", - "prettier": "^3.3.3", + "prettier": "^3.4.1", "prettier-plugin-jsdoc": "^1.3.0", "prettier-plugin-organize-imports": "^4.1.0", "rollup-plugin-flow": "^1.1.1", "sass": "^1.81.0", "semver": "^7.6.3", - "storybook": "^8.4.5", + "storybook": "^8.4.6", "storybook-dark-mode": "^4.0.2", "ts-node": "^10.9.2", "tslib": "^2.8.1", "tsx": "^4.19.2", "typescript": "^5.7.2", - "typescript-eslint": "^8.15.0", - "vite": "^5.4.11", + "typescript-eslint": "^8.16.0", + "vite": "^6.0.1", "vite-plugin-image-optimizer": "^1.1.8", - "vite-plugin-static-copy": "^2.1.0", + "vite-plugin-static-copy": "^2.2.0", "vite-plugin-svgr": "^4.3.0", "vite-tsconfig-paths": "^5.1.3", - "vitest": "^2.1.5" + "vitest": "^2.1.6" }, "dependencies": { "@floating-ui/react": "^0.26.28", diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index befaf8517..ac1672c38 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -296,7 +296,7 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj if err != nil { return err } - conn := conncontroller.GetConn(credentialCtx, opts, false) + conn := conncontroller.GetConn(credentialCtx, opts, false, &wshrpc.ConnKeywords{}) connStatus := conn.DeriveConnStatus() if connStatus.Status != conncontroller.Status_Connected { return fmt.Errorf("not connected, cannot start shellproc") @@ -538,7 +538,7 @@ func CheckConnStatus(blockId string) error { if err != nil { return fmt.Errorf("error parsing connection name: %w", err) } - conn := conncontroller.GetConn(context.Background(), opts, false) + conn := conncontroller.GetConn(context.Background(), opts, false, &wshrpc.ConnKeywords{}) connStatus := conn.DeriveConnStatus() if connStatus.Status != conncontroller.Status_Connected { return fmt.Errorf("not connected: %s", connStatus.Status) diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index b03507e2e..32d2d02c5 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -52,6 +52,7 @@ var activeConnCounter = &atomic.Int32{} type SSHConn struct { Lock *sync.Mutex Status string + WshEnabled *atomic.Bool Opts *remote.SSHOpts Client *ssh.Client SockName string @@ -290,6 +291,12 @@ type WshInstallOpts struct { NoUserPrompt bool } +type WshInstallSkipError struct{} + +func (wise *WshInstallSkipError) Error() string { + return "skipping wsh installation" +} + func (conn *SSHConn) CheckAndInstallWsh(ctx context.Context, clientDisplayName string, opts *WshInstallOpts) error { if opts == nil { opts = &WshInstallOpts{} @@ -325,12 +332,23 @@ func (conn *SSHConn) CheckAndInstallWsh(ctx context.Context, clientDisplayName s QueryText: queryText, Title: title, Markdown: true, - CheckBoxMsg: "Don't show me this again", + CheckBoxMsg: "Automatically install for all connections", + OkLabel: "Install wsh", + CancelLabel: "No wsh", } response, err := userinput.GetUserInput(ctx, request) - if err != nil || !response.Confirm { + if err != nil { return err } + if !response.Confirm { + meta := make(map[string]any) + meta["wshenabled"] = false + err = wconfig.SetConnectionsConfigValue(conn.GetName(), meta) + if err != nil { + log.Printf("warning: error writing to connections file: %v", err) + } + return &WshInstallSkipError{} + } if response.CheckboxStat { meta := waveobj.MetaMapType{ wconfig.ConfigKey_ConnAskBeforeWshInstall: false, @@ -371,7 +389,7 @@ func (conn *SSHConn) Reconnect(ctx context.Context) error { if err != nil { return err } - return conn.Connect(ctx) + return conn.Connect(ctx, &wshrpc.ConnKeywords{}) } func (conn *SSHConn) WaitForConnect(ctx context.Context) error { @@ -399,7 +417,7 @@ func (conn *SSHConn) WaitForConnect(ctx context.Context) error { } // does not return an error since that error is stored inside of SSHConn -func (conn *SSHConn) Connect(ctx context.Context) error { +func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords) error { var connectAllowed bool conn.WithLock(func() { if conn.Status == Status_Connecting || conn.Status == Status_Connected { @@ -415,13 +433,13 @@ func (conn *SSHConn) Connect(ctx context.Context) error { return fmt.Errorf("cannot connect to %q when status is %q", conn.GetName(), conn.GetStatus()) } conn.FireConnChangeEvent() - err := conn.connectInternal(ctx) + err := conn.connectInternal(ctx, connFlags) conn.WithLock(func() { if err != nil { conn.Status = Status_Error conn.Error = err.Error() conn.close_nolock() - telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{ + telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{ Conn: map[string]int{"ssh:connecterror": 1}, }, "ssh-connconnect") } else { @@ -430,7 +448,7 @@ func (conn *SSHConn) Connect(ctx context.Context) error { if conn.ActiveConnNum == 0 { conn.ActiveConnNum = int(activeConnCounter.Add(1)) } - telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{ + telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{ Conn: map[string]int{"ssh:connect": 1}, }, "ssh-connconnect") } @@ -445,8 +463,8 @@ func (conn *SSHConn) WithLock(fn func()) { fn() } -func (conn *SSHConn) connectInternal(ctx context.Context) error { - client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0) +func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.ConnKeywords) error { + client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0, connFlags) if err != nil { log.Printf("error: failed to connect to client %s: %s\n", conn.GetName(), err) return err @@ -462,15 +480,40 @@ func (conn *SSHConn) connectInternal(ctx context.Context) error { return err } config := wconfig.ReadFullConfig() - installErr := conn.CheckAndInstallWsh(ctx, clientDisplayName, &WshInstallOpts{NoUserPrompt: !config.Settings.ConnAskBeforeWshInstall}) - if installErr != nil { - log.Printf("error: unable to install wsh shell extensions for %s: %v\n", conn.GetName(), err) - return fmt.Errorf("conncontroller %s wsh install error: %v", conn.GetName(), installErr) + enableWsh := config.Settings.ConnWshEnabled + askBeforeInstall := config.Settings.ConnAskBeforeWshInstall + connSettings, ok := config.Connections[conn.GetName()] + if ok { + if connSettings.WshEnabled != nil { + enableWsh = *connSettings.WshEnabled + } + if connSettings.AskBeforeWshInstall != nil { + askBeforeInstall = *connSettings.AskBeforeWshInstall + } } - csErr := conn.StartConnServer() - if csErr != nil { - log.Printf("error: unable to start conn server for %s: %v\n", conn.GetName(), csErr) - return fmt.Errorf("conncontroller %s start wsh connserver error: %v", conn.GetName(), csErr) + if enableWsh { + installErr := conn.CheckAndInstallWsh(ctx, clientDisplayName, &WshInstallOpts{NoUserPrompt: !askBeforeInstall}) + if errors.Is(installErr, &WshInstallSkipError{}) { + // skips are not true errors + conn.WithLock(func() { + conn.WshEnabled.Store(false) + }) + } else if installErr != nil { + log.Printf("error: unable to install wsh shell extensions for %s: %v\n", conn.GetName(), err) + return fmt.Errorf("conncontroller %s wsh install error: %v", conn.GetName(), installErr) + } else { + conn.WshEnabled.Store(true) + } + + if conn.WshEnabled.Load() { + csErr := conn.StartConnServer() + if csErr != nil { + log.Printf("error: unable to start conn server for %s: %v\n", conn.GetName(), csErr) + return fmt.Errorf("conncontroller %s start wsh connserver error: %v", conn.GetName(), csErr) + } + } + } else { + conn.WshEnabled.Store(false) } conn.HasWaiter.Store(true) go conn.waitForDisconnect() @@ -504,16 +547,16 @@ func getConnInternal(opts *remote.SSHOpts) *SSHConn { defer globalLock.Unlock() rtn := clientControllerMap[*opts] if rtn == nil { - rtn = &SSHConn{Lock: &sync.Mutex{}, Status: Status_Init, Opts: opts, HasWaiter: &atomic.Bool{}} + rtn = &SSHConn{Lock: &sync.Mutex{}, Status: Status_Init, WshEnabled: &atomic.Bool{}, Opts: opts, HasWaiter: &atomic.Bool{}} clientControllerMap[*opts] = rtn } return rtn } -func GetConn(ctx context.Context, opts *remote.SSHOpts, shouldConnect bool) *SSHConn { +func GetConn(ctx context.Context, opts *remote.SSHOpts, shouldConnect bool, connFlags *wshrpc.ConnKeywords) *SSHConn { conn := getConnInternal(opts) if conn.Client == nil && shouldConnect { - conn.Connect(ctx) + conn.Connect(ctx, connFlags) } return conn } @@ -527,7 +570,7 @@ func EnsureConnection(ctx context.Context, connName string) error { if err != nil { return fmt.Errorf("error parsing connection name: %w", err) } - conn := GetConn(ctx, connOpts, false) + conn := GetConn(ctx, connOpts, false, &wshrpc.ConnKeywords{}) if conn == nil { return fmt.Errorf("connection not found: %s", connName) } @@ -538,7 +581,7 @@ func EnsureConnection(ctx context.Context, connName string) error { case Status_Connecting: return conn.WaitForConnect(ctx) case Status_Init, Status_Disconnected: - return conn.Connect(ctx) + return conn.Connect(ctx, &wshrpc.ConnKeywords{}) case Status_Error: return fmt.Errorf("connection error: %s", connStatus.Error) default: diff --git a/pkg/remote/sshclient.go b/pkg/remote/sshclient.go index 69fe5cb10..134873073 100644 --- a/pkg/remote/sshclient.go +++ b/pkg/remote/sshclient.go @@ -17,7 +17,6 @@ import ( "os/exec" "os/user" "path/filepath" - "strconv" "strings" "sync" "time" @@ -28,6 +27,7 @@ import ( "github.com/wavetermdev/waveterm/pkg/userinput" "github.com/wavetermdev/waveterm/pkg/util/shellutil" "github.com/wavetermdev/waveterm/pkg/wavebase" + "github.com/wavetermdev/waveterm/pkg/wshrpc" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" xknownhosts "golang.org/x/crypto/ssh/knownhosts" @@ -101,13 +101,13 @@ func createDummySigner() ([]ssh.Signer, error) { // they were successes. An error in this function prevents any other // keys from being attempted. But if there's an error because of a dummy // file, the library can still try again with a new key. -func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords, authSockSignersExt []ssh.Signer, agentClient agent.ExtendedAgent, debugInfo *ConnectionDebugInfo) func() ([]ssh.Signer, error) { +func createPublicKeyCallback(connCtx context.Context, sshKeywords *wshrpc.ConnKeywords, authSockSignersExt []ssh.Signer, agentClient agent.ExtendedAgent, debugInfo *ConnectionDebugInfo) func() ([]ssh.Signer, error) { var identityFiles []string existingKeys := make(map[string][]byte) // checking the file early prevents us from needing to send a // dummy signer if there's a problem with the signer - for _, identityFile := range sshKeywords.IdentityFile { + for _, identityFile := range sshKeywords.SshIdentityFile { filePath, err := wavebase.ExpandHomeDir(identityFile) if err != nil { continue @@ -151,7 +151,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords, if err == nil { signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey) if err == nil { - if sshKeywords.AddKeysToAgent && agentClient != nil { + if sshKeywords.SshAddKeysToAgent && agentClient != nil { agentClient.Add(agent.AddedKey{ PrivateKey: unencryptedPrivateKey, }) @@ -165,7 +165,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords, } // batch mode deactivates user input - if sshKeywords.BatchMode { + if sshKeywords.SshBatchMode { // skip this key and try with the next return createDummySigner() } @@ -194,7 +194,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords, // skip this key and try with the next return createDummySigner() } - if sshKeywords.AddKeysToAgent && agentClient != nil { + if sshKeywords.SshAddKeysToAgent && agentClient != nil { agentClient.Add(agent.AddedKey{ PrivateKey: unencryptedPrivateKey, }) @@ -333,7 +333,14 @@ func createUnknownKeyVerifier(knownHostsFile string, hostname string, remote str return func() (*userinput.UserInputResponse, error) { ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) defer cancelFn() - return userinput.GetUserInput(ctx, request) + resp, err := userinput.GetUserInput(ctx, request) + if err != nil { + return nil, err + } + if !resp.Confirm { + return nil, fmt.Errorf("user selected no") + } + return resp, nil } } @@ -357,7 +364,14 @@ func createMissingKnownHostsVerifier(knownHostsFile string, hostname string, rem return func() (*userinput.UserInputResponse, error) { ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) defer cancelFn() - return userinput.GetUserInput(ctx, request) + resp, err := userinput.GetUserInput(ctx, request) + if err != nil { + return nil, err + } + if !resp.Confirm { + return nil, fmt.Errorf("user selected no") + } + return resp, nil } } @@ -370,9 +384,9 @@ func lineContainsMatch(line []byte, matches [][]byte) bool { return false } -func createHostKeyCallback(sshKeywords *SshKeywords) (ssh.HostKeyCallback, HostKeyAlgorithms, error) { - globalKnownHostsFiles := sshKeywords.GlobalKnownHostsFile - userKnownHostsFiles := sshKeywords.UserKnownHostsFile +func createHostKeyCallback(sshKeywords *wshrpc.ConnKeywords) (ssh.HostKeyCallback, HostKeyAlgorithms, error) { + globalKnownHostsFiles := sshKeywords.SshGlobalKnownHostsFile + userKnownHostsFiles := sshKeywords.SshUserKnownHostsFile osUser, err := user.Current() if err != nil { @@ -536,12 +550,12 @@ func createHostKeyCallback(sshKeywords *SshKeywords) (ssh.HostKeyCallback, HostK return waveHostKeyCallback, hostKeyAlgorithms, nil } -func createClientConfig(connCtx context.Context, sshKeywords *SshKeywords, debugInfo *ConnectionDebugInfo) (*ssh.ClientConfig, error) { - remoteName := sshKeywords.User + "@" + xknownhosts.Normalize(sshKeywords.HostName+":"+sshKeywords.Port) +func createClientConfig(connCtx context.Context, sshKeywords *wshrpc.ConnKeywords, debugInfo *ConnectionDebugInfo) (*ssh.ClientConfig, error) { + remoteName := sshKeywords.SshUser + "@" + xknownhosts.Normalize(sshKeywords.SshHostName+":"+sshKeywords.SshPort) var authSockSigners []ssh.Signer var agentClient agent.ExtendedAgent - conn, err := net.Dial("unix", sshKeywords.IdentityAgent) + conn, err := net.Dial("unix", sshKeywords.SshIdentityAgent) if err != nil { log.Printf("Failed to open Identity Agent Socket: %v", err) } else { @@ -555,20 +569,20 @@ func createClientConfig(connCtx context.Context, sshKeywords *SshKeywords, debug // exclude gssapi-with-mic and hostbased until implemented authMethodMap := map[string]ssh.AuthMethod{ - "publickey": ssh.RetryableAuthMethod(publicKeyCallback, len(sshKeywords.IdentityFile)+len(authSockSigners)), + "publickey": ssh.RetryableAuthMethod(publicKeyCallback, len(sshKeywords.SshIdentityFile)+len(authSockSigners)), "keyboard-interactive": ssh.RetryableAuthMethod(keyboardInteractive, 1), "password": ssh.RetryableAuthMethod(passwordCallback, 1), } // note: batch mode turns off interactive input authMethodActiveMap := map[string]bool{ - "publickey": sshKeywords.PubkeyAuthentication, - "keyboard-interactive": sshKeywords.KbdInteractiveAuthentication && !sshKeywords.BatchMode, - "password": sshKeywords.PasswordAuthentication && !sshKeywords.BatchMode, + "publickey": sshKeywords.SshPubkeyAuthentication, + "keyboard-interactive": sshKeywords.SshKbdInteractiveAuthentication && !sshKeywords.SshBatchMode, + "password": sshKeywords.SshPasswordAuthentication && !sshKeywords.SshBatchMode, } var authMethods []ssh.AuthMethod - for _, authMethodName := range sshKeywords.PreferredAuthentications { + for _, authMethodName := range sshKeywords.SshPreferredAuthentications { authMethodActive, ok := authMethodActiveMap[authMethodName] if !ok || !authMethodActive { continue @@ -585,9 +599,9 @@ func createClientConfig(connCtx context.Context, sshKeywords *SshKeywords, debug return nil, err } - networkAddr := sshKeywords.HostName + ":" + sshKeywords.Port + networkAddr := sshKeywords.SshHostName + ":" + sshKeywords.SshPort return &ssh.ClientConfig{ - User: sshKeywords.User, + User: sshKeywords.SshUser, Auth: authMethods, HostKeyCallback: hostKeyCallback, HostKeyAlgorithms: hostKeyAlgorithms(networkAddr), @@ -616,7 +630,7 @@ func connectInternal(ctx context.Context, networkAddr string, clientConfig *ssh. return ssh.NewClient(c, chans, reqs), nil } -func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.Client, jumpNum int32) (*ssh.Client, int32, error) { +func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.Client, jumpNum int32, connFlags *wshrpc.ConnKeywords) (*ssh.Client, int32, error) { debugInfo := &ConnectionDebugInfo{ CurrentClient: currentClient, NextOpts: opts, @@ -631,12 +645,16 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh. return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} } - sshKeywords, err := combineSshKeywords(opts, sshConfigKeywords) + connFlags.SshUser = opts.SSHUser + connFlags.SshHostName = opts.SSHHost + connFlags.SshPort = fmt.Sprintf("%d", opts.SSHPort) + + sshKeywords, err := combineSshKeywords(connFlags, sshConfigKeywords) if err != nil { return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} } - for _, proxyName := range sshKeywords.ProxyJump { + for _, proxyName := range sshKeywords.SshProxyJump { proxyOpts, err := ParseOpts(proxyName) if err != nil { return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} @@ -647,7 +665,8 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh. jumpNum += 1 } - debugInfo.CurrentClient, jumpNum, err = ConnectToClient(connCtx, proxyOpts, debugInfo.CurrentClient, jumpNum) + // do not apply supplied keywords to proxies - ssh config must be used for that + debugInfo.CurrentClient, jumpNum, err = ConnectToClient(connCtx, proxyOpts, debugInfo.CurrentClient, jumpNum, &wshrpc.ConnKeywords{}) if err != nil { // do not add a context on a recursive call // (this can cause a recursive nested context that's arbitrarily deep) @@ -658,7 +677,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh. if err != nil { return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} } - networkAddr := sshKeywords.HostName + ":" + sshKeywords.Port + networkAddr := sshKeywords.SshHostName + ":" + sshKeywords.SshPort client, err := connectInternal(connCtx, networkAddr, clientConfig, debugInfo.CurrentClient) if err != nil { return client, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} @@ -666,68 +685,51 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh. return client, debugInfo.JumpNum, nil } -type SshKeywords struct { - User string - HostName string - Port string - IdentityFile []string - BatchMode bool - PubkeyAuthentication bool - PasswordAuthentication bool - KbdInteractiveAuthentication bool - PreferredAuthentications []string - AddKeysToAgent bool - IdentityAgent string - ProxyJump []string - UserKnownHostsFile []string - GlobalKnownHostsFile []string -} +func combineSshKeywords(userProvidedOpts *wshrpc.ConnKeywords, configKeywords *wshrpc.ConnKeywords) (*wshrpc.ConnKeywords, error) { + sshKeywords := &wshrpc.ConnKeywords{} -func combineSshKeywords(opts *SSHOpts, configKeywords *SshKeywords) (*SshKeywords, error) { - sshKeywords := &SshKeywords{} - - if opts.SSHUser != "" { - sshKeywords.User = opts.SSHUser - } else if configKeywords.User != "" { - sshKeywords.User = configKeywords.User + if userProvidedOpts.SshUser != "" { + sshKeywords.SshUser = userProvidedOpts.SshUser + } else if configKeywords.SshUser != "" { + sshKeywords.SshUser = configKeywords.SshUser } else { user, err := user.Current() if err != nil { return nil, fmt.Errorf("failed to get user for ssh: %+v", err) } - sshKeywords.User = user.Username + sshKeywords.SshUser = user.Username } // we have to check the host value because of the weird way // we store the pattern as the hostname for imported remotes - if configKeywords.HostName != "" { - sshKeywords.HostName = configKeywords.HostName + if configKeywords.SshHostName != "" { + sshKeywords.SshHostName = configKeywords.SshHostName } else { - sshKeywords.HostName = opts.SSHHost + sshKeywords.SshHostName = userProvidedOpts.SshHostName } - if opts.SSHPort != 0 && opts.SSHPort != 22 { - sshKeywords.Port = strconv.Itoa(opts.SSHPort) - } else if configKeywords.Port != "" && configKeywords.Port != "22" { - sshKeywords.Port = configKeywords.Port + if userProvidedOpts.SshPort != "0" && userProvidedOpts.SshPort != "22" { + sshKeywords.SshPort = userProvidedOpts.SshPort + } else if configKeywords.SshPort != "" && configKeywords.SshPort != "22" { + sshKeywords.SshPort = configKeywords.SshPort } else { - sshKeywords.Port = "22" + sshKeywords.SshPort = "22" } - sshKeywords.IdentityFile = configKeywords.IdentityFile + sshKeywords.SshIdentityFile = append(userProvidedOpts.SshIdentityFile, configKeywords.SshIdentityFile...) // these are not officially supported in the waveterm frontend but can be configured // in ssh config files - sshKeywords.BatchMode = configKeywords.BatchMode - sshKeywords.PubkeyAuthentication = configKeywords.PubkeyAuthentication - sshKeywords.PasswordAuthentication = configKeywords.PasswordAuthentication - sshKeywords.KbdInteractiveAuthentication = configKeywords.KbdInteractiveAuthentication - sshKeywords.PreferredAuthentications = configKeywords.PreferredAuthentications - sshKeywords.AddKeysToAgent = configKeywords.AddKeysToAgent - sshKeywords.IdentityAgent = configKeywords.IdentityAgent - sshKeywords.ProxyJump = configKeywords.ProxyJump - sshKeywords.UserKnownHostsFile = configKeywords.UserKnownHostsFile - sshKeywords.GlobalKnownHostsFile = configKeywords.GlobalKnownHostsFile + sshKeywords.SshBatchMode = configKeywords.SshBatchMode + sshKeywords.SshPubkeyAuthentication = configKeywords.SshPubkeyAuthentication + sshKeywords.SshPasswordAuthentication = configKeywords.SshPasswordAuthentication + sshKeywords.SshKbdInteractiveAuthentication = configKeywords.SshKbdInteractiveAuthentication + sshKeywords.SshPreferredAuthentications = configKeywords.SshPreferredAuthentications + sshKeywords.SshAddKeysToAgent = configKeywords.SshAddKeysToAgent + sshKeywords.SshIdentityAgent = configKeywords.SshIdentityAgent + sshKeywords.SshProxyJump = configKeywords.SshProxyJump + sshKeywords.SshUserKnownHostsFile = configKeywords.SshUserKnownHostsFile + sshKeywords.SshGlobalKnownHostsFile = configKeywords.SshGlobalKnownHostsFile return sshKeywords, nil } @@ -735,59 +737,60 @@ func combineSshKeywords(opts *SSHOpts, configKeywords *SshKeywords) (*SshKeyword // note that a `var == "yes"` will default to false // but `var != "no"` will default to true // when given unexpected strings -func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) { +func findSshConfigKeywords(hostPattern string) (*wshrpc.ConnKeywords, error) { WaveSshConfigUserSettings().ReloadConfigs() - sshKeywords := &SshKeywords{} + sshKeywords := &wshrpc.ConnKeywords{} var err error + //config := wconfig.ReadFullConfig() userRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "User") if err != nil { return nil, err } - sshKeywords.User = trimquotes.TryTrimQuotes(userRaw) + sshKeywords.SshUser = trimquotes.TryTrimQuotes(userRaw) hostNameRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "HostName") if err != nil { return nil, err } - sshKeywords.HostName = trimquotes.TryTrimQuotes(hostNameRaw) + sshKeywords.SshHostName = trimquotes.TryTrimQuotes(hostNameRaw) portRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "Port") if err != nil { return nil, err } - sshKeywords.Port = trimquotes.TryTrimQuotes(portRaw) + sshKeywords.SshPort = trimquotes.TryTrimQuotes(portRaw) identityFileRaw := WaveSshConfigUserSettings().GetAll(hostPattern, "IdentityFile") for i := 0; i < len(identityFileRaw); i++ { identityFileRaw[i] = trimquotes.TryTrimQuotes(identityFileRaw[i]) } - sshKeywords.IdentityFile = identityFileRaw + sshKeywords.SshIdentityFile = identityFileRaw batchModeRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "BatchMode") if err != nil { return nil, err } - sshKeywords.BatchMode = (strings.ToLower(trimquotes.TryTrimQuotes(batchModeRaw)) == "yes") + sshKeywords.SshBatchMode = (strings.ToLower(trimquotes.TryTrimQuotes(batchModeRaw)) == "yes") // we currently do not support host-bound or unbound but will use yes when they are selected pubkeyAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "PubkeyAuthentication") if err != nil { return nil, err } - sshKeywords.PubkeyAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(pubkeyAuthenticationRaw)) != "no") + sshKeywords.SshPubkeyAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(pubkeyAuthenticationRaw)) != "no") passwordAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "PasswordAuthentication") if err != nil { return nil, err } - sshKeywords.PasswordAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(passwordAuthenticationRaw)) != "no") + sshKeywords.SshPasswordAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(passwordAuthenticationRaw)) != "no") kbdInteractiveAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "KbdInteractiveAuthentication") if err != nil { return nil, err } - sshKeywords.KbdInteractiveAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(kbdInteractiveAuthenticationRaw)) != "no") + sshKeywords.SshKbdInteractiveAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(kbdInteractiveAuthenticationRaw)) != "no") // these are parsed as a single string and must be separated // these are case sensitive in openssh so they are here too @@ -795,12 +798,12 @@ func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) { if err != nil { return nil, err } - sshKeywords.PreferredAuthentications = strings.Split(trimquotes.TryTrimQuotes(preferredAuthenticationsRaw), ",") + sshKeywords.SshPreferredAuthentications = strings.Split(trimquotes.TryTrimQuotes(preferredAuthenticationsRaw), ",") addKeysToAgentRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "AddKeysToAgent") if err != nil { return nil, err } - sshKeywords.AddKeysToAgent = (strings.ToLower(trimquotes.TryTrimQuotes(addKeysToAgentRaw)) == "yes") + sshKeywords.SshAddKeysToAgent = (strings.ToLower(trimquotes.TryTrimQuotes(addKeysToAgentRaw)) == "yes") identityAgentRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "IdentityAgent") if err != nil { @@ -815,7 +818,7 @@ func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) { if err != nil { return nil, err } - sshKeywords.IdentityAgent = agentPath + sshKeywords.SshIdentityAgent = agentPath } else { log.Printf("unable to find SSH_AUTH_SOCK: %v\n", err) } @@ -824,7 +827,7 @@ func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) { if err != nil { return nil, err } - sshKeywords.IdentityAgent = agentPath + sshKeywords.SshIdentityAgent = agentPath } proxyJumpRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "ProxyJump") @@ -837,12 +840,12 @@ func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) { if proxyJumpName == "" || strings.ToLower(proxyJumpName) == "none" { continue } - sshKeywords.ProxyJump = append(sshKeywords.ProxyJump, proxyJumpName) + sshKeywords.SshProxyJump = append(sshKeywords.SshProxyJump, proxyJumpName) } rawUserKnownHostsFile, _ := WaveSshConfigUserSettings().GetStrict(hostPattern, "UserKnownHostsFile") - sshKeywords.UserKnownHostsFile = strings.Fields(rawUserKnownHostsFile) // TODO - smarter splitting escaped spaces and quotes + sshKeywords.SshUserKnownHostsFile = strings.Fields(rawUserKnownHostsFile) // TODO - smarter splitting escaped spaces and quotes rawGlobalKnownHostsFile, _ := WaveSshConfigUserSettings().GetStrict(hostPattern, "GlobalKnownHostsFile") - sshKeywords.GlobalKnownHostsFile = strings.Fields(rawGlobalKnownHostsFile) // TODO - smarter splitting escaped spaces and quotes + sshKeywords.SshGlobalKnownHostsFile = strings.Fields(rawGlobalKnownHostsFile) // TODO - smarter splitting escaped spaces and quotes return sshKeywords, nil } diff --git a/pkg/service/clientservice/clientservice.go b/pkg/service/clientservice/clientservice.go index b8ec51fbd..ba5a52867 100644 --- a/pkg/service/clientservice/clientservice.go +++ b/pkg/service/clientservice/clientservice.go @@ -10,7 +10,6 @@ import ( "time" "github.com/wavetermdev/waveterm/pkg/remote/conncontroller" - "github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/wcloud" "github.com/wavetermdev/waveterm/pkg/wconfig" @@ -26,23 +25,10 @@ type ClientService struct{} const DefaultTimeout = 2 * time.Second func (cs *ClientService) GetClientData() (*waveobj.Client, error) { + log.Println("GetClientData") ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) defer cancelFn() - clientData, err := wstore.DBGetSingleton[*waveobj.Client](ctx) - if err != nil { - return nil, fmt.Errorf("error getting client data: %w", err) - } - return clientData, nil -} - -func (cs *ClientService) GetWorkspace(workspaceId string) (*waveobj.Workspace, error) { - ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) - defer cancelFn() - ws, err := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId) - if err != nil { - return nil, fmt.Errorf("error getting workspace: %w", err) - } - return ws, nil + return wcore.GetClientData(ctx) } func (cs *ClientService) GetTab(tabId string) (*waveobj.Tab, error) { @@ -55,28 +41,6 @@ func (cs *ClientService) GetTab(tabId string) (*waveobj.Tab, error) { return tab, nil } -func (cs *ClientService) GetWindow(windowId string) (*waveobj.Window, error) { - ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) - defer cancelFn() - window, err := wstore.DBGet[*waveobj.Window](ctx, windowId) - if err != nil { - return nil, fmt.Errorf("error getting window: %w", err) - } - return window, nil -} - -func (cs *ClientService) MakeWindow(ctx context.Context) (*waveobj.Window, error) { - window, err := wcore.CreateWindow(ctx, nil) - if err != nil { - return nil, err - } - err = wlayout.BootstrapNewWindowLayout(ctx, window) - if err != nil { - return window, err - } - return window, nil -} - func (cs *ClientService) GetAllConnStatus(ctx context.Context) ([]wshrpc.ConnStatus, error) { sshStatuses := conncontroller.GetAllConnStatus() wslStatuses := wsl.GetAllConnStatus() @@ -85,16 +49,8 @@ func (cs *ClientService) GetAllConnStatus(ctx context.Context) ([]wshrpc.ConnSta // moves the window to the front of the windowId stack func (cs *ClientService) FocusWindow(ctx context.Context, windowId string) error { - client, err := cs.GetClientData() - if err != nil { - return err - } - winIdx := utilfn.SliceIdx(client.WindowIds, windowId) - if winIdx == -1 { - return nil - } - client.WindowIds = utilfn.MoveSliceIdxToFront(client.WindowIds, winIdx) - return wstore.DBUpdate(ctx, client) + log.Printf("FocusWindow %s\n", windowId) + return wcore.FocusWindow(ctx, windowId) } func (cs *ClientService) AgreeTos(ctx context.Context) (waveobj.UpdatesRtnType, error) { diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go index 4e69fe9a4..6b1bc99d1 100644 --- a/pkg/service/objectservice/objectservice.go +++ b/pkg/service/objectservice/objectservice.go @@ -9,12 +9,9 @@ import ( "strings" "time" - "github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/tsgen/tsgenmeta" "github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/wcore" - "github.com/wavetermdev/waveterm/pkg/wlayout" - "github.com/wavetermdev/waveterm/pkg/wps" "github.com/wavetermdev/waveterm/pkg/wstore" ) @@ -74,86 +71,6 @@ func (svc *ObjectService) GetObjects(orefStrArr []string) ([]waveobj.WaveObj, er return wstore.DBSelectORefs(ctx, orefArr) } -func (svc *ObjectService) AddTabToWorkspace_Meta() tsgenmeta.MethodMeta { - return tsgenmeta.MethodMeta{ - ArgNames: []string{"windowId", "tabName", "activateTab"}, - ReturnDesc: "tabId", - } -} - -func (svc *ObjectService) AddTabToWorkspace(windowId string, tabName string, activateTab bool) (string, waveobj.UpdatesRtnType, error) { - ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) - defer cancelFn() - ctx = waveobj.ContextWithUpdates(ctx) - tabId, err := wcore.CreateTab(ctx, windowId, tabName, activateTab) - if err != nil { - return "", nil, fmt.Errorf("error creating tab: %w", err) - } - err = wlayout.ApplyPortableLayout(ctx, tabId, wlayout.GetNewTabLayout()) - if err != nil { - return "", nil, fmt.Errorf("error applying new tab layout: %w", err) - } - updates := waveobj.ContextGetUpdatesRtn(ctx) - go func() { - defer panichandler.PanicHandler("ObjectService:AddTabToWorkspace:SendUpdateEvents") - wps.Broker.SendUpdateEvents(updates) - }() - return tabId, updates, nil -} - -func (svc *ObjectService) UpdateWorkspaceTabIds_Meta() tsgenmeta.MethodMeta { - return tsgenmeta.MethodMeta{ - ArgNames: []string{"uiContext", "workspaceId", "tabIds"}, - } -} - -func (svc *ObjectService) UpdateWorkspaceTabIds(uiContext waveobj.UIContext, workspaceId string, tabIds []string) (waveobj.UpdatesRtnType, error) { - ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) - defer cancelFn() - ctx = waveobj.ContextWithUpdates(ctx) - err := wstore.UpdateWorkspaceTabIds(ctx, workspaceId, tabIds) - if err != nil { - return nil, fmt.Errorf("error updating workspace tab ids: %w", err) - } - return waveobj.ContextGetUpdatesRtn(ctx), nil -} - -func (svc *ObjectService) SetActiveTab_Meta() tsgenmeta.MethodMeta { - return tsgenmeta.MethodMeta{ - ArgNames: []string{"uiContext", "tabId"}, - } -} - -func (svc *ObjectService) SetActiveTab(windowId string, tabId string) (waveobj.UpdatesRtnType, error) { - ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) - defer cancelFn() - ctx = waveobj.ContextWithUpdates(ctx) - err := wstore.SetActiveTab(ctx, windowId, tabId) - if err != nil { - return nil, fmt.Errorf("error setting active tab: %w", err) - } - // check all blocks in tab and start controllers (if necessary) - tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId) - if err != nil { - return nil, fmt.Errorf("error getting tab: %w", err) - } - blockORefs := tab.GetBlockORefs() - blocks, err := wstore.DBSelectORefs(ctx, blockORefs) - if err != nil { - return nil, fmt.Errorf("error getting tab blocks: %w", err) - } - updates := waveobj.ContextGetUpdatesRtn(ctx) - go func() { - defer panichandler.PanicHandler("ObjectService:SetActiveTab:SendUpdateEvents") - wps.Broker.SendUpdateEvents(updates) - }() - var extraUpdates waveobj.UpdatesRtnType - extraUpdates = append(extraUpdates, updates...) - extraUpdates = append(extraUpdates, waveobj.MakeUpdate(tab)) - extraUpdates = append(extraUpdates, waveobj.MakeUpdates(blocks)...) - return extraUpdates, nil -} - func (svc *ObjectService) UpdateTabName_Meta() tsgenmeta.MethodMeta { return tsgenmeta.MethodMeta{ ArgNames: []string{"uiContext", "tabId", "name"}, diff --git a/pkg/service/service.go b/pkg/service/service.go index ddbf21494..c02979ee9 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -15,6 +15,7 @@ import ( "github.com/wavetermdev/waveterm/pkg/service/objectservice" "github.com/wavetermdev/waveterm/pkg/service/userinputservice" "github.com/wavetermdev/waveterm/pkg/service/windowservice" + "github.com/wavetermdev/waveterm/pkg/service/workspaceservice" "github.com/wavetermdev/waveterm/pkg/tsgen/tsgenmeta" "github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/waveobj" @@ -27,6 +28,7 @@ var ServiceMap = map[string]any{ "file": &fileservice.FileService{}, "client": &clientservice.ClientService{}, "window": &windowservice.WindowService{}, + "workspace": &workspaceservice.WorkspaceService{}, "userinput": &userinputservice.UserInputService{}, } diff --git a/pkg/service/windowservice/windowservice.go b/pkg/service/windowservice/windowservice.go index 5366d12b5..ed8345395 100644 --- a/pkg/service/windowservice/windowservice.go +++ b/pkg/service/windowservice/windowservice.go @@ -9,11 +9,9 @@ import ( "log" "time" - "github.com/wavetermdev/waveterm/pkg/blockcontroller" "github.com/wavetermdev/waveterm/pkg/eventbus" "github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/tsgen/tsgenmeta" - "github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/wcore" "github.com/wavetermdev/waveterm/pkg/wlayout" @@ -25,6 +23,61 @@ const DefaultTimeout = 2 * time.Second type WindowService struct{} +func (svc *WindowService) GetWindow_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ArgNames: []string{"windowId"}, + } +} + +func (svc *WindowService) GetWindow(windowId string) (*waveobj.Window, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + window, err := wstore.DBGet[*waveobj.Window](ctx, windowId) + if err != nil { + return nil, fmt.Errorf("error getting window: %w", err) + } + return window, nil +} + +func (svc *WindowService) CreateWindow_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ArgNames: []string{"ctx", "winSize", "workspaceId"}, + } +} + +func (svc *WindowService) CreateWindow(ctx context.Context, winSize *waveobj.WinSize, workspaceId string) (*waveobj.Window, error) { + window, err := wcore.CreateWindow(ctx, winSize, workspaceId) + if err != nil { + return nil, fmt.Errorf("error creating window: %w", err) + } + ws, err := wcore.GetWorkspace(ctx, window.WorkspaceId) + if err != nil { + return nil, fmt.Errorf("error getting workspace: %w", err) + } + if len(ws.TabIds) == 0 { + _, err = wcore.CreateTab(ctx, ws.OID, "", true) + if err != nil { + return window, fmt.Errorf("error creating tab: %w", err) + } + ws, err = wcore.GetWorkspace(ctx, window.WorkspaceId) + if err != nil { + return nil, fmt.Errorf("error getting updated workspace: %w", err) + } + err = wlayout.BootstrapNewWorkspaceLayout(ctx, ws) + if err != nil { + return window, fmt.Errorf("error bootstrapping new workspace layout: %w", err) + } + } + return window, nil +} + +func (svc *WindowService) SetWindowPosAndSize_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + Desc: "set window position and size", + ArgNames: []string{"ctx", "windowId", "pos", "size"}, + } +} + func (ws *WindowService) SetWindowPosAndSize(ctx context.Context, windowId string, pos *waveobj.Point, size *waveobj.WinSize) (waveobj.UpdatesRtnType, error) { if pos == nil && size == nil { return nil, nil @@ -48,73 +101,6 @@ func (ws *WindowService) SetWindowPosAndSize(ctx context.Context, windowId strin return waveobj.ContextGetUpdatesRtn(ctx), nil } -type CloseTabRtnType struct { - CloseWindow bool `json:"closewindow,omitempty"` - NewActiveTabId string `json:"newactivetabid,omitempty"` -} - -// returns the new active tabid -func (svc *WindowService) CloseTab(ctx context.Context, windowId string, tabId string, fromElectron bool) (*CloseTabRtnType, waveobj.UpdatesRtnType, error) { - ctx = waveobj.ContextWithUpdates(ctx) - window, err := wstore.DBMustGet[*waveobj.Window](ctx, windowId) - if err != nil { - return nil, nil, fmt.Errorf("error getting window: %w", err) - } - tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId) - if err != nil { - return nil, nil, fmt.Errorf("error getting tab: %w", err) - } - ws, err := wstore.DBMustGet[*waveobj.Workspace](ctx, window.WorkspaceId) - if err != nil { - return nil, nil, fmt.Errorf("error getting workspace: %w", err) - } - tabIndex := -1 - for i, id := range ws.TabIds { - if id == tabId { - tabIndex = i - break - } - } - go func() { - defer panichandler.PanicHandler("WindowService:CloseTab:StopBlockControllers") - for _, blockId := range tab.BlockIds { - blockcontroller.StopBlockController(blockId) - } - }() - if err := wcore.DeleteTab(ctx, window.WorkspaceId, tabId); err != nil { - return nil, nil, fmt.Errorf("error closing tab: %w", err) - } - rtn := &CloseTabRtnType{} - if window.ActiveTabId == tabId && tabIndex != -1 { - if len(ws.TabIds) == 1 { - rtn.CloseWindow = true - svc.CloseWindow(ctx, windowId, fromElectron) - if !fromElectron { - eventbus.SendEventToElectron(eventbus.WSEventType{ - EventType: eventbus.WSEvent_ElectronCloseWindow, - Data: windowId, - }) - } - } else { - if tabIndex < len(ws.TabIds)-1 { - newActiveTabId := ws.TabIds[tabIndex+1] - wstore.SetActiveTab(ctx, windowId, newActiveTabId) - rtn.NewActiveTabId = newActiveTabId - } else { - newActiveTabId := ws.TabIds[tabIndex-1] - wstore.SetActiveTab(ctx, windowId, newActiveTabId) - rtn.NewActiveTabId = newActiveTabId - } - } - } - updates := waveobj.ContextGetUpdatesRtn(ctx) - go func() { - defer panichandler.PanicHandler("WindowService:CloseTab:SendUpdateEvents") - wps.Broker.SendUpdateEvents(updates) - }() - return rtn, updates, nil -} - func (svc *WindowService) MoveBlockToNewWindow_Meta() tsgenmeta.MethodMeta { return tsgenmeta.MethodMeta{ Desc: "move block to new window", @@ -140,11 +126,15 @@ func (svc *WindowService) MoveBlockToNewWindow(ctx context.Context, currentTabId if !foundBlock { return nil, fmt.Errorf("block not found in current tab") } - newWindow, err := wcore.CreateWindow(ctx, nil) + newWindow, err := wcore.CreateWindow(ctx, nil, "") if err != nil { return nil, fmt.Errorf("error creating window: %w", err) } - err = wstore.MoveBlockToTab(ctx, currentTabId, newWindow.ActiveTabId, blockId) + ws, err := wcore.GetWorkspace(ctx, newWindow.WorkspaceId) + if err != nil { + return nil, fmt.Errorf("error getting workspace: %w", err) + } + err = wstore.MoveBlockToTab(ctx, currentTabId, ws.ActiveTabId, blockId) if err != nil { return nil, fmt.Errorf("error moving block to tab: %w", err) } @@ -160,7 +150,7 @@ func (svc *WindowService) MoveBlockToNewWindow(ctx context.Context, currentTabId ActionType: wlayout.LayoutActionDataType_Remove, BlockId: blockId, }) - wlayout.QueueLayoutActionForTab(ctx, newWindow.ActiveTabId, waveobj.LayoutActionData{ + wlayout.QueueLayoutActionForTab(ctx, ws.ActiveTabId, waveobj.LayoutActionData{ ActionType: wlayout.LayoutActionDataType_Insert, BlockId: blockId, Focused: true, @@ -168,38 +158,31 @@ func (svc *WindowService) MoveBlockToNewWindow(ctx context.Context, currentTabId return waveobj.ContextGetUpdatesRtn(ctx), nil } +func (svc *WindowService) SwitchWorkspace_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ArgNames: []string{"ctx", "windowId", "workspaceId"}, + } +} + +func (svc *WindowService) SwitchWorkspace(ctx context.Context, windowId string, workspaceId string) (*waveobj.Workspace, error) { + ctx = waveobj.ContextWithUpdates(ctx) + ws, err := wcore.SwitchWorkspace(ctx, windowId, workspaceId) + + updates := waveobj.ContextGetUpdatesRtn(ctx) + go func() { + defer panichandler.PanicHandler("WindowService:SwitchWorkspace:SendUpdateEvents") + wps.Broker.SendUpdateEvents(updates) + }() + return ws, err +} + +func (svc *WindowService) CloseWindow_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ArgNames: []string{"ctx", "windowId", "fromElectron"}, + } +} + func (svc *WindowService) CloseWindow(ctx context.Context, windowId string, fromElectron bool) error { ctx = waveobj.ContextWithUpdates(ctx) - window, err := wstore.DBMustGet[*waveobj.Window](ctx, windowId) - if err != nil { - return fmt.Errorf("error getting window: %w", err) - } - workspace, err := wstore.DBMustGet[*waveobj.Workspace](ctx, window.WorkspaceId) - if err != nil { - return fmt.Errorf("error getting workspace: %w", err) - } - for _, tabId := range workspace.TabIds { - _, _, err := svc.CloseTab(ctx, windowId, tabId, fromElectron) - if err != nil { - return fmt.Errorf("error closing tab: %w", err) - } - } - err = wstore.DBDelete(ctx, waveobj.OType_Workspace, window.WorkspaceId) - if err != nil { - return fmt.Errorf("error deleting workspace: %w", err) - } - err = wstore.DBDelete(ctx, waveobj.OType_Window, windowId) - if err != nil { - return fmt.Errorf("error deleting window: %w", err) - } - client, err := wstore.DBGetSingleton[*waveobj.Client](ctx) - if err != nil { - return fmt.Errorf("error getting client: %w", err) - } - client.WindowIds = utilfn.RemoveElemFromSlice(client.WindowIds, windowId) - err = wstore.DBUpdate(ctx, client) - if err != nil { - return fmt.Errorf("error updating client: %w", err) - } - return nil + return wcore.CloseWindow(ctx, windowId, fromElectron) } diff --git a/pkg/service/workspaceservice/workspaceservice.go b/pkg/service/workspaceservice/workspaceservice.go new file mode 100644 index 000000000..1c4f55b59 --- /dev/null +++ b/pkg/service/workspaceservice/workspaceservice.go @@ -0,0 +1,196 @@ +package workspaceservice + +import ( + "context" + "fmt" + "time" + + "github.com/wavetermdev/waveterm/pkg/blockcontroller" + "github.com/wavetermdev/waveterm/pkg/panichandler" + "github.com/wavetermdev/waveterm/pkg/tsgen/tsgenmeta" + "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wcore" + "github.com/wavetermdev/waveterm/pkg/wlayout" + "github.com/wavetermdev/waveterm/pkg/wps" + "github.com/wavetermdev/waveterm/pkg/wstore" +) + +const DefaultTimeout = 2 * time.Second + +type WorkspaceService struct{} + +func (svc *WorkspaceService) GetWorkspace_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ArgNames: []string{"workspaceId"}, + } +} + +func (svc *WorkspaceService) GetWorkspace(workspaceId string) (*waveobj.Workspace, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + ws, err := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId) + if err != nil { + return nil, fmt.Errorf("error getting workspace: %w", err) + } + return ws, nil +} + +func (svc *WorkspaceService) DeleteWorkspace_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ArgNames: []string{"workspaceId"}, + } +} + +func (svc *WorkspaceService) DeleteWorkspace(workspaceId string) (waveobj.UpdatesRtnType, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + ctx = waveobj.ContextWithUpdates(ctx) + deleted, err := wcore.DeleteWorkspace(ctx, workspaceId, true) + if err != nil { + return nil, fmt.Errorf("error deleting workspace: %w", err) + } + if !deleted { + return nil, nil + } + updates := waveobj.ContextGetUpdatesRtn(ctx) + go func() { + defer panichandler.PanicHandler("WorkspaceService:DeleteWorkspace:SendUpdateEvents") + wps.Broker.SendUpdateEvents(updates) + }() + return updates, nil +} + +func (svg *WorkspaceService) ListWorkspaces() (waveobj.WorkspaceList, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + return wcore.ListWorkspaces(ctx) +} + +func (svc *WorkspaceService) CreateTab_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ArgNames: []string{"workspaceId", "tabName", "activateTab"}, + ReturnDesc: "tabId", + } +} + +func (svc *WorkspaceService) CreateTab(workspaceId string, tabName string, activateTab bool) (string, waveobj.UpdatesRtnType, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + ctx = waveobj.ContextWithUpdates(ctx) + tabId, err := wcore.CreateTab(ctx, workspaceId, tabName, activateTab) + if err != nil { + return "", nil, fmt.Errorf("error creating tab: %w", err) + } + err = wlayout.ApplyPortableLayout(ctx, tabId, wlayout.GetNewTabLayout()) + if err != nil { + return "", nil, fmt.Errorf("error applying new tab layout: %w", err) + } + updates := waveobj.ContextGetUpdatesRtn(ctx) + go func() { + defer panichandler.PanicHandler("WorkspaceService:CreateTab:SendUpdateEvents") + wps.Broker.SendUpdateEvents(updates) + }() + return tabId, updates, nil +} + +func (svc *WorkspaceService) UpdateTabIds_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ArgNames: []string{"uiContext", "workspaceId", "tabIds"}, + } +} + +func (svc *WorkspaceService) UpdateTabIds(uiContext waveobj.UIContext, workspaceId string, tabIds []string) (waveobj.UpdatesRtnType, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + ctx = waveobj.ContextWithUpdates(ctx) + err := wcore.UpdateWorkspaceTabIds(ctx, workspaceId, tabIds) + if err != nil { + return nil, fmt.Errorf("error updating workspace tab ids: %w", err) + } + return waveobj.ContextGetUpdatesRtn(ctx), nil +} + +func (svc *WorkspaceService) SetActiveTab_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ArgNames: []string{"workspaceId", "tabId"}, + } +} + +func (svc *WorkspaceService) SetActiveTab(workspaceId string, tabId string) (waveobj.UpdatesRtnType, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + ctx = waveobj.ContextWithUpdates(ctx) + err := wcore.SetActiveTab(ctx, workspaceId, tabId) + if err != nil { + return nil, fmt.Errorf("error setting active tab: %w", err) + } + // check all blocks in tab and start controllers (if necessary) + tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId) + if err != nil { + return nil, fmt.Errorf("error getting tab: %w", err) + } + blockORefs := tab.GetBlockORefs() + blocks, err := wstore.DBSelectORefs(ctx, blockORefs) + if err != nil { + return nil, fmt.Errorf("error getting tab blocks: %w", err) + } + updates := waveobj.ContextGetUpdatesRtn(ctx) + go func() { + defer panichandler.PanicHandler("WorkspaceService:SetActiveTab:SendUpdateEvents") + wps.Broker.SendUpdateEvents(updates) + }() + var extraUpdates waveobj.UpdatesRtnType + extraUpdates = append(extraUpdates, updates...) + extraUpdates = append(extraUpdates, waveobj.MakeUpdate(tab)) + extraUpdates = append(extraUpdates, waveobj.MakeUpdates(blocks)...) + return extraUpdates, nil +} + +type CloseTabRtnType struct { + CloseWindow bool `json:"closewindow,omitempty"` + NewActiveTabId string `json:"newactivetabid,omitempty"` +} + +func (svc *WorkspaceService) CloseTab_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ArgNames: []string{"ctx", "workspaceId", "tabId", "fromElectron"}, + } +} + +// returns the new active tabid +func (svc *WorkspaceService) CloseTab(ctx context.Context, workspaceId string, tabId string, fromElectron bool) (*CloseTabRtnType, waveobj.UpdatesRtnType, error) { + ctx = waveobj.ContextWithUpdates(ctx) + tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId) + if err != nil { + return nil, nil, fmt.Errorf("error getting tab: %w", err) + } + go func() { + for _, blockId := range tab.BlockIds { + blockcontroller.StopBlockController(blockId) + } + }() + newActiveTabId, err := wcore.DeleteTab(ctx, workspaceId, tabId) + if err != nil { + return nil, nil, fmt.Errorf("error closing tab: %w", err) + } + rtn := &CloseTabRtnType{} + if newActiveTabId == "" { + windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId) + if err != nil { + return rtn, nil, fmt.Errorf("unable to find window for workspace id %v: %w", workspaceId, err) + } + rtn.CloseWindow = true + err = wcore.CloseWindow(ctx, windowId, fromElectron) + if err != nil { + return rtn, nil, err + } + } else { + rtn.NewActiveTabId = newActiveTabId + } + updates := waveobj.ContextGetUpdatesRtn(ctx) + go func() { + defer panichandler.PanicHandler("WorkspaceService:CloseTab:SendUpdateEvents") + wps.Broker.SendUpdateEvents(updates) + }() + return rtn, updates, nil +} diff --git a/pkg/shellexec/shellexec.go b/pkg/shellexec/shellexec.go index dd76af838..b6316f867 100644 --- a/pkg/shellexec/shellexec.go +++ b/pkg/shellexec/shellexec.go @@ -237,6 +237,47 @@ func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr st func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) { client := conn.GetClient() + if !conn.WshEnabled.Load() { + // no wsh code + session, err := client.NewSession() + if err != nil { + return nil, err + } + + remoteStdinRead, remoteStdinWriteOurs, err := os.Pipe() + if err != nil { + return nil, err + } + + remoteStdoutReadOurs, remoteStdoutWrite, err := os.Pipe() + if err != nil { + return nil, err + } + + pipePty := &PipePty{ + remoteStdinWrite: remoteStdinWriteOurs, + remoteStdoutRead: remoteStdoutReadOurs, + } + if termSize.Rows == 0 || termSize.Cols == 0 { + termSize.Rows = shellutil.DefaultTermRows + termSize.Cols = shellutil.DefaultTermCols + } + if termSize.Rows <= 0 || termSize.Cols <= 0 { + return nil, fmt.Errorf("invalid term size: %v", termSize) + } + session.Stdin = remoteStdinRead + session.Stdout = remoteStdoutWrite + session.Stderr = remoteStdoutWrite + + session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil) + sessionWrap := SessionWrap{session, "", pipePty, pipePty} + err = session.Shell() + if err != nil { + pipePty.Close() + return nil, err + } + return &ShellProc{Cmd: sessionWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil + } shellPath := cmdOpts.ShellPath if shellPath == "" { remoteShellPath, err := remote.DetectShell(client) diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go index 9acda99f5..4356498a5 100644 --- a/pkg/telemetry/telemetry.go +++ b/pkg/telemetry/telemetry.go @@ -14,41 +14,12 @@ import ( "github.com/wavetermdev/waveterm/pkg/util/dbutil" "github.com/wavetermdev/waveterm/pkg/wavebase" "github.com/wavetermdev/waveterm/pkg/wconfig" + "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wstore" ) const MaxTzNameLen = 50 -type ActivityDisplayType struct { - Width int `json:"width"` - Height int `json:"height"` - DPR float64 `json:"dpr"` - Internal bool `json:"internal,omitempty"` -} - -type ActivityUpdate struct { - FgMinutes int `json:"fgminutes,omitempty"` - ActiveMinutes int `json:"activeminutes,omitempty"` - OpenMinutes int `json:"openminutes,omitempty"` - NumTabs int `json:"numtabs,omitempty"` - NewTab int `json:"newtab,omitempty"` - NumBlocks int `json:"numblocks,omitempty"` - NumWindows int `json:"numwindows,omitempty"` - NumSSHConn int `json:"numsshconn,omitempty"` - NumWSLConn int `json:"numwslconn,omitempty"` - NumMagnify int `json:"nummagnify,omitempty"` - NumPanics int `json:"numpanics,omitempty"` - Startup int `json:"startup,omitempty"` - Shutdown int `json:"shutdown,omitempty"` - SetTabTheme int `json:"settabtheme,omitempty"` - BuildTime string `json:"buildtime,omitempty"` - Displays []ActivityDisplayType `json:"displays,omitempty"` - Renderers map[string]int `json:"renderers,omitempty"` - Blocks map[string]int `json:"blocks,omitempty"` - WshCmds map[string]int `json:"wshcmds,omitempty"` - Conn map[string]int `json:"conn,omitempty"` -} - type ActivityType struct { Day string `json:"day"` Uploaded bool `json:"-"` @@ -62,25 +33,25 @@ type ActivityType struct { } type TelemetryData struct { - ActiveMinutes int `json:"activeminutes"` - FgMinutes int `json:"fgminutes"` - OpenMinutes int `json:"openminutes"` - NumTabs int `json:"numtabs"` - NumBlocks int `json:"numblocks,omitempty"` - NumWindows int `json:"numwindows,omitempty"` - NumSSHConn int `json:"numsshconn,omitempty"` - NumWSLConn int `json:"numwslconn,omitempty"` - NumMagnify int `json:"nummagnify,omitempty"` - NewTab int `json:"newtab"` - NumStartup int `json:"numstartup,omitempty"` - NumShutdown int `json:"numshutdown,omitempty"` - NumPanics int `json:"numpanics,omitempty"` - SetTabTheme int `json:"settabtheme,omitempty"` - Displays []ActivityDisplayType `json:"displays,omitempty"` - Renderers map[string]int `json:"renderers,omitempty"` - Blocks map[string]int `json:"blocks,omitempty"` - WshCmds map[string]int `json:"wshcmds,omitempty"` - Conn map[string]int `json:"conn,omitempty"` + ActiveMinutes int `json:"activeminutes"` + FgMinutes int `json:"fgminutes"` + OpenMinutes int `json:"openminutes"` + NumTabs int `json:"numtabs"` + NumBlocks int `json:"numblocks,omitempty"` + NumWindows int `json:"numwindows,omitempty"` + NumSSHConn int `json:"numsshconn,omitempty"` + NumWSLConn int `json:"numwslconn,omitempty"` + NumMagnify int `json:"nummagnify,omitempty"` + NewTab int `json:"newtab"` + NumStartup int `json:"numstartup,omitempty"` + NumShutdown int `json:"numshutdown,omitempty"` + NumPanics int `json:"numpanics,omitempty"` + SetTabTheme int `json:"settabtheme,omitempty"` + Displays []wshrpc.ActivityDisplayType `json:"displays,omitempty"` + Renderers map[string]int `json:"renderers,omitempty"` + Blocks map[string]int `json:"blocks,omitempty"` + WshCmds map[string]int `json:"wshcmds,omitempty"` + Conn map[string]int `json:"conn,omitempty"` } func (tdata TelemetryData) Value() (driver.Value, error) { @@ -107,7 +78,7 @@ func AutoUpdateChannel() string { } // Wraps UpdateCurrentActivity, spawns goroutine, and logs errors -func GoUpdateActivityWrap(update ActivityUpdate, debugStr string) { +func GoUpdateActivityWrap(update wshrpc.ActivityUpdate, debugStr string) { go func() { defer panichandler.PanicHandlerNoTelemetry("GoUpdateActivityWrap") ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) @@ -120,7 +91,7 @@ func GoUpdateActivityWrap(update ActivityUpdate, debugStr string) { }() } -func UpdateActivity(ctx context.Context, update ActivityUpdate) error { +func UpdateActivity(ctx context.Context, update wshrpc.ActivityUpdate) error { now := time.Now() dayStr := daystr.GetCurDayStr() txErr := wstore.WithTx(ctx, func(tx *wstore.TxWrap) error { diff --git a/pkg/tsgen/tsgen.go b/pkg/tsgen/tsgen.go index bf17a2f41..116df27c0 100644 --- a/pkg/tsgen/tsgen.go +++ b/pkg/tsgen/tsgen.go @@ -61,7 +61,7 @@ var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem() var errorRType = reflect.TypeOf((*error)(nil)).Elem() var anyRType = reflect.TypeOf((*interface{})(nil)).Elem() var metaRType = reflect.TypeOf((*waveobj.MetaMapType)(nil)).Elem() -var metaSettingsType = reflect.TypeOf((*wconfig.MetaSettingsType)(nil)).Elem() +var metaSettingsType = reflect.TypeOf((*wshrpc.MetaSettingsType)(nil)).Elem() var uiContextRType = reflect.TypeOf((*waveobj.UIContext)(nil)).Elem() var waveObjRType = reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem() var updatesRtnRType = reflect.TypeOf(waveobj.UpdatesRtnType{}) @@ -362,7 +362,7 @@ func GenerateMethodSignature(serviceName string, method reflect.Method, meta tsg wroteArg = true } sb.WriteString("): ") - wroteRtn := false + rtnTypes := []string{} for idx := 0; idx < method.Type.NumOut(); idx++ { outType := method.Type.Out(idx) if outType == errorRType { @@ -372,11 +372,14 @@ func GenerateMethodSignature(serviceName string, method reflect.Method, meta tsg continue } tsTypeName, _ := TypeToTSType(outType, tsTypesMap) - sb.WriteString(fmt.Sprintf("Promise<%s>", tsTypeName)) - wroteRtn = true + rtnTypes = append(rtnTypes, tsTypeName) } - if !wroteRtn { + if len(rtnTypes) == 0 { sb.WriteString("Promise") + } else if len(rtnTypes) == 1 { + sb.WriteString(fmt.Sprintf("Promise<%s>", rtnTypes[0])) + } else { + sb.WriteString(fmt.Sprintf("Promise<[%s]>", strings.Join(rtnTypes, ", "))) } sb.WriteString(" {\n") return sb.String() diff --git a/pkg/userinput/userinput.go b/pkg/userinput/userinput.go index 0ebf9dd0b..b6c99c735 100644 --- a/pkg/userinput/userinput.go +++ b/pkg/userinput/userinput.go @@ -25,6 +25,8 @@ type UserInputRequest struct { TimeoutMs int `json:"timeoutms"` CheckBoxMsg string `json:"checkboxmsg"` PublicText bool `json:"publictext"` + OkLabel string `json:"oklabel,omitempty"` + CancelLabel string `json:"cancellabel,omitempty"` } type UserInputResponse struct { diff --git a/pkg/util/utilfn/utilfn.go b/pkg/util/utilfn/utilfn.go index b9539ea6f..baeb56da9 100644 --- a/pkg/util/utilfn/utilfn.go +++ b/pkg/util/utilfn/utilfn.go @@ -925,6 +925,15 @@ func GetLineColFromOffset(barr []byte, offset int) (int, int) { return line, col } +func FindStringInSlice(slice []string, val string) int { + for idx, v := range slice { + if v == val { + return idx + } + } + return -1 +} + func FormatLsTime(t time.Time) string { now := time.Now() sixMonthsAgo := now.AddDate(0, -6, 0) diff --git a/pkg/waveobj/wtype.go b/pkg/waveobj/wtype.go index 228bc596d..22e5ed088 100644 --- a/pkg/waveobj/wtype.go +++ b/pkg/waveobj/wtype.go @@ -129,7 +129,6 @@ type Client struct { Meta MetaMapType `json:"meta"` TosAgreed int64 `json:"tosagreed,omitempty"` HasOldHistory bool `json:"hasoldhistory,omitempty"` - NextTabId int `json:"nexttabid,omitempty"` TempOID string `json:"tempoid,omitempty"` } @@ -137,13 +136,11 @@ func (*Client) GetOType() string { return OType_Client } -// stores the ui-context of the window -// workspaceid, active tab, active block within each tab, window size, etc. +// stores the ui-context of the window, points to a workspace containing the actual data being displayed in the window type Window struct { OID string `json:"oid"` Version int `json:"version"` WorkspaceId string `json:"workspaceid"` - ActiveTabId string `json:"activetabid"` IsNew bool `json:"isnew,omitempty"` // set when a window is created on the backend so the FE can size it properly. cleared on first resize Pos Point `json:"pos"` WinSize WinSize `json:"winsize"` @@ -155,12 +152,22 @@ func (*Window) GetOType() string { return OType_Window } +type WorkspaceListEntry struct { + WorkspaceId string `json:"workspaceid"` + WindowId string `json:"windowid"` +} + +type WorkspaceList []*WorkspaceListEntry + type Workspace struct { - OID string `json:"oid"` - Version int `json:"version"` - Name string `json:"name"` - TabIds []string `json:"tabids"` - Meta MetaMapType `json:"meta"` + OID string `json:"oid"` + Version int `json:"version"` + Name string `json:"name"` + Icon string `json:"icon"` + Color string `json:"color"` + TabIds []string `json:"tabids"` + ActiveTabId string `json:"activetabid"` + Meta MetaMapType `json:"meta"` } func (*Workspace) GetOType() string { diff --git a/pkg/wconfig/defaultconfig/settings.json b/pkg/wconfig/defaultconfig/settings.json index 634aebcd2..fa8d98f88 100644 --- a/pkg/wconfig/defaultconfig/settings.json +++ b/pkg/wconfig/defaultconfig/settings.json @@ -7,6 +7,7 @@ "autoupdate:installonquit": true, "autoupdate:intervalms": 3600000, "conn:askbeforewshinstall": true, + "conn:wshenabled": true, "editor:minimapenabled": true, "web:defaulturl": "https://github.com/wavetermdev/waveterm", "web:defaultsearch": "https://www.google.com/search?q={query}", diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go index 31d769b21..3c5ea6c80 100644 --- a/pkg/wconfig/metaconsts.go +++ b/pkg/wconfig/metaconsts.go @@ -71,5 +71,6 @@ const ( ConfigKey_ConnClear = "conn:*" ConfigKey_ConnAskBeforeWshInstall = "conn:askbeforewshinstall" + ConfigKey_ConnWshEnabled = "conn:wshenabled" ) diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index e139c2f2f..de046b578 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -19,9 +19,11 @@ import ( "github.com/wavetermdev/waveterm/pkg/wavebase" "github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/wconfig/defaultconfig" + "github.com/wavetermdev/waveterm/pkg/wshrpc" ) const SettingsFile = "settings.json" +const ConnectionsFile = "connections.json" const AnySchema = ` { @@ -30,25 +32,6 @@ const AnySchema = ` } ` -type MetaSettingsType struct { - waveobj.MetaMapType -} - -func (m *MetaSettingsType) UnmarshalJSON(data []byte) error { - var metaMap waveobj.MetaMapType - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.UseNumber() - if err := decoder.Decode(&metaMap); err != nil { - return err - } - *m = MetaSettingsType{MetaMapType: metaMap} - return nil -} - -func (m MetaSettingsType) MarshalJSON() ([]byte, error) { - return json.Marshal(m.MetaMapType) -} - type SettingsType struct { AiClear bool `json:"ai:*,omitempty"` AiPreset string `json:"ai:preset,omitempty"` @@ -115,6 +98,7 @@ type SettingsType struct { ConnClear bool `json:"conn:*,omitempty"` ConnAskBeforeWshInstall bool `json:"conn:askbeforewshinstall,omitempty"` + ConnWshEnabled bool `json:"conn:wshenabled,omitempty"` } type ConfigError struct { @@ -123,12 +107,14 @@ type ConfigError struct { } type FullConfigType struct { - Settings SettingsType `json:"settings" merge:"meta"` - MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"` - Widgets map[string]WidgetConfigType `json:"widgets"` - Presets map[string]waveobj.MetaMapType `json:"presets"` - TermThemes map[string]TermThemeType `json:"termthemes"` - ConfigErrors []ConfigError `json:"configerrors" configfile:"-"` + 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"` + Connections map[string]wshrpc.ConnKeywords `json:"connections"` + ConfigErrors []ConfigError `json:"configerrors" configfile:"-"` } func goBackWS(barr []byte, offset int) int { @@ -401,12 +387,12 @@ func reindentJson(barr []byte, indentStr string) []byte { if barr[0] != '{' && barr[0] != '[' { return barr } - if bytes.Contains(barr, []byte("\n")) { + if !bytes.Contains(barr, []byte("\n")) { return barr } outputLines := bytes.Split(barr, []byte("\n")) for i, line := range outputLines { - if i == 0 || i == len(outputLines)-1 { + if i == 0 { continue } outputLines[i] = append([]byte(indentStr), line...) @@ -509,6 +495,25 @@ func SetBaseConfigValue(toMerge waveobj.MetaMapType) error { return WriteWaveHomeConfigFile(SettingsFile, m) } +func SetConnectionsConfigValue(connName string, toMerge waveobj.MetaMapType) error { + m, cerrs := ReadWaveHomeConfigFile(ConnectionsFile) + if len(cerrs) > 0 { + return fmt.Errorf("error reading config file: %v", cerrs[0]) + } + if m == nil { + m = make(waveobj.MetaMapType) + } + connData := m.GetMap(connName) + if connData == nil { + connData = make(waveobj.MetaMapType) + } + for configKey, val := range toMerge { + connData[configKey] = val + } + m[connName] = connData + return WriteWaveHomeConfigFile(ConnectionsFile, m) +} + type WidgetConfigType struct { DisplayOrder float64 `json:"display:order,omitempty"` Icon string `json:"icon,omitempty"` diff --git a/pkg/wcore/block.go b/pkg/wcore/block.go new file mode 100644 index 000000000..4741883e5 --- /dev/null +++ b/pkg/wcore/block.go @@ -0,0 +1,91 @@ +package wcore + +import ( + "context" + "fmt" + "time" + + "github.com/wavetermdev/waveterm/pkg/blockcontroller" + "github.com/wavetermdev/waveterm/pkg/panichandler" + "github.com/wavetermdev/waveterm/pkg/telemetry" + "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wps" + "github.com/wavetermdev/waveterm/pkg/wshrpc" + "github.com/wavetermdev/waveterm/pkg/wstore" +) + +func CreateSubBlock(ctx context.Context, blockId string, blockDef *waveobj.BlockDef) (*waveobj.Block, error) { + if blockDef == nil { + return nil, fmt.Errorf("blockDef is nil") + } + if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" { + return nil, fmt.Errorf("no view provided for new block") + } + blockData, err := wstore.CreateSubBlock(ctx, blockId, blockDef) + if err != nil { + return nil, fmt.Errorf("error creating sub block: %w", err) + } + return blockData, nil +} + +func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) { + if blockDef == nil { + return nil, fmt.Errorf("blockDef is nil") + } + if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" { + return nil, fmt.Errorf("no view provided for new block") + } + blockData, err := wstore.CreateBlock(ctx, tabId, blockDef, rtOpts) + if err != nil { + return nil, fmt.Errorf("error creating block: %w", err) + } + go func() { + defer panichandler.PanicHandler("CreateBlock:telemetry") + blockView := blockDef.Meta.GetString(waveobj.MetaKey_View, "") + if blockView == "" { + return + } + tctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFn() + telemetry.UpdateActivity(tctx, wshrpc.ActivityUpdate{ + Renderers: map[string]int{blockView: 1}, + }) + }() + return blockData, nil +} + +func DeleteBlock(ctx context.Context, blockId string) error { + block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId) + if err != nil { + return fmt.Errorf("error getting block: %w", err) + } + if block == nil { + return nil + } + if len(block.SubBlockIds) > 0 { + for _, subBlockId := range block.SubBlockIds { + err := DeleteBlock(ctx, subBlockId) + if err != nil { + return fmt.Errorf("error deleting subblock %s: %w", subBlockId, err) + } + } + } + err = wstore.DeleteBlock(ctx, blockId) + if err != nil { + return fmt.Errorf("error deleting block: %w", err) + } + go blockcontroller.StopBlockController(blockId) + sendBlockCloseEvent(blockId) + return nil +} + +func sendBlockCloseEvent(blockId string) { + waveEvent := wps.WaveEvent{ + Event: wps.Event_BlockClose, + Scopes: []string{ + waveobj.MakeORef(waveobj.OType_Block, blockId).String(), + }, + Data: blockId, + } + wps.Broker.Publish(waveEvent) +} diff --git a/pkg/wcore/wcore.go b/pkg/wcore/wcore.go index 2e90e37d3..35d0ce3fc 100644 --- a/pkg/wcore/wcore.go +++ b/pkg/wcore/wcore.go @@ -11,11 +11,7 @@ import ( "time" "github.com/google/uuid" - "github.com/wavetermdev/waveterm/pkg/blockcontroller" - "github.com/wavetermdev/waveterm/pkg/panichandler" - "github.com/wavetermdev/waveterm/pkg/telemetry" "github.com/wavetermdev/waveterm/pkg/waveobj" - "github.com/wavetermdev/waveterm/pkg/wps" "github.com/wavetermdev/waveterm/pkg/wstore" ) @@ -27,220 +23,60 @@ import ( const DefaultTimeout = 2 * time.Second const DefaultActivateBlockTimeout = 60 * time.Second -func DeleteBlock(ctx context.Context, blockId string) error { - block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId) - if err != nil { - return fmt.Errorf("error getting block: %w", err) - } - if block == nil { - return nil - } - if len(block.SubBlockIds) > 0 { - for _, subBlockId := range block.SubBlockIds { - err := DeleteBlock(ctx, subBlockId) - if err != nil { - return fmt.Errorf("error deleting subblock %s: %w", subBlockId, err) - } - } - } - err = wstore.DeleteBlock(ctx, blockId) - if err != nil { - return fmt.Errorf("error deleting block: %w", err) - } - go blockcontroller.StopBlockController(blockId) - sendBlockCloseEvent(blockId) - return nil -} - -func sendBlockCloseEvent(blockId string) { - waveEvent := wps.WaveEvent{ - Event: wps.Event_BlockClose, - Scopes: []string{ - waveobj.MakeORef(waveobj.OType_Block, blockId).String(), - }, - Data: blockId, - } - wps.Broker.Publish(waveEvent) -} - -func DeleteTab(ctx context.Context, workspaceId string, tabId string) error { - tabData, err := wstore.DBGet[*waveobj.Tab](ctx, tabId) - if err != nil { - return fmt.Errorf("error getting tab: %w", err) - } - if tabData == nil { - return nil - } - // close blocks (sends events + stops block controllers) - for _, blockId := range tabData.BlockIds { - err := DeleteBlock(ctx, blockId) - if err != nil { - return fmt.Errorf("error deleting block %s: %w", blockId, err) - } - } - // now delete tab (also deletes layout) - err = wstore.DeleteTab(ctx, workspaceId, tabId) - if err != nil { - return fmt.Errorf("error deleting tab: %w", err) - } - - return nil -} - -// returns tabid -func CreateTab(ctx context.Context, windowId string, tabName string, activateTab bool) (string, error) { - windowData, err := wstore.DBMustGet[*waveobj.Window](ctx, windowId) - if err != nil { - return "", fmt.Errorf("error getting window: %w", err) - } - if tabName == "" { - ws, err := wstore.DBMustGet[*waveobj.Workspace](ctx, windowData.WorkspaceId) - if err != nil { - return "", fmt.Errorf("error getting workspace: %w", err) - } - tabName = "T" + fmt.Sprint(len(ws.TabIds)+1) - client, err := wstore.DBGetSingleton[*waveobj.Client](ctx) - if err != nil { - return "", fmt.Errorf("error getting client: %w", err) - } - client.NextTabId++ - err = wstore.DBUpdate(ctx, client) - if err != nil { - return "", fmt.Errorf("error updating client: %w", err) - } - } - tab, err := wstore.CreateTab(ctx, windowData.WorkspaceId, tabName) - if err != nil { - return "", fmt.Errorf("error creating tab: %w", err) - } - if activateTab { - err = wstore.SetActiveTab(ctx, windowId, tab.OID) - if err != nil { - return "", fmt.Errorf("error setting active tab: %w", err) - } - } - telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{NewTab: 1}, "createtab") - return tab.OID, nil -} - -func CreateWindow(ctx context.Context, winSize *waveobj.WinSize) (*waveobj.Window, error) { - windowId := uuid.NewString() - workspaceId := uuid.NewString() - if winSize == nil { - winSize = &waveobj.WinSize{ - Width: 0, - Height: 0, - } - } - window := &waveobj.Window{ - OID: windowId, - WorkspaceId: workspaceId, - IsNew: true, - Pos: waveobj.Point{ - X: 0, - Y: 0, - }, - WinSize: *winSize, - } - err := wstore.DBInsert(ctx, window) - if err != nil { - return nil, fmt.Errorf("error inserting window: %w", err) - } - ws := &waveobj.Workspace{ - OID: workspaceId, - Name: "w" + workspaceId[0:8], - } - err = wstore.DBInsert(ctx, ws) - if err != nil { - return nil, fmt.Errorf("error inserting workspace: %w", err) - } - _, err = CreateTab(ctx, windowId, "", true) - if err != nil { - return nil, fmt.Errorf("error inserting tab: %w", err) - } - client, err := wstore.DBGetSingleton[*waveobj.Client](ctx) - if err != nil { - return nil, fmt.Errorf("error getting client: %w", err) - } - client.WindowIds = append(client.WindowIds, windowId) - err = wstore.DBUpdate(ctx, client) - if err != nil { - return nil, fmt.Errorf("error updating client: %w", err) - } - return wstore.DBMustGet[*waveobj.Window](ctx, windowId) -} - -func checkAndFixWindow(ctx context.Context, windowId string) { - window, err := wstore.DBMustGet[*waveobj.Window](ctx, windowId) - if err != nil { - log.Printf("error getting window %q (in checkAndFixWindow): %v\n", windowId, err) - return - } - workspace, err := wstore.DBMustGet[*waveobj.Workspace](ctx, window.WorkspaceId) - if err != nil { - log.Printf("error getting workspace %q (in checkAndFixWindow): %v\n", window.WorkspaceId, err) - return - } - if len(workspace.TabIds) == 0 { - log.Printf("fixing workspace with no tabs %q (in checkAndFixWindow)\n", workspace.OID) - _, err = CreateTab(ctx, windowId, "", true) - if err != nil { - log.Printf("error creating tab (in checkAndFixWindow): %v\n", err) - } - } -} - -// returns (new-window, first-time, error) -func EnsureInitialData() (*waveobj.Window, bool, error) { +// Ensures that the initial data is present in the store, creates an initial window if needed +func EnsureInitialData() error { // does not need to run in a transaction since it is called on startup ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) defer cancelFn() - firstRun := false client, err := wstore.DBGetSingleton[*waveobj.Client](ctx) if err == wstore.ErrNotFound { client, err = CreateClient(ctx) if err != nil { - return nil, false, fmt.Errorf("error creating client: %w", err) + return fmt.Errorf("error creating client: %w", err) } - firstRun = true - } - if client.NextTabId == 0 { - tabCount, err := wstore.DBGetCount[*waveobj.Tab](ctx) - if err != nil { - return nil, false, fmt.Errorf("error getting tab count: %w", err) - } - client.NextTabId = tabCount + 1 - err = wstore.DBUpdate(ctx, client) - if err != nil { - return nil, false, fmt.Errorf("error updating client: %w", err) + migrateErr := wstore.TryMigrateOldHistory() + if migrateErr != nil { + log.Printf("error migrating old history: %v\n", migrateErr) } } if client.TempOID == "" { + log.Println("client.TempOID is empty") client.TempOID = uuid.NewString() err = wstore.DBUpdate(ctx, client) if err != nil { - return nil, false, fmt.Errorf("error updating client: %w", err) + return fmt.Errorf("error updating client: %w", err) } } log.Printf("clientid: %s\n", client.OID) if len(client.WindowIds) == 1 { - checkAndFixWindow(ctx, client.WindowIds[0]) + log.Println("client has one window") + CheckAndFixWindow(ctx, client.WindowIds[0]) + return nil } if len(client.WindowIds) > 0 { - return nil, false, nil + log.Println("client has windows") + return nil } - window, err := CreateWindow(ctx, nil) + log.Println("client has no windows, creating default workspace") + defaultWs, err := CreateWorkspace(ctx, "Default workspace", "circle", "green") if err != nil { - return nil, false, fmt.Errorf("error creating window: %w", err) + return fmt.Errorf("error creating default workspace: %w", err) } - return window, firstRun, nil + _, err = CreateTab(ctx, defaultWs.OID, "", true) + if err != nil { + return fmt.Errorf("error creating tab: %w", err) + } + _, err = CreateWindow(ctx, nil, defaultWs.OID) + if err != nil { + return fmt.Errorf("error creating window: %w", err) + } + return nil } func CreateClient(ctx context.Context) (*waveobj.Client, error) { client := &waveobj.Client{ OID: uuid.NewString(), WindowIds: []string{}, - NextTabId: 1, } err := wstore.DBInsert(ctx, client) if err != nil { @@ -249,42 +85,11 @@ func CreateClient(ctx context.Context) (*waveobj.Client, error) { return client, nil } -func CreateSubBlock(ctx context.Context, blockId string, blockDef *waveobj.BlockDef) (*waveobj.Block, error) { - if blockDef == nil { - return nil, fmt.Errorf("blockDef is nil") - } - if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" { - return nil, fmt.Errorf("no view provided for new block") - } - blockData, err := wstore.CreateSubBlock(ctx, blockId, blockDef) +func GetClientData(ctx context.Context) (*waveobj.Client, error) { + clientData, err := wstore.DBGetSingleton[*waveobj.Client](ctx) if err != nil { - return nil, fmt.Errorf("error creating sub block: %w", err) + return nil, fmt.Errorf("error getting client data: %w", err) } - return blockData, nil -} - -func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) { - if blockDef == nil { - return nil, fmt.Errorf("blockDef is nil") - } - if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" { - return nil, fmt.Errorf("no view provided for new block") - } - blockData, err := wstore.CreateBlock(ctx, tabId, blockDef, rtOpts) - if err != nil { - return nil, fmt.Errorf("error creating block: %w", err) - } - go func() { - defer panichandler.PanicHandler("CreateBlock:telemetry") - blockView := blockDef.Meta.GetString(waveobj.MetaKey_View, "") - if blockView == "" { - return - } - tctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) - defer cancelFn() - telemetry.UpdateActivity(tctx, telemetry.ActivityUpdate{ - Renderers: map[string]int{blockView: 1}, - }) - }() - return blockData, nil + log.Printf("clientData: %v\n", clientData) + return clientData, nil } diff --git a/pkg/wcore/window.go b/pkg/wcore/window.go new file mode 100644 index 000000000..08ec9d8dd --- /dev/null +++ b/pkg/wcore/window.go @@ -0,0 +1,200 @@ +package wcore + +import ( + "context" + "fmt" + "log" + + "github.com/google/uuid" + "github.com/wavetermdev/waveterm/pkg/eventbus" + "github.com/wavetermdev/waveterm/pkg/util/utilfn" + "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wshrpc" + "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" + "github.com/wavetermdev/waveterm/pkg/wshutil" + "github.com/wavetermdev/waveterm/pkg/wstore" +) + +func SwitchWorkspace(ctx context.Context, windowId string, workspaceId string) (*waveobj.Workspace, error) { + log.Printf("SwitchWorkspace %s %s\n", windowId, workspaceId) + ws, err := GetWorkspace(ctx, workspaceId) + if err != nil { + return nil, fmt.Errorf("error getting new workspace: %w", err) + } + window, err := GetWindow(ctx, windowId) + if err != nil { + return nil, fmt.Errorf("error getting window: %w", err) + } + if window.WorkspaceId == workspaceId { + return nil, nil + } + + allWindows, err := wstore.DBGetAllObjsByType[*waveobj.Window](ctx, waveobj.OType_Window) + if err != nil { + return nil, fmt.Errorf("error getting all windows: %w", err) + } + + for _, w := range allWindows { + if w.WorkspaceId == workspaceId { + log.Printf("workspace %s already has a window %s, focusing that window\n", workspaceId, w.OID) + client := wshclient.GetBareRpcClient() + err = wshclient.FocusWindowCommand(client, w.OID, &wshrpc.RpcOpts{Route: wshutil.ElectronRoute}) + return nil, err + } + } + + curWs, err := GetWorkspace(ctx, window.WorkspaceId) + if err != nil { + return nil, fmt.Errorf("error getting current workspace: %w", err) + } + deleted, err := DeleteWorkspace(ctx, curWs.OID, false) + if err != nil { + return nil, fmt.Errorf("error deleting current workspace: %w", err) + } + if !deleted { + log.Printf("current workspace %s was not deleted\n", curWs.OID) + } else { + log.Printf("deleted current workspace %s\n", curWs.OID) + } + + window.WorkspaceId = workspaceId + log.Printf("switching window %s to workspace %s\n", windowId, workspaceId) + return ws, wstore.DBUpdate(ctx, window) +} + +func GetWindow(ctx context.Context, windowId string) (*waveobj.Window, error) { + window, err := wstore.DBMustGet[*waveobj.Window](ctx, windowId) + if err != nil { + log.Printf("error getting window %q: %v\n", windowId, err) + return nil, err + } + return window, nil +} + +func CreateWindow(ctx context.Context, winSize *waveobj.WinSize, workspaceId string) (*waveobj.Window, error) { + log.Printf("CreateWindow %v %v\n", winSize, workspaceId) + var ws *waveobj.Workspace + if workspaceId == "" { + ws1, err := CreateWorkspace(ctx, "", "", "") + if err != nil { + return nil, fmt.Errorf("error creating workspace: %w", err) + } + ws = ws1 + } else { + ws1, err := GetWorkspace(ctx, workspaceId) + if err != nil { + return nil, fmt.Errorf("error getting workspace: %w", err) + } + ws = ws1 + } + windowId := uuid.NewString() + if winSize == nil { + winSize = &waveobj.WinSize{ + Width: 0, + Height: 0, + } + } + window := &waveobj.Window{ + OID: windowId, + WorkspaceId: ws.OID, + IsNew: true, + Pos: waveobj.Point{ + X: 0, + Y: 0, + }, + WinSize: *winSize, + } + err := wstore.DBInsert(ctx, window) + if err != nil { + return nil, fmt.Errorf("error inserting window: %w", err) + } + client, err := GetClientData(ctx) + if err != nil { + return nil, fmt.Errorf("error getting client: %w", err) + } + client.WindowIds = append(client.WindowIds, windowId) + err = wstore.DBUpdate(ctx, client) + if err != nil { + return nil, fmt.Errorf("error updating client: %w", err) + } + return GetWindow(ctx, windowId) +} + +func CloseWindow(ctx context.Context, windowId string, fromElectron bool) error { + log.Printf("CloseWindow %s\n", windowId) + window, err := GetWindow(ctx, windowId) + if err == nil { + log.Printf("got window %s\n", windowId) + deleted, err := DeleteWorkspace(ctx, window.WorkspaceId, false) + if err != nil { + log.Printf("error deleting workspace: %v\n", err) + } + if deleted { + log.Printf("deleted workspace %s\n", window.WorkspaceId) + } + err = wstore.DBDelete(ctx, waveobj.OType_Window, windowId) + if err != nil { + return fmt.Errorf("error deleting window: %w", err) + } + log.Printf("deleted window %s\n", windowId) + } else { + log.Printf("error getting window %s: %v\n", windowId, err) + } + client, err := wstore.DBGetSingleton[*waveobj.Client](ctx) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + client.WindowIds = utilfn.RemoveElemFromSlice(client.WindowIds, windowId) + err = wstore.DBUpdate(ctx, client) + if err != nil { + return fmt.Errorf("error updating client: %w", err) + } + log.Printf("updated client\n") + if !fromElectron { + eventbus.SendEventToElectron(eventbus.WSEventType{ + EventType: eventbus.WSEvent_ElectronCloseWindow, + Data: windowId, + }) + } + return nil +} + +func CheckAndFixWindow(ctx context.Context, windowId string) *waveobj.Window { + log.Printf("CheckAndFixWindow %s\n", windowId) + window, err := GetWindow(ctx, windowId) + if err != nil { + log.Printf("error getting window %q (in checkAndFixWindow): %v\n", windowId, err) + return nil + } + ws, err := GetWorkspace(ctx, window.WorkspaceId) + if err != nil { + log.Printf("error getting workspace %q (in checkAndFixWindow): %v\n", window.WorkspaceId, err) + CloseWindow(ctx, windowId, false) + return nil + } + if len(ws.TabIds) == 0 { + log.Printf("fixing workspace with no tabs %q (in checkAndFixWindow)\n", ws.OID) + _, err = CreateTab(ctx, ws.OID, "", true) + if err != nil { + log.Printf("error creating tab (in checkAndFixWindow): %v\n", err) + } + } + return window +} + +func FocusWindow(ctx context.Context, windowId string) error { + log.Printf("FocusWindow %s\n", windowId) + client, err := GetClientData(ctx) + if err != nil { + log.Printf("error getting client data: %v\n", err) + return err + } + winIdx := utilfn.SliceIdx(client.WindowIds, windowId) + if winIdx == -1 { + log.Printf("window %s not found in client data\n", windowId) + return nil + } + client.WindowIds = utilfn.MoveSliceIdxToFront(client.WindowIds, winIdx) + log.Printf("client.WindowIds: %v\n", client.WindowIds) + return wstore.DBUpdate(ctx, client) +} diff --git a/pkg/wcore/workspace.go b/pkg/wcore/workspace.go new file mode 100644 index 000000000..2402512a8 --- /dev/null +++ b/pkg/wcore/workspace.go @@ -0,0 +1,249 @@ +package wcore + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/google/uuid" + "github.com/wavetermdev/waveterm/pkg/telemetry" + "github.com/wavetermdev/waveterm/pkg/util/utilfn" + "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wshrpc" + "github.com/wavetermdev/waveterm/pkg/wstore" +) + +func CreateWorkspace(ctx context.Context, name string, icon string, color string) (*waveobj.Workspace, error) { + log.Println("CreateWorkspace") + ws := &waveobj.Workspace{ + OID: uuid.NewString(), + TabIds: []string{}, + Name: name, + Icon: icon, + Color: color, + } + wstore.DBInsert(ctx, ws) + return ws, nil +} + +func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool, error) { + log.Printf("DeleteWorkspace %s\n", workspaceId) + workspace, err := wstore.DBMustGet[*waveobj.Workspace](ctx, workspaceId) + if err != nil { + return false, fmt.Errorf("error getting workspace: %w", err) + } + if workspace.Name != "" && workspace.Icon != "" && !force { + log.Printf("Ignoring DeleteWorkspace for workspace %s as it is named\n", workspaceId) + return false, nil + } + for _, tabId := range workspace.TabIds { + log.Printf("deleting tab %s\n", tabId) + _, err := DeleteTab(ctx, workspaceId, tabId) + if err != nil { + return false, fmt.Errorf("error closing tab: %w", err) + } + } + err = wstore.DBDelete(ctx, waveobj.OType_Workspace, workspaceId) + if err != nil { + return false, fmt.Errorf("error deleting workspace: %w", err) + } + log.Printf("deleted workspace %s\n", workspaceId) + return true, nil +} + +func GetWorkspace(ctx context.Context, wsID string) (*waveobj.Workspace, error) { + return wstore.DBMustGet[*waveobj.Workspace](ctx, wsID) +} + +func createTabObj(ctx context.Context, workspaceId string, name string) (*waveobj.Tab, error) { + ws, err := GetWorkspace(ctx, workspaceId) + if err != nil { + return nil, fmt.Errorf("workspace %s not found: %w", workspaceId, err) + } + layoutStateId := uuid.NewString() + tab := &waveobj.Tab{ + OID: uuid.NewString(), + Name: name, + BlockIds: []string{}, + LayoutState: layoutStateId, + } + layoutState := &waveobj.LayoutState{ + OID: layoutStateId, + } + ws.TabIds = append(ws.TabIds, tab.OID) + wstore.DBInsert(ctx, tab) + wstore.DBInsert(ctx, layoutState) + wstore.DBUpdate(ctx, ws) + return tab, nil +} + +// returns tabid +func CreateTab(ctx context.Context, workspaceId string, tabName string, activateTab bool) (string, error) { + if tabName == "" { + ws, err := GetWorkspace(ctx, workspaceId) + if err != nil { + return "", fmt.Errorf("workspace %s not found: %w", workspaceId, err) + } + tabName = "T" + fmt.Sprint(len(ws.TabIds)+1) + } + tab, err := createTabObj(ctx, workspaceId, tabName) + if err != nil { + return "", fmt.Errorf("error creating tab: %w", err) + } + if activateTab { + err = SetActiveTab(ctx, workspaceId, tab.OID) + if err != nil { + return "", fmt.Errorf("error setting active tab: %w", err) + } + } + telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{NewTab: 1}, "createtab") + return tab.OID, nil +} + +// Must delete all blocks individually first. +// Also deletes LayoutState. +// Returns new active tab id, error. +func DeleteTab(ctx context.Context, workspaceId string, tabId string) (string, error) { + ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId) + if ws == nil { + return "", fmt.Errorf("workspace not found: %q", workspaceId) + } + tab, _ := wstore.DBGet[*waveobj.Tab](ctx, tabId) + if tab == nil { + return "", fmt.Errorf("tab not found: %q", tabId) + } + + // close blocks (sends events + stops block controllers) + for _, blockId := range tab.BlockIds { + err := DeleteBlock(ctx, blockId) + if err != nil { + return "", fmt.Errorf("error deleting block %s: %w", blockId, err) + } + } + tabIdx := utilfn.FindStringInSlice(ws.TabIds, tabId) + if tabIdx == -1 { + return "", nil + } + ws.TabIds = append(ws.TabIds[:tabIdx], ws.TabIds[tabIdx+1:]...) + newActiveTabId := ws.ActiveTabId + if len(ws.TabIds) > 0 { + if ws.ActiveTabId == tabId { + newActiveTabId = ws.TabIds[max(0, min(tabIdx-1, len(ws.TabIds)-1))] + } + } else { + newActiveTabId = "" + } + ws.ActiveTabId = newActiveTabId + + wstore.DBUpdate(ctx, ws) + wstore.DBDelete(ctx, waveobj.OType_Tab, tabId) + wstore.DBDelete(ctx, waveobj.OType_LayoutState, tab.LayoutState) + return newActiveTabId, nil +} + +func SetActiveTab(ctx context.Context, workspaceId string, tabId string) error { + if tabId != "" { + workspace, err := GetWorkspace(ctx, workspaceId) + if err != nil { + return fmt.Errorf("workspace %s not found: %w", workspaceId, err) + } + tab, _ := wstore.DBGet[*waveobj.Tab](ctx, tabId) + if tab == nil { + return fmt.Errorf("tab not found: %q", tabId) + } + workspace.ActiveTabId = tabId + wstore.DBUpdate(ctx, workspace) + } + return nil +} + +func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string) error { + ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId) + if ws == nil { + return fmt.Errorf("workspace not found: %q", workspaceId) + } + ws.TabIds = tabIds + wstore.DBUpdate(ctx, ws) + return nil +} + +func ListWorkspaces(ctx context.Context) (waveobj.WorkspaceList, error) { + workspaces, err := wstore.DBGetAllObjsByType[*waveobj.Workspace](ctx, waveobj.OType_Workspace) + if err != nil { + return nil, err + } + + log.Println("got workspaces") + + windows, err := wstore.DBGetAllObjsByType[*waveobj.Window](ctx, waveobj.OType_Window) + if err != nil { + return nil, err + } + + workspaceToWindow := make(map[string]string) + for _, window := range windows { + workspaceToWindow[window.WorkspaceId] = window.OID + } + + var wl waveobj.WorkspaceList + for _, workspace := range workspaces { + if workspace.Name == "" || workspace.Icon == "" || workspace.Color == "" { + continue + } + windowId, ok := workspaceToWindow[workspace.OID] + if !ok { + windowId = "" + } + wl = append(wl, &waveobj.WorkspaceListEntry{ + WorkspaceId: workspace.OID, + WindowId: windowId, + }) + } + return wl, nil +} + +func SetIcon(workspaceId string, icon string) error { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + ws, e := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId) + if e != nil { + return e + } + if ws == nil { + return fmt.Errorf("workspace not found: %q", workspaceId) + } + ws.Icon = icon + wstore.DBUpdate(ctx, ws) + return nil +} + +func SetColor(workspaceId string, color string) error { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + ws, e := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId) + if e != nil { + return e + } + if ws == nil { + return fmt.Errorf("workspace not found: %q", workspaceId) + } + ws.Color = color + wstore.DBUpdate(ctx, ws) + return nil +} + +func SetName(workspaceId string, name string) error { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + ws, e := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId) + if e != nil { + return e + } + if ws == nil { + return fmt.Errorf("workspace not found: %q", workspaceId) + } + ws.Name = name + wstore.DBUpdate(ctx, ws) + return nil +} diff --git a/pkg/wlayout/wlayout.go b/pkg/wlayout/wlayout.go index 96885d17b..aba38c7fb 100644 --- a/pkg/wlayout/wlayout.go +++ b/pkg/wlayout/wlayout.go @@ -153,8 +153,9 @@ func ApplyPortableLayout(ctx context.Context, tabId string, layout PortableLayou return nil } -func BootstrapNewWindowLayout(ctx context.Context, window *waveobj.Window) error { - tabId := window.ActiveTabId +func BootstrapNewWorkspaceLayout(ctx context.Context, workspace *waveobj.Workspace) error { + log.Printf("BootstrapNewWorkspaceLayout, workspace: %v\n", workspace) + tabId := workspace.ActiveTabId newTabLayout := GetNewTabLayout() err := ApplyPortableLayout(ctx, tabId, newTabLayout) @@ -184,7 +185,12 @@ func BootstrapStarterLayout(ctx context.Context) error { return fmt.Errorf("error getting window: %w", err) } - tabId := window.ActiveTabId + workspace, err := wstore.DBMustGet[*waveobj.Workspace](ctx, window.WorkspaceId) + if err != nil { + return fmt.Errorf("error getting workspace: %w", err) + } + + tabId := workspace.ActiveTabId starterLayout := GetStarterLayout() diff --git a/pkg/wshrpc/wshclient/barerpcclient.go b/pkg/wshrpc/wshclient/barerpcclient.go new file mode 100644 index 000000000..18a4810e0 --- /dev/null +++ b/pkg/wshrpc/wshclient/barerpcclient.go @@ -0,0 +1,36 @@ +package wshclient + +import ( + "sync" + + "github.com/wavetermdev/waveterm/pkg/wps" + "github.com/wavetermdev/waveterm/pkg/wshrpc" + "github.com/wavetermdev/waveterm/pkg/wshutil" +) + +type WshServer struct{} + +func (*WshServer) WshServerImpl() {} + +var WshServerImpl = WshServer{} + +const ( + DefaultOutputChSize = 32 + DefaultInputChSize = 32 +) + +var waveSrvClient_Singleton *wshutil.WshRpc +var waveSrvClient_Once = &sync.Once{} + +const BareClientRoute = "bare" + +func GetBareRpcClient() *wshutil.WshRpc { + waveSrvClient_Once.Do(func() { + inputCh := make(chan []byte, DefaultInputChSize) + outputCh := make(chan []byte, DefaultOutputChSize) + waveSrvClient_Singleton = wshutil.MakeWshRpc(inputCh, outputCh, wshrpc.RpcContext{}, &WshServerImpl) + wshutil.DefaultRouter.RegisterRoute(BareClientRoute, waveSrvClient_Singleton, true) + wps.Broker.SetClient(wshutil.DefaultRouter) + }) + return waveSrvClient_Singleton +} diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index cf943b436..88635130f 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -9,14 +9,12 @@ import ( "github.com/wavetermdev/waveterm/pkg/wshutil" "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/waveobj" - "github.com/wavetermdev/waveterm/pkg/wconfig" "github.com/wavetermdev/waveterm/pkg/wps" "github.com/wavetermdev/waveterm/pkg/vdom" - "github.com/wavetermdev/waveterm/pkg/telemetry" ) // command "activity", wshserver.ActivityCommand -func ActivityCommand(w *wshutil.WshRpc, data telemetry.ActivityUpdate, opts *wshrpc.RpcOpts) error { +func ActivityCommand(w *wshutil.WshRpc, data wshrpc.ActivityUpdate, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "activity", data, opts) return err } @@ -40,7 +38,7 @@ func BlockInfoCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) (*ws } // command "connconnect", wshserver.ConnConnectCommand -func ConnConnectCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { +func ConnConnectCommand(w *wshutil.WshRpc, data wshrpc.ConnRequest, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "connconnect", data, opts) return err } @@ -207,6 +205,12 @@ func FileWriteCommand(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshr return err } +// command "focuswindow", wshserver.FocusWindowCommand +func FocusWindowCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "focuswindow", 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) @@ -290,7 +294,7 @@ func RouteUnannounceCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) error { } // command "setconfig", wshserver.SetConfigCommand -func SetConfigCommand(w *wshutil.WshRpc, data wconfig.MetaSettingsType, opts *wshrpc.RpcOpts) error { +func SetConfigCommand(w *wshutil.WshRpc, data wshrpc.MetaSettingsType, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "setconfig", data, opts) return err } @@ -374,6 +378,12 @@ func WebSelectorCommand(w *wshutil.WshRpc, data wshrpc.CommandWebSelectorData, o return resp, err } +// command "workspacelist", wshserver.WorkspaceListCommand +func WorkspaceListCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) ([]wshrpc.WorkspaceInfoData, error) { + resp, err := sendRpcRequestCallHelper[[]wshrpc.WorkspaceInfoData](w, "workspacelist", nil, opts) + return resp, err +} + // command "wshactivity", wshserver.WshActivityCommand func WshActivityCommand(w *wshutil.WshRpc, data map[string]int, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "wshactivity", data, opts) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 949b11c46..4f0992f11 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -5,17 +5,17 @@ package wshrpc import ( + "bytes" "context" + "encoding/json" "log" "os" "reflect" "github.com/wavetermdev/waveterm/pkg/filestore" "github.com/wavetermdev/waveterm/pkg/ijson" - "github.com/wavetermdev/waveterm/pkg/telemetry" "github.com/wavetermdev/waveterm/pkg/vdom" "github.com/wavetermdev/waveterm/pkg/waveobj" - "github.com/wavetermdev/waveterm/pkg/wconfig" "github.com/wavetermdev/waveterm/pkg/wps" ) @@ -80,8 +80,11 @@ const ( Command_WslList = "wsllist" Command_WslDefaultDistro = "wsldefaultdistro" + Command_WorkspaceList = "workspacelist" + Command_WebSelector = "webselector" Command_Notify = "notify" + Command_FocusWindow = "focuswindow" Command_GetUpdateChannel = "getupdatechannel" Command_VDomCreateContext = "vdomcreatecontext" @@ -133,11 +136,11 @@ type WshRpcInterface interface { StreamWaveAiCommand(ctx context.Context, request OpenAiStreamRequest) chan RespOrErrorUnion[OpenAIPacketType] StreamCpuDataCommand(ctx context.Context, request CpuDataRequest) chan RespOrErrorUnion[TimeSeriesData] TestCommand(ctx context.Context, data string) error - SetConfigCommand(ctx context.Context, data wconfig.MetaSettingsType) error + SetConfigCommand(ctx context.Context, data MetaSettingsType) error BlockInfoCommand(ctx context.Context, blockId string) (*BlockInfoData, error) WaveInfoCommand(ctx context.Context) (*WaveInfoData, error) WshActivityCommand(ct context.Context, data map[string]int) error - ActivityCommand(ctx context.Context, data telemetry.ActivityUpdate) error + ActivityCommand(ctx context.Context, data ActivityUpdate) error GetVarCommand(ctx context.Context, data CommandVarData) (*CommandVarResponseData, error) SetVarCommand(ctx context.Context, data CommandVarData) error @@ -146,7 +149,7 @@ type WshRpcInterface interface { WslStatusCommand(ctx context.Context) ([]ConnStatus, error) ConnEnsureCommand(ctx context.Context, connName string) error ConnReinstallWshCommand(ctx context.Context, connName string) error - ConnConnectCommand(ctx context.Context, connName string) error + ConnConnectCommand(ctx context.Context, connRequest ConnRequest) error ConnDisconnectCommand(ctx context.Context, connName string) error ConnListCommand(ctx context.Context) ([]string, error) WslListCommand(ctx context.Context) ([]string, error) @@ -166,6 +169,9 @@ type WshRpcInterface interface { // emain WebSelectorCommand(ctx context.Context, data CommandWebSelectorData) ([]string, error) NotifyCommand(ctx context.Context, notificationOptions WaveNotificationOptions) error + FocusWindowCommand(ctx context.Context, windowId string) error + + WorkspaceListCommand(ctx context.Context) ([]WorkspaceInfoData, error) GetUpdateChannelCommand(ctx context.Context) (string, error) // terminal @@ -440,6 +446,31 @@ type CommandRemoteWriteFileData struct { CreateMode os.FileMode `json:"createmode,omitempty"` } +type ConnKeywords struct { + WshEnabled *bool `json:"wshenabled,omitempty"` + AskBeforeWshInstall *bool `json:"askbeforewshinstall,omitempty"` + + SshUser string `json:"ssh:user,omitempty"` + SshHostName string `json:"ssh:hostname,omitempty"` + SshPort string `json:"ssh:port,omitempty"` + SshIdentityFile []string `json:"ssh:identityfile,omitempty"` + SshBatchMode bool `json:"ssh:batchmode,omitempty"` + SshPubkeyAuthentication bool `json:"ssh:pubkeyauthentication,omitempty"` + SshPasswordAuthentication bool `json:"ssh:passwordauthentication,omitempty"` + SshKbdInteractiveAuthentication bool `json:"ssh:kbdinteractiveauthentication,omitempty"` + SshPreferredAuthentications []string `json:"ssh:preferredauthentications,omitempty"` + SshAddKeysToAgent bool `json:"ssh:addkeystoagent,omitempty"` + SshIdentityAgent string `json:"ssh:identityagent,omitempty"` + SshProxyJump []string `json:"ssh:proxyjump,omitempty"` + SshUserKnownHostsFile []string `json:"ssh:userknownhostsfile,omitempty"` + SshGlobalKnownHostsFile []string `json:"ssh:globalknownhostsfile,omitempty"` +} + +type ConnRequest struct { + Host string `json:"host"` + Keywords ConnKeywords `json:"keywords,omitempty"` +} + const ( TimeSeries_Cpu = "cpu" ) @@ -449,8 +480,28 @@ type TimeSeriesData struct { Values map[string]float64 `json:"values"` } +type MetaSettingsType struct { + waveobj.MetaMapType +} + +func (m *MetaSettingsType) UnmarshalJSON(data []byte) error { + var metaMap waveobj.MetaMapType + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + if err := decoder.Decode(&metaMap); err != nil { + return err + } + *m = MetaSettingsType{MetaMapType: metaMap} + return nil +} + +func (m MetaSettingsType) MarshalJSON() ([]byte, error) { + return json.Marshal(m.MetaMapType) +} + type ConnStatus struct { Status string `json:"status"` + WshEnabled bool `json:"wshenabled"` Connection string `json:"connection"` Connected bool `json:"connected"` HasConnected bool `json:"hasconnected"` // true if it has *ever* connected successfully @@ -464,18 +515,18 @@ type WebSelectorOpts struct { } type CommandWebSelectorData struct { - WindowId string `json:"windowid"` - BlockId string `json:"blockid" wshcontext:"BlockId"` - TabId string `json:"tabid" wshcontext:"TabId"` - Selector string `json:"selector"` - Opts *WebSelectorOpts `json:"opts,omitempty"` + WorkspaceId string `json:"workspaceid"` + BlockId string `json:"blockid" wshcontext:"BlockId"` + TabId string `json:"tabid" wshcontext:"TabId"` + Selector string `json:"selector"` + Opts *WebSelectorOpts `json:"opts,omitempty"` } type BlockInfoData struct { - BlockId string `json:"blockid"` - TabId string `json:"tabid"` - WindowId string `json:"windowid"` - Block *waveobj.Block `json:"block"` + BlockId string `json:"blockid"` + TabId string `json:"tabid"` + WorkspaceId string `json:"workspaceid"` + Block *waveobj.Block `json:"block"` } type WaveNotificationOptions struct { @@ -505,6 +556,11 @@ type WaveInfoData struct { DataDir string `json:"datadir"` } +type WorkspaceInfoData struct { + WindowId string `json:"windowid"` + WorkspaceData *waveobj.Workspace `json:"workspacedata"` +} + type AiMessageData struct { Message string `json:"message,omitempty"` } @@ -522,3 +578,33 @@ type CommandVarResponseData struct { Val string `json:"val"` Exists bool `json:"exists"` } + +type ActivityDisplayType struct { + Width int `json:"width"` + Height int `json:"height"` + DPR float64 `json:"dpr"` + Internal bool `json:"internal,omitempty"` +} + +type ActivityUpdate struct { + FgMinutes int `json:"fgminutes,omitempty"` + ActiveMinutes int `json:"activeminutes,omitempty"` + OpenMinutes int `json:"openminutes,omitempty"` + NumTabs int `json:"numtabs,omitempty"` + NewTab int `json:"newtab,omitempty"` + NumBlocks int `json:"numblocks,omitempty"` + NumWindows int `json:"numwindows,omitempty"` + NumSSHConn int `json:"numsshconn,omitempty"` + NumWSLConn int `json:"numwslconn,omitempty"` + NumMagnify int `json:"nummagnify,omitempty"` + NumPanics int `json:"numpanics,omitempty"` + Startup int `json:"startup,omitempty"` + Shutdown int `json:"shutdown,omitempty"` + SetTabTheme int `json:"settabtheme,omitempty"` + BuildTime string `json:"buildtime,omitempty"` + Displays []ActivityDisplayType `json:"displays,omitempty"` + Renderers map[string]int `json:"renderers,omitempty"` + Blocks map[string]int `json:"blocks,omitempty"` + WshCmds map[string]int `json:"wshcmds,omitempty"` + Conn map[string]int `json:"conn,omitempty"` +} diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index c8f69afa6..c23e813eb 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -180,13 +180,6 @@ func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.Command if err != nil { return nil, fmt.Errorf("error creating block: %w", err) } - windowId, err := wstore.DBFindWindowForTabId(ctx, tabId) - if err != nil { - return nil, fmt.Errorf("error finding window for tab: %w", err) - } - if windowId == "" { - return nil, fmt.Errorf("no window found for tab") - } err = wlayout.QueueLayoutActionForTab(ctx, tabId, waveobj.LayoutActionData{ ActionType: wlayout.LayoutActionDataType_Insert, BlockId: blockData.OID, @@ -512,13 +505,6 @@ func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.Command if tabId == "" { return fmt.Errorf("no tab found for block") } - windowId, err := wstore.DBFindWindowForTabId(ctx, tabId) - if err != nil { - return fmt.Errorf("error finding window for tab: %w", err) - } - if windowId == "" { - return fmt.Errorf("no window found for tab") - } err = wcore.DeleteBlock(ctx, data.BlockId) if err != nil { return fmt.Errorf("error deleting block: %w", err) @@ -587,7 +573,7 @@ func (ws *WshServer) EventReadHistoryCommand(ctx context.Context, data wshrpc.Co return events, nil } -func (ws *WshServer) SetConfigCommand(ctx context.Context, data wconfig.MetaSettingsType) error { +func (ws *WshServer) SetConfigCommand(ctx context.Context, data wshrpc.MetaSettingsType) error { log.Printf("SETCONFIG: %v\n", data) return wconfig.SetBaseConfigValue(data.MetaMapType) } @@ -623,14 +609,15 @@ func (ws *WshServer) ConnDisconnectCommand(ctx context.Context, connName string) if err != nil { return fmt.Errorf("error parsing connection name: %w", err) } - conn := conncontroller.GetConn(ctx, connOpts, false) + conn := conncontroller.GetConn(ctx, connOpts, false, &wshrpc.ConnKeywords{}) if conn == nil { return fmt.Errorf("connection not found: %s", connName) } return conn.Close() } -func (ws *WshServer) ConnConnectCommand(ctx context.Context, connName string) error { +func (ws *WshServer) ConnConnectCommand(ctx context.Context, connRequest wshrpc.ConnRequest) error { + connName := connRequest.Host if strings.HasPrefix(connName, "wsl://") { distroName := strings.TrimPrefix(connName, "wsl://") conn := wsl.GetWslConn(ctx, distroName, false) @@ -643,11 +630,11 @@ func (ws *WshServer) ConnConnectCommand(ctx context.Context, connName string) er if err != nil { return fmt.Errorf("error parsing connection name: %w", err) } - conn := conncontroller.GetConn(ctx, connOpts, false) + conn := conncontroller.GetConn(ctx, connOpts, false, &connRequest.Keywords) if conn == nil { return fmt.Errorf("connection not found: %s", connName) } - return conn.Connect(ctx) + return conn.Connect(ctx, &connRequest.Keywords) } func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, connName string) error { @@ -663,7 +650,7 @@ func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, connName strin if err != nil { return fmt.Errorf("error parsing connection name: %w", err) } - conn := conncontroller.GetConn(ctx, connOpts, false) + conn := conncontroller.GetConn(ctx, connOpts, false, &wshrpc.ConnKeywords{}) if conn == nil { return fmt.Errorf("connection not found: %s", connName) } @@ -710,15 +697,15 @@ func (ws *WshServer) BlockInfoCommand(ctx context.Context, blockId string) (*wsh if err != nil { return nil, fmt.Errorf("error finding tab for block: %w", err) } - windowId, err := wstore.DBFindWindowForTabId(ctx, tabId) + workspaceId, err := wstore.DBFindWorkspaceForTabId(ctx, tabId) if err != nil { return nil, fmt.Errorf("error finding window for tab: %w", err) } return &wshrpc.BlockInfoData{ - BlockId: blockId, - TabId: tabId, - WindowId: windowId, - Block: blockData, + BlockId: blockId, + TabId: tabId, + WorkspaceId: workspaceId, + Block: blockData, }, nil } @@ -736,6 +723,25 @@ func (ws *WshServer) WaveInfoCommand(ctx context.Context) (*wshrpc.WaveInfoData, }, nil } +func (ws *WshServer) WorkspaceListCommand(ctx context.Context) ([]wshrpc.WorkspaceInfoData, error) { + workspaceList, err := wcore.ListWorkspaces(ctx) + if err != nil { + return nil, fmt.Errorf("error listing workspaces: %w", err) + } + var rtn []wshrpc.WorkspaceInfoData + for _, workspaceEntry := range workspaceList { + workspaceData, err := wcore.GetWorkspace(ctx, workspaceEntry.WorkspaceId) + if err != nil { + return nil, fmt.Errorf("error getting workspace: %w", err) + } + rtn = append(rtn, wshrpc.WorkspaceInfoData{ + WindowId: workspaceEntry.WindowId, + WorkspaceData: workspaceData, + }) + } + return rtn, nil +} + var wshActivityRe = regexp.MustCompile(`^[a-z:#]+$`) func (ws *WshServer) WshActivityCommand(ctx context.Context, data map[string]int) error { @@ -753,14 +759,14 @@ func (ws *WshServer) WshActivityCommand(ctx context.Context, data map[string]int delete(data, key) } } - activityUpdate := telemetry.ActivityUpdate{ + activityUpdate := wshrpc.ActivityUpdate{ WshCmds: data, } telemetry.GoUpdateActivityWrap(activityUpdate, "wsh-activity") return nil } -func (ws *WshServer) ActivityCommand(ctx context.Context, activity telemetry.ActivityUpdate) error { +func (ws *WshServer) ActivityCommand(ctx context.Context, activity wshrpc.ActivityUpdate) error { telemetry.GoUpdateActivityWrap(activity, "wshrpc-activity") return nil } diff --git a/pkg/wsl/wsl.go b/pkg/wsl/wsl.go index eb364144e..331e58a46 100644 --- a/pkg/wsl/wsl.go +++ b/pkg/wsl/wsl.go @@ -394,7 +394,7 @@ func (conn *WslConn) Connect(ctx context.Context) error { conn.Status = Status_Error conn.Error = err.Error() conn.close_nolock() - telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{ + telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{ Conn: map[string]int{"wsl:connecterror": 1}, }, "wsl-connconnect") } else { @@ -403,7 +403,7 @@ func (conn *WslConn) Connect(ctx context.Context) error { if conn.ActiveConnNum == 0 { conn.ActiveConnNum = int(activeConnCounter.Add(1)) } - telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{ + telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{ Conn: map[string]int{"wsl:connect": 1}, }, "wsl-connconnect") } diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 0872ec45b..2d724c757 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -18,69 +18,6 @@ func init() { } } -func CreateTab(ctx context.Context, workspaceId string, name string) (*waveobj.Tab, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (*waveobj.Tab, error) { - ws, _ := DBGet[*waveobj.Workspace](tx.Context(), workspaceId) - if ws == nil { - return nil, fmt.Errorf("workspace not found: %q", workspaceId) - } - layoutStateId := uuid.NewString() - tab := &waveobj.Tab{ - OID: uuid.NewString(), - Name: name, - BlockIds: []string{}, - LayoutState: layoutStateId, - } - layoutState := &waveobj.LayoutState{ - OID: layoutStateId, - } - ws.TabIds = append(ws.TabIds, tab.OID) - DBInsert(tx.Context(), tab) - DBInsert(tx.Context(), layoutState) - DBUpdate(tx.Context(), ws) - return tab, nil - }) -} - -func CreateWorkspace(ctx context.Context) (*waveobj.Workspace, error) { - ws := &waveobj.Workspace{ - OID: uuid.NewString(), - TabIds: []string{}, - } - DBInsert(ctx, ws) - return ws, nil -} - -func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string) error { - return WithTx(ctx, func(tx *TxWrap) error { - ws, _ := DBGet[*waveobj.Workspace](tx.Context(), workspaceId) - if ws == nil { - return fmt.Errorf("workspace not found: %q", workspaceId) - } - ws.TabIds = tabIds - DBUpdate(tx.Context(), ws) - return nil - }) -} - -func SetActiveTab(ctx context.Context, windowId string, tabId string) error { - return WithTx(ctx, func(tx *TxWrap) error { - window, _ := DBGet[*waveobj.Window](tx.Context(), windowId) - if window == nil { - return fmt.Errorf("window not found: %q", windowId) - } - if tabId != "" { - tab, _ := DBGet[*waveobj.Tab](tx.Context(), tabId) - if tab == nil { - return fmt.Errorf("tab not found: %q", tabId) - } - } - window.ActiveTabId = tabId - DBUpdate(tx.Context(), window) - return nil - }) -} - func UpdateTabName(ctx context.Context, tabId, name string) error { return WithTx(ctx, func(tx *TxWrap) error { tab, _ := DBGet[*waveobj.Tab](tx.Context(), tabId) @@ -137,15 +74,6 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, }) } -func findStringInSlice(slice []string, val string) int { - for idx, v := range slice { - if v == val { - return idx - } - } - return -1 -} - func DeleteBlock(ctx context.Context, blockId string) error { return WithTx(ctx, func(tx *TxWrap) error { block, err := DBGet[*waveobj.Block](tx.Context(), blockId) @@ -235,7 +163,7 @@ func MoveBlockToTab(ctx context.Context, currentTabId string, newTabId string, b if newTab == nil { return fmt.Errorf("new tab not found: %q", newTabId) } - blockIdx := findStringInSlice(currentTab.BlockIds, blockId) + blockIdx := utilfn.FindStringInSlice(currentTab.BlockIds, blockId) if blockIdx == -1 { return fmt.Errorf("block not found in current tab: %q", blockId) } diff --git a/pkg/wstore/wstore_dbops.go b/pkg/wstore/wstore_dbops.go index 3683f0eaa..b8f104b1a 100644 --- a/pkg/wstore/wstore_dbops.go +++ b/pkg/wstore/wstore_dbops.go @@ -187,6 +187,42 @@ func DBSelectORefs(ctx context.Context, orefs []waveobj.ORef) ([]waveobj.WaveObj }) } +func DBGetAllOIDsByType(ctx context.Context, otype string) ([]string, error) { + return WithTxRtn(ctx, func(tx *TxWrap) ([]string, error) { + rtn := make([]string, 0) + table := tableNameFromOType(otype) + log.Printf("DBGetAllOIDsByType table: %s\n", table) + query := fmt.Sprintf("SELECT oid FROM %s", table) + var rows []idDataType + tx.Select(&rows, query) + for _, row := range rows { + rtn = append(rtn, row.OId) + } + return rtn, nil + }) +} + +func DBGetAllObjsByType[T waveobj.WaveObj](ctx context.Context, otype string) ([]T, error) { + return WithTxRtn(ctx, func(tx *TxWrap) ([]T, error) { + rtn := make([]T, 0) + table := tableNameFromOType(otype) + log.Printf("DBGetAllObjsByType table: %s\n", table) + query := fmt.Sprintf("SELECT oid, version, data FROM %s", table) + var rows []idDataType + tx.Select(&rows, query) + for _, row := range rows { + waveObj, err := waveobj.FromJson(row.Data) + if err != nil { + return nil, err + } + waveobj.SetVersion(waveObj, row.Version) + + rtn = append(rtn, waveObj.(T)) + } + return rtn, nil + }) +} + func DBResolveEasyOID(ctx context.Context, oid string) (*waveobj.ORef, error) { return WithTxRtn(ctx, func(tx *TxWrap) (*waveobj.ORef, error) { for _, rtype := range waveobj.AllWaveObjTypes() { @@ -284,13 +320,6 @@ func DBInsert(ctx context.Context, val waveobj.WaveObj) error { }) } -func DBFindWindowForTabId(ctx context.Context, tabId string) (string, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (string, error) { - query := "SELECT oid FROM db_window WHERE data->>'activetabid' = ?" - return tx.GetString(query, tabId), nil - }) -} - func DBFindTabForBlockId(ctx context.Context, blockId string) (string, error) { return WithTxRtn(ctx, func(tx *TxWrap) (string, error) { iterNum := 1 @@ -329,3 +358,12 @@ func DBFindWorkspaceForTabId(ctx context.Context, tabId string) (string, error) return tx.GetString(query, tabId), nil }) } + +func DBFindWindowForWorkspaceId(ctx context.Context, workspaceId string) (string, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (string, error) { + query := ` + SELECT w.oid + FROM db_window w WHERE json_extract(data, '$.workspaceid') = ?` + return tx.GetString(query, workspaceId), nil + }) +} diff --git a/yarn.lock b/yarn.lock index b1d8c6114..05c899c23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4065,21 +4065,19 @@ __metadata: languageName: node linkType: hard -"@joshwooding/vite-plugin-react-docgen-typescript@npm:0.3.0": - version: 0.3.0 - resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.3.0" +"@joshwooding/vite-plugin-react-docgen-typescript@npm:0.4.2": + version: 0.4.2 + resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.4.2" dependencies: - glob: "npm:^7.2.0" - glob-promise: "npm:^4.2.0" magic-string: "npm:^0.27.0" react-docgen-typescript: "npm:^2.2.2" peerDependencies: typescript: ">= 4.3.x" - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/31098ad8fcc2440437534599c111d9f2951dd74821e8ba46c521b969bae4c918d830b7bb0484efbad29a51711bb62d3bc623d5a1ed5b1695b5b5594ea9dd4ca0 + checksum: 10c0/355d13ad92a9da786b561a25250e6c94a8e51d235ced345e54ebfe709abc45ab60c2a8d06599df6ec0441fba01720f189883429943cb62dff9a4c31b59f0939c languageName: node linkType: hard @@ -4839,128 +4837,128 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.24.4" +"@rollup/rollup-android-arm-eabi@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.27.4" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-android-arm64@npm:4.24.4" +"@rollup/rollup-android-arm64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-android-arm64@npm:4.27.4" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-darwin-arm64@npm:4.24.4" +"@rollup/rollup-darwin-arm64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-darwin-arm64@npm:4.27.4" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-darwin-x64@npm:4.24.4" +"@rollup/rollup-darwin-x64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-darwin-x64@npm:4.27.4" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.24.4" +"@rollup/rollup-freebsd-arm64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.27.4" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-freebsd-x64@npm:4.24.4" +"@rollup/rollup-freebsd-x64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-freebsd-x64@npm:4.27.4" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.24.4" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.27.4" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.24.4" +"@rollup/rollup-linux-arm-musleabihf@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.27.4" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.24.4" +"@rollup/rollup-linux-arm64-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.27.4" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.24.4" +"@rollup/rollup-linux-arm64-musl@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.27.4" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-powerpc64le-gnu@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.24.4" +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.27.4" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.24.4" +"@rollup/rollup-linux-riscv64-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.27.4" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.24.4" +"@rollup/rollup-linux-s390x-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.27.4" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.24.4" +"@rollup/rollup-linux-x64-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.27.4" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.24.4" +"@rollup/rollup-linux-x64-musl@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.27.4" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.24.4" +"@rollup/rollup-win32-arm64-msvc@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.27.4" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.24.4" +"@rollup/rollup-win32-ia32-msvc@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.27.4" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.24.4": - version: 4.24.4 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.24.4" +"@rollup/rollup-win32-x64-msvc@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.27.4" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -5043,9 +5041,9 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-actions@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-actions@npm:8.4.5" +"@storybook/addon-actions@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/addon-actions@npm:8.4.6" dependencies: "@storybook/global": "npm:^5.0.0" "@types/uuid": "npm:^9.0.1" @@ -5053,164 +5051,164 @@ __metadata: polished: "npm:^4.2.2" uuid: "npm:^9.0.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/b689c16a01302c4d64f24dc777b666456bddc1ab820aaf9b6b6f9d3ab7081d6f573a6641bc2dcb9ee8c3ec9425f36426737abd6735da6fcfc670ee6b9f3d8280 + storybook: ^8.4.6 + checksum: 10c0/80b2feceacb4ebe7f2be06b2fe3f49ded5ee08ca8bd036ff47a65d45d8796d29081ccadd0526984c8022bcfa24348e0ad4ce3f37cee4a60a928bae372bfc8afe languageName: node linkType: hard -"@storybook/addon-backgrounds@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-backgrounds@npm:8.4.5" +"@storybook/addon-backgrounds@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/addon-backgrounds@npm:8.4.6" dependencies: "@storybook/global": "npm:^5.0.0" memoizerific: "npm:^1.11.3" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/863c4cb60957c1113231a5bedf833de8ba86846509b950e878acd788d1e1ad13e07ad9b2e183c96a8bee8c01442b22ee8cdf2f324e6fca297d88f43b26b3fef1 + storybook: ^8.4.6 + checksum: 10c0/2125d6905bf44194adf79e92698753d5e4ff75fac1ffbba1fc95ae705ba9ac8dc6ca9249c9a862aa05ea207d916d23142faefa759bb9ce21c6e16f0e329d28d2 languageName: node linkType: hard -"@storybook/addon-controls@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-controls@npm:8.4.5" +"@storybook/addon-controls@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/addon-controls@npm:8.4.6" dependencies: "@storybook/global": "npm:^5.0.0" dequal: "npm:^2.0.2" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/1ca92a32d5ff018f2120d8a8787b834d1ee2bbf2423b422ccb6f2a9f1ce0f66ad5f67de6e268330434d47734dbe0cec8b130678392705db36510d13770ce6616 + storybook: ^8.4.6 + checksum: 10c0/f5f0ab2de8de80c8c3726de81802042cc29a6f2ec50de3b8bd463286c9056e87800e4ea9b350c6a41ce4c4175a11cb7d3d490da5cfc20bbf2a2e3549f77a82a7 languageName: node linkType: hard -"@storybook/addon-docs@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-docs@npm:8.4.5" +"@storybook/addon-docs@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/addon-docs@npm:8.4.6" dependencies: "@mdx-js/react": "npm:^3.0.0" - "@storybook/blocks": "npm:8.4.5" - "@storybook/csf-plugin": "npm:8.4.5" - "@storybook/react-dom-shim": "npm:8.4.5" + "@storybook/blocks": "npm:8.4.6" + "@storybook/csf-plugin": "npm:8.4.6" + "@storybook/react-dom-shim": "npm:8.4.6" react: "npm:^16.8.0 || ^17.0.0 || ^18.0.0" react-dom: "npm:^16.8.0 || ^17.0.0 || ^18.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/cb3731d6cc738ea01094acc83dfe92b918aed4dde3d0251b61aaa2105fd5b692686be06c092f27d37653855f2ffae86d24888a6066e121f6fc97c92b86dfd2c1 + storybook: ^8.4.6 + checksum: 10c0/ae53bf71048fe7476862ae733f0f765a22d0d1da32457f7ca7e3bdd23bb1cd452c56bc4e1f586cf978599c3f5acb835caeb569ff394eaec09d3259382f4954be languageName: node linkType: hard -"@storybook/addon-essentials@npm:^8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-essentials@npm:8.4.5" +"@storybook/addon-essentials@npm:^8.4.6": + version: 8.4.6 + resolution: "@storybook/addon-essentials@npm:8.4.6" dependencies: - "@storybook/addon-actions": "npm:8.4.5" - "@storybook/addon-backgrounds": "npm:8.4.5" - "@storybook/addon-controls": "npm:8.4.5" - "@storybook/addon-docs": "npm:8.4.5" - "@storybook/addon-highlight": "npm:8.4.5" - "@storybook/addon-measure": "npm:8.4.5" - "@storybook/addon-outline": "npm:8.4.5" - "@storybook/addon-toolbars": "npm:8.4.5" - "@storybook/addon-viewport": "npm:8.4.5" + "@storybook/addon-actions": "npm:8.4.6" + "@storybook/addon-backgrounds": "npm:8.4.6" + "@storybook/addon-controls": "npm:8.4.6" + "@storybook/addon-docs": "npm:8.4.6" + "@storybook/addon-highlight": "npm:8.4.6" + "@storybook/addon-measure": "npm:8.4.6" + "@storybook/addon-outline": "npm:8.4.6" + "@storybook/addon-toolbars": "npm:8.4.6" + "@storybook/addon-viewport": "npm:8.4.6" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/fed258f3bbe6b380d61dd14f77b22049f8b2c38ac63cb08b66aa368301be7209cc7d7f2dea57caeed4f7021bedc6d35468ba42fda3e1e1cfe67a91713c0e0564 + storybook: ^8.4.6 + checksum: 10c0/b8fb83e018fcb1e8cad04b371af5f8ce9933e3a500a78a889715ecfe4efd9faa52acce2d0f97fb04fe9ae0898e661112816c052bfe9b5f01189938b122055a44 languageName: node linkType: hard -"@storybook/addon-highlight@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-highlight@npm:8.4.5" +"@storybook/addon-highlight@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/addon-highlight@npm:8.4.6" dependencies: "@storybook/global": "npm:^5.0.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/ba0b3f824e17279339ddafdf9c6b5e50158601c0f48185e24d26ff4070537d5e095452ad632402bd8d48a886a1a696c70bf996dd74637158858d3e98c18de44f + storybook: ^8.4.6 + checksum: 10c0/67a23a5e3b8f7740c7101e8fa886f3f9c6c61b6db3cb3430d2c805231f7ad170d2d926c12e7c9bfc4af327c5abac5b4155f4c0d70ea423b04704fe3def845acc languageName: node linkType: hard -"@storybook/addon-interactions@npm:^8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-interactions@npm:8.4.5" +"@storybook/addon-interactions@npm:^8.4.6": + version: 8.4.6 + resolution: "@storybook/addon-interactions@npm:8.4.6" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/instrumenter": "npm:8.4.5" - "@storybook/test": "npm:8.4.5" + "@storybook/instrumenter": "npm:8.4.6" + "@storybook/test": "npm:8.4.6" polished: "npm:^4.2.2" ts-dedent: "npm:^2.2.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/cbf639389a1d72bc17164eb273b69f76f245dfff242e6fbcad039faaf3a1f5bf770bc39ab02f826c04ab921d7a3cc1a4147223bd57da01bb453ea054f3fdbce5 + storybook: ^8.4.6 + checksum: 10c0/42e4bc2df354dba10217385687ac20fb355f4e1a2a7390812081d6b387151b67bca868211794e531c1e112dc4ce50c70dffa55c8f4338b0bd860d59363d58d5b languageName: node linkType: hard -"@storybook/addon-links@npm:^8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-links@npm:8.4.5" +"@storybook/addon-links@npm:^8.4.6": + version: 8.4.6 + resolution: "@storybook/addon-links@npm:8.4.6" dependencies: "@storybook/csf": "npm:^0.1.11" "@storybook/global": "npm:^5.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.5 + storybook: ^8.4.6 peerDependenciesMeta: react: optional: true - checksum: 10c0/842db4f5a1a9232cc27bd93e08ab777b02ceb186ccf8f7c6ac47dfc0c5cc111d94f8250185c99ca4715e84a19816593aac49bc4c597b430e75013fdce9af1f20 + checksum: 10c0/9360122d9c5370706a583526fb72efd0901d7e64c7467bfb4d832712cc41928d4fcfa397a53cfa17a1ae3875b8ef92ce6a10fb0bf0ce00149dc0d0eb1d66e27b languageName: node linkType: hard -"@storybook/addon-measure@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-measure@npm:8.4.5" +"@storybook/addon-measure@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/addon-measure@npm:8.4.6" dependencies: "@storybook/global": "npm:^5.0.0" tiny-invariant: "npm:^1.3.1" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/793670594ac4154b8456f2aba6bb113dd4afbf5079c547d54092ea91f8d0d5139d61a15180b5e24402b309874249c628f3e1ff389cba446abd98be31153bb917 + storybook: ^8.4.6 + checksum: 10c0/fd05b49fdb102a991fc696a0f75fde08d372b692778340ab2abc2c73fbd31a07dfa27a7a9d775dda7baaa9bd8a18972ed0bd86e9ce27948afb0305778f7b5a95 languageName: node linkType: hard -"@storybook/addon-outline@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-outline@npm:8.4.5" +"@storybook/addon-outline@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/addon-outline@npm:8.4.6" dependencies: "@storybook/global": "npm:^5.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/14f3993aa88a33035a048ea00713d936f0025055eb205c3033a65e5d885012c54b77a7363f994faeaefa362e6f88aad1f174bf01a1f6b0c85b3f96fbe8332772 + storybook: ^8.4.6 + checksum: 10c0/62600a9f4164a8d91118d37cd7be4f4dd871e849a156ba7728f463bc2cfc5a8a233df09055dd5e5733a042fde7a63b08616cb3c61b26c363c1e2d4ce20d92584 languageName: node linkType: hard -"@storybook/addon-toolbars@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-toolbars@npm:8.4.5" +"@storybook/addon-toolbars@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/addon-toolbars@npm:8.4.6" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/dbb76bad06d5c7ded93881d6195f2e63a744a006011cf177cd316e51cc42de323aa47b5d9c5c9a374f4b1c8c13c1dd503b079501bf0379672bc6be83df0863f0 + storybook: ^8.4.6 + checksum: 10c0/6525e71aaa3870ae97d407b662323022ade98859f89975110f5fb4a1d3f34b6c918d47fcc8a6a271f4a77acfcaadc963a846a83ebc6c748b37df50422ad60e7e languageName: node linkType: hard -"@storybook/addon-viewport@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-viewport@npm:8.4.5" +"@storybook/addon-viewport@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/addon-viewport@npm:8.4.6" dependencies: memoizerific: "npm:^1.11.3" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/4d828612602605f13fcac83e8f5095713ecfe791a47f36a301e306d15892ce038c15e9fb075a1ea18974cdf5a7a8da2f81e85260fcbb9ebd9b0ac1f2e60eae35 + storybook: ^8.4.6 + checksum: 10c0/824438cc44a45f90748ac5f20ac148a36d975a94fa89504a583e0e1188de8c574e042ad3cd537bc16ddb30d4e44e90f5a63263239b13419aec5334e2ece18cd0 languageName: node linkType: hard -"@storybook/blocks@npm:8.4.5, @storybook/blocks@npm:^8.4.5": - version: 8.4.5 - resolution: "@storybook/blocks@npm:8.4.5" +"@storybook/blocks@npm:8.4.6, @storybook/blocks@npm:^8.4.6": + version: 8.4.6 + resolution: "@storybook/blocks@npm:8.4.6" dependencies: "@storybook/csf": "npm:^0.1.11" "@storybook/icons": "npm:^1.2.12" @@ -5218,36 +5216,36 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.5 + storybook: ^8.4.6 peerDependenciesMeta: react: optional: true react-dom: optional: true - checksum: 10c0/6839e8439e0cec41c8562ed2b68641780ad017dd5ff45ea7414df00a85dc168cb06dc339523be46997827f630225ea77afdbbf859ed5a322d974a4aa92a14522 + checksum: 10c0/36d79c3aeb3d27f4ba966d62302e13fc17fd7b450dbfbcf538adfc6df3cfecb13c92f9d2542871fa747a77d7c770e413b358623049135355fb01454d6eb52d9a languageName: node linkType: hard -"@storybook/builder-vite@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/builder-vite@npm:8.4.5" +"@storybook/builder-vite@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/builder-vite@npm:8.4.6" dependencies: - "@storybook/csf-plugin": "npm:8.4.5" + "@storybook/csf-plugin": "npm:8.4.6" browser-assert: "npm:^1.2.1" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.5 - vite: ^4.0.0 || ^5.0.0 - checksum: 10c0/4588ac40606ac20ae523cda8c0c073e892e19ff5eecba4e39a04f84b7f7986587b3ce8386356bb423994e30903fd8100cd9d163d875c5c1a748854fc63ac3ec9 + storybook: ^8.4.6 + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 + checksum: 10c0/36998ffea04023a9f634ebbafe0d1ab3bd3e7c7fec8e8e6c4caef3ce0c94ce01fa44f332f40d0053edb788548f95096baf8561cd35c23fe3c9bcfd872f74f631 languageName: node linkType: hard -"@storybook/components@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/components@npm:8.4.5" +"@storybook/components@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/components@npm:8.4.6" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10c0/b166a73e79fee2747360d4e49a5d7171c3b45869dcc5a5bc72475bb711fc3d0bdf7dd1264ec248b69bf9a9afc7d85a1077616036ccb05e2f5c219aecab077176 + checksum: 10c0/1622b2f12b6d18e5c495a623deb2930888b3e8b173a271cbe42a7cbd6e14e80b736c57792ea97d5269dff0e6c0db40385d3ea80ab6e46d4cb6e104aee6cac6bc languageName: node linkType: hard @@ -5269,9 +5267,9 @@ __metadata: languageName: node linkType: hard -"@storybook/core@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/core@npm:8.4.5" +"@storybook/core@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/core@npm:8.4.6" dependencies: "@storybook/csf": "npm:^0.1.11" better-opn: "npm:^3.0.2" @@ -5289,18 +5287,18 @@ __metadata: peerDependenciesMeta: prettier: optional: true - checksum: 10c0/426327ebb7042c3f574fd076fa80c20662b26bdfab3f75c752f6facc03fa9100dfa7afda9c026c04dfe8a7f426524650423c644d1e511cbc96bdbc6c8c4c20e4 + checksum: 10c0/1e30268eec18458dd78ed4b97fb12ac47b2c3cb41ffcbe9e9f5934b3f0c83b0bfcb0c0d508926344779383cc5260f992dcd534ffffab3f05425c7cee8c90687c languageName: node linkType: hard -"@storybook/csf-plugin@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/csf-plugin@npm:8.4.5" +"@storybook/csf-plugin@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/csf-plugin@npm:8.4.6" dependencies: unplugin: "npm:^1.3.1" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/c23b423740820679a4fcef9df8b077b24a047f250d1710e87a2fd6918b71bfeb513749eb41c0c072f1ac86e5888e666486cd7f58105d44e5e5bd727653ca1401 + storybook: ^8.4.6 + checksum: 10c0/d771f36ee768c6ff62ecd930c6ff64a4ba45bdbb7f7fb41e5f4ffd02204e3f54b17ed091049b265a6d371922bf599bfe749eb9deabfcd7e2b4fb5a5444655241 languageName: node linkType: hard @@ -5330,24 +5328,24 @@ __metadata: languageName: node linkType: hard -"@storybook/instrumenter@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/instrumenter@npm:8.4.5" +"@storybook/instrumenter@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/instrumenter@npm:8.4.6" dependencies: "@storybook/global": "npm:^5.0.0" "@vitest/utils": "npm:^2.1.1" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/8fcebc70c5818182c81c8361e4e82ceb75978366d66a6c02a423b2c0efc7cf30c68addc9fd87c2801ee92664962eb0091c324c74eb3ba6dc7514e529d37d3d42 + storybook: ^8.4.6 + checksum: 10c0/602017872236124dc9dfa77d6bc2c5987d540063f15c7ace83bf91060d9343fdbe113a61cba44e17cae2247aeeb69875ebf45ff66ce9c28d364d2d3638eb3ec8 languageName: node linkType: hard -"@storybook/manager-api@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/manager-api@npm:8.4.5" +"@storybook/manager-api@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/manager-api@npm:8.4.6" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10c0/bf75ad329d7bcc66e810b34930ea39bf22d8fb052c6c8e26d113f0531b7e294374cd6b1c7250a9e3f0fb668a9026627a13a80f61f2e3991facf7a288020589ad + checksum: 10c0/5921ec72df0be765bd398aa906186c9b121a8b3415a7e1a10014a8d17c44aec386b59de3d240017bfc925be00c40a4da8d26991b5fa39023f23ba8efe1b0d58e languageName: node linkType: hard @@ -5360,34 +5358,34 @@ __metadata: languageName: node linkType: hard -"@storybook/preview-api@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/preview-api@npm:8.4.5" +"@storybook/preview-api@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/preview-api@npm:8.4.6" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10c0/e00955596f28e12ae19060d4e0c04c7b4e39f31293200afe861dfd94f5da1a3389a1a223afe3cf01dc5552c1dac46b23d88b07eee6a7d2be36ecc90aa98f8af8 + checksum: 10c0/63967f4813c75e410634bff20189b5a670a061cfeeaa601ec07f0de82e2b4955af292836030d5a8432c3c7e48968285e121ed2bb55d2b5c70d17dbb4ada3c051 languageName: node linkType: hard -"@storybook/react-dom-shim@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/react-dom-shim@npm:8.4.5" +"@storybook/react-dom-shim@npm:8.4.6": + version: 8.4.6 + resolution: "@storybook/react-dom-shim@npm:8.4.6" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.5 - checksum: 10c0/358bdb85346517128acca483ffad9110e79c4d279d64b40929256158190f5d5b774b16631c84b121ab39b616ac893468d7172c19d542dd53368456bb649ebb52 + storybook: ^8.4.6 + checksum: 10c0/b97c6faa3adc3efe1b7b6f5e38476e040c0a988b14db68e368d704c68f3f4d4bf7866b36607c118a0483242921b34944b5f5f72614d9852476476f6ead462e5c languageName: node linkType: hard -"@storybook/react-vite@npm:^8.4.5": - version: 8.4.5 - resolution: "@storybook/react-vite@npm:8.4.5" +"@storybook/react-vite@npm:^8.4.6": + version: 8.4.6 + resolution: "@storybook/react-vite@npm:8.4.6" dependencies: - "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.3.0" + "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.4.2" "@rollup/pluginutils": "npm:^5.0.2" - "@storybook/builder-vite": "npm:8.4.5" - "@storybook/react": "npm:8.4.5" + "@storybook/builder-vite": "npm:8.4.6" + "@storybook/react": "npm:8.4.6" find-up: "npm:^5.0.0" magic-string: "npm:^0.30.0" react-docgen: "npm:^7.0.0" @@ -5396,61 +5394,61 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.5 - vite: ^4.0.0 || ^5.0.0 - checksum: 10c0/667ca7c7d8309ff36e96b6820c00bddfe11b634fd591f7ed0d467613ceba84d89b518215c87070d0a27d5be4b332c0c8320a05cc1a19ad7d3071902cfbfe8e14 + storybook: ^8.4.6 + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 + checksum: 10c0/9f81a19461dbbf11932a13f8fb611dbcd95fbfa695ee5536daf7e078bf0feb5ddda2738606073826131e3fee710e230dce9042e3f7f985203392376aa8407643 languageName: node linkType: hard -"@storybook/react@npm:8.4.5, @storybook/react@npm:^8.4.5": - version: 8.4.5 - resolution: "@storybook/react@npm:8.4.5" +"@storybook/react@npm:8.4.6, @storybook/react@npm:^8.4.6": + version: 8.4.6 + resolution: "@storybook/react@npm:8.4.6" dependencies: - "@storybook/components": "npm:8.4.5" + "@storybook/components": "npm:8.4.6" "@storybook/global": "npm:^5.0.0" - "@storybook/manager-api": "npm:8.4.5" - "@storybook/preview-api": "npm:8.4.5" - "@storybook/react-dom-shim": "npm:8.4.5" - "@storybook/theming": "npm:8.4.5" + "@storybook/manager-api": "npm:8.4.6" + "@storybook/preview-api": "npm:8.4.6" + "@storybook/react-dom-shim": "npm:8.4.6" + "@storybook/theming": "npm:8.4.6" peerDependencies: - "@storybook/test": 8.4.5 + "@storybook/test": 8.4.6 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.5 + storybook: ^8.4.6 typescript: ">= 4.2.x" peerDependenciesMeta: "@storybook/test": optional: true typescript: optional: true - checksum: 10c0/207e03c3dfcabb0b11d3a2440166d8eeb4f76318e32dd274c87a9503af7f2bedee255a13d358d653654f6eca2b81fb579c88f909f3e86f6f167187ca0aaadba9 + checksum: 10c0/1441f8ab3be91757647c6b1a05eb1ef0d78a454ffd14b01a14fdde00e92a8be8fc7c8408c4670b46bc20a5a04995514f0890e98ed6ee35c362ff36141da02f02 languageName: node linkType: hard -"@storybook/test@npm:8.4.5, @storybook/test@npm:^8.4.5": - version: 8.4.5 - resolution: "@storybook/test@npm:8.4.5" +"@storybook/test@npm:8.4.6, @storybook/test@npm:^8.4.6": + version: 8.4.6 + resolution: "@storybook/test@npm:8.4.6" dependencies: "@storybook/csf": "npm:^0.1.11" "@storybook/global": "npm:^5.0.0" - "@storybook/instrumenter": "npm:8.4.5" + "@storybook/instrumenter": "npm:8.4.6" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.5.0" "@testing-library/user-event": "npm:14.5.2" "@vitest/expect": "npm:2.0.5" "@vitest/spy": "npm:2.0.5" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/2e52d7a7f1da506bae551a1242152d2d6ac0d5415fee0b39c583c02537b4931cb73937b482d8529ad6c8847ca6b28838cb2b421c809fb9d7057ae0a05b005bda + storybook: ^8.4.6 + checksum: 10c0/fbf7c2ac7773a7fe18145876eb67491ce90b000ba5f8e364a319569e56d56e706fdd1c7ef24d3ab2ffa3dfcdb92377d8050c8ffbd457d2d8b613aba2a4845a04 languageName: node linkType: hard -"@storybook/theming@npm:8.4.5, @storybook/theming@npm:^8.4.5": - version: 8.4.5 - resolution: "@storybook/theming@npm:8.4.5" +"@storybook/theming@npm:8.4.6, @storybook/theming@npm:^8.4.6": + version: 8.4.6 + resolution: "@storybook/theming@npm:8.4.6" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10c0/9dbb92605f88eef3a5d4ca3b01a8815939e9a08c9eb3cef55e05c8f196c6bcd1a92ab1592ff0a489256382e172587c385a7cfdac227feb64e21cba65017fa818 + checksum: 10c0/7d9c8e5ef2c1d974cd5258301350a2345890326e7be7a5ed6bdd0db70fd1648c0bbb8ee1d905f8e66fa57b75c47aefe7ec9772ec0bfb9691d127dcc19286e4c9 languageName: node linkType: hard @@ -6159,16 +6157,6 @@ __metadata: languageName: node linkType: hard -"@types/glob@npm:^7.1.3": - version: 7.2.0 - resolution: "@types/glob@npm:7.2.0" - dependencies: - "@types/minimatch": "npm:*" - "@types/node": "npm:*" - checksum: 10c0/a8eb5d5cb5c48fc58c7ca3ff1e1ddf771ee07ca5043da6e4871e6757b4472e2e73b4cfef2644c38983174a4bc728c73f8da02845c28a1212f98cabd293ecae98 - languageName: node - linkType: hard - "@types/hast@npm:^3.0.0": version: 3.0.4 resolution: "@types/hast@npm:3.0.4" @@ -6295,13 +6283,6 @@ __metadata: languageName: node linkType: hard -"@types/minimatch@npm:*": - version: 5.1.2 - resolution: "@types/minimatch@npm:5.1.2" - checksum: 10c0/83cf1c11748891b714e129de0585af4c55dd4c2cafb1f1d5233d79246e5e1e19d1b5ad9e8db449667b3ffa2b6c80125c429dbee1054e9efb45758dbc4e118562 - languageName: node - linkType: hard - "@types/ms@npm:*": version: 0.7.34 resolution: "@types/ms@npm:0.7.34" @@ -6343,12 +6324,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^22.9.1": - version: 22.9.1 - resolution: "@types/node@npm:22.9.1" +"@types/node@npm:^22.10.1": + version: 22.10.1 + resolution: "@types/node@npm:22.10.1" dependencies: - undici-types: "npm:~6.19.8" - checksum: 10c0/ea489ae603aa8874e4e88980aab6f2dad09c755da779c88dd142983bfe9609803c89415ca7781f723072934066f63daf2b3339ef084a8ad1a8079cf3958be243 + undici-types: "npm:~6.20.0" + checksum: 10c0/0fbb6d29fa35d807f0223a4db709c598ac08d66820240a2cd6a8a69b8f0bc921d65b339d850a666b43b4e779f967e6ed6cf6f0fca3575e08241e6b900364c234 languageName: node linkType: hard @@ -6676,15 +6657,15 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.15.0": - version: 8.15.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.15.0" +"@typescript-eslint/eslint-plugin@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.16.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.15.0" - "@typescript-eslint/type-utils": "npm:8.15.0" - "@typescript-eslint/utils": "npm:8.15.0" - "@typescript-eslint/visitor-keys": "npm:8.15.0" + "@typescript-eslint/scope-manager": "npm:8.16.0" + "@typescript-eslint/type-utils": "npm:8.16.0" + "@typescript-eslint/utils": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" graphemer: "npm:^1.4.0" ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" @@ -6695,44 +6676,44 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/90ef10cc7d37a81abec4f4a3ffdfc3a0da8e99d949e03c75437e96e8ab2e896e34b85ab64718690180a7712581031b8611c5d8e7666d6ed4d60b9ace834d58e3 + checksum: 10c0/b03612b726ee5aff631cd50e05ceeb06a522e64465e4efdc134e3a27a09406b959ef7a05ec4acef1956b3674dc4fedb6d3a62ce69382f9e30c227bd4093003e5 languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.15.0": - version: 8.15.0 - resolution: "@typescript-eslint/parser@npm:8.15.0" +"@typescript-eslint/parser@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/parser@npm:8.16.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.15.0" - "@typescript-eslint/types": "npm:8.15.0" - "@typescript-eslint/typescript-estree": "npm:8.15.0" - "@typescript-eslint/visitor-keys": "npm:8.15.0" + "@typescript-eslint/scope-manager": "npm:8.16.0" + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/typescript-estree": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/19c25aea0dc51faa758701a5319a89950fd30494d9d645db8ced84fb60714c5e7d4b51fc4ee8ccb07ddefec88c51ee307ee7e49addd6330ee8f3e7ee9ba329fc + checksum: 10c0/e49c6640a7a863a16baecfbc5b99392a4731e9c7e9c9aaae4efbc354e305485fe0f39a28bf0acfae85bc01ce37fe0cc140fd315fdaca8b18f9b5e0addff8ceae languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.15.0": - version: 8.15.0 - resolution: "@typescript-eslint/scope-manager@npm:8.15.0" +"@typescript-eslint/scope-manager@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/scope-manager@npm:8.16.0" dependencies: - "@typescript-eslint/types": "npm:8.15.0" - "@typescript-eslint/visitor-keys": "npm:8.15.0" - checksum: 10c0/c27dfdcea4100cc2d6fa967f857067cbc93155b55e648f9f10887a1b9372bb76cf864f7c804f3fa48d7868d9461cdef10bcea3dab7637d5337e8aa8042dc08b9 + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" + checksum: 10c0/23b7c738b83f381c6419a36e6ca951944187e3e00abb8e012bce8041880410fe498303e28bdeb0e619023a69b14cf32a5ec1f9427c5382807788cd8e52a46a6e languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.15.0": - version: 8.15.0 - resolution: "@typescript-eslint/type-utils@npm:8.15.0" +"@typescript-eslint/type-utils@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/type-utils@npm:8.16.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:8.15.0" - "@typescript-eslint/utils": "npm:8.15.0" + "@typescript-eslint/typescript-estree": "npm:8.16.0" + "@typescript-eslint/utils": "npm:8.16.0" debug: "npm:^4.3.4" ts-api-utils: "npm:^1.3.0" peerDependencies: @@ -6740,23 +6721,23 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/20f09c79c83b38a962cf7eff10d47a2c01bcc0bab7bf6d762594221cd89023ef8c7aec26751c47b524f53f5c8d38bba55a282529b3df82d5f5ab4350496316f9 + checksum: 10c0/24c0e815c8bdf99bf488c7528bd6a7c790e8b3b674cb7fb075663afc2ee26b48e6f4cf7c0d14bb21e2376ca62bd8525cbcb5688f36135b00b62b1d353d7235b9 languageName: node linkType: hard -"@typescript-eslint/types@npm:8.15.0": - version: 8.15.0 - resolution: "@typescript-eslint/types@npm:8.15.0" - checksum: 10c0/84abc6fd954aff13822a76ac49efdcb90a55c0025c20eee5d8cebcfb68faff33b79bbc711ea524e0209cecd90c5ee3a5f92babc7083c081d3a383a0710264a41 +"@typescript-eslint/types@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/types@npm:8.16.0" + checksum: 10c0/141e257ab4060a9c0e2e14334ca14ab6be713659bfa38acd13be70a699fb5f36932a2584376b063063ab3d723b24bc703dbfb1ce57d61d7cfd7ec5bd8a975129 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.15.0": - version: 8.15.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.15.0" +"@typescript-eslint/typescript-estree@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.16.0" dependencies: - "@typescript-eslint/types": "npm:8.15.0" - "@typescript-eslint/visitor-keys": "npm:8.15.0" + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" debug: "npm:^4.3.4" fast-glob: "npm:^3.3.2" is-glob: "npm:^4.0.3" @@ -6766,34 +6747,34 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/3af5c129532db3575349571bbf64d32aeccc4f4df924ac447f5d8f6af8b387148df51965eb2c9b99991951d3dadef4f2509d7ce69bf34a2885d013c040762412 + checksum: 10c0/f28fea5af4798a718b6735d1758b791a331af17386b83cb2856d89934a5d1693f7cb805e73c3b33f29140884ac8ead9931b1d7c3de10176fa18ca7a346fe10d0 languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.15.0": - version: 8.15.0 - resolution: "@typescript-eslint/utils@npm:8.15.0" +"@typescript-eslint/utils@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/utils@npm:8.16.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:8.15.0" - "@typescript-eslint/types": "npm:8.15.0" - "@typescript-eslint/typescript-estree": "npm:8.15.0" + "@typescript-eslint/scope-manager": "npm:8.16.0" + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/typescript-estree": "npm:8.16.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/65743f51845a1f6fd2d21f66ca56182ba33e966716bdca73d30b7a67c294e47889c322de7d7b90ab0818296cd33c628e5eeeb03cec7ef2f76c47de7a453eeda2 + checksum: 10c0/1e61187eef3da1ab1486d2a977d8f3b1cb8ef7fa26338500a17eb875ca42a8942ef3f2241f509eef74cf7b5620c109483afc7d83d5b0ab79b1e15920f5a49818 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.15.0": - version: 8.15.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.15.0" +"@typescript-eslint/visitor-keys@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.16.0" dependencies: - "@typescript-eslint/types": "npm:8.15.0" + "@typescript-eslint/types": "npm:8.16.0" eslint-visitor-keys: "npm:^4.2.0" - checksum: 10c0/02a954c3752c4328482a884eb1da06ca8fb72ae78ef28f1d854b18f3779406ed47263af22321cf3f65a637ec7584e5f483e34a263b5c8cec60ec85aebc263574 + checksum: 10c0/537df37801831aa8d91082b2adbffafd40305ed4518f0e7d3cbb17cc466d8b9ac95ac91fa232e7fe585d7c522d1564489ec80052ebb2a6ab9bbf89ef9dd9b7bc languageName: node linkType: hard @@ -6804,20 +6785,20 @@ __metadata: languageName: node linkType: hard -"@vitejs/plugin-react-swc@npm:^3.7.1": - version: 3.7.1 - resolution: "@vitejs/plugin-react-swc@npm:3.7.1" +"@vitejs/plugin-react-swc@npm:^3.7.2": + version: 3.7.2 + resolution: "@vitejs/plugin-react-swc@npm:3.7.2" dependencies: "@swc/core": "npm:^1.7.26" peerDependencies: - vite: ^4 || ^5 - checksum: 10c0/2d613e69c0d0b809c94df80ca2b0caf39c50f0b98aa1f8599fd086bc37dac1449898eb6572000e1c133313137cac93440c4cb0861e05820c78bd2c07a52e64a8 + vite: ^4 || ^5 || ^6 + checksum: 10c0/9b9a5e0540791ba96a9fe4e8b8146ab274edcc730315535705f20126d6dfaffe72ae474bac9904ce841976e1959b6ecffd047bb2f0b7abf4d85aae7fbfdd00ab languageName: node linkType: hard -"@vitest/coverage-istanbul@npm:^2.1.5": - version: 2.1.5 - resolution: "@vitest/coverage-istanbul@npm:2.1.5" +"@vitest/coverage-istanbul@npm:^2.1.6": + version: 2.1.6 + resolution: "@vitest/coverage-istanbul@npm:2.1.6" dependencies: "@istanbuljs/schema": "npm:^0.1.3" debug: "npm:^4.3.7" @@ -6830,8 +6811,8 @@ __metadata: test-exclude: "npm:^7.0.1" tinyrainbow: "npm:^1.2.0" peerDependencies: - vitest: 2.1.5 - checksum: 10c0/368aefd5a8348f633a5351dbd6ccf34de7107c6f96a4718330beb096aa30db41af3d4bcd4c32a01c89df41318aea2b61936398fe8a77d20fadd6d2c87e5c2a76 + vitest: 2.1.6 + checksum: 10c0/ec70ee61a60357f586f0f7911018caef7a41ff622f8c2e9683d36dd620f50bc9ed2445916aced64e1f34428a2c3ea7c1d2ddfdb23201a0f59d72c0d828e30216 languageName: node linkType: hard @@ -6847,34 +6828,34 @@ __metadata: languageName: node linkType: hard -"@vitest/expect@npm:2.1.5": - version: 2.1.5 - resolution: "@vitest/expect@npm:2.1.5" +"@vitest/expect@npm:2.1.6": + version: 2.1.6 + resolution: "@vitest/expect@npm:2.1.6" dependencies: - "@vitest/spy": "npm:2.1.5" - "@vitest/utils": "npm:2.1.5" + "@vitest/spy": "npm:2.1.6" + "@vitest/utils": "npm:2.1.6" chai: "npm:^5.1.2" tinyrainbow: "npm:^1.2.0" - checksum: 10c0/68f7011e7883dea1d1974fa05d30d7a1eff72f08741312e84f1b138f474e75e9db7ff7ced23a50fc16605baa123a2f10ef9a834b418e03dbeed23d1e0043fc90 + checksum: 10c0/86327692f03b2ec6895486b118f25e1a141749c31ba671d253da4e33cf81db81f40755198ac9b46616155a8b74765d0ab15b8080041dbe139c83a9a0690004a2 languageName: node linkType: hard -"@vitest/mocker@npm:2.1.5": - version: 2.1.5 - resolution: "@vitest/mocker@npm:2.1.5" +"@vitest/mocker@npm:2.1.6": + version: 2.1.6 + resolution: "@vitest/mocker@npm:2.1.6" dependencies: - "@vitest/spy": "npm:2.1.5" + "@vitest/spy": "npm:2.1.6" estree-walker: "npm:^3.0.3" magic-string: "npm:^0.30.12" peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 + vite: ^5.0.0 || ^6.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - checksum: 10c0/57034aa3476768133042c6b4193d71dbd4ace98c39241ae2c1fa21c33d5afd6d469de86511cdc59a0d7dd5585c05ac605406c60b0ae3cfbf3f650326642d4aca + checksum: 10c0/f8f4482e196a72cc9f202edb07da6f8d612f0daef272525a4c2447a5ffa435cc1cfe758cb79af0e296a60faa973ee0e2767cbb8bb6769eff2b5b9c6941ceda7c languageName: node linkType: hard @@ -6896,33 +6877,33 @@ __metadata: languageName: node linkType: hard -"@vitest/pretty-format@npm:2.1.5, @vitest/pretty-format@npm:^2.1.5": - version: 2.1.5 - resolution: "@vitest/pretty-format@npm:2.1.5" +"@vitest/pretty-format@npm:2.1.6, @vitest/pretty-format@npm:^2.1.6": + version: 2.1.6 + resolution: "@vitest/pretty-format@npm:2.1.6" dependencies: tinyrainbow: "npm:^1.2.0" - checksum: 10c0/d6667f1e5d272f557f8cca440af65645346b5aa74a04041466859087f14a78a296e3f1928caa05de0cc558880cc8a49ce14696fef7b8f5dbc3eb856d672b0abf + checksum: 10c0/5c82496e5816c0c388bbe18a88ed01b39f5492aaa8e0df90868a65a50ee135105da367e58b4a0bed0dc67201c0518c451a32a5d9b81f56665b76b1c75c550686 languageName: node linkType: hard -"@vitest/runner@npm:2.1.5": - version: 2.1.5 - resolution: "@vitest/runner@npm:2.1.5" +"@vitest/runner@npm:2.1.6": + version: 2.1.6 + resolution: "@vitest/runner@npm:2.1.6" dependencies: - "@vitest/utils": "npm:2.1.5" + "@vitest/utils": "npm:2.1.6" pathe: "npm:^1.1.2" - checksum: 10c0/d39ea4c6f8805aa3e52130ac0a3d325506a4d4bb97d0d7ac80734beb21d9a496ee50586de9801f4b66f2dc8ff38f27a75065a258fd3633bc1cfe68bd9c1dd73e + checksum: 10c0/3105aaa875ac58e237626ae3f9734cf003c2e40886a96ba20e78f1c1f721c9013d4f720d4a2fa4cd3a415ccd529bb4c7ff73021871c8ecda423f7e1fb7695cd4 languageName: node linkType: hard -"@vitest/snapshot@npm:2.1.5": - version: 2.1.5 - resolution: "@vitest/snapshot@npm:2.1.5" +"@vitest/snapshot@npm:2.1.6": + version: 2.1.6 + resolution: "@vitest/snapshot@npm:2.1.6" dependencies: - "@vitest/pretty-format": "npm:2.1.5" + "@vitest/pretty-format": "npm:2.1.6" magic-string: "npm:^0.30.12" pathe: "npm:^1.1.2" - checksum: 10c0/3dc44b5a043acbbd15e08c3c0519ef5a344d06ade10ee9522b4e4305f4826f2be8353b58d0b6e11aa272078ba42ff0d2ffa62368b6e0cf996ad0d7977df9f22f + checksum: 10c0/91708ee9fdf9ccaa637d9fa58141342831aaf2119641f050a45f80cca37f0a4c1f7d65363e609a6353594418d5ecfebacd090c64273cd63de6915d186c948632 languageName: node linkType: hard @@ -6935,12 +6916,12 @@ __metadata: languageName: node linkType: hard -"@vitest/spy@npm:2.1.5": - version: 2.1.5 - resolution: "@vitest/spy@npm:2.1.5" +"@vitest/spy@npm:2.1.6": + version: 2.1.6 + resolution: "@vitest/spy@npm:2.1.6" dependencies: tinyspy: "npm:^3.0.2" - checksum: 10c0/c5222cc7074db5705573e5da674b8488f9e46d61a2bd64e992f5f5819feff35f015e8d0236c7e07d1870bddf5d36dc0622f674c071ab4ca8fa4f4f5d02172315 + checksum: 10c0/4d3e965f9096968125dd4f53a66a8afd7a105852a41bd36865288bef5b6a79f3ec6f2056cc1c7be5a0afb2cd86b3b83e354451f452bf049ddd4516005a748f6f languageName: node linkType: hard @@ -6956,14 +6937,14 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:2.1.5": - version: 2.1.5 - resolution: "@vitest/utils@npm:2.1.5" +"@vitest/utils@npm:2.1.6": + version: 2.1.6 + resolution: "@vitest/utils@npm:2.1.6" dependencies: - "@vitest/pretty-format": "npm:2.1.5" + "@vitest/pretty-format": "npm:2.1.6" loupe: "npm:^3.1.2" tinyrainbow: "npm:^1.2.0" - checksum: 10c0/3d1e65025e418948b215b8856548a91856522660d898b872485a91acf397e085e90968ee9c3f521589b5274717da32e954ef8a549aa60cc1c3338224fdfb4c5e + checksum: 10c0/1ec077b9707ec627075348f1a98687c7bb2fcbf9edf6e73dea6842c328d0d51a33663d22679e2a90a75f45afca9246f8fa3ea452447f474ae9d5f4eca4023b91 languageName: node linkType: hard @@ -10603,7 +10584,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0": +"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0, esbuild@npm:^0.24.0": version: 0.24.0 resolution: "esbuild@npm:0.24.0" dependencies: @@ -10686,7 +10667,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.21.3, esbuild@npm:^0.21.5": +"esbuild@npm:^0.21.5": version: 0.21.5 resolution: "esbuild@npm:0.21.5" dependencies: @@ -11961,17 +11942,6 @@ __metadata: languageName: node linkType: hard -"glob-promise@npm:^4.2.0": - version: 4.2.2 - resolution: "glob-promise@npm:4.2.2" - dependencies: - "@types/glob": "npm:^7.1.3" - peerDependencies: - glob: ^7.1.6 - checksum: 10c0/3eb01bed2901539365df6a4d27800afb8788840647d01f9bf3500b3de756597f2ff4b8c823971ace34db228c83159beca459dc42a70968d4e9c8200ed2cc96bd - languageName: node - linkType: hard - "glob-to-regexp@npm:^0.4.1": version: 0.4.1 resolution: "glob-to-regexp@npm:0.4.1" @@ -11995,7 +11965,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.0.0, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.2.0": +"glob@npm:^7.0.0, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -16364,7 +16334,7 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.0": +"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 @@ -17250,7 +17220,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.21, postcss@npm:^8.4.24, postcss@npm:^8.4.26, postcss@npm:^8.4.33, postcss@npm:^8.4.38, postcss@npm:^8.4.43": +"postcss@npm:^8.4.21, postcss@npm:^8.4.24, postcss@npm:^8.4.26, postcss@npm:^8.4.33, postcss@npm:^8.4.38": version: 8.4.47 resolution: "postcss@npm:8.4.47" dependencies: @@ -17261,6 +17231,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.4.49": + version: 8.4.49 + resolution: "postcss@npm:8.4.49" + dependencies: + nanoid: "npm:^3.3.7" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/f1b3f17aaf36d136f59ec373459f18129908235e65dbdc3aee5eef8eba0756106f52de5ec4682e29a2eab53eb25170e7e871b3e4b52a8f1de3d344a514306be3 + languageName: node + linkType: hard + "prebuild-install@npm:^7.1.1": version: 7.1.2 resolution: "prebuild-install@npm:7.1.2" @@ -17326,12 +17307,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.3.3": - version: 3.3.3 - resolution: "prettier@npm:3.3.3" +"prettier@npm:^3.4.1": + version: 3.4.1 + resolution: "prettier@npm:3.4.1" bin: prettier: bin/prettier.cjs - checksum: 10c0/b85828b08e7505716324e4245549b9205c0cacb25342a030ba8885aba2039a115dbcf75a0b7ca3b37bc9d101ee61fab8113fc69ca3359f2a226f1ecc07ad2e26 + checksum: 10c0/2d6cc3101ad9de72b49c59339480b0983e6ff6742143da0c43f476bf3b5ef88ede42ebd9956d7a0a8fa59f7a5990e8ef03c9ad4c37f7e4c9e5db43ee0853156c languageName: node linkType: hard @@ -19011,28 +18992,28 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.20.0": - version: 4.24.4 - resolution: "rollup@npm:4.24.4" +"rollup@npm:^4.23.0": + version: 4.27.4 + resolution: "rollup@npm:4.27.4" dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.24.4" - "@rollup/rollup-android-arm64": "npm:4.24.4" - "@rollup/rollup-darwin-arm64": "npm:4.24.4" - "@rollup/rollup-darwin-x64": "npm:4.24.4" - "@rollup/rollup-freebsd-arm64": "npm:4.24.4" - "@rollup/rollup-freebsd-x64": "npm:4.24.4" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.24.4" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.24.4" - "@rollup/rollup-linux-arm64-gnu": "npm:4.24.4" - "@rollup/rollup-linux-arm64-musl": "npm:4.24.4" - "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.24.4" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.24.4" - "@rollup/rollup-linux-s390x-gnu": "npm:4.24.4" - "@rollup/rollup-linux-x64-gnu": "npm:4.24.4" - "@rollup/rollup-linux-x64-musl": "npm:4.24.4" - "@rollup/rollup-win32-arm64-msvc": "npm:4.24.4" - "@rollup/rollup-win32-ia32-msvc": "npm:4.24.4" - "@rollup/rollup-win32-x64-msvc": "npm:4.24.4" + "@rollup/rollup-android-arm-eabi": "npm:4.27.4" + "@rollup/rollup-android-arm64": "npm:4.27.4" + "@rollup/rollup-darwin-arm64": "npm:4.27.4" + "@rollup/rollup-darwin-x64": "npm:4.27.4" + "@rollup/rollup-freebsd-arm64": "npm:4.27.4" + "@rollup/rollup-freebsd-x64": "npm:4.27.4" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.27.4" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.27.4" + "@rollup/rollup-linux-arm64-gnu": "npm:4.27.4" + "@rollup/rollup-linux-arm64-musl": "npm:4.27.4" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.27.4" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.27.4" + "@rollup/rollup-linux-s390x-gnu": "npm:4.27.4" + "@rollup/rollup-linux-x64-gnu": "npm:4.27.4" + "@rollup/rollup-linux-x64-musl": "npm:4.27.4" + "@rollup/rollup-win32-arm64-msvc": "npm:4.27.4" + "@rollup/rollup-win32-ia32-msvc": "npm:4.27.4" + "@rollup/rollup-win32-x64-msvc": "npm:4.27.4" "@types/estree": "npm:1.0.6" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -19076,7 +19057,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10c0/8e9e9ce4dc8cc48acf258a26519ed1bbbbdac99fd701e89d11c31271e01b4663fe61d839f7906a49c0983b1a49e2acc622948d7665ff0f57ecc48d872835d1ce + checksum: 10c0/1442650cfea5e4617ce14743784f6f578817e31db56f9c8aaf96a82daa9bc20b6ccd66c0d677dbf302a4da3e70664dc3bef11a1aec85e6aff3cecccb945b1d35 languageName: node linkType: hard @@ -19981,11 +19962,11 @@ __metadata: languageName: node linkType: hard -"storybook@npm:^8.4.5": - version: 8.4.5 - resolution: "storybook@npm:8.4.5" +"storybook@npm:^8.4.6": + version: 8.4.6 + resolution: "storybook@npm:8.4.6" dependencies: - "@storybook/core": "npm:8.4.5" + "@storybook/core": "npm:8.4.6" peerDependencies: prettier: ^2 || ^3 peerDependenciesMeta: @@ -19995,7 +19976,7 @@ __metadata: getstorybook: ./bin/index.cjs sb: ./bin/index.cjs storybook: ./bin/index.cjs - checksum: 10c0/8dd216ea47ab8e76bb9cb24776999373b6d6cde061ff89db4e469e899e6b35b7f5882123e769eb6bf48457a995d0870a08f57a257afc2099161fbb6f6f098c4e + checksum: 10c0/e15249718c1efab3d3d05f3152df28fc8f7e2e988bf7414cd4abf2adfb5d6c3b802f05dad5be0521c30d0ba43e55abf516e6f874b0671e0d1e84a7096cb47d3d languageName: node linkType: hard @@ -20769,19 +20750,19 @@ __metadata: languageName: node linkType: hard -"typescript-eslint@npm:^8.15.0": - version: 8.15.0 - resolution: "typescript-eslint@npm:8.15.0" +"typescript-eslint@npm:^8.16.0": + version: 8.16.0 + resolution: "typescript-eslint@npm:8.16.0" dependencies: - "@typescript-eslint/eslint-plugin": "npm:8.15.0" - "@typescript-eslint/parser": "npm:8.15.0" - "@typescript-eslint/utils": "npm:8.15.0" + "@typescript-eslint/eslint-plugin": "npm:8.16.0" + "@typescript-eslint/parser": "npm:8.16.0" + "@typescript-eslint/utils": "npm:8.16.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/589aebf0d0b9b79db1cd0b7c2ea08c6b5727c1db095d39077d070c332066c7d549a0eb2ef60b0d41619720c317c1955236c5c8ee6320bc7c6ae475add7223b55 + checksum: 10c0/3da9401d6c2416b9d95c96a41a9423a5379d233a120cd3304e2c03f191d350ce91cf0c7e60017f7b10c93b4cc1190592702735735b771c1ce1bf68f71a9f1647 languageName: node linkType: hard @@ -20852,6 +20833,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.20.0": + version: 6.20.0 + resolution: "undici-types@npm:6.20.0" + checksum: 10c0/68e659a98898d6a836a9a59e6adf14a5d799707f5ea629433e025ac90d239f75e408e2e5ff086afc3cace26f8b26ee52155293564593fbb4a2f666af57fc59bf + languageName: node + linkType: hard + "unicode-canonical-property-names-ecmascript@npm:^2.0.0": version: 2.0.1 resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.1" @@ -21421,18 +21409,18 @@ __metadata: languageName: node linkType: hard -"vite-node@npm:2.1.5": - version: 2.1.5 - resolution: "vite-node@npm:2.1.5" +"vite-node@npm:2.1.6": + version: 2.1.6 + resolution: "vite-node@npm:2.1.6" dependencies: cac: "npm:^6.7.14" debug: "npm:^4.3.7" es-module-lexer: "npm:^1.5.4" pathe: "npm:^1.1.2" - vite: "npm:^5.0.0" + vite: "npm:^5.0.0 || ^6.0.0" bin: vite-node: vite-node.mjs - checksum: 10c0/4ebe6bdf52f5ed65cb6f18af087faa87d8dca8e1a87413d1dbb8ead141d6e5d359ae006bd6c5e8f8c89cd5d90499bbf1d3f9e9a161dcc4bc86ec526862c01360 + checksum: 10c0/513c815a6d4ca09a48fc8741bd4bfb1a6cbfe5555b009081b194069863326aede7d66197c2b60927514d22cc6efc2c793d1ca1bb70e13dca120e01a507bcf35b languageName: node linkType: hard @@ -21448,17 +21436,17 @@ __metadata: languageName: node linkType: hard -"vite-plugin-static-copy@npm:^2.1.0": - version: 2.1.0 - resolution: "vite-plugin-static-copy@npm:2.1.0" +"vite-plugin-static-copy@npm:^2.2.0": + version: 2.2.0 + resolution: "vite-plugin-static-copy@npm:2.2.0" dependencies: chokidar: "npm:^3.5.3" fast-glob: "npm:^3.2.11" fs-extra: "npm:^11.1.0" picocolors: "npm:^1.0.0" peerDependencies: - vite: ^5.0.0 - checksum: 10c0/76b7e3ccd2f7d3b5067954ddac8605da333d842239e63f60cb49d02f5e42e8ab7474fee67e1c8d886c1c533ba121d07b88811b0d00d1cf82df90efbe32f8a557 + vite: ^5.0.0 || ^6.0.0 + checksum: 10c0/c5174926d66776697bfe8aa3013bfea62a48868c683784973b9b329c43b57a915685031047d397a9c0ae8dd1fd734bde37438af3939d395f9b82ada341b4fff7 languageName: node linkType: hard @@ -21491,29 +21479,34 @@ __metadata: languageName: node linkType: hard -"vite@npm:^5.0.0": - version: 5.4.10 - resolution: "vite@npm:5.4.10" +"vite@npm:^5.0.0 || ^6.0.0, vite@npm:^6.0.1": + version: 6.0.1 + resolution: "vite@npm:6.0.1" dependencies: - esbuild: "npm:^0.21.3" + esbuild: "npm:^0.24.0" fsevents: "npm:~2.3.3" - postcss: "npm:^8.4.43" - rollup: "npm:^4.20.0" + postcss: "npm:^8.4.49" + rollup: "npm:^4.23.0" peerDependencies: - "@types/node": ^18.0.0 || >=20.0.0 + "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: ">=1.21.0" less: "*" lightningcss: ^1.21.0 sass: "*" sass-embedded: "*" stylus: "*" sugarss: "*" - terser: ^5.4.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 dependenciesMeta: fsevents: optional: true peerDependenciesMeta: "@types/node": optional: true + jiti: + optional: true less: optional: true lightningcss: @@ -21528,66 +21521,27 @@ __metadata: optional: true terser: optional: true - bin: - vite: bin/vite.js - checksum: 10c0/4ef4807d2fd166a920de244dbcec791ba8a903b017a7d8e9f9b4ac40d23f8152c1100610583d08f542b47ca617a0505cfc5f8407377d610599d58296996691ed - languageName: node - linkType: hard - -"vite@npm:^5.4.11": - version: 5.4.11 - resolution: "vite@npm:5.4.11" - dependencies: - esbuild: "npm:^0.21.3" - fsevents: "npm:~2.3.3" - postcss: "npm:^8.4.43" - rollup: "npm:^4.20.0" - peerDependencies: - "@types/node": ^18.0.0 || >=20.0.0 - less: "*" - lightningcss: ^1.21.0 - sass: "*" - sass-embedded: "*" - stylus: "*" - sugarss: "*" - terser: ^5.4.0 - dependenciesMeta: - fsevents: + tsx: optional: true - peerDependenciesMeta: - "@types/node": - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: + yaml: optional: true bin: vite: bin/vite.js - checksum: 10c0/d536bb7af57dd0eca2a808f95f5ff1d7b7ffb8d86e17c6893087680a0448bd0d15e07475270c8a6de65cb5115592d037130a1dd979dc76bcef8c1dda202a1874 + checksum: 10c0/e4d853eb9042ff29fa4d7cee1484738faaee4b1d9dcf786a94783bebb736b39af0afa7ac1a209000530638098d0a1b240b51f509d32addb028b222453f862916 languageName: node linkType: hard -"vitest@npm:^2.1.5": - version: 2.1.5 - resolution: "vitest@npm:2.1.5" +"vitest@npm:^2.1.6": + version: 2.1.6 + resolution: "vitest@npm:2.1.6" dependencies: - "@vitest/expect": "npm:2.1.5" - "@vitest/mocker": "npm:2.1.5" - "@vitest/pretty-format": "npm:^2.1.5" - "@vitest/runner": "npm:2.1.5" - "@vitest/snapshot": "npm:2.1.5" - "@vitest/spy": "npm:2.1.5" - "@vitest/utils": "npm:2.1.5" + "@vitest/expect": "npm:2.1.6" + "@vitest/mocker": "npm:2.1.6" + "@vitest/pretty-format": "npm:^2.1.6" + "@vitest/runner": "npm:2.1.6" + "@vitest/snapshot": "npm:2.1.6" + "@vitest/spy": "npm:2.1.6" + "@vitest/utils": "npm:2.1.6" chai: "npm:^5.1.2" debug: "npm:^4.3.7" expect-type: "npm:^1.1.0" @@ -21598,14 +21552,14 @@ __metadata: tinyexec: "npm:^0.3.1" tinypool: "npm:^1.0.1" tinyrainbow: "npm:^1.2.0" - vite: "npm:^5.0.0" - vite-node: "npm:2.1.5" + vite: "npm:^5.0.0 || ^6.0.0" + vite-node: "npm:2.1.6" why-is-node-running: "npm:^2.3.0" peerDependencies: "@edge-runtime/vm": "*" - "@types/node": ^18.0.0 || >=20.0.0 - "@vitest/browser": 2.1.5 - "@vitest/ui": 2.1.5 + "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 + "@vitest/browser": 2.1.6 + "@vitest/ui": 2.1.6 happy-dom: "*" jsdom: "*" peerDependenciesMeta: @@ -21623,7 +21577,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10c0/1befb842da0826eed8761fe6cbd6ecae6b38d1ae83ac6619b994544d07e47905feaff2b254210315aa8e9b86645174c71a63b5d809799a289679a0063381c9a4 + checksum: 10c0/a183e4f573aacccf14d6466fdee0cee9ecd3f258bf98ebffa2da33c4d5aa1886d156f50d9eeb3d751c634679257e2196d8b3c9988256f2e860b9a1d7070bd4df languageName: node linkType: hard @@ -21714,7 +21668,7 @@ __metadata: eslint: "npm:^9.15.0" eslint-config-prettier: "npm:^9.1.0" eslint-plugin-mdx: "npm:^3.1.5" - prettier: "npm:^3.3.3" + prettier: "npm:^3.4.1" prettier-plugin-jsdoc: "npm:^1.3.0" prettier-plugin-organize-imports: "npm:^4.1.0" prism-react-renderer: "npm:^2.3.0" @@ -21728,7 +21682,7 @@ __metadata: remark-preset-lint-recommended: "npm:^7.0.0" remark-typescript-code-import: "npm:^1.0.1" typescript: "npm:^5.7.2" - typescript-eslint: "npm:^8.15.0" + typescript-eslint: "npm:^8.16.0" ua-parser-js: "npm:^2.0.0" languageName: unknown linkType: soft @@ -21745,14 +21699,14 @@ __metadata: "@observablehq/plot": "npm:^0.6.16" "@react-hook/resize-observer": "npm:^2.0.2" "@rollup/plugin-node-resolve": "npm:^15.3.0" - "@storybook/addon-essentials": "npm:^8.4.5" - "@storybook/addon-interactions": "npm:^8.4.5" - "@storybook/addon-links": "npm:^8.4.5" - "@storybook/blocks": "npm:^8.4.5" - "@storybook/react": "npm:^8.4.5" - "@storybook/react-vite": "npm:^8.4.5" - "@storybook/test": "npm:^8.4.5" - "@storybook/theming": "npm:^8.4.5" + "@storybook/addon-essentials": "npm:^8.4.6" + "@storybook/addon-interactions": "npm:^8.4.6" + "@storybook/addon-links": "npm:^8.4.6" + "@storybook/blocks": "npm:^8.4.6" + "@storybook/react": "npm:^8.4.6" + "@storybook/react-vite": "npm:^8.4.6" + "@storybook/test": "npm:^8.4.6" + "@storybook/theming": "npm:^8.4.6" "@table-nav/core": "npm:^0.0.7" "@table-nav/react": "npm:^0.0.7" "@tanstack/react-table": "npm:^8.20.5" @@ -21760,7 +21714,7 @@ __metadata: "@types/css-tree": "npm:^2" "@types/debug": "npm:^4" "@types/electron": "npm:^1.6.12" - "@types/node": "npm:^22.9.1" + "@types/node": "npm:^22.10.1" "@types/papaparse": "npm:^5" "@types/pngjs": "npm:^6.0.5" "@types/prop-types": "npm:^15" @@ -21773,8 +21727,8 @@ __metadata: "@types/tinycolor2": "npm:^1" "@types/uuid": "npm:^10.0.0" "@types/ws": "npm:^8" - "@vitejs/plugin-react-swc": "npm:^3.7.1" - "@vitest/coverage-istanbul": "npm:^2.1.5" + "@vitejs/plugin-react-swc": "npm:^3.7.2" + "@vitest/coverage-istanbul": "npm:^2.1.6" "@xterm/addon-fit": "npm:^0.10.0" "@xterm/addon-serialize": "npm:^0.13.0" "@xterm/addon-web-links": "npm:^0.11.0" @@ -21805,7 +21759,7 @@ __metadata: overlayscrollbars-react: "npm:^0.5.6" papaparse: "npm:^5.4.1" pngjs: "npm:^7.0.0" - prettier: "npm:^3.3.3" + prettier: "npm:^3.4.1" prettier-plugin-jsdoc: "npm:^1.3.0" prettier-plugin-organize-imports: "npm:^4.1.0" prop-types: "npm:^15.8.1" @@ -21830,7 +21784,7 @@ __metadata: sharp: "npm:^0.33.5" shell-quote: "npm:^1.8.1" sprintf-js: "npm:^1.1.3" - storybook: "npm:^8.4.5" + storybook: "npm:^8.4.6" storybook-dark-mode: "npm:^4.0.2" throttle-debounce: "npm:^5.0.2" tinycolor2: "npm:^1.6.0" @@ -21838,14 +21792,14 @@ __metadata: tslib: "npm:^2.8.1" tsx: "npm:^4.19.2" typescript: "npm:^5.7.2" - typescript-eslint: "npm:^8.15.0" + typescript-eslint: "npm:^8.16.0" use-device-pixel-ratio: "npm:^1.1.2" - vite: "npm:^5.4.11" + vite: "npm:^6.0.1" vite-plugin-image-optimizer: "npm:^1.1.8" - vite-plugin-static-copy: "npm:^2.1.0" + vite-plugin-static-copy: "npm:^2.2.0" vite-plugin-svgr: "npm:^4.3.0" vite-tsconfig-paths: "npm:^5.1.3" - vitest: "npm:^2.1.5" + vitest: "npm:^2.1.6" winston: "npm:^3.17.0" ws: "npm:^8.18.0" yaml: "npm:^2.6.1"