1844 lines
60 KiB
JavaScript
1844 lines
60 KiB
JavaScript
import { createRequire } from "node:module";
|
|
var __create = Object.create;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __toESM = (mod, isNodeMode, target) => {
|
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
for (let key of __getOwnPropNames(mod))
|
|
if (!__hasOwnProp.call(to, key))
|
|
__defProp(to, key, {
|
|
get: () => mod[key],
|
|
enumerable: true
|
|
});
|
|
return to;
|
|
};
|
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
|
// src/main.ts
|
|
import { app, BrowserWindow, ipcMain } from "electron";
|
|
import { createConnection, createServer } from "net";
|
|
import { fileURLToPath } from "url";
|
|
import { dirname, resolve as resolvePath, join as joinPath } from "path";
|
|
import fs from "fs";
|
|
import { createRequire as createRequire2 } from "module";
|
|
|
|
// src/protocol.ts
|
|
var MAGIC = {
|
|
HELLO: 1464813644,
|
|
FRAME: 1464813138,
|
|
MOUSE_MOVE: 1464814925,
|
|
MOUSE_BUTTON: 1464814914,
|
|
KEYBOARD: 1464814402,
|
|
SCROLL: 1464816451,
|
|
ACTION: 1464812353,
|
|
FOCUS_CHANGE: 1464813123,
|
|
WINDOW_META: 1464817485,
|
|
WINDOW_POS: 1464817488,
|
|
SYSCALL: 1464816473,
|
|
FRAME_ACK: 1464813121,
|
|
SURFACE_BUFFER: 1464816450,
|
|
SHM_BUFFER: 1464816461,
|
|
DMABUF_FRAME: 1464812614,
|
|
FORWARD_POINTER: 1464815685,
|
|
FORWARD_KEYBOARD: 1464814405,
|
|
FORWARD_RELATIVE_POINTER: 1464816197,
|
|
POINTER_LOCK_REQUEST: 1464815692,
|
|
ENV_UPDATE: 1464812885
|
|
};
|
|
function stringToBuffer(str) {
|
|
return Buffer.from(str, "utf-8");
|
|
}
|
|
|
|
// src/damage-buffer.ts
|
|
import { writeSync } from "fs";
|
|
var ARGB8888 = fourccCode("A", "R", "2", "4");
|
|
|
|
class DamageBuffer {
|
|
width;
|
|
height;
|
|
stride;
|
|
frame;
|
|
constructor(width, height, stride = width * 4) {
|
|
this.width = width;
|
|
this.height = height;
|
|
this.stride = stride;
|
|
this.frame = Buffer.alloc(this.stride * this.height);
|
|
}
|
|
getWidth() {
|
|
return this.width;
|
|
}
|
|
getHeight() {
|
|
return this.height;
|
|
}
|
|
getStride() {
|
|
return this.stride;
|
|
}
|
|
getFrameBuffer() {
|
|
return this.frame;
|
|
}
|
|
reset(width, height, stride = width * 4) {
|
|
this.width = width;
|
|
this.height = height;
|
|
this.stride = stride;
|
|
this.frame = Buffer.alloc(this.stride * this.height);
|
|
}
|
|
applyFullFrame(frame, frameStride = this.width * 4) {
|
|
if (frameStride === this.stride) {
|
|
Buffer.from(frame).copy(this.frame, 0, 0, this.frame.length);
|
|
return;
|
|
}
|
|
const rowBytes = Math.min(frameStride, this.stride);
|
|
for (let y = 0;y < this.height; y += 1) {
|
|
const srcStart = y * frameStride;
|
|
const dstStart = y * this.stride;
|
|
Buffer.from(frame).copy(this.frame, dstStart, srcStart, srcStart + rowBytes);
|
|
}
|
|
}
|
|
applyPatch(patch) {
|
|
const rect = clampRect(patch.rect, this.width, this.height);
|
|
if (rect.width <= 0 || rect.height <= 0) {
|
|
return;
|
|
}
|
|
const patchStride = patch.stride ?? rect.width * 4;
|
|
const dstStartX = rect.x * 4;
|
|
const rowBytes = rect.width * 4;
|
|
for (let row = 0;row < rect.height; row += 1) {
|
|
const srcStart = row * patchStride;
|
|
const dstStart = (rect.y + row) * this.stride + dstStartX;
|
|
Buffer.from(patch.rgba).copy(this.frame, dstStart, srcStart, srcStart + rowBytes);
|
|
}
|
|
}
|
|
applyPatches(patches) {
|
|
for (const patch of patches) {
|
|
this.applyPatch(patch);
|
|
}
|
|
}
|
|
}
|
|
|
|
class WoDmabufSender {
|
|
socket;
|
|
windowName;
|
|
native;
|
|
seq = 1n;
|
|
constructor(socket, windowName, native) {
|
|
this.socket = socket;
|
|
this.windowName = windowName;
|
|
this.native = native;
|
|
}
|
|
send(buffer) {
|
|
const imported = this.native.importRgba(buffer.getFrameBuffer(), buffer.getWidth(), buffer.getHeight(), buffer.getStride());
|
|
try {
|
|
const socketFd = getSocketFd(this.socket);
|
|
const nameBuf = Buffer.from(this.windowName, "utf8");
|
|
const header = Buffer.alloc(4 + 4 + nameBuf.length + 8 + 4 + 4 + 4 + 4);
|
|
let offset = 0;
|
|
header.writeUInt32LE(MAGIC.FRAME, offset);
|
|
offset += 4;
|
|
header.writeUInt32LE(nameBuf.length, offset);
|
|
offset += 4;
|
|
nameBuf.copy(header, offset);
|
|
offset += nameBuf.length;
|
|
header.writeBigUInt64LE(this.seq, offset);
|
|
offset += 8;
|
|
header.writeUInt32LE(buffer.getWidth(), offset);
|
|
offset += 4;
|
|
header.writeUInt32LE(buffer.getHeight(), offset);
|
|
offset += 4;
|
|
header.writeUInt32LE(ARGB8888, offset);
|
|
offset += 4;
|
|
header.writeUInt32LE(1, offset);
|
|
const plane = Buffer.alloc(16);
|
|
plane.writeUInt32LE(imported.offset, 0);
|
|
plane.writeUInt32LE(imported.stride, 4);
|
|
plane.writeUInt32LE(imported.modifier_hi, 8);
|
|
plane.writeUInt32LE(imported.modifier_lo, 12);
|
|
const combined = Buffer.concat([header, plane]);
|
|
writeSync(socketFd, combined, 0, combined.length);
|
|
this.native.sendFd(socketFd, imported.fd);
|
|
const sentSeq = this.seq;
|
|
this.seq += 1n;
|
|
return sentSeq;
|
|
} finally {
|
|
this.native.releaseBuffer(imported.token);
|
|
}
|
|
}
|
|
}
|
|
function applyDamagePayload(buffer, payload) {
|
|
if (payload.width !== buffer.getWidth() || payload.height !== buffer.getHeight()) {
|
|
buffer.reset(payload.width, payload.height);
|
|
}
|
|
if (payload.fullFrame) {
|
|
buffer.applyFullFrame(payload.fullFrame, payload.fullStride ?? payload.width * 4);
|
|
}
|
|
if (payload.patches && payload.patches.length > 0) {
|
|
buffer.applyPatches(payload.patches);
|
|
}
|
|
}
|
|
function loadNativeDmabufModule(requireFn, nativePath) {
|
|
const native = requireFn(nativePath);
|
|
return native;
|
|
}
|
|
function getSocketFd(socket) {
|
|
const fd = socket?._handle?.fd;
|
|
if (typeof fd !== "number" || !Number.isFinite(fd) || fd < 0) {
|
|
throw new Error("Unable to read underlying socket file descriptor");
|
|
}
|
|
return fd;
|
|
}
|
|
function clampRect(rect, width, height) {
|
|
const x = Math.max(0, Math.min(width, rect.x));
|
|
const y = Math.max(0, Math.min(height, rect.y));
|
|
const right = Math.max(x, Math.min(width, rect.x + rect.width));
|
|
const bottom = Math.max(y, Math.min(height, rect.y + rect.height));
|
|
return { x, y, width: Math.max(0, right - x), height: Math.max(0, bottom - y) };
|
|
}
|
|
function fourccCode(a, b, c, d) {
|
|
return (a.charCodeAt(0) | b.charCodeAt(0) << 8 | c.charCodeAt(0) << 16 | d.charCodeAt(0) << 24) >>> 0;
|
|
}
|
|
|
|
// src/main.ts
|
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
var __dirname2 = dirname(__filename2);
|
|
var require2 = createRequire2(import.meta.url);
|
|
var DEBUG_LOG = "/tmp/wo-electron-debug.log";
|
|
function debugLog(...args) {
|
|
const msg = `[${new Date().toISOString()}] ` + args.join(" ") + `
|
|
`;
|
|
try {
|
|
fs.appendFileSync(DEBUG_LOG, msg);
|
|
} catch (e) {}
|
|
console.log(...args);
|
|
}
|
|
debugLog("[Wo] Electron starting, NODE_VERSION=", process.version);
|
|
var IPC_SOCKET = process.env.WO_IPC_SOCKET || "/run/user/1000/wo-ipc.sock";
|
|
debugLog("[Wo] WO_WINDOW_CONFIG env =", process.env.WO_WINDOW_CONFIG);
|
|
var WINDOW_CONFIG = JSON.parse(process.env.WO_WINDOW_CONFIG || "{}");
|
|
debugLog("[Wo] WINDOW_CONFIG parsed =", JSON.stringify(WINDOW_CONFIG));
|
|
var CLIENT_MODE = process.env.WO_CLIENT_MODE === "1";
|
|
var {
|
|
name = "default",
|
|
url = null,
|
|
html = null,
|
|
width = 1920,
|
|
height = 1080
|
|
} = WINDOW_CONFIG;
|
|
var mainWindow = null;
|
|
var ipcSocket = null;
|
|
var portalUiServer = null;
|
|
var portalPendingRequests = new Map;
|
|
var PORTAL_UI_SOCKET = "/tmp/wo-portal-ui.sock";
|
|
var ipcConnected = false;
|
|
var ipcReconnectTimer = null;
|
|
var windowPositionUpdateTimer = null;
|
|
var dmabufSender = null;
|
|
var damageBuffer = null;
|
|
var nativeDmabuf = null;
|
|
var compositorRxBuffer = Buffer.alloc(0);
|
|
var surfaceBufferPending = new Map;
|
|
var surfaceBufferTimer = null;
|
|
var shmReadBufferCache = new Map;
|
|
var shmFdCache = new Map;
|
|
function closeShmFdCache() {
|
|
for (const entry of shmFdCache.values()) {
|
|
try {
|
|
fs.closeSync(entry.extFd);
|
|
} catch {}
|
|
}
|
|
shmFdCache.clear();
|
|
}
|
|
function getOrOpenShmFd(windowName, pid, fd) {
|
|
const cached = shmFdCache.get(windowName);
|
|
if (cached && cached.pid === pid && cached.fd === fd) {
|
|
return cached.extFd;
|
|
}
|
|
if (cached) {
|
|
try {
|
|
fs.closeSync(cached.extFd);
|
|
} catch {}
|
|
}
|
|
const fdPath = `/proc/${pid}/fd/${fd}`;
|
|
const extFd = fs.openSync(fdPath, "r");
|
|
shmFdCache.set(windowName, { pid, fd, extFd });
|
|
return extFd;
|
|
}
|
|
function getReusableShmBuffer(windowName, size) {
|
|
const existing = shmReadBufferCache.get(windowName);
|
|
if (existing && existing.length >= size) {
|
|
return existing;
|
|
}
|
|
const next = Buffer.allocUnsafe(size);
|
|
shmReadBufferCache.set(windowName, next);
|
|
return next;
|
|
}
|
|
function flushSurfaceBuffers() {
|
|
surfaceBufferTimer = null;
|
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
|
surfaceBufferPending.clear();
|
|
return;
|
|
}
|
|
for (const entry of surfaceBufferPending.values()) {
|
|
mainWindow.webContents.send("wo:surface-buffer", entry);
|
|
}
|
|
surfaceBufferPending.clear();
|
|
}
|
|
var inFlightFrameSeqs = new Set;
|
|
var MAX_IN_FLIGHT_FRAMES = 2;
|
|
var appUsedDamageHelper = false;
|
|
var pointerX = 0;
|
|
var pointerY = 0;
|
|
function respondToPortalRequest(requestId, params) {
|
|
const pending = portalPendingRequests.get(requestId);
|
|
if (!pending) {
|
|
return { ok: false, error: `No pending portal request: ${requestId}` };
|
|
}
|
|
let response;
|
|
if (pending.kind === "screen_share") {
|
|
response = {
|
|
allowed: Boolean(params.allowed),
|
|
sourceType: params.type === "window" ? "Window" : "Monitor",
|
|
windowName: typeof params.windowName === "string" ? params.windowName : null
|
|
};
|
|
} else {
|
|
response = { allowed: false };
|
|
}
|
|
try {
|
|
pending.socket.write(`${JSON.stringify(response)}
|
|
`);
|
|
pending.socket.end();
|
|
portalPendingRequests.delete(requestId);
|
|
return { ok: true };
|
|
} catch (error) {
|
|
portalPendingRequests.delete(requestId);
|
|
return { ok: false, error: `Failed to send portal response: ${String(error)}` };
|
|
}
|
|
}
|
|
function startPortalUiBridge() {
|
|
try {
|
|
if (fs.existsSync(PORTAL_UI_SOCKET)) {
|
|
fs.unlinkSync(PORTAL_UI_SOCKET);
|
|
}
|
|
} catch {}
|
|
portalUiServer = createServer((socket) => {
|
|
let received = "";
|
|
socket.on("data", (chunk) => {
|
|
received += chunk.toString("utf8");
|
|
const nl = received.indexOf(`
|
|
`);
|
|
if (nl < 0)
|
|
return;
|
|
const line = received.slice(0, nl).trim();
|
|
received = received.slice(nl + 1);
|
|
if (!line)
|
|
return;
|
|
try {
|
|
const req = JSON.parse(line);
|
|
if (req.type !== "screen_share_request" || !req.sessionId) {
|
|
socket.end(`{"allowed":false,"reason":"invalid_request"}
|
|
`);
|
|
return;
|
|
}
|
|
const requestId = req.sessionId;
|
|
portalPendingRequests.set(requestId, {
|
|
socket,
|
|
kind: "screen_share",
|
|
sessionId: req.sessionId
|
|
});
|
|
socket.setTimeout(90000, () => {
|
|
const pending = portalPendingRequests.get(requestId);
|
|
if (pending) {
|
|
portalPendingRequests.delete(requestId);
|
|
pending.socket.end(`{"allowed":false,"reason":"timeout"}
|
|
`);
|
|
}
|
|
});
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
const payload = {
|
|
requestId,
|
|
kind: "screen_share",
|
|
appName: req.appName || "Application",
|
|
sessionId: req.sessionId
|
|
};
|
|
mainWindow.webContents.send("wo:portal-request", payload);
|
|
} else {
|
|
portalPendingRequests.delete(requestId);
|
|
socket.end(`{"allowed":false,"reason":"no_ui"}
|
|
`);
|
|
}
|
|
} catch {
|
|
socket.end(`{"allowed":false,"reason":"invalid_json"}
|
|
`);
|
|
}
|
|
});
|
|
socket.on("close", () => {
|
|
for (const [sessionId, pending] of portalPendingRequests.entries()) {
|
|
if (pending.socket === socket) {
|
|
portalPendingRequests.delete(sessionId);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
socket.on("error", () => {});
|
|
});
|
|
portalUiServer.listen(PORTAL_UI_SOCKET, () => {
|
|
debugLog("[Wo] Portal UI bridge listening at", PORTAL_UI_SOCKET);
|
|
});
|
|
portalUiServer.on("error", (err) => {
|
|
console.error("[Wo] Portal UI bridge error:", err);
|
|
});
|
|
}
|
|
function stopPortalUiBridge() {
|
|
for (const pending of portalPendingRequests.values()) {
|
|
try {
|
|
pending.socket.end(`{"allowed":false,"reason":"shutdown"}
|
|
`);
|
|
} catch {}
|
|
}
|
|
portalPendingRequests.clear();
|
|
if (portalUiServer) {
|
|
portalUiServer.close();
|
|
portalUiServer = null;
|
|
}
|
|
try {
|
|
if (fs.existsSync(PORTAL_UI_SOCKET)) {
|
|
fs.unlinkSync(PORTAL_UI_SOCKET);
|
|
}
|
|
} catch {}
|
|
}
|
|
function parseClientWindows() {
|
|
try {
|
|
const raw = process.env.WO_CLIENT_WINDOWS;
|
|
if (raw) {
|
|
const parsed = JSON.parse(raw);
|
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
return parsed.map((w, idx) => ({
|
|
id: String(w.id ?? w.name ?? `win-${idx + 1}`),
|
|
title: String(w.title ?? w.name ?? `Window ${idx + 1}`),
|
|
width: Number(w.width ?? 960),
|
|
height: Number(w.height ?? 540)
|
|
}));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn("[Wo client] invalid WO_CLIENT_WINDOWS:", error);
|
|
}
|
|
return [{ id: String(name), title: String(name), width: Number(width), height: Number(height) }];
|
|
}
|
|
var clientWindows = parseClientWindows();
|
|
var compositorWindows = [];
|
|
function parseQuotedStrings(output) {
|
|
const matches = output.matchAll(/"((?:[^"\\]|\\.)*)"/g);
|
|
const values = [];
|
|
for (const match of matches) {
|
|
const raw = match[1];
|
|
values.push(raw.replace(/\\"/g, '"').replace(/\\\\/g, "\\"));
|
|
}
|
|
return values;
|
|
}
|
|
function runBusctlUser(args) {
|
|
try {
|
|
const { execFileSync } = require2("child_process");
|
|
return execFileSync("busctl", ["--user", ...args], {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "ignore"]
|
|
}).trim();
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
function parseServiceAndPath(itemRef) {
|
|
const slashIdx = itemRef.indexOf("/");
|
|
if (slashIdx > 0) {
|
|
return {
|
|
service: itemRef.slice(0, slashIdx),
|
|
objectPath: itemRef.slice(slashIdx)
|
|
};
|
|
}
|
|
if (!itemRef) {
|
|
return null;
|
|
}
|
|
return {
|
|
service: itemRef,
|
|
objectPath: "/StatusNotifierItem"
|
|
};
|
|
}
|
|
function parseBusctlStringValue(output) {
|
|
const quoted = parseQuotedStrings(output);
|
|
if (quoted.length > 0) {
|
|
return quoted[0];
|
|
}
|
|
const parts = output.split(/\s+/);
|
|
return parts.length >= 2 ? parts.slice(1).join(" ").trim() : "";
|
|
}
|
|
function getBusProperty(service, objectPath, iface, prop) {
|
|
const out = runBusctlUser(["get-property", service, objectPath, iface, prop]);
|
|
return out ? parseBusctlStringValue(out) : "";
|
|
}
|
|
function mapTrayIcon(iconName, title) {
|
|
const n = iconName.toLowerCase();
|
|
const t = title.toLowerCase();
|
|
if (n.includes("network") || t.includes("network") || t.includes("wifi"))
|
|
return "mdi:wifi";
|
|
if (n.includes("audio") || n.includes("volume") || t.includes("volume"))
|
|
return "mdi:volume-high";
|
|
if (n.includes("battery") || t.includes("battery"))
|
|
return "mdi:battery";
|
|
if (n.includes("bluetooth") || t.includes("bluetooth"))
|
|
return "mdi:bluetooth";
|
|
if (n.includes("telegram") || t.includes("telegram"))
|
|
return "mdi:telegram";
|
|
if (n.includes("discord") || t.includes("discord"))
|
|
return "mdi:discord";
|
|
if (n.includes("steam") || t.includes("steam"))
|
|
return "mdi:steam";
|
|
if (n.includes("dropbox") || t.includes("dropbox"))
|
|
return "mdi:dropbox";
|
|
if (n.includes("mail") || t.includes("mail"))
|
|
return "mdi:email-outline";
|
|
if (n.includes("kde") || t.includes("kde"))
|
|
return "mdi:kde";
|
|
return "mdi:circle-medium";
|
|
}
|
|
function iconPathCandidates(iconName) {
|
|
const direct = iconName.startsWith("/") ? [iconName] : [];
|
|
if (iconName.startsWith("/")) {
|
|
return direct;
|
|
}
|
|
const exts = ["png", "svg"];
|
|
const bases = [
|
|
"/usr/share/pixmaps",
|
|
"/usr/share/icons/hicolor/16x16/apps",
|
|
"/usr/share/icons/hicolor/22x22/apps",
|
|
"/usr/share/icons/hicolor/24x24/apps",
|
|
"/usr/share/icons/hicolor/32x32/apps",
|
|
"/usr/share/icons/hicolor/48x48/apps",
|
|
"/usr/share/icons/hicolor/scalable/apps",
|
|
"/usr/share/icons/breeze/apps/22",
|
|
"/usr/share/icons/breeze/apps/24",
|
|
"/usr/share/icons/breeze/apps/32"
|
|
];
|
|
const results = [];
|
|
for (const base of bases) {
|
|
for (const ext of exts) {
|
|
results.push(`${base}/${iconName}.${ext}`);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
function toDataUrlForIconFile(iconPath) {
|
|
try {
|
|
if (!fs.existsSync(iconPath)) {
|
|
return;
|
|
}
|
|
const lower = iconPath.toLowerCase();
|
|
const mime = lower.endsWith(".svg") ? "image/svg+xml" : lower.endsWith(".png") ? "image/png" : "";
|
|
if (!mime) {
|
|
return;
|
|
}
|
|
const base64 = fs.readFileSync(iconPath).toString("base64");
|
|
return `data:${mime};base64,${base64}`;
|
|
} catch {
|
|
return;
|
|
}
|
|
}
|
|
function resolveTrayIconDataUrl(iconName) {
|
|
for (const candidate of iconPathCandidates(iconName)) {
|
|
const dataUrl = toDataUrlForIconFile(candidate);
|
|
if (dataUrl) {
|
|
return dataUrl;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
function getStatusNotifierTrayItems() {
|
|
const watcherCall = runBusctlUser([
|
|
"call",
|
|
"org.kde.StatusNotifierWatcher",
|
|
"/StatusNotifierWatcher",
|
|
"org.kde.StatusNotifierWatcher",
|
|
"RegisteredStatusNotifierItems"
|
|
]);
|
|
if (!watcherCall) {
|
|
return [];
|
|
}
|
|
const refs = parseQuotedStrings(watcherCall);
|
|
const results = [];
|
|
for (const itemRef of refs) {
|
|
const parsed = parseServiceAndPath(itemRef);
|
|
if (!parsed) {
|
|
continue;
|
|
}
|
|
const statusRaw = getBusProperty(parsed.service, parsed.objectPath, "org.kde.StatusNotifierItem", "Status");
|
|
const title = getBusProperty(parsed.service, parsed.objectPath, "org.kde.StatusNotifierItem", "Title") || getBusProperty(parsed.service, parsed.objectPath, "org.kde.StatusNotifierItem", "Id") || parsed.service;
|
|
const iconName = getBusProperty(parsed.service, parsed.objectPath, "org.kde.StatusNotifierItem", "AttentionIconName") || getBusProperty(parsed.service, parsed.objectPath, "org.kde.StatusNotifierItem", "IconName");
|
|
const menuPath = getBusProperty(parsed.service, parsed.objectPath, "org.kde.StatusNotifierItem", "Menu");
|
|
const iconDataUrl = iconName ? resolveTrayIconDataUrl(iconName) : undefined;
|
|
let status = "active";
|
|
const s = statusRaw.toLowerCase();
|
|
if (s.includes("passive")) {
|
|
status = "passive";
|
|
} else if (s.includes("attention") || s.includes("needsattention")) {
|
|
status = "attention";
|
|
}
|
|
results.push({
|
|
id: `${parsed.service}${parsed.objectPath}`,
|
|
title,
|
|
status,
|
|
icon: mapTrayIcon(iconName || title, title),
|
|
iconDataUrl,
|
|
service: parsed.service,
|
|
objectPath: parsed.objectPath,
|
|
menuPath: menuPath || undefined,
|
|
hasMenu: Boolean(menuPath && menuPath !== "/NO_DBUSMENU")
|
|
});
|
|
}
|
|
return results;
|
|
}
|
|
function activateStatusNotifierItem(service, objectPath) {
|
|
const out = runBusctlUser([
|
|
"call",
|
|
service,
|
|
objectPath,
|
|
"org.kde.StatusNotifierItem",
|
|
"Activate",
|
|
"ii",
|
|
"0",
|
|
"0"
|
|
]);
|
|
return out.length > 0;
|
|
}
|
|
function secondaryActivateStatusNotifierItem(service, objectPath) {
|
|
const out = runBusctlUser([
|
|
"call",
|
|
service,
|
|
objectPath,
|
|
"org.kde.StatusNotifierItem",
|
|
"SecondaryActivate",
|
|
"ii",
|
|
"0",
|
|
"0"
|
|
]);
|
|
return out.length > 0;
|
|
}
|
|
function openStatusNotifierContextMenu(service, objectPath, x, y) {
|
|
const out = runBusctlUser([
|
|
"call",
|
|
service,
|
|
objectPath,
|
|
"org.kde.StatusNotifierItem",
|
|
"ContextMenu",
|
|
"ii",
|
|
String(Math.trunc(x)),
|
|
String(Math.trunc(y))
|
|
]);
|
|
return out.length > 0;
|
|
}
|
|
function getDBusMenuLayout(service, menuPath) {
|
|
try {
|
|
const out = runBusctlUser([
|
|
"call",
|
|
service,
|
|
menuPath.startsWith("/") ? menuPath : "/com/canonical/dbusmenu",
|
|
"com.canonical.dbusmenu",
|
|
"GetLayout",
|
|
"u",
|
|
"0"
|
|
]);
|
|
if (!out) {
|
|
return [];
|
|
}
|
|
const lines = out.split(`
|
|
`).filter((l) => l.trim().length > 0);
|
|
const items = [];
|
|
let depth = 0;
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
const newDepth = (line.match(/^\s*/)?.[0]?.length ?? 0) / 2;
|
|
if (trimmed.startsWith("(") && trimmed.includes('"')) {
|
|
const quoted = parseQuotedStrings(trimmed);
|
|
if (quoted.length > 0) {
|
|
const label = quoted[0];
|
|
const item = {
|
|
id: label,
|
|
label,
|
|
enabled: !line.toLowerCase().includes("disabled")
|
|
};
|
|
if (newDepth === 0) {
|
|
items.push(item);
|
|
} else if (items.length > 0 && newDepth > depth) {
|
|
const parent = items[items.length - 1];
|
|
if (!parent.children) {
|
|
parent.children = [];
|
|
}
|
|
parent.children.push(item);
|
|
}
|
|
}
|
|
depth = newDepth;
|
|
}
|
|
}
|
|
return items;
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
function triggerDBusMenuEvent(service, menuPath, menuId) {
|
|
try {
|
|
const out = runBusctlUser([
|
|
"call",
|
|
service,
|
|
menuPath.startsWith("/") ? menuPath : "/com/canonical/dbusmenu",
|
|
"com.canonical.dbusmenu",
|
|
"Event",
|
|
"sis",
|
|
menuId,
|
|
"clicked",
|
|
""
|
|
]);
|
|
return out.length > 0;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
function scheduleIpcReconnect(delayMs = 1000) {
|
|
if (CLIENT_MODE || process.env.WO_STANDALONE === "1") {
|
|
return;
|
|
}
|
|
if (ipcReconnectTimer) {
|
|
return;
|
|
}
|
|
ipcReconnectTimer = setTimeout(() => {
|
|
ipcReconnectTimer = null;
|
|
connectToCompositor().then(() => {
|
|
debugLog("[Wo] IPC reconnect succeeded");
|
|
}).catch((error) => {
|
|
debugLog("[Wo] IPC reconnect failed:", String(error));
|
|
scheduleIpcReconnect();
|
|
});
|
|
}, delayMs);
|
|
}
|
|
function sendActionToCompositor(action, payload) {
|
|
if (!ipcSocket || !ipcConnected) {
|
|
return false;
|
|
}
|
|
try {
|
|
const actionBuf = stringToBuffer(action);
|
|
const payloadStr = payload ? JSON.stringify(payload) : "";
|
|
const payloadBuf = stringToBuffer(payloadStr);
|
|
const messageBuf = Buffer.alloc(12 + actionBuf.length + payloadBuf.length);
|
|
let offset = 0;
|
|
messageBuf.writeUInt32LE(MAGIC.ACTION, offset);
|
|
offset += 4;
|
|
messageBuf.writeUInt32LE(actionBuf.length, offset);
|
|
offset += 4;
|
|
actionBuf.copy(messageBuf, offset);
|
|
offset += actionBuf.length;
|
|
messageBuf.writeUInt32LE(payloadBuf.length, offset);
|
|
offset += 4;
|
|
payloadBuf.copy(messageBuf, offset);
|
|
ipcSocket.write(messageBuf);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
function sendWindowPositionUpdate(x, y, width2, height2) {
|
|
if (!ipcSocket || !ipcConnected) {
|
|
return false;
|
|
}
|
|
try {
|
|
const nameBuf = stringToBuffer(name);
|
|
const messageBuf = Buffer.alloc(8 + nameBuf.length + 16);
|
|
let offset = 0;
|
|
messageBuf.writeUInt32LE(MAGIC.WINDOW_POS, offset);
|
|
offset += 4;
|
|
messageBuf.writeUInt32LE(nameBuf.length, offset);
|
|
offset += 4;
|
|
nameBuf.copy(messageBuf, offset);
|
|
offset += nameBuf.length;
|
|
messageBuf.writeInt32LE(x, offset);
|
|
offset += 4;
|
|
messageBuf.writeInt32LE(y, offset);
|
|
offset += 4;
|
|
messageBuf.writeUInt32LE(width2, offset);
|
|
offset += 4;
|
|
messageBuf.writeUInt32LE(height2, offset);
|
|
ipcSocket.write(messageBuf);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
function linuxButtonToElectron(btn) {
|
|
if (btn === 273)
|
|
return "right";
|
|
if (btn === 274)
|
|
return "middle";
|
|
return "left";
|
|
}
|
|
var modifierState = { shift: false, control: false, alt: false, meta: false };
|
|
var MODIFIER_KEYCODES = {
|
|
29: "control",
|
|
42: "shift",
|
|
54: "shift",
|
|
56: "alt",
|
|
97: "control",
|
|
100: "alt",
|
|
125: "meta",
|
|
126: "meta"
|
|
};
|
|
function getModifierArray() {
|
|
const mods = [];
|
|
if (modifierState.shift)
|
|
mods.push("shift");
|
|
if (modifierState.control)
|
|
mods.push("control");
|
|
if (modifierState.alt)
|
|
mods.push("alt");
|
|
if (modifierState.meta)
|
|
mods.push("meta");
|
|
return mods;
|
|
}
|
|
var SHIFTED_CHARS = {
|
|
"1": "!",
|
|
"2": "@",
|
|
"3": "#",
|
|
"4": "$",
|
|
"5": "%",
|
|
"6": "^",
|
|
"7": "&",
|
|
"8": "*",
|
|
"9": "(",
|
|
"0": ")",
|
|
"-": "_",
|
|
"=": "+",
|
|
"[": "{",
|
|
"]": "}",
|
|
"\\": "|",
|
|
";": ":",
|
|
"'": '"',
|
|
"`": "~",
|
|
",": "<",
|
|
".": ">",
|
|
"/": "?"
|
|
};
|
|
var LINUX_KEYCODE_MAP = {
|
|
1: "Escape",
|
|
59: "F1",
|
|
60: "F2",
|
|
61: "F3",
|
|
62: "F4",
|
|
63: "F5",
|
|
64: "F6",
|
|
65: "F7",
|
|
66: "F8",
|
|
67: "F9",
|
|
68: "F10",
|
|
87: "F11",
|
|
88: "F12",
|
|
41: "`",
|
|
2: "1",
|
|
3: "2",
|
|
4: "3",
|
|
5: "4",
|
|
6: "5",
|
|
7: "6",
|
|
8: "7",
|
|
9: "8",
|
|
10: "9",
|
|
11: "0",
|
|
12: "-",
|
|
13: "=",
|
|
14: "Backspace",
|
|
15: "Tab",
|
|
16: "q",
|
|
17: "w",
|
|
18: "e",
|
|
19: "r",
|
|
20: "t",
|
|
21: "y",
|
|
22: "u",
|
|
23: "i",
|
|
24: "o",
|
|
25: "p",
|
|
26: "[",
|
|
27: "]",
|
|
43: "\\",
|
|
58: "CapsLock",
|
|
30: "a",
|
|
31: "s",
|
|
32: "d",
|
|
33: "f",
|
|
34: "g",
|
|
35: "h",
|
|
36: "j",
|
|
37: "k",
|
|
38: "l",
|
|
39: ";",
|
|
40: "'",
|
|
28: "Return",
|
|
42: "Shift",
|
|
44: "z",
|
|
45: "x",
|
|
46: "c",
|
|
47: "v",
|
|
48: "b",
|
|
49: "n",
|
|
50: "m",
|
|
51: ",",
|
|
52: ".",
|
|
53: "/",
|
|
54: "Shift",
|
|
29: "Control",
|
|
125: "Meta",
|
|
56: "Alt",
|
|
57: "Space",
|
|
100: "Alt",
|
|
126: "Meta",
|
|
97: "Control",
|
|
110: "Insert",
|
|
102: "Home",
|
|
104: "PageUp",
|
|
111: "Delete",
|
|
107: "End",
|
|
109: "PageDown",
|
|
103: "Up",
|
|
105: "Left",
|
|
108: "Down",
|
|
106: "Right",
|
|
69: "NumLock",
|
|
98: "numdiv",
|
|
55: "nummult",
|
|
74: "numsub",
|
|
71: "num7",
|
|
72: "num8",
|
|
73: "num9",
|
|
78: "numadd",
|
|
75: "num4",
|
|
76: "num5",
|
|
77: "num6",
|
|
79: "num1",
|
|
80: "num2",
|
|
81: "num3",
|
|
96: "Enter",
|
|
82: "num0",
|
|
83: "numdec",
|
|
99: "PrintScreen",
|
|
70: "ScrollLock",
|
|
119: "Pause",
|
|
113: "VolumeMute",
|
|
114: "VolumeDown",
|
|
115: "VolumeUp",
|
|
163: "MediaNextTrack",
|
|
165: "MediaPreviousTrack",
|
|
164: "MediaPlayPause",
|
|
166: "MediaStop"
|
|
};
|
|
function connectToCompositor() {
|
|
return new Promise((resolve, reject) => {
|
|
if (ipcReconnectTimer) {
|
|
clearTimeout(ipcReconnectTimer);
|
|
ipcReconnectTimer = null;
|
|
}
|
|
if (ipcSocket && !ipcSocket.destroyed) {
|
|
ipcSocket.destroy();
|
|
}
|
|
debugLog("[Wo] Connecting to compositor IPC:", IPC_SOCKET);
|
|
ipcSocket = createConnection(IPC_SOCKET, () => {
|
|
const windowName = name;
|
|
const nameBuf = stringToBuffer(windowName);
|
|
const messageBuf = Buffer.alloc(8 + nameBuf.length + 8);
|
|
let offset = 0;
|
|
messageBuf.writeUInt32LE(MAGIC.HELLO, offset);
|
|
offset += 4;
|
|
messageBuf.writeUInt32LE(nameBuf.length, offset);
|
|
offset += 4;
|
|
nameBuf.copy(messageBuf, offset);
|
|
offset += nameBuf.length;
|
|
messageBuf.writeUInt32LE(width, offset);
|
|
offset += 4;
|
|
messageBuf.writeUInt32LE(height, offset);
|
|
ipcSocket.write(messageBuf);
|
|
ipcConnected = true;
|
|
debugLog("[Wo] IPC connected, sent HELLO message");
|
|
try {
|
|
const nativePath = resolvePath(__dirname2, "../native/build/Release/wo_dmabuf.node");
|
|
nativeDmabuf = loadNativeDmabufModule(require2, nativePath);
|
|
nativeDmabuf.init(process.env.WO_DRM_RENDER_NODE || "/dev/dri/renderD128");
|
|
dmabufSender = new WoDmabufSender(ipcSocket, name, nativeDmabuf);
|
|
debugLog("[Wo] DMABUF sender initialized");
|
|
} catch (error) {
|
|
debugLog("[Wo] DMABUF sender unavailable:", error);
|
|
dmabufSender = null;
|
|
}
|
|
resolve();
|
|
});
|
|
let ipcDataCalls = 0;
|
|
let ipcDataLastLog = Date.now();
|
|
let pendingMouseMove = null;
|
|
let inputFlushTimer = null;
|
|
const flushInputEvents = () => {
|
|
inputFlushTimer = null;
|
|
if (pendingMouseMove && mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.sendInputEvent({
|
|
type: "mouseMove",
|
|
x: pendingMouseMove.x,
|
|
y: pendingMouseMove.y
|
|
});
|
|
pendingMouseMove = null;
|
|
}
|
|
};
|
|
ipcSocket.on("data", (chunk) => {
|
|
ipcDataCalls++;
|
|
const now = Date.now();
|
|
if (now - ipcDataLastLog > 2000) {
|
|
ipcDataCalls = 0;
|
|
ipcDataLastLog = now;
|
|
}
|
|
const dataT0 = Date.now();
|
|
compositorRxBuffer = Buffer.concat([compositorRxBuffer, chunk]);
|
|
while (compositorRxBuffer.length >= 4) {
|
|
const magic = compositorRxBuffer.readUInt32LE(0);
|
|
if (magic === MAGIC.FRAME_ACK) {
|
|
if (compositorRxBuffer.length < 12)
|
|
break;
|
|
const seq = compositorRxBuffer.readBigUInt64LE(4).toString();
|
|
inFlightFrameSeqs.delete(seq);
|
|
compositorRxBuffer = compositorRxBuffer.slice(12);
|
|
continue;
|
|
}
|
|
if (magic === MAGIC.MOUSE_MOVE) {
|
|
if (compositorRxBuffer.length < 20)
|
|
break;
|
|
const x = compositorRxBuffer.readDoubleLE(4);
|
|
const y = compositorRxBuffer.readDoubleLE(12);
|
|
compositorRxBuffer = compositorRxBuffer.slice(20);
|
|
pointerX = Math.round(x);
|
|
pointerY = Math.round(y);
|
|
if (!inputFlushTimer) {
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.sendInputEvent({ type: "mouseMove", x: pointerX, y: pointerY });
|
|
}
|
|
} else {
|
|
pendingMouseMove = { x: pointerX, y: pointerY };
|
|
inputFlushTimer = setTimeout(flushInputEvents, 0);
|
|
}
|
|
continue;
|
|
}
|
|
if (magic === MAGIC.MOUSE_BUTTON) {
|
|
if (compositorRxBuffer.length < 16)
|
|
break;
|
|
const button = compositorRxBuffer.readUInt32LE(4);
|
|
const pressed = compositorRxBuffer.readUInt32LE(8) !== 0;
|
|
compositorRxBuffer = compositorRxBuffer.slice(16);
|
|
if (pendingMouseMove && mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.sendInputEvent({
|
|
type: "mouseMove",
|
|
x: pendingMouseMove.x,
|
|
y: pendingMouseMove.y
|
|
});
|
|
pendingMouseMove = null;
|
|
}
|
|
if (inputFlushTimer) {
|
|
clearTimeout(inputFlushTimer);
|
|
inputFlushTimer = null;
|
|
}
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.sendInputEvent({
|
|
type: pressed ? "mouseDown" : "mouseUp",
|
|
x: pointerX,
|
|
y: pointerY,
|
|
button: linuxButtonToElectron(button),
|
|
clickCount: 1,
|
|
modifiers: getModifierArray()
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
if (magic === MAGIC.KEYBOARD) {
|
|
if (compositorRxBuffer.length < 16)
|
|
break;
|
|
const key = compositorRxBuffer.readUInt32LE(4);
|
|
const pressed = compositorRxBuffer.readUInt32LE(8) !== 0;
|
|
compositorRxBuffer = compositorRxBuffer.slice(16);
|
|
const modKey = MODIFIER_KEYCODES[key];
|
|
if (modKey) {
|
|
modifierState[modKey] = pressed;
|
|
}
|
|
const keyCode = LINUX_KEYCODE_MAP[key];
|
|
if (keyCode && mainWindow && !mainWindow.isDestroyed()) {
|
|
const modifiers = getModifierArray();
|
|
mainWindow.webContents.sendInputEvent({
|
|
type: pressed ? "keyDown" : "keyUp",
|
|
keyCode,
|
|
modifiers
|
|
});
|
|
if (pressed && keyCode.length === 1) {
|
|
let charKey = keyCode;
|
|
if (modifierState.shift) {
|
|
if (charKey >= "a" && charKey <= "z") {
|
|
charKey = charKey.toUpperCase();
|
|
} else if (SHIFTED_CHARS[charKey]) {
|
|
charKey = SHIFTED_CHARS[charKey];
|
|
}
|
|
}
|
|
mainWindow.webContents.sendInputEvent({ type: "char", keyCode: charKey, modifiers });
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (magic === MAGIC.SCROLL) {
|
|
if (compositorRxBuffer.length < 16)
|
|
break;
|
|
const vertical = compositorRxBuffer.readInt32LE(4);
|
|
const horizontal = compositorRxBuffer.readInt32LE(8);
|
|
compositorRxBuffer = compositorRxBuffer.slice(16);
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.sendInputEvent({
|
|
type: "mouseWheel",
|
|
x: pointerX,
|
|
y: pointerY,
|
|
deltaX: horizontal,
|
|
deltaY: vertical,
|
|
canScroll: true
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
if (magic === MAGIC.FOCUS_CHANGE) {
|
|
if (compositorRxBuffer.length < 12)
|
|
break;
|
|
const nameLen = compositorRxBuffer.readUInt32LE(4);
|
|
if (compositorRxBuffer.length < 12 + nameLen)
|
|
break;
|
|
const focused = compositorRxBuffer.readUInt32LE(8) !== 0;
|
|
const windowName = compositorRxBuffer.slice(12, 12 + nameLen).toString("utf8");
|
|
compositorRxBuffer = compositorRxBuffer.slice(12 + nameLen);
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send("wo:focus-change", { window: windowName, focused });
|
|
}
|
|
continue;
|
|
}
|
|
if (magic === MAGIC.WINDOW_META) {
|
|
if (compositorRxBuffer.length < 8)
|
|
break;
|
|
const payloadLen = compositorRxBuffer.readUInt32LE(4);
|
|
if (compositorRxBuffer.length < 8 + payloadLen)
|
|
break;
|
|
const metadata = compositorRxBuffer.slice(8, 8 + payloadLen).toString("utf8");
|
|
compositorRxBuffer = compositorRxBuffer.slice(8 + payloadLen);
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send("wo:window-metadata", { type: "windowMetadata", metadata });
|
|
try {
|
|
const parsed = JSON.parse(metadata);
|
|
if (parsed && Array.isArray(parsed.windows)) {
|
|
compositorWindows = parsed.windows;
|
|
mainWindow.webContents.send("wo:windows", compositorWindows);
|
|
}
|
|
} catch {}
|
|
}
|
|
continue;
|
|
}
|
|
if (magic === MAGIC.SHM_BUFFER) {
|
|
if (compositorRxBuffer.length < 8)
|
|
break;
|
|
const nameLen = compositorRxBuffer.readUInt32LE(4);
|
|
const baseHeader = 8 + nameLen + 20 + 4;
|
|
if (compositorRxBuffer.length < baseHeader)
|
|
break;
|
|
let off = 8;
|
|
const windowName = compositorRxBuffer.slice(off, off + nameLen).toString("utf8");
|
|
off += nameLen;
|
|
const sbWidth = compositorRxBuffer.readUInt32LE(off);
|
|
off += 4;
|
|
const sbHeight = compositorRxBuffer.readUInt32LE(off);
|
|
off += 4;
|
|
const sbStride = compositorRxBuffer.readUInt32LE(off);
|
|
off += 4;
|
|
const pid = compositorRxBuffer.readUInt32LE(off);
|
|
off += 4;
|
|
const fd = compositorRxBuffer.readUInt32LE(off);
|
|
off += 4;
|
|
const numRects = compositorRxBuffer.readUInt32LE(off);
|
|
off += 4;
|
|
const fullHeader = baseHeader + numRects * 16;
|
|
if (compositorRxBuffer.length < fullHeader)
|
|
break;
|
|
const damageRects = [];
|
|
for (let i = 0;i < numRects; i++) {
|
|
damageRects.push({
|
|
x: compositorRxBuffer.readUInt32LE(off),
|
|
y: compositorRxBuffer.readUInt32LE(off + 4),
|
|
width: compositorRxBuffer.readUInt32LE(off + 8),
|
|
height: compositorRxBuffer.readUInt32LE(off + 12)
|
|
});
|
|
off += 16;
|
|
}
|
|
compositorRxBuffer = compositorRxBuffer.slice(fullHeader);
|
|
try {
|
|
const extFd = getOrOpenShmFd(windowName, pid, fd);
|
|
const fullSize = sbStride * sbHeight;
|
|
const safeBuffer = getReusableShmBuffer(windowName, fullSize);
|
|
const hasUsableRects = damageRects.length > 0 && damageRects.length < 64;
|
|
if (hasUsableRects) {
|
|
let minY = sbHeight, maxY = 0;
|
|
for (const r of damageRects) {
|
|
const ry = Math.max(0, r.y);
|
|
const ryEnd = Math.min(sbHeight, r.y + r.height);
|
|
if (ry < minY)
|
|
minY = ry;
|
|
if (ryEnd > maxY)
|
|
maxY = ryEnd;
|
|
}
|
|
if (minY < maxY) {
|
|
const readOffset = minY * sbStride;
|
|
const readLen = (maxY - minY) * sbStride;
|
|
let totalRead = 0;
|
|
while (totalRead < readLen) {
|
|
const bytesRead = fs.readSync(extFd, safeBuffer, readOffset + totalRead, readLen - totalRead, readOffset + totalRead);
|
|
if (bytesRead <= 0)
|
|
break;
|
|
totalRead += bytesRead;
|
|
}
|
|
}
|
|
} else {
|
|
let totalRead = 0;
|
|
while (totalRead < fullSize) {
|
|
const bytesRead = fs.readSync(extFd, safeBuffer, totalRead, fullSize - totalRead, totalRead);
|
|
if (bytesRead <= 0)
|
|
break;
|
|
totalRead += bytesRead;
|
|
}
|
|
}
|
|
surfaceBufferPending.set(windowName, {
|
|
name: windowName,
|
|
width: sbWidth,
|
|
height: sbHeight,
|
|
stride: sbStride,
|
|
pixels: safeBuffer,
|
|
damageRects: hasUsableRects ? damageRects : undefined
|
|
});
|
|
if (!surfaceBufferTimer) {
|
|
surfaceBufferTimer = setTimeout(flushSurfaceBuffers, 10);
|
|
}
|
|
} catch (err) {
|
|
const cached = shmFdCache.get(windowName);
|
|
if (cached) {
|
|
try {
|
|
fs.closeSync(cached.extFd);
|
|
} catch {}
|
|
shmFdCache.delete(windowName);
|
|
}
|
|
debugLog(`[Wo] SHM_BUFFER fs.readSync failed for ${windowName}:`, err);
|
|
}
|
|
continue;
|
|
}
|
|
if (magic === MAGIC.DMABUF_FRAME) {
|
|
if (compositorRxBuffer.length < 8)
|
|
break;
|
|
const nameLen = compositorRxBuffer.readUInt32LE(4);
|
|
const numPlanesOffset = 8 + nameLen + 12;
|
|
if (compositorRxBuffer.length < numPlanesOffset + 4)
|
|
break;
|
|
let off = 8;
|
|
const dmabufName = compositorRxBuffer.slice(off, off + nameLen).toString("utf8");
|
|
off += nameLen;
|
|
const dmabufW = compositorRxBuffer.readUInt32LE(off);
|
|
off += 4;
|
|
const dmabufH = compositorRxBuffer.readUInt32LE(off);
|
|
off += 4;
|
|
const dmabufFormat = compositorRxBuffer.readUInt32LE(off);
|
|
off += 4;
|
|
const numPlanes = compositorRxBuffer.readUInt32LE(off);
|
|
off += 4;
|
|
const totalSize = off + numPlanes * 24;
|
|
if (compositorRxBuffer.length < totalSize)
|
|
break;
|
|
try {
|
|
if (nativeDmabuf && ipcSocket) {
|
|
const socketFd = ipcSocket?._handle?.fd;
|
|
if (typeof socketFd === "number" && socketFd >= 0) {
|
|
const dmabufFd = nativeDmabuf.recvFd(socketFd);
|
|
if (dmabufFd >= 0) {
|
|
try {
|
|
const textureInfo = nativeDmabuf.importDmabufTexture(dmabufName, dmabufFd, dmabufW, dmabufH, dmabufFormat);
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send("wo:dmabuf-frame", {
|
|
name: dmabufName,
|
|
texture: textureInfo.texture,
|
|
width: textureInfo.width,
|
|
height: textureInfo.height
|
|
});
|
|
}
|
|
} finally {
|
|
fs.closeSync(dmabufFd);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
debugLog(`[Wo] DMABUF import failed for ${dmabufName}:`, err);
|
|
}
|
|
compositorRxBuffer = compositorRxBuffer.slice(totalSize);
|
|
continue;
|
|
}
|
|
if (magic === MAGIC.POINTER_LOCK_REQUEST) {
|
|
if (compositorRxBuffer.length < 12)
|
|
break;
|
|
const nameLen = compositorRxBuffer.readUInt32LE(4);
|
|
if (compositorRxBuffer.length < 12 + nameLen)
|
|
break;
|
|
const lock = compositorRxBuffer.readUInt32LE(8) !== 0;
|
|
const windowName = compositorRxBuffer.slice(12, 12 + nameLen).toString("utf8");
|
|
compositorRxBuffer = compositorRxBuffer.slice(12 + nameLen);
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send("wo:pointer-lock-request", { window: windowName, lock });
|
|
}
|
|
continue;
|
|
}
|
|
if (magic === MAGIC.ENV_UPDATE) {
|
|
if (compositorRxBuffer.length < 8)
|
|
break;
|
|
const jsonLen = compositorRxBuffer.readUInt32LE(4);
|
|
if (compositorRxBuffer.length < 8 + jsonLen)
|
|
break;
|
|
const jsonStr = compositorRxBuffer.slice(8, 8 + jsonLen).toString("utf8");
|
|
compositorRxBuffer = compositorRxBuffer.slice(8 + jsonLen);
|
|
try {
|
|
const vars = JSON.parse(jsonStr);
|
|
for (const [key, value] of Object.entries(vars)) {
|
|
process.env[key] = value;
|
|
debugLog(`[Wo] env update: ${key}=${value}`);
|
|
}
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send("wo:env-update", vars);
|
|
}
|
|
} catch (err) {
|
|
debugLog("[Wo] ENV_UPDATE parse error:", err);
|
|
}
|
|
continue;
|
|
}
|
|
debugLog("[Wo] UNKNOWN magic 0x" + magic.toString(16).padStart(8, "0") + " bufLen=" + compositorRxBuffer.length + " — stream may be corrupted");
|
|
compositorRxBuffer = compositorRxBuffer.slice(1);
|
|
}
|
|
const dataElapsed = Date.now() - dataT0;
|
|
if (dataElapsed > 50) {
|
|
console.warn(`[IPC SLOW] data handler took ${dataElapsed}ms chunkLen=${chunk.length}`);
|
|
}
|
|
});
|
|
ipcSocket.on("error", (error) => {
|
|
debugLog("[Wo] IPC connection error:", error);
|
|
const hadConnected = ipcConnected;
|
|
ipcConnected = false;
|
|
dmabufSender = null;
|
|
if (hadConnected) {
|
|
scheduleIpcReconnect();
|
|
return;
|
|
}
|
|
reject(error);
|
|
});
|
|
ipcSocket.on("close", () => {
|
|
debugLog("[Wo] IPC connection closed");
|
|
ipcConnected = false;
|
|
ipcSocket = null;
|
|
dmabufSender = null;
|
|
inFlightFrameSeqs.clear();
|
|
compositorRxBuffer = Buffer.alloc(0);
|
|
scheduleIpcReconnect();
|
|
});
|
|
setTimeout(() => {
|
|
if (!ipcConnected) {
|
|
debugLog("[Wo] IPC connection timeout");
|
|
reject(new Error("Failed to connect to compositor IPC socket"));
|
|
}
|
|
}, 5000);
|
|
});
|
|
}
|
|
function setupIpcHandlers() {
|
|
ipcMain.handle("action", (_event, action, payload) => {
|
|
return sendActionToCompositor(action, payload);
|
|
});
|
|
ipcMain.on("action-sync", (_event, action, payload) => {
|
|
sendActionToCompositor(action, payload);
|
|
});
|
|
ipcMain.handle("wo:get-windows", async () => compositorWindows.length > 0 ? compositorWindows : clientWindows);
|
|
ipcMain.on("wo:keybind-action", (_event, action) => {
|
|
if (action === "shuffle") {
|
|
clientWindows = [...clientWindows].sort(() => Math.random() - 0.5);
|
|
} else if (action === "reverse") {
|
|
clientWindows = [...clientWindows].reverse();
|
|
}
|
|
if (mainWindow) {
|
|
mainWindow.webContents.send("wo:windows", clientWindows);
|
|
}
|
|
});
|
|
ipcMain.handle("wo:submit-damage-frame", (_event, payload) => {
|
|
if (!dmabufSender) {
|
|
return { ok: false, reason: "dmabuf-sender-not-ready" };
|
|
}
|
|
if (inFlightFrameSeqs.size >= MAX_IN_FLIGHT_FRAMES) {
|
|
return { ok: true, skipped: true, reason: "backpressure" };
|
|
}
|
|
if (!damageBuffer) {
|
|
damageBuffer = new DamageBuffer(payload.width, payload.height);
|
|
}
|
|
appUsedDamageHelper = true;
|
|
applyDamagePayload(damageBuffer, payload);
|
|
const seq = dmabufSender.send(damageBuffer);
|
|
inFlightFrameSeqs.add(seq.toString());
|
|
return { ok: true, seq: seq.toString() };
|
|
});
|
|
ipcMain.on("wo:forward-keyboard", (_event, windowName, key, pressed, time) => {
|
|
if (ipcSocket && ipcConnected) {
|
|
const nameBuf = Buffer.from(windowName, "utf8");
|
|
const msg = Buffer.alloc(4 + 4 + nameBuf.length + 12);
|
|
let off = 0;
|
|
msg.writeUInt32LE(MAGIC.FORWARD_KEYBOARD, off);
|
|
off += 4;
|
|
msg.writeUInt32LE(nameBuf.length, off);
|
|
off += 4;
|
|
nameBuf.copy(msg, off);
|
|
off += nameBuf.length;
|
|
msg.writeUInt32LE(key, off);
|
|
off += 4;
|
|
msg.writeUInt32LE(pressed ? 1 : 0, off);
|
|
off += 4;
|
|
msg.writeUInt32LE(time || 0, off);
|
|
ipcSocket.write(msg);
|
|
}
|
|
});
|
|
ipcMain.on("wo:forward-relative-pointer", (_event, windowName, dx, dy) => {
|
|
if (ipcSocket && ipcConnected) {
|
|
const nameBuf = Buffer.from(windowName, "utf8");
|
|
const msg = Buffer.alloc(4 + 4 + nameBuf.length + 16);
|
|
let off = 0;
|
|
msg.writeUInt32LE(MAGIC.FORWARD_RELATIVE_POINTER, off);
|
|
off += 4;
|
|
msg.writeUInt32LE(nameBuf.length, off);
|
|
off += 4;
|
|
nameBuf.copy(msg, off);
|
|
off += nameBuf.length;
|
|
msg.writeDoubleLE(dx, off);
|
|
off += 8;
|
|
msg.writeDoubleLE(dy, off);
|
|
ipcSocket.write(msg);
|
|
}
|
|
});
|
|
const iconCache = new Map;
|
|
function resolveDesktopIconFast(iconName, fs2, path) {
|
|
if (iconCache.has(iconName))
|
|
return iconCache.get(iconName) ?? null;
|
|
let result = null;
|
|
if (iconName.startsWith("/")) {
|
|
try {
|
|
if (fs2.existsSync(iconName)) {
|
|
const ext = path.extname(iconName).toLowerCase();
|
|
const mime = ext === ".svg" ? "image/svg+xml" : "image/png";
|
|
result = { type: "base64", data: fs2.readFileSync(iconName).toString("base64"), mimeType: mime };
|
|
}
|
|
} catch {}
|
|
iconCache.set(iconName, result);
|
|
return result;
|
|
}
|
|
const quickPaths = [
|
|
[`/usr/share/pixmaps/${iconName}.png`, "image/png"],
|
|
[`/usr/share/pixmaps/${iconName}.svg`, "image/svg+xml"],
|
|
[`/usr/share/icons/hicolor/48x48/apps/${iconName}.png`, "image/png"],
|
|
[`/usr/share/icons/hicolor/scalable/apps/${iconName}.svg`, "image/svg+xml"]
|
|
];
|
|
for (const [iconPath, mime] of quickPaths) {
|
|
try {
|
|
if (fs2.existsSync(iconPath)) {
|
|
result = { type: "base64", data: fs2.readFileSync(iconPath).toString("base64"), mimeType: mime };
|
|
break;
|
|
}
|
|
} catch {}
|
|
}
|
|
iconCache.set(iconName, result);
|
|
return result;
|
|
}
|
|
ipcMain.handle("wo:syscall", async (_event, type, params) => {
|
|
try {
|
|
switch (type) {
|
|
case "list_applications": {
|
|
const path = await import("path");
|
|
const fs2 = await import("fs");
|
|
const apps = [];
|
|
const dirs = ["/usr/share/applications", `${process.env.HOME}/.local/share/applications`];
|
|
const seen = new Set;
|
|
for (const dir of dirs) {
|
|
try {
|
|
if (!fs2.default.existsSync(dir))
|
|
continue;
|
|
const files = fs2.default.readdirSync(dir).filter((f) => f.endsWith(".desktop"));
|
|
for (const file of files) {
|
|
const filePath = path.default.join(dir, file);
|
|
try {
|
|
const content = fs2.default.readFileSync(filePath, "utf-8");
|
|
if (content.includes("NoDisplay=true"))
|
|
continue;
|
|
const nameMatch = content.match(/^Name=(.+)$/m);
|
|
const execMatch = content.match(/^Exec=(.+)$/m);
|
|
const iconMatch = content.match(/^Icon=(.+)$/m);
|
|
if (nameMatch && execMatch) {
|
|
const name2 = nameMatch[1].trim();
|
|
if (!seen.has(name2)) {
|
|
seen.add(name2);
|
|
const exec = execMatch[1].trim().split(" ")[0].split("/").pop() || execMatch[1].trim();
|
|
const app2 = {
|
|
name: name2,
|
|
command: exec
|
|
};
|
|
if (iconMatch) {
|
|
const iconName = iconMatch[1].trim();
|
|
const resolved = resolveDesktopIconFast(iconName, fs2.default, path.default);
|
|
if (resolved) {
|
|
app2.icon = resolved;
|
|
} else {
|
|
app2.icon = { type: "iconify", data: iconName };
|
|
}
|
|
}
|
|
apps.push(app2);
|
|
}
|
|
}
|
|
} catch {}
|
|
}
|
|
} catch {}
|
|
}
|
|
return apps.length > 0 ? apps : [
|
|
{ name: "Terminal", icon: { type: "iconify", data: "mdi:console" }, command: "xterm" },
|
|
{ name: "Firefox", icon: { type: "iconify", data: "mdi:firefox" }, command: "firefox" },
|
|
{ name: "Files", icon: { type: "iconify", data: "mdi:folder" }, command: "nautilus" }
|
|
];
|
|
}
|
|
case "launch":
|
|
case "exec": {
|
|
const { spawn } = await import("child_process");
|
|
const command = params.command || "";
|
|
if (!command) {
|
|
return { ok: false, error: "No command specified" };
|
|
}
|
|
try {
|
|
const child = spawn(command, {
|
|
detached: true,
|
|
stdio: "ignore",
|
|
shell: true
|
|
});
|
|
child.unref();
|
|
return { ok: true, pid: child.pid };
|
|
} catch (error) {
|
|
return { ok: false, error: String(error) };
|
|
}
|
|
}
|
|
case "listdir": {
|
|
const fs2 = await import("fs");
|
|
const path = await import("path");
|
|
const dirPath = params.path || process.env.HOME || "/home";
|
|
try {
|
|
const entries = fs2.default.readdirSync(dirPath, { withFileTypes: true });
|
|
return {
|
|
ok: true,
|
|
path: dirPath,
|
|
entries: entries.map((e) => ({
|
|
name: e.name,
|
|
type: e.isDirectory() ? "dir" : "file"
|
|
}))
|
|
};
|
|
} catch (error) {
|
|
return { ok: false, error: String(error) };
|
|
}
|
|
}
|
|
case "read": {
|
|
const fs2 = await import("fs");
|
|
const filePath = params.path;
|
|
if (!filePath) {
|
|
return { ok: false, error: "No file path specified" };
|
|
}
|
|
try {
|
|
const content = fs2.default.readFileSync(filePath, "utf-8");
|
|
return { ok: true, content, size: content.length };
|
|
} catch (error) {
|
|
return { ok: false, error: String(error) };
|
|
}
|
|
}
|
|
case "write": {
|
|
const fs2 = await import("fs");
|
|
const filePath = params.path;
|
|
const content = params.content;
|
|
if (!filePath || content === undefined) {
|
|
return { ok: false, error: "Missing file path or content" };
|
|
}
|
|
try {
|
|
fs2.default.writeFileSync(filePath, content);
|
|
return { ok: true, bytesWritten: content.length };
|
|
} catch (error) {
|
|
return { ok: false, error: String(error) };
|
|
}
|
|
}
|
|
case "shutdown": {
|
|
const { spawn } = await import("child_process");
|
|
spawn("systemctl", ["poweroff"], { detached: true, stdio: "ignore" }).unref();
|
|
return { ok: true };
|
|
}
|
|
case "restart": {
|
|
const { spawn } = await import("child_process");
|
|
spawn("systemctl", ["reboot"], { detached: true, stdio: "ignore" }).unref();
|
|
return { ok: true };
|
|
}
|
|
case "logout": {
|
|
const { spawn } = await import("child_process");
|
|
const sessionId = process.env.XDG_SESSION_ID || "";
|
|
if (sessionId) {
|
|
spawn("loginctl", ["terminate-session", sessionId], { detached: true, stdio: "ignore" }).unref();
|
|
} else {
|
|
sendActionToCompositor("quit", { code: 0 });
|
|
}
|
|
return { ok: true };
|
|
}
|
|
case "lock": {
|
|
const { spawn } = await import("child_process");
|
|
spawn("loginctl", ["lock-session"], { detached: true, stdio: "ignore" }).unref();
|
|
return { ok: true };
|
|
}
|
|
case "sleep": {
|
|
const { spawn } = await import("child_process");
|
|
spawn("systemctl", ["suspend"], { detached: true, stdio: "ignore" }).unref();
|
|
return { ok: true };
|
|
}
|
|
case "notify": {
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send("wo:notification", {
|
|
id: params.id || `notify-${Date.now()}`,
|
|
title: params.title || "Notification",
|
|
body: params.body || "",
|
|
icon: params.icon,
|
|
timeout: params.timeout ?? 5000,
|
|
timestamp: Date.now()
|
|
});
|
|
}
|
|
return { ok: true };
|
|
}
|
|
case "tray_list": {
|
|
return getStatusNotifierTrayItems();
|
|
}
|
|
case "tray_activate": {
|
|
const service = String(params.service || "");
|
|
const objectPath = String(params.objectPath || "");
|
|
if (!service || !objectPath) {
|
|
return { ok: false, error: "Missing service or objectPath" };
|
|
}
|
|
const ok = activateStatusNotifierItem(service, objectPath);
|
|
return ok ? { ok: true } : { ok: false, error: "Activation failed" };
|
|
}
|
|
case "tray_secondary_activate": {
|
|
const service = String(params.service || "");
|
|
const objectPath = String(params.objectPath || "");
|
|
if (!service || !objectPath) {
|
|
return { ok: false, error: "Missing service or objectPath" };
|
|
}
|
|
const ok = secondaryActivateStatusNotifierItem(service, objectPath);
|
|
return ok ? { ok: true } : { ok: false, error: "Secondary activation failed" };
|
|
}
|
|
case "tray_context_menu": {
|
|
const service = String(params.service || "");
|
|
const objectPath = String(params.objectPath || "");
|
|
const x = Number(params.x ?? 0);
|
|
const y = Number(params.y ?? 0);
|
|
if (!service || !objectPath) {
|
|
return { ok: false, error: "Missing service or objectPath" };
|
|
}
|
|
const ok = openStatusNotifierContextMenu(service, objectPath, x, y);
|
|
return ok ? { ok: true } : { ok: false, error: "Context menu failed" };
|
|
}
|
|
case "tray_menu_items": {
|
|
const service = String(params.service || "");
|
|
const menuPath = String(params.menuPath || "");
|
|
if (!service || !menuPath) {
|
|
return [];
|
|
}
|
|
return getDBusMenuLayout(service, menuPath);
|
|
}
|
|
case "tray_menu_event": {
|
|
const service = String(params.service || "");
|
|
const menuPath = String(params.menuPath || "");
|
|
const menuId = String(params.menuId || "");
|
|
if (!service || !menuPath || !menuId) {
|
|
return { ok: false, error: "Missing parameters" };
|
|
}
|
|
const ok = triggerDBusMenuEvent(service, menuPath, menuId);
|
|
return ok ? { ok: true } : { ok: false, error: "Menu event failed" };
|
|
}
|
|
case "portal_respond": {
|
|
const requestId = String(params.requestId || "");
|
|
if (!requestId) {
|
|
return { ok: false, error: "Missing portal requestId" };
|
|
}
|
|
return respondToPortalRequest(requestId, {
|
|
allowed: Boolean(params.allowed),
|
|
type: typeof params.type === "string" ? params.type : undefined,
|
|
windowName: params.windowName
|
|
});
|
|
}
|
|
default:
|
|
return { ok: false, error: `Unknown syscall: ${type}` };
|
|
}
|
|
} catch (error) {
|
|
return { ok: false, error: `Syscall error: ${String(error)}` };
|
|
}
|
|
});
|
|
}
|
|
async function createWindow() {
|
|
debugLog("[Wo] createWindow starting");
|
|
const preloadPath = joinPath(__dirname2, "preload.js");
|
|
debugLog("[Wo] preloadPath=" + preloadPath);
|
|
try {
|
|
mainWindow = new BrowserWindow({
|
|
width: CLIENT_MODE ? 1400 : width,
|
|
height: CLIENT_MODE ? 900 : height,
|
|
transparent: true,
|
|
backgroundColor: "#00000000",
|
|
frame: CLIENT_MODE,
|
|
webPreferences: {
|
|
sandbox: false,
|
|
contextIsolation: true,
|
|
preload: preloadPath,
|
|
nodeIntegration: false,
|
|
offscreen: !CLIENT_MODE
|
|
},
|
|
show: CLIENT_MODE
|
|
});
|
|
debugLog("[Wo] BrowserWindow created, CLIENT_MODE=" + CLIENT_MODE + " offscreen=" + !CLIENT_MODE);
|
|
setupIpcHandlers();
|
|
debugLog("[Wo] setupIpcHandlers completed");
|
|
mainWindow.webContents.send("wo:windows", clientWindows);
|
|
if (CLIENT_MODE) {
|
|
return;
|
|
}
|
|
debugLog("[Wo] Loading content URL: " + (url || "minimal fallback"));
|
|
try {
|
|
const pageUrl = url || "data:text/html,<!DOCTYPE html><html><head><style>body{background:white;margin:0;}</style></head><body></body></html>";
|
|
const loadPromise = mainWindow.loadURL(pageUrl);
|
|
await Promise.race([
|
|
loadPromise,
|
|
new Promise((r) => setTimeout(() => {
|
|
debugLog("[Wo] Load timeout, continuing");
|
|
r(undefined);
|
|
}, 3000))
|
|
]);
|
|
debugLog("[Wo] Content loaded from: " + pageUrl);
|
|
} catch (e) {
|
|
debugLog("[Wo] Load failed:", String(e));
|
|
}
|
|
debugLog("[Wo] Page initialized, setting up rendering");
|
|
mainWindow.webContents.focus();
|
|
if (!CLIENT_MODE) {
|
|
debugLog("[Wo] Setting up OSR rendering, FPS=" + (WINDOW_CONFIG.fps ?? 60));
|
|
mainWindow.webContents.setFrameRate(WINDOW_CONFIG.fps ?? 60);
|
|
mainWindow.webContents.on("did-finish-load", () => {
|
|
mainWindow.webContents.setFrameRate(WINDOW_CONFIG.fps ?? 60);
|
|
});
|
|
const frameIntervalMs = Math.round(1000 / (WINDOW_CONFIG.fps ?? 60));
|
|
setInterval(() => {
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.invalidate();
|
|
}
|
|
}, frameIntervalMs);
|
|
let skippedCount = 0;
|
|
mainWindow.webContents.on("paint", (_event, _dirty, image) => {
|
|
if (!dmabufSender || !ipcConnected) {
|
|
skippedCount++;
|
|
debugLog("[Wo] SKIPPED paint (dmabufSender=" + !!dmabufSender + " ipcConnected=" + ipcConnected + ")");
|
|
return;
|
|
}
|
|
if (appUsedDamageHelper) {
|
|
return;
|
|
}
|
|
if (inFlightFrameSeqs.size >= MAX_IN_FLIGHT_FRAMES) {
|
|
skippedCount++;
|
|
if (skippedCount === 1 || skippedCount % 120 === 0) {
|
|
debugLog("[Wo] SKIPPED paint due to frame backpressure inFlight=" + inFlightFrameSeqs.size);
|
|
}
|
|
return;
|
|
}
|
|
const size = image.getSize();
|
|
if (!damageBuffer) {
|
|
damageBuffer = new DamageBuffer(size.width, size.height);
|
|
}
|
|
const pixels = image.getBitmap();
|
|
applyDamagePayload(damageBuffer, {
|
|
width: size.width,
|
|
height: size.height,
|
|
fullFrame: new Uint8Array(pixels.buffer, pixels.byteOffset, pixels.byteLength)
|
|
});
|
|
try {
|
|
const seq = dmabufSender.send(damageBuffer);
|
|
inFlightFrameSeqs.add(seq.toString());
|
|
} catch (e) {
|
|
debugLog("[Wo] ERROR sending frame:", String(e));
|
|
}
|
|
});
|
|
}
|
|
if (process.env.WO_DEBUG === "1") {
|
|
mainWindow.webContents.openDevTools();
|
|
}
|
|
debugLog("[Wo] createWindow completed successfully");
|
|
} catch (error) {
|
|
debugLog("[Wo] createWindow error:", error);
|
|
throw error;
|
|
}
|
|
mainWindow.on("closed", () => {
|
|
mainWindow = null;
|
|
if (windowPositionUpdateTimer) {
|
|
clearInterval(windowPositionUpdateTimer);
|
|
windowPositionUpdateTimer = null;
|
|
}
|
|
});
|
|
if (!CLIENT_MODE && ipcConnected) {
|
|
const sendPosition = () => {
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
const bounds = mainWindow.getBounds();
|
|
sendWindowPositionUpdate(bounds.x, bounds.y, bounds.width, bounds.height);
|
|
}
|
|
};
|
|
sendPosition();
|
|
windowPositionUpdateTimer = setInterval(sendPosition, 100);
|
|
mainWindow.on("move", sendPosition);
|
|
mainWindow.on("resize", sendPosition);
|
|
}
|
|
}
|
|
app.on("ready", async () => {
|
|
debugLog("[Wo] App ready, CLIENT_MODE=", CLIENT_MODE, "WO_STANDALONE=", process.env.WO_STANDALONE);
|
|
try {
|
|
startPortalUiBridge();
|
|
if (!CLIENT_MODE && process.env.WO_STANDALONE !== "1") {
|
|
debugLog("[Wo] Will connect to compositor");
|
|
await connectToCompositor();
|
|
}
|
|
debugLog("[Wo] Creating window");
|
|
await createWindow();
|
|
debugLog("[Wo] Window created successfully");
|
|
} catch (error) {
|
|
debugLog("[Wo] Failed to start app:", error);
|
|
app.quit();
|
|
}
|
|
});
|
|
app.on("window-all-closed", () => {
|
|
stopPortalUiBridge();
|
|
if (process.platform !== "darwin") {
|
|
app.quit();
|
|
}
|
|
});
|
|
app.on("activate", () => {
|
|
if (mainWindow === null) {
|
|
createWindow().catch(console.error);
|
|
}
|
|
});
|
|
app.on("before-quit", () => {
|
|
stopPortalUiBridge();
|
|
closeShmFdCache();
|
|
});
|
|
export {
|
|
sendActionToCompositor,
|
|
connectToCompositor
|
|
};
|