diff --git a/emain/emain-window.ts b/emain/emain-window.ts index 3261a2d6c..550b27b21 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -1,14 +1,21 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { ClientService, FileService, WindowService, WorkspaceService } from "@/app/store/services"; +import { ClientService, FileService, ObjectService, 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 { + getGlobalIsQuitting, + getGlobalIsRelaunching, + setGlobalIsRelaunching, + setWasActive, + setWasInFg, +} from "./emain-activity"; import { getOrCreateWebViewForTab, getWaveTabViewByWebContentsId, WaveTabView } from "./emain-tabview"; import { delay, ensureBoundsAreVisible } from "./emain-util"; +import { log } from "./log"; import { getElectronAppBasePath, unamePlatform } from "./platform"; import { updater } from "./updater"; export type WindowOpts = { @@ -272,6 +279,10 @@ export class WaveBrowserWindow extends BaseWindow { async switchWorkspace(workspaceId: string) { console.log("switchWorkspace", workspaceId, this.waveWindowId); + if (workspaceId == this.workspaceId) { + console.log("switchWorkspace already on this workspace", this.waveWindowId); + return; + } const curWorkspace = await WorkspaceService.GetWorkspace(this.workspaceId); if (curWorkspace.tabids.length > 1 && (!curWorkspace.name || !curWorkspace.icon)) { const choice = dialog.showMessageBoxSync(this, { @@ -603,19 +614,100 @@ ipcMain.on("close-tab", async (event, workspaceId, tabId) => { 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("switch-workspace", (event, workspaceId) => { + fireAndForget(async () => { + 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.destroy(); +export async function createWorkspace(window: WaveBrowserWindow) { + if (!window) { + return; } + const newWsId = await WorkspaceService.CreateWorkspace(); + if (newWsId) { + await window.switchWorkspace(newWsId); + } +} + +ipcMain.on("create-workspace", (event) => { + fireAndForget(async () => { + const ww = getWaveWindowByWebContentsId(event.sender.id); + console.log("create-workspace", ww?.waveWindowId); + await createWorkspace(ww); + }); }); + +ipcMain.on("delete-workspace", (event, workspaceId) => { + fireAndForget(async () => { + 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.destroy(); + } + }); +}); + +export async function createNewWaveWindow() { + log("createNewWaveWindow"); + const clientData = await ClientService.GetClientData(); + const fullConfig = await 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 ObjectService.GetObject("window:" + existingWindowId)) as WaveWindow; + if (existingWindowData != null) { + const win = await createBrowserWindow(existingWindowData, fullConfig, { unamePlatform }); + await win.waveReadyPromise; + win.show(); + recreatedWindow = true; + } + } + if (recreatedWindow) { + console.log("recreated window, returning"); + return; + } + console.log("creating new window"); + const newBrowserWindow = await createBrowserWindow(null, fullConfig, { unamePlatform }); + await newBrowserWindow.waveReadyPromise; + newBrowserWindow.show(); +} + +export async function relaunchBrowserWindows() { + console.log("relaunchBrowserWindows"); + setGlobalIsRelaunching(true); + const windows = getAllWaveWindows(); + for (const window of windows) { + console.log("relaunch -- closing window", window.waveWindowId); + window.close(); + } + setGlobalIsRelaunching(false); + + const clientData = await ClientService.GetClientData(); + const fullConfig = await FileService.GetFullConfig(); + const wins: WaveBrowserWindow[] = []; + for (const windowId of clientData.windowids.slice().reverse()) { + const windowData: WaveWindow = await WindowService.GetWindow(windowId); + if (windowData == null) { + console.log("relaunch -- window data not found, closing window", windowId); + await WindowService.CloseWindow(windowId, true); + continue; + } + console.log("relaunch -- creating window", windowId, windowData); + const win = await createBrowserWindow(windowData, fullConfig, { unamePlatform }); + wins.push(win); + } + for (const win of wins) { + await win.waveReadyPromise; + console.log("show window", win.waveWindowId); + win.show(); + } +} diff --git a/emain/emain-wsh.ts b/emain/emain-wsh.ts index b27d63f56..70e5cbf2a 100644 --- a/emain/emain-wsh.ts +++ b/emain/emain-wsh.ts @@ -55,6 +55,16 @@ export class ElectronWshClientType extends WshClient { } ww.focus(); } + + // async handle_workspaceupdate(rh: RpcResponseHelper) { + // console.log("workspaceupdate"); + // fireAndForget(async () => { + // console.log("workspace menu clicked"); + // const updatedWorkspaceMenu = await getWorkspaceMenu(); + // const workspaceMenu = Menu.getApplicationMenu().getMenuItemById("workspace-menu"); + // workspaceMenu.submenu = Menu.buildFromTemplate(updatedWorkspaceMenu); + // }); + // } } export let ElectronWshClient: ElectronWshClientType; diff --git a/emain/emain.ts b/emain/emain.ts index 733702637..660b03461 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -10,8 +10,6 @@ import * as path from "path"; import { PNG } from "pngjs"; import { sprintf } from "sprintf-js"; import { Readable } from "stream"; -import * as util from "util"; -import winston from "winston"; import * as services from "../frontend/app/store/services"; import { initElectronWshrpc, shutdownWshrpc } from "../frontend/app/store/wshrpcutil"; import { getWebServerEndpoint } from "../frontend/util/endpoints"; @@ -25,7 +23,6 @@ import { getGlobalIsRelaunching, setForceQuit, setGlobalIsQuitting, - setGlobalIsRelaunching, setGlobalIsStarting, setWasActive, setWasInFg, @@ -35,16 +32,19 @@ import { handleCtrlShiftState } from "./emain-util"; import { getIsWaveSrvDead, getWaveSrvProc, getWaveSrvReady, getWaveVersion, runWaveSrv } from "./emain-wavesrv"; import { createBrowserWindow, + createNewWaveWindow, focusedWaveWindow, getAllWaveWindows, getWaveWindowById, getWaveWindowByWebContentsId, getWaveWindowByWorkspaceId, + relaunchBrowserWindows, WaveBrowserWindow, } from "./emain-window"; import { ElectronWshClient, initElectronWshClient } from "./emain-wsh"; import { getLaunchSettings } from "./launchsettings"; -import { getAppMenu } from "./menu"; +import { log } from "./log"; +import { instantiateAppMenu, makeAppMenu } from "./menu"; import { getElectronAppBasePath, getElectronAppUnpackedBasePath, @@ -65,30 +65,7 @@ electron.nativeTheme.themeSource = "dark"; let webviewFocusId: number = null; // set to the getWebContentsId of the webview that has focus (null if not focused) let webviewKeys: string[] = []; // the keys to trap when webview has focus -const oldConsoleLog = console.log; -const loggerTransports: winston.transport[] = [ - new winston.transports.File({ filename: path.join(waveDataDir, "waveapp.log"), level: "info" }), -]; -if (isDev) { - loggerTransports.push(new winston.transports.Console()); -} -const loggerConfig = { - level: "info", - format: winston.format.combine( - winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }), - winston.format.printf((info) => `${info.timestamp} ${info.message}`) - ), - transports: loggerTransports, -}; -const logger = winston.createLogger(loggerConfig); -function log(...msg: any[]) { - try { - logger.info(util.format(...msg)); - } catch (e) { - oldConsoleLog(...msg); - } -} console.log = log; console.log( sprintf( @@ -375,34 +352,6 @@ 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 = await createBrowserWindow(existingWindowData, fullConfig, { unamePlatform }); - await win.waveReadyPromise; - win.show(); - recreatedWindow = true; - } - } - if (recreatedWindow) { - console.log("recreated window, returning"); - return; - } - console.log("creating new window"); - const newBrowserWindow = await createBrowserWindow(null, fullConfig, { unamePlatform }); - await newBrowserWindow.waveReadyPromise; - newBrowserWindow.show(); -} - electron.ipcMain.on("set-window-init-status", (event, status: "ready" | "wave-ready") => { const tabView = getWaveTabViewByWebContentsId(event.sender.id); if (tabView == null || tabView.initResolve == null) { @@ -481,10 +430,10 @@ electron.ipcMain.on("contextmenu-show", (event, menuDefArr?: ElectronContextMenu if (menuDefArr?.length === 0) { return; } - const menu = menuDefArr ? convertMenuDefArrToMenu(menuDefArr) : instantiateAppMenu(); - // const { x, y } = electron.screen.getCursorScreenPoint(); - // const windowPos = window.getPosition(); - menu.popup(); + fireAndForget(async () => { + const menu = menuDefArr ? convertMenuDefArrToMenu(menuDefArr) : await instantiateAppMenu(); + menu.popup(); + }); event.returnValue = true; }); @@ -561,18 +510,6 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro return electron.Menu.buildFromTemplate(menuItems); } -function instantiateAppMenu(): electron.Menu { - return getAppMenu({ - createNewWaveWindow, - relaunchBrowserWindows, - }); -} - -function makeAppMenu() { - const menu = instantiateAppMenu(); - electron.Menu.setApplicationMenu(menu); -} - function hideWindowWithCatch(window: WaveBrowserWindow) { if (window == null) { return; @@ -649,37 +586,6 @@ process.on("uncaughtException", (error) => { electronApp.quit(); }); -async function relaunchBrowserWindows(): Promise { - console.log("relaunchBrowserWindows"); - setGlobalIsRelaunching(true); - const windows = getAllWaveWindows(); - for (const window of windows) { - console.log("relaunch -- closing window", window.waveWindowId); - window.close(); - } - setGlobalIsRelaunching(false); - - const clientData = await services.ClientService.GetClientData(); - const fullConfig = await services.FileService.GetFullConfig(); - const wins: WaveBrowserWindow[] = []; - for (const windowId of clientData.windowids.slice().reverse()) { - const windowData: WaveWindow = await services.WindowService.GetWindow(windowId); - if (windowData == null) { - console.log("relaunch -- window data not found, closing window", windowId); - await services.WindowService.CloseWindow(windowId, true); - continue; - } - console.log("relaunch -- creating window", windowId, windowData); - const win = await createBrowserWindow(windowData, fullConfig, { unamePlatform }); - wins.push(win); - } - for (const win of wins) { - await win.waveReadyPromise; - console.log("show window", win.waveWindowId); - win.show(); - } -} - async function appMain() { // Set disableHardwareAcceleration as early as possible, if required. const launchSettings = getLaunchSettings(); @@ -694,7 +600,6 @@ async function appMain() { electronApp.quit(); return; } - makeAppMenu(); try { await runWaveSrv(handleWSEvent); } catch (e) { @@ -715,6 +620,7 @@ async function appMain() { } catch (e) { console.log("error initializing wshrpc", e); } + makeAppMenu(); await configureAutoUpdater(); setGlobalIsStarting(false); if (fullConfig?.settings?.["window:maxtabcachesize"] != null) { diff --git a/emain/log.ts b/emain/log.ts new file mode 100644 index 000000000..bba5e9b88 --- /dev/null +++ b/emain/log.ts @@ -0,0 +1,31 @@ +import path from "path"; +import { format } from "util"; +import winston from "winston"; +import { getWaveDataDir, isDev } from "./platform"; + +const oldConsoleLog = console.log; + +const loggerTransports: winston.transport[] = [ + new winston.transports.File({ filename: path.join(getWaveDataDir(), "waveapp.log"), level: "info" }), +]; +if (isDev) { + loggerTransports.push(new winston.transports.Console()); +} +const loggerConfig = { + level: "info", + format: winston.format.combine( + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }), + winston.format.printf((info) => `${info.timestamp} ${info.message}`) + ), + transports: loggerTransports, +}; +const logger = winston.createLogger(loggerConfig); +function log(...msg: any[]) { + try { + logger.info(format(...msg)); + } catch (e) { + oldConsoleLog(...msg); + } +} + +export { log }; diff --git a/emain/menu.ts b/emain/menu.ts index bc55424d1..a97e98fa7 100644 --- a/emain/menu.ts +++ b/emain/menu.ts @@ -1,10 +1,19 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { waveEventSubscribe } from "@/app/store/wps"; +import { RpcApi } from "@/app/store/wshclientapi"; import * as electron from "electron"; import { fireAndForget } from "../frontend/util/util"; import { clearTabCache } from "./emain-tabview"; -import { focusedWaveWindow, WaveBrowserWindow } from "./emain-window"; +import { + createNewWaveWindow, + createWorkspace, + focusedWaveWindow, + relaunchBrowserWindows, + WaveBrowserWindow, +} from "./emain-window"; +import { ElectronWshClient } from "./emain-wsh"; import { unamePlatform } from "./platform"; import { updater } from "./updater"; @@ -27,7 +36,35 @@ function getWindowWebContents(window: electron.BaseWindow): electron.WebContents return null; } -function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu { +async function getWorkspaceMenu(): Promise { + const workspaceList = await RpcApi.WorkspaceListCommand(ElectronWshClient); + console.log("workspaceList:", workspaceList); + const workspaceMenu: Electron.MenuItemConstructorOptions[] = [ + { + label: "Create New Workspace", + click: (_, window) => { + const ww = window as WaveBrowserWindow; + fireAndForget(() => createWorkspace(ww)); + }, + }, + ]; + workspaceList?.length && + workspaceMenu.push( + { type: "separator" }, + ...workspaceList.map((workspace) => { + return { + label: `Switch to ${workspace.workspacedata.name} (${workspace.workspacedata.oid.slice(0, 5)})`, + click: (_, window) => { + const ww = window as WaveBrowserWindow; + ww.switchWorkspace(workspace.workspacedata.oid); + }, + }; + }) + ); + return workspaceMenu; +} + +async function getAppMenu(callbacks: AppMenuCallbacks): Promise { const fileMenu: Electron.MenuItemConstructorOptions[] = [ { label: "New Window", @@ -224,6 +261,9 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu { role: "togglefullscreen", }, ]; + + const workspaceMenu = await getWorkspaceMenu(); + const windowMenu: Electron.MenuItemConstructorOptions[] = [ { role: "minimize", accelerator: "" }, { role: "zoom" }, @@ -249,6 +289,11 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu { role: "viewMenu", submenu: viewMenu, }, + { + label: "Workspace", + id: "workspace-menu", + submenu: workspaceMenu, + }, { role: "windowMenu", submenu: windowMenu, @@ -257,4 +302,23 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu { return electron.Menu.buildFromTemplate(menuTemplate); } +export function instantiateAppMenu(): Promise { + return getAppMenu({ + createNewWaveWindow, + relaunchBrowserWindows, + }); +} + +export function makeAppMenu() { + fireAndForget(async () => { + const menu = await instantiateAppMenu(); + electron.Menu.setApplicationMenu(menu); + }); +} + +waveEventSubscribe({ + eventType: "workspace:update", + handler: makeAppMenu, +}); + export { getAppMenu }; diff --git a/emain/preload.ts b/emain/preload.ts index 1636eda94..b9048ba08 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -40,6 +40,7 @@ contextBridge.exposeInMainWorld("api", { registerGlobalWebviewKeys: (keys) => ipcRenderer.send("register-global-webview-keys", keys), onControlShiftStateUpdate: (callback) => ipcRenderer.on("control-shift-state-update", (_event, state) => callback(state)), + createWorkspace: () => ipcRenderer.send("create-workspace"), switchWorkspace: (workspaceId) => ipcRenderer.send("switch-workspace", workspaceId), deleteWorkspace: (workspaceId) => ipcRenderer.send("delete-workspace", workspaceId), setActiveTab: (tabId) => ipcRenderer.send("set-active-tab", tabId), diff --git a/frontend/app/store/services.ts b/frontend/app/store/services.ts index fab70a66d..8884917a4 100644 --- a/frontend/app/store/services.ts +++ b/frontend/app/store/services.ts @@ -183,6 +183,11 @@ class WorkspaceServiceType { return WOS.callBackendService("workspace", "CreateTab", Array.from(arguments)) } + // @returns workspaceId + CreateWorkspace(): Promise { + return WOS.callBackendService("workspace", "CreateWorkspace", Array.from(arguments)) + } + // @returns object updates DeleteWorkspace(workspaceId: string): Promise { return WOS.callBackendService("workspace", "DeleteWorkspace", Array.from(arguments)) diff --git a/frontend/app/tab/workspaceswitcher.tsx b/frontend/app/tab/workspaceswitcher.tsx index fc33615e2..7f322c590 100644 --- a/frontend/app/tab/workspaceswitcher.tsx +++ b/frontend/app/tab/workspaceswitcher.tsx @@ -235,16 +235,23 @@ const WorkspaceSwitcher = () => { - {!isActiveWorkspaceSaved && ( -
+
+ {isActiveWorkspaceSaved ? ( + getApi().createWorkspace()}> + + + +
Create new workspace
+
+ ) : ( saveWorkspace()}>
Save workspace
-
- )} + )} +
); diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 6c8053bf1..f2c9fc9cf 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -89,6 +89,7 @@ declare global { setWebviewFocus: (focusedId: number) => void; // focusedId si the getWebContentsId of the webview registerGlobalWebviewKeys: (keys: string[]) => void; onControlShiftStateUpdate: (callback: (state: boolean) => void) => void; + createWorkspace: () => void; switchWorkspace: (workspaceId: string) => void; deleteWorkspace: (workspaceId: string) => void; setActiveTab: (tabId: string) => void; diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go index 40baca645..b489ad664 100644 --- a/pkg/service/objectservice/objectservice.go +++ b/pkg/service/objectservice/objectservice.go @@ -12,6 +12,7 @@ import ( "github.com/wavetermdev/waveterm/pkg/tsgen/tsgenmeta" "github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/wcore" + "github.com/wavetermdev/waveterm/pkg/wps" "github.com/wavetermdev/waveterm/pkg/wstore" ) @@ -174,6 +175,10 @@ func (svc *ObjectService) UpdateObject(uiContext waveobj.UIContext, waveObj wave if err != nil { return nil, fmt.Errorf("error updating object: %w", err) } + if (waveObj.GetOType() == waveobj.OType_Workspace) && (waveObj.(*waveobj.Workspace).Name != "") { + wps.Broker.Publish(wps.WaveEvent{ + Event: wps.Event_WorkspaceUpdate}) + } if returnUpdates { return waveobj.ContextGetUpdatesRtn(ctx), nil } diff --git a/pkg/service/windowservice/windowservice.go b/pkg/service/windowservice/windowservice.go index 2c4e62bcf..e4f0bb31d 100644 --- a/pkg/service/windowservice/windowservice.go +++ b/pkg/service/windowservice/windowservice.go @@ -50,23 +50,14 @@ func (svc *WindowService) CreateWindow(ctx context.Context, winSize *waveobj.Win 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) + return window, fmt.Errorf("error getting workspace: %w", err) } - if len(ws.TabIds) == 0 { - _, err = wcore.CreateTab(ctx, ws.OID, "", true, false) - 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) - } + err = wlayout.BootstrapNewWorkspaceLayout(ctx, ws) + if err != nil { + return window, fmt.Errorf("error bootstrapping new workspace layout: %w", err) } return window, nil } diff --git a/pkg/service/workspaceservice/workspaceservice.go b/pkg/service/workspaceservice/workspaceservice.go index 2c6dfb92d..61272f3b0 100644 --- a/pkg/service/workspaceservice/workspaceservice.go +++ b/pkg/service/workspaceservice/workspaceservice.go @@ -20,6 +20,25 @@ const DefaultTimeout = 2 * time.Second type WorkspaceService struct{} +func (svc *WorkspaceService) CreateWorkspace_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ReturnDesc: "workspaceId", + } +} + +func (svc *WorkspaceService) CreateWorkspace(ctx context.Context) (string, error) { + newWS, err := wcore.CreateWorkspace(ctx, "", "", "") + if err != nil { + return "", fmt.Errorf("error creating workspace: %w", err) + } + + err = wlayout.BootstrapNewWorkspaceLayout(ctx, newWS) + if err != nil { + return newWS.OID, fmt.Errorf("error bootstrapping new workspace layout: %w", err) + } + return newWS.OID, nil +} + func (svc *WorkspaceService) GetWorkspace_Meta() tsgenmeta.MethodMeta { return tsgenmeta.MethodMeta{ ArgNames: []string{"workspaceId"}, diff --git a/pkg/wcore/block.go b/pkg/wcore/block.go index 118a582bf..75c4b6d18 100644 --- a/pkg/wcore/block.go +++ b/pkg/wcore/block.go @@ -152,21 +152,18 @@ func DeleteBlock(ctx context.Context, blockId string, recursive bool) error { log.Printf("DeleteBlock: parentBlockCount: %d", parentBlockCount) parentORef := waveobj.ParseORefNoErr(block.ParentORef) - if parentORef.OType == waveobj.OType_Tab { - if parentBlockCount == 0 && recursive { - // if parent tab has no blocks, delete the tab - log.Printf("DeleteBlock: parent tab has no blocks, deleting tab %s", parentORef.OID) - parentWorkspaceId, err := wstore.DBFindWorkspaceForTabId(ctx, parentORef.OID) - if err != nil { - return fmt.Errorf("error finding workspace for tab to delete %s: %w", parentORef.OID, err) - } - newActiveTabId, err := DeleteTab(ctx, parentWorkspaceId, parentORef.OID, true) - if err != nil { - return fmt.Errorf("error deleting tab %s: %w", parentORef.OID, err) - } - SendActiveTabUpdate(ctx, parentWorkspaceId, newActiveTabId) + if recursive && parentORef.OType == waveobj.OType_Tab && parentBlockCount == 0 { + // if parent tab has no blocks, delete the tab + log.Printf("DeleteBlock: parent tab has no blocks, deleting tab %s", parentORef.OID) + parentWorkspaceId, err := wstore.DBFindWorkspaceForTabId(ctx, parentORef.OID) + if err != nil { + return fmt.Errorf("error finding workspace for tab to delete %s: %w", parentORef.OID, err) } - + newActiveTabId, err := DeleteTab(ctx, parentWorkspaceId, parentORef.OID, true) + if err != nil { + return fmt.Errorf("error deleting tab %s: %w", parentORef.OID, err) + } + SendActiveTabUpdate(ctx, parentWorkspaceId, newActiveTabId) } go blockcontroller.StopBlockController(blockId) sendBlockCloseEvent(blockId) diff --git a/pkg/wcore/workspace.go b/pkg/wcore/workspace.go index 4d0b6e8d7..b07f58bb9 100644 --- a/pkg/wcore/workspace.go +++ b/pkg/wcore/workspace.go @@ -11,6 +11,7 @@ import ( "github.com/wavetermdev/waveterm/pkg/telemetry" "github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wps" "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wstore" ) @@ -25,7 +26,21 @@ func CreateWorkspace(ctx context.Context, name string, icon string, color string Icon: icon, Color: color, } - wstore.DBInsert(ctx, ws) + err := wstore.DBInsert(ctx, ws) + if err != nil { + return nil, fmt.Errorf("error inserting workspace: %w", err) + } + + _, err = CreateTab(ctx, ws.OID, "", true, false) + if err != nil { + return nil, fmt.Errorf("error creating tab: %w", err) + } + ws, err = GetWorkspace(ctx, ws.OID) + if err != nil { + return nil, fmt.Errorf("error getting updated workspace: %w", err) + } + wps.Broker.Publish(wps.WaveEvent{ + Event: wps.Event_WorkspaceUpdate}) return ws, nil } @@ -38,7 +53,7 @@ func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool, if err != nil { return false, fmt.Errorf("error getting workspace: %w", err) } - if workspace.Name != "" && workspace.Icon != "" && !force && len(workspace.TabIds) > 0 && len(workspace.PinnedTabIds) > 0 { + if workspace.Name != "" && workspace.Icon != "" && !force && (len(workspace.TabIds) > 0 || len(workspace.PinnedTabIds) > 0) { log.Printf("Ignoring DeleteWorkspace for workspace %s as it is named\n", workspaceId) return false, nil } @@ -56,6 +71,8 @@ func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool, return false, fmt.Errorf("error deleting workspace: %w", err) } log.Printf("deleted workspace %s\n", workspaceId) + wps.Broker.Publish(wps.WaveEvent{ + Event: wps.Event_WorkspaceUpdate}) return true, nil } @@ -163,7 +180,7 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string, recursive wstore.DBDelete(ctx, waveobj.OType_LayoutState, tab.LayoutState) // if no tabs remaining, close window - if newActiveTabId == "" && recursive { + if recursive && newActiveTabId == "" { log.Printf("no tabs remaining in workspace %s, closing window\n", workspaceId) windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId) if err != nil { diff --git a/pkg/wps/wpstypes.go b/pkg/wps/wpstypes.go index e3bcdd45f..1a21046aa 100644 --- a/pkg/wps/wpstypes.go +++ b/pkg/wps/wpstypes.go @@ -12,6 +12,7 @@ const ( Event_Config = "config" Event_UserInput = "userinput" Event_RouteGone = "route:gone" + Event_WorkspaceUpdate = "workspace:update" ) type WaveEvent struct {