Workspaces are back! (#1282)

This commit is contained in:
Evan Simkowitz 2024-12-02 13:56:56 -05:00 committed by GitHub
parent 10250966fa
commit 82f53dc1fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 2567 additions and 1898 deletions

View File

@ -296,7 +296,12 @@ func main() {
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn()
if !firstRun {
err = wlayout.BootstrapNewWindowLayout(ctx, window)
ws, err := wcore.GetWorkspace(ctx, window.WorkspaceId)
if err != nil {
log.Printf("error getting workspace: %v\n", err)
return
}
err = wlayout.BootstrapNewWorkspaceLayout(ctx, ws)
if err != nil {
log.Panicf("error applying new window layout: %v\n", err)
return

View File

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

View File

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

View File

@ -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
);

View File

@ -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
);

236
emain/emain-tabview.ts Normal file
View File

@ -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<number, WaveTabView>();
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<void>;
savedInitOpts: WaveInitOpts;
waveReadyPromise: Promise<void>;
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<string, WaveTabView>();
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);
}
}

View File

@ -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<string, WaveBrowserWindow>(); // waveWindowId -> WaveBrowserWindow
let focusedWaveWindow = null; // on blur we do not set this to null (but on destroy we do)
const wcvCache = new Map<string, WaveTabView>();
const wcIdToWaveTabMap = new Map<number, WaveTabView>();
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<string, WaveTabView>();
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);
}

View File

@ -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<WebContents> {
const prtn = new Promise<WebContents>((resolve, reject) => {

566
emain/emain-window.ts Normal file
View File

@ -0,0 +1,566 @@
// 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<string, WaveBrowserWindow>(); // 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<void>;
allTabViews: Map<string, WaveTabView>;
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<string, WaveTabView>();
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) {
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;
}
if (this.allTabViews.size) {
for (const tab of this.allTabViews.values()) {
tab?.destroy();
}
}
this.workspaceId = workspaceId;
this.allTabViews = new Map();
const fullConfig = await FileService.GetFullConfig();
const [tabView, tabInitialized] = getOrCreateWebViewForTab(fullConfig, newWs.activetabid);
await this.queueTabSwitch(tabView, tabInitialized);
}
async setActiveTab(tabId: string) {
console.log("setActiveTab", this);
const workspace = await WorkspaceService.GetWorkspace(this.workspaceId);
await WorkspaceService.SetActiveTab(workspace.oid, 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);
this.setActiveTab(tabId);
}
async closeTab(tabId: string) {
const tabView = this.allTabViews.get(tabId);
if (tabView) {
const rtn = await WorkspaceService.CloseTab(this.workspaceId, tabId, true);
this.allTabViews.delete(tabId);
if (rtn?.closewindow) {
this.close();
} else if (rtn?.newactivetabid) {
this.setActiveTab(rtn.newactivetabid);
}
}
}
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<WaveBrowserWindow> {
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);
}
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);
});
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();
}
});

View File

@ -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<string[]> {
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<string> {
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;

View File

@ -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";
@ -106,29 +103,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 +250,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 +364,34 @@ electron.ipcMain.on("open-native-path", (event, filePath: string) => {
});
async function createNewWaveWindow(): Promise<void> {
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 +401,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 +420,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 +501,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 +557,6 @@ function instantiateAppMenu(): electron.Menu {
return getAppMenu({
createNewWaveWindow,
relaunchBrowserWindows,
getLastFocusedWaveWindow: getLastFocusedWaveWindow,
});
}
@ -679,6 +642,7 @@ process.on("uncaughtException", (error) => {
});
async function relaunchBrowserWindows(): Promise<void> {
console.log("relaunchBrowserWindows");
setGlobalIsRelaunching(true);
const windows = getAllWaveWindows();
for (const window of windows) {
@ -691,14 +655,14 @@ async function relaunchBrowserWindows(): Promise<void> {
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 +713,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);
}
});
}

View File

@ -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<void>;
relaunchBrowserWindows: () => Promise<void>;
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();
},
},
];

View File

@ -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),

View File

@ -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());
}

View File

