2024-06-12 02:42:10 +02:00
|
|
|
// Copyright 2024, Command Line Inc.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
2024-11-20 04:41:53 +01:00
|
|
|
import { RpcApi } from "@/app/store/wshclientapi";
|
2024-06-12 02:42:10 +02:00
|
|
|
import * as electron from "electron";
|
Update data and config paths to match platform defaults (#1047)
Going forward for new installations, config and data files will be
stored at the platform default paths, as defined by
[env-paths](https://www.npmjs.com/package/env-paths).
For backwards compatibility, if the `~/.waveterm` or `WAVETERM_HOME`
directory exists and contains valid data, it will be used. If this check
fails, then `WAVETERM_DATA_HOME` and `WAVETERM_CONFIG_HOME` will be
used. If these are not defined, then `XDG_DATA_HOME` and
`XDG_CONFIG_HOME` will be used. Finally, if none of these are defined,
the [env-paths](https://www.npmjs.com/package/env-paths) defaults will
be used.
As with the existing app, dev instances will write to `waveterm-dev`
directories, while all others will write to `waveterm`.
2024-10-22 18:26:58 +02:00
|
|
|
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 { sprintf } from "sprintf-js";
|
|
|
|
import { Readable } from "stream";
|
|
|
|
import * as services from "../frontend/app/store/services";
|
|
|
|
import { initElectronWshrpc, shutdownWshrpc } from "../frontend/app/store/wshrpcutil";
|
|
|
|
import { getWebServerEndpoint } from "../frontend/util/endpoints";
|
|
|
|
import * as keyutil from "../frontend/util/keyutil";
|
|
|
|
import { fireAndForget } from "../frontend/util/util";
|
|
|
|
import { AuthKey, configureAuthKeyRequestInjection } from "./authkey";
|
|
|
|
import { initDocsite } from "./docsite";
|
2024-10-17 23:34:02 +02:00
|
|
|
import {
|
|
|
|
getActivityState,
|
|
|
|
getForceQuit,
|
|
|
|
getGlobalIsRelaunching,
|
|
|
|
setForceQuit,
|
|
|
|
setGlobalIsQuitting,
|
|
|
|
setGlobalIsStarting,
|
|
|
|
setWasActive,
|
|
|
|
setWasInFg,
|
Update data and config paths to match platform defaults (#1047)
Going forward for new installations, config and data files will be
stored at the platform default paths, as defined by
[env-paths](https://www.npmjs.com/package/env-paths).
For backwards compatibility, if the `~/.waveterm` or `WAVETERM_HOME`
directory exists and contains valid data, it will be used. If this check
fails, then `WAVETERM_DATA_HOME` and `WAVETERM_CONFIG_HOME` will be
used. If these are not defined, then `XDG_DATA_HOME` and
`XDG_CONFIG_HOME` will be used. Finally, if none of these are defined,
the [env-paths](https://www.npmjs.com/package/env-paths) defaults will
be used.
As with the existing app, dev instances will write to `waveterm-dev`
directories, while all others will write to `waveterm`.
2024-10-22 18:26:58 +02:00
|
|
|
} from "./emain-activity";
|
2024-12-02 19:56:56 +01:00
|
|
|
import { ensureHotSpareTab, getWaveTabViewByWebContentsId, setMaxTabCacheSize } from "./emain-tabview";
|
Update data and config paths to match platform defaults (#1047)
Going forward for new installations, config and data files will be
stored at the platform default paths, as defined by
[env-paths](https://www.npmjs.com/package/env-paths).
For backwards compatibility, if the `~/.waveterm` or `WAVETERM_HOME`
directory exists and contains valid data, it will be used. If this check
fails, then `WAVETERM_DATA_HOME` and `WAVETERM_CONFIG_HOME` will be
used. If these are not defined, then `XDG_DATA_HOME` and
`XDG_CONFIG_HOME` will be used. Finally, if none of these are defined,
the [env-paths](https://www.npmjs.com/package/env-paths) defaults will
be used.
As with the existing app, dev instances will write to `waveterm-dev`
directories, while all others will write to `waveterm`.
2024-10-22 18:26:58 +02:00
|
|
|
import { handleCtrlShiftState } from "./emain-util";
|
2024-12-02 19:56:56 +01:00
|
|
|
import { getIsWaveSrvDead, getWaveSrvProc, getWaveSrvReady, getWaveVersion, runWaveSrv } from "./emain-wavesrv";
|
2024-10-17 23:34:02 +02:00
|
|
|
import {
|
|
|
|
createBrowserWindow,
|
2024-12-07 00:33:00 +01:00
|
|
|
createNewWaveWindow,
|
2024-12-02 19:56:56 +01:00
|
|
|
focusedWaveWindow,
|
2024-10-17 23:34:02 +02:00
|
|
|
getAllWaveWindows,
|
|
|
|
getWaveWindowById,
|
|
|
|
getWaveWindowByWebContentsId,
|
2024-12-03 18:38:46 +01:00
|
|
|
getWaveWindowByWorkspaceId,
|
2024-12-07 00:33:00 +01:00
|
|
|
relaunchBrowserWindows,
|
2024-12-02 19:56:56 +01:00
|
|
|
WaveBrowserWindow,
|
|
|
|
} from "./emain-window";
|
2024-09-19 20:04:18 +02:00
|
|
|
import { ElectronWshClient, initElectronWshClient } from "./emain-wsh";
|
2024-09-25 23:43:05 +02:00
|
|
|
import { getLaunchSettings } from "./launchsettings";
|
2024-12-07 00:33:00 +01:00
|
|
|
import { log } from "./log";
|
2024-12-07 04:10:34 +01:00
|
|
|
import { makeAppMenu } from "./menu";
|
2024-08-22 00:04:39 +02:00
|
|
|
import {
|
|
|
|
getElectronAppBasePath,
|
2024-10-04 05:28:05 +02:00
|
|
|
getElectronAppUnpackedBasePath,
|
Update data and config paths to match platform defaults (#1047)
Going forward for new installations, config and data files will be
stored at the platform default paths, as defined by
[env-paths](https://www.npmjs.com/package/env-paths).
For backwards compatibility, if the `~/.waveterm` or `WAVETERM_HOME`
directory exists and contains valid data, it will be used. If this check
fails, then `WAVETERM_DATA_HOME` and `WAVETERM_CONFIG_HOME` will be
used. If these are not defined, then `XDG_DATA_HOME` and
`XDG_CONFIG_HOME` will be used. Finally, if none of these are defined,
the [env-paths](https://www.npmjs.com/package/env-paths) defaults will
be used.
As with the existing app, dev instances will write to `waveterm-dev`
directories, while all others will write to `waveterm`.
2024-10-22 18:26:58 +02:00
|
|
|
getWaveConfigDir,
|
|
|
|
getWaveDataDir,
|
2024-08-22 00:04:39 +02:00
|
|
|
isDev,
|
|
|
|
unameArch,
|
|
|
|
unamePlatform,
|
|
|
|
} from "./platform";
|
2024-08-06 20:05:26 +02:00
|
|
|
import { configureAutoUpdater, updater } from "./updater";
|
2024-06-12 02:42:10 +02:00
|
|
|
|
|
|
|
const electronApp = electron.app;
|
|
|
|
|
Update data and config paths to match platform defaults (#1047)
Going forward for new installations, config and data files will be
stored at the platform default paths, as defined by
[env-paths](https://www.npmjs.com/package/env-paths).
For backwards compatibility, if the `~/.waveterm` or `WAVETERM_HOME`
directory exists and contains valid data, it will be used. If this check
fails, then `WAVETERM_DATA_HOME` and `WAVETERM_CONFIG_HOME` will be
used. If these are not defined, then `XDG_DATA_HOME` and
`XDG_CONFIG_HOME` will be used. Finally, if none of these are defined,
the [env-paths](https://www.npmjs.com/package/env-paths) defaults will
be used.
As with the existing app, dev instances will write to `waveterm-dev`
directories, while all others will write to `waveterm`.
2024-10-22 18:26:58 +02:00
|
|
|
const waveDataDir = getWaveDataDir();
|
|
|
|
const waveConfigDir = getWaveConfigDir();
|
|
|
|
|
2024-07-12 21:12:34 +02:00
|
|
|
electron.nativeTheme.themeSource = "dark";
|
2024-06-12 02:42:10 +02:00
|
|
|
|
2024-08-30 01:06:15 +02:00
|
|
|
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
|
2024-07-18 03:42:49 +02:00
|
|
|
|
|
|
|
console.log = log;
|
|
|
|
console.log(
|
|
|
|
sprintf(
|
Update data and config paths to match platform defaults (#1047)
Going forward for new installations, config and data files will be
stored at the platform default paths, as defined by
[env-paths](https://www.npmjs.com/package/env-paths).
For backwards compatibility, if the `~/.waveterm` or `WAVETERM_HOME`
directory exists and contains valid data, it will be used. If this check
fails, then `WAVETERM_DATA_HOME` and `WAVETERM_CONFIG_HOME` will be
used. If these are not defined, then `XDG_DATA_HOME` and
`XDG_CONFIG_HOME` will be used. Finally, if none of these are defined,
the [env-paths](https://www.npmjs.com/package/env-paths) defaults will
be used.
As with the existing app, dev instances will write to `waveterm-dev`
directories, while all others will write to `waveterm`.
2024-10-22 18:26:58 +02:00
|
|
|
"waveterm-app starting, data_dir=%s, config_dir=%s electronpath=%s gopath=%s arch=%s/%s",
|
2024-12-03 00:03:20 +01:00
|
|
|
waveDataDir,
|
Update data and config paths to match platform defaults (#1047)
Going forward for new installations, config and data files will be
stored at the platform default paths, as defined by
[env-paths](https://www.npmjs.com/package/env-paths).
For backwards compatibility, if the `~/.waveterm` or `WAVETERM_HOME`
directory exists and contains valid data, it will be used. If this check
fails, then `WAVETERM_DATA_HOME` and `WAVETERM_CONFIG_HOME` will be
used. If these are not defined, then `XDG_DATA_HOME` and
`XDG_CONFIG_HOME` will be used. Finally, if none of these are defined,
the [env-paths](https://www.npmjs.com/package/env-paths) defaults will
be used.
As with the existing app, dev instances will write to `waveterm-dev`
directories, while all others will write to `waveterm`.
2024-10-22 18:26:58 +02:00
|
|
|
waveConfigDir,
|
2024-07-18 03:42:49 +02:00
|
|
|
getElectronAppBasePath(),
|
2024-10-04 05:28:05 +02:00
|
|
|
getElectronAppUnpackedBasePath(),
|
2024-07-18 03:42:49 +02:00
|
|
|
unamePlatform,
|
|
|
|
unameArch
|
|
|
|
)
|
|
|
|
);
|
|
|
|
if (isDev) {
|
|
|
|
console.log("waveterm-app WAVETERM_DEV set");
|
2024-06-12 02:42:10 +02:00
|
|
|
}
|
|
|
|
|
2024-12-02 19:56:56 +01:00
|
|
|
function handleWSEvent(evtMsg: WSEventType) {
|
|
|
|
fireAndForget(async () => {
|
|
|
|
console.log("handleWSEvent", evtMsg?.eventtype);
|
|
|
|
if (evtMsg.eventtype == "electron:newwindow") {
|
|
|
|
console.log("electron:newwindow", evtMsg.data);
|
|
|
|
const windowId: string = evtMsg.data;
|
|
|
|
const windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow;
|
|
|
|
if (windowData == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const fullConfig = await services.FileService.GetFullConfig();
|
|
|
|
const newWin = await createBrowserWindow(windowData, fullConfig, { unamePlatform });
|
|
|
|
newWin.show();
|
|
|
|
} else if (evtMsg.eventtype == "electron:closewindow") {
|
|
|
|
console.log("electron:closewindow", evtMsg.data);
|
|
|
|
if (evtMsg.data === undefined) return;
|
|
|
|
const ww = getWaveWindowById(evtMsg.data);
|
|
|
|
if (ww != null) {
|
|
|
|
ww.destroy(); // bypass the "are you sure?" dialog
|
|
|
|
}
|
2024-12-03 18:38:46 +01:00
|
|
|
} else if (evtMsg.eventtype == "electron:updateactivetab") {
|
|
|
|
const activeTabUpdate: { workspaceid: string; newactivetabid: string } = evtMsg.data;
|
|
|
|
console.log("electron:updateactivetab", activeTabUpdate);
|
|
|
|
const ww = getWaveWindowByWorkspaceId(activeTabUpdate.workspaceid);
|
|
|
|
if (ww == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
await ww.setActiveTab(activeTabUpdate.newactivetabid, false);
|
2024-12-02 19:56:56 +01:00
|
|
|
} else {
|
|
|
|
console.log("unhandled electron ws eventtype", evtMsg.eventtype);
|
2024-07-23 01:39:45 +02:00
|
|
|
}
|
2024-12-02 19:56:56 +01:00
|
|
|
});
|
2024-06-25 23:56:37 +02:00
|
|
|
}
|
|
|
|
|
2024-06-28 03:09:30 +02:00
|
|
|
// Listen for the open-external event from the renderer process
|
|
|
|
electron.ipcMain.on("open-external", (event, url) => {
|
|
|
|
if (url && typeof url === "string") {
|
|
|
|
electron.shell.openExternal(url).catch((err) => {
|
|
|
|
console.error(`Failed to open URL ${url}:`, err);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
console.error("Invalid URL received in open-external event:", url);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-10-05 01:34:05 +02:00
|
|
|
type UrlInSessionResult = {
|
|
|
|
stream: Readable;
|
|
|
|
mimeType: string;
|
|
|
|
fileName: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
function getSingleHeaderVal(headers: Record<string, string | string[]>, key: string): string {
|
|
|
|
const val = headers[key];
|
|
|
|
if (val == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if (Array.isArray(val)) {
|
|
|
|
return val[0];
|
|
|
|
}
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
|
|
|
|
function cleanMimeType(mimeType: string): string {
|
|
|
|
if (mimeType == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const parts = mimeType.split(";");
|
|
|
|
return parts[0].trim();
|
|
|
|
}
|
|
|
|
|
|
|
|
function getFileNameFromUrl(url: string): string {
|
|
|
|
try {
|
|
|
|
const pathname = new URL(url).pathname;
|
|
|
|
const filename = pathname.substring(pathname.lastIndexOf("/") + 1);
|
|
|
|
return filename;
|
|
|
|
} catch (e) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getUrlInSession(session: Electron.Session, url: string): Promise<UrlInSessionResult> {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
// Handle data URLs directly
|
|
|
|
if (url.startsWith("data:")) {
|
|
|
|
const parts = url.split(",");
|
|
|
|
if (parts.length < 2) {
|
|
|
|
return reject(new Error("Invalid data URL"));
|
|
|
|
}
|
|
|
|
const header = parts[0]; // Get the data URL header (e.g., data:image/png;base64)
|
|
|
|
const base64Data = parts[1]; // Get the base64 data part
|
|
|
|
const mimeType = header.split(";")[0].slice(5); // Extract the MIME type (after "data:")
|
|
|
|
const buffer = Buffer.from(base64Data, "base64");
|
|
|
|
const readable = Readable.from(buffer);
|
|
|
|
resolve({ stream: readable, mimeType, fileName: "image" });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const request = electron.net.request({
|
|
|
|
url,
|
|
|
|
method: "GET",
|
|
|
|
session, // Attach the session directly to the request
|
|
|
|
});
|
|
|
|
const readable = new Readable({
|
|
|
|
read() {}, // No-op, we'll push data manually
|
|
|
|
});
|
|
|
|
request.on("response", (response) => {
|
|
|
|
const mimeType = cleanMimeType(getSingleHeaderVal(response.headers, "content-type"));
|
|
|
|
const fileName = getFileNameFromUrl(url) || "image";
|
|
|
|
response.on("data", (chunk) => {
|
|
|
|
readable.push(chunk); // Push data to the readable stream
|
|
|
|
});
|
|
|
|
response.on("end", () => {
|
|
|
|
readable.push(null); // Signal the end of the stream
|
|
|
|
resolve({ stream: readable, mimeType, fileName });
|
|
|
|
});
|
|
|
|
});
|
|
|
|
request.on("error", (err) => {
|
|
|
|
readable.destroy(err); // Destroy the stream on error
|
|
|
|
reject(err);
|
|
|
|
});
|
|
|
|
request.end();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
electron.ipcMain.on("webview-image-contextmenu", (event: electron.IpcMainEvent, payload: { src: string }) => {
|
|
|
|
const menu = new electron.Menu();
|
2024-10-17 23:34:02 +02:00
|
|
|
const win = getWaveWindowByWebContentsId(event.sender.hostWebContents.id);
|
2024-10-05 01:34:05 +02:00
|
|
|
if (win == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
menu.append(
|
|
|
|
new electron.MenuItem({
|
|
|
|
label: "Save Image",
|
|
|
|
click: () => {
|
|
|
|
const resultP = getUrlInSession(event.sender.session, payload.src);
|
|
|
|
resultP
|
|
|
|
.then((result) => {
|
|
|
|
saveImageFileWithNativeDialog(result.fileName, result.mimeType, result.stream);
|
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
console.log("error getting image", e);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
const { x, y } = electron.screen.getCursorScreenPoint();
|
|
|
|
const windowPos = win.getPosition();
|
2024-10-17 23:34:02 +02:00
|
|
|
menu.popup();
|
2024-10-05 01:34:05 +02:00
|
|
|
});
|
|
|
|
|
2024-06-26 21:14:59 +02:00
|
|
|
electron.ipcMain.on("download", (event, payload) => {
|
2024-07-18 03:49:27 +02:00
|
|
|
const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?path=" + encodeURIComponent(payload.filePath);
|
2024-10-17 23:34:02 +02:00
|
|
|
event.sender.downloadURL(streamingUrl);
|
|
|
|
});
|
|
|
|
|
2024-08-22 00:04:39 +02:00
|
|
|
electron.ipcMain.on("get-cursor-point", (event) => {
|
2024-10-17 23:34:02 +02:00
|
|
|
const tabView = getWaveTabViewByWebContentsId(event.sender.id);
|
|
|
|
if (tabView == null) {
|
|
|
|
event.returnValue = null;
|
|
|
|
return;
|
|
|
|
}
|
2024-06-19 20:15:14 +02:00
|
|
|
const screenPoint = electron.screen.getCursorScreenPoint();
|
2024-10-17 23:34:02 +02:00
|
|
|
const windowRect = tabView.getBounds();
|
2024-06-19 21:20:20 +02:00
|
|
|
const retVal: Electron.Point = {
|
2024-06-19 20:15:14 +02:00
|
|
|
x: screenPoint.x - windowRect.x,
|
|
|
|
y: screenPoint.y - windowRect.y,
|
|
|
|
};
|
|
|
|
event.returnValue = retVal;
|
|
|
|
});
|
|
|
|
|
2024-08-22 00:04:39 +02:00
|
|
|
electron.ipcMain.on("get-env", (event, varName) => {
|
2024-07-18 03:42:49 +02:00
|
|
|
event.returnValue = process.env[varName] ?? null;
|
|
|
|
});
|
|
|
|
|
2024-09-04 06:45:44 +02:00
|
|
|
electron.ipcMain.on("get-about-modal-details", (event) => {
|
2024-10-17 23:34:02 +02:00
|
|
|
event.returnValue = getWaveVersion() as AboutModalDetails;
|
2024-09-04 06:45:44 +02:00
|
|
|
});
|
|
|
|
|
2024-08-30 01:06:15 +02:00
|
|
|
const hasBeforeInputRegisteredMap = new Map<number, boolean>();
|
|
|
|
|
|
|
|
electron.ipcMain.on("webview-focus", (event: Electron.IpcMainEvent, focusedId: number) => {
|
|
|
|
webviewFocusId = focusedId;
|
|
|
|
console.log("webview-focus", focusedId);
|
|
|
|
if (focusedId == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const parentWc = event.sender;
|
|
|
|
const webviewWc = electron.webContents.fromId(focusedId);
|
|
|
|
if (webviewWc == null) {
|
|
|
|
webviewFocusId = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!hasBeforeInputRegisteredMap.get(focusedId)) {
|
|
|
|
hasBeforeInputRegisteredMap.set(focusedId, true);
|
|
|
|
webviewWc.on("before-input-event", (e, input) => {
|
|
|
|
let waveEvent = keyutil.adaptFromElectronKeyEvent(input);
|
|
|
|
// console.log(`WEB ${focusedId}`, waveEvent.type, waveEvent.code);
|
|
|
|
handleCtrlShiftState(parentWc, waveEvent);
|
|
|
|
if (webviewFocusId != focusedId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (input.type != "keyDown") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for (let keyDesc of webviewKeys) {
|
|
|
|
if (keyutil.checkKeyPressed(waveEvent, keyDesc)) {
|
|
|
|
e.preventDefault();
|
|
|
|
parentWc.send("reinject-key", waveEvent);
|
|
|
|
console.log("webview reinject-key", keyDesc);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
webviewWc.on("destroyed", () => {
|
|
|
|
hasBeforeInputRegisteredMap.delete(focusedId);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
electron.ipcMain.on("register-global-webview-keys", (event, keys: string[]) => {
|
|
|
|
webviewKeys = keys ?? [];
|
|
|
|
});
|
|
|
|
|
2024-08-21 00:58:21 +02:00
|
|
|
if (unamePlatform !== "darwin") {
|
|
|
|
const fac = new FastAverageColor();
|
|
|
|
|
|
|
|
electron.ipcMain.on("update-window-controls-overlay", async (event, rect: Dimensions) => {
|
2024-09-25 07:05:38 +02:00
|
|
|
// Bail out if the user requests the native titlebar
|
|
|
|
const fullConfig = await services.FileService.GetFullConfig();
|
|
|
|
if (fullConfig.settings["window:nativetitlebar"]) return;
|
|
|
|
|
2024-08-19 23:16:09 +02:00
|
|
|
const zoomFactor = event.sender.getZoomFactor();
|
|
|
|
const electronRect: Electron.Rectangle = {
|
|
|
|
x: rect.left * zoomFactor,
|
|
|
|
y: rect.top * zoomFactor,
|
|
|
|
height: rect.height * zoomFactor,
|
|
|
|
width: rect.width * zoomFactor,
|
|
|
|
};
|
|
|
|
const overlay = await event.sender.capturePage(electronRect);
|
|
|
|
const overlayBuffer = overlay.toPNG();
|
2024-08-20 22:18:47 +02:00
|
|
|
const png = PNG.sync.read(overlayBuffer);
|
|
|
|
const color = fac.prepareResult(fac.getColorFromArray4(png.data));
|
2024-10-17 23:34:02 +02:00
|
|
|
const ww = getWaveWindowByWebContentsId(event.sender.id);
|
|
|
|
ww.setTitleBarOverlay({
|
2024-08-19 23:16:09 +02:00
|
|
|
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",
|
|
|
|
});
|
2024-08-21 00:58:21 +02:00
|
|
|
});
|
|
|
|
}
|
2024-08-19 23:16:09 +02:00
|
|
|
|
2024-10-10 00:12:20 +02:00
|
|
|
electron.ipcMain.on("quicklook", (event, filePath: string) => {
|
|
|
|
if (unamePlatform == "darwin") {
|
|
|
|
child_process.execFile("/usr/bin/qlmanage", ["-p", filePath], (error, stdout, stderr) => {
|
|
|
|
if (error) {
|
|
|
|
console.error(`Error opening Quick Look: ${error}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-10-31 20:34:30 +01:00
|
|
|
electron.ipcMain.on("open-native-path", (event, filePath: string) => {
|
|
|
|
console.log("open-native-path", filePath);
|
2024-12-06 03:09:54 +01:00
|
|
|
fireAndForget(() =>
|
2024-12-03 07:23:44 +01:00
|
|
|
electron.shell.openPath(filePath).then((excuse) => {
|
|
|
|
if (excuse) console.error(`Failed to open ${filePath} in native application: ${excuse}`);
|
|
|
|
})
|
|
|
|
);
|
2024-10-31 20:34:30 +01:00
|
|
|
});
|
|
|
|
|
2024-10-17 23:34:02 +02:00
|
|
|
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") {
|
|
|
|
tabView.initResolve();
|
|
|
|
if (tabView.savedInitOpts) {
|
2024-12-06 09:10:17 +01:00
|
|
|
// this handles the "reload" case. we'll re-send the init opts to the frontend
|
|
|
|
console.log("savedInitOpts calling wave-init", tabView.waveTabId);
|
2024-10-17 23:34:02 +02:00
|
|
|
tabView.webContents.send("wave-init", tabView.savedInitOpts);
|
|
|
|
}
|
|
|
|
} else if (status === "wave-ready") {
|
|
|
|
tabView.waveReadyResolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
electron.ipcMain.on("fe-log", (event, logStr: string) => {
|
|
|
|
console.log("fe-log", logStr);
|
|
|
|
});
|
|
|
|
|
2024-10-05 01:34:05 +02:00
|
|
|
function saveImageFileWithNativeDialog(defaultFileName: string, mimeType: string, readStream: Readable) {
|
|
|
|
if (defaultFileName == null || defaultFileName == "") {
|
|
|
|
defaultFileName = "image";
|
|
|
|
}
|
2024-12-02 19:56:56 +01:00
|
|
|
const ww = focusedWaveWindow;
|
2024-12-10 19:58:50 +01:00
|
|
|
if (ww == null) {
|
|
|
|
return;
|
|
|
|
}
|
2024-10-05 01:34:05 +02:00
|
|
|
const mimeToExtension: { [key: string]: string } = {
|
|
|
|
"image/png": "png",
|
|
|
|
"image/jpeg": "jpg",
|
|
|
|
"image/gif": "gif",
|
|
|
|
"image/webp": "webp",
|
|
|
|
"image/bmp": "bmp",
|
|
|
|
"image/tiff": "tiff",
|
|
|
|
"image/heic": "heic",
|
|
|
|
};
|
|
|
|
function addExtensionIfNeeded(fileName: string, mimeType: string): string {
|
|
|
|
const extension = mimeToExtension[mimeType];
|
|
|
|
if (!path.extname(fileName) && extension) {
|
|
|
|
return `${fileName}.${extension}`;
|
|
|
|
}
|
|
|
|
return fileName;
|
|
|
|
}
|
|
|
|
defaultFileName = addExtensionIfNeeded(defaultFileName, mimeType);
|
|
|
|
electron.dialog
|
2024-10-17 23:34:02 +02:00
|
|
|
.showSaveDialog(ww, {
|
2024-10-05 01:34:05 +02:00
|
|
|
title: "Save Image",
|
|
|
|
defaultPath: defaultFileName,
|
|
|
|
filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "gif", "webp", "bmp", "tiff", "heic"] }],
|
|
|
|
})
|
|
|
|
.then((file) => {
|
|
|
|
if (file.canceled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const writeStream = fs.createWriteStream(file.filePath);
|
|
|
|
readStream.pipe(writeStream);
|
|
|
|
writeStream.on("finish", () => {
|
|
|
|
console.log("saved file", file.filePath);
|
|
|
|
});
|
|
|
|
writeStream.on("error", (err) => {
|
|
|
|
console.log("error saving file (writeStream)", err);
|
|
|
|
readStream.destroy();
|
|
|
|
});
|
|
|
|
readStream.on("error", (err) => {
|
|
|
|
console.error("error saving file (readStream)", err);
|
|
|
|
writeStream.destroy(); // Stop the write stream
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.log("error trying to save file", err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-08-22 00:04:39 +02:00
|
|
|
electron.ipcMain.on("open-new-window", () => fireAndForget(createNewWaveWindow));
|
2024-06-20 00:42:19 +02:00
|
|
|
|
2024-11-20 04:41:53 +01:00
|
|
|
// we try to set the primary display as index [0]
|
|
|
|
function getActivityDisplays(): ActivityDisplayType[] {
|
|
|
|
const displays = electron.screen.getAllDisplays();
|
|
|
|
const primaryDisplay = electron.screen.getPrimaryDisplay();
|
|
|
|
const rtn: ActivityDisplayType[] = [];
|
|
|
|
for (const display of displays) {
|
|
|
|
const adt = {
|
|
|
|
width: display.size.width,
|
|
|
|
height: display.size.height,
|
|
|
|
dpr: display.scaleFactor,
|
|
|
|
internal: display.internal,
|
|
|
|
};
|
|
|
|
if (display.id === primaryDisplay?.id) {
|
|
|
|
rtn.unshift(adt);
|
|
|
|
} else {
|
|
|
|
rtn.push(adt);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return rtn;
|
|
|
|
}
|
|
|
|
|
2024-12-02 19:56:56 +01:00
|
|
|
function logActiveState() {
|
|
|
|
fireAndForget(async () => {
|
|
|
|
const astate = getActivityState();
|
|
|
|
const activity: ActivityUpdate = { openminutes: 1 };
|
|
|
|
if (astate.wasInFg) {
|
|
|
|
activity.fgminutes = 1;
|
|
|
|
}
|
|
|
|
if (astate.wasActive) {
|
|
|
|
activity.activeminutes = 1;
|
|
|
|
}
|
|
|
|
activity.displays = getActivityDisplays();
|
|
|
|
try {
|
|
|
|
await RpcApi.ActivityCommand(ElectronWshClient, activity, { noresponse: true });
|
|
|
|
} catch (e) {
|
|
|
|
console.log("error logging active state", e);
|
|
|
|
} finally {
|
|
|
|
// for next iteration
|
|
|
|
const ww = focusedWaveWindow;
|
|
|
|
setWasInFg(ww?.isFocused() ?? false);
|
|
|
|
setWasActive(false);
|
|
|
|
}
|
|
|
|
});
|
2024-08-09 03:24:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// this isn't perfect, but gets the job done without being complicated
|
|
|
|
function runActiveTimer() {
|
|
|
|
logActiveState();
|
|
|
|
setTimeout(runActiveTimer, 60000);
|
|
|
|
}
|
|
|
|
|
2024-10-28 17:56:42 +01:00
|
|
|
function hideWindowWithCatch(window: WaveBrowserWindow) {
|
|
|
|
if (window == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
if (window.isDestroyed()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
window.hide();
|
|
|
|
} catch (e) {
|
|
|
|
console.log("error hiding window", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-22 22:33:10 +02:00
|
|
|
electronApp.on("window-all-closed", () => {
|
2024-10-17 23:34:02 +02:00
|
|
|
if (getGlobalIsRelaunching()) {
|
2024-08-01 11:05:48 +02:00
|
|
|
return;
|
|
|
|
}
|
2024-07-20 00:59:47 +02:00
|
|
|
if (unamePlatform !== "darwin") {
|
2024-07-22 22:33:10 +02:00
|
|
|
electronApp.quit();
|
2024-07-20 00:59:47 +02:00
|
|
|
}
|
|
|
|
});
|
2024-10-06 22:40:02 +02:00
|
|
|
electronApp.on("before-quit", (e) => {
|
2024-10-17 23:34:02 +02:00
|
|
|
setGlobalIsQuitting(true);
|
2024-08-08 20:55:44 +02:00
|
|
|
updater?.stop();
|
2024-10-09 03:39:26 +02:00
|
|
|
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;
|
|
|
|
}
|
2024-10-17 23:34:02 +02:00
|
|
|
getWaveSrvProc()?.kill("SIGINT");
|
2024-10-06 22:40:02 +02:00
|
|
|
shutdownWshrpc();
|
2024-10-17 23:34:02 +02:00
|
|
|
if (getForceQuit()) {
|
2024-10-06 22:40:02 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
2024-10-17 23:34:02 +02:00
|
|
|
const allWindows = getAllWaveWindows();
|
2024-10-06 22:40:02 +02:00
|
|
|
for (const window of allWindows) {
|
2024-10-28 17:56:42 +01:00
|
|
|
hideWindowWithCatch(window);
|
2024-10-06 22:40:02 +02:00
|
|
|
}
|
2024-10-17 23:34:02 +02:00
|
|
|
if (getIsWaveSrvDead()) {
|
2024-10-06 22:40:02 +02:00
|
|
|
console.log("wavesrv is dead, quitting immediately");
|
2024-10-17 23:34:02 +02:00
|
|
|
setForceQuit(true);
|
2024-10-06 22:40:02 +02:00
|
|
|
electronApp.quit();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
setTimeout(() => {
|
|
|
|
console.log("waiting for wavesrv to exit...");
|
2024-10-17 23:34:02 +02:00
|
|
|
setForceQuit(true);
|
2024-10-06 22:40:02 +02:00
|
|
|
electronApp.quit();
|
|
|
|
}, 3000);
|
2024-06-20 04:10:53 +02:00
|
|
|
});
|
2024-07-18 04:16:09 +02:00
|
|
|
process.on("SIGINT", () => {
|
|
|
|
console.log("Caught SIGINT, shutting down");
|
2024-07-22 22:33:10 +02:00
|
|
|
electronApp.quit();
|
2024-07-18 04:16:09 +02:00
|
|
|
});
|
|
|
|
process.on("SIGHUP", () => {
|
|
|
|
console.log("Caught SIGHUP, shutting down");
|
2024-07-22 22:33:10 +02:00
|
|
|
electronApp.quit();
|
2024-07-18 04:16:09 +02:00
|
|
|
});
|
|
|
|
process.on("SIGTERM", () => {
|
|
|
|
console.log("Caught SIGTERM, shutting down");
|
2024-07-22 22:33:10 +02:00
|
|
|
electronApp.quit();
|
2024-07-18 04:16:09 +02:00
|
|
|
});
|
|
|
|
let caughtException = false;
|
|
|
|
process.on("uncaughtException", (error) => {
|
|
|
|
if (caughtException) {
|
|
|
|
return;
|
|
|
|
}
|
2024-12-10 03:36:53 +01:00
|
|
|
|
|
|
|
// Check if the error is related to QUIC protocol, if so, ignore (can happen with the updater)
|
|
|
|
if (error?.message?.includes("net::ERR_QUIC_PROTOCOL_ERROR")) {
|
|
|
|
console.log("Ignoring QUIC protocol error:", error.message);
|
|
|
|
console.log("Stack Trace:", error.stack);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-07-18 04:16:09 +02:00
|
|
|
caughtException = true;
|
2024-10-10 01:15:42 +02:00
|
|
|
console.log("Uncaught Exception, shutting down: ", error);
|
|
|
|
console.log("Stack Trace:", error.stack);
|
2024-07-18 04:16:09 +02:00
|
|
|
// Optionally, handle cleanup or exit the app
|
2024-07-22 22:33:10 +02:00
|
|
|
electronApp.quit();
|
2024-07-18 04:16:09 +02:00
|
|
|
});
|
2024-06-20 04:10:53 +02:00
|
|
|
|
|
|
|
async function appMain() {
|
2024-09-25 23:43:05 +02:00
|
|
|
// Set disableHardwareAcceleration as early as possible, if required.
|
|
|
|
const launchSettings = getLaunchSettings();
|
|
|
|
if (launchSettings?.["window:disablehardwareacceleration"]) {
|
|
|
|
console.log("disabling hardware acceleration, per launch settings");
|
|
|
|
electronApp.disableHardwareAcceleration();
|
|
|
|
}
|
2024-06-12 02:42:10 +02:00
|
|
|
const startTs = Date.now();
|
|
|
|
const instanceLock = electronApp.requestSingleInstanceLock();
|
|
|
|
if (!instanceLock) {
|
|
|
|
console.log("waveterm-app could not get single-instance-lock, shutting down");
|
|
|
|
electronApp.quit();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
2024-10-17 23:34:02 +02:00
|
|
|
await runWaveSrv(handleWSEvent);
|
2024-06-12 02:42:10 +02:00
|
|
|
} catch (e) {
|
|
|
|
console.log(e.toString());
|
|
|
|
}
|
2024-10-17 23:34:02 +02:00
|
|
|
const ready = await getWaveSrvReady();
|
2024-06-12 02:42:10 +02:00
|
|
|
console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms");
|
|
|
|
await electronApp.whenReady();
|
2024-08-26 22:20:37 +02:00
|
|
|
configureAuthKeyRequestInjection(electron.session.defaultSession);
|
2024-10-17 23:34:02 +02:00
|
|
|
const fullConfig = await services.FileService.GetFullConfig();
|
|
|
|
ensureHotSpareTab(fullConfig);
|
2024-08-07 00:32:29 +02:00
|
|
|
await relaunchBrowserWindows();
|
2024-10-04 05:28:05 +02:00
|
|
|
await initDocsite();
|
2024-08-09 03:24:54 +02:00
|
|
|
setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe
|
2024-09-18 08:10:09 +02:00
|
|
|
try {
|
2024-09-19 20:04:18 +02:00
|
|
|
initElectronWshClient();
|
|
|
|
initElectronWshrpc(ElectronWshClient, { authKey: AuthKey });
|
2024-09-18 08:10:09 +02:00
|
|
|
} catch (e) {
|
|
|
|
console.log("error initializing wshrpc", e);
|
|
|
|
}
|
2024-12-07 00:33:00 +01:00
|
|
|
makeAppMenu();
|
2024-09-19 20:04:18 +02:00
|
|
|
await configureAutoUpdater();
|
2024-10-17 23:34:02 +02:00
|
|
|
setGlobalIsStarting(false);
|
|
|
|
if (fullConfig?.settings?.["window:maxtabcachesize"] != null) {
|
|
|
|
setMaxTabCacheSize(fullConfig.settings["window:maxtabcachesize"]);
|
|
|
|
}
|
2024-06-12 02:42:10 +02:00
|
|
|
|
2024-12-02 19:56:56 +01:00
|
|
|
electronApp.on("activate", () => {
|
2024-10-17 23:34:02 +02:00
|
|
|
const allWindows = getAllWaveWindows();
|
|
|
|
if (allWindows.length === 0) {
|
2024-12-02 19:56:56 +01:00
|
|
|
fireAndForget(createNewWaveWindow);
|
2024-06-12 02:42:10 +02:00
|
|
|
}
|
|
|
|
});
|
2024-06-20 04:10:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
appMain().catch((e) => {
|
|
|
|
console.log("appMain error", e);
|
|
|
|
electronApp.quit();
|
|
|
|
});
|