From 739e24b38b00c43e72c110bb90b39eb8a24dd9f3 Mon Sep 17 00:00:00 2001 From: Hellings Date: Mon, 26 Oct 2026 00:05:21 -0400 Subject: [PATCH] Initial commit --- README.md | 19 +- electron/dist/main.js | 328 ++++++++++++++++++++++++++++---- electron/src/main.ts | 322 ++++++++++++++++++++++++++++--- electron/src/preload.ts | 15 ++ electron/src/wo-capabilities.ts | 2 +- electron/tsconfig.json | 2 +- electron/types/index.ts | 1 + electron/types/types.d.ts | 16 +- whatever.txt | 158 +++++++-------- 9 files changed, 705 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index f5aeb0e..d42f9c4 100644 --- a/README.md +++ b/README.md @@ -66,11 +66,14 @@ script assets. Wo prefetches and verifies those files before navigation, caches verified assets by URL and hash, then asks the user before exposing the requested API groups. Granting access applies only to that page/window; a top-level navigation revokes it. Scripts for a manifest-enabled page are served -from Wo's verified cache, and unlisted script loads are blocked. +from Wo's verified cache, and unlisted script loads are blocked. A saved grant +is reused only for the same origin, document hash, declared script hashes, and +capability request. The available permission names are `window-management`, `overlay-rendering`, `desktop-applications`, `xdg`, `dbus`, `portals`, `filesystem`, -`process-launch`, `system-control`, `notifications`, and `audio-capture`. A +`process-launch`, `system-control`, `notifications`, `audio-capture`, and +`settings`. A page can inspect `window.wo.permissions.requested`, `.granted`, and `.has(name)`. The compositor bridge groups (`window.compositor` and `window.woClient`) are present only after at least one API group is approved; unapproved groups are @@ -95,6 +98,18 @@ component theme tokens and keyboard focus styling that Wo preloads and applies to the page. Websites can use the same manifest format to request a window surface or use the overlay-rendering bridge for HTML/CSS/JS surfaces. +API consent is remembered per origin and exact site build. Wo records the +verified HTML hash, the declared JavaScript asset hashes, and the requested +capability set. The grant survives restarts; changing the HTML, a declared +script, or requested capabilities produces a new consent request. Sites with +`settings` can use `window.wo.settings` (`get`, `set`, `remove`, `keys`) for +persistent JSON values. Wo keeps each web origin in its own user settings file +with owner-only permissions, so apps can share their settings across pages on +the same origin without sharing them with other hosts. Store passwords and +tokens with `getSecret`, `setSecret`, and `removeSecret`; Wo encrypts these using +GNOME Keyring/Secret Service or KDE Wallet and declines secret writes when a +secure desktop keyring is unavailable. + Sites with `window-management` can subscribe to the compositor's monitor layout with `window.wo.windows?.monitors(callback)`. Sites with `overlay-rendering` can pin an element to a monitor's global desktop rectangle: diff --git a/electron/dist/main.js b/electron/dist/main.js index fce94de..9d56888 100644 --- a/electron/dist/main.js +++ b/electron/dist/main.js @@ -1,5 +1,5 @@ // src/main.ts -import { app, BrowserWindow, dialog, ipcMain, Menu, protocol } from "electron"; +import { app, BrowserWindow, dialog, ipcMain, Menu, protocol, safeStorage } from "electron"; import { createConnection, createServer } from "net"; import { fileURLToPath as fileURLToPath2 } from "url"; import { dirname, resolve as resolvePath, join as joinPath } from "path"; @@ -281,7 +281,8 @@ var WO_CAPABILITIES = [ "process-launch", "system-control", "notifications", - "audio-capture" + "audio-capture", + "settings" ]; var MAX_INDEX_BYTES = 4 * 1024 * 1024; var MAX_ASSET_BYTES = 16 * 1024 * 1024; @@ -791,7 +792,7 @@ function setupWoCacheProtocol() { debugLog(`[Wo] Could not rewrite cached module imports for ${hash.slice(0, 12)}: ${String(error)}`); } } - return new Response(responseBytes, { + return new Response(new Uint8Array(responseBytes), { headers: { "Content-Type": isStylesheet ? "text/css; charset=utf-8" : "text/javascript; charset=utf-8", "Access-Control-Allow-Origin": "*", @@ -968,6 +969,164 @@ function resourceApprovalKey(pageUrl, resourceUrl) { return createHash2("sha256").update(`${pageUrl} ${resourceUrl}`).digest("hex"); } +function apiConsentKey(site) { + let origin = ""; + try { + origin = new URL(site.pageUrl).origin; + } catch {} + const identity = { + origin, + appId: site.appId, + indexSha256: site.indexSha256, + scriptSha256: site.scriptBodies.map((script) => script.hash).sort(), + capabilities: [...site.capabilities].sort() + }; + return createHash2("sha256").update(JSON.stringify(identity)).digest("hex"); +} +function apiConsentPath(key) { + return joinPath(app.getPath("userData"), "wo-api-consents", `${key}.json`); +} +async function loadApiConsent(site) { + if (!site.trusted || !site.indexSha256) + return null; + try { + const key = apiConsentKey(site); + const record = JSON.parse(await fs.promises.readFile(apiConsentPath(key), "utf8")); + const origin = new URL(site.pageUrl).origin; + const scriptSha256 = site.scriptBodies.map((script) => script.hash).sort(); + if (record.key !== key || record.origin !== origin || record.indexSha256 !== site.indexSha256 || JSON.stringify(record.scriptSha256) !== JSON.stringify(scriptSha256) || !Array.isArray(record.capabilities) || JSON.stringify([...record.capabilities].sort()) !== JSON.stringify([...site.capabilities].sort()) || !Array.isArray(record.granted) || !record.granted.every((capability) => site.capabilities.includes(capability))) + return null; + return record.granted; + } catch { + return null; + } +} +async function saveApiConsent(site, granted) { + const key = apiConsentKey(site); + const origin = new URL(site.pageUrl).origin; + const path = apiConsentPath(key); + const directory = dirname(path); + const temporary = `${path}.${randomUUID()}.tmp`; + const record = { + key, + origin, + appId: site.appId, + indexSha256: site.indexSha256, + scriptSha256: site.scriptBodies.map((script) => script.hash).sort(), + capabilities: [...site.capabilities].sort(), + granted: [...granted].sort(), + savedAt: new Date().toISOString() + }; + await fs.promises.mkdir(directory, { recursive: true, mode: 448 }); + await fs.promises.writeFile(temporary, JSON.stringify(record, null, 2), { mode: 384 }); + await fs.promises.rename(temporary, path); +} +var MAX_SITE_SETTINGS_BYTES = 64 * 1024; +var SETTINGS_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/; +var siteSettingsTransactions = new Map; +var siteSecretTransactions = new Map; +function siteSettingsPath(origin) { + const originHash = createHash2("sha256").update(origin).digest("hex"); + return joinPath(app.getPath("userData"), "site-settings", `${originHash}.json`); +} +function siteSecretsPath(origin) { + const originHash = createHash2("sha256").update(origin).digest("hex"); + return joinPath(app.getPath("userData"), "site-secrets", `${originHash}.json`); +} +function secureSecretStorageAvailable() { + if (!safeStorage.isEncryptionAvailable()) + return false; + const backend = safeStorage.getSelectedStorageBackend(); + return ["gnome_libsecret", "kwallet", "kwallet5", "kwallet6"].includes(backend); +} +async function readSiteSecrets(origin) { + if (!secureSecretStorageAvailable()) + throw new Error("Secure secret storage requires GNOME Keyring/Secret Service or KDE Wallet"); + let stored; + try { + const parsed = JSON.parse(await fs.promises.readFile(siteSecretsPath(origin), "utf8")); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) + return Object.create(null); + stored = parsed; + } catch (error) { + if (error.code === "ENOENT") + return Object.create(null); + throw error; + } + const decrypted = Object.create(null); + for (const [key, encoded] of Object.entries(stored)) { + if (typeof encoded !== "string") + continue; + decrypted[key] = safeStorage.decryptString(Buffer.from(encoded, "base64")); + } + return decrypted; +} +async function updateSiteSecrets(origin, update) { + if (!secureSecretStorageAvailable()) + throw new Error("Secure secret storage requires GNOME Keyring/Secret Service or KDE Wallet"); + const previous = siteSecretTransactions.get(origin) ?? Promise.resolve(); + const transaction = previous.catch(() => { + return; + }).then(async () => { + const secrets = await readSiteSecrets(origin); + update(secrets); + const encrypted = Object.create(null); + for (const [key, value] of Object.entries(secrets)) { + encrypted[key] = safeStorage.encryptString(value).toString("base64"); + } + const path = siteSecretsPath(origin); + const directory = dirname(path); + const temporary = `${path}.${randomUUID()}.tmp`; + await fs.promises.mkdir(directory, { recursive: true, mode: 448 }); + await fs.promises.writeFile(temporary, JSON.stringify(encrypted), { mode: 384 }); + await fs.promises.rename(temporary, path); + }); + siteSecretTransactions.set(origin, transaction); + try { + await transaction; + } finally { + if (siteSecretTransactions.get(origin) === transaction) + siteSecretTransactions.delete(origin); + } +} +async function readSiteSettings(origin) { + try { + const value = JSON.parse(await fs.promises.readFile(siteSettingsPath(origin), "utf8")); + if (!value || typeof value !== "object" || Array.isArray(value)) + return {}; + return Object.assign(Object.create(null), value); + } catch { + return {}; + } +} +async function writeSiteSettings(origin, settings) { + const path = siteSettingsPath(origin); + const directory = dirname(path); + const temporary = `${path}.${randomUUID()}.tmp`; + const bytes = JSON.stringify(settings); + if (Buffer.byteLength(bytes) > 1024 * 1024) + throw new Error("site settings exceed 1 MiB"); + await fs.promises.mkdir(directory, { recursive: true, mode: 448 }); + await fs.promises.writeFile(temporary, bytes, { mode: 384 }); + await fs.promises.rename(temporary, path); +} +async function updateSiteSettings(origin, update) { + const previous = siteSettingsTransactions.get(origin) ?? Promise.resolve(); + const transaction = previous.catch(() => { + return; + }).then(async () => { + const settings = await readSiteSettings(origin); + update(settings); + await writeSiteSettings(origin, settings); + }); + siteSettingsTransactions.set(origin, transaction); + try { + await transaction; + } finally { + if (siteSettingsTransactions.get(origin) === transaction) + siteSettingsTransactions.delete(origin); + } +} function resourceCacheDirectory() { return joinPath(app.getPath("userData"), "wo-approved-resources"); } @@ -1206,9 +1365,16 @@ function debugLog(...args) { } debugLog("[Wo] Electron starting, NODE_VERSION=", process.version); var requestedOzonePlatform = (process.env.WO_ELECTRON_OZONE_PLATFORM || "auto").toLowerCase(); -var defaultGpuBackend = "angle"; +var defaultGpuBackend = "auto"; var requestedGpuBackend = (process.env.WO_ELECTRON_GPU_BACKEND || defaultGpuBackend).toLowerCase(); -var gpuBackend = ["vulkan", "gl", "angle", "desktop", "auto"].includes(requestedGpuBackend) ? requestedGpuBackend : defaultGpuBackend; +var validatedGpuBackend = ["vulkan", "gl", "angle", "desktop", "auto"].includes(requestedGpuBackend) ? requestedGpuBackend : defaultGpuBackend; +var waylandOzone = requestedOzonePlatform === "wayland"; +var gpuBackend = waylandOzone && ["auto", "vulkan"].includes(validatedGpuBackend) ? "angle" : validatedGpuBackend; +if (waylandOzone && validatedGpuBackend === "vulkan") { + debugLog("[Wo] Vulkan was requested, but Chromium Vulkan is incompatible with native Wayland; using ANGLE OpenGL."); +} else if (waylandOzone && validatedGpuBackend === "auto") { + debugLog("[Wo] Native Wayland uses ANGLE OpenGL to avoid Chromium selecting its incompatible Vulkan surface path."); +} if (gpuBackend === "desktop") { app.commandLine.appendSwitch("use-gl", "desktop"); } else if (gpuBackend === "angle") { @@ -1229,9 +1395,9 @@ var chromiumFeatures = [ ]; if (gpuBackend === "vulkan") { chromiumFeatures.push("Vulkan", "DefaultANGLEVulkan", "VulkanFromANGLE"); -} else if (requestedOzonePlatform === "wayland" || gpuBackend !== "auto") { +} else if (waylandOzone || gpuBackend !== "auto") { app.commandLine.appendSwitch("disable-vulkan"); - app.commandLine.appendSwitch("disable-features", "Vulkan"); + app.commandLine.appendSwitch("disable-features", "Vulkan,DefaultANGLEVulkan,VulkanFromANGLE"); } app.commandLine.appendSwitch("enable-features", chromiumFeatures.join(",")); app.commandLine.appendSwitch("enable-accelerated-video-decode"); @@ -2587,6 +2753,63 @@ function sendSiteHashesToCompositor() { } function setupIpcHandlers() { ipcMain.handle("wo:performance-status", () => getPerformanceStatus()); + ipcMain.handle("wo:settings", async (event, operation, key, value) => { + if (!senderHasCapability(event.sender, "settings")) + return permissionDenied("settings"); + if (typeof operation !== "string" || !["get", "set", "remove", "keys", "getSecret", "setSecret", "removeSecret"].includes(operation)) { + return { ok: false, error: "Unknown settings operation" }; + } + if (operation !== "keys" && (typeof key !== "string" || !SETTINGS_KEY_PATTERN.test(key) || ["__proto__", "prototype", "constructor"].includes(key))) { + return { ok: false, error: "Setting keys must be 1-128 letters, digits, dots, underscores, colons, or hyphens" }; + } + let origin; + try { + origin = new URL(event.sender.getURL()).origin; + } catch { + return { ok: false, error: "Settings require a valid web origin" }; + } + try { + if (operation === "get") { + const settings = await readSiteSettings(origin); + return settings[key]; + } + if (operation === "keys") + return Object.keys(await readSiteSettings(origin)).sort(); + if (operation === "getSecret") + return (await readSiteSecrets(origin))[key]; + if (operation === "removeSecret") { + await updateSiteSecrets(origin, (secrets) => { + delete secrets[key]; + }); + return { ok: true }; + } + if (operation === "setSecret") { + if (typeof value !== "string" || Buffer.byteLength(value) > MAX_SITE_SETTINGS_BYTES) { + return { ok: false, error: "Secret values must be text and at most 64 KiB" }; + } + await updateSiteSecrets(origin, (secrets) => { + secrets[key] = value; + }); + return { ok: true }; + } + if (operation === "remove") { + await updateSiteSettings(origin, (settings) => { + delete settings[key]; + }); + return { ok: true }; + } + const encoded = JSON.stringify(value); + if (encoded === undefined || Buffer.byteLength(encoded) > MAX_SITE_SETTINGS_BYTES) { + return { ok: false, error: "Settings values must be JSON and at most 64 KiB" }; + } + await updateSiteSettings(origin, (settings) => { + settings[key] = JSON.parse(encoded); + }); + return { ok: true }; + } catch (error) { + return { ok: false, error: String(error) }; + } + }); ipcMain.on("wo:portal-ui-ready", (event) => { if (CLIENT_MODE || !mainWindow || event.sender !== mainWindow.webContents) return; @@ -2796,8 +3019,7 @@ function setupIpcHandlers() { theme: typeof params.theme === "string" ? params.theme : undefined }) ?? null; } - case "launch": - case "exec": { + case "launch": { const { spawn } = await import("child_process"); const command = params.command || ""; if (!command) { @@ -2815,6 +3037,26 @@ function setupIpcHandlers() { return { ok: false, error: String(error) }; } } + case "exec": { + const { execFile } = await import("child_process"); + const command = typeof params.command === "string" ? params.command : ""; + const args = Array.isArray(params.args) ? params.args.filter((arg) => typeof arg === "string").slice(0, 64) : []; + if (!command || args.length !== (Array.isArray(params.args) ? params.args.length : 0)) { + return { ok: false, error: "Command and string arguments are required" }; + } + return await new Promise((resolve) => { + execFile(command, args, { encoding: "utf8", timeout: 15000, maxBuffer: 1048576 }, (error, stdout, stderr) => { + const exitCode = typeof error?.code === "number" ? error.code : error ? 1 : 0; + resolve({ + ok: !error, + stdout: String(stdout || ""), + stderr: String(stderr || ""), + exitCode, + ...error ? { error: String(error) } : {} + }); + }); + }); + } case "listdir": { const fs2 = await import("fs"); const path = await import("path"); @@ -3069,24 +3311,39 @@ async function createWindow() { apiGrantPageUrl = siteAssets.pageUrl; debugLog("[Wo] Granted verified comraw system-shell capabilities"); } else if (siteAssets.trusted && siteAssets.capabilities.length > 0) { - const permissionResult = await dialog.showMessageBox({ - type: "question", - title: "Wo API permission request", - message: `${siteAssets.appId} requests access to Wo APIs`, - detail: `Page: ${pageUrl} -Verified index.html SHA-256: ${siteAssets.indexSha256} + const cachedGrant = await loadApiConsent(siteAssets); + if (cachedGrant) { + apiGrants = new Set(cachedGrant); + apiGrantPageUrl = siteAssets.pageUrl; + debugLog(`[Wo] Restored remembered API consent for ${siteAssets.appId} (${apiConsentKey(siteAssets).slice(0, 12)})`); + } else { + const scriptHashes = siteAssets.scriptBodies.map((script) => script.hash.slice(0, 12)).join(", ") || "(no external scripts)"; + const permissionResult = await dialog.showMessageBox({ + type: "question", + title: "Wo API permission request", + message: `${siteAssets.appId} requests access to Wo APIs`, + detail: `Origin: ${new URL(pageUrl).origin} +Verified HTML SHA-256: ${siteAssets.indexSha256} +Verified JavaScript SHA-256: ${scriptHashes} Requested permissions: ${siteAssets.capabilities.join(", ")} -The site APIs are exposed only for this window while it is open.`, - buttons: ["Allow requested APIs", "Open without Wo APIs"], - defaultId: 1, - cancelId: 1, - noLink: true - }); - if (permissionResult.response === 0) { - apiGrants = new Set(siteAssets.capabilities); - apiGrantPageUrl = siteAssets.pageUrl; +Allowing remembers this decision for this origin and these exact HTML/JavaScript assets. Wo will ask again if any of them change.`, + buttons: ["Allow and remember", "Open without Wo APIs"], + defaultId: 1, + cancelId: 1, + noLink: true + }); + if (permissionResult.response === 0) { + apiGrants = new Set(siteAssets.capabilities); + apiGrantPageUrl = siteAssets.pageUrl; + try { + await saveApiConsent(siteAssets, [...apiGrants]); + debugLog(`[Wo] Remembered API consent for ${siteAssets.appId} (${apiConsentKey(siteAssets).slice(0, 12)})`); + } catch (error) { + debugLog("[Wo] Could not persist API consent:", String(error)); + } + } } } const requested = siteAssets.trusted ? siteAssets.capabilities : []; @@ -3312,14 +3569,21 @@ The site APIs are exposed only for this window while it is open.`, debugLog("[Wo] Loading content URL: " + pageUrl); try { 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); + let loadTimeout; + const navigationOutcome = await Promise.race([ + loadPromise.then(() => "loaded"), + new Promise((resolve) => { + loadTimeout = setTimeout(() => { + debugLog("[Wo] Page navigation is still pending after 3s; continuing compositor startup"); + resolve("pending"); + }, 3000); + }) + ]).finally(() => { + if (loadTimeout) + clearTimeout(loadTimeout); + }); + if (navigationOutcome === "loaded") + debugLog("[Wo] Content loaded from: " + pageUrl); } catch (e) { debugLog("[Wo] Load failed:", String(e)); } diff --git a/electron/src/main.ts b/electron/src/main.ts index 1563011..1ca1736 100644 --- a/electron/src/main.ts +++ b/electron/src/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, dialog, ipcMain, Menu, protocol } from 'electron'; +import { app, BrowserWindow, dialog, ipcMain, Menu, protocol, safeStorage } from 'electron'; import { createConnection, createServer, Socket, Server } from 'net'; import { fileURLToPath } from 'url'; import { dirname, resolve as resolvePath, join as joinPath } from 'path'; @@ -57,7 +57,7 @@ function setupWoCacheProtocol() { debugLog(`[Wo] Could not rewrite cached module imports for ${hash.slice(0, 12)}: ${String(error)}`); } } - return new Response(responseBytes, { + return new Response(new Uint8Array(responseBytes), { headers: { 'Content-Type': isStylesheet ? 'text/css; charset=utf-8' : 'text/javascript; charset=utf-8', 'Access-Control-Allow-Origin': '*', @@ -218,6 +218,168 @@ function resourceApprovalKey(pageUrl: string, resourceUrl: string): string { return createHash('sha256').update(`${pageUrl}\n${resourceUrl}`).digest('hex'); } +function apiConsentKey(site: WoSiteAssets): string { + let origin = ''; + try { origin = new URL(site.pageUrl).origin; } catch { /* untrusted URLs are not cached */ } + const identity = { + origin, + appId: site.appId, + indexSha256: site.indexSha256, + scriptSha256: site.scriptBodies.map((script) => script.hash).sort(), + capabilities: [...site.capabilities].sort(), + }; + return createHash('sha256').update(JSON.stringify(identity)).digest('hex'); +} + +function apiConsentPath(key: string): string { + return joinPath(app.getPath('userData'), 'wo-api-consents', `${key}.json`); +} + +async function loadApiConsent(site: WoSiteAssets): Promise { + if (!site.trusted || !site.indexSha256) return null; + try { + const key = apiConsentKey(site); + const record = JSON.parse(await fs.promises.readFile(apiConsentPath(key), 'utf8')) as { + key?: string; origin?: string; indexSha256?: string; scriptSha256?: string[]; + capabilities?: WoCapability[]; granted?: WoCapability[]; + }; + const origin = new URL(site.pageUrl).origin; + const scriptSha256 = site.scriptBodies.map((script) => script.hash).sort(); + if (record.key !== key || record.origin !== origin || record.indexSha256 !== site.indexSha256 + || JSON.stringify(record.scriptSha256) !== JSON.stringify(scriptSha256) + || !Array.isArray(record.capabilities) + || JSON.stringify([...record.capabilities].sort()) !== JSON.stringify([...site.capabilities].sort()) + || !Array.isArray(record.granted) + || !record.granted.every((capability) => site.capabilities.includes(capability))) return null; + return record.granted; + } catch { + return null; + } +} + +async function saveApiConsent(site: WoSiteAssets, granted: WoCapability[]): Promise { + const key = apiConsentKey(site); + const origin = new URL(site.pageUrl).origin; + const path = apiConsentPath(key); + const directory = dirname(path); + const temporary = `${path}.${randomUUID()}.tmp`; + const record = { + key, + origin, + appId: site.appId, + indexSha256: site.indexSha256, + scriptSha256: site.scriptBodies.map((script) => script.hash).sort(), + capabilities: [...site.capabilities].sort(), + granted: [...granted].sort(), + savedAt: new Date().toISOString(), + }; + await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 }); + await fs.promises.writeFile(temporary, JSON.stringify(record, null, 2), { mode: 0o600 }); + await fs.promises.rename(temporary, path); +} + +const MAX_SITE_SETTINGS_BYTES = 64 * 1024; +const SETTINGS_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/; +const siteSettingsTransactions = new Map>(); +const siteSecretTransactions = new Map>(); + +function siteSettingsPath(origin: string): string { + const originHash = createHash('sha256').update(origin).digest('hex'); + return joinPath(app.getPath('userData'), 'site-settings', `${originHash}.json`); +} + +function siteSecretsPath(origin: string): string { + const originHash = createHash('sha256').update(origin).digest('hex'); + return joinPath(app.getPath('userData'), 'site-secrets', `${originHash}.json`); +} + +function secureSecretStorageAvailable(): boolean { + if (!safeStorage.isEncryptionAvailable()) return false; + const backend = safeStorage.getSelectedStorageBackend(); + return ['gnome_libsecret', 'kwallet', 'kwallet5', 'kwallet6'].includes(backend); +} + +async function readSiteSecrets(origin: string): Promise> { + if (!secureSecretStorageAvailable()) throw new Error('Secure secret storage requires GNOME Keyring/Secret Service or KDE Wallet'); + let stored: Record; + try { + const parsed = JSON.parse(await fs.promises.readFile(siteSecretsPath(origin), 'utf8')); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return Object.create(null); + stored = parsed; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return Object.create(null); + throw error; + } + const decrypted = Object.create(null) as Record; + for (const [key, encoded] of Object.entries(stored)) { + if (typeof encoded !== 'string') continue; + decrypted[key] = safeStorage.decryptString(Buffer.from(encoded, 'base64')); + } + return decrypted; +} + +async function updateSiteSecrets(origin: string, update: (secrets: Record) => void): Promise { + if (!secureSecretStorageAvailable()) throw new Error('Secure secret storage requires GNOME Keyring/Secret Service or KDE Wallet'); + const previous = siteSecretTransactions.get(origin) ?? Promise.resolve(); + const transaction = previous.catch(() => undefined).then(async () => { + const secrets = await readSiteSecrets(origin); + update(secrets); + const encrypted = Object.create(null) as Record; + for (const [key, value] of Object.entries(secrets)) { + encrypted[key] = safeStorage.encryptString(value).toString('base64'); + } + const path = siteSecretsPath(origin); + const directory = dirname(path); + const temporary = `${path}.${randomUUID()}.tmp`; + await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 }); + await fs.promises.writeFile(temporary, JSON.stringify(encrypted), { mode: 0o600 }); + await fs.promises.rename(temporary, path); + }); + siteSecretTransactions.set(origin, transaction); + try { await transaction; } + finally { + if (siteSecretTransactions.get(origin) === transaction) siteSecretTransactions.delete(origin); + } +} + +async function readSiteSettings(origin: string): Promise> { + try { + const value = JSON.parse(await fs.promises.readFile(siteSettingsPath(origin), 'utf8')); + if (!value || typeof value !== 'object' || Array.isArray(value)) return {}; + return Object.assign(Object.create(null) as Record, value); + } catch { + return {}; + } +} + +async function writeSiteSettings(origin: string, settings: Record): Promise { + const path = siteSettingsPath(origin); + const directory = dirname(path); + const temporary = `${path}.${randomUUID()}.tmp`; + const bytes = JSON.stringify(settings); + if (Buffer.byteLength(bytes) > 1024 * 1024) throw new Error('site settings exceed 1 MiB'); + await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 }); + await fs.promises.writeFile(temporary, bytes, { mode: 0o600 }); + await fs.promises.rename(temporary, path); +} + +async function updateSiteSettings( + origin: string, + update: (settings: Record) => void, +): Promise { + const previous = siteSettingsTransactions.get(origin) ?? Promise.resolve(); + const transaction = previous.catch(() => undefined).then(async () => { + const settings = await readSiteSettings(origin); + update(settings); + await writeSiteSettings(origin, settings); + }); + siteSettingsTransactions.set(origin, transaction); + try { await transaction; } + finally { + if (siteSettingsTransactions.get(origin) === transaction) siteSettingsTransactions.delete(origin); + } +} + function resourceCacheDirectory(): string { return joinPath(app.getPath('userData'), 'wo-approved-resources'); } @@ -469,14 +631,25 @@ debugLog('[Wo] Electron starting, NODE_VERSION=', process.version); // Wayland without depending on a separate X server; explicit backend choices // remain available for systems that need desktop GL or Vulkan. const requestedOzonePlatform = (process.env.WO_ELECTRON_OZONE_PLATFORM || 'auto').toLowerCase(); -// The nested Wayland surface cannot present through Chromium's Vulkan path. -// Pin ANGLE/OpenGL by default; set WO_ELECTRON_GPU_BACKEND=auto to ask Chromium -// to choose its own implementation when diagnosing a driver-specific issue. -const defaultGpuBackend = 'angle'; +// Let Chromium select the GL implementation from the active Wayland/driver +// stack by default. Chromium's automatic backend currently selects Vulkan on +// some Wayland/NVIDIA setups even when Vulkan is disabled below, so pin native +// Wayland to ANGLE's OpenGL renderer. This keeps the hardware EGL/GLES path and +// avoids Chromium's incompatible Wayland/Vulkan surface factory. +const defaultGpuBackend = 'auto'; const requestedGpuBackend = (process.env.WO_ELECTRON_GPU_BACKEND || defaultGpuBackend).toLowerCase(); -const gpuBackend = ['vulkan', 'gl', 'angle', 'desktop', 'auto'].includes(requestedGpuBackend) +const validatedGpuBackend = ['vulkan', 'gl', 'angle', 'desktop', 'auto'].includes(requestedGpuBackend) ? requestedGpuBackend : defaultGpuBackend; +const waylandOzone = requestedOzonePlatform === 'wayland'; +const gpuBackend = waylandOzone && ['auto', 'vulkan'].includes(validatedGpuBackend) + ? 'angle' + : validatedGpuBackend; +if (waylandOzone && validatedGpuBackend === 'vulkan') { + debugLog('[Wo] Vulkan was requested, but Chromium Vulkan is incompatible with native Wayland; using ANGLE OpenGL.'); +} else if (waylandOzone && validatedGpuBackend === 'auto') { + debugLog('[Wo] Native Wayland uses ANGLE OpenGL to avoid Chromium selecting its incompatible Vulkan surface path.'); +} if (gpuBackend === 'desktop') { // On X11, ask Chromium for the system GLX/EGL implementation directly. // ANGLE's GL backend can fail to initialize on nested XWayland and then @@ -500,11 +673,12 @@ const chromiumFeatures = [ ]; if (gpuBackend === 'vulkan') { chromiumFeatures.push('Vulkan', 'DefaultANGLEVulkan', 'VulkanFromANGLE'); -} else if (requestedOzonePlatform === 'wayland' || gpuBackend !== 'auto') { - // Chromium's Linux Wayland surface factory rejects Vulkan. Keep it disabled - // while using the explicitly selected ANGLE/OpenGL implementation. +} else if (waylandOzone || gpuBackend !== 'auto') { + // Chromium may start its Vulkan surface path even when its reported feature + // status says Vulkan is disabled. Disable the Vulkan feature family on + // Wayland, while leaving GL/EGL backend selection automatic and accelerated. app.commandLine.appendSwitch('disable-vulkan'); - app.commandLine.appendSwitch('disable-features', 'Vulkan'); + app.commandLine.appendSwitch('disable-features', 'Vulkan,DefaultANGLEVulkan,VulkanFromANGLE'); } app.commandLine.appendSwitch('enable-features', chromiumFeatures.join(',')); app.commandLine.appendSwitch('enable-accelerated-video-decode'); @@ -1845,6 +2019,51 @@ function sendSiteHashesToCompositor(): void { function setupIpcHandlers() { ipcMain.handle('wo:performance-status', () => getPerformanceStatus()); + ipcMain.handle('wo:settings', async (event, operation: unknown, key: unknown, value?: unknown) => { + if (!senderHasCapability(event.sender, 'settings')) return permissionDenied('settings'); + if (typeof operation !== 'string' || !['get', 'set', 'remove', 'keys', 'getSecret', 'setSecret', 'removeSecret'].includes(operation)) { + return { ok: false, error: 'Unknown settings operation' }; + } + if (operation !== 'keys' && (typeof key !== 'string' || !SETTINGS_KEY_PATTERN.test(key) + || ['__proto__', 'prototype', 'constructor'].includes(key))) { + return { ok: false, error: 'Setting keys must be 1-128 letters, digits, dots, underscores, colons, or hyphens' }; + } + let origin: string; + try { origin = new URL(event.sender.getURL()).origin; } + catch { return { ok: false, error: 'Settings require a valid web origin' }; } + try { + if (operation === 'get') { + const settings = await readSiteSettings(origin); + return settings[key as string]; + } + if (operation === 'keys') return Object.keys(await readSiteSettings(origin)).sort(); + if (operation === 'getSecret') return (await readSiteSecrets(origin))[key as string]; + if (operation === 'removeSecret') { + await updateSiteSecrets(origin, (secrets) => { delete secrets[key as string]; }); + return { ok: true }; + } + if (operation === 'setSecret') { + if (typeof value !== 'string' || Buffer.byteLength(value) > MAX_SITE_SETTINGS_BYTES) { + return { ok: false, error: 'Secret values must be text and at most 64 KiB' }; + } + await updateSiteSecrets(origin, (secrets) => { secrets[key as string] = value; }); + return { ok: true }; + } + if (operation === 'remove') { + await updateSiteSettings(origin, (settings) => { delete settings[key as string]; }); + return { ok: true }; + } + const encoded = JSON.stringify(value); + if (encoded === undefined || Buffer.byteLength(encoded) > MAX_SITE_SETTINGS_BYTES) { + return { ok: false, error: 'Settings values must be JSON and at most 64 KiB' }; + } + await updateSiteSettings(origin, (settings) => { settings[key as string] = JSON.parse(encoded); }); + return { ok: true }; + } catch (error) { + return { ok: false, error: String(error) }; + } + }); + ipcMain.on('wo:portal-ui-ready', (event) => { if (CLIENT_MODE || !mainWindow || event.sender !== mainWindow.webContents) return; portalUiRendererReady = true; @@ -2067,7 +2286,7 @@ function setupIpcHandlers() { } case 'launch': - case 'exec': { + { const { spawn } = await import('child_process'); const command = (params.command as string) || ''; @@ -2088,6 +2307,29 @@ function setupIpcHandlers() { } } + case 'exec': { + const { execFile } = await import('child_process'); + const command = typeof params.command === 'string' ? params.command : ''; + const args = Array.isArray(params.args) + ? params.args.filter((arg): arg is string => typeof arg === 'string').slice(0, 64) + : []; + if (!command || args.length !== (Array.isArray(params.args) ? params.args.length : 0)) { + return { ok: false, error: 'Command and string arguments are required' }; + } + return await new Promise((resolve) => { + execFile(command, args, { encoding: 'utf8', timeout: 15_000, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => { + const exitCode = typeof error?.code === 'number' ? error.code : error ? 1 : 0; + resolve({ + ok: !error, + stdout: String(stdout || ''), + stderr: String(stderr || ''), + exitCode, + ...(error ? { error: String(error) } : {}), + }); + }); + }); + } + case 'listdir': { const fs = await import('fs'); const path = await import('path'); @@ -2369,19 +2611,33 @@ async function createWindow() { apiGrantPageUrl = siteAssets.pageUrl; debugLog('[Wo] Granted verified comraw system-shell capabilities'); } else if (siteAssets.trusted && siteAssets.capabilities.length > 0) { - const permissionResult = await dialog.showMessageBox({ - type: 'question', - title: 'Wo API permission request', - message: `${siteAssets.appId} requests access to Wo APIs`, - detail: `Page: ${pageUrl}\nVerified index.html SHA-256: ${siteAssets.indexSha256}\n\nRequested permissions: ${siteAssets.capabilities.join(', ')}\n\nThe site APIs are exposed only for this window while it is open.`, - buttons: ['Allow requested APIs', 'Open without Wo APIs'], - defaultId: 1, - cancelId: 1, - noLink: true, - }); - if (permissionResult.response === 0) { - apiGrants = new Set(siteAssets.capabilities); + const cachedGrant = await loadApiConsent(siteAssets); + if (cachedGrant) { + apiGrants = new Set(cachedGrant); apiGrantPageUrl = siteAssets.pageUrl; + debugLog(`[Wo] Restored remembered API consent for ${siteAssets.appId} (${apiConsentKey(siteAssets).slice(0, 12)})`); + } else { + const scriptHashes = siteAssets.scriptBodies.map((script) => script.hash.slice(0, 12)).join(', ') || '(no external scripts)'; + const permissionResult = await dialog.showMessageBox({ + type: 'question', + title: 'Wo API permission request', + message: `${siteAssets.appId} requests access to Wo APIs`, + detail: `Origin: ${new URL(pageUrl).origin}\nVerified HTML SHA-256: ${siteAssets.indexSha256}\nVerified JavaScript SHA-256: ${scriptHashes}\n\nRequested permissions: ${siteAssets.capabilities.join(', ')}\n\nAllowing remembers this decision for this origin and these exact HTML/JavaScript assets. Wo will ask again if any of them change.`, + buttons: ['Allow and remember', 'Open without Wo APIs'], + defaultId: 1, + cancelId: 1, + noLink: true, + }); + if (permissionResult.response === 0) { + apiGrants = new Set(siteAssets.capabilities); + apiGrantPageUrl = siteAssets.pageUrl; + try { + await saveApiConsent(siteAssets, [...apiGrants]); + debugLog(`[Wo] Remembered API consent for ${siteAssets.appId} (${apiConsentKey(siteAssets).slice(0, 12)})`); + } catch (error) { + debugLog('[Wo] Could not persist API consent:', String(error)); + } + } } } const requested = siteAssets.trusted ? siteAssets.capabilities : []; @@ -2624,11 +2880,19 @@ async function createWindow() { debugLog('[Wo] Loading content URL: ' + pageUrl); try { 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); + let loadTimeout: ReturnType | undefined; + const navigationOutcome = await Promise.race([ + loadPromise.then(() => 'loaded' as const), + new Promise<'pending'>((resolve) => { + loadTimeout = setTimeout(() => { + debugLog('[Wo] Page navigation is still pending after 3s; continuing compositor startup'); + resolve('pending'); + }, 3000); + }), + ]).finally(() => { + if (loadTimeout) clearTimeout(loadTimeout); + }); + if (navigationOutcome === 'loaded') debugLog('[Wo] Content loaded from: ' + pageUrl); } catch (e) { debugLog('[Wo] Load failed:', String(e)); } diff --git a/electron/src/preload.ts b/electron/src/preload.ts index 85f7741..edcb7ce 100644 --- a/electron/src/preload.ts +++ b/electron/src/preload.ts @@ -31,6 +31,7 @@ import type { WoPermissionState, WoPerformanceStatus, WoMonitor, + WoSettings, } from 'wo-types'; import { createRendererDamageHelper } from './renderer-damage-helper.js'; @@ -89,6 +90,16 @@ const compositorAPI: CompositorAPI = { return await ipcRenderer.invoke('wo:syscall', type, params); }, + settings: { + get: (key: string) => ipcRenderer.invoke('wo:settings', 'get', key) as Promise, + set: (key: string, value: unknown) => ipcRenderer.invoke('wo:settings', 'set', key, value) as Promise<{ ok: boolean; error?: string }>, + remove: (key: string) => ipcRenderer.invoke('wo:settings', 'remove', key) as Promise<{ ok: boolean; error?: string }>, + keys: () => ipcRenderer.invoke('wo:settings', 'keys') as Promise, + getSecret: (key: string) => ipcRenderer.invoke('wo:settings', 'getSecret', key) as Promise, + setSecret: (key: string, value: string) => ipcRenderer.invoke('wo:settings', 'setSecret', key, value) as Promise<{ ok: boolean; error?: string }>, + removeSecret: (key: string) => ipcRenderer.invoke('wo:settings', 'removeSecret', key) as Promise<{ ok: boolean; error?: string }>, + } satisfies WoSettings, + dbus: { call: (options: DBusCallOptions) => ipcRenderer.invoke('wo:syscall', 'dbus_call', options), getProperty: (options: DBusPropertyOptions) => ipcRenderer.invoke('wo:syscall', 'dbus_get_property', options), @@ -279,6 +290,7 @@ const woClientAPI: WoClientAPI = { icons: compositorAPI.icons, xdg: compositorAPI.xdg, audio: compositorAPI.audio, + settings: compositorAPI.settings, }; function readCapabilityArgument(name: string): WoCapability[] { @@ -326,6 +338,7 @@ if (!grantedCapabilities.has('dbus')) hide('dbus'); if (!grantedCapabilities.has('xdg')) hide('icons', 'xdg', 'onEnvUpdate'); if (!grantedCapabilities.has('portals')) hide('portals', 'onPortalRequest', 'onScreencopyEvent'); if (!grantedCapabilities.has('audio-capture')) hide('audio'); +if (!grantedCapabilities.has('settings')) hide('settings'); if (!grantedCapabilities.has('notifications')) hide('onNotification'); if (grantedCapabilities.size > 0) { contextBridge.exposeInMainWorld('compositor', compositorExposure); @@ -341,6 +354,7 @@ if (grantedCapabilities.size > 0) { if (!grantedCapabilities.has('xdg')) delete clientExposure.icons; if (!grantedCapabilities.has('xdg')) delete clientExposure.xdg; if (!grantedCapabilities.has('audio-capture')) delete clientExposure.audio; + if (!grantedCapabilities.has('settings')) delete clientExposure.settings; contextBridge.exposeInMainWorld('woClient', clientExposure); const woExposure: Record = { @@ -410,6 +424,7 @@ if (grantedCapabilities.size > 0) { if (grantedCapabilities.has('dbus')) woExposure.dbus = compositorAPI.dbus; if (grantedCapabilities.has('portals')) woExposure.portals = compositorAPI.portals; if (grantedCapabilities.has('audio-capture')) woExposure.audio = compositorAPI.audio; + if (grantedCapabilities.has('settings')) woExposure.settings = compositorAPI.settings; if (grantedCapabilities.has('notifications')) woExposure.onNotification = compositorAPI.onNotification; woExposure.syscall = compositorAPI.syscall; contextBridge.exposeInMainWorld('wo', woExposure); diff --git a/electron/src/wo-capabilities.ts b/electron/src/wo-capabilities.ts index 8e2655c..0d7992e 100644 --- a/electron/src/wo-capabilities.ts +++ b/electron/src/wo-capabilities.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'; export const WO_CAPABILITIES = [ 'window-management', 'overlay-rendering', 'desktop-applications', 'xdg', 'dbus', 'portals', - 'filesystem', 'process-launch', 'system-control', 'notifications', 'audio-capture', + 'filesystem', 'process-launch', 'system-control', 'notifications', 'audio-capture', 'settings', ] as const; export type WoCapability = typeof WO_CAPABILITIES[number]; diff --git a/electron/tsconfig.json b/electron/tsconfig.json index cf78736..5f3528a 100644 --- a/electron/tsconfig.json +++ b/electron/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "esnext", "module": "ESNext", - "lib": ["ES2020"], + "lib": ["ES2020", "DOM"], "moduleResolution": "node", "baseUrl": ".", diff --git a/electron/types/index.ts b/electron/types/index.ts index 52a8afe..6b3cf74 100644 --- a/electron/types/index.ts +++ b/electron/types/index.ts @@ -14,6 +14,7 @@ export type { WoAudio, WoAudioSource, WoAudioChunk, + WoSettings, XdgDirectories, DBusBus, DBusCallOptions, diff --git a/electron/types/types.d.ts b/electron/types/types.d.ts index 098a0b7..6e697db 100644 --- a/electron/types/types.d.ts +++ b/electron/types/types.d.ts @@ -124,7 +124,7 @@ export interface PortalRequestEvent { export type WoCapability = | 'window-management' | 'overlay-rendering' | 'desktop-applications' | 'xdg' | 'dbus' - | 'portals' | 'filesystem' | 'process-launch' | 'system-control' | 'notifications' | 'audio-capture'; + | 'portals' | 'filesystem' | 'process-launch' | 'system-control' | 'notifications' | 'audio-capture' | 'settings'; export interface WoPermissionState { requested: readonly WoCapability[]; @@ -161,10 +161,22 @@ export interface WoGlobalAPI { dbus?: WoDBus; portals?: WoPortals; audio?: WoAudio; + /** Persistent JSON settings isolated to the requesting web origin. */ + settings?: WoSettings; syscall?: Compositor['syscall']; onNotification?: Compositor['onNotification']; } +export interface WoSettings { + get(key: string): Promise; + set(key: string, value: unknown): Promise<{ ok: boolean; error?: string }>; + remove(key: string): Promise<{ ok: boolean; error?: string }>; + keys(): Promise; + getSecret(key: string): Promise; + setSecret(key: string, value: string): Promise<{ ok: boolean; error?: string }>; + removeSecret(key: string): Promise<{ ok: boolean; error?: string }>; +} + export interface ScreencopyEvent { active: boolean; clientCount: number; @@ -339,6 +351,7 @@ export interface WoClient { icons: WoIcons; xdg: WoXdg; audio: WoAudio; + settings: WoSettings; } export interface WoPortals { @@ -419,6 +432,7 @@ export interface Compositor { icons: WoIcons; xdg: WoXdg; audio: WoAudio; + settings: WoSettings; /** * Subscribe to focus changes diff --git a/whatever.txt b/whatever.txt index 9f2a732..599210c 100644 --- a/whatever.txt +++ b/whatever.txt @@ -1,34 +1,34 @@ -2026-10-26T03:36:18.813061Z  INFO wo: wo compositor starting -2026-10-26T03:36:18.813121Z  INFO wo::config: Loading config from "/home/hellings/.config/wo/config.toml" -2026-10-26T03:36:18.813191Z  INFO wo: Attempting nested mode (gles renderer) -2026-10-26T03:36:18.813218Z  INFO wo::nested: Starting nested compositor mode -2026-10-26T03:36:18.813231Z  INFO wo::nested: Press ESC to exit -2026-10-26T03:36:18.813449Z  INFO wo::nested: Wayland socket created socket=wo-0 -2026-10-26T03:36:18.813504Z  INFO wo::nested: Using host Wayland display for nested startup socket=wayland-0 -2026-10-26T03:36:18.856187Z ERROR smithay::backend::egl::ffi: [EGL] 0x3004 (BAD_ATTRIBUTE) eglCreateSyncKHR: EGL_BAD_ATTRIBUTE error: In eglCreateSyncKHR: EGL_SYNC_NATIVE_FENCE_FD_ANDROID specified valid fd butEGL_SYNC_STATUS is also being set +2026-10-26T04:03:16.173817Z  INFO wo: wo compositor starting +2026-10-26T04:03:16.173881Z  INFO wo::config: Loading config from "/home/hellings/.config/wo/config.toml" +2026-10-26T04:03:16.173950Z  INFO wo: Attempting nested mode (gles renderer) +2026-10-26T04:03:16.173979Z  INFO wo::nested: Starting nested compositor mode +2026-10-26T04:03:16.173994Z  INFO wo::nested: Press ESC to exit +2026-10-26T04:03:16.174203Z  INFO wo::nested: Wayland socket created socket=wo-0 +2026-10-26T04:03:16.174268Z  INFO wo::nested: Using host Wayland display for nested startup socket=wayland-0 +2026-10-26T04:03:16.223721Z ERROR smithay::backend::egl::ffi: [EGL] 0x3004 (BAD_ATTRIBUTE) eglCreateSyncKHR: EGL_BAD_ATTRIBUTE error: In eglCreateSyncKHR: EGL_SYNC_NATIVE_FENCE_FD_ANDROID specified valid fd butEGL_SYNC_STATUS is also being set -2026-10-26T03:36:18.911103Z  INFO wo::nested: Nested window size width=1280 height=800 -2026-10-26T03:36:18.911169Z  INFO wo::nested: Using render node: /dev/dri/renderD128 -2026-10-26T03:36:18.911238Z  INFO wo::nested: Collected 672 dmabuf formats from renderer -2026-10-26T03:36:18.911502Z  INFO wo: Started configured service before root windows name=vite pid=1378486 -2026-10-26T03:36:18.913053Z  INFO wo::handlers::screencopy: Registered zwlr_screencopy_manager_v1 global (v3) -2026-10-26T03:36:18.913289Z  INFO wo::cursor: Loading cursor theme 'Adwaita' (size 24) -2026-10-26T03:36:18.913344Z  INFO wo::cursor: Loading cursor theme 'default' (size 24) -2026-10-26T03:36:18.913378Z  INFO wo::cursor: Loading cursor theme 'DMZ-White' (size 24) -2026-10-26T03:36:18.913400Z  INFO wo::cursor: Loading cursor theme 'breeze' (size 24) -2026-10-26T03:36:18.913478Z  INFO wo::electron: IPC socket listening socket="/run/user/1000/wo-ipc.sock" -2026-10-26T03:36:18.916655Z  INFO wo::nested: XWayland server starting -2026-10-26T03:36:18.916688Z  INFO wo::nested: Set WAYLAND_DISPLAY=wo-0 -2026-10-26T03:36:18.919374Z  INFO wo::state: Propagated ["WAYLAND_DISPLAY"] to DBus activation environment -2026-10-26T03:36:18.922988Z  INFO wo::state: Propagated ["WAYLAND_DISPLAY"] to systemd user environment -2026-10-26T03:36:18.923014Z  INFO wo::nested: Nested compositor running -2026-10-26T03:36:18.923039Z  INFO wo::nested: Window resized to 1280x800 -2026-10-26T03:36:18.923058Z  INFO wo::nested: Window resized to 1280x800 -2026-10-26T03:36:18.962626Z  INFO wo::handlers::compositor: Client missing ClientData (likely XWayland), using fallback compositor state -2026-10-26T03:36:19.023297Z  INFO wo::nested: Spawned portal service: "/home/hellings/.local/share/wo/bin/wo-portal" -2026-10-26T03:36:19.025666Z  INFO wo_portal: xdg-desktop-portal-wo starting -2026-10-26T03:36:19.026881Z  INFO wo_portal: Portal service registered as org.freedesktop.impl.portal.desktop.wo -2026-10-26T03:36:19.026938Z  INFO wo_portal: Listening for screen capture requests... +2026-10-26T04:03:16.283939Z  INFO wo::nested: Nested window size width=1280 height=800 +2026-10-26T04:03:16.283997Z  INFO wo::nested: Using render node: /dev/dri/renderD128 +2026-10-26T04:03:16.284065Z  INFO wo::nested: Collected 672 dmabuf formats from renderer +2026-10-26T04:03:16.284257Z  INFO wo: Started configured service before root windows name=vite pid=1405610 +2026-10-26T04:03:16.285775Z  INFO wo::handlers::screencopy: Registered zwlr_screencopy_manager_v1 global (v3) +2026-10-26T04:03:16.286007Z  INFO wo::cursor: Loading cursor theme 'Adwaita' (size 24) +2026-10-26T04:03:16.286058Z  INFO wo::cursor: Loading cursor theme 'default' (size 24) +2026-10-26T04:03:16.286091Z  INFO wo::cursor: Loading cursor theme 'DMZ-White' (size 24) +2026-10-26T04:03:16.286114Z  INFO wo::cursor: Loading cursor theme 'breeze' (size 24) +2026-10-26T04:03:16.286187Z  INFO wo::electron: IPC socket listening socket="/run/user/1000/wo-ipc.sock" +2026-10-26T04:03:16.288689Z  INFO wo::nested: XWayland server starting +2026-10-26T04:03:16.288724Z  INFO wo::nested: Set WAYLAND_DISPLAY=wo-0 +2026-10-26T04:03:16.291220Z  INFO wo::state: Propagated ["WAYLAND_DISPLAY"] to DBus activation environment +2026-10-26T04:03:16.294950Z  INFO wo::state: Propagated ["WAYLAND_DISPLAY"] to systemd user environment +2026-10-26T04:03:16.294980Z  INFO wo::nested: Nested compositor running +2026-10-26T04:03:16.295013Z  INFO wo::nested: Window resized to 1280x800 +2026-10-26T04:03:16.295036Z  INFO wo::nested: Window resized to 1280x800 +2026-10-26T04:03:16.328406Z  INFO wo::handlers::compositor: Client missing ClientData (likely XWayland), using fallback compositor state +2026-10-26T04:03:16.395419Z  INFO wo::nested: Spawned portal service: "/home/hellings/.local/share/wo/bin/wo-portal" +2026-10-26T04:03:16.397631Z  INFO wo_portal: xdg-desktop-portal-wo starting +2026-10-26T04:03:16.398478Z  INFO wo_portal: Portal service registered as org.freedesktop.impl.portal.desktop.wo +2026-10-26T04:03:16.398508Z  INFO wo_portal: Listening for screen capture requests... (II) GLX: Initialized glamor GL provider for screen 0 The XKEYBOARD keymap compiler (xkbcomp) reports: > Warning: Could not resolve keysym XF86CameraAccessEnable @@ -49,106 +49,80 @@ The XKEYBOARD keymap compiler (xkbcomp) reports: > Warning: Could not resolve keysym XF86SidevuSonar > Warning: Could not resolve keysym XF86NavInfo Errors from xkbcomp are not fatal to the X server -2026-10-26T03:36:19.106508Z  INFO wo::nested: XWayland ready on DISPLAY :1 -2026-10-26T03:36:19.108904Z  INFO wo::state: Propagated ["DISPLAY", "WAYLAND_DISPLAY"] to DBus activation environment -2026-10-26T03:36:19.112684Z  INFO wo::state: Propagated ["DISPLAY", "WAYLAND_DISPLAY"] to systemd user environment +2026-10-26T04:03:16.476543Z  INFO wo::nested: XWayland ready on DISPLAY :1 +2026-10-26T04:03:16.478811Z  INFO wo::state: Propagated ["DISPLAY", "WAYLAND_DISPLAY"] to DBus activation environment +2026-10-26T04:03:16.482425Z  INFO wo::state: Propagated ["DISPLAY", "WAYLAND_DISPLAY"] to systemd user environment The XKEYBOARD keymap compiler (xkbcomp) reports: > Warning: Unsupported maximum keycode 708, clipping. > X11 cannot support keycodes above 255. > Warning: Virtual modifier ScrollLock multiply defined > Using 0, ignoring 0 Errors from xkbcomp are not fatal to the X server -2026-10-26T03:36:19.116806Z  INFO wo::nested: X11 window manager attached -2026-10-26T03:36:19.116829Z  INFO wo::electron: Spawning Electron for window 'main' with config: WindowConfig { name: "main", system_shell: true, url: Some("http://127.0.0.1:5173"), html: None, css: None, js: None, width: 1280, height: 800, x: 0, y: 0, z_order: -1073741824, fps: 0, format: "ARGB8888", focusable: false, interactive: false, floating: false } -2026-10-26T03:36:19.116868Z  INFO wo::electron: Serialized config: {"name":"main","url":"http://127.0.0.1:5173","html":null,"css":null,"js":null,"width":1280,"height":800,"x":0,"y":0,"z_order":-1073741824,"fps":0,"format":"ARGB8888","focusable":false,"interactive":false,"floating":false} -2026-10-26T03:36:19.117076Z  INFO wo::electron: spawned Electron process window=main pid=1378527 render_node="/dev/dri/renderD128" ozone=wayland +2026-10-26T04:03:16.486053Z  INFO wo::nested: X11 window manager attached +2026-10-26T04:03:16.486078Z  INFO wo::electron: Spawning Electron for window 'main' with config: WindowConfig { name: "main", system_shell: true, url: Some("http://127.0.0.1:5173"), html: None, css: None, js: None, width: 1280, height: 800, x: 0, y: 0, z_order: -1073741824, fps: 0, format: "ARGB8888", focusable: false, interactive: false, floating: false } +2026-10-26T04:03:16.486111Z  INFO wo::electron: Serialized config: {"name":"main","url":"http://127.0.0.1:5173","html":null,"css":null,"js":null,"width":1280,"height":800,"x":0,"y":0,"z_order":-1073741824,"fps":0,"format":"ARGB8888","focusable":false,"interactive":false,"floating":false} +2026-10-26T04:03:16.486335Z  INFO wo::electron: spawned Electron process window=main pid=1405653 render_node="/dev/dri/renderD128" ozone=wayland [Wo] Electron starting, NODE_VERSION= v24.22.0 [Wo] GPU backend preference: auto; Ozone platform: wayland -[Wo] Graphics process context: {"uid":1000,"groups":[4,24,27,30,44,46,100,117,125,995,1000],"env":{"WAYLAND_DISPLAY":"wo-0","DISPLAY":":1","XDG_SESSION_TYPE":"tty","XDG_RUNTIME_DIR":"/run/user/1000","EGL_PLATFORM":"wayland"},"renderNodeAccess":{"path":"/dev/dri/renderD128","mode":"660","readable":true,"writable":true},"switches":[{"name":"ozone-platform","present":true,"value":"wayland"},{"name":"use-gl","present":false,"value":""},{"name":"use-angle","present":false,"value":""},{"name":"disable-gpu","present":false,"value":""},{"name":"disable-vulkan","present":true,"value":""},{"name":"disable-features","present":true,"value":"Vulkan"}]} +[Wo] Graphics process context: {"uid":1000,"groups":[4,24,27,30,44,46,100,117,125,995,1000],"env":{"WAYLAND_DISPLAY":"wo-0","DISPLAY":":1","XDG_SESSION_TYPE":"tty","XDG_RUNTIME_DIR":"/run/user/1000","EGL_PLATFORM":"wayland"},"renderNodeAccess":{"path":"/dev/dri/renderD128","mode":"660","readable":true,"writable":true},"switches":[{"name":"ozone-platform","present":true,"value":"wayland"},{"name":"use-gl","present":false,"value":""},{"name":"use-angle","present":false,"value":""},{"name":"disable-gpu","present":false,"value":""},{"name":"disable-vulkan","present":true,"value":""},{"name":"disable-features","present":true,"value":"Vulkan,DefaultANGLEVulkan,VulkanFromANGLE"}]} [Wo] WO_WINDOW_CONFIG env = {"name":"main","url":"http://127.0.0.1:5173","html":null,"css":null,"js":null,"width":1280,"height":800,"x":0,"y":0,"z_order":-1073741824,"fps":0,"format":"ARGB8888","focusable":false,"interactive":false,"floating":false} [Wo] WINDOW_CONFIG parsed = {"name":"main","url":"http://127.0.0.1:5173","html":null,"css":null,"js":null,"width":1280,"height":800,"x":0,"y":0,"z_order":-1073741824,"fps":0,"format":"ARGB8888","focusable":false,"interactive":false,"floating":false} [Wo] App ready, CLIENT_MODE= false WO_STANDALONE= undefined -[Wo] GPU feature status: {"2d_canvas":"disabled_software","direct_rendering_display_compositor":"disabled_off_ok","gpu_compositing":"disabled_software","multiple_raster_threads":"enabled_force_on","opengl":"disabled_off","rasterization":"disabled_software","raw_draw":"enabled_on","skia_graphite":"disabled_off","trees_in_viz":"disabled_off","video_decode":"disabled_software","video_encode":"disabled_software","vulkan":"disabled_off","webgl":"disabled_off","webgpu":"disabled_off","webgpu_on_vk_via_gl_interop":"disabled_off","webnn":"disabled_off"} +[Wo] GPU feature status: {"2d_canvas":"disabled_software","direct_rendering_display_compositor":"disabled_off_ok","gpu_compositing":"disabled_software","multiple_raster_threads":"enabled_force_on","opengl":"disabled_off","rasterization":"disabled_software","raw_draw":"disabled_off_ok","skia_graphite":"disabled_off","trees_in_viz":"disabled_off","video_decode":"disabled_software","video_encode":"disabled_software","vulkan":"disabled_off","webgl":"disabled_off","webgpu":"disabled_off","webgpu_on_vk_via_gl_interop":"disabled_off","webnn":"disabled_off"} [Wo] GPU info: {"auxAttributes":{"amdSwitchable":false,"canSupportThreadedTextureMailbox":false,"glImplementationParts":"(gl=none,angle=none)","glResetNotificationStrategy":0,"hardwareSupportsVulkan":false,"inProcessGpu":true,"initializationTime":0,"isAsan":false,"isClangCoverage":false,"jpegDecodeAcceleratorSupported":false,"optimus":false,"passthroughCmdDecoder":false,"sandboxed":false,"skiaBackendType":"None","subpixelFontRendering":true,"targetCpuBits":64,"visibilityCallbackCallCount":0},"gpuDevice":[{"active":true,"deviceId":9352,"gpuPreference":0,"vendorId":4318}]} [Wo] Offscreen shared texture: disabled (GPU compositor=disabled_software, threaded texture mailbox=false, WebGL=disabled_off) [Wo] Will connect to compositor [Wo] Connecting to compositor IPC: /run/user/1000/wo-ipc.sock [Wo] Portal UI bridge listening at /run/user/1000/wo-portal-ui.sock -2026-10-26T03:36:19.251134Z  INFO wo::electron: Electron window connected window=main +2026-10-26T04:03:16.636555Z  INFO wo::electron: Electron window connected window=main [Wo] IPC connected, sent HELLO message [Wo] Initializing GBM on render node: /dev/dri/renderD128 [Wo] DMABUF sender initialized [Wo] Creating window [Wo] createWindow starting -127.0.0.1 - - [25/Oct/2026 23:36:19] "GET / HTTP/1.1" 200 - -127.0.0.1 - - [25/Oct/2026 23:36:19] "GET /_wo.capabilities.json HTTP/1.1" 200 - -127.0.0.1 - - [25/Oct/2026 23:36:19] "GET /_wo.components.css HTTP/1.1" 200 - -127.0.0.1 - - [25/Oct/2026 23:36:19] "GET /assets/index-HBQlw-Wm.js HTTP/1.1" 200 - -127.0.0.1 - - [25/Oct/2026 23:36:19] "GET /assets/index-D7GQWtqC.css HTTP/1.1" 200 - -[Wo] Verified site rip.crit.comraw; requested capabilities: window-management, overlay-rendering, desktop-applications, xdg, dbus, portals, process-launch, system-control, notifications +127.0.0.1 - - [26/Oct/2026 00:03:16] "GET / HTTP/1.1" 200 - +127.0.0.1 - - [26/Oct/2026 00:03:16] "GET /_wo.capabilities.json HTTP/1.1" 200 - +127.0.0.1 - - [26/Oct/2026 00:03:16] "GET /_wo.components.css HTTP/1.1" 200 - +127.0.0.1 - - [26/Oct/2026 00:03:16] "GET /assets/index-CW7FlnjF.js HTTP/1.1" 200 - +127.0.0.1 - - [26/Oct/2026 00:03:16] "GET /assets/index-C8Au-Yf1.css HTTP/1.1" 200 - +[Wo] Verified site rip.crit.comraw; requested capabilities: window-management, overlay-rendering, desktop-applications, xdg, dbus, portals, process-launch, system-control, notifications, settings [Wo] Granted verified comraw system-shell capabilities [Wo] preloadPath=/home/hellings/.local/share/wo/electron/dist/preload.js -2026-10-26T03:36:19.290019Z  INFO wo::state: new_fractional_scale: setting preferred_scale(1.0) -2026-10-26T03:36:19.290063Z  INFO wo::state: new_fractional_scale: done -2026-10-26T03:36:19.290115Z  INFO wo::nested: computed and registered document and stylesheet hashes window=main page=http://127.0.0.1:5173/ +2026-10-26T04:03:16.676016Z  INFO wo::state: new_fractional_scale: setting preferred_scale(1.0) +2026-10-26T04:03:16.676055Z  INFO wo::state: new_fractional_scale: done +2026-10-26T04:03:16.676116Z  INFO wo::nested: computed and registered document and stylesheet hashes window=main page=http://127.0.0.1:5173/ [Wo] BrowserWindow created, CLIENT_MODE=false offscreen=true painting=true [Wo] setupIpcHandlers completed [Wo] Loading content URL: http://127.0.0.1:5173 -127.0.0.1 - - [25/Oct/2026 23:36:19] "GET / HTTP/1.1" 304 - +127.0.0.1 - - [26/Oct/2026 00:03:16] "GET / HTTP/1.1" 304 - [Wo] Locking verified script http://127.0.0.1:5173/_wo.components.css -> 8337fca251ea -[Wo] Locking verified script http://127.0.0.1:5173/assets/index-HBQlw-Wm.js -> d1647de85605 -[Wo] Locking verified script http://127.0.0.1:5173/assets/index-D7GQWtqC.css -> c0e2f97ed4d1 -[1378559:1025/233619.349346:ERROR:ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc:249] '--ozone-platform=wayland' is not compatible with Vulkan. Consider switching to '--ozone-platform=x11' or disabling Vulkan +[Wo] Locking verified script http://127.0.0.1:5173/assets/index-CW7FlnjF.js -> ca1bcebf0e74 +[Wo] Locking verified script http://127.0.0.1:5173/assets/index-C8Au-Yf1.css -> 6f4280063490 [Wo] Resource request failed: net::ERR_ABORTED stylesheet http://127.0.0.1:5173/_wo.components.css -[Wo] Resource request failed: net::ERR_ABORTED stylesheet http://127.0.0.1:5173/assets/index-D7GQWtqC.css +[Wo] Resource request failed: net::ERR_ABORTED stylesheet http://127.0.0.1:5173/assets/index-C8Au-Yf1.css [Wo] Serving verified script 8337fca251ea (2190 bytes) -[Wo] Resource request failed: net::ERR_ABORTED script http://127.0.0.1:5173/assets/index-HBQlw-Wm.js -[Wo] Serving verified script c0e2f97ed4d1 (36664 bytes) -[Wo] Serving verified script d1647de85605 (268639 bytes) +[Wo] Resource request failed: net::ERR_ABORTED script http://127.0.0.1:5173/assets/index-CW7FlnjF.js +[Wo] Serving verified script 6f4280063490 (37886 bytes) +[Wo] Serving verified script ca1bcebf0e74 (274364 bytes) +[1405690:1026/000316.820865:ERROR:ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc:249] '--ozone-platform=wayland' is not compatible with Vulkan. Consider switching to '--ozone-platform=x11' or disabling Vulkan [Wo] Content loaded from: http://127.0.0.1:5173 -127.0.0.1 - - [25/Oct/2026 23:36:19] "GET /elementary.jpg HTTP/1.1" 304 - -[Wo] Page render state: {"readyState":"complete","title":"comraw","bodyText":"1\n2\n3\n4\nSun, Oct 25\n11:36 PM","bodyBackground":"rgba(0, 0, 0, 0)","bodyBackgroundImage":"none","bodyDisplay":"block","portalCardBackground":"","portalCardBounds":null,"buttons":12,"resourceConsentBridge":false,"rootChildren":1,"shellPresent":true,"wallpaperImage":"url(\"http://127.0.0.1:5173/elementary.jpg\")","compositorApi":true,"woApi":true} +127.0.0.1 - - [26/Oct/2026 00:03:16] "GET /elementary.jpg HTTP/1.1" 304 - +[Wo] Page render state: {"readyState":"complete","title":"comraw","bodyText":"1\n2\n3\n4\nMon, Oct 26\n12:03 AM","bodyBackground":"rgba(0, 0, 0, 0)","bodyBackgroundImage":"none","bodyDisplay":"block","portalCardBackground":"","portalCardBounds":null,"buttons":13,"resourceConsentBridge":false,"rootChildren":1,"shellPresent":true,"wallpaperImage":"url(\"http://127.0.0.1:5173/elementary.jpg\")","compositorApi":true,"woApi":true} [Wo] Page initialized, setting up rendering [Wo] Offscreen frame pacing: 60 Hz (window cap auto) [Wo] createWindow completed successfully [Wo] Window created successfully [Wo] Ignoring empty offscreen paint 0x0; content=1280x800 dirty=0,0,1280,800 -[1378559:1025/233619.594500:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:438] Unable to initialize SkSurface -[1378559:1025/233619.594551:ERROR:gpu/command_buffer/service/raster_decoder.cc:2939] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glBeginRasterCHROMIUM: failed to create surface -[1378559:1025/233619.594579:ERROR:gpu/command_buffer/service/raster_decoder.cc:3001] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glRasterCHROMIUM: RasterCHROMIUM without BeginRasterCHROMIUM -[1378559:1025/233619.594599:ERROR:gpu/command_buffer/service/raster_decoder.cc:3090] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glEndRasterCHROMIUM: EndRasterCHROMIUM without BeginRasterCHROMIUM -[1378559:1025/233619.594683:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:438] Unable to initialize SkSurface -[1378559:1025/233619.594712:ERROR:gpu/command_buffer/service/raster_decoder.cc:2939] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glBeginRasterCHROMIUM: failed to create surface -[1378559:1025/233619.594738:ERROR:gpu/command_buffer/service/raster_decoder.cc:3001] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glRasterCHROMIUM: RasterCHROMIUM without BeginRasterCHROMIUM -[1378559:1025/233619.594758:ERROR:gpu/command_buffer/service/raster_decoder.cc:3090] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glEndRasterCHROMIUM: EndRasterCHROMIUM without BeginRasterCHROMIUM -[1378559:1025/233619.594801:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:438] Unable to initialize SkSurface -[1378559:1025/233619.594816:ERROR:gpu/command_buffer/service/raster_decoder.cc:2939] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glBeginRasterCHROMIUM: failed to create surface -[1378559:1025/233619.594828:ERROR:gpu/command_buffer/service/raster_decoder.cc:3001] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glRasterCHROMIUM: RasterCHROMIUM without BeginRasterCHROMIUM -[1378559:1025/233619.594848:ERROR:gpu/command_buffer/service/raster_decoder.cc:3090] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glEndRasterCHROMIUM: EndRasterCHROMIUM without BeginRasterCHROMIUM -[1378559:1025/233619.604006:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:438] Unable to initialize SkSurface -[1378559:1025/233619.604035:ERROR:gpu/command_buffer/service/raster_decoder.cc:2939] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glBeginRasterCHROMIUM: failed to create surface -[1378559:1025/233619.604047:ERROR:gpu/command_buffer/service/raster_decoder.cc:3001] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glRasterCHROMIUM: RasterCHROMIUM without BeginRasterCHROMIUM -[1378559:1025/233619.604061:ERROR:gpu/command_buffer/service/raster_decoder.cc:3090] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glEndRasterCHROMIUM: EndRasterCHROMIUM without BeginRasterCHROMIUM -[1378559:1025/233619.604410:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:593] Attempt to read from an uninitialized SharedImage. Initialized region: (0, 0, 0, 0) Size: (1280, 832) -[1378559:1025/233619.607032:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:593] Attempt to read from an uninitialized SharedImage. Initialized region: (0, 0, 0, 0) Size: (1280, 64) -[1378559:1025/233619.609323:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:593] Attempt to read from an uninitialized SharedImage. Initialized region: (0, 0, 0, 0) Size: (192, 256) -[1378559:1025/233619.609467:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:593] Attempt to read from an uninitialized SharedImage. Initialized region: (0, 0, 0, 0) Size: (64, 64) -[Wo] First bitmap pixels (BGRA top-left/mid-left/center/bottom-right): [[255,255,255,255],[255,255,255,255],[255,255,255,255],[255,255,255,255]] +[Wo] First bitmap pixels (BGRA top-left/mid-left/center/bottom-right): [[22,17,17,255],[62,51,111,255],[77,67,127,255],[50,35,49,255]] [Wo] First bitmap frame sent as DMA-BUF: seq=1 size=1280x800 stride=5120 -[Wo] Second bitmap pixels (BGRA top-left/mid-left/center/bottom-right): [[255,255,255,255],[255,255,255,255],[255,255,255,255],[255,255,255,255]] +[Wo] Second bitmap pixels (BGRA top-left/mid-left/center/bottom-right): [[22,17,17,255],[62,51,111,255],[77,67,127,255],[50,35,49,255]] +2026-10-26T04:03:17.187064Z  INFO wo::nested: first Electron frame imported as a DMA-BUF texture window=main width=1280 height=800 seq=1 +2026-10-26T04:03:17.187588Z  INFO wo::cursor: Loaded cursor 'default' (24x24, hotspot 3,1) from /usr/share/icons/Adwaita/cursors/default +2026-10-26T04:03:17.188289Z  INFO wo::nested: first Electron texture rendered into the output frame window=main x=0 y=0 output_width=1280 output_height=800 [Wo] SKIPPED paint due to frame backpressure inFlight=2 -2026-10-26T03:36:19.634388Z  INFO wo::nested: first Electron frame imported as a DMA-BUF texture window=main width=1280 height=800 seq=1 -2026-10-26T03:36:19.634880Z  INFO wo::cursor: Loaded cursor 'default' (24x24, hotspot 3,1) from /usr/share/icons/Adwaita/cursors/default -2026-10-26T03:36:19.635561Z  INFO wo::nested: first Electron texture rendered into the output frame window=main x=0 y=0 output_width=1280 output_height=800 -2026-10-26T03:36:19.663496Z  INFO wo::nested: first nested output frame submitted successfully -[1378559:1025/233619.899568:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:438] Unable to initialize SkSurface -[1378559:1025/233619.899611:ERROR:gpu/command_buffer/service/raster_decoder.cc:2939] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glBeginRasterCHROMIUM: failed to create surface -[1378559:1025/233619.899629:ERROR:gpu/command_buffer/service/raster_decoder.cc:3001] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glRasterCHROMIUM: RasterCHROMIUM without BeginRasterCHROMIUM -[1378559:1025/233619.899638:ERROR:gpu/command_buffer/service/raster_decoder.cc:3090] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glEndRasterCHROMIUM: EndRasterCHROMIUM without BeginRasterCHROMIUM -[1378559:1025/233619.900098:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:593] Attempt to read from an uninitialized SharedImage. Initialized region: (0, 0, 0, 0) Size: (1280, 832) -[Wo] Load timeout, continuing +2026-10-26T04:03:17.218902Z  INFO wo::nested: first nested output frame submitted successfully Serving HTTP on 127.0.0.1 port 5173 (http://127.0.0.1:5173/) ... Keyboard interrupt received, exiting. [Wo] IPC connection closed -Gdk-Message: 23:36:25.499: Error reading events from display: Broken pipe +Gdk-Message: 00:03:23.498: Error reading events from display: Broken pipe