mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-02-21 02:33:34 +01:00
Workspaces are back! (#1282)
This commit is contained in:
parent
10250966fa
commit
82f53dc1fb
@ -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
|
||||
|
@ -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,
|
||||
|
51
cmd/wsh/cmd/wshcmd-workspace.go
Normal file
51
cmd/wsh/cmd/wshcmd-workspace.go
Normal 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")
|
||||
}
|
20
db/migrations-wstore/000006_workspace.down.sql
Normal file
20
db/migrations-wstore/000006_workspace.down.sql
Normal 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
|
||||
);
|
18
db/migrations-wstore/000006_workspace.up.sql
Normal file
18
db/migrations-wstore/000006_workspace.up.sql
Normal 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
236
emain/emain-tabview.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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
566
emain/emain-window.ts
Normal 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();
|
||||
}
|
||||
});
|
@ -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;
|
||||
|
176
emain/emain.ts
176
emain/emain.ts
@ -30,20 +30,17 @@ import {
|
||||
setWasActive,
|
||||
setWasInFg,
|
||||
} from "./emain-activity";
|
||||
import { ensureHotSpareTab, getWaveTabViewByWebContentsId, setMaxTabCacheSize } from "./emain-tabview";
|
||||
import { handleCtrlShiftState } from "./emain-util";
|
||||
import { getIsWaveSrvDead, getWaveSrvProc, getWaveSrvReady, getWaveVersion, runWaveSrv } from "./emain-wavesrv";
|
||||
import {
|
||||
createBrowserWindow,
|
||||
ensureHotSpareTab,
|
||||
focusedWaveWindow,
|
||||
getAllWaveWindows,
|
||||
getFocusedWaveWindow,
|
||||
getLastFocusedWaveWindow,
|
||||
getWaveTabViewByWebContentsId,
|
||||
getWaveWindowById,
|
||||
getWaveWindowByWebContentsId,
|
||||
setActiveTab,
|
||||
setMaxTabCacheSize,
|
||||
} from "./emain-viewmgr";
|
||||
import { getIsWaveSrvDead, getWaveSrvProc, getWaveSrvReady, getWaveVersion, runWaveSrv } from "./emain-wavesrv";
|
||||
WaveBrowserWindow,
|
||||
} from "./emain-window";
|
||||
import { ElectronWshClient, initElectronWshClient } from "./emain-wsh";
|
||||
import { getLaunchSettings } from "./launchsettings";
|
||||
import { getAppMenu } from "./menu";
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -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),
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 };
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
24
frontend/types/custom.d.ts
vendored
24
frontend/types/custom.d.ts
vendored
@ -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;
|
||||
|
23
frontend/types/gotypes.d.ts
vendored
23
frontend/types/gotypes.d.ts
vendored
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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"},
|
||||
|
@ -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{},
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
222
pkg/service/workspaceservice/workspaceservice.go
Normal file
222
pkg/service/workspaceservice/workspaceservice.go
Normal 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
|
||||
}
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
200
pkg/wcore/window.go
Normal 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
246
pkg/wcore/workspace.go
Normal 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
|
||||
}
|
@ -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()
|
||||
|
||||
|
36
pkg/wshrpc/wshclient/barerpcclient.go
Normal file
36
pkg/wshrpc/wshclient/barerpcclient.go
Normal 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
|
||||
}
|
@ -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)
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user