@ -40,15 +40,6 @@ class ClientServiceType {
GetTab(arg1: string): Promise<Tab> {
return WOS.callBackendService("client", "GetTab", Array.from(arguments))
}
GetWindow(arg1: string): Promise<WaveWindow> {
return WOS.callBackendService("client", "GetWindow", Array.from(arguments))
}
GetWorkspace(arg1: string): Promise<Workspace> {
return WOS.callBackendService("client", "GetWorkspace", Array.from(arguments))
}
MakeWindow(): Promise<WaveWindow> {
return WOS.callBackendService("client", "MakeWindow", Array.from(arguments))
}
TelemetryUpdate(arg2: boolean): Promise<void> {
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<string> {
return WOS.callBackendService("object", "AddTabToWorkspace", Array.from(arguments))
}
// @returns blockId (and object updates)
CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<string> {
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<void> {
return WOS.callBackendService("object", "SetActiveTab", Array.from(arguments))
}
// @returns object updates
UpdateObject(waveObj: WaveObj, returnUpdates: boolean): Promise<void> {
return WOS.callBackendService("object", "UpdateObject", Array.from(arguments))
@ -133,11 +114,6 @@ class ObjectServiceType {
UpdateTabName(tabId: string, name: string): Promise<void> {
return WOS.callBackendService("object", "UpdateTabName", Array.from(arguments))
}
// @returns object updates
UpdateWorkspaceTabIds(workspaceId: string, tabIds: string[]): Promise<void> {
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<CloseTabRtnType> {
return WOS.callBackendService("window", "CloseTab", Array.from(arguments))
}
CloseWindow(arg2: string, arg3: boolean): Promise<void> {
CloseWindow(windowId: string, fromElectron: boolean): Promise<void> {
return WOS.callBackendService("window", "CloseWindow", Array.from(arguments))
}
CreateWindow(winSize: WinSize, workspaceId: string): Promise<WaveWindow> {
return WOS.callBackendService("window", "CreateWindow", Array.from(arguments))
}
GetWindow(windowId: string): Promise<WaveWindow> {
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<void> {
SetWindowPosAndSize(windowId: string, pos: Point, size: WinSize): Promise<void> {
return WOS.callBackendService("window", "SetWindowPosAndSize", Array.from(arguments))
}
SwitchWorkspace(windowId: string, workspaceId: string): Promise<Workspace> {
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<CloseTabRtnType> {
return WOS.callBackendService("workspace", "CloseTab", Array.from(arguments))
}
// @returns tabId (and object updates)
CreateTab(workspaceId: string, tabName: string, activateTab: boolean): Promise<string> {
return WOS.callBackendService("workspace", "CreateTab", Array.from(arguments))
}
// @returns object updates
DeleteWorkspace(workspaceId: string): Promise<void> {
return WOS.callBackendService("workspace", "DeleteWorkspace", Array.from(arguments))
}
GetWorkspace(workspaceId: string): Promise<Workspace> {
return WOS.callBackendService("workspace", "GetWorkspace", Array.from(arguments))
}
ListWorkspaces(): Promise<WorkspaceListEntry[]> {
return WOS.callBackendService("workspace", "ListWorkspaces", Array.from(arguments))
}
// @returns object updates
SetActiveTab(workspaceId: string, tabId: string): Promise<void> {
return WOS.callBackendService("workspace", "SetActiveTab", Array.from(arguments))
}
// @returns object updates
UpdateTabIds(workspaceId: string, tabIds: string[]): Promise<void> {
return WOS.callBackendService("workspace", "UpdateTabIds", Array.from(arguments))
}
}
export const WorkspaceService = new WorkspaceServiceType();

View File

@ -167,6 +167,11 @@ class RpcApiType {
return client.wshRpcCall("filewrite", data, opts);
}
// command "focuswindow" [call]
FocusWindowCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("focuswindow", data, opts);
}
// command "getmeta" [call]
GetMetaCommand(client: WshClient, data: CommandGetMetaData, opts?: RpcOpts): Promise<MetaType> {
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<WorkspaceInfoData[]> {
return client.wshRpcCall("workspacelist", null, opts);
}
// command "wshactivity" [call]
WshActivityCommand(client: WshClient, data: {[key: string]: number}, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("wshactivity", data, opts);

View File

@ -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<HTMLElement
);
};
const TabBar = React.memo(({ workspace }: TabBarProps) => {
const TabBar = memo(({ workspace }: TabBarProps) => {
const [tabIds, setTabIds] = useState<string[]>([]);
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
const [draggingTab, setDraggingTab] = useState<string>();
@ -439,7 +440,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
@ -544,7 +545,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
<WindowDrag ref={draggerLeftRef} className="left" />
{appMenuButton}
{devLabel}
{isDev() ? <WorkspaceSwitcher></WorkspaceSwitcher> : null}
<WorkspaceSwitcher></WorkspaceSwitcher>
<div className="tab-bar" ref={tabBarRef} data-overlayscrollbars-initialize>
<div className="tabs-wrapper" ref={tabsWrapperRef} style={{ width: `${tabsWrapperWidth}px` }}>
{tabIds.map((tabId, index) => {

View File

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

View File

@ -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 { memo, useEffect, useRef } from "react";
import { CSSProperties, 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}
/>
<div className="delete-ws-btn-wrapper">
<Button className="ghost grey font-size-12" onClick={onDeleteWorkspace}>
<Button className="ghost red font-size-12" onClick={onDeleteWorkspace}>
Delete workspace
</Button>
</div>
@ -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<WorkspaceDataType[]>(workspaceData);
type WorkspaceListEntry = {
windowId: string;
workspace: Workspace;
};
type WorkspaceList = WorkspaceListEntry[];
const workspaceMapAtom = atom<WorkspaceList>([]);
const workspaceSplitAtom = splitAtom(workspaceMapAtom);
const editingWorkspaceAtom = atom<string>();
const WorkspaceSwitcher = () => {
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 ? (
<i className={clsx("left-icon", makeIconClass(icon, false))} style={{ color: color }}></i>
) : null;
title.leftElement = leftElement;
title.rightElement = isActive ? <i className="fa-sharp fa-solid fa-check" style={{ color: color }}></i> : null;
if (label === "Default") {
return {
id,
type: "group",
title: {
leftElement: <WorkspaceSVG></WorkspaceSVG>,
label: "Default",
rightElement: isActive ? <i className="fa-sharp fa-solid fa-check"></i> : 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 }) => (
<ColorAndIconSelector
title={label}
icon={icon}
color={color}
focusInput={isOpen}
onTitleChange={(title) => 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 (
<ExpandableMenuItem key={item.id ?? index} withHoverEffect={false}>
{item.leftElement && (
<ExpandableMenuItemLeftElement>{item.leftElement}</ExpandableMenuItemLeftElement>
)}
<div className="content">{contentElement}</div>
{item.rightElement && (
<ExpandableMenuItemRightElement>{item.rightElement}</ExpandableMenuItemRightElement>
)}
</ExpandableMenuItem>
);
} else if (item.type === "group") {
return (
<ExpandableMenuItemGroup
key={item.id}
isOpen={item.isOpen}
className={clsx({ "is-active": item.id === activeWorkspace.id })}
>
<ExpandableMenuItemGroupTitle onClick={() => setActiveWorkspace(item.id)}>
<div
className="menu-group-title-wrapper"
style={{
backgroundColor: item.id === activeWorkspace.id ? modWorkspaceColor : "transparent",
}}
>
{item.title.leftElement && (
<ExpandableMenuItemLeftElement>
{item.title.leftElement}
</ExpandableMenuItemLeftElement>
)}
<div className="label">{item.title.label}</div>
{item.title.rightElement && (
<ExpandableMenuItemRightElement>
{item.title.rightElement}
</ExpandableMenuItemRightElement>
)}
</div>
</ExpandableMenuItemGroupTitle>
{item.children && item.children.length > 0 && renderExpandableMenu(item.children, item.isOpen)}
</ExpandableMenuItemGroup>
);
}
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 ? (
<i className={makeIconClass(activeWorkspace.icon, false)} style={{ color: activeWorkspace.color }}></i>
) : (
<WorkspaceSVG />
);
if (activeWorkspace.label == "Default") {
workspaceIcon = <WorkspaceSVG></WorkspaceSVG>;
}
const saveWorkspace = () => {
setObjectValue({ ...activeWorkspace, name: "New Workspace", icon: "circle", color: "green" }, undefined, true);
setTimeout(() => {
fireAndForget(updateWorkspaceList);
}, 10);
};
return (
<Popover className="workspace-switcher-popover">
<PopoverButton className="workspace-switcher-button grey" as="div">
<Popover className="workspace-switcher-popover" onDismiss={() => setEditingWorkspace(null)}>
<PopoverButton
className="workspace-switcher-button grey"
as="div"
onClick={() => {
fireAndForget(updateWorkspaceList);
}}
>
<span className="workspace-icon">{workspaceIcon}</span>
{/* <span className="divider" />
<span className="icon-right">
<ThunderSVG></ThunderSVG>
</span> */}
</PopoverButton>
<PopoverContent className="workspace-switcher-content">
<div className="title">Switch workspace</div>
<div className="title">{isActiveWorkspaceSaved ? "Switch workspace" : "Open workspace"}</div>
<OverlayScrollbarsComponent className={"scrollable"} options={{ scrollbars: { autoHide: "leave" } }}>
<ExpandableMenu noIndent singleOpen>
{renderExpandableMenu(data)}
{workspaceList.map((entry, i) => (
<WorkspaceSwitcherItem key={i} entryAtom={entry} onDeleteWorkspace={onDeleteWorkspace} />
))}
</ExpandableMenu>
</OverlayScrollbarsComponent>
<div className="actions">
<ExpandableMenuItem onClick={() => handleAddNewWorkspace()}>
<ExpandableMenuItemLeftElement>
<i className="fa-sharp fa-solid fa-plus"></i>
</ExpandableMenuItemLeftElement>
<div className="content">New workspace</div>
</ExpandableMenuItem>
</div>
{!isActiveWorkspaceSaved && (
<div className="actions">
<ExpandableMenuItem onClick={() => saveWorkspace()}>
<ExpandableMenuItemLeftElement>
<i className="fa-sharp fa-solid fa-floppy-disk"></i>
</ExpandableMenuItemLeftElement>
<div className="content">Save workspace</div>
</ExpandableMenuItem>
</div>
)}
</PopoverContent>
</Popover>
);
};
const WorkspaceSwitcherItem = ({
entryAtom,
onDeleteWorkspace,
}: {
entryAtom: PrimitiveAtom<WorkspaceListEntry>;
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 (
<ExpandableMenuItemGroup
key={workspace.oid}
isOpen={isEditing}
className={clsx({ "is-current": isCurrentWorkspace })}
>
<ExpandableMenuItemGroupTitle
onClick={() => {
getApi().switchWorkspace(workspace.oid);
// Create a fake escape key event to close the popover
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
}}
>
<div
className="menu-group-title-wrapper"
style={
{
"--workspace-color": workspace.color,
} as CSSProperties
}
>
<ExpandableMenuItemLeftElement>
<i
className={clsx("left-icon", makeIconClass(workspace.icon, false))}
style={{ color: workspace.color }}
/>
</ExpandableMenuItemLeftElement>
<div className="label">{workspace.name}</div>
<ExpandableMenuItemRightElement>
<div className="icons">
<IconButton decl={editIconDecl} />
{isActive && <IconButton decl={windowIconDecl} />}
</div>
</ExpandableMenuItemRightElement>
</div>
</ExpandableMenuItemGroupTitle>
<ExpandableMenuItem>
<ColorAndIconSelector
title={workspace.name}
icon={workspace.icon}
color={workspace.color}
focusInput={isEditing}
onTitleChange={(title) => setWorkspace({ ...workspace, name: title })}
onColorChange={(color) => setWorkspace({ ...workspace, color })}
onIconChange={(icon) => setWorkspace({ ...workspace, icon })}
onDeleteWorkspace={() => onDeleteWorkspace(workspace.oid)}
/>
</ExpandableMenuItem>
</ExpandableMenuItemGroup>
);
};
export { WorkspaceSwitcher };

View File

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

View File

@ -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<CSSProperties>();
const [innerRectDebounceTimeout, setInnerRectDebounceTimeout] = useState<NodeJS.Timeout>();
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);

View File

@ -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<void>;
allTabViews: Map<string, WaveTabView>;
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<void>;
savedInitOpts: WaveInitOpts;
waveReadyPromise: Promise<void>;
initResolve: () => void;
waveReadyResolve: () => void;
};
type TimeSeriesMeta = {
name?: string;
color?: string;

View File

@ -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;
@ -1070,7 +1069,6 @@ declare global {
// waveobj.Window
type WaveWindow = WaveObj & {
workspaceid: string;
activetabid: string;
isnew?: boolean;
pos: Point;
winsize: WinSize;
@ -1118,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

View File

@ -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) {

View File

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

View File

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

View File

@ -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("WorkspaceService: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)
}

View File

@ -0,0 +1,222 @@
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)
}
ws, err := wstore.DBMustGet[*waveobj.Workspace](ctx, 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() {
for _, blockId := range tab.BlockIds {
blockcontroller.StopBlockController(blockId)
}
}()
if err := wcore.DeleteTab(ctx, workspaceId, tabId); err != nil {
return nil, nil, fmt.Errorf("error closing tab: %w", err)
}
rtn := &CloseTabRtnType{}
if ws.ActiveTabId == tabId && tabIndex != -1 {
if len(ws.TabIds) == 1 {
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 {
if tabIndex < len(ws.TabIds)-1 {
newActiveTabId := ws.TabIds[tabIndex+1]
err := wcore.SetActiveTab(ctx, ws.OID, newActiveTabId)
if err != nil {
return rtn, nil, err
}
rtn.NewActiveTabId = newActiveTabId
} else {
newActiveTabId := ws.TabIds[tabIndex-1]
err := wcore.SetActiveTab(ctx, ws.OID, newActiveTabId)
if err != nil {
return rtn, nil, err
}
rtn.NewActiveTabId = newActiveTabId
}
}
}
updates := waveobj.ContextGetUpdatesRtn(ctx)
go func() {
defer panichandler.PanicHandler("WorkspaceService:CloseTab:SendUpdateEvents")
wps.Broker.SendUpdateEvents(updates)
}()
return rtn, updates, nil
}

View File

@ -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<void>")
} 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()

View File

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

View File

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

View File

@ -64,133 +64,6 @@ func sendBlockCloseEvent(blockId string) {
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(wshrpc.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) {
// does not need to run in a transaction since it is called on startup
@ -205,18 +78,8 @@ func EnsureInitialData() (*waveobj.Window, bool, error) {
}
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)
}
}
if client.TempOID == "" {
log.Println("client.TempOID is empty")
client.TempOID = uuid.NewString()
err = wstore.DBUpdate(ctx, client)
if err != nil {
@ -225,12 +88,26 @@ func EnsureInitialData() (*waveobj.Window, bool, error) {
}
log.Printf("clientid: %s\n", client.OID)
if len(client.WindowIds) == 1 {
checkAndFixWindow(ctx, client.WindowIds[0])
log.Println("client has one window")
window := CheckAndFixWindow(ctx, client.WindowIds[0])
if window != nil {
return window, firstRun, nil
}
}
if len(client.WindowIds) > 0 {
log.Println("client has windows")
return nil, false, 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 default workspace: %w", err)
}
_, err = CreateTab(ctx, defaultWs.OID, "", true)
if err != nil {
return nil, false, fmt.Errorf("error creating tab: %w", err)
}
window, err := CreateWindow(ctx, nil, defaultWs.OID)
if err != nil {
return nil, false, fmt.Errorf("error creating window: %w", err)
}
@ -241,7 +118,6 @@ 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 {
@ -289,3 +165,12 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
}()
return blockData, nil
}
func GetClientData(ctx context.Context) (*waveobj.Client, error) {
clientData, err := wstore.DBGetSingleton[*waveobj.Client](ctx)
if err != nil {
return nil, fmt.Errorf("error getting client data: %w", err)
}
log.Printf("clientData: %v\n", clientData)
return clientData, nil
}

200
pkg/wcore/window.go Normal file
View File

@ -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 := 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 CloseWindow(ctx context.Context, windowId string, fromElectron bool) error {
log.Printf("CloseWindow %s\n", windowId)
window, err := wstore.DBMustGet[*waveobj.Window](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)
}

246
pkg/wcore/workspace.go Normal file
View File

@ -0,0 +1,246 @@
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, _ := wstore.DBGet[*waveobj.Workspace](ctx, 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)
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 == "" {
client, err := wstore.DBGetSingleton[*waveobj.Client](ctx)
if err != nil {
return "", fmt.Errorf("error getting client: %w", err)
}
ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
if ws == nil {
return "", fmt.Errorf("workspace not found: %q", workspaceId)
}
tabName = "T" + fmt.Sprint(len(ws.TabIds)+1)
err = wstore.DBUpdate(ctx, client)
if err != nil {
return "", fmt.Errorf("error updating client: %w", err)
}
}
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
func DeleteTab(ctx context.Context, workspaceId string, tabId 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:]...)
wstore.DBUpdate(ctx, ws)
wstore.DBDelete(ctx, waveobj.OType_Tab, tabId)
wstore.DBDelete(ctx, waveobj.OType_LayoutState, tab.LayoutState)
return nil
}
func SetActiveTab(ctx context.Context, workspaceId string, tabId string) error {
workspace, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
if workspace == nil {
return fmt.Errorf("workspace not found: %q", workspaceId)
}
if tabId != "" {
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
}

View File

@ -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()

View File

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

View File

@ -205,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)
@ -372,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)

View File

@ -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"
@ -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
@ -509,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 {
@ -550,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"`
}

View File

@ -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)
@ -711,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
}
@ -737,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 {

View File

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

View File

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