mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
browser view (#1005)
This commit is contained in:
parent
7628e667ca
commit
c1c90bb4f8
@ -30,7 +30,7 @@ var webOpenCmd = &cobra.Command{
|
||||
var webGetCmd = &cobra.Command{
|
||||
Use: "get [--inner] [--all] [--json] blockid css-selector",
|
||||
Short: "get the html for a css selector",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Hidden: true,
|
||||
RunE: webGetRun,
|
||||
}
|
||||
@ -51,7 +51,7 @@ func init() {
|
||||
}
|
||||
|
||||
func webGetRun(cmd *cobra.Command, args []string) error {
|
||||
oref := args[0]
|
||||
oref := blockArg
|
||||
if oref == "" {
|
||||
return fmt.Errorf("blockid not specified")
|
||||
}
|
||||
@ -74,7 +74,7 @@ func webGetRun(cmd *cobra.Command, args []string) error {
|
||||
WindowId: blockInfo.WindowId,
|
||||
BlockId: fullORef.OID,
|
||||
TabId: blockInfo.TabId,
|
||||
Selector: args[1],
|
||||
Selector: args[0],
|
||||
Opts: &wshrpc.WebSelectorOpts{
|
||||
Inner: webGetInner,
|
||||
All: webGetAll,
|
||||
|
54
emain/emain-activity.ts
Normal file
54
emain/emain-activity.ts
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// for activity updates
|
||||
let wasActive = true;
|
||||
let wasInFg = true;
|
||||
let globalIsQuitting = false;
|
||||
let globalIsStarting = true;
|
||||
let globalIsRelaunching = false;
|
||||
let forceQuit = false;
|
||||
|
||||
export function setWasActive(val: boolean) {
|
||||
wasActive = val;
|
||||
}
|
||||
|
||||
export function setWasInFg(val: boolean) {
|
||||
wasInFg = val;
|
||||
}
|
||||
|
||||
export function getActivityState(): { wasActive: boolean; wasInFg: boolean } {
|
||||
return { wasActive, wasInFg };
|
||||
}
|
||||
|
||||
export function setGlobalIsQuitting(val: boolean) {
|
||||
globalIsQuitting = val;
|
||||
}
|
||||
|
||||
export function getGlobalIsQuitting(): boolean {
|
||||
return globalIsQuitting;
|
||||
}
|
||||
|
||||
export function setGlobalIsStarting(val: boolean) {
|
||||
globalIsStarting = val;
|
||||
}
|
||||
|
||||
export function getGlobalIsStarting(): boolean {
|
||||
return globalIsStarting;
|
||||
}
|
||||
|
||||
export function setGlobalIsRelaunching(val: boolean) {
|
||||
globalIsRelaunching = val;
|
||||
}
|
||||
|
||||
export function getGlobalIsRelaunching(): boolean {
|
||||
return globalIsRelaunching;
|
||||
}
|
||||
|
||||
export function setForceQuit(val: boolean) {
|
||||
forceQuit = val;
|
||||
}
|
||||
|
||||
export function getForceQuit(): boolean {
|
||||
return forceQuit;
|
||||
}
|
168
emain/emain-util.ts
Normal file
168
emain/emain-util.ts
Normal file
@ -0,0 +1,168 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as electron from "electron";
|
||||
import { getWebServerEndpoint } from "../frontend/util/endpoints";
|
||||
|
||||
export const WaveAppPathVarName = "WAVETERM_APP_PATH";
|
||||
|
||||
export function delay(ms): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function setCtrlShift(wc: Electron.WebContents, state: boolean) {
|
||||
wc.send("control-shift-state-update", state);
|
||||
}
|
||||
|
||||
export function handleCtrlShiftFocus(sender: Electron.WebContents, focused: boolean) {
|
||||
if (!focused) {
|
||||
setCtrlShift(sender, false);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleCtrlShiftState(sender: Electron.WebContents, waveEvent: WaveKeyboardEvent) {
|
||||
if (waveEvent.type == "keyup") {
|
||||
if (waveEvent.key === "Control" || waveEvent.key === "Shift") {
|
||||
setCtrlShift(sender, false);
|
||||
}
|
||||
if (waveEvent.key == "Meta") {
|
||||
if (waveEvent.control && waveEvent.shift) {
|
||||
setCtrlShift(sender, true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (waveEvent.type == "keydown") {
|
||||
if (waveEvent.key === "Control" || waveEvent.key === "Shift" || waveEvent.key === "Meta") {
|
||||
if (waveEvent.control && waveEvent.shift && !waveEvent.meta) {
|
||||
// Set the control and shift without the Meta key
|
||||
setCtrlShift(sender, true);
|
||||
} else {
|
||||
// Unset if Meta is pressed
|
||||
setCtrlShift(sender, false);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function shNavHandler(event: Electron.Event<Electron.WebContentsWillNavigateEventParams>, url: string) {
|
||||
if (url.startsWith("http://127.0.0.1:5173/index.html") || url.startsWith("http://localhost:5173/index.html")) {
|
||||
// this is a dev-mode hot-reload, ignore it
|
||||
console.log("allowing hot-reload of index.html");
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (url.startsWith("https://") || url.startsWith("http://") || url.startsWith("file://")) {
|
||||
console.log("open external, shNav", url);
|
||||
electron.shell.openExternal(url);
|
||||
} else {
|
||||
console.log("navigation canceled", url);
|
||||
}
|
||||
}
|
||||
|
||||
export function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNavigateEventParams>) {
|
||||
if (!event.frame?.parent) {
|
||||
// only use this handler to process iframe events (non-iframe events go to shNavHandler)
|
||||
return;
|
||||
}
|
||||
const url = event.url;
|
||||
console.log(`frame-navigation url=${url} frame=${event.frame.name}`);
|
||||
if (event.frame.name == "webview") {
|
||||
// "webview" links always open in new window
|
||||
// this will *not* effect the initial load because srcdoc does not count as an electron navigation
|
||||
console.log("open external, frameNav", url);
|
||||
event.preventDefault();
|
||||
electron.shell.openExternal(url);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.frame.name == "pdfview" &&
|
||||
(url.startsWith("blob:file:///") || url.startsWith(getWebServerEndpoint() + "/wave/stream-file?"))
|
||||
) {
|
||||
// allowed
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
console.log("frame navigation canceled");
|
||||
}
|
||||
|
||||
function isWindowFullyVisible(bounds: electron.Rectangle): boolean {
|
||||
const displays = electron.screen.getAllDisplays();
|
||||
|
||||
// Helper function to check if a point is inside any display
|
||||
function isPointInDisplay(x: number, y: number) {
|
||||
for (const display of displays) {
|
||||
const { x: dx, y: dy, width, height } = display.bounds;
|
||||
if (x >= dx && x < dx + width && y >= dy && y < dy + height) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check all corners of the window
|
||||
const topLeft = isPointInDisplay(bounds.x, bounds.y);
|
||||
const topRight = isPointInDisplay(bounds.x + bounds.width, bounds.y);
|
||||
const bottomLeft = isPointInDisplay(bounds.x, bounds.y + bounds.height);
|
||||
const bottomRight = isPointInDisplay(bounds.x + bounds.width, bounds.y + bounds.height);
|
||||
|
||||
return topLeft && topRight && bottomLeft && bottomRight;
|
||||
}
|
||||
|
||||
function findDisplayWithMostArea(bounds: electron.Rectangle): electron.Display {
|
||||
const displays = electron.screen.getAllDisplays();
|
||||
let maxArea = 0;
|
||||
let bestDisplay = null;
|
||||
|
||||
for (let display of displays) {
|
||||
const { x, y, width, height } = display.bounds;
|
||||
const overlapX = Math.max(0, Math.min(bounds.x + bounds.width, x + width) - Math.max(bounds.x, x));
|
||||
const overlapY = Math.max(0, Math.min(bounds.y + bounds.height, y + height) - Math.max(bounds.y, y));
|
||||
const overlapArea = overlapX * overlapY;
|
||||
|
||||
if (overlapArea > maxArea) {
|
||||
maxArea = overlapArea;
|
||||
bestDisplay = display;
|
||||
}
|
||||
}
|
||||
|
||||
return bestDisplay;
|
||||
}
|
||||
|
||||
function adjustBoundsToFitDisplay(bounds: electron.Rectangle, display: electron.Display): electron.Rectangle {
|
||||
const { x: dx, y: dy, width: dWidth, height: dHeight } = display.workArea;
|
||||
let { x, y, width, height } = bounds;
|
||||
|
||||
// Adjust width and height to fit within the display's work area
|
||||
width = Math.min(width, dWidth);
|
||||
height = Math.min(height, dHeight);
|
||||
|
||||
// Adjust x to ensure the window fits within the display
|
||||
if (x < dx) {
|
||||
x = dx;
|
||||
} else if (x + width > dx + dWidth) {
|
||||
x = dx + dWidth - width;
|
||||
}
|
||||
|
||||
// Adjust y to ensure the window fits within the display
|
||||
if (y < dy) {
|
||||
y = dy;
|
||||
} else if (y + height > dy + dHeight) {
|
||||
y = dy + dHeight - height;
|
||||
}
|
||||
return { x, y, width, height };
|
||||
}
|
||||
|
||||
export function ensureBoundsAreVisible(bounds: electron.Rectangle): electron.Rectangle {
|
||||
if (!isWindowFullyVisible(bounds)) {
|
||||
let targetDisplay = findDisplayWithMostArea(bounds);
|
||||
|
||||
if (!targetDisplay) {
|
||||
targetDisplay = electron.screen.getPrimaryDisplay();
|
||||
}
|
||||
|
||||
return adjustBoundsToFitDisplay(bounds, targetDisplay);
|
||||
}
|
||||
return bounds;
|
||||
}
|
556
emain/emain-viewmgr.ts
Normal file
556
emain/emain-viewmgr.ts
Normal file
@ -0,0 +1,556 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { ClientService, FileService, ObjectService, WindowService } from "@/app/store/services";
|
||||
import * as electron from "electron";
|
||||
import {
|
||||
delay,
|
||||
ensureBoundsAreVisible,
|
||||
handleCtrlShiftFocus,
|
||||
handleCtrlShiftState,
|
||||
shFrameNavHandler,
|
||||
shNavHandler,
|
||||
} from "emain/emain-util";
|
||||
import * as keyutil from "frontend/util/keyutil";
|
||||
import * as path from "path";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import { configureAuthKeyRequestInjection } from "./authkey";
|
||||
import { getGlobalIsQuitting, getGlobalIsStarting, setWasActive, setWasInFg } from "./emain-activity";
|
||||
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>();
|
||||
|
||||
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: -10000,
|
||||
y: -10000,
|
||||
width: winBounds.width,
|
||||
height: winBounds.height,
|
||||
});
|
||||
}
|
||||
|
||||
async function repositionTabsSlowly(
|
||||
newTabView: WaveTabView,
|
||||
oldTabView: WaveTabView,
|
||||
delayMs: number,
|
||||
winBounds: Electron.Rectangle
|
||||
) {
|
||||
if (newTabView == null) {
|
||||
return;
|
||||
}
|
||||
newTabView.setBounds({
|
||||
x: winBounds.width - 10,
|
||||
y: winBounds.height - 10,
|
||||
width: winBounds.width,
|
||||
height: winBounds.height,
|
||||
});
|
||||
await delay(delayMs);
|
||||
newTabView.setBounds({ x: 0, y: 0, width: winBounds.width, height: winBounds.height });
|
||||
oldTabView?.setBounds({
|
||||
x: -10000,
|
||||
y: -10000,
|
||||
width: winBounds.width,
|
||||
height: winBounds.height,
|
||||
});
|
||||
}
|
||||
|
||||
function positionTabOnScreen(tabView: WaveTabView, winBounds: Electron.Rectangle) {
|
||||
if (tabView == null) {
|
||||
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;
|
||||
}
|
||||
for (const tabView of waveWindow.allTabViews.values()) {
|
||||
destroyTab(tabView);
|
||||
}
|
||||
waveWindowMap.delete(waveWindow.waveWindowId);
|
||||
}
|
||||
|
||||
export function destroyTab(tabView: WaveTabView) {
|
||||
if (tabView == null) {
|
||||
return;
|
||||
}
|
||||
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("focus", () => {
|
||||
setWasInFg(true);
|
||||
setWasActive(true);
|
||||
if (getGlobalIsStarting()) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
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 resizing window", e);
|
||||
}
|
||||
}
|
||||
|
||||
type WindowOpts = {
|
||||
unamePlatform: string;
|
||||
};
|
||||
|
||||
function createBaseWaveBrowserWindow(
|
||||
waveWindow: WaveWindow,
|
||||
fullConfig: FullConfigType,
|
||||
opts: WindowOpts
|
||||
): WaveBrowserWindow {
|
||||
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>();
|
||||
win.on(
|
||||
"resize",
|
||||
debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win))
|
||||
);
|
||||
win.on("resize", () => {
|
||||
if (win.isDestroyed() || win.fullScreen) {
|
||||
return;
|
||||
}
|
||||
positionTabOnScreen(win.activeTabView, win.getContentBounds());
|
||||
});
|
||||
win.on(
|
||||
"move",
|
||||
debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win))
|
||||
);
|
||||
win.on("enter-full-screen", async () => {
|
||||
const tabView = win.activeTabView;
|
||||
if (tabView) {
|
||||
tabView.webContents.send("fullscreen-change", true);
|
||||
}
|
||||
});
|
||||
win.on("leave-full-screen", async () => {
|
||||
const tabView = win.activeTabView;
|
||||
if (tabView) {
|
||||
tabView.webContents.send("fullscreen-change", false);
|
||||
}
|
||||
});
|
||||
win.on("focus", () => {
|
||||
focusedWaveWindow = win;
|
||||
console.log("focus win", win.waveWindowId);
|
||||
ClientService.FocusWindow(win.waveWindowId);
|
||||
});
|
||||
win.on("blur", () => {
|
||||
if (focusedWaveWindow == win) {
|
||||
focusedWaveWindow = null;
|
||||
}
|
||||
});
|
||||
win.on("close", (e) => {
|
||||
if (getGlobalIsQuitting() || updater?.status == "installing") {
|
||||
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();
|
||||
}
|
||||
});
|
||||
win.on("closed", () => {
|
||||
if (getGlobalIsQuitting() || updater?.status == "installing") {
|
||||
return;
|
||||
}
|
||||
const numWindows = waveWindowMap.size;
|
||||
if (numWindows == 0) {
|
||||
return;
|
||||
}
|
||||
if (!win.alreadyClosed) {
|
||||
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);
|
||||
setTabViewIntoWindow(waveWindow, tabView, tabInitialized);
|
||||
}
|
||||
|
||||
async function setTabViewIntoWindow(bwin: WaveBrowserWindow, tabView: WaveTabView, tabInitialized: boolean) {
|
||||
const curTabView: WaveTabView = bwin.getContentView() as any;
|
||||
const clientData = await ClientService.GetClientData();
|
||||
if (curTabView != null) {
|
||||
curTabView.isActiveTab = false;
|
||||
}
|
||||
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());
|
||||
repositionTabsSlowly(tabView, oldActiveView, 100, bwin.getContentBounds());
|
||||
} else {
|
||||
console.log("reusing an existing tab");
|
||||
repositionTabsSlowly(tabView, oldActiveView, 35, bwin.getContentBounds());
|
||||
tabView.webContents.send("wave-init", tabView.savedInitOpts); // reinit
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
118
emain/emain-wavesrv.ts
Normal file
118
emain/emain-wavesrv.ts
Normal file
@ -0,0 +1,118 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { WebServerEndpointVarName, WSServerEndpointVarName } from "@/util/endpoints";
|
||||
import * as electron from "electron";
|
||||
import { AuthKey, AuthKeyEnv } from "emain/authkey";
|
||||
import { setForceQuit } from "emain/emain-activity";
|
||||
import { WaveAppPathVarName } from "emain/emain-util";
|
||||
import { getElectronAppUnpackedBasePath, getWaveSrvCwd, getWaveSrvPath } from "emain/platform";
|
||||
import { updater } from "emain/updater";
|
||||
import * as child_process from "node:child_process";
|
||||
import * as readline from "readline";
|
||||
|
||||
export const WaveSrvReadySignalPidVarName = "WAVETERM_READY_SIGNAL_PID";
|
||||
|
||||
let isWaveSrvDead = false;
|
||||
let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null;
|
||||
let WaveVersion = "unknown"; // set by WAVESRV-ESTART
|
||||
let WaveBuildTime = 0; // set by WAVESRV-ESTART
|
||||
|
||||
export function getWaveVersion(): { version: string; buildTime: number } {
|
||||
return { version: WaveVersion, buildTime: WaveBuildTime };
|
||||
}
|
||||
|
||||
let waveSrvReadyResolve = (value: boolean) => {};
|
||||
const waveSrvReady: Promise<boolean> = new Promise((resolve, _) => {
|
||||
waveSrvReadyResolve = resolve;
|
||||
});
|
||||
|
||||
export function getWaveSrvReady(): Promise<boolean> {
|
||||
return waveSrvReady;
|
||||
}
|
||||
|
||||
export function getWaveSrvProc(): child_process.ChildProcessWithoutNullStreams | null {
|
||||
return waveSrvProc;
|
||||
}
|
||||
|
||||
export function getIsWaveSrvDead(): boolean {
|
||||
return isWaveSrvDead;
|
||||
}
|
||||
|
||||
export function runWaveSrv(handleWSEvent: (evtMsg: WSEventType) => void): Promise<boolean> {
|
||||
let pResolve: (value: boolean) => void;
|
||||
let pReject: (reason?: any) => void;
|
||||
const rtnPromise = new Promise<boolean>((argResolve, argReject) => {
|
||||
pResolve = argResolve;
|
||||
pReject = argReject;
|
||||
});
|
||||
const envCopy = { ...process.env };
|
||||
envCopy[WaveAppPathVarName] = getElectronAppUnpackedBasePath();
|
||||
envCopy[WaveSrvReadySignalPidVarName] = process.pid.toString();
|
||||
envCopy[AuthKeyEnv] = AuthKey;
|
||||
const waveSrvCmd = getWaveSrvPath();
|
||||
console.log("trying to run local server", waveSrvCmd);
|
||||
const proc = child_process.spawn(getWaveSrvPath(), {
|
||||
cwd: getWaveSrvCwd(),
|
||||
env: envCopy,
|
||||
});
|
||||
proc.on("exit", (e) => {
|
||||
if (updater?.status == "installing") {
|
||||
return;
|
||||
}
|
||||
console.log("wavesrv exited, shutting down");
|
||||
setForceQuit(true);
|
||||
isWaveSrvDead = true;
|
||||
electron.app.quit();
|
||||
});
|
||||
proc.on("spawn", (e) => {
|
||||
console.log("spawned wavesrv");
|
||||
waveSrvProc = proc;
|
||||
pResolve(true);
|
||||
});
|
||||
proc.on("error", (e) => {
|
||||
console.log("error running wavesrv", e);
|
||||
pReject(e);
|
||||
});
|
||||
const rlStdout = readline.createInterface({
|
||||
input: proc.stdout,
|
||||
terminal: false,
|
||||
});
|
||||
rlStdout.on("line", (line) => {
|
||||
console.log(line);
|
||||
});
|
||||
const rlStderr = readline.createInterface({
|
||||
input: proc.stderr,
|
||||
terminal: false,
|
||||
});
|
||||
rlStderr.on("line", (line) => {
|
||||
if (line.includes("WAVESRV-ESTART")) {
|
||||
const startParams = /ws:([a-z0-9.:]+) web:([a-z0-9.:]+) version:([a-z0-9.\-]+) buildtime:(\d+)/gm.exec(
|
||||
line
|
||||
);
|
||||
if (startParams == null) {
|
||||
console.log("error parsing WAVESRV-ESTART line", line);
|
||||
electron.app.quit();
|
||||
return;
|
||||
}
|
||||
process.env[WSServerEndpointVarName] = startParams[1];
|
||||
process.env[WebServerEndpointVarName] = startParams[2];
|
||||
WaveVersion = startParams[3];
|
||||
WaveBuildTime = parseInt(startParams[4]);
|
||||
waveSrvReadyResolve(true);
|
||||
return;
|
||||
}
|
||||
if (line.startsWith("WAVESRV-EVENT:")) {
|
||||
const evtJson = line.slice("WAVESRV-EVENT:".length);
|
||||
try {
|
||||
const evtMsg: WSEventType = JSON.parse(evtJson);
|
||||
handleWSEvent(evtMsg);
|
||||
} catch (e) {
|
||||
console.log("error handling WAVESRV-EVENT", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
console.log(line);
|
||||
});
|
||||
return rtnPromise;
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { BrowserWindow, ipcMain, webContents, WebContents } from "electron";
|
||||
import { ipcMain, webContents, WebContents } from "electron";
|
||||
|
||||
export function getWebContentsByBlockId(win: BrowserWindow, tabId: string, blockId: string): Promise<WebContents> {
|
||||
export function getWebContentsByBlockId(ww: WaveBrowserWindow, tabId: string, blockId: string): Promise<WebContents> {
|
||||
const prtn = new Promise<WebContents>((resolve, reject) => {
|
||||
const randId = Math.floor(Math.random() * 1000000000).toString();
|
||||
const respCh = `getWebContentsByBlockId-${randId}`;
|
||||
win.webContents.send("webcontentsid-from-blockid", blockId, respCh);
|
||||
ww?.activeTabView?.webContents.send("webcontentsid-from-blockid", blockId, respCh);
|
||||
ipcMain.once(respCh, (event, webContentsId) => {
|
||||
if (webContentsId == null) {
|
||||
resolve(null);
|
||||
|
@ -1,12 +1,11 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import electron from "electron";
|
||||
import { Notification } from "electron";
|
||||
import { getWaveWindowById } from "emain/emain-viewmgr";
|
||||
import { RpcResponseHelper, WshClient } from "../frontend/app/store/wshclient";
|
||||
import { getWebContentsByBlockId, webGetSelector } from "./emain-web";
|
||||
|
||||
type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string; readyPromise: Promise<void> };
|
||||
|
||||
export class ElectronWshClientType extends WshClient {
|
||||
constructor() {
|
||||
super("electron");
|
||||
@ -16,12 +15,11 @@ export class ElectronWshClientType extends WshClient {
|
||||
if (!data.tabid || !data.blockid || !data.windowid) {
|
||||
throw new Error("tabid and blockid are required");
|
||||
}
|
||||
const windows = electron.BrowserWindow.getAllWindows();
|
||||
const win = windows.find((w) => (w as WaveBrowserWindow).waveWindowId === data.windowid);
|
||||
if (win == null) {
|
||||
const ww = getWaveWindowById(data.windowid);
|
||||
if (ww == null) {
|
||||
throw new Error(`no window found with id ${data.windowid}`);
|
||||
}
|
||||
const wc = await getWebContentsByBlockId(win, data.tabid, data.blockid);
|
||||
const wc = await getWebContentsByBlockId(ww, data.tabid, data.blockid);
|
||||
if (wc == null) {
|
||||
throw new Error(`no webcontents found with blockid ${data.blockid}`);
|
||||
}
|
||||
@ -30,7 +28,7 @@ export class ElectronWshClientType extends WshClient {
|
||||
}
|
||||
|
||||
async handle_notify(rh: RpcResponseHelper, notificationOptions: WaveNotificationOptions) {
|
||||
new electron.Notification({
|
||||
new Notification({
|
||||
title: notificationOptions.title,
|
||||
body: notificationOptions.body,
|
||||
silent: notificationOptions.silent,
|
||||
|
731
emain/emain.ts
731
emain/emain.ts
@ -2,24 +2,47 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as electron from "electron";
|
||||
import {
|
||||
getActivityState,
|
||||
getForceQuit,
|
||||
getGlobalIsRelaunching,
|
||||
setForceQuit,
|
||||
setGlobalIsQuitting,
|
||||
setGlobalIsRelaunching,
|
||||
setGlobalIsStarting,
|
||||
setWasActive,
|
||||
setWasInFg,
|
||||
} from "emain/emain-activity";
|
||||
import { handleCtrlShiftState } from "emain/emain-util";
|
||||
import {
|
||||
createBrowserWindow,
|
||||
ensureHotSpareTab,
|
||||
getAllWaveWindows,
|
||||
getFocusedWaveWindow,
|
||||
getLastFocusedWaveWindow,
|
||||
getWaveTabViewByWebContentsId,
|
||||
getWaveWindowById,
|
||||
getWaveWindowByWebContentsId,
|
||||
setActiveTab,
|
||||
setMaxTabCacheSize,
|
||||
} from "emain/emain-viewmgr";
|
||||
import { getIsWaveSrvDead, getWaveSrvProc, getWaveSrvReady, getWaveVersion, runWaveSrv } from "emain/emain-wavesrv";
|
||||
import { FastAverageColor } from "fast-average-color";
|
||||
import fs from "fs";
|
||||
import * as child_process from "node:child_process";
|
||||
import * as path from "path";
|
||||
import { PNG } from "pngjs";
|
||||
import * as readline from "readline";
|
||||
import { sprintf } from "sprintf-js";
|
||||
import { Readable } from "stream";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import * as util from "util";
|
||||
import winston from "winston";
|
||||
import * as services from "../frontend/app/store/services";
|
||||
import { initElectronWshrpc, shutdownWshrpc } from "../frontend/app/store/wshrpcutil";
|
||||
import { WSServerEndpointVarName, WebServerEndpointVarName, getWebServerEndpoint } from "../frontend/util/endpoints";
|
||||
import { getWebServerEndpoint } from "../frontend/util/endpoints";
|
||||
import { fetch } from "../frontend/util/fetchutil";
|
||||
import * as keyutil from "../frontend/util/keyutil";
|
||||
import { fireAndForget } from "../frontend/util/util";
|
||||
import { AuthKey, AuthKeyEnv, configureAuthKeyRequestInjection } from "./authkey";
|
||||
import { AuthKey, configureAuthKeyRequestInjection } from "./authkey";
|
||||
import { initDocsite } from "./docsite";
|
||||
import { ElectronWshClient, initElectronWshClient } from "./emain-wsh";
|
||||
import { getLaunchSettings } from "./launchsettings";
|
||||
@ -28,46 +51,19 @@ import {
|
||||
getElectronAppBasePath,
|
||||
getElectronAppUnpackedBasePath,
|
||||
getWaveHomeDir,
|
||||
getWaveSrvCwd,
|
||||
getWaveSrvPath,
|
||||
isDev,
|
||||
isDevVite,
|
||||
unameArch,
|
||||
unamePlatform,
|
||||
} from "./platform";
|
||||
import { configureAutoUpdater, updater } from "./updater";
|
||||
|
||||
const electronApp = electron.app;
|
||||
let WaveVersion = "unknown"; // set by WAVESRV-ESTART
|
||||
let WaveBuildTime = 0; // set by WAVESRV-ESTART
|
||||
let forceQuit = false;
|
||||
let isWaveSrvDead = false;
|
||||
|
||||
const WaveAppPathVarName = "WAVETERM_APP_PATH";
|
||||
const WaveSrvReadySignalPidVarName = "WAVETERM_READY_SIGNAL_PID";
|
||||
electron.nativeTheme.themeSource = "dark";
|
||||
|
||||
type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string; readyPromise: Promise<void> };
|
||||
|
||||
let waveSrvReadyResolve = (value: boolean) => {};
|
||||
const waveSrvReady: Promise<boolean> = new Promise((resolve, _) => {
|
||||
waveSrvReadyResolve = resolve;
|
||||
});
|
||||
let globalIsQuitting = false;
|
||||
let globalIsStarting = true;
|
||||
let globalIsRelaunching = false;
|
||||
|
||||
// for activity updates
|
||||
let wasActive = true;
|
||||
let wasInFg = true;
|
||||
|
||||
let webviewFocusId: number = null; // set to the getWebContentsId of the webview that has focus (null if not focused)
|
||||
let webviewKeys: string[] = []; // the keys to trap when webview has focus
|
||||
|
||||
let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null;
|
||||
|
||||
const waveHome = getWaveHomeDir();
|
||||
|
||||
const oldConsoleLog = console.log;
|
||||
|
||||
const loggerTransports: winston.transport[] = [
|
||||
@ -79,7 +75,7 @@ if (isDev) {
|
||||
const loggerConfig = {
|
||||
level: "info",
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
|
||||
winston.format.printf((info) => `${info.timestamp} ${info.message}`)
|
||||
),
|
||||
transports: loggerTransports,
|
||||
@ -107,125 +103,6 @@ if (isDev) {
|
||||
console.log("waveterm-app WAVETERM_DEV set");
|
||||
}
|
||||
|
||||
function getWindowForEvent(event: Electron.IpcMainEvent): Electron.BrowserWindow {
|
||||
const windowId = event.sender.id;
|
||||
return electron.BrowserWindow.fromId(windowId);
|
||||
}
|
||||
|
||||
function setCtrlShift(wc: Electron.WebContents, state: boolean) {
|
||||
wc.send("control-shift-state-update", state);
|
||||
}
|
||||
|
||||
function handleCtrlShiftState(sender: Electron.WebContents, waveEvent: WaveKeyboardEvent) {
|
||||
if (waveEvent.type == "keyup") {
|
||||
if (waveEvent.key === "Control" || waveEvent.key === "Shift") {
|
||||
setCtrlShift(sender, false);
|
||||
}
|
||||
if (waveEvent.key == "Meta") {
|
||||
if (waveEvent.control && waveEvent.shift) {
|
||||
setCtrlShift(sender, true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (waveEvent.type == "keydown") {
|
||||
if (waveEvent.key === "Control" || waveEvent.key === "Shift" || waveEvent.key === "Meta") {
|
||||
if (waveEvent.control && waveEvent.shift && !waveEvent.meta) {
|
||||
// Set the control and shift without the Meta key
|
||||
setCtrlShift(sender, true);
|
||||
} else {
|
||||
// Unset if Meta is pressed
|
||||
setCtrlShift(sender, false);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCtrlShiftFocus(sender: Electron.WebContents, focused: boolean) {
|
||||
if (!focused) {
|
||||
setCtrlShift(sender, false);
|
||||
}
|
||||
}
|
||||
|
||||
function runWaveSrv(): Promise<boolean> {
|
||||
let pResolve: (value: boolean) => void;
|
||||
let pReject: (reason?: any) => void;
|
||||
const rtnPromise = new Promise<boolean>((argResolve, argReject) => {
|
||||
pResolve = argResolve;
|
||||
pReject = argReject;
|
||||
});
|
||||
const envCopy = { ...process.env };
|
||||
envCopy[WaveAppPathVarName] = getElectronAppUnpackedBasePath();
|
||||
envCopy[WaveSrvReadySignalPidVarName] = process.pid.toString();
|
||||
envCopy[AuthKeyEnv] = AuthKey;
|
||||
const waveSrvCmd = getWaveSrvPath();
|
||||
console.log("trying to run local server", waveSrvCmd);
|
||||
const proc = child_process.spawn(getWaveSrvPath(), {
|
||||
cwd: getWaveSrvCwd(),
|
||||
env: envCopy,
|
||||
});
|
||||
proc.on("exit", (e) => {
|
||||
if (updater?.status == "installing") {
|
||||
return;
|
||||
}
|
||||
console.log("wavesrv exited, shutting down");
|
||||
forceQuit = true;
|
||||
isWaveSrvDead = true;
|
||||
electronApp.quit();
|
||||
});
|
||||
proc.on("spawn", (e) => {
|
||||
console.log("spawned wavesrv");
|
||||
waveSrvProc = proc;
|
||||
pResolve(true);
|
||||
});
|
||||
proc.on("error", (e) => {
|
||||
console.log("error running wavesrv", e);
|
||||
pReject(e);
|
||||
});
|
||||
const rlStdout = readline.createInterface({
|
||||
input: proc.stdout,
|
||||
terminal: false,
|
||||
});
|
||||
rlStdout.on("line", (line) => {
|
||||
console.log(line);
|
||||
});
|
||||
const rlStderr = readline.createInterface({
|
||||
input: proc.stderr,
|
||||
terminal: false,
|
||||
});
|
||||
rlStderr.on("line", (line) => {
|
||||
if (line.includes("WAVESRV-ESTART")) {
|
||||
const startParams = /ws:([a-z0-9.:]+) web:([a-z0-9.:]+) version:([a-z0-9.\-]+) buildtime:(\d+)/gm.exec(
|
||||
line
|
||||
);
|
||||
if (startParams == null) {
|
||||
console.log("error parsing WAVESRV-ESTART line", line);
|
||||
electronApp.quit();
|
||||
return;
|
||||
}
|
||||
process.env[WSServerEndpointVarName] = startParams[1];
|
||||
process.env[WebServerEndpointVarName] = startParams[2];
|
||||
WaveVersion = startParams[3];
|
||||
WaveBuildTime = parseInt(startParams[4]);
|
||||
waveSrvReadyResolve(true);
|
||||
return;
|
||||
}
|
||||
if (line.startsWith("WAVESRV-EVENT:")) {
|
||||
const evtJson = line.slice("WAVESRV-EVENT:".length);
|
||||
try {
|
||||
const evtMsg: WSEventType = JSON.parse(evtJson);
|
||||
handleWSEvent(evtMsg);
|
||||
} catch (e) {
|
||||
console.log("error handling WAVESRV-EVENT", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
console.log(line);
|
||||
});
|
||||
return rtnPromise;
|
||||
}
|
||||
|
||||
async function handleWSEvent(evtMsg: WSEventType) {
|
||||
console.log("handleWSEvent", evtMsg?.eventtype);
|
||||
if (evtMsg.eventtype == "electron:newwindow") {
|
||||
@ -236,391 +113,21 @@ async function handleWSEvent(evtMsg: WSEventType) {
|
||||
}
|
||||
const clientData = await services.ClientService.GetClientData();
|
||||
const fullConfig = await services.FileService.GetFullConfig();
|
||||
const newWin = createBrowserWindow(clientData.oid, windowData, fullConfig);
|
||||
await newWin.readyPromise;
|
||||
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 windows = electron.BrowserWindow.getAllWindows();
|
||||
for (const window of windows) {
|
||||
if ((window as any).waveWindowId === evtMsg.data) {
|
||||
// Bypass the "Are you sure?" dialog, since this event is called when there's no more tabs for the window.
|
||||
window.destroy();
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
async function persistWindowBounds(windowId: string, bounds: electron.Rectangle) {
|
||||
try {
|
||||
await services.WindowService.SetWindowPosAndSize(
|
||||
windowId,
|
||||
{ x: bounds.x, y: bounds.y },
|
||||
{ width: bounds.width, height: bounds.height }
|
||||
);
|
||||
} catch (e) {
|
||||
console.log("error resizing window", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function mainResizeHandler(_: any, windowId: string, win: WaveBrowserWindow) {
|
||||
if (win == null || win.isDestroyed() || win.fullScreen) {
|
||||
return;
|
||||
}
|
||||
const bounds = win.getBounds();
|
||||
await persistWindowBounds(windowId, bounds);
|
||||
}
|
||||
|
||||
function shNavHandler(event: Electron.Event<Electron.WebContentsWillNavigateEventParams>, url: string) {
|
||||
if (url.startsWith("http://127.0.0.1:5173/index.html") || url.startsWith("http://localhost:5173/index.html")) {
|
||||
// this is a dev-mode hot-reload, ignore it
|
||||
console.log("allowing hot-reload of index.html");
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (url.startsWith("https://") || url.startsWith("http://") || url.startsWith("file://")) {
|
||||
console.log("open external, shNav", url);
|
||||
electron.shell.openExternal(url);
|
||||
} else {
|
||||
console.log("navigation canceled", url);
|
||||
}
|
||||
}
|
||||
|
||||
function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNavigateEventParams>) {
|
||||
if (!event.frame?.parent) {
|
||||
// only use this handler to process iframe events (non-iframe events go to shNavHandler)
|
||||
return;
|
||||
}
|
||||
const url = event.url;
|
||||
console.log(`frame-navigation url=${url} frame=${event.frame.name}`);
|
||||
if (event.frame.name == "webview") {
|
||||
// "webview" links always open in new window
|
||||
// this will *not* effect the initial load because srcdoc does not count as an electron navigation
|
||||
console.log("open external, frameNav", url);
|
||||
event.preventDefault();
|
||||
electron.shell.openExternal(url);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.frame.name == "pdfview" &&
|
||||
(url.startsWith("blob:file:///") || url.startsWith(getWebServerEndpoint() + "/wave/stream-file?"))
|
||||
) {
|
||||
// allowed
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
console.log("frame navigation canceled");
|
||||
}
|
||||
|
||||
function computeNewWinBounds(waveWindow: WaveWindow): Electron.Rectangle {
|
||||
const targetWidth = waveWindow.winsize?.width || 2000;
|
||||
const targetHeight = waveWindow.winsize?.height || 1080;
|
||||
const primaryDisplay = electron.screen.getPrimaryDisplay();
|
||||
const workArea = primaryDisplay.workArea;
|
||||
const targetPadding = 100;
|
||||
const minPadding = 10;
|
||||
let rtn = {
|
||||
x: workArea.x + targetPadding,
|
||||
y: workArea.y + targetPadding,
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
};
|
||||
const spareWidth = workArea.width - targetWidth;
|
||||
if (spareWidth < 2 * minPadding) {
|
||||
rtn.x = workArea.x + minPadding;
|
||||
rtn.width = workArea.width - 2 * minPadding;
|
||||
} else if (spareWidth > 2 * targetPadding) {
|
||||
rtn.x = workArea.x + targetPadding;
|
||||
} else {
|
||||
rtn.x = workArea.y + Math.floor(spareWidth / 2);
|
||||
}
|
||||
const spareHeight = workArea.height - targetHeight;
|
||||
if (spareHeight < 2 * minPadding) {
|
||||
rtn.y = workArea.y + minPadding;
|
||||
rtn.height = workArea.height - 2 * minPadding;
|
||||
} else if (spareHeight > 2 * targetPadding) {
|
||||
rtn.y = workArea.y + targetPadding;
|
||||
} else {
|
||||
rtn.y = workArea.y + Math.floor(spareHeight / 2);
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
|
||||
function computeWinBounds(waveWindow: WaveWindow): Electron.Rectangle {
|
||||
if (waveWindow.isnew) {
|
||||
return computeNewWinBounds(waveWindow);
|
||||
}
|
||||
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,
|
||||
};
|
||||
return winBounds;
|
||||
}
|
||||
|
||||
// note, this does not *show* the window.
|
||||
// to show, await win.readyPromise and then win.show()
|
||||
function createBrowserWindow(clientId: string, waveWindow: WaveWindow, fullConfig: FullConfigType): WaveBrowserWindow {
|
||||
let winBounds = computeWinBounds(waveWindow);
|
||||
winBounds = ensureBoundsAreVisible(winBounds);
|
||||
persistWindowBounds(waveWindow.oid, winBounds);
|
||||
const settings = fullConfig?.settings;
|
||||
const winOpts: Electron.BrowserWindowConstructorOptions = {
|
||||
titleBarStyle:
|
||||
unamePlatform === "darwin" ? "hiddenInset" : settings["window:nativetitlebar"] ? "default" : "hidden",
|
||||
titleBarOverlay:
|
||||
unamePlatform !== "darwin"
|
||||
? {
|
||||
symbolColor: "white",
|
||||
color: "#00000000",
|
||||
}
|
||||
: false,
|
||||
x: winBounds.x,
|
||||
y: winBounds.y,
|
||||
width: winBounds.width,
|
||||
height: winBounds.height,
|
||||
minWidth: 400,
|
||||
minHeight: 300,
|
||||
icon:
|
||||
unamePlatform == "linux"
|
||||
? path.join(getElectronAppBasePath(), "public/logos/wave-logo-dark.png")
|
||||
: undefined,
|
||||
webPreferences: {
|
||||
preload: path.join(getElectronAppBasePath(), "preload", "index.cjs"),
|
||||
webviewTag: true,
|
||||
},
|
||||
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 (unamePlatform) {
|
||||
case "win32": {
|
||||
winOpts.backgroundMaterial = "acrylic";
|
||||
break;
|
||||
}
|
||||
case "darwin": {
|
||||
winOpts.vibrancy = "fullscreen-ui";
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
winOpts.backgroundColor = "#222222";
|
||||
}
|
||||
const bwin = new electron.BrowserWindow(winOpts);
|
||||
(bwin as any).waveWindowId = waveWindow.oid;
|
||||
let readyResolve: (value: void) => void;
|
||||
(bwin as any).readyPromise = new Promise((resolve, _) => {
|
||||
readyResolve = resolve;
|
||||
});
|
||||
const win: WaveBrowserWindow = bwin as WaveBrowserWindow;
|
||||
const usp = new URLSearchParams();
|
||||
usp.set("clientid", clientId);
|
||||
usp.set("windowid", waveWindow.oid);
|
||||
const indexHtml = "index.html";
|
||||
if (isDevVite) {
|
||||
console.log("running as dev server");
|
||||
win.loadURL(`${process.env.ELECTRON_RENDERER_URL}/index.html?${usp.toString()}`);
|
||||
} else {
|
||||
console.log("running as file");
|
||||
win.loadFile(path.join(getElectronAppBasePath(), "frontend", indexHtml), { search: usp.toString() });
|
||||
}
|
||||
win.once("ready-to-show", () => {
|
||||
readyResolve();
|
||||
});
|
||||
win.webContents.on("will-navigate", shNavHandler);
|
||||
win.webContents.on("will-frame-navigate", shFrameNavHandler);
|
||||
win.webContents.on("did-attach-webview", (event, wc) => {
|
||||
wc.setWindowOpenHandler((details) => {
|
||||
win.webContents.send("webview-new-window", wc.id, details);
|
||||
return { action: "deny" };
|
||||
});
|
||||
});
|
||||
win.webContents.on("before-input-event", (e, input) => {
|
||||
const waveEvent = keyutil.adaptFromElectronKeyEvent(input);
|
||||
// console.log("WIN bie", waveEvent.type, waveEvent.code);
|
||||
handleCtrlShiftState(win.webContents, waveEvent);
|
||||
if (win.isFocused()) {
|
||||
wasActive = true;
|
||||
}
|
||||
});
|
||||
win.on(
|
||||
// @ts-expect-error
|
||||
"resize",
|
||||
debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win))
|
||||
);
|
||||
win.on(
|
||||
// @ts-expect-error
|
||||
"move",
|
||||
debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win))
|
||||
);
|
||||
win.on("focus", () => {
|
||||
wasInFg = true;
|
||||
wasActive = true;
|
||||
if (globalIsStarting) {
|
||||
return;
|
||||
}
|
||||
console.log("focus", waveWindow.oid);
|
||||
services.ClientService.FocusWindow(waveWindow.oid);
|
||||
});
|
||||
win.on("blur", () => {
|
||||
handleCtrlShiftFocus(win.webContents, false);
|
||||
});
|
||||
win.on("enter-full-screen", async () => {
|
||||
win.webContents.send("fullscreen-change", true);
|
||||
});
|
||||
win.on("leave-full-screen", async () => {
|
||||
win.webContents.send("fullscreen-change", false);
|
||||
});
|
||||
win.on("close", (e) => {
|
||||
if (globalIsQuitting || updater?.status == "installing") {
|
||||
return;
|
||||
}
|
||||
const numWindows = electron.BrowserWindow.getAllWindows().length;
|
||||
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();
|
||||
}
|
||||
});
|
||||
win.on("closed", () => {
|
||||
if (globalIsQuitting || updater?.status == "installing") {
|
||||
return;
|
||||
}
|
||||
const numWindows = electron.BrowserWindow.getAllWindows().length;
|
||||
if (numWindows == 0) {
|
||||
return;
|
||||
}
|
||||
services.WindowService.CloseWindow(waveWindow.oid);
|
||||
});
|
||||
win.webContents.on("zoom-changed", (e) => {
|
||||
win.webContents.send("zoom-changed");
|
||||
});
|
||||
win.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" };
|
||||
});
|
||||
configureAuthKeyRequestInjection(win.webContents.session);
|
||||
return win;
|
||||
}
|
||||
|
||||
function isWindowFullyVisible(bounds: electron.Rectangle): boolean {
|
||||
const displays = electron.screen.getAllDisplays();
|
||||
|
||||
// Helper function to check if a point is inside any display
|
||||
function isPointInDisplay(x: number, y: number) {
|
||||
for (const display of displays) {
|
||||
const { x: dx, y: dy, width, height } = display.bounds;
|
||||
if (x >= dx && x < dx + width && y >= dy && y < dy + height) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check all corners of the window
|
||||
const topLeft = isPointInDisplay(bounds.x, bounds.y);
|
||||
const topRight = isPointInDisplay(bounds.x + bounds.width, bounds.y);
|
||||
const bottomLeft = isPointInDisplay(bounds.x, bounds.y + bounds.height);
|
||||
const bottomRight = isPointInDisplay(bounds.x + bounds.width, bounds.y + bounds.height);
|
||||
|
||||
return topLeft && topRight && bottomLeft && bottomRight;
|
||||
}
|
||||
|
||||
function findDisplayWithMostArea(bounds: electron.Rectangle): electron.Display {
|
||||
const displays = electron.screen.getAllDisplays();
|
||||
let maxArea = 0;
|
||||
let bestDisplay = null;
|
||||
|
||||
for (let display of displays) {
|
||||
const { x, y, width, height } = display.bounds;
|
||||
const overlapX = Math.max(0, Math.min(bounds.x + bounds.width, x + width) - Math.max(bounds.x, x));
|
||||
const overlapY = Math.max(0, Math.min(bounds.y + bounds.height, y + height) - Math.max(bounds.y, y));
|
||||
const overlapArea = overlapX * overlapY;
|
||||
|
||||
if (overlapArea > maxArea) {
|
||||
maxArea = overlapArea;
|
||||
bestDisplay = display;
|
||||
}
|
||||
}
|
||||
|
||||
return bestDisplay;
|
||||
}
|
||||
|
||||
function adjustBoundsToFitDisplay(bounds: electron.Rectangle, display: electron.Display): electron.Rectangle {
|
||||
const { x: dx, y: dy, width: dWidth, height: dHeight } = display.workArea;
|
||||
let { x, y, width, height } = bounds;
|
||||
|
||||
// Adjust width and height to fit within the display's work area
|
||||
width = Math.min(width, dWidth);
|
||||
height = Math.min(height, dHeight);
|
||||
|
||||
// Adjust x to ensure the window fits within the display
|
||||
if (x < dx) {
|
||||
x = dx;
|
||||
} else if (x + width > dx + dWidth) {
|
||||
x = dx + dWidth - width;
|
||||
}
|
||||
|
||||
// Adjust y to ensure the window fits within the display
|
||||
if (y < dy) {
|
||||
y = dy;
|
||||
} else if (y + height > dy + dHeight) {
|
||||
y = dy + dHeight - height;
|
||||
}
|
||||
return { x, y, width, height };
|
||||
}
|
||||
|
||||
function ensureBoundsAreVisible(bounds: electron.Rectangle): electron.Rectangle {
|
||||
if (!isWindowFullyVisible(bounds)) {
|
||||
let targetDisplay = findDisplayWithMostArea(bounds);
|
||||
|
||||
if (!targetDisplay) {
|
||||
targetDisplay = electron.screen.getPrimaryDisplay();
|
||||
}
|
||||
|
||||
return adjustBoundsToFitDisplay(bounds, targetDisplay);
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
|
||||
// Listen for the open-external event from the renderer process
|
||||
electron.ipcMain.on("open-external", (event, url) => {
|
||||
if (url && typeof url === "string") {
|
||||
@ -712,7 +219,7 @@ function getUrlInSession(session: Electron.Session, url: string): Promise<UrlInS
|
||||
|
||||
electron.ipcMain.on("webview-image-contextmenu", (event: electron.IpcMainEvent, payload: { src: string }) => {
|
||||
const menu = new electron.Menu();
|
||||
const win = electron.BrowserWindow.fromWebContents(event.sender.hostWebContents);
|
||||
const win = getWaveWindowByWebContentsId(event.sender.hostWebContents.id);
|
||||
if (win == null) {
|
||||
return;
|
||||
}
|
||||
@ -733,19 +240,66 @@ electron.ipcMain.on("webview-image-contextmenu", (event: electron.IpcMainEvent,
|
||||
);
|
||||
const { x, y } = electron.screen.getCursorScreenPoint();
|
||||
const windowPos = win.getPosition();
|
||||
menu.popup({ window: win, x: x - windowPos[0], y: y - windowPos[1] });
|
||||
menu.popup();
|
||||
});
|
||||
|
||||
electron.ipcMain.on("download", (event, payload) => {
|
||||
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
||||
const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?path=" + encodeURIComponent(payload.filePath);
|
||||
window.webContents.downloadURL(streamingUrl);
|
||||
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 window = electron.BrowserWindow.fromWebContents(event.sender);
|
||||
const tabView = getWaveTabViewByWebContentsId(event.sender.id);
|
||||
if (tabView == null) {
|
||||
event.returnValue = null;
|
||||
return;
|
||||
}
|
||||
const screenPoint = electron.screen.getCursorScreenPoint();
|
||||
const windowRect = window.getContentBounds();
|
||||
const windowRect = tabView.getBounds();
|
||||
const retVal: Electron.Point = {
|
||||
x: screenPoint.x - windowRect.x,
|
||||
y: screenPoint.y - windowRect.y,
|
||||
@ -758,7 +312,7 @@ electron.ipcMain.on("get-env", (event, varName) => {
|
||||
});
|
||||
|
||||
electron.ipcMain.on("get-about-modal-details", (event) => {
|
||||
event.returnValue = { version: WaveVersion, buildTime: WaveBuildTime } as AboutModalDetails;
|
||||
event.returnValue = getWaveVersion() as AboutModalDetails;
|
||||
});
|
||||
|
||||
const hasBeforeInputRegisteredMap = new Map<number, boolean>();
|
||||
@ -825,8 +379,8 @@ if (unamePlatform !== "darwin") {
|
||||
const overlayBuffer = overlay.toPNG();
|
||||
const png = PNG.sync.read(overlayBuffer);
|
||||
const color = fac.prepareResult(fac.getColorFromArray4(png.data));
|
||||
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
||||
window.setTitleBarOverlay({
|
||||
const ww = getWaveWindowByWebContentsId(event.sender.id);
|
||||
ww.setTitleBarOverlay({
|
||||
color: unamePlatform === "linux" ? color.rgba : "#00000000", // Windows supports a true transparent overlay, so we don't need to set a background color.
|
||||
symbolColor: color.isDark ? "white" : "black",
|
||||
});
|
||||
@ -848,13 +402,14 @@ async function createNewWaveWindow(): Promise<void> {
|
||||
const clientData = await services.ClientService.GetClientData();
|
||||
const fullConfig = await services.FileService.GetFullConfig();
|
||||
let recreatedWindow = false;
|
||||
if (electron.BrowserWindow.getAllWindows().length === 0 && clientData?.windowids?.length >= 1) {
|
||||
const allWindows = getAllWaveWindows();
|
||||
if (allWindows.length === 0 && clientData?.windowids?.length >= 1) {
|
||||
// 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);
|
||||
await win.readyPromise;
|
||||
const win = createBrowserWindow(clientData.oid, existingWindowData, fullConfig, { unamePlatform });
|
||||
await win.waveReadyPromise;
|
||||
win.show();
|
||||
recreatedWindow = true;
|
||||
}
|
||||
@ -863,16 +418,37 @@ async function createNewWaveWindow(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
const newWindow = await services.ClientService.MakeWindow();
|
||||
const newBrowserWindow = createBrowserWindow(clientData.oid, newWindow, fullConfig);
|
||||
await newBrowserWindow.readyPromise;
|
||||
const newBrowserWindow = createBrowserWindow(clientData.oid, newWindow, fullConfig, { unamePlatform });
|
||||
await newBrowserWindow.waveReadyPromise;
|
||||
newBrowserWindow.show();
|
||||
}
|
||||
|
||||
electron.ipcMain.on("set-window-init-status", (event, status: "ready" | "wave-ready") => {
|
||||
const tabView = getWaveTabViewByWebContentsId(event.sender.id);
|
||||
if (tabView == null || tabView.initResolve == null) {
|
||||
return;
|
||||
}
|
||||
if (status === "ready") {
|
||||
console.log("initResolve");
|
||||
tabView.initResolve();
|
||||
if (tabView.savedInitOpts) {
|
||||
tabView.webContents.send("wave-init", tabView.savedInitOpts);
|
||||
}
|
||||
} else if (status === "wave-ready") {
|
||||
console.log("waveReadyResolve");
|
||||
tabView.waveReadyResolve();
|
||||
}
|
||||
});
|
||||
|
||||
electron.ipcMain.on("fe-log", (event, logStr: string) => {
|
||||
console.log("fe-log", logStr);
|
||||
});
|
||||
|
||||
function saveImageFileWithNativeDialog(defaultFileName: string, mimeType: string, readStream: Readable) {
|
||||
if (defaultFileName == null || defaultFileName == "") {
|
||||
defaultFileName = "image";
|
||||
}
|
||||
const window = electron.BrowserWindow.getFocusedWindow(); // Get the current window context
|
||||
const ww = getFocusedWaveWindow();
|
||||
const mimeToExtension: { [key: string]: string } = {
|
||||
"image/png": "png",
|
||||
"image/jpeg": "jpg",
|
||||
@ -891,7 +467,7 @@ function saveImageFileWithNativeDialog(defaultFileName: string, mimeType: string
|
||||
}
|
||||
defaultFileName = addExtensionIfNeeded(defaultFileName, mimeType);
|
||||
electron.dialog
|
||||
.showSaveDialog(window, {
|
||||
.showSaveDialog(ww, {
|
||||
title: "Save Image",
|
||||
defaultPath: defaultFileName,
|
||||
filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "gif", "webp", "bmp", "tiff", "heic"] }],
|
||||
@ -922,20 +498,19 @@ function saveImageFileWithNativeDialog(defaultFileName: string, mimeType: string
|
||||
electron.ipcMain.on("open-new-window", () => fireAndForget(createNewWaveWindow));
|
||||
|
||||
electron.ipcMain.on("contextmenu-show", (event, menuDefArr?: ElectronContextMenuItem[]) => {
|
||||
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
||||
if (menuDefArr?.length === 0) {
|
||||
return;
|
||||
}
|
||||
const menu = menuDefArr ? convertMenuDefArrToMenu(menuDefArr) : instantiateAppMenu();
|
||||
const { x, y } = electron.screen.getCursorScreenPoint();
|
||||
const windowPos = window.getPosition();
|
||||
|
||||
menu.popup({ window, x: x - windowPos[0], y: y - windowPos[1] });
|
||||
// const { x, y } = electron.screen.getCursorScreenPoint();
|
||||
// const windowPos = window.getPosition();
|
||||
menu.popup();
|
||||
event.returnValue = true;
|
||||
});
|
||||
|
||||
async function logActiveState() {
|
||||
const activeState = { fg: wasInFg, active: wasActive, open: true };
|
||||
const astate = getActivityState();
|
||||
const activeState = { fg: astate.wasInFg, active: astate.wasActive, open: true };
|
||||
const url = new URL(getWebServerEndpoint() + "/wave/log-active-state");
|
||||
try {
|
||||
const resp = await fetch(url, { method: "post", body: JSON.stringify(activeState) });
|
||||
@ -947,8 +522,9 @@ async function logActiveState() {
|
||||
console.log("error logging active state", e);
|
||||
} finally {
|
||||
// for next iteration
|
||||
wasInFg = electron.BrowserWindow.getFocusedWindow()?.isFocused() ?? false;
|
||||
wasActive = false;
|
||||
const ww = getFocusedWaveWindow();
|
||||
setWasInFg(ww?.isFocused() ?? false);
|
||||
setWasActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -966,7 +542,9 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
|
||||
label: menuDef.label,
|
||||
type: menuDef.type,
|
||||
click: (_, window) => {
|
||||
(window as electron.BrowserWindow)?.webContents?.send("contextmenu-click", menuDef.id);
|
||||
const ww = window as WaveBrowserWindow;
|
||||
const tabView = ww.activeTabView;
|
||||
tabView?.webContents?.send("contextmenu-click", menuDef.id);
|
||||
},
|
||||
checked: menuDef.checked,
|
||||
};
|
||||
@ -980,7 +558,11 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
|
||||
}
|
||||
|
||||
function instantiateAppMenu(): electron.Menu {
|
||||
return getAppMenu({ createNewWaveWindow, relaunchBrowserWindows });
|
||||
return getAppMenu({
|
||||
createNewWaveWindow,
|
||||
relaunchBrowserWindows,
|
||||
getLastFocusedWaveWindow: getLastFocusedWaveWindow,
|
||||
});
|
||||
}
|
||||
|
||||
function makeAppMenu() {
|
||||
@ -989,7 +571,7 @@ function makeAppMenu() {
|
||||
}
|
||||
|
||||
electronApp.on("window-all-closed", () => {
|
||||
if (globalIsRelaunching) {
|
||||
if (getGlobalIsRelaunching()) {
|
||||
return;
|
||||
}
|
||||
if (unamePlatform !== "darwin") {
|
||||
@ -997,32 +579,32 @@ electronApp.on("window-all-closed", () => {
|
||||
}
|
||||
});
|
||||
electronApp.on("before-quit", (e) => {
|
||||
globalIsQuitting = true;
|
||||
setGlobalIsQuitting(true);
|
||||
updater?.stop();
|
||||
if (unamePlatform == "win32") {
|
||||
// win32 doesn't have a SIGINT, so we just let electron die, which
|
||||
// ends up killing wavesrv via closing it's stdin.
|
||||
return;
|
||||
}
|
||||
waveSrvProc?.kill("SIGINT");
|
||||
getWaveSrvProc()?.kill("SIGINT");
|
||||
shutdownWshrpc();
|
||||
if (forceQuit) {
|
||||
if (getForceQuit()) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
const allWindows = electron.BrowserWindow.getAllWindows();
|
||||
const allWindows = getAllWaveWindows();
|
||||
for (const window of allWindows) {
|
||||
window.hide();
|
||||
}
|
||||
if (isWaveSrvDead) {
|
||||
if (getIsWaveSrvDead()) {
|
||||
console.log("wavesrv is dead, quitting immediately");
|
||||
forceQuit = true;
|
||||
setForceQuit(true);
|
||||
electronApp.quit();
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
console.log("waiting for wavesrv to exit...");
|
||||
forceQuit = true;
|
||||
setForceQuit(true);
|
||||
electronApp.quit();
|
||||
}, 3000);
|
||||
});
|
||||
@ -1051,13 +633,13 @@ process.on("uncaughtException", (error) => {
|
||||
});
|
||||
|
||||
async function relaunchBrowserWindows(): Promise<void> {
|
||||
globalIsRelaunching = true;
|
||||
const windows = electron.BrowserWindow.getAllWindows();
|
||||
setGlobalIsRelaunching(true);
|
||||
const windows = getAllWaveWindows();
|
||||
for (const window of windows) {
|
||||
window.removeAllListeners();
|
||||
window.close();
|
||||
}
|
||||
globalIsRelaunching = false;
|
||||
setGlobalIsRelaunching(false);
|
||||
|
||||
const clientData = await services.ClientService.GetClientData();
|
||||
const fullConfig = await services.FileService.GetFullConfig();
|
||||
@ -1065,16 +647,16 @@ async function relaunchBrowserWindows(): Promise<void> {
|
||||
for (const windowId of clientData.windowids.slice().reverse()) {
|
||||
const windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow;
|
||||
if (windowData == null) {
|
||||
services.WindowService.CloseWindow(windowId).catch((e) => {
|
||||
services.WindowService.CloseWindow(windowId, true).catch((e) => {
|
||||
/* ignore */
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const win = createBrowserWindow(clientData.oid, windowData, fullConfig);
|
||||
const win = createBrowserWindow(clientData.oid, windowData, fullConfig, { unamePlatform });
|
||||
wins.push(win);
|
||||
}
|
||||
for (const win of wins) {
|
||||
await win.readyPromise;
|
||||
await win.waveReadyPromise;
|
||||
console.log("show", win.waveWindowId);
|
||||
win.show();
|
||||
}
|
||||
@ -1087,7 +669,6 @@ async function appMain() {
|
||||
console.log("disabling hardware acceleration, per launch settings");
|
||||
electronApp.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
const startTs = Date.now();
|
||||
const instanceLock = electronApp.requestSingleInstanceLock();
|
||||
if (!instanceLock) {
|
||||
@ -1101,14 +682,16 @@ async function appMain() {
|
||||
}
|
||||
makeAppMenu();
|
||||
try {
|
||||
await runWaveSrv();
|
||||
await runWaveSrv(handleWSEvent);
|
||||
} catch (e) {
|
||||
console.log(e.toString());
|
||||
}
|
||||
const ready = await waveSrvReady;
|
||||
const ready = await getWaveSrvReady();
|
||||
console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms");
|
||||
await electronApp.whenReady();
|
||||
configureAuthKeyRequestInjection(electron.session.defaultSession);
|
||||
const fullConfig = await services.FileService.GetFullConfig();
|
||||
ensureHotSpareTab(fullConfig);
|
||||
await relaunchBrowserWindows();
|
||||
await initDocsite();
|
||||
setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe
|
||||
@ -1120,10 +703,14 @@ async function appMain() {
|
||||
}
|
||||
await configureAutoUpdater();
|
||||
|
||||
globalIsStarting = false;
|
||||
setGlobalIsStarting(false);
|
||||
if (fullConfig?.settings?.["window:maxtabcachesize"] != null) {
|
||||
setMaxTabCacheSize(fullConfig.settings["window:maxtabcachesize"]);
|
||||
}
|
||||
|
||||
electronApp.on("activate", async () => {
|
||||
if (electron.BrowserWindow.getAllWindows().length === 0) {
|
||||
const allWindows = getAllWaveWindows();
|
||||
if (allWindows.length === 0) {
|
||||
await createNewWaveWindow();
|
||||
}
|
||||
});
|
||||
|
@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as electron from "electron";
|
||||
import { clearTabCache, getFocusedWaveWindow } from "emain/emain-viewmgr";
|
||||
import { fireAndForget } from "../frontend/util/util";
|
||||
import { unamePlatform } from "./platform";
|
||||
import { updater } from "./updater";
|
||||
@ -9,14 +10,19 @@ import { updater } from "./updater";
|
||||
type AppMenuCallbacks = {
|
||||
createNewWaveWindow: () => Promise<void>;
|
||||
relaunchBrowserWindows: () => Promise<void>;
|
||||
getLastFocusedWaveWindow: () => WaveBrowserWindow;
|
||||
};
|
||||
|
||||
function getWindowWebContents(window: electron.BaseWindow): electron.WebContents {
|
||||
if (window == null) {
|
||||
return null;
|
||||
}
|
||||
if (window instanceof electron.BrowserWindow) {
|
||||
return window.webContents;
|
||||
if (window instanceof electron.BaseWindow) {
|
||||
const waveWin = window as WaveBrowserWindow;
|
||||
if (waveWin.activeTabView) {
|
||||
return waveWin.activeTabView.webContents;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -32,7 +38,7 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu {
|
||||
role: "close",
|
||||
accelerator: "", // clear the accelerator
|
||||
click: () => {
|
||||
electron.BrowserWindow.getFocusedWindow()?.close();
|
||||
getFocusedWaveWindow()?.close();
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -112,9 +118,14 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu {
|
||||
},
|
||||
];
|
||||
|
||||
const devToolsAccel = unamePlatform === "darwin" ? "Option+Command+I" : "Alt+Meta+I";
|
||||
const viewMenu: Electron.MenuItemConstructorOptions[] = [
|
||||
{
|
||||
role: "forceReload",
|
||||
label: "Reload Tab",
|
||||
accelerator: "Shift+CommandOrControl+R",
|
||||
click: (_, window) => {
|
||||
getWindowWebContents(window)?.reloadIgnoringCache();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Relaunch All Windows",
|
||||
@ -123,7 +134,18 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu {
|
||||
},
|
||||
},
|
||||
{
|
||||
role: "toggleDevTools",
|
||||
label: "Clear Tab Cache",
|
||||
click: () => {
|
||||
clearTabCache();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Toggle DevTools",
|
||||
accelerator: devToolsAccel,
|
||||
click: (_, window) => {
|
||||
let wc = getWindowWebContents(window);
|
||||
wc?.toggleDevTools();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
@ -143,6 +165,9 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu {
|
||||
if (wc == null) {
|
||||
return;
|
||||
}
|
||||
if (wc.getZoomFactor() >= 5) {
|
||||
return;
|
||||
}
|
||||
wc.setZoomFactor(wc.getZoomFactor() + 0.2);
|
||||
},
|
||||
},
|
||||
@ -154,6 +179,9 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu {
|
||||
if (wc == null) {
|
||||
return;
|
||||
}
|
||||
if (wc.getZoomFactor() >= 5) {
|
||||
return;
|
||||
}
|
||||
wc.setZoomFactor(wc.getZoomFactor() + 0.2);
|
||||
},
|
||||
visible: false,
|
||||
@ -167,9 +195,28 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu {
|
||||
if (wc == null) {
|
||||
return;
|
||||
}
|
||||
if (wc.getZoomFactor() <= 0.2) {
|
||||
return;
|
||||
}
|
||||
wc.setZoomFactor(wc.getZoomFactor() - 0.2);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Zoom Out (hidden)",
|
||||
accelerator: "CommandOrControl+Shift+-",
|
||||
click: (_, window) => {
|
||||
const wc = getWindowWebContents(window);
|
||||
if (wc == null) {
|
||||
return;
|
||||
}
|
||||
if (wc.getZoomFactor() <= 0.2) {
|
||||
return;
|
||||
}
|
||||
wc.setZoomFactor(wc.getZoomFactor() - 0.2);
|
||||
},
|
||||
visible: false,
|
||||
acceleratorWorksWhenHidden: true,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
|
@ -38,6 +38,12 @@ contextBridge.exposeInMainWorld("api", {
|
||||
registerGlobalWebviewKeys: (keys) => ipcRenderer.send("register-global-webview-keys", keys),
|
||||
onControlShiftStateUpdate: (callback) =>
|
||||
ipcRenderer.on("control-shift-state-update", (_event, state) => callback(state)),
|
||||
setActiveTab: (tabId) => ipcRenderer.send("set-active-tab", tabId),
|
||||
createTab: () => ipcRenderer.send("create-tab"),
|
||||
closeTab: (tabId) => ipcRenderer.send("close-tab", tabId),
|
||||
setWindowInitStatus: (status) => ipcRenderer.send("set-window-init-status", status),
|
||||
onWaveInit: (callback) => ipcRenderer.on("wave-init", (_event, initOpts) => callback(initOpts)),
|
||||
sendLog: (log) => ipcRenderer.send("fe-log", log),
|
||||
onQuicklook: (filePath: string) => ipcRenderer.send("quicklook", filePath),
|
||||
});
|
||||
|
||||
|
@ -2,8 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { BrowserWindow, dialog, ipcMain, Notification } from "electron";
|
||||
import { dialog, ipcMain, Notification } from "electron";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { getAllWaveWindows, getFocusedWaveWindow } from "emain/emain-viewmgr";
|
||||
import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
import YAML from "yaml";
|
||||
@ -109,8 +110,11 @@ export class Updater {
|
||||
|
||||
private set status(value: UpdaterStatus) {
|
||||
this._status = value;
|
||||
BrowserWindow.getAllWindows().forEach((window) => {
|
||||
window.webContents.send("app-update-status", value);
|
||||
getAllWaveWindows().forEach((window) => {
|
||||
const allTabs = Array.from(window.allTabViews.values());
|
||||
allTabs.forEach((tab) => {
|
||||
tab.webContents.send("app-update-status", value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -159,7 +163,7 @@ export class Updater {
|
||||
type: "info",
|
||||
message: "There are currently no updates available.",
|
||||
};
|
||||
dialog.showMessageBox(BrowserWindow.getFocusedWindow(), dialogOpts);
|
||||
dialog.showMessageBox(getFocusedWaveWindow(), dialogOpts);
|
||||
}
|
||||
|
||||
// Only update the last check time if this is an automatic check. This ensures the interval remains consistent.
|
||||
@ -179,15 +183,14 @@ export class Updater {
|
||||
detail: "A new version has been downloaded. Restart the application to apply the updates.",
|
||||
};
|
||||
|
||||
const allWindows = BrowserWindow.getAllWindows();
|
||||
const allWindows = getAllWaveWindows();
|
||||
if (allWindows.length > 0) {
|
||||
await dialog
|
||||
.showMessageBox(BrowserWindow.getFocusedWindow() ?? allWindows[0], dialogOpts)
|
||||
.then(({ response }) => {
|
||||
if (response === 0) {
|
||||
this.installUpdate();
|
||||
}
|
||||
});
|
||||
const focusedWindow = getFocusedWaveWindow();
|
||||
await dialog.showMessageBox(focusedWindow ?? allWindows[0], dialogOpts).then(({ response }) => {
|
||||
if (response === 0) {
|
||||
this.installUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||
import * as util from "@/util/util";
|
||||
import useResizeObserver from "@react-hook/resize-observer";
|
||||
@ -72,7 +75,7 @@ function processBackgroundUrls(cssText: string): string {
|
||||
|
||||
export function AppBackground() {
|
||||
const bgRef = useRef<HTMLDivElement>(null);
|
||||
const tabId = useAtomValue(atoms.activeTabId);
|
||||
const tabId = useAtomValue(atoms.staticTabId);
|
||||
const [tabData] = useWaveObjectValue<Tab>(WOS.makeORef("tab", tabId));
|
||||
const bgAttr = tabData?.meta?.bg;
|
||||
const style: CSSProperties = {};
|
||||
|
@ -18,6 +18,10 @@ body {
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.is-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a.plain-link {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
@ -23,7 +23,10 @@ import { CenteredDiv } from "./element/quickelems";
|
||||
const dlog = debug("wave:app");
|
||||
const focusLog = debug("wave:focus");
|
||||
|
||||
const App = () => {
|
||||
const App = ({ onFirstRender }: { onFirstRender: () => void }) => {
|
||||
useEffect(() => {
|
||||
onFirstRender();
|
||||
}, []);
|
||||
return (
|
||||
<Provider store={globalStore}>
|
||||
<AppInner />
|
||||
@ -115,18 +118,20 @@ function AppSettingsUpdater() {
|
||||
(windowSettings?.["window:transparent"] || windowSettings?.["window:blur"]) ?? false;
|
||||
const opacity = util.boundNumber(windowSettings?.["window:opacity"] ?? 0.8, 0, 1);
|
||||
let baseBgColor = windowSettings?.["window:bgcolor"];
|
||||
let mainDiv = document.getElementById("main");
|
||||
// console.log("window settings", windowSettings, isTransparentOrBlur, opacity, baseBgColor, mainDiv);
|
||||
if (isTransparentOrBlur) {
|
||||
document.body.classList.add("is-transparent");
|
||||
mainDiv.classList.add("is-transparent");
|
||||
const rootStyles = getComputedStyle(document.documentElement);
|
||||
if (baseBgColor == null) {
|
||||
baseBgColor = rootStyles.getPropertyValue("--main-bg-color").trim();
|
||||
}
|
||||
const color = new Color(baseBgColor);
|
||||
const rgbaColor = color.alpha(opacity).string();
|
||||
document.body.style.backgroundColor = rgbaColor;
|
||||
mainDiv.style.backgroundColor = rgbaColor;
|
||||
} else {
|
||||
document.body.classList.remove("is-transparent");
|
||||
document.body.style.opacity = null;
|
||||
mainDiv.classList.remove("is-transparent");
|
||||
mainDiv.style.opacity = null;
|
||||
}
|
||||
}, [windowSettings]);
|
||||
return null;
|
||||
|
@ -28,7 +28,7 @@ import {
|
||||
} from "@/app/store/global";
|
||||
import * as services from "@/app/store/services";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { WindowRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { ErrorBoundary } from "@/element/errorboundary";
|
||||
import { IconButton } from "@/element/iconbutton";
|
||||
import { MagnifyIcon } from "@/element/magnify";
|
||||
@ -63,7 +63,7 @@ function handleHeaderContextMenu(
|
||||
{
|
||||
label: "Move to New Window",
|
||||
click: () => {
|
||||
const currentTabId = globalStore.get(atoms.activeTabId);
|
||||
const currentTabId = globalStore.get(atoms.staticTabId);
|
||||
try {
|
||||
services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid);
|
||||
} catch (e) {
|
||||
@ -321,7 +321,7 @@ const ConnStatusOverlay = React.memo(
|
||||
}, [width, connStatus, setShowError]);
|
||||
|
||||
const handleTryReconnect = React.useCallback(() => {
|
||||
const prtn = RpcApi.ConnConnectCommand(WindowRpcClient, connName, { timeout: 60000 });
|
||||
const prtn = RpcApi.ConnConnectCommand(TabRpcClient, connName, { timeout: 60000 });
|
||||
prtn.catch((e) => console.log("error reconnecting", connName, e));
|
||||
}, [connName]);
|
||||
|
||||
@ -437,7 +437,7 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
||||
const connName = blockData?.meta?.connection;
|
||||
if (!util.isBlank(connName)) {
|
||||
console.log("ensure conn", nodeModel.blockId, connName);
|
||||
RpcApi.ConnEnsureCommand(WindowRpcClient, connName, { timeout: 60000 }).catch((e) => {
|
||||
RpcApi.ConnEnsureCommand(TabRpcClient, connName, { timeout: 60000 }).catch((e) => {
|
||||
console.log("error ensuring connection", nodeModel.blockId, connName, e);
|
||||
});
|
||||
}
|
||||
@ -536,7 +536,7 @@ const ChangeConnectionBlockModal = React.memo(
|
||||
setConnList([]);
|
||||
return;
|
||||
}
|
||||
const prtn = RpcApi.ConnListCommand(WindowRpcClient, { timeout: 2000 });
|
||||
const prtn = RpcApi.ConnListCommand(TabRpcClient, { timeout: 2000 });
|
||||
prtn.then((newConnList) => {
|
||||
setConnList(newConnList ?? []);
|
||||
}).catch((e) => console.log("unable to load conn list from backend. using blank list: ", e));
|
||||
@ -557,12 +557,12 @@ const ChangeConnectionBlockModal = React.memo(
|
||||
} else {
|
||||
newCwd = "~";
|
||||
}
|
||||
await RpcApi.SetMetaCommand(WindowRpcClient, {
|
||||
await RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("block", blockId),
|
||||
meta: { connection: connName, file: newCwd },
|
||||
});
|
||||
try {
|
||||
await RpcApi.ConnEnsureCommand(WindowRpcClient, connName, { timeout: 60000 });
|
||||
await RpcApi.ConnEnsureCommand(TabRpcClient, connName, { timeout: 60000 });
|
||||
} catch (e) {
|
||||
console.log("error connecting", blockId, connName, e);
|
||||
}
|
||||
@ -608,7 +608,7 @@ const ChangeConnectionBlockModal = React.memo(
|
||||
label: `Reconnect to ${connStatus.connection}`,
|
||||
value: "",
|
||||
onSelect: async (_: string) => {
|
||||
const prtn = RpcApi.ConnConnectCommand(WindowRpcClient, connStatus.connection, { timeout: 60000 });
|
||||
const prtn = RpcApi.ConnConnectCommand(TabRpcClient, connStatus.connection, { timeout: 60000 });
|
||||
prtn.catch((e) => console.log("error reconnecting", connStatus.connection, e));
|
||||
},
|
||||
};
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
import { CopyButton } from "@/app/element/copybutton";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { WindowRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||
import { isBlank, makeConnRoute, useAtomValueSafe } from "@/util/util";
|
||||
import { clsx } from "clsx";
|
||||
@ -143,7 +143,7 @@ const MarkdownImg = ({
|
||||
}
|
||||
const resolveFn = async () => {
|
||||
const route = makeConnRoute(resolveOpts.connName);
|
||||
const fileInfo = await RpcApi.RemoteFileJoinCommand(WindowRpcClient, [resolveOpts.baseDir, props.src], {
|
||||
const fileInfo = await RpcApi.RemoteFileJoinCommand(TabRpcClient, [resolveOpts.baseDir, props.src], {
|
||||
route: route,
|
||||
});
|
||||
const usp = new URLSearchParams();
|
||||
|
@ -1,3 +1,6 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { debounce } from "throttle-debounce";
|
||||
@ -56,6 +59,49 @@ export function useDimensionsWithCallbackRef<T extends HTMLElement>(
|
||||
return [refCallback, ref, domRect];
|
||||
}
|
||||
|
||||
export function useOnResize<T extends HTMLElement>(
|
||||
ref: React.RefObject<T>,
|
||||
callback: (domRect: DOMRectReadOnly) => void,
|
||||
debounceMs: number = null
|
||||
) {
|
||||
const isFirst = React.useRef(true);
|
||||
const rszObjRef = React.useRef<ResizeObserver>(null);
|
||||
const oldHtmlElem = React.useRef<T>(null);
|
||||
const setDomRectDebounced = React.useCallback(debounceMs == null ? callback : debounce(debounceMs, callback), [
|
||||
debounceMs,
|
||||
callback,
|
||||
]);
|
||||
React.useEffect(() => {
|
||||
if (!rszObjRef.current) {
|
||||
rszObjRef.current = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (isFirst.current) {
|
||||
isFirst.current = false;
|
||||
callback(entry.contentRect);
|
||||
} else {
|
||||
setDomRectDebounced(entry.contentRect);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (ref.current) {
|
||||
rszObjRef.current.observe(ref.current);
|
||||
oldHtmlElem.current = ref.current;
|
||||
}
|
||||
return () => {
|
||||
if (oldHtmlElem.current) {
|
||||
rszObjRef.current?.unobserve(oldHtmlElem.current);
|
||||
oldHtmlElem.current = null;
|
||||
}
|
||||
};
|
||||
}, [ref.current, callback]);
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
rszObjRef.current?.disconnect();
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
// will not react to ref changes
|
||||
// pass debounceMs of null to not debounce
|
||||
export function useDimensionsWithExistingRef<T extends HTMLElement>(
|
||||
|
@ -2,12 +2,12 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import {
|
||||
getLayoutModelForActiveTab,
|
||||
getLayoutModelForTabById,
|
||||
LayoutTreeActionType,
|
||||
LayoutTreeInsertNodeAction,
|
||||
newLayoutNode,
|
||||
} from "@/layout/index";
|
||||
import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks";
|
||||
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||
import { fetch } from "@/util/fetchutil";
|
||||
import { getPrefixedSettings, isBlank } from "@/util/util";
|
||||
@ -26,6 +26,7 @@ const Counters = new Map<string, number>();
|
||||
const ConnStatusMap = new Map<string, PrimitiveAtom<ConnStatus>>();
|
||||
|
||||
type GlobalInitOptions = {
|
||||
tabId: string;
|
||||
platform: NodeJS.Platform;
|
||||
windowId: string;
|
||||
clientId: string;
|
||||
@ -46,10 +47,9 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
|
||||
const windowIdAtom = atom(initOpts.windowId) as PrimitiveAtom<string>;
|
||||
const clientIdAtom = atom(initOpts.clientId) as PrimitiveAtom<string>;
|
||||
const uiContextAtom = atom((get) => {
|
||||
const windowData = get(windowDataAtom);
|
||||
const uiContext: UIContext = {
|
||||
windowid: get(atoms.windowId),
|
||||
activetabid: windowData?.activetabid,
|
||||
windowid: initOpts.windowId,
|
||||
activetabid: initOpts.tabId,
|
||||
};
|
||||
return uiContext;
|
||||
}) as Atom<UIContext>;
|
||||
@ -99,18 +99,10 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
|
||||
return get(fullConfigAtom)?.settings ?? {};
|
||||
}) as Atom<SettingsType>;
|
||||
const tabAtom: Atom<Tab> = atom((get) => {
|
||||
const windowData = get(windowDataAtom);
|
||||
if (windowData == null) {
|
||||
return null;
|
||||
}
|
||||
return WOS.getObjectValue(WOS.makeORef("tab", windowData.activetabid), get);
|
||||
return WOS.getObjectValue(WOS.makeORef("tab", initOpts.tabId), get);
|
||||
});
|
||||
const activeTabIdAtom: Atom<string> = atom((get) => {
|
||||
const windowData = get(windowDataAtom);
|
||||
if (windowData == null) {
|
||||
return null;
|
||||
}
|
||||
return windowData.activetabid;
|
||||
const staticTabIdAtom: Atom<string> = atom((get) => {
|
||||
return initOpts.tabId;
|
||||
});
|
||||
const controlShiftDelayAtom = atom(false);
|
||||
const updaterStatusAtom = atom<UpdaterStatus>("up-to-date") as PrimitiveAtom<UpdaterStatus>;
|
||||
@ -151,7 +143,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
|
||||
const flashErrorsAtom = atom<FlashErrorType[]>([]);
|
||||
atoms = {
|
||||
// initialized in wave.ts (will not be null inside of application)
|
||||
windowId: windowIdAtom,
|
||||
clientId: clientIdAtom,
|
||||
uiContext: uiContextAtom,
|
||||
client: clientAtom,
|
||||
@ -160,7 +151,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
|
||||
fullConfigAtom,
|
||||
settingsAtom,
|
||||
tabAtom,
|
||||
activeTabId: activeTabIdAtom,
|
||||
staticTabId: staticTabIdAtom,
|
||||
isFullScreen: isFullScreenAtom,
|
||||
controlShiftDelayAtom,
|
||||
updaterStatusAtom,
|
||||
@ -301,8 +292,8 @@ async function createBlock(blockDef: BlockDef, magnified = false): Promise<strin
|
||||
magnified,
|
||||
focused: true,
|
||||
};
|
||||
const activeTabId = globalStore.get(atoms.uiContext).activetabid;
|
||||
const layoutModel = getLayoutModelForTabById(activeTabId);
|
||||
const tabId = globalStore.get(atoms.staticTabId);
|
||||
const layoutModel = getLayoutModelForTabById(tabId);
|
||||
layoutModel.treeReducer(insertNodeAction);
|
||||
return blockId;
|
||||
}
|
||||
@ -339,7 +330,7 @@ async function fetchWaveFile(
|
||||
}
|
||||
|
||||
function setNodeFocus(nodeId: string) {
|
||||
const layoutModel = getLayoutModelForActiveTab();
|
||||
const layoutModel = getLayoutModelForStaticTab();
|
||||
layoutModel.focusNode(nodeId);
|
||||
}
|
||||
|
||||
@ -414,7 +405,7 @@ function refocusNode(blockId: string) {
|
||||
if (blockId == null) {
|
||||
return;
|
||||
}
|
||||
const layoutModel = getLayoutModelForActiveTab();
|
||||
const layoutModel = getLayoutModelForStaticTab();
|
||||
const layoutNodeId = layoutModel.getNodeByBlockId(blockId);
|
||||
if (layoutNodeId?.id == null) {
|
||||
return;
|
||||
@ -522,12 +513,17 @@ function removeFlashError(id: string) {
|
||||
});
|
||||
}
|
||||
|
||||
async function createTab(): Promise<void> {
|
||||
await getApi().createTab();
|
||||
}
|
||||
|
||||
export {
|
||||
atoms,
|
||||
counterInc,
|
||||
countersClear,
|
||||
countersPrint,
|
||||
createBlock,
|
||||
createTab,
|
||||
fetchWaveFile,
|
||||
getApi,
|
||||
getBlockComponentModel,
|
||||
|
@ -1,24 +1,32 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { atoms, createBlock, getApi, getBlockComponentModel, globalStore, refocusNode, WOS } from "@/app/store/global";
|
||||
import * as services from "@/app/store/services";
|
||||
import {
|
||||
atoms,
|
||||
createBlock,
|
||||
createTab,
|
||||
getApi,
|
||||
getBlockComponentModel,
|
||||
globalStore,
|
||||
refocusNode,
|
||||
WOS,
|
||||
} from "@/app/store/global";
|
||||
import {
|
||||
deleteLayoutModelForTab,
|
||||
getLayoutModelForActiveTab,
|
||||
getLayoutModelForTab,
|
||||
getLayoutModelForTabById,
|
||||
NavigateDirection,
|
||||
} from "@/layout/index";
|
||||
import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks";
|
||||
import * as keyutil from "@/util/keyutil";
|
||||
import * as jotai from "jotai";
|
||||
|
||||
const simpleControlShiftAtom = jotai.atom(false);
|
||||
const globalKeyMap = new Map<string, (waveEvent: WaveKeyboardEvent) => boolean>();
|
||||
|
||||
function getFocusedBlockInActiveTab() {
|
||||
const activeTabId = globalStore.get(atoms.activeTabId);
|
||||
const layoutModel = getLayoutModelForTabById(activeTabId);
|
||||
function getFocusedBlockInStaticTab() {
|
||||
const tabId = globalStore.get(atoms.staticTabId);
|
||||
const layoutModel = getLayoutModelForTabById(tabId);
|
||||
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
||||
return focusedNode.data?.blockId;
|
||||
}
|
||||
@ -70,7 +78,7 @@ function genericClose(tabId: string) {
|
||||
}
|
||||
if (tabData.blockids == null || tabData.blockids.length == 0) {
|
||||
// close tab
|
||||
services.WindowService.CloseTab(tabId);
|
||||
getApi().closeTab(tabId);
|
||||
deleteLayoutModelForTab(tabId);
|
||||
return;
|
||||
}
|
||||
@ -79,7 +87,7 @@ function genericClose(tabId: string) {
|
||||
}
|
||||
|
||||
function switchBlockByBlockNum(index: number) {
|
||||
const layoutModel = getLayoutModelForActiveTab();
|
||||
const layoutModel = getLayoutModelForStaticTab();
|
||||
if (!layoutModel) {
|
||||
return;
|
||||
}
|
||||
@ -92,21 +100,24 @@ function switchBlockInDirection(tabId: string, direction: NavigateDirection) {
|
||||
}
|
||||
|
||||
function switchTabAbs(index: number) {
|
||||
console.log("switchTabAbs", index);
|
||||
const ws = globalStore.get(atoms.workspace);
|
||||
const waveWindow = globalStore.get(atoms.waveWindow);
|
||||
const newTabIdx = index - 1;
|
||||
if (newTabIdx < 0 || newTabIdx >= ws.tabids.length) {
|
||||
return;
|
||||
}
|
||||
const newActiveTabId = ws.tabids[newTabIdx];
|
||||
services.ObjectService.SetActiveTab(newActiveTabId);
|
||||
getApi().setActiveTab(newActiveTabId);
|
||||
}
|
||||
|
||||
function switchTab(offset: number) {
|
||||
console.log("switchTab", offset);
|
||||
const ws = globalStore.get(atoms.workspace);
|
||||
const activeTabId = globalStore.get(atoms.tabAtom).oid;
|
||||
const curTabId = globalStore.get(atoms.staticTabId);
|
||||
let tabIdx = -1;
|
||||
for (let i = 0; i < ws.tabids.length; i++) {
|
||||
if (ws.tabids[i] == activeTabId) {
|
||||
if (ws.tabids[i] == curTabId) {
|
||||
tabIdx = i;
|
||||
break;
|
||||
}
|
||||
@ -116,11 +127,11 @@ function switchTab(offset: number) {
|
||||
}
|
||||
const newTabIdx = (tabIdx + offset + ws.tabids.length) % ws.tabids.length;
|
||||
const newActiveTabId = ws.tabids[newTabIdx];
|
||||
services.ObjectService.SetActiveTab(newActiveTabId);
|
||||
getApi().setActiveTab(newActiveTabId);
|
||||
}
|
||||
|
||||
function handleCmdI() {
|
||||
const layoutModel = getLayoutModelForActiveTab();
|
||||
const layoutModel = getLayoutModelForStaticTab();
|
||||
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
||||
if (focusedNode == null) {
|
||||
// focus a node
|
||||
@ -141,7 +152,7 @@ async function handleCmdN() {
|
||||
controller: "shell",
|
||||
},
|
||||
};
|
||||
const layoutModel = getLayoutModelForActiveTab();
|
||||
const layoutModel = getLayoutModelForStaticTab();
|
||||
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
||||
if (focusedNode != null) {
|
||||
const blockAtom = WOS.getWaveObjectAtom<Block>(WOS.makeORef("block", focusedNode.data?.blockId));
|
||||
@ -163,7 +174,7 @@ function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
|
||||
if (handled) {
|
||||
return true;
|
||||
}
|
||||
const layoutModel = getLayoutModelForActiveTab();
|
||||
const layoutModel = getLayoutModelForStaticTab();
|
||||
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
||||
const blockId = focusedNode?.data?.blockId;
|
||||
if (blockId != null && shouldDispatchToBlock(waveEvent)) {
|
||||
@ -225,18 +236,16 @@ function registerGlobalKeys() {
|
||||
return true;
|
||||
});
|
||||
globalKeyMap.set("Cmd:t", () => {
|
||||
const workspace = globalStore.get(atoms.workspace);
|
||||
const newTabName = `T${workspace.tabids.length + 1}`;
|
||||
services.ObjectService.AddTabToWorkspace(newTabName, true);
|
||||
createTab();
|
||||
return true;
|
||||
});
|
||||
globalKeyMap.set("Cmd:w", () => {
|
||||
const tabId = globalStore.get(atoms.activeTabId);
|
||||
const tabId = globalStore.get(atoms.staticTabId);
|
||||
genericClose(tabId);
|
||||
return true;
|
||||
});
|
||||
globalKeyMap.set("Cmd:m", () => {
|
||||
const layoutModel = getLayoutModelForActiveTab();
|
||||
const layoutModel = getLayoutModelForStaticTab();
|
||||
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
||||
if (focusedNode != null) {
|
||||
layoutModel.magnifyNodeToggle(focusedNode.id);
|
||||
@ -244,27 +253,27 @@ function registerGlobalKeys() {
|
||||
return true;
|
||||
});
|
||||
globalKeyMap.set("Ctrl:Shift:ArrowUp", () => {
|
||||
const tabId = globalStore.get(atoms.activeTabId);
|
||||
const tabId = globalStore.get(atoms.staticTabId);
|
||||
switchBlockInDirection(tabId, NavigateDirection.Up);
|
||||
return true;
|
||||
});
|
||||
globalKeyMap.set("Ctrl:Shift:ArrowDown", () => {
|
||||
const tabId = globalStore.get(atoms.activeTabId);
|
||||
const tabId = globalStore.get(atoms.staticTabId);
|
||||
switchBlockInDirection(tabId, NavigateDirection.Down);
|
||||
return true;
|
||||
});
|
||||
globalKeyMap.set("Ctrl:Shift:ArrowLeft", () => {
|
||||
const tabId = globalStore.get(atoms.activeTabId);
|
||||
const tabId = globalStore.get(atoms.staticTabId);
|
||||
switchBlockInDirection(tabId, NavigateDirection.Left);
|
||||
return true;
|
||||
});
|
||||
globalKeyMap.set("Ctrl:Shift:ArrowRight", () => {
|
||||
const tabId = globalStore.get(atoms.activeTabId);
|
||||
const tabId = globalStore.get(atoms.staticTabId);
|
||||
switchBlockInDirection(tabId, NavigateDirection.Right);
|
||||
return true;
|
||||
});
|
||||
globalKeyMap.set("Cmd:g", () => {
|
||||
const bcm = getBlockComponentModel(getFocusedBlockInActiveTab());
|
||||
const bcm = getBlockComponentModel(getFocusedBlockInStaticTab());
|
||||
if (bcm.openSwitchConnection != null) {
|
||||
bcm.openSwitchConnection();
|
||||
return true;
|
||||
|
@ -88,7 +88,7 @@ export const FileService = new FileServiceType();
|
||||
// objectservice.ObjectService (object)
|
||||
class ObjectServiceType {
|
||||
// @returns tabId (and object updates)
|
||||
AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<string> {
|
||||
AddTabToWorkspace(windowId: string, tabName: string, activateTab: boolean): Promise<string> {
|
||||
return WOS.callBackendService("object", "AddTabToWorkspace", Array.from(arguments))
|
||||
}
|
||||
|
||||
@ -113,7 +113,7 @@ class ObjectServiceType {
|
||||
}
|
||||
|
||||
// @returns object updates
|
||||
SetActiveTab(tabId: string): Promise<void> {
|
||||
SetActiveTab(uiContext: string, tabId: string): Promise<void> {
|
||||
return WOS.callBackendService("object", "SetActiveTab", Array.from(arguments))
|
||||
}
|
||||
|
||||
@ -152,10 +152,10 @@ export const UserInputService = new UserInputServiceType();
|
||||
// windowservice.WindowService (window)
|
||||
class WindowServiceType {
|
||||
// @returns object updates
|
||||
CloseTab(arg3: string): Promise<void> {
|
||||
CloseTab(arg2: string, arg3: string, arg4: boolean): Promise<CloseTabRtnType> {
|
||||
return WOS.callBackendService("window", "CloseTab", Array.from(arguments))
|
||||
}
|
||||
CloseWindow(arg2: string): Promise<void> {
|
||||
CloseWindow(arg2: string, arg3: boolean): Promise<void> {
|
||||
return WOS.callBackendService("window", "CloseWindow", Array.from(arguments))
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
// WaveObjectStore
|
||||
|
||||
import { waveEventSubscribe } from "@/app/store/wps";
|
||||
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||
import { fetch } from "@/util/fetchutil";
|
||||
import { atom, Atom, Getter, PrimitiveAtom, Setter, useAtomValue } from "jotai";
|
||||
@ -76,6 +77,16 @@ function debugLogBackendCall(methodName: string, durationStr: string, args: any[
|
||||
console.log("[service]", methodName, durationStr);
|
||||
}
|
||||
|
||||
function wpsSubscribeToObject(oref: string): () => void {
|
||||
return waveEventSubscribe({
|
||||
eventType: "waveobj:update",
|
||||
scope: oref,
|
||||
handler: (event) => {
|
||||
updateWaveObject(event.data);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function callBackendService(service: string, method: string, args: any[], noUIContext?: boolean): Promise<any> {
|
||||
const startTs = Date.now();
|
||||
let uiContext: UIContext = null;
|
||||
@ -130,6 +141,19 @@ function clearWaveObjectCache() {
|
||||
|
||||
const defaultHoldTime = 5000; // 5-seconds
|
||||
|
||||
function reloadWaveObject<T extends WaveObj>(oref: string): Promise<T> {
|
||||
let wov = waveObjectValueCache.get(oref);
|
||||
if (wov === undefined) {
|
||||
wov = getWaveObjectValue<T>(oref, true);
|
||||
return wov.pendingPromise;
|
||||
}
|
||||
const prtn = GetObject<T>(oref);
|
||||
prtn.then((val) => {
|
||||
globalStore.set(wov.dataAtom, { value: val, loading: false });
|
||||
});
|
||||
return prtn;
|
||||
}
|
||||
|
||||
function createWaveValueObject<T extends WaveObj>(oref: string, shouldFetch: boolean): WaveObjectValue<T> {
|
||||
const wov = { pendingPromise: null, dataAtom: null, refCount: 0, holdTime: Date.now() + 5000 };
|
||||
wov.dataAtom = atom({ value: null, loading: true });
|
||||
@ -290,8 +314,10 @@ export {
|
||||
getWaveObjectLoadingAtom,
|
||||
loadAndPinWaveObject,
|
||||
makeORef,
|
||||
reloadWaveObject,
|
||||
setObjectValue,
|
||||
updateWaveObject,
|
||||
updateWaveObjects,
|
||||
useWaveObjectValue,
|
||||
wpsSubscribeToObject,
|
||||
};
|
||||
|
@ -36,7 +36,7 @@ class WSControl {
|
||||
opening: boolean = false;
|
||||
reconnectTimes: number = 0;
|
||||
msgQueue: any[] = [];
|
||||
windowId: string;
|
||||
tabId: string;
|
||||
messageCallback: WSEventCallback;
|
||||
watchSessionId: string = null;
|
||||
watchScreenId: string = null;
|
||||
@ -48,13 +48,13 @@ class WSControl {
|
||||
|
||||
constructor(
|
||||
baseHostPort: string,
|
||||
windowId: string,
|
||||
tabId: string,
|
||||
messageCallback: WSEventCallback,
|
||||
electronOverrideOpts?: ElectronOverrideOpts
|
||||
) {
|
||||
this.baseHostPort = baseHostPort;
|
||||
this.messageCallback = messageCallback;
|
||||
this.windowId = windowId;
|
||||
this.tabId = tabId;
|
||||
this.open = false;
|
||||
this.eoOpts = electronOverrideOpts;
|
||||
setInterval(this.sendPing.bind(this), 5000);
|
||||
@ -73,7 +73,7 @@ class WSControl {
|
||||
dlog("try reconnect:", desc);
|
||||
this.opening = true;
|
||||
this.wsConn = newWebSocket(
|
||||
this.baseHostPort + "/ws?windowid=" + this.windowId,
|
||||
this.baseHostPort + "/ws?tabid=" + this.tabId,
|
||||
this.eoOpts
|
||||
? {
|
||||
[AuthKeyHeader]: this.eoOpts.authKey,
|
||||
@ -221,11 +221,11 @@ class WSControl {
|
||||
let globalWS: WSControl;
|
||||
function initGlobalWS(
|
||||
baseHostPort: string,
|
||||
windowId: string,
|
||||
tabId: string,
|
||||
messageCallback: WSEventCallback,
|
||||
electronOverrideOpts?: ElectronOverrideOpts
|
||||
) {
|
||||
globalWS = new WSControl(baseHostPort, windowId, messageCallback, electronOverrideOpts);
|
||||
globalWS = new WSControl(baseHostPort, tabId, messageCallback, electronOverrideOpts);
|
||||
}
|
||||
|
||||
function sendRawRpcMessage(msg: RpcMessage) {
|
||||
|
@ -15,14 +15,14 @@ type RouteInfo = {
|
||||
destRouteId: string;
|
||||
};
|
||||
|
||||
function makeWindowRouteId(windowId: string): string {
|
||||
return `window:${windowId}`;
|
||||
}
|
||||
|
||||
function makeFeBlockRouteId(feBlockId: string): string {
|
||||
return `feblock:${feBlockId}`;
|
||||
}
|
||||
|
||||
function makeTabRouteId(tabId: string): string {
|
||||
return `tab:${tabId}`;
|
||||
}
|
||||
|
||||
class WshRouter {
|
||||
routeMap: Map<string, AbstractWshClient>; // routeid -> client
|
||||
upstreamClient: AbstractWshClient;
|
||||
@ -149,4 +149,4 @@ class WshRouter {
|
||||
}
|
||||
}
|
||||
|
||||
export { makeFeBlockRouteId, makeWindowRouteId, WshRouter };
|
||||
export { makeFeBlockRouteId, makeTabRouteId, WshRouter };
|
||||
|
@ -3,12 +3,12 @@
|
||||
|
||||
import { wpsReconnectHandler } from "@/app/store/wps";
|
||||
import { WshClient } from "@/app/store/wshclient";
|
||||
import { makeWindowRouteId, WshRouter } from "@/app/store/wshrouter";
|
||||
import { makeTabRouteId, WshRouter } from "@/app/store/wshrouter";
|
||||
import { getWSServerEndpoint } from "@/util/endpoints";
|
||||
import { addWSReconnectHandler, ElectronOverrideOpts, globalWS, initGlobalWS, WSControl } from "./ws";
|
||||
|
||||
let DefaultRouter: WshRouter;
|
||||
let WindowRpcClient: WshClient;
|
||||
let TabRpcClient: WshClient;
|
||||
|
||||
async function* rpcResponseGenerator(
|
||||
openRpcs: Map<string, ClientRpcEntry>,
|
||||
@ -126,15 +126,15 @@ function shutdownWshrpc() {
|
||||
globalWS?.shutdown();
|
||||
}
|
||||
|
||||
function initWshrpc(windowId: string): WSControl {
|
||||
function initWshrpc(tabId: string): WSControl {
|
||||
DefaultRouter = new WshRouter(new UpstreamWshRpcProxy());
|
||||
const handleFn = (event: WSEventType) => {
|
||||
DefaultRouter.recvRpcMessage(event.data);
|
||||
};
|
||||
initGlobalWS(getWSServerEndpoint(), windowId, handleFn);
|
||||
initGlobalWS(getWSServerEndpoint(), tabId, handleFn);
|
||||
globalWS.connectNow("connectWshrpc");
|
||||
WindowRpcClient = new WshClient(makeWindowRouteId(windowId));
|
||||
DefaultRouter.registerRoute(WindowRpcClient.routeId, WindowRpcClient);
|
||||
TabRpcClient = new WshClient(makeTabRouteId(tabId));
|
||||
DefaultRouter.registerRoute(TabRpcClient.routeId, TabRpcClient);
|
||||
addWSReconnectHandler(() => {
|
||||
DefaultRouter.reannounceRoutes();
|
||||
});
|
||||
@ -149,12 +149,4 @@ class UpstreamWshRpcProxy implements AbstractWshClient {
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
DefaultRouter,
|
||||
initElectronWshrpc,
|
||||
initWshrpc,
|
||||
sendRpcCommand,
|
||||
sendRpcResponse,
|
||||
shutdownWshrpc,
|
||||
WindowRpcClient,
|
||||
};
|
||||
export { DefaultRouter, initElectronWshrpc, initWshrpc, sendRpcCommand, sendRpcResponse, shutdownWshrpc, TabRpcClient };
|
||||
|
@ -5,7 +5,7 @@ import { Button } from "@/app/element/button";
|
||||
import { modalsModel } from "@/app/store/modalmodel";
|
||||
import { WindowDrag } from "@/element/windowdrag";
|
||||
import { deleteLayoutModelForTab } from "@/layout/index";
|
||||
import { atoms, getApi, isDev, PLATFORM } from "@/store/global";
|
||||
import { atoms, createTab, getApi, isDev, PLATFORM } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { OverlayScrollbars } from "overlayscrollbars";
|
||||
@ -134,10 +134,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
||||
const updateStatusButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const configErrorButtonRef = useRef<HTMLElement>(null);
|
||||
const prevAllLoadedRef = useRef<boolean>(false);
|
||||
|
||||
const windowData = useAtomValue(atoms.waveWindow);
|
||||
const { activetabid } = windowData;
|
||||
|
||||
const activeTabId = useAtomValue(atoms.staticTabId);
|
||||
const isFullScreen = useAtomValue(atoms.isFullScreen);
|
||||
|
||||
const settings = useAtomValue(atoms.settingsAtom);
|
||||
@ -483,17 +480,12 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
||||
|
||||
const handleSelectTab = (tabId: string) => {
|
||||
if (!draggingTabDataRef.current.dragged) {
|
||||
services.ObjectService.SetActiveTab(tabId);
|
||||
getApi().setActiveTab(tabId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddTab = () => {
|
||||
const newTabName = `T${tabIds.length + 1}`;
|
||||
services.ObjectService.AddTabToWorkspace(newTabName, true).then((tabId) => {
|
||||
setTabIds([...tabIds, tabId]);
|
||||
setNewTabId(tabId);
|
||||
});
|
||||
services.ObjectService.GetObject;
|
||||
createTab();
|
||||
tabsWrapperRef.current.style.transition;
|
||||
tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.1s ease");
|
||||
|
||||
@ -509,7 +501,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
||||
|
||||
const handleCloseTab = (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null, tabId: string) => {
|
||||
event?.stopPropagation();
|
||||
services.WindowService.CloseTab(tabId);
|
||||
getApi().closeTab(tabId);
|
||||
tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.3s ease");
|
||||
deleteLayoutModelForTab(tabId);
|
||||
};
|
||||
@ -525,7 +517,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
||||
}, []);
|
||||
|
||||
const isBeforeActive = (tabId: string) => {
|
||||
return tabIds.indexOf(tabId) === tabIds.indexOf(activetabid) - 1;
|
||||
return tabIds.indexOf(tabId) === tabIds.indexOf(activeTabId) - 1;
|
||||
};
|
||||
|
||||
function onEllipsisClick() {
|
||||
@ -560,7 +552,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
||||
id={tabId}
|
||||
isFirst={index === 0}
|
||||
onSelect={() => handleSelectTab(tabId)}
|
||||
active={activetabid === tabId}
|
||||
active={activeTabId === tabId}
|
||||
onDragStart={(event) => handleDragStart(event, tabId, tabRefs.current[index])}
|
||||
onClose={(event) => handleCloseTab(event, tabId)}
|
||||
onLoaded={() => handleTabLoaded(tabId)}
|
||||
|
@ -12,7 +12,7 @@ import * as React from "react";
|
||||
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
|
||||
import { waveEventSubscribe } from "@/app/store/wps";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { WindowRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import "./cpuplot.less";
|
||||
|
||||
const DefaultNumPoints = 120;
|
||||
@ -112,7 +112,7 @@ class CpuPlotViewModel {
|
||||
this.incrementCount = jotai.atom(null, async (get, set) => {
|
||||
const meta = get(this.blockAtom).meta;
|
||||
const count = meta.count ?? 0;
|
||||
await RpcApi.SetMetaCommand(WindowRpcClient, {
|
||||
await RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("block", this.blockId),
|
||||
meta: { count: count + 1 },
|
||||
});
|
||||
@ -140,7 +140,7 @@ class CpuPlotViewModel {
|
||||
try {
|
||||
const numPoints = globalStore.get(this.numPoints);
|
||||
const connName = globalStore.get(this.connection);
|
||||
const initialData = await RpcApi.EventReadHistoryCommand(WindowRpcClient, {
|
||||
const initialData = await RpcApi.EventReadHistoryCommand(TabRpcClient, {
|
||||
event: "sysinfo",
|
||||
scope: connName,
|
||||
maxitems: numPoints,
|
||||
|
@ -6,7 +6,7 @@ import { TypeAheadModal } from "@/app/modals/typeaheadmodal";
|
||||
import { ContextMenuModel } from "@/app/store/contextmenu";
|
||||
import { tryReinjectKey } from "@/app/store/keymodel";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { WindowRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { CodeEditor } from "@/app/view/codeeditor/codeeditor";
|
||||
import { Markdown } from "@/element/markdown";
|
||||
import { NodeModel } from "@/layout/index";
|
||||
@ -496,7 +496,7 @@ export class PreviewModel implements ViewModel {
|
||||
async getParentInfo(fileInfo: FileInfo): Promise<FileInfo | undefined> {
|
||||
const conn = globalStore.get(this.connection);
|
||||
try {
|
||||
const parentFileInfo = await RpcApi.RemoteFileJoinCommand(WindowRpcClient, [fileInfo.path, ".."], {
|
||||
const parentFileInfo = await RpcApi.RemoteFileJoinCommand(TabRpcClient, [fileInfo.path, ".."], {
|
||||
route: makeConnRoute(conn),
|
||||
});
|
||||
return parentFileInfo;
|
||||
@ -517,7 +517,7 @@ export class PreviewModel implements ViewModel {
|
||||
}
|
||||
const conn = globalStore.get(this.connection);
|
||||
try {
|
||||
const newFileInfo = await RpcApi.RemoteFileJoinCommand(WindowRpcClient, [fileInfo.path, ".."], {
|
||||
const newFileInfo = await RpcApi.RemoteFileJoinCommand(TabRpcClient, [fileInfo.path, ".."], {
|
||||
route: makeConnRoute(conn),
|
||||
});
|
||||
if (newFileInfo.path != "" && newFileInfo.notfound) {
|
||||
@ -600,7 +600,7 @@ export class PreviewModel implements ViewModel {
|
||||
}
|
||||
const conn = globalStore.get(this.connection);
|
||||
try {
|
||||
const newFileInfo = await RpcApi.RemoteFileJoinCommand(WindowRpcClient, [fileInfo.dir, filePath], {
|
||||
const newFileInfo = await RpcApi.RemoteFileJoinCommand(TabRpcClient, [fileInfo.dir, filePath], {
|
||||
route: makeConnRoute(conn),
|
||||
});
|
||||
this.updateOpenFileModalAndError(false);
|
||||
|
@ -4,7 +4,7 @@
|
||||
import { getAllGlobalKeyBindings } from "@/app/store/keymodel";
|
||||
import { waveEventSubscribe } from "@/app/store/wps";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { WindowRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { VDomView } from "@/app/view/term/vdom";
|
||||
import { WOS, atoms, getConnStatusAtom, getSettingsKeyAtom, globalStore, useSettingsPrefixAtom } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
@ -169,7 +169,7 @@ class TermViewModel {
|
||||
}
|
||||
|
||||
setTerminalTheme(themeName: string) {
|
||||
RpcApi.SetMetaCommand(WindowRpcClient, {
|
||||
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("block", this.blockId),
|
||||
meta: { "term:theme": themeName },
|
||||
});
|
||||
@ -202,8 +202,8 @@ class TermViewModel {
|
||||
rows: this.termRef.current?.terminal?.rows,
|
||||
cols: this.termRef.current?.terminal?.cols,
|
||||
};
|
||||
const prtn = RpcApi.ControllerResyncCommand(WindowRpcClient, {
|
||||
tabid: globalStore.get(atoms.activeTabId),
|
||||
const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, {
|
||||
tabid: globalStore.get(atoms.staticTabId),
|
||||
blockid: this.blockId,
|
||||
forcerestart: true,
|
||||
rtopts: { termsize: termsize },
|
||||
@ -268,7 +268,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
RpcApi.SetMetaCommand(WindowRpcClient, {
|
||||
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("block", blockId),
|
||||
meta: { "term:mode": null },
|
||||
});
|
||||
@ -291,8 +291,8 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||
}
|
||||
if (shellProcStatusRef.current != "running" && keyutil.checkKeyPressed(waveEvent, "Enter")) {
|
||||
// restart
|
||||
const tabId = globalStore.get(atoms.activeTabId);
|
||||
const prtn = RpcApi.ControllerResyncCommand(WindowRpcClient, { tabid: tabId, blockid: blockId });
|
||||
const tabId = globalStore.get(atoms.staticTabId);
|
||||
const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, { tabid: tabId, blockid: blockId });
|
||||
prtn.catch((e) => console.log("error controller resync (enter)", blockId, e));
|
||||
return false;
|
||||
}
|
||||
@ -356,7 +356,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
|
||||
// reset term:mode
|
||||
RpcApi.SetMetaCommand(WindowRpcClient, {
|
||||
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("block", blockId),
|
||||
meta: { "term:mode": null },
|
||||
});
|
||||
@ -367,7 +367,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||
return false;
|
||||
}
|
||||
const b64data = util.stringToBase64(asciiVal);
|
||||
RpcApi.ControllerInputCommand(WindowRpcClient, { blockid: blockId, inputdata64: b64data });
|
||||
RpcApi.ControllerInputCommand(TabRpcClient, { blockid: blockId, inputdata64: b64data });
|
||||
return true;
|
||||
};
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { WindowRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { createBlock } from "@/store/global";
|
||||
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||
import { stringToBase64 } from "@/util/util";
|
||||
@ -101,7 +101,7 @@ function TermSticker({ sticker, config }: { sticker: StickerType; config: Sticke
|
||||
console.log("clickHandler", sticker.clickcmd, sticker.clickblockdef);
|
||||
if (sticker.clickcmd) {
|
||||
const b64data = stringToBase64(sticker.clickcmd);
|
||||
RpcApi.ControllerInputCommand(WindowRpcClient, { blockid: config.blockId, inputdata64: b64data });
|
||||
RpcApi.ControllerInputCommand(TabRpcClient, { blockid: config.blockId, inputdata64: b64data });
|
||||
}
|
||||
if (sticker.clickblockdef) {
|
||||
createBlock(sticker.clickblockdef);
|
||||
|
@ -4,7 +4,7 @@
|
||||
import { getFileSubject } from "@/app/store/wps";
|
||||
import { sendWSCommand } from "@/app/store/ws";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { WindowRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { PLATFORM, WOS, atoms, fetchWaveFile, getSettingsKeyAtom, globalStore, openLink } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as util from "@/util/util";
|
||||
@ -168,7 +168,7 @@ export class TermWrap {
|
||||
|
||||
handleTermData(data: string) {
|
||||
const b64data = util.stringToBase64(data);
|
||||
RpcApi.ControllerInputCommand(WindowRpcClient, { blockid: this.blockId, inputdata64: b64data });
|
||||
RpcApi.ControllerInputCommand(TabRpcClient, { blockid: this.blockId, inputdata64: b64data });
|
||||
}
|
||||
|
||||
addFocusListener(focusFn: () => void) {
|
||||
@ -230,10 +230,10 @@ export class TermWrap {
|
||||
|
||||
async resyncController(reason: string) {
|
||||
dlog("resync controller", this.blockId, reason);
|
||||
const tabId = globalStore.get(atoms.activeTabId);
|
||||
const tabId = globalStore.get(atoms.staticTabId);
|
||||
const rtOpts: RuntimeOpts = { termsize: { rows: this.terminal.rows, cols: this.terminal.cols } };
|
||||
try {
|
||||
await RpcApi.ControllerResyncCommand(WindowRpcClient, {
|
||||
await RpcApi.ControllerResyncCommand(TabRpcClient, {
|
||||
tabid: tabId,
|
||||
blockid: this.blockId,
|
||||
rtopts: rtOpts,
|
||||
|
@ -5,7 +5,7 @@ import { Button } from "@/app/element/button";
|
||||
import { Markdown } from "@/app/element/markdown";
|
||||
import { TypingIndicator } from "@/app/element/typingindicator";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { WindowRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { atoms, fetchWaveFile, globalStore, WOS } from "@/store/global";
|
||||
import { BlockService, ObjectService } from "@/store/services";
|
||||
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
|
||||
@ -274,7 +274,7 @@ export class WaveAiModel implements ViewModel {
|
||||
};
|
||||
let fullMsg = "";
|
||||
try {
|
||||
const aiGen = RpcApi.StreamWaveAiCommand(WindowRpcClient, beMsg, { timeout: opts.timeoutms });
|
||||
const aiGen = RpcApi.StreamWaveAiCommand(TabRpcClient, beMsg, { timeout: opts.timeoutms });
|
||||
for await (const msg of aiGen) {
|
||||
fullMsg += msg.text ?? "";
|
||||
globalStore.set(this.updateLastMessageAtom, msg.text ?? "", true);
|
||||
|
@ -5,7 +5,7 @@ import { getApi, getSettingsKeyAtom, openLink } from "@/app/store/global";
|
||||
import { getSimpleControlShiftAtom } from "@/app/store/keymodel";
|
||||
import { ObjectService } from "@/app/store/services";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { WindowRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { NodeModel } from "@/layout/index";
|
||||
import { WOS, globalStore } from "@/store/global";
|
||||
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
|
||||
@ -370,17 +370,17 @@ export class WebViewModel implements ViewModel {
|
||||
if (url != null && url != "") {
|
||||
switch (scope) {
|
||||
case "block":
|
||||
await RpcApi.SetMetaCommand(WindowRpcClient, {
|
||||
await RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("block", this.blockId),
|
||||
meta: { pinnedurl: url },
|
||||
});
|
||||
break;
|
||||
case "global":
|
||||
await RpcApi.SetMetaCommand(WindowRpcClient, {
|
||||
await RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("block", this.blockId),
|
||||
meta: { pinnedurl: "" },
|
||||
});
|
||||
await RpcApi.SetConfigCommand(WindowRpcClient, { "web:defaulturl": url });
|
||||
await RpcApi.SetConfigCommand(TabRpcClient, { "web:defaulturl": url });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -94,20 +94,18 @@ const Widget = memo(({ widget }: { widget: WidgetConfigType }) => {
|
||||
});
|
||||
|
||||
const WorkspaceElem = memo(() => {
|
||||
const windowData = useAtomValue(atoms.waveWindow);
|
||||
const activeTabId = windowData?.activetabid;
|
||||
const tabId = useAtomValue(atoms.staticTabId);
|
||||
const ws = useAtomValue(atoms.workspace);
|
||||
|
||||
return (
|
||||
<div className="workspace">
|
||||
<TabBar key={ws.oid} workspace={ws} />
|
||||
<div className="workspace-tabcontent">
|
||||
<ErrorBoundary key={activeTabId}>
|
||||
{activeTabId == "" ? (
|
||||
<ErrorBoundary key={tabId}>
|
||||
{tabId == "" ? (
|
||||
<CenteredDiv>No Active Tab</CenteredDiv>
|
||||
) : (
|
||||
<>
|
||||
<TabContent key={activeTabId} tabId={activeTabId} />
|
||||
<TabContent key={tabId} tabId={tabId} />
|
||||
<Widgets />
|
||||
<ModalsRenderer />
|
||||
</>
|
||||
|
@ -5,7 +5,7 @@ import { TileLayout } from "./lib/TileLayout";
|
||||
import { LayoutModel } from "./lib/layoutModel";
|
||||
import {
|
||||
deleteLayoutModelForTab,
|
||||
getLayoutModelForActiveTab,
|
||||
getLayoutModelForStaticTab,
|
||||
getLayoutModelForTab,
|
||||
getLayoutModelForTabById,
|
||||
useDebouncedNodeInnerRect,
|
||||
@ -37,7 +37,7 @@ import { DropDirection, LayoutTreeActionType, NavigateDirection } from "./lib/ty
|
||||
export {
|
||||
deleteLayoutModelForTab,
|
||||
DropDirection,
|
||||
getLayoutModelForActiveTab,
|
||||
getLayoutModelForStaticTab,
|
||||
getLayoutModelForTab,
|
||||
getLayoutModelForTabById,
|
||||
LayoutModel,
|
||||
|
@ -128,7 +128,6 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export const TileLayout = memo(TileLayoutComponent) as typeof TileLayoutComponent;
|
||||
|
||||
interface DisplayNodesWrapperProps {
|
||||
@ -247,6 +246,7 @@ const DisplayNode = ({ layoutModel, node }: DisplayNodeProps) => {
|
||||
magnified: addlProps?.isMagnifiedNode,
|
||||
"last-magnified": addlProps?.isLastMagnifiedNode,
|
||||
})}
|
||||
key={node.id}
|
||||
ref={tileNodeRef}
|
||||
id={node.id}
|
||||
style={addlProps?.transform}
|
||||
|
@ -39,6 +39,10 @@ export function withLayoutTreeStateAtomFromTab(tabAtom: Atom<Tab>): WritableLayo
|
||||
const stateAtom = getLayoutStateAtomFromTab(tabAtom, get);
|
||||
if (!stateAtom) return;
|
||||
const waveObjVal = get(stateAtom);
|
||||
if (waveObjVal == null) {
|
||||
console.log("in withLayoutTreeStateAtomFromTab, waveObjVal is null", value);
|
||||
return;
|
||||
}
|
||||
waveObjVal.rootnode = value.rootNode;
|
||||
waveObjVal.magnifiednodeid = value.magnifiedNodeId;
|
||||
waveObjVal.focusednodeid = value.focusedNodeId;
|
||||
|
@ -1,9 +1,9 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useOnResize } from "@/app/hook/useDimensions";
|
||||
import { atoms, globalStore, WOS } from "@/app/store/global";
|
||||
import { fireAndForget } from "@/util/util";
|
||||
import useResizeObserver from "@react-hook/resize-observer";
|
||||
import { Atom, useAtomValue } from "jotai";
|
||||
import { CSSProperties, useCallback, useEffect, useLayoutEffect, useState } from "react";
|
||||
import { debounce } from "throttle-debounce";
|
||||
@ -36,8 +36,8 @@ export function getLayoutModelForTabById(tabId: string) {
|
||||
return getLayoutModelForTab(tabAtom);
|
||||
}
|
||||
|
||||
export function getLayoutModelForActiveTab() {
|
||||
const tabId = globalStore.get(atoms.activeTabId);
|
||||
export function getLayoutModelForStaticTab() {
|
||||
const tabId = globalStore.get(atoms.staticTabId);
|
||||
return getLayoutModelForTabById(tabId);
|
||||
}
|
||||
|
||||
@ -53,7 +53,8 @@ export function useTileLayout(tabAtom: Atom<Tab>, tileContent: TileLayoutContent
|
||||
// Use tab data to ensure we can reload if the tab is disposed and remade (such as during Hot Module Reloading)
|
||||
useAtomValue(tabAtom);
|
||||
const layoutModel = useLayoutModel(tabAtom);
|
||||
useResizeObserver(layoutModel?.displayContainerRef, layoutModel?.onContainerResize);
|
||||
|
||||
useOnResize(layoutModel?.displayContainerRef, layoutModel?.onContainerResize);
|
||||
|
||||
// Once the TileLayout is mounted, re-run the state update to get all the nodes to flow in the layout.
|
||||
useEffect(() => fireAndForget(async () => layoutModel.onTreeStateAtomUpdated(true)), []);
|
||||
|
39
frontend/types/custom.d.ts
vendored
39
frontend/types/custom.d.ts
vendored
@ -7,16 +7,15 @@ import type * as rxjs from "rxjs";
|
||||
|
||||
declare global {
|
||||
type GlobalAtomsType = {
|
||||
windowId: jotai.Atom<string>; // readonly
|
||||
clientId: jotai.Atom<string>; // readonly
|
||||
client: jotai.Atom<Client>; // driven from WOS
|
||||
uiContext: jotai.Atom<UIContext>; // driven from windowId, activetabid, etc.
|
||||
uiContext: jotai.Atom<UIContext>; // driven from windowId, tabId
|
||||
waveWindow: jotai.Atom<WaveWindow>; // driven from WOS
|
||||
workspace: jotai.Atom<Workspace>; // driven from WOS
|
||||
fullConfigAtom: jotai.PrimitiveAtom<FullConfigType>; // driven from WOS, settings -- updated via WebSocket
|
||||
settingsAtom: jotai.Atom<SettingsType>; // derrived from fullConfig
|
||||
tabAtom: jotai.Atom<Tab>; // driven from WOS
|
||||
activeTabId: jotai.Atom<string>; // derrived from windowDataAtom
|
||||
staticTabId: jotai.Atom<string>;
|
||||
isFullScreen: jotai.PrimitiveAtom<boolean>;
|
||||
controlShiftDelayAtom: jotai.PrimitiveAtom<boolean>;
|
||||
prefersReducedMotionAtom: jotai.Atom<boolean>;
|
||||
@ -50,6 +49,13 @@ declare global {
|
||||
blockId: string;
|
||||
};
|
||||
|
||||
type WaveInitOpts = {
|
||||
tabId: string;
|
||||
clientId: string;
|
||||
windowId: string;
|
||||
activate: boolean;
|
||||
};
|
||||
|
||||
type ElectronApi = {
|
||||
getAuthKey(): string;
|
||||
getIsDev(): boolean;
|
||||
@ -78,6 +84,12 @@ declare global {
|
||||
setWebviewFocus: (focusedId: number) => void; // focusedId si the getWebContentsId of the webview
|
||||
registerGlobalWebviewKeys: (keys: string[]) => void;
|
||||
onControlShiftStateUpdate: (callback: (state: boolean) => void) => void;
|
||||
setActiveTab: (tabId: string) => void;
|
||||
createTab: () => void;
|
||||
closeTab: (tabId: string) => void;
|
||||
setWindowInitStatus: (status: "ready" | "wave-ready") => void;
|
||||
onWaveInit: (callback: (initOpts: WaveInitOpts) => void) => void;
|
||||
sendLog: (log: string) => void;
|
||||
onQuicklook: (filePath: string) => void;
|
||||
};
|
||||
|
||||
@ -332,6 +344,27 @@ declare global {
|
||||
command: string;
|
||||
msgFn: (msg: RpcMessage) => void;
|
||||
};
|
||||
|
||||
type WaveBrowserWindow = Electron.BaseWindow & {
|
||||
waveWindowId: string;
|
||||
waveReadyPromise: Promise<void>;
|
||||
allTabViews: Map<string, WaveTabView>;
|
||||
activeTabView: WaveTabView;
|
||||
alreadyClosed: 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;
|
||||
};
|
||||
}
|
||||
|
||||
export {};
|
||||
|
7
frontend/types/gotypes.d.ts
vendored
7
frontend/types/gotypes.d.ts
vendored
@ -47,6 +47,12 @@ declare global {
|
||||
hasoldhistory?: boolean;
|
||||
};
|
||||
|
||||
// windowservice.CloseTabRtnType
|
||||
type CloseTabRtnType = {
|
||||
closewindow?: boolean;
|
||||
newactivetabid?: string;
|
||||
};
|
||||
|
||||
// wshrpc.CommandAppendIJsonData
|
||||
type CommandAppendIJsonData = {
|
||||
zoneid: string;
|
||||
@ -472,6 +478,7 @@ declare global {
|
||||
"window:showmenubar"?: boolean;
|
||||
"window:nativetitlebar"?: boolean;
|
||||
"window:disablehardwareacceleration"?: boolean;
|
||||
"window:maxtabcachesize"?: number;
|
||||
"telemetry:*"?: boolean;
|
||||
"telemetry:enabled"?: boolean;
|
||||
"conn:*"?: boolean;
|
||||
|
140
frontend/wave.ts
140
frontend/wave.ts
@ -8,11 +8,11 @@ import {
|
||||
registerGlobalKeys,
|
||||
} from "@/app/store/keymodel";
|
||||
import { modalsModel } from "@/app/store/modalmodel";
|
||||
import { FileService, ObjectService } from "@/app/store/services";
|
||||
import { FileService } from "@/app/store/services";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { initWshrpc, WindowRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { initWshrpc, TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { loadMonaco } from "@/app/view/codeeditor/codeeditor";
|
||||
import { getLayoutModelForActiveTab } from "@/layout/index";
|
||||
import { getLayoutModelForStaticTab } from "@/layout/index";
|
||||
import {
|
||||
atoms,
|
||||
countersClear,
|
||||
@ -32,18 +32,9 @@ import { createElement } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
const platform = getApi().getPlatform();
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const windowId = urlParams.get("windowid");
|
||||
const clientId = urlParams.get("clientid");
|
||||
document.title = `Wave Terminal`;
|
||||
let savedInitOpts: WaveInitOpts = null;
|
||||
|
||||
console.log("Wave Starting");
|
||||
console.log("clientid", clientId, "windowid", windowId);
|
||||
|
||||
initGlobal({ clientId, windowId, platform, environment: "renderer" });
|
||||
|
||||
setKeyUtilPlatform(platform);
|
||||
|
||||
loadFonts();
|
||||
(window as any).WOS = WOS;
|
||||
(window as any).globalStore = globalStore;
|
||||
(window as any).globalAtoms = atoms;
|
||||
@ -51,29 +42,109 @@ loadFonts();
|
||||
(window as any).isFullScreen = false;
|
||||
(window as any).countersPrint = countersPrint;
|
||||
(window as any).countersClear = countersClear;
|
||||
(window as any).getLayoutModelForActiveTab = getLayoutModelForActiveTab;
|
||||
(window as any).getLayoutModelForStaticTab = getLayoutModelForStaticTab;
|
||||
(window as any).pushFlashError = pushFlashError;
|
||||
(window as any).modalsModel = modalsModel;
|
||||
|
||||
document.title = `Wave (${windowId.substring(0, 8)})`;
|
||||
async function initBare() {
|
||||
getApi().sendLog("Init Bare");
|
||||
document.body.style.visibility = "hidden";
|
||||
document.body.style.opacity = "0";
|
||||
document.body.classList.add("is-transparent");
|
||||
getApi().onWaveInit(initWaveWrap);
|
||||
setKeyUtilPlatform(platform);
|
||||
loadFonts();
|
||||
document.fonts.ready.then(() => {
|
||||
console.log("Init Bare Done");
|
||||
getApi().setWindowInitStatus("ready");
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
console.log("DOMContentLoaded");
|
||||
document.addEventListener("DOMContentLoaded", initBare);
|
||||
|
||||
async function initWaveWrap(initOpts: WaveInitOpts) {
|
||||
try {
|
||||
if (savedInitOpts) {
|
||||
await reinitWave();
|
||||
return;
|
||||
}
|
||||
savedInitOpts = initOpts;
|
||||
await initWave(initOpts);
|
||||
} catch (e) {
|
||||
getApi().sendLog("Error in initWave " + e.message);
|
||||
console.error("Error in initWave", e);
|
||||
} finally {
|
||||
document.body.style.visibility = null;
|
||||
document.body.style.opacity = null;
|
||||
document.body.classList.remove("is-transparent");
|
||||
}
|
||||
}
|
||||
|
||||
async function reinitWave() {
|
||||
console.log("Reinit Wave");
|
||||
getApi().sendLog("Reinit Wave");
|
||||
const client = await WOS.reloadWaveObject<Client>(WOS.makeORef("client", savedInitOpts.clientId));
|
||||
const waveWindow = await WOS.reloadWaveObject<WaveWindow>(WOS.makeORef("window", savedInitOpts.windowId));
|
||||
await WOS.reloadWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid));
|
||||
const initialTab = await WOS.reloadWaveObject<Tab>(WOS.makeORef("tab", savedInitOpts.tabId));
|
||||
await WOS.reloadWaveObject<LayoutState>(WOS.makeORef("layout", initialTab.layoutstate));
|
||||
document.title = `Wave Terminal - ${initialTab.name}`; // TODO update with tab name change
|
||||
getApi().setWindowInitStatus("wave-ready");
|
||||
}
|
||||
|
||||
function loadAllWorkspaceTabs(ws: Workspace) {
|
||||
if (ws == null || ws.tabids == null) {
|
||||
return;
|
||||
}
|
||||
ws.tabids.forEach((tabid) => {
|
||||
WOS.getObjectValue<Tab>(WOS.makeORef("tab", tabid));
|
||||
});
|
||||
}
|
||||
|
||||
async function initWave(initOpts: WaveInitOpts) {
|
||||
getApi().sendLog("Init Wave " + JSON.stringify(initOpts));
|
||||
console.log(
|
||||
"Wave Init",
|
||||
"tabid",
|
||||
initOpts.tabId,
|
||||
"clientid",
|
||||
initOpts.clientId,
|
||||
"windowid",
|
||||
initOpts.windowId,
|
||||
"platform",
|
||||
platform
|
||||
);
|
||||
initGlobal({
|
||||
tabId: initOpts.tabId,
|
||||
clientId: initOpts.clientId,
|
||||
windowId: initOpts.windowId,
|
||||
platform,
|
||||
environment: "renderer",
|
||||
});
|
||||
(window as any).globalAtoms = atoms;
|
||||
|
||||
// Init WPS event handlers
|
||||
const globalWS = initWshrpc(windowId);
|
||||
const globalWS = initWshrpc(initOpts.tabId);
|
||||
(window as any).globalWS = globalWS;
|
||||
(window as any).WindowRpcClient = WindowRpcClient;
|
||||
(window as any).TabRpcClient = TabRpcClient;
|
||||
await loadConnStatus();
|
||||
initGlobalWaveEventSubs();
|
||||
subscribeToConnEvents();
|
||||
|
||||
// ensures client/window/workspace are loaded into the cache before rendering
|
||||
const client = await WOS.loadAndPinWaveObject<Client>(WOS.makeORef("client", clientId));
|
||||
const waveWindow = await WOS.loadAndPinWaveObject<WaveWindow>(WOS.makeORef("window", windowId));
|
||||
await WOS.loadAndPinWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid));
|
||||
const initialTab = await WOS.loadAndPinWaveObject<Tab>(WOS.makeORef("tab", waveWindow.activetabid));
|
||||
await WOS.loadAndPinWaveObject<LayoutState>(WOS.makeORef("layout", initialTab.layoutstate));
|
||||
const [client, waveWindow, initialTab] = await Promise.all([
|
||||
WOS.loadAndPinWaveObject<Client>(WOS.makeORef("client", initOpts.clientId)),
|
||||
WOS.loadAndPinWaveObject<WaveWindow>(WOS.makeORef("window", initOpts.windowId)),
|
||||
WOS.loadAndPinWaveObject<Tab>(WOS.makeORef("tab", initOpts.tabId)),
|
||||
]);
|
||||
const [ws, layoutState] = await Promise.all([
|
||||
WOS.loadAndPinWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid)),
|
||||
WOS.reloadWaveObject<LayoutState>(WOS.makeORef("layout", initialTab.layoutstate)),
|
||||
]);
|
||||
loadAllWorkspaceTabs(ws);
|
||||
WOS.wpsSubscribeToObject(WOS.makeORef("workspace", waveWindow.workspaceid));
|
||||
|
||||
document.title = `Wave Terminal - ${initialTab.name}`; // TODO update with tab name change
|
||||
|
||||
registerGlobalKeys();
|
||||
registerElectronReinjectKeyHandler();
|
||||
@ -82,15 +153,16 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const fullConfig = await FileService.GetFullConfig();
|
||||
console.log("fullconfig", fullConfig);
|
||||
globalStore.set(atoms.fullConfigAtom, fullConfig);
|
||||
const prtn = ObjectService.SetActiveTab(waveWindow.activetabid); // no need to wait
|
||||
prtn.catch((e) => {
|
||||
console.log("error on initial SetActiveTab", e);
|
||||
console.log("Wave First Render");
|
||||
let firstRenderResolveFn: () => void = null;
|
||||
let firstRenderPromise = new Promise<void>((resolve) => {
|
||||
firstRenderResolveFn = resolve;
|
||||
});
|
||||
const reactElem = createElement(App, null, null);
|
||||
const reactElem = createElement(App, { onFirstRender: firstRenderResolveFn }, null);
|
||||
const elem = document.getElementById("main");
|
||||
const root = createRoot(elem);
|
||||
document.fonts.ready.then(() => {
|
||||
console.log("Wave First Render");
|
||||
root.render(reactElem);
|
||||
});
|
||||
});
|
||||
root.render(reactElem);
|
||||
await firstRenderPromise;
|
||||
console.log("Wave First Render Done");
|
||||
getApi().setWindowInitStatus("wave-ready");
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color scheme" content="light dark" />
|
||||
<title>Wave</title>
|
||||
<link rel="stylesheet" href="/fontawesome/css/fontawesome.min.css" />
|
||||
<link rel="stylesheet" href="/fontawesome/css/brands.min.css" />
|
||||
@ -11,7 +12,7 @@
|
||||
<link rel="stylesheet" href="/fontawesome/css/sharp-regular.min.css" />
|
||||
<script type="module" src="frontend/wave.ts"></script>
|
||||
</head>
|
||||
<body>
|
||||
<body class="init">
|
||||
<div id="main"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -359,9 +359,6 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer func() {
|
||||
log.Printf("[shellproc] shellInputCh loop done\n")
|
||||
}()
|
||||
// handles input from the shellInputCh, sent to pty
|
||||
// use shellInputCh instead of bc.ShellInputCh (because we want to be attached to *this* ch. bc.ShellInputCh can be updated)
|
||||
for ic := range shellInputCh {
|
||||
|
@ -10,8 +10,6 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -27,21 +25,19 @@ type WSEventType struct {
|
||||
}
|
||||
|
||||
type WindowWatchData struct {
|
||||
WindowWSCh chan any
|
||||
WaveWindowId string
|
||||
WatchedORefs map[waveobj.ORef]bool
|
||||
WindowWSCh chan any
|
||||
TabId string
|
||||
}
|
||||
|
||||
var globalLock = &sync.Mutex{}
|
||||
var wsMap = make(map[string]*WindowWatchData) // websocketid => WindowWatchData
|
||||
|
||||
func RegisterWSChannel(connId string, windowId string, ch chan any) {
|
||||
func RegisterWSChannel(connId string, tabId string, ch chan any) {
|
||||
globalLock.Lock()
|
||||
defer globalLock.Unlock()
|
||||
wsMap[connId] = &WindowWatchData{
|
||||
WindowWSCh: ch,
|
||||
WaveWindowId: windowId,
|
||||
WatchedORefs: make(map[waveobj.ORef]bool),
|
||||
WindowWSCh: ch,
|
||||
TabId: tabId,
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +52,7 @@ func getWindowWatchesForWindowId(windowId string) []*WindowWatchData {
|
||||
defer globalLock.Unlock()
|
||||
var watches []*WindowWatchData
|
||||
for _, wdata := range wsMap {
|
||||
if wdata.WaveWindowId == windowId {
|
||||
if wdata.TabId == windowId {
|
||||
watches = append(watches, wdata)
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"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,25 +75,28 @@ func (svc *ObjectService) GetObjects(orefStrArr []string) ([]waveobj.WaveObj, er
|
||||
|
||||
func (svc *ObjectService) AddTabToWorkspace_Meta() tsgenmeta.MethodMeta {
|
||||
return tsgenmeta.MethodMeta{
|
||||
ArgNames: []string{"uiContext", "tabName", "activateTab"},
|
||||
ArgNames: []string{"windowId", "tabName", "activateTab"},
|
||||
ReturnDesc: "tabId",
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *ObjectService) AddTabToWorkspace(uiContext waveobj.UIContext, tabName string, activateTab bool) (string, waveobj.UpdatesRtnType, error) {
|
||||
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, uiContext.WindowId, tabName, activateTab)
|
||||
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)
|
||||
}
|
||||
return tabId, waveobj.ContextGetUpdatesRtn(ctx), nil
|
||||
updates := waveobj.ContextGetUpdatesRtn(ctx)
|
||||
go func() {
|
||||
wps.Broker.SendUpdateEvents(updates)
|
||||
}()
|
||||
return tabId, updates, nil
|
||||
}
|
||||
|
||||
func (svc *ObjectService) UpdateWorkspaceTabIds_Meta() tsgenmeta.MethodMeta {
|
||||
@ -118,11 +122,11 @@ func (svc *ObjectService) SetActiveTab_Meta() tsgenmeta.MethodMeta {
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *ObjectService) SetActiveTab(uiContext waveobj.UIContext, tabId string) (waveobj.UpdatesRtnType, error) {
|
||||
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, uiContext.WindowId, tabId)
|
||||
err := wstore.SetActiveTab(ctx, windowId, tabId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error setting active tab: %w", err)
|
||||
}
|
||||
@ -137,9 +141,14 @@ func (svc *ObjectService) SetActiveTab(uiContext waveobj.UIContext, tabId string
|
||||
return nil, fmt.Errorf("error getting tab blocks: %w", err)
|
||||
}
|
||||
updates := waveobj.ContextGetUpdatesRtn(ctx)
|
||||
updates = append(updates, waveobj.MakeUpdate(tab))
|
||||
updates = append(updates, waveobj.MakeUpdates(blocks)...)
|
||||
return updates, nil
|
||||
go func() {
|
||||
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 {
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -46,19 +47,25 @@ func (ws *WindowService) SetWindowPosAndSize(ctx context.Context, windowId strin
|
||||
return waveobj.ContextGetUpdatesRtn(ctx), nil
|
||||
}
|
||||
|
||||
func (svc *WindowService) CloseTab(ctx context.Context, uiContext waveobj.UIContext, tabId string) (waveobj.UpdatesRtnType, error) {
|
||||
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, uiContext.WindowId)
|
||||
window, err := wstore.DBMustGet[*waveobj.Window](ctx, windowId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting window: %w", err)
|
||||
return nil, nil, fmt.Errorf("error getting window: %w", err)
|
||||
}
|
||||
tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting tab: %w", err)
|
||||
return nil, nil, fmt.Errorf("error getting tab: %w", err)
|
||||
}
|
||||
ws, err := wstore.DBMustGet[*waveobj.Workspace](ctx, window.WorkspaceId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting workspace: %w", err)
|
||||
return nil, nil, fmt.Errorf("error getting workspace: %w", err)
|
||||
}
|
||||
tabIndex := -1
|
||||
for i, id := range ws.TabIds {
|
||||
@ -73,26 +80,36 @@ func (svc *WindowService) CloseTab(ctx context.Context, uiContext waveobj.UICont
|
||||
}
|
||||
}()
|
||||
if err := wcore.DeleteTab(ctx, window.WorkspaceId, tabId); err != nil {
|
||||
return nil, fmt.Errorf("error closing tab: %w", err)
|
||||
return nil, nil, fmt.Errorf("error closing tab: %w", err)
|
||||
}
|
||||
rtn := &CloseTabRtnType{}
|
||||
if window.ActiveTabId == tabId && tabIndex != -1 {
|
||||
if len(ws.TabIds) == 1 {
|
||||
svc.CloseWindow(ctx, uiContext.WindowId)
|
||||
eventbus.SendEventToElectron(eventbus.WSEventType{
|
||||
EventType: eventbus.WSEvent_ElectronCloseWindow,
|
||||
Data: uiContext.WindowId,
|
||||
})
|
||||
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, uiContext.WindowId, newActiveTabId)
|
||||
wstore.SetActiveTab(ctx, windowId, newActiveTabId)
|
||||
rtn.NewActiveTabId = newActiveTabId
|
||||
} else {
|
||||
newActiveTabId := ws.TabIds[tabIndex-1]
|
||||
wstore.SetActiveTab(ctx, uiContext.WindowId, newActiveTabId)
|
||||
wstore.SetActiveTab(ctx, windowId, newActiveTabId)
|
||||
rtn.NewActiveTabId = newActiveTabId
|
||||
}
|
||||
}
|
||||
}
|
||||
return waveobj.ContextGetUpdatesRtn(ctx), nil
|
||||
updates := waveobj.ContextGetUpdatesRtn(ctx)
|
||||
go func() {
|
||||
wps.Broker.SendUpdateEvents(updates)
|
||||
}()
|
||||
return rtn, updates, nil
|
||||
}
|
||||
|
||||
func (svc *WindowService) MoveBlockToNewWindow_Meta() tsgenmeta.MethodMeta {
|
||||
@ -148,7 +165,7 @@ func (svc *WindowService) MoveBlockToNewWindow(ctx context.Context, currentTabId
|
||||
return waveobj.ContextGetUpdatesRtn(ctx), nil
|
||||
}
|
||||
|
||||
func (svc *WindowService) CloseWindow(ctx context.Context, windowId string) error {
|
||||
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 {
|
||||
@ -159,8 +176,7 @@ func (svc *WindowService) CloseWindow(ctx context.Context, windowId string) erro
|
||||
return fmt.Errorf("error getting workspace: %w", err)
|
||||
}
|
||||
for _, tabId := range workspace.TabIds {
|
||||
uiContext := waveobj.UIContext{WindowId: windowId}
|
||||
_, err := svc.CloseTab(ctx, uiContext, tabId)
|
||||
_, _, err := svc.CloseTab(ctx, windowId, tabId, fromElectron)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error closing tab: %w", err)
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
"web:defaulturl": "https://github.com/wavetermdev/waveterm",
|
||||
"web:defaultsearch": "https://www.google.com/search?q={query}",
|
||||
"window:tilegapsize": 3,
|
||||
"window:maxtabcachesize": 10,
|
||||
"telemetry:enabled": true,
|
||||
"term:copyonselect": true
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ const (
|
||||
ConfigKey_WindowShowMenuBar = "window:showmenubar"
|
||||
ConfigKey_WindowNativeTitleBar = "window:nativetitlebar"
|
||||
ConfigKey_WindowDisableHardwareAcceleration = "window:disablehardwareacceleration"
|
||||
ConfigKey_WindowMaxTabCacheSize = "window:maxtabcachesize"
|
||||
|
||||
ConfigKey_TelemetryClear = "telemetry:*"
|
||||
ConfigKey_TelemetryEnabled = "telemetry:enabled"
|
||||
|
@ -101,6 +101,7 @@ type SettingsType struct {
|
||||
WindowShowMenuBar bool `json:"window:showmenubar,omitempty"`
|
||||
WindowNativeTitleBar bool `json:"window:nativetitlebar,omitempty"`
|
||||
WindowDisableHardwareAcceleration bool `json:"window:disablehardwareacceleration,omitempty"`
|
||||
WindowMaxTabCacheSize int `json:"window:maxtabcachesize,omitempty"`
|
||||
|
||||
TelemetryClear bool `json:"telemetry:*,omitempty"`
|
||||
TelemetryEnabled bool `json:"telemetry:enabled,omitempty"`
|
||||
|
@ -78,6 +78,13 @@ func CreateTab(ctx context.Context, windowId string, tabName string, activateTab
|
||||
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)
|
||||
}
|
||||
tab, err := wstore.CreateTab(ctx, windowData.WorkspaceId, tabName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating tab: %w", err)
|
||||
|
@ -241,11 +241,10 @@ func WriteLoop(conn *websocket.Conn, outputCh chan any, closeCh chan any) {
|
||||
}
|
||||
|
||||
func HandleWsInternal(w http.ResponseWriter, r *http.Request) error {
|
||||
windowId := r.URL.Query().Get("windowid")
|
||||
if windowId == "" {
|
||||
return fmt.Errorf("windowid is required")
|
||||
tabId := r.URL.Query().Get("tabid")
|
||||
if tabId == "" {
|
||||
return fmt.Errorf("tabid is required")
|
||||
}
|
||||
|
||||
err := authkey.ValidateIncomingRequest(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
@ -258,15 +257,15 @@ func HandleWsInternal(w http.ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
defer conn.Close()
|
||||
wsConnId := uuid.New().String()
|
||||
log.Printf("New websocket connection: windowid:%s connid:%s\n", windowId, wsConnId)
|
||||
log.Printf("New websocket connection: tabid:%s connid:%s\n", tabId, wsConnId)
|
||||
outputCh := make(chan any, 100)
|
||||
closeCh := make(chan any)
|
||||
eventbus.RegisterWSChannel(wsConnId, windowId, outputCh)
|
||||
eventbus.RegisterWSChannel(wsConnId, tabId, outputCh)
|
||||
var routeId string
|
||||
if windowId == wshutil.ElectronRoute {
|
||||
if tabId == wshutil.ElectronRoute {
|
||||
routeId = wshutil.ElectronRoute
|
||||
} else {
|
||||
routeId = wshutil.MakeWindowRouteId(windowId)
|
||||
routeId = wshutil.MakeTabRouteId(tabId)
|
||||
}
|
||||
defer eventbus.UnregisterWSChannel(wsConnId)
|
||||
// we create a wshproxy to handle rpc messages to/from the window
|
||||
|
@ -5,7 +5,6 @@
|
||||
package wps
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -76,7 +75,7 @@ func (b *BrokerType) GetClient() Client {
|
||||
|
||||
// if already subscribed, this will *resubscribe* with the new subscription (remove the old one, and replace with this one)
|
||||
func (b *BrokerType) Subscribe(subRouteId string, sub SubscriptionRequest) {
|
||||
log.Printf("[wps] sub %s %s\n", subRouteId, sub.Event)
|
||||
// log.Printf("[wps] sub %s %s\n", subRouteId, sub.Event)
|
||||
if sub.Event == "" {
|
||||
return
|
||||
}
|
||||
@ -138,7 +137,7 @@ func addStrToScopeMap(scopeMap map[string][]string, scope string, routeId string
|
||||
}
|
||||
|
||||
func (b *BrokerType) Unsubscribe(subRouteId string, eventName string) {
|
||||
log.Printf("[wps] unsub %s %s\n", subRouteId, eventName)
|
||||
// log.Printf("[wps] unsub %s %s\n", subRouteId, eventName)
|
||||
b.Lock.Lock()
|
||||
defer b.Lock.Unlock()
|
||||
b.unsubscribe_nolock(subRouteId, eventName)
|
||||
|
@ -120,7 +120,7 @@ func (ws *WshServer) GetMetaCommand(ctx context.Context, data wshrpc.CommandGetM
|
||||
}
|
||||
|
||||
func (ws *WshServer) SetMetaCommand(ctx context.Context, data wshrpc.CommandSetMetaData) error {
|
||||
log.Printf("SETMETA: %s | %v\n", data.ORef, data.Meta)
|
||||
log.Printf("SetMetaCommand: %s | %v\n", data.ORef, data.Meta)
|
||||
oref := data.ORef
|
||||
err := wstore.UpdateObjectMeta(ctx, oref, data.Meta)
|
||||
if err != nil {
|
||||
@ -422,7 +422,7 @@ func (ws *WshServer) EventPublishCommand(ctx context.Context, data wps.WaveEvent
|
||||
}
|
||||
|
||||
func (ws *WshServer) EventSubCommand(ctx context.Context, data wps.SubscriptionRequest) error {
|
||||
log.Printf("EventSubCommand: %v\n", data)
|
||||
// log.Printf("EventSubCommand: %v\n", data)
|
||||
rpcSource := wshutil.GetRpcSourceFromContext(ctx)
|
||||
if rpcSource == "" {
|
||||
return fmt.Errorf("no rpc source set")
|
||||
|
@ -52,14 +52,14 @@ func MakeControllerRouteId(blockId string) string {
|
||||
return "controller:" + blockId
|
||||
}
|
||||
|
||||
func MakeWindowRouteId(windowId string) string {
|
||||
return "window:" + windowId
|
||||
}
|
||||
|
||||
func MakeProcRouteId(procId string) string {
|
||||
return "proc:" + procId
|
||||
}
|
||||
|
||||
func MakeTabRouteId(tabId string) string {
|
||||
return "tab:" + tabId
|
||||
}
|
||||
|
||||
var DefaultRouter = NewWshRouter()
|
||||
|
||||
func NewWshRouter() *WshRouter {
|
||||
|
@ -277,3 +277,13 @@ func DBFindTabForBlockId(ctx context.Context, blockId string) (string, error) {
|
||||
return tx.GetString(query, blockId), nil
|
||||
})
|
||||
}
|
||||
|
||||
func DBFindWorkspaceForTabId(ctx context.Context, tabId string) (string, error) {
|
||||
return WithTxRtn(ctx, func(tx *TxWrap) (string, error) {
|
||||
query := `
|
||||
SELECT w.oid
|
||||
FROM db_workspace w, json_each(data->'tabids') je
|
||||
WHERE je.value = ?`
|
||||
return tx.GetString(query, tabId), nil
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user