Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workspaces are back! #1282

Merged
merged 78 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
b4a356e
save
esimkowitz Oct 24, 2024
52170aa
Merge branch 'dev-v0.9' into evan/workspace-0.9
esimkowitz Oct 24, 2024
440965c
generate
esimkowitz Oct 24, 2024
517351b
Merge branch 'dev-v0.9' into evan/workspace-0.9
esimkowitz Oct 25, 2024
b9a74bd
save
esimkowitz Oct 25, 2024
47faaf9
Merge branch 'main' into evan/workspace-0.9
esimkowitz Oct 27, 2024
ab791fb
Merge branch 'main' into evan/workspace-0.9
esimkowitz Nov 14, 2024
d8dae8e
save
esimkowitz Nov 14, 2024
d3499ab
Merge branch 'main' into evan/workspace-0.9
esimkowitz Nov 15, 2024
c946c62
save
esimkowitz Nov 15, 2024
f50feda
Merge branch 'main' into evan/workspace-0.9
esimkowitz Nov 16, 2024
147fdb0
generate
esimkowitz Nov 16, 2024
03018aa
save
esimkowitz Nov 16, 2024
32382f7
save
esimkowitz Nov 16, 2024
96c2bce
save
esimkowitz Nov 16, 2024
d090052
save
esimkowitz Nov 16, 2024
e83bca8
yay it started!
esimkowitz Nov 16, 2024
897856e
Merge branch 'main' into evan/workspace-0.9
esimkowitz Nov 18, 2024
bfd8d09
save
esimkowitz Nov 19, 2024
ffeaf30
save
esimkowitz Nov 19, 2024
d4a8f96
save
esimkowitz Nov 19, 2024
f241e38
svve
esimkowitz Nov 19, 2024
1ff6a46
save
esimkowitz Nov 19, 2024
fd675a5
save
esimkowitz Nov 19, 2024
e19316b
save
esimkowitz Nov 19, 2024
e72748c
save
esimkowitz Nov 19, 2024
c10f009
save
esimkowitz Nov 19, 2024
07cd990
save
esimkowitz Nov 19, 2024
185b45d
Merge branch 'evan/workspace-0.9' into evan/workspace-emain-viewmgr
esimkowitz Nov 19, 2024
3a1d79a
Got classes set up
esimkowitz Nov 19, 2024
ad10318
add copyright, remove viewmgr
esimkowitz Nov 19, 2024
bf3b109
got closetab working
esimkowitz Nov 19, 2024
2641bd7
Fixed new tab
esimkowitz Nov 19, 2024
03f3457
remove windowid from getorcreatewebviewfortab
esimkowitz Nov 19, 2024
4846c7e
delete alltabviews entry when closing tab
esimkowitz Nov 19, 2024
b66cf62
Merge branch 'main' into evan/workspace-0.9
esimkowitz Nov 20, 2024
070e8f3
clean up wcore
esimkowitz Nov 20, 2024
c5d49dc
oops
esimkowitz Nov 20, 2024
845a96b
Merge branch 'main' into evan/workspace-0.9
esimkowitz Nov 20, 2024
678cf60
save
esimkowitz Nov 20, 2024
6043c7c
save
esimkowitz Nov 20, 2024
d65dfdd
save
esimkowitz Nov 21, 2024
5d134d4
integrated switcher, though the actual switch is failing
esimkowitz Nov 21, 2024
fbcb839
save
esimkowitz Nov 21, 2024
c9eb12e
save
esimkowitz Nov 21, 2024
22da9d4
Merge branch 'main' into evan/workspace-0.9
esimkowitz Nov 21, 2024
27a503c
improve window close logic
esimkowitz Nov 22, 2024
8437bc7
Merge branch 'main' into evan/workspace-0.9
esimkowitz Nov 22, 2024
812e7c8
delete erroneous component
esimkowitz Nov 22, 2024
6534d69
It works!!!!
esimkowitz Nov 22, 2024
0662166
update icons
esimkowitz Nov 22, 2024
4919a6c
fix colors
esimkowitz Nov 22, 2024
61779db
almost got window switching working
esimkowitz Nov 22, 2024
a833dae
save
esimkowitz Nov 22, 2024
808b2cf
fix metas
esimkowitz Nov 22, 2024
124584a
updated services to support iterables
esimkowitz Nov 22, 2024
faf11ac
add bare rpc client
esimkowitz Nov 22, 2024
33f15a9
bugggggsss
esimkowitz Nov 23, 2024
0e2177b
janky but it works
esimkowitz Nov 23, 2024
b454b09
fix color
esimkowitz Nov 23, 2024
4f57032
Merge branch 'main' into evan/workspace-0.9
esimkowitz Nov 23, 2024
95bfc6d
Merge branch 'main' into evan/workspace-0.9
esimkowitz Nov 23, 2024
725df50
Merge branch 'main' into evan/workspace-0.9
esimkowitz Nov 28, 2024
dfdbaff
Fix blank window on first launch (#1356)
esimkowitz Nov 28, 2024
d969bd5
add default workspace
esimkowitz Nov 28, 2024
9563db1
replace unnecessary printf wit
esimkowitz Nov 28, 2024
3f95176
Close popover on switch
esimkowitz Nov 28, 2024
e61252c
only one editing at a time
esimkowitz Nov 28, 2024
95488f3
only show bg color for current workspace
esimkowitz Nov 28, 2024
cc6e562
Merge branch 'main' into evan/workspace-0.9
esimkowitz Nov 28, 2024
57da9eb
toggle with edit button
esimkowitz Nov 28, 2024
ce31b54
make workspace switcher always visible
esimkowitz Nov 28, 2024
8e79184
update imports, fireandforget
esimkowitz Nov 28, 2024
3b8d602
fix debounced inner rect
esimkowitz Nov 28, 2024
6d312b5
update wsh cmd
esimkowitz Nov 28, 2024
4c918f5
use json output for list
esimkowitz Nov 28, 2024
1dd51d1
Merge branch 'main' into evan/workspace-0.9
esimkowitz Dec 2, 2024
6ef4ea5
open workspace vs switch workspace
esimkowitz Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion cmd/server/main-server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions cmd/wsh/cmd/wshcmd-web.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
51 changes: 51 additions & 0 deletions cmd/wsh/cmd/wshcmd-workspace.go
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions db/migrations-wstore/000006_workspace.down.sql
Original file line number Diff line number Diff line change
@@ -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 changes: 18 additions & 0 deletions db/migrations-wstore/000006_workspace.up.sql
Original file line number Diff line number Diff line change
@@ -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 changes: 236 additions & 0 deletions emain/emain-tabview.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading