diff --git a/emain/emain.ts b/emain/emain.ts index cd5ac4b85..e1e3473a1 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -21,12 +21,14 @@ const AuthKeyFile = "waveterm.authkey"; const DevServerEndpoint = "http://127.0.0.1:8190"; const ProdServerEndpoint = "http://127.0.0.1:1719"; -type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string }; +type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string; readyPromise: Promise }; let waveSrvReadyResolve = (value: boolean) => {}; let waveSrvReady: Promise = new Promise((resolve, _) => { waveSrvReadyResolve = resolve; }); +let globalIsQuitting = false; +let globalIsStarting = true; let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null; electronApp.setName(isDev ? "NextWave (Dev)" : "NextWave"); @@ -74,6 +76,11 @@ function getWaveSrvCwd(): string { return getWaveHomeDir(); } +function getWindowForEvent(event: Electron.IpcMainEvent): Electron.BrowserWindow { + const windowId = event.sender.id; + return electron.BrowserWindow.fromId(windowId); +} + function runWaveSrv(): Promise { let pResolve: (value: boolean) => void; let pReject: (reason?: any) => void; @@ -99,6 +106,9 @@ function runWaveSrv(): Promise { env: envCopy, }); proc.on("exit", (e) => { + if (globalIsQuitting) { + return; + } console.log("wavesrv exited, shutting down"); electronApp.quit(); }); @@ -189,31 +199,21 @@ function shFrameNavHandler(event: Electron.Event primaryDisplay.workAreaSize.height) { - winHeight = primaryDisplay.workAreaSize.height; - } - if (winWidth > primaryDisplay.workAreaSize.width) { - winWidth = primaryDisplay.workAreaSize.width; - } - let winX = waveWindow.pos.x; - let winY = waveWindow.pos.y; - if (winX + winWidth > primaryDisplay.workAreaSize.width) { - winX = Math.floor((primaryDisplay.workAreaSize.width - winWidth) / 2); - } - if (winY + winHeight > primaryDisplay.workAreaSize.height) { - winY = Math.floor((primaryDisplay.workAreaSize.height - winHeight) / 2); - } +function createBrowserWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow { + let winBounds = { + x: waveWindow.pos.x, + y: waveWindow.pos.y, + width: waveWindow.winsize.width, + height: waveWindow.winsize.height, + }; + winBounds = ensureBoundsAreVisible(winBounds); const bwin = new electron.BrowserWindow({ - x: winX, - y: winY, titleBarStyle: "hiddenInset", - width: winWidth, - height: winHeight, - minWidth: 500, + x: winBounds.x, + y: winBounds.y, + width: winBounds.width, + height: winBounds.height, + minWidth: 400, minHeight: 300, icon: unamePlatform == "linux" @@ -227,10 +227,11 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow backgroundColor: "#000000", }); (bwin as any).waveWindowId = waveWindow.oid; - const win: WaveBrowserWindow = bwin as WaveBrowserWindow; - win.once("ready-to-show", () => { - win.show(); + let readyResolve: (value: void) => void; + (bwin as any).readyPromise = new Promise((resolve, _) => { + readyResolve = resolve; }); + const win: WaveBrowserWindow = bwin as WaveBrowserWindow; // const indexHtml = isDev ? "index-dev.html" : "index.html"; let usp = new URLSearchParams(); usp.set("clientid", client.oid); @@ -243,7 +244,9 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow console.log("running as file"); win.loadFile(path.join(getElectronAppBasePath(), "frontend", indexHtml), { search: usp.toString() }); } - + win.once("ready-to-show", () => { + readyResolve(); + }); win.webContents.on("will-navigate", shNavHandler); win.webContents.on("will-frame-navigate", shFrameNavHandler); win.on( @@ -254,6 +257,33 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow "move", debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win)) ); + win.on("focus", () => { + if (globalIsStarting) { + return; + } + console.log("focus", waveWindow.oid); + services.ClientService.FocusWindow(waveWindow.oid); + }); + win.on("close", (e) => { + if (globalIsQuitting) { + 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(); + } + }); + win.on("closed", () => { + if (globalIsQuitting) { + return; + } + services.WindowService.CloseWindow(waveWindow.oid); + }); win.webContents.on("zoom-changed", (e) => { win.webContents.send("zoom-changed"); }); @@ -268,6 +298,86 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow return win; } +function isWindowFullyVisible(bounds: electron.Rectangle): boolean { + const displays = electron.screen.getAllDisplays(); + + // Helper function to check if a point is inside any display + function isPointInDisplay(x, y) { + for (let display of displays) { + const { x: dx, y: dy, width, height } = display.bounds; + if (x >= dx && x < dx + width && y >= dy && y < dy + height) { + return true; + } + } + return false; + } + + // Check all corners of the window + const topLeft = isPointInDisplay(bounds.x, bounds.y); + const topRight = isPointInDisplay(bounds.x + bounds.width, bounds.y); + const bottomLeft = isPointInDisplay(bounds.x, bounds.y + bounds.height); + const bottomRight = isPointInDisplay(bounds.x + bounds.width, bounds.y + bounds.height); + + return topLeft && topRight && bottomLeft && bottomRight; +} + +function findDisplayWithMostArea(bounds: electron.Rectangle): electron.Display { + const displays = electron.screen.getAllDisplays(); + let maxArea = 0; + let bestDisplay = null; + + for (let display of displays) { + const { x, y, width, height } = display.bounds; + const overlapX = Math.max(0, Math.min(bounds.x + bounds.width, x + width) - Math.max(bounds.x, x)); + const overlapY = Math.max(0, Math.min(bounds.y + bounds.height, y + height) - Math.max(bounds.y, y)); + const overlapArea = overlapX * overlapY; + + if (overlapArea > maxArea) { + maxArea = overlapArea; + bestDisplay = display; + } + } + + return bestDisplay; +} + +function adjustBoundsToFitDisplay(bounds: electron.Rectangle, display: electron.Display): electron.Rectangle { + const { x: dx, y: dy, width: dWidth, height: dHeight } = display.workArea; + let { x, y, width, height } = bounds; + + // Adjust width and height to fit within the display's work area + width = Math.min(width, dWidth); + height = Math.min(height, dHeight); + + // Adjust x to ensure the window fits within the display + if (x < dx) { + x = dx; + } else if (x + width > dx + dWidth) { + x = dx + dWidth - width; + } + + // Adjust y to ensure the window fits within the display + if (y < dy) { + y = dy; + } else if (y + height > dy + dHeight) { + y = dy + dHeight - height; + } + return { x, y, width, height }; +} + +function ensureBoundsAreVisible(bounds: electron.Rectangle): electron.Rectangle { + if (!isWindowFullyVisible(bounds)) { + let targetDisplay = findDisplayWithMostArea(bounds); + + if (!targetDisplay) { + targetDisplay = electron.screen.getPrimaryDisplay(); + } + + return adjustBoundsToFitDisplay(bounds, targetDisplay); + } + return bounds; +} + electron.ipcMain.on("isDev", (event) => { event.returnValue = isDev; }); @@ -287,7 +397,13 @@ electron.ipcMain.on("getCursorPoint", (event) => { event.returnValue = retVal; }); -electron.ipcMain.on("openNewWindow", (event) => {}); +async function createNewWaveWindow() { + let clientData = await services.ClientService.GetClientData(); + const newWindow = await services.ClientService.MakeWindow(); + createBrowserWindow(clientData, newWindow); +} + +electron.ipcMain.on("openNewWindow", createNewWaveWindow); electron.ipcMain.on("context-editmenu", (_, { x, y }, opts) => { if (opts == null) { @@ -337,7 +453,45 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro return electron.Menu.buildFromTemplate(menuItems); } -(async () => { +function makeAppMenu() { + let fileMenu: Electron.MenuItemConstructorOptions[] = []; + fileMenu.push({ + label: "New Window", + click: createNewWaveWindow, + }); + fileMenu.push({ + label: "Close Window", + click: () => { + electron.BrowserWindow.getFocusedWindow()?.close(); + }, + }); + const menuTemplate: Electron.MenuItemConstructorOptions[] = [ + { + role: "appMenu", + }, + { + role: "fileMenu", + submenu: fileMenu, + }, + { + role: "editMenu", + }, + { + role: "viewMenu", + }, + { + role: "windowMenu", + }, + ]; + const menu = electron.Menu.buildFromTemplate(menuTemplate); + electron.Menu.setApplicationMenu(menu); +} + +electron.app.on("before-quit", () => { + globalIsQuitting = true; +}); + +async function appMain() { const startTs = Date.now(); const instanceLock = electronApp.requestSingleInstanceLock(); if (!instanceLock) { @@ -349,6 +503,7 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro if (!fs.existsSync(waveHomeDir)) { fs.mkdirSync(waveHomeDir); } + makeAppMenu(); try { await runWaveSrv(); } catch (e) { @@ -358,17 +513,30 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms"); console.log("get client data"); - let clientData = (await services.ClientService.GetClientData().catch((e) => console.log(e))) as Client; + let clientData = await services.ClientService.GetClientData(); console.log("client data ready"); - let windowData: WaveWindow = (await services.ObjectService.GetObject( - "window:" + clientData.mainwindowid - )) as WaveWindow; await electronApp.whenReady(); - createWindow(clientData, windowData); + let wins: WaveBrowserWindow[] = []; + for (let windowId of clientData.windowids.slice().reverse()) { + let windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow; + const win = createBrowserWindow(clientData, windowData); + wins.push(win); + } + for (let win of wins) { + await win.readyPromise; + console.log("show", win.waveWindowId); + win.show(); + } + globalIsStarting = false; electronApp.on("activate", () => { if (electron.BrowserWindow.getAllWindows().length === 0) { - createWindow(clientData, windowData); + createNewWaveWindow(); } }); -})(); +} + +appMain().catch((e) => { + console.log("appMain error", e); + electronApp.quit(); +}); diff --git a/frontend/app/block/block.less b/frontend/app/block/block.less index e3808e975..bfcc97519 100644 --- a/frontend/app/block/block.less +++ b/frontend/app/block/block.less @@ -60,6 +60,10 @@ &.block-preview { background-color: var(--main-bg-color); + + .block-frame-tech-close { + display: none; + } } &.block-focused { @@ -68,10 +72,6 @@ .block-frame-tech-header { color: var(--main-text-color); } - - .block-frame-tech-close { - display: none; - } } .block-frame-tech-header { diff --git a/frontend/app/store/services.ts b/frontend/app/store/services.ts index df5de6455..91ab53c52 100644 --- a/frontend/app/store/services.ts +++ b/frontend/app/store/services.ts @@ -21,18 +21,24 @@ export const BlockService = new BlockServiceType() // clientservice.ClientService (client) class ClientServiceType { + FocusWindow(arg2: string): Promise { + return WOS.callBackendService("client", "FocusWindow", Array.from(arguments)) + } GetClientData(): Promise { return WOS.callBackendService("client", "GetClientData", Array.from(arguments)) } GetTab(arg1: string): Promise { return WOS.callBackendService("client", "GetTab", Array.from(arguments)) } - GetWindow(arg1: string): Promise { + 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)) + } } export const ClientService = new ClientServiceType() @@ -59,11 +65,6 @@ class ObjectServiceType { return WOS.callBackendService("object", "AddTabToWorkspace", Array.from(arguments)) } - // @returns object updates - CloseTab(tabId: string): Promise { - return WOS.callBackendService("object", "CloseTab", Array.from(arguments)) - } - // @returns blockId (and object updates) CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise { return WOS.callBackendService("object", "CreateBlock", Array.from(arguments)) @@ -109,6 +110,14 @@ export const ObjectService = new ObjectServiceType() // windowservice.WindowService (window) class WindowServiceType { + // @returns object updates + CloseTab(arg3: string): Promise { + return WOS.callBackendService("window", "CloseTab", Array.from(arguments)) + } + CloseWindow(arg2: string): Promise { + return WOS.callBackendService("window", "CloseWindow", Array.from(arguments)) + } + // @returns object updates SetWindowPosAndSize(arg2: string, arg3: Point, arg4: WinSize): Promise { return WOS.callBackendService("window", "SetWindowPosAndSize", Array.from(arguments)) diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index 8ead33ec2..8b0ce000f 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -313,7 +313,7 @@ const TabBar = ({ workspace }: TabBarProps) => { }; const handleCloseTab = (tabId: string) => { - services.ObjectService.CloseTab(tabId); + services.WindowService.CloseTab(tabId); deleteLayoutStateAtomForTab(tabId); }; diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index de676f35a..d5d6f4653 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -31,7 +31,7 @@ declare global { type BlockCommand = { command: string; - } & ( BlockAppendIJsonCommand | ResolveIdsCommand | BlockInputCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockAppendFileCommand ); + } & ( BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockAppendFileCommand | BlockAppendIJsonCommand | ResolveIdsCommand | BlockInputCommand | BlockSetViewCommand ); // wstore.BlockDef type BlockDef = { @@ -84,6 +84,7 @@ declare global { // wstore.Client type Client = WaveObj & { mainwindowid: string; + windowids: string[]; meta: MetaType; }; @@ -253,6 +254,18 @@ declare global { obj?: WaveObj; }; + // wstore.Window + type WaveWindow = WaveObj & { + workspaceid: string; + activetabid: string; + activeblockid?: string; + activeblockmap: {[key: string]: string}; + pos: Point; + winsize: WinSize; + lastfocusts: number; + meta: MetaType; + }; + // service.WebCallType type WebCallType = { service: string; @@ -275,18 +288,6 @@ declare global { height: number; }; - // wstore.Window - type WaveWindow = WaveObj & { - workspaceid: string; - activetabid: string; - activeblockid?: string; - activeblockmap: {[key: string]: string}; - pos: Point; - winsize: WinSize; - lastfocusts: number; - meta: MetaType; - }; - // wstore.Workspace type Workspace = WaveObj & { name: string; diff --git a/frontend/wave.ts b/frontend/wave.ts index 32b687389..94758e2ed 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -27,6 +27,8 @@ function matchViewportSize() { document.body.style.height = window.visualViewport.height + "px"; } +document.title = `The Next Wave (${windowId.substring(0, 8)})`; + matchViewportSize(); document.addEventListener("DOMContentLoaded", async () => { diff --git a/pkg/service/clientservice/clientservice.go b/pkg/service/clientservice/clientservice.go index 5d0a0c6db..252fc1db5 100644 --- a/pkg/service/clientservice/clientservice.go +++ b/pkg/service/clientservice/clientservice.go @@ -8,6 +8,7 @@ import ( "fmt" "time" + "github.com/wavetermdev/thenextwave/pkg/util/utilfn" "github.com/wavetermdev/thenextwave/pkg/wstore" ) @@ -54,3 +55,21 @@ func (cs *ClientService) GetWindow(windowId string) (*wstore.Window, error) { } return window, nil } + +func (cs *ClientService) MakeWindow(ctx context.Context) (*wstore.Window, error) { + return wstore.CreateWindow(ctx) +} + +// 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) +} diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go index b05859f63..e682ed75b 100644 --- a/pkg/service/objectservice/objectservice.go +++ b/pkg/service/objectservice/objectservice.go @@ -205,41 +205,6 @@ func (svc *ObjectService) CloseTab_Meta() tsgenmeta.MethodMeta { } } -func (svc *ObjectService) CloseTab(uiContext wstore.UIContext, tabId string) (wstore.UpdatesRtnType, error) { - ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) - defer cancelFn() - ctx = wstore.ContextWithUpdates(ctx) - window, err := wstore.DBMustGet[*wstore.Window](ctx, uiContext.WindowId) - if err != nil { - return nil, fmt.Errorf("error getting window: %w", err) - } - tab, err := wstore.DBMustGet[*wstore.Tab](ctx, tabId) - if err != nil { - return nil, fmt.Errorf("error getting tab: %w", err) - } - for _, blockId := range tab.BlockIds { - blockcontroller.StopBlockController(blockId) - } - err = wstore.CloseTab(ctx, window.WorkspaceId, tabId) - if err != nil { - return nil, fmt.Errorf("error closing tab: %w", err) - } - if window.ActiveTabId == tabId { - ws, err := wstore.DBMustGet[*wstore.Workspace](ctx, window.WorkspaceId) - if err != nil { - return nil, fmt.Errorf("error getting workspace: %w", err) - } - var newActiveTabId string - if len(ws.TabIds) > 0 { - newActiveTabId = ws.TabIds[0] - } else { - newActiveTabId = "" - } - wstore.SetActiveTab(ctx, uiContext.WindowId, newActiveTabId) - } - return wstore.ContextGetUpdatesRtn(ctx), nil -} - func (svc *ObjectService) UpdateObjectMeta_Meta() tsgenmeta.MethodMeta { return tsgenmeta.MethodMeta{ ArgNames: []string{"uiContext", "oref", "meta"}, diff --git a/pkg/service/windowservice/windowservice.go b/pkg/service/windowservice/windowservice.go index f9b24a6fd..1c81afe84 100644 --- a/pkg/service/windowservice/windowservice.go +++ b/pkg/service/windowservice/windowservice.go @@ -5,10 +5,15 @@ package windowservice import ( "context" + "fmt" + "time" + "github.com/wavetermdev/thenextwave/pkg/blockcontroller" "github.com/wavetermdev/thenextwave/pkg/wstore" ) +const DefaultTimeout = 2 * time.Second + type WindowService struct{} func (ws *WindowService) SetWindowPosAndSize(ctx context.Context, windowId string, pos *wstore.Point, size *wstore.WinSize) (wstore.UpdatesRtnType, error) { @@ -32,3 +37,64 @@ func (ws *WindowService) SetWindowPosAndSize(ctx context.Context, windowId strin } return wstore.ContextGetUpdatesRtn(ctx), nil } + +func (svc *WindowService) CloseTab(ctx context.Context, uiContext wstore.UIContext, tabId string) (wstore.UpdatesRtnType, error) { + ctx = wstore.ContextWithUpdates(ctx) + window, err := wstore.DBMustGet[*wstore.Window](ctx, uiContext.WindowId) + if err != nil { + return nil, fmt.Errorf("error getting window: %w", err) + } + tab, err := wstore.DBMustGet[*wstore.Tab](ctx, tabId) + if err != nil { + return nil, fmt.Errorf("error getting tab: %w", err) + } + for _, blockId := range tab.BlockIds { + blockcontroller.StopBlockController(blockId) + } + err = wstore.DeleteTab(ctx, window.WorkspaceId, tabId) + if err != nil { + return nil, fmt.Errorf("error closing tab: %w", err) + } + if window.ActiveTabId == tabId { + ws, err := wstore.DBMustGet[*wstore.Workspace](ctx, window.WorkspaceId) + if err != nil { + return nil, fmt.Errorf("error getting workspace: %w", err) + } + var newActiveTabId string + if len(ws.TabIds) > 0 { + newActiveTabId = ws.TabIds[0] + } else { + newActiveTabId = "" + } + wstore.SetActiveTab(ctx, uiContext.WindowId, newActiveTabId) + } + return wstore.ContextGetUpdatesRtn(ctx), nil +} + +func (svc *WindowService) CloseWindow(ctx context.Context, windowId string) error { + ctx = wstore.ContextWithUpdates(ctx) + window, err := wstore.DBMustGet[*wstore.Window](ctx, windowId) + if err != nil { + return fmt.Errorf("error getting window: %w", err) + } + workspace, err := wstore.DBMustGet[*wstore.Workspace](ctx, window.WorkspaceId) + if err != nil { + return fmt.Errorf("error getting workspace: %w", err) + } + for _, tabId := range workspace.TabIds { + uiContext := wstore.UIContext{WindowId: windowId} + _, err := svc.CloseTab(ctx, uiContext, tabId) + if err != nil { + return fmt.Errorf("error closing tab: %w", err) + } + } + err = wstore.DBDelete(ctx, wstore.OType_Workspace, window.WorkspaceId) + if err != nil { + return fmt.Errorf("error deleting workspace: %w", err) + } + err = wstore.DBDelete(ctx, wstore.OType_Window, windowId) + if err != nil { + return fmt.Errorf("error deleting window: %w", err) + } + return nil +} diff --git a/pkg/tsgen/tsgen.go b/pkg/tsgen/tsgen.go index b1cd903ec..ae1313efd 100644 --- a/pkg/tsgen/tsgen.go +++ b/pkg/tsgen/tsgen.go @@ -101,7 +101,11 @@ func TypeToTSType(t reflect.Type, tsTypesMap map[reflect.Type]string) (string, [ } return fmt.Sprintf("{[key: string]: %s}", elemType), subTypes case reflect.Struct: - return t.Name(), []reflect.Type{t} + name := t.Name() + if tsRename := tsRenameMap[name]; tsRename != "" { + name = tsRename + } + return name, []reflect.Type{t} case reflect.Ptr: return TypeToTSType(t.Elem(), tsTypesMap) case reflect.Interface: diff --git a/pkg/util/utilfn/utilfn.go b/pkg/util/utilfn/utilfn.go index 90c727ac2..b866980e3 100644 --- a/pkg/util/utilfn/utilfn.go +++ b/pkg/util/utilfn/utilfn.go @@ -740,3 +740,25 @@ func DoMapStucture(out any, input any) error { } return decoder.Decode(input) } + +func SliceIdx[T comparable](arr []T, elem T) int { + for idx, e := range arr { + if e == elem { + return idx + } + } + return -1 +} + +func MoveSliceIdxToFront[T any](arr []T, idx int) []T { + // create and return a new slice with idx moved to the front + if idx == 0 || idx >= len(arr) { + // make a copy still + return append([]T(nil), arr...) + } + rtn := make([]T, 0, len(arr)) + rtn = append(rtn, arr[idx]) + rtn = append(rtn, arr[0:idx]...) + rtn = append(rtn, arr[idx+1:]...) + return rtn +} diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 017f06bb1..70fc1a3b7 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -259,7 +259,7 @@ func DeleteBlock(ctx context.Context, tabId string, blockId string) error { }) } -func CloseTab(ctx context.Context, workspaceId string, tabId string) error { +func DeleteTab(ctx context.Context, workspaceId string, tabId string) error { return WithTx(ctx, func(tx *TxWrap) error { ws, _ := DBGet[*Workspace](tx.Context(), workspaceId) if ws == nil { @@ -319,29 +319,11 @@ func UpdateObjectMeta(ctx context.Context, oref waveobj.ORef, meta map[string]an }) } -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() - clientCount, err := DBGetCount[*Client](ctx) - if err != nil { - return fmt.Errorf("error getting client count: %w", err) - } - if clientCount > 0 { - return nil - } +func CreateWindow(ctx context.Context) (*Window, error) { windowId := uuid.NewString() workspaceId := uuid.NewString() tabId := uuid.NewString() layoutNodeId := uuid.NewString() - client := &Client{ - OID: uuid.NewString(), - MainWindowId: windowId, - } - err = DBInsert(ctx, client) - if err != nil { - return fmt.Errorf("error inserting client: %w", err) - } window := &Window{ OID: windowId, WorkspaceId: workspaceId, @@ -356,28 +338,28 @@ func EnsureInitialData() error { Height: 600, }, } - err = DBInsert(ctx, window) + err := DBInsert(ctx, window) if err != nil { - return fmt.Errorf("error inserting window: %w", err) + return nil, fmt.Errorf("error inserting window: %w", err) } ws := &Workspace{ OID: workspaceId, - Name: "default", + Name: "w" + workspaceId[0:8], TabIds: []string{tabId}, } err = DBInsert(ctx, ws) if err != nil { - return fmt.Errorf("error inserting workspace: %w", err) + return nil, fmt.Errorf("error inserting workspace: %w", err) } tab := &Tab{ OID: tabId, - Name: "Tab-1", + Name: "T1", BlockIds: []string{}, LayoutNode: layoutNodeId, } err = DBInsert(ctx, tab) if err != nil { - return fmt.Errorf("error inserting tab: %w", err) + return nil, fmt.Errorf("error inserting tab: %w", err) } layoutNode := &LayoutNode{ @@ -385,7 +367,62 @@ func EnsureInitialData() error { } err = DBInsert(ctx, layoutNode) if err != nil { - return fmt.Errorf("error inserting layout node: %w", err) + return nil, fmt.Errorf("error inserting layout node: %w", err) + } + client, err := DBGetSingleton[*Client](ctx) + if err != nil { + return nil, fmt.Errorf("error getting client: %w", err) + } + client.WindowIds = append(client.WindowIds, windowId) + err = DBUpdate(ctx, client) + if err != nil { + return nil, fmt.Errorf("error updating client: %w", err) + } + return DBMustGet[*Window](ctx, windowId) +} + +func CreateClient(ctx context.Context) (*Client, error) { + client := &Client{ + OID: uuid.NewString(), + WindowIds: []string{}, + } + err := DBInsert(ctx, client) + if err != nil { + return nil, fmt.Errorf("error inserting client: %w", err) + } + return client, nil +} + +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() + client, err := DBGetSingleton[*Client](ctx) + if err == ErrNotFound { + client, err = CreateClient(ctx) + if err != nil { + return fmt.Errorf("error creating client: %w", err) + } + } + if client.MainWindowId != "" { + // convert to windowIds + client.WindowIds = []string{client.MainWindowId} + client.MainWindowId = "" + err = DBUpdate(ctx, client) + if err != nil { + return fmt.Errorf("error updating client: %w", err) + } + client, err = DBGetSingleton[*Client](ctx) + if err != nil { + return fmt.Errorf("error getting client (after main window update): %w", err) + } + } + if len(client.WindowIds) > 0 { + return nil + } + _, err = CreateWindow(ctx) + if err != nil { + return fmt.Errorf("error creating window: %w", err) } return nil } diff --git a/pkg/wstore/wstore_dbops.go b/pkg/wstore/wstore_dbops.go index 5ad2aff97..0f1285ea0 100644 --- a/pkg/wstore/wstore_dbops.go +++ b/pkg/wstore/wstore_dbops.go @@ -67,7 +67,10 @@ func DBGetSingletonByType(ctx context.Context, otype string) (waveobj.WaveObj, e table := tableNameFromOType(otype) query := fmt.Sprintf("SELECT oid, version, data FROM %s LIMIT 1", table) var row idDataType - tx.Get(&row, query) + found := tx.Get(&row, query) + if !found { + return nil, ErrNotFound + } rtn, err := waveobj.FromJson(row.Data) if err != nil { return rtn, err diff --git a/pkg/wstore/wstore_types.go b/pkg/wstore/wstore_types.go index ec46df742..ec296e9a2 100644 --- a/pkg/wstore/wstore_types.go +++ b/pkg/wstore/wstore_types.go @@ -115,7 +115,8 @@ func (update *WaveObjUpdate) UnmarshalJSON(data []byte) error { type Client struct { OID string `json:"oid"` Version int `json:"version"` - MainWindowId string `json:"mainwindowid"` + MainWindowId string `json:"mainwindowid"` // deprecated + WindowIds []string `json:"windowids"` Meta map[string]any `json:"meta"` }