initial commit
This commit is contained in:
parent
877455b386
commit
30102c9871
57 changed files with 9924 additions and 2955 deletions
460
Cargo.lock
generated
460
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
13
Cargo.toml
13
Cargo.toml
|
|
@ -12,6 +12,10 @@ path = "src/main.rs"
|
||||||
name = "wo-portal"
|
name = "wo-portal"
|
||||||
path = "src/bin/wo-portal.rs"
|
path = "src/bin/wo-portal.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "xdg-desktop-portal-wo"
|
||||||
|
path = "src/bin/wo-portal.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Smithay compositor framework
|
# Smithay compositor framework
|
||||||
smithay = { version = "0.7", default-features = false, features = ["backend_drm", "backend_gbm", "backend_egl", "backend_libinput", "backend_udev", "backend_session_libseat", "backend_winit", "renderer_gl", "wayland_frontend", "desktop", "xwayland", "use_system_lib", "wayland-client", "backend_session"] }
|
smithay = { version = "0.7", default-features = false, features = ["backend_drm", "backend_gbm", "backend_egl", "backend_libinput", "backend_udev", "backend_session_libseat", "backend_winit", "renderer_gl", "wayland_frontend", "desktop", "xwayland", "use_system_lib", "wayland-client", "backend_session"] }
|
||||||
|
|
@ -21,7 +25,7 @@ calloop = { version = "0.14", features = ["executor"] }
|
||||||
calloop-wayland-source = "0.4"
|
calloop-wayland-source = "0.4"
|
||||||
|
|
||||||
# DRM/GBM
|
# DRM/GBM
|
||||||
drm = "0.14"
|
drm = "0.15"
|
||||||
gbm = "0.18"
|
gbm = "0.18"
|
||||||
|
|
||||||
# EGL / GL
|
# EGL / GL
|
||||||
|
|
@ -38,13 +42,14 @@ tokio = { version = "1", features = ["full"] }
|
||||||
nix = { version = "0.31", features = ["socket", "uio", "fs", "net", "time"] }
|
nix = { version = "0.31", features = ["socket", "uio", "fs", "net", "time"] }
|
||||||
|
|
||||||
# D-Bus and PipeWire for portal
|
# D-Bus and PipeWire for portal
|
||||||
zbus = { version = "5", default-features = false, features = ["tokio"] }
|
zbus = { version = "6", default-features = false, features = ["tokio", "service"] }
|
||||||
pipewire = { version = "0.9", default-features = false }
|
pipewire = { version = "0.10", default-features = false }
|
||||||
|
|
||||||
# Config
|
# Config
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
toml = "1.0"
|
sha2 = "0.10"
|
||||||
|
toml = "1.1"
|
||||||
|
|
||||||
# Platform syscalls (TIOCOUTQ ioctl for socket buffer checks)
|
# Platform syscalls (TIOCOUTQ ioctl for socket buffer checks)
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|
|
||||||
8
PKGBUILD
8
PKGBUILD
|
|
@ -10,7 +10,7 @@ license=('MIT')
|
||||||
provides=('wo')
|
provides=('wo')
|
||||||
conflicts=('wo')
|
conflicts=('wo')
|
||||||
depends=(
|
depends=(
|
||||||
'electron'
|
'electron>=45'
|
||||||
'libdrm'
|
'libdrm'
|
||||||
'libinput'
|
'libinput'
|
||||||
'seatd'
|
'seatd'
|
||||||
|
|
@ -19,7 +19,8 @@ depends=(
|
||||||
'pipewire'
|
'pipewire'
|
||||||
'wayland'
|
'wayland'
|
||||||
'xdg-desktop-portal'
|
'xdg-desktop-portal'
|
||||||
'xorg-xwayland'
|
'xorg-xwayland',
|
||||||
|
'python3'
|
||||||
)
|
)
|
||||||
makedepends=(
|
makedepends=(
|
||||||
'bun'
|
'bun'
|
||||||
|
|
@ -56,6 +57,8 @@ build() {
|
||||||
|
|
||||||
cd "$srcdir/comraw"
|
cd "$srcdir/comraw"
|
||||||
bun install --no-audit --no-fund
|
bun install --no-audit --no-fund
|
||||||
|
rm -rf node_modules/@wl-wo/wo-types
|
||||||
|
cp -a "$srcdir/wo/electron/types" node_modules/@wl-wo/wo-types
|
||||||
bun run build
|
bun run build
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,6 +67,7 @@ package() {
|
||||||
|
|
||||||
install -Dm755 target/release/wo "$pkgdir/usr/bin/wo"
|
install -Dm755 target/release/wo "$pkgdir/usr/bin/wo"
|
||||||
install -Dm755 target/release/wo-portal "$pkgdir/usr/bin/wo-portal"
|
install -Dm755 target/release/wo-portal "$pkgdir/usr/bin/wo-portal"
|
||||||
|
install -Dm755 target/release/xdg-desktop-portal-wo "$pkgdir/usr/bin/xdg-desktop-portal-wo"
|
||||||
|
|
||||||
install -Dm644 packaging/wayland-sessions/wo.desktop \
|
install -Dm644 packaging/wayland-sessions/wo.desktop \
|
||||||
"$pkgdir/usr/share/wayland-sessions/wo.desktop"
|
"$pkgdir/usr/share/wayland-sessions/wo.desktop"
|
||||||
|
|
|
||||||
169
README.md
169
README.md
|
|
@ -1,3 +1,170 @@
|
||||||
# wo compositor
|
# wo compositor
|
||||||
|
|
||||||
don't bother (woe)
|
Wo is an Electron-centered Smithay Wayland compositor. The portal backend is
|
||||||
|
available as `xdg-desktop-portal-wo`; run `wo portals` to start it directly.
|
||||||
|
The packaged xdg-desktop-portal configuration also starts it on demand.
|
||||||
|
|
||||||
|
## User-local install
|
||||||
|
|
||||||
|
With Rust, Bun, and Python 3 installed, run `just install` from this checkout.
|
||||||
|
It builds Wo, Electron, and comraw, then installs them under `~/.local` and
|
||||||
|
`$XDG_DATA_HOME/wo`. On first install it copies `config.example.toml` to
|
||||||
|
`$XDG_CONFIG_HOME/wo/config.toml` and configures the copied comraw build to be
|
||||||
|
served locally. Existing user configuration is preserved. Add `~/.local/bin`
|
||||||
|
to `PATH` and select the Wo session from your display manager.
|
||||||
|
|
||||||
|
## Portal requests in the Wo API
|
||||||
|
|
||||||
|
Renderer code can subscribe to compositor-mediated requests and resolve them
|
||||||
|
through the context-isolated API. Requests are denied by default if the
|
||||||
|
application does not respond.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const unsubscribe = window.woClient.portals.onRequest(async ({ requestId, payload }) => {
|
||||||
|
const allowed = await showConsentDialog(payload);
|
||||||
|
await window.woClient.portals.respond(requestId, {
|
||||||
|
allowed,
|
||||||
|
type: 'monitor', // or 'window'
|
||||||
|
windowName: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The API is also available as `window.compositor.portals` in compositor UI
|
||||||
|
contexts. Portal approval transport uses a per-user runtime socket with owner
|
||||||
|
only permissions.
|
||||||
|
|
||||||
|
## Desktop and system APIs
|
||||||
|
|
||||||
|
Wo exposes a typed convenience layer as `window.compositor` and
|
||||||
|
`window.woClient`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const directories = await window.compositor.xdg.directories();
|
||||||
|
const apps = await window.compositor.desktop.applications.list();
|
||||||
|
await window.compositor.desktop.applications.launch('org.gnome.Terminal.desktop');
|
||||||
|
const icon = await window.compositor.icons.resolve('utilities-terminal', { size: 48 });
|
||||||
|
const names = await window.compositor.dbus.listNames('user');
|
||||||
|
```
|
||||||
|
|
||||||
|
Desktop entries are read from the XDG data directories, localized names and
|
||||||
|
user directories are honored, icons are resolved from XDG icon themes, and
|
||||||
|
desktop `Exec` fields are launched as argument vectors without a shell. The
|
||||||
|
D-Bus helpers use `busctl`; call arguments should include a D-Bus signature.
|
||||||
|
`window.compositor.audio` lists PipeWire audio sources and can forward PCM from
|
||||||
|
a desktop monitor source to the renderer. PipeWire and `pw-cat` must be
|
||||||
|
available. Audio forwarding is exposed separately from ScreenCast because the
|
||||||
|
standard XDG ScreenCast stream contains video, not a companion audio stream.
|
||||||
|
|
||||||
|
## Website-first Wo API and component overlays
|
||||||
|
|
||||||
|
Wo treats loaded pages as websites first: ordinary pages receive no compositor
|
||||||
|
bridge. A site can publish `/_wo.capabilities.json` and
|
||||||
|
`/_wo.components.css`. The manifest requests named APIs and binds the request to
|
||||||
|
the exact `index.html` SHA-256, a stylesheet SHA-256, and SHA-256 values for its
|
||||||
|
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.
|
||||||
|
|
||||||
|
The available permission names are `window-management`, `overlay-rendering`,
|
||||||
|
`desktop-applications`, `xdg`, `dbus`, `portals`, `filesystem`,
|
||||||
|
`process-launch`, `system-control`, `notifications`, and `audio-capture`. 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
|
||||||
|
omitted and their IPC operations remain denied by the main process.
|
||||||
|
|
||||||
|
Pages granted `portals` can open a desktop context menu with
|
||||||
|
`await window.wo.portals.contextMenu({ x, y, items })`. It resolves with the
|
||||||
|
selected item ID after dismissal. Electron delegates the menu to the native
|
||||||
|
desktop popup implementation, so Wayland presents it as a popup surface and it
|
||||||
|
uses the desktop's native menu theme. Root portal UIs such as comraw's screen
|
||||||
|
share prompt can be themed with `_wo.components.css`.
|
||||||
|
|
||||||
|
Comraw publishes `_wo.capabilities.json`, its schema, and `_wo.components.css`
|
||||||
|
from `comraw/public`. The schema is `_wo.capabilities.schema.json`. Its Vite
|
||||||
|
production build writes the current `index.html` hash, stylesheet hash, and
|
||||||
|
script hashes into the public manifest, so the request stays tied to the exact
|
||||||
|
build. The development server generates the matching index and stylesheet
|
||||||
|
hashes on request. Wo sends the verified
|
||||||
|
document and stylesheet bytes to the compositor, which computes SHA-256 itself
|
||||||
|
and stores both hashes against the owning window. `_wo.components.css` provides
|
||||||
|
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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const unmount = window.wo.overlay?.mount(element, monitor.id);
|
||||||
|
// Call unmount() to restore the element's original inline positioning.
|
||||||
|
```
|
||||||
|
|
||||||
|
Monitor coordinates are global desktop coordinates, so pointer positions and
|
||||||
|
window placement remain consistent when an element is mounted on another
|
||||||
|
display.
|
||||||
|
|
||||||
|
Comraw provides four persistent virtual desktops. Windows and their transient
|
||||||
|
dialogs are assigned together, hidden from other desktops, and can be moved
|
||||||
|
from the window titlebar; each monitor has an independent desktop selection,
|
||||||
|
and Ctrl+Alt+Left/Right switches desktops on the monitor under the pointer.
|
||||||
|
The DRM backend discovers connected outputs, assigns compatible CRTCs, places
|
||||||
|
them side by side, and composites the shared desktop scene into a per-output
|
||||||
|
swapchain. Native Wayland windows use the same global Smithay `Space` and can
|
||||||
|
span monitors. Newly connected DRM outputs are detected by a two-second
|
||||||
|
topology poll and attached when a compatible free CRTC is available. Unplug
|
||||||
|
cleanup and live mode replacement are not implemented yet; restart Wo after
|
||||||
|
removing a monitor or changing its mode. Workspace hiding remains a comraw
|
||||||
|
shell feature; it does not isolate native Wayland clients. Portal monitor
|
||||||
|
capture currently publishes the primary output rather than a stitched
|
||||||
|
multi-monitor image.
|
||||||
|
|
||||||
|
## Native offscreen frames
|
||||||
|
|
||||||
|
On Linux the Electron renderer requests `webPreferences.offscreen.useSharedTexture`
|
||||||
|
and forwards the native-pixmap DMA-BUF planes to Wo over SCM_RIGHTS. This avoids
|
||||||
|
the JS bitmap readback and Wo's RGBA-to-DMA-BUF import for normal offscreen
|
||||||
|
frames. Wo still imports the DMA-BUF into Smithay for composition. The Electron
|
||||||
|
and CEF source trees used to verify upstream support are separate checkouts;
|
||||||
|
their Linux shared-texture/accelerated-paint APIs already expose native pixmap
|
||||||
|
planes, so Wo's integration uses those APIs rather than carrying a fork of the
|
||||||
|
frame capture implementation. The CEF tree is not a current runtime backend.
|
||||||
|
|
||||||
|
`packaging/engines` contains GN argument presets for an optional source build.
|
||||||
|
For Electron, copy `electron-wo.gn` into the Electron checkout's
|
||||||
|
`build/args/wo.gn` and import it from the Chromium `out/<config>/args.gn` as
|
||||||
|
`import("//electron/build/args/wo.gn")`. For CEF, copy `cef-wo.gn` into the CEF
|
||||||
|
checkout's `tools/wo.gn` and import it as `import("//cef/tools/wo.gn")`. Both
|
||||||
|
source builds need the matching Chromium checkout and platform toolchain. The
|
||||||
|
Arch package currently links to the system Electron package, so those presets
|
||||||
|
do not change its Electron binary. Ubuntu builds bundle Electron with
|
||||||
|
`packaging/ubuntu/build-deb.sh` and install a matching launcher/runtime pair.
|
||||||
|
See `packaging/ubuntu/README.md` for the build requirements and install command.
|
||||||
|
|
||||||
|
## GPU rendering and native input
|
||||||
|
|
||||||
|
Wo's compositor renderer uses Smithay GLES through EGL. Electron/Chromium
|
||||||
|
content leaves GPU backend selection to Chromium by default. Vulkan is disabled
|
||||||
|
for native Wayland Electron clients because Chromium's Wayland surface factory
|
||||||
|
does not support it; this leaves Chromium free to use its GL/EGL path. Set
|
||||||
|
`WO_ELECTRON_GPU_BACKEND=angle` or `gl` to select ANGLE OpenGL, `vulkan` for
|
||||||
|
ANGLE/Vulkan, or `desktop` for native X11 GL. To test XWayland/GLX for Electron
|
||||||
|
while Wo itself remains a Wayland compositor, launch with
|
||||||
|
`WO_ELECTRON_OZONE_PLATFORM=x11 WO_ELECTRON_GPU_BACKEND=desktop`. Wo's nested
|
||||||
|
backend prefers an inherited `WAYLAND_DISPLAY`, or discovers a `wayland-*`
|
||||||
|
socket under `XDG_RUNTIME_DIR` when launchers omit that variable. GPU support
|
||||||
|
still depends on the system driver and Chromium build.
|
||||||
|
|
||||||
|
Wayland clients receive pointer motion, buttons, scroll axes, relative pointer
|
||||||
|
motion, and locked-pointer cursor hints through Smithay's native protocols.
|
||||||
|
Forwarded input accounts for client-surface geometry offsets so window borders
|
||||||
|
and client-side decorations do not skew pointer coordinates.
|
||||||
|
|
||||||
|
## Headless tests
|
||||||
|
|
||||||
|
Run the Electron renderer and buffer tests with `cd electron && bun run test`.
|
||||||
|
Run configuration tests without starting Wayland with `cargo test --lib config::tests`.
|
||||||
|
|
|
||||||
1
comraw
Submodule
1
comraw
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 807af019a84995a047a4417d9decd1f64061384b
|
||||||
|
|
@ -8,16 +8,10 @@ portal_socket = "$XDG_RUNTIME_DIR/wo-portal.sock"
|
||||||
enable_syscalls = true
|
enable_syscalls = true
|
||||||
focus_notify = true
|
focus_notify = true
|
||||||
|
|
||||||
# Environment variables passed to all spawned child processes.
|
|
||||||
# DISPLAY defaults to ":0" so X11 apps (e.g. Steam) work even before
|
|
||||||
# XWayland is ready. XWayland overrides DISPLAY with the actual value.
|
|
||||||
[compositor.environment]
|
|
||||||
DISPLAY = ":0"
|
|
||||||
|
|
||||||
[[root]]
|
[[root]]
|
||||||
name = "main"
|
name = "main"
|
||||||
url = "http://localhost:5173"
|
url = "http://127.0.0.1:5173"
|
||||||
fps = 60
|
fps = 0 # 0 follows the display refresh rate; set a positive value to cap this window
|
||||||
focusable = false
|
focusable = false
|
||||||
interactive = false
|
interactive = false
|
||||||
|
|
||||||
|
|
@ -27,5 +21,6 @@ command = "wo://portal/start"
|
||||||
delay = 100
|
delay = 100
|
||||||
|
|
||||||
[[autostart]]
|
[[autostart]]
|
||||||
name = "vite"
|
name = "desktop-site"
|
||||||
command = "cd /
|
command = "python3 -m http.server 5173 --bind 127.0.0.1 --directory \"$WO_DATA_DIR/comraw\" >/dev/null 2>&1"
|
||||||
|
delay = 0
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,17 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/electron": "^latest",
|
"@types/node": "^25.3.5",
|
||||||
"@types/node": "^latest",
|
"electron": "^45.0.0",
|
||||||
"electron": "^29.0.0",
|
"node-gyp": "^10.3.1",
|
||||||
"node-gyp": "^10.0.0",
|
"typescript": "^5.9.3",
|
||||||
"typescript": "^latest",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="],
|
"@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.6", "", {}, "sha512-/aQLgFDdCSnwChfUwqekSmE1q2dCTzhUy5saIHBb5vyYn5LzWaKPaIz1wM0lDMCZYBoIG4svJNvAJb6vkI8BuA=="],
|
||||||
|
|
||||||
|
"@electron/get": ["@electron/get@5.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^3.0.0", "graceful-fs": "^4.2.11", "progress": "^2.0.3", "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "optionalDependencies": { "undici": "^7.24.4" } }, "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA=="],
|
||||||
|
|
||||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||||
|
|
||||||
|
|
@ -28,26 +29,10 @@
|
||||||
|
|
||||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||||
|
|
||||||
"@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
|
|
||||||
|
|
||||||
"@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="],
|
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
||||||
|
|
||||||
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
|
|
||||||
|
|
||||||
"@types/electron": ["@types/electron@1.6.12", "", { "dependencies": { "electron": "*" } }, "sha512-NIJokDkGv9h+MStCL1IuiL1FOHYVkszoWeNxJtSI5dcEKRGbX83JcVYNAgk019qOQgJkHtz9WdP0CDXvrArrGg=="],
|
|
||||||
|
|
||||||
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="],
|
|
||||||
|
|
||||||
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
|
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="],
|
"@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="],
|
||||||
|
|
||||||
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
|
|
||||||
|
|
||||||
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
|
||||||
|
|
||||||
"abbrev": ["abbrev@2.0.0", "", {}, "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ=="],
|
"abbrev": ["abbrev@2.0.0", "", {}, "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ=="],
|
||||||
|
|
||||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||||
|
|
@ -60,26 +45,16 @@
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
|
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
||||||
|
|
||||||
"cacache": ["cacache@18.0.4", "", { "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^4.0.0", "ssri": "^10.0.0", "tar": "^6.1.11", "unique-filename": "^3.0.0" } }, "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ=="],
|
"cacache": ["cacache@18.0.4", "", { "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^4.0.0", "ssri": "^10.0.0", "tar": "^6.1.11", "unique-filename": "^3.0.0" } }, "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ=="],
|
||||||
|
|
||||||
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
|
|
||||||
|
|
||||||
"cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],
|
|
||||||
|
|
||||||
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
|
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
|
||||||
|
|
||||||
"clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
|
"clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
|
||||||
|
|
||||||
"clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
|
|
||||||
|
|
||||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
@ -88,72 +63,32 @@
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
|
||||||
|
|
||||||
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
|
|
||||||
|
|
||||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
|
||||||
|
|
||||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
|
||||||
|
|
||||||
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
|
|
||||||
|
|
||||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||||
|
|
||||||
"electron": ["electron@29.4.6", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^20.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-fz8ndj8cmmf441t4Yh2FDP3Rn0JhLkVGvtUf2YVMbJ5SdJPlc0JWll9jYkhh60jDKVVCr/tBAmfxqRnXMWJpzg=="],
|
"electron": ["electron@45.0.0", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-yjz4uhMBcO6L5X7CWgRMobf2EMJPnb9j4zuf+nu/TU/6E4Sqfx7fn/a2eACo4qngjml/0JnsoZhxYroJLxLd2w=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
|
|
||||||
"encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="],
|
"encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="],
|
||||||
|
|
||||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
|
||||||
|
|
||||||
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||||
|
|
||||||
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
|
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
|
||||||
|
|
||||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
|
||||||
|
|
||||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
|
||||||
|
|
||||||
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
|
|
||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
|
||||||
|
|
||||||
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
|
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
|
||||||
|
|
||||||
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
|
|
||||||
|
|
||||||
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
|
||||||
|
|
||||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||||
|
|
||||||
"fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
|
|
||||||
|
|
||||||
"fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
|
"fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
|
||||||
|
|
||||||
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
|
|
||||||
|
|
||||||
"glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
"glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||||
|
|
||||||
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
|
|
||||||
|
|
||||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
|
||||||
|
|
||||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
|
||||||
|
|
||||||
"got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="],
|
|
||||||
|
|
||||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
|
||||||
|
|
||||||
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||||
|
|
||||||
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||||
|
|
||||||
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
|
|
||||||
|
|
||||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
@ -172,24 +107,10 @@
|
||||||
|
|
||||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||||
|
|
||||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
|
||||||
|
|
||||||
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
|
||||||
|
|
||||||
"jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
|
||||||
|
|
||||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
|
||||||
|
|
||||||
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
|
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"make-fetch-happen": ["make-fetch-happen@13.0.1", "", { "dependencies": { "@npmcli/agent": "^2.0.0", "cacache": "^18.0.0", "http-cache-semantics": "^4.1.1", "is-lambda": "^1.0.1", "minipass": "^7.0.2", "minipass-fetch": "^3.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "proc-log": "^4.2.0", "promise-retry": "^2.0.1", "ssri": "^10.0.0" } }, "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA=="],
|
"make-fetch-happen": ["make-fetch-happen@13.0.1", "", { "dependencies": { "@npmcli/agent": "^2.0.0", "cacache": "^18.0.0", "http-cache-semantics": "^4.1.1", "is-lambda": "^1.0.1", "minipass": "^7.0.2", "minipass-fetch": "^3.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "proc-log": "^4.2.0", "promise-retry": "^2.0.1", "ssri": "^10.0.0" } }, "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA=="],
|
||||||
|
|
||||||
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
|
|
||||||
|
|
||||||
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
|
||||||
|
|
||||||
"minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
"minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
||||||
|
|
||||||
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||||
|
|
@ -218,14 +139,6 @@
|
||||||
|
|
||||||
"nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="],
|
"nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="],
|
||||||
|
|
||||||
"normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="],
|
|
||||||
|
|
||||||
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
|
||||||
|
|
||||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
|
||||||
|
|
||||||
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
|
|
||||||
|
|
||||||
"p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
|
"p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
|
||||||
|
|
||||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||||
|
|
@ -234,34 +147,18 @@
|
||||||
|
|
||||||
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||||
|
|
||||||
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
|
|
||||||
|
|
||||||
"proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="],
|
"proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="],
|
||||||
|
|
||||||
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
|
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
|
||||||
|
|
||||||
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
|
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
|
||||||
|
|
||||||
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
|
|
||||||
|
|
||||||
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
|
|
||||||
|
|
||||||
"resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
|
|
||||||
|
|
||||||
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
|
|
||||||
|
|
||||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||||
|
|
||||||
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
|
||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
|
|
||||||
|
|
||||||
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
|
|
||||||
|
|
||||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
@ -274,8 +171,6 @@
|
||||||
|
|
||||||
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
|
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
|
||||||
|
|
||||||
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
|
||||||
|
|
||||||
"ssri": ["ssri@10.0.6", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ=="],
|
"ssri": ["ssri@10.0.6", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ=="],
|
||||||
|
|
||||||
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||||
|
|
@ -290,49 +185,33 @@
|
||||||
|
|
||||||
"tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
|
"tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
|
||||||
|
|
||||||
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici": ["undici@7.30.1", "", {}, "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
"unique-filename": ["unique-filename@3.0.0", "", { "dependencies": { "unique-slug": "^4.0.0" } }, "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g=="],
|
"unique-filename": ["unique-filename@3.0.0", "", { "dependencies": { "unique-slug": "^4.0.0" } }, "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g=="],
|
||||||
|
|
||||||
"unique-slug": ["unique-slug@4.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ=="],
|
"unique-slug": ["unique-slug@4.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ=="],
|
||||||
|
|
||||||
"universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
|
||||||
|
|
||||||
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||||
|
|
||||||
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
|
||||||
|
|
||||||
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
|
"@electron/get/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
||||||
|
|
||||||
"@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
|
||||||
|
|
||||||
"@types/cacheable-request/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="],
|
|
||||||
|
|
||||||
"@types/keyv/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="],
|
|
||||||
|
|
||||||
"@types/responselike/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="],
|
|
||||||
|
|
||||||
"@types/yauzl/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="],
|
|
||||||
|
|
||||||
"bun-types/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="],
|
"bun-types/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="],
|
||||||
|
|
||||||
"cacache/fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
|
"cacache/fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
|
||||||
|
|
||||||
"clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
|
|
||||||
|
|
||||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
"electron/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="],
|
"electron/@types/node": ["@types/node@24.19.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ=="],
|
||||||
|
|
||||||
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
|
|
||||||
|
|
@ -358,19 +237,11 @@
|
||||||
|
|
||||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"@types/cacheable-request/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
||||||
|
|
||||||
"@types/keyv/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
||||||
|
|
||||||
"@types/responselike/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
||||||
|
|
||||||
"@types/yauzl/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
||||||
|
|
||||||
"bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
"electron/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"electron/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||||
|
|
||||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
|
|
||||||
2783
electron/dist/main.js
vendored
2783
electron/dist/main.js
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -2,10 +2,16 @@
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cerrno>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <poll.h>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
// Linux / GBM / DRM
|
// Linux / GBM / DRM
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
|
@ -211,6 +217,40 @@ static void release_buf(uint32_t tok) {
|
||||||
pool_return(bo, w, h, stride);
|
pool_return(bo, w, h, stride);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static gbm_bo *create_cpu_writable_texture(uint32_t w, uint32_t h) {
|
||||||
|
// importRgba writes through gbm_bo_map(), so these buffers need a CPU
|
||||||
|
// readable layout and compositor texture use. Requesting RENDERING here is
|
||||||
|
// both unnecessary and rejected by some GBM drivers when combined with
|
||||||
|
// LINEAR (notably proprietary NVIDIA GBM).
|
||||||
|
constexpr uint32_t candidates[] = {
|
||||||
|
GBM_BO_USE_LINEAR | GBM_BO_USE_WRITE,
|
||||||
|
GBM_BO_USE_LINEAR,
|
||||||
|
GBM_BO_USE_WRITE,
|
||||||
|
0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (uint32_t flags : candidates) {
|
||||||
|
if (!gbm_device_is_format_supported(g_gbm, GBM_FORMAT_ARGB8888, flags))
|
||||||
|
continue;
|
||||||
|
gbm_bo *bo = gbm_bo_create(g_gbm, w, h, GBM_FORMAT_ARGB8888, flags);
|
||||||
|
if (bo) return bo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some GBM backends implement modifiers more reliably than the legacy
|
||||||
|
// create call. Keep the requested layout linear because the CPU maps it.
|
||||||
|
const uint64_t linear = DRM_FORMAT_MOD_LINEAR;
|
||||||
|
constexpr uint32_t modifier_flags[] = {
|
||||||
|
GBM_BO_USE_LINEAR | GBM_BO_USE_WRITE,
|
||||||
|
GBM_BO_USE_LINEAR,
|
||||||
|
};
|
||||||
|
for (uint32_t flags : modifier_flags) {
|
||||||
|
gbm_bo *bo = gbm_bo_create_with_modifiers2(
|
||||||
|
g_gbm, w, h, GBM_FORMAT_ARGB8888, &linear, 1, flags);
|
||||||
|
if (bo) return bo;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
Napi::Value Init(const Napi::CallbackInfo &info) {
|
Napi::Value Init(const Napi::CallbackInfo &info) {
|
||||||
Napi::Env env = info.Env();
|
Napi::Env env = info.Env();
|
||||||
std::string dev = "/dev/dri/renderD128";
|
std::string dev = "/dev/dri/renderD128";
|
||||||
|
|
@ -238,6 +278,13 @@ Napi::Value ImportRgba(const Napi::CallbackInfo &info) {
|
||||||
uint32_t h = info[2].As<Napi::Number>().Uint32Value();
|
uint32_t h = info[2].As<Napi::Number>().Uint32Value();
|
||||||
uint32_t src_stride = info[3].As<Napi::Number>().Uint32Value();
|
uint32_t src_stride = info[3].As<Napi::Number>().Uint32Value();
|
||||||
|
|
||||||
|
if (w == 0 || h == 0 || static_cast<uint64_t>(src_stride) < static_cast<uint64_t>(w) * 4U ||
|
||||||
|
pixel_buf.Length() < static_cast<size_t>(src_stride) * h) {
|
||||||
|
Napi::RangeError::New(env, "importRgba requires non-zero dimensions and a complete pixel buffer")
|
||||||
|
.ThrowAsJavaScriptException();
|
||||||
|
return env.Null();
|
||||||
|
}
|
||||||
|
|
||||||
ensure_gbm();
|
ensure_gbm();
|
||||||
if (!g_gbm) {
|
if (!g_gbm) {
|
||||||
Napi::Error::New(env, "GBM device not initialised")
|
Napi::Error::New(env, "GBM device not initialised")
|
||||||
|
|
@ -250,20 +297,20 @@ Napi::Value ImportRgba(const Napi::CallbackInfo &info) {
|
||||||
gbm_bo *bo = pool_acquire(w, h, &dst_stride);
|
gbm_bo *bo = pool_acquire(w, h, &dst_stride);
|
||||||
|
|
||||||
if (!bo) {
|
if (!bo) {
|
||||||
bo = gbm_bo_create_with_modifiers2(g_gbm, w, h, GBM_FORMAT_ARGB8888,
|
bo = create_cpu_writable_texture(w, h);
|
||||||
(const uint64_t[]){DRM_FORMAT_MOD_LINEAR},
|
|
||||||
1,
|
|
||||||
GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
|
|
||||||
if (!bo) {
|
|
||||||
bo = gbm_bo_create(g_gbm, w, h, GBM_FORMAT_ARGB8888,
|
|
||||||
GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
|
|
||||||
}
|
|
||||||
if (!bo) {
|
|
||||||
bo = gbm_bo_create(g_gbm, w, h, GBM_FORMAT_ARGB8888, GBM_BO_USE_WRITE);
|
|
||||||
}
|
|
||||||
if (!bo) {
|
if (!bo) {
|
||||||
|
const char *backend = gbm_device_get_backend_name(g_gbm);
|
||||||
|
const bool format_write = gbm_device_is_format_supported(
|
||||||
|
g_gbm, GBM_FORMAT_ARGB8888, GBM_BO_USE_LINEAR | GBM_BO_USE_WRITE);
|
||||||
|
const bool format_linear = gbm_device_is_format_supported(
|
||||||
|
g_gbm, GBM_FORMAT_ARGB8888, GBM_BO_USE_LINEAR);
|
||||||
Napi::Error::New(
|
Napi::Error::New(
|
||||||
env, "gbm_bo_create failed: all buffer creation methods exhausted")
|
env, "gbm_bo_create failed: all buffer creation methods exhausted for " +
|
||||||
|
std::to_string(w) + "x" + std::to_string(h) +
|
||||||
|
" ARGB8888; backend=" + (backend ? backend : "unknown") +
|
||||||
|
" linear-write-supported=" + (format_write ? "yes" : "no") +
|
||||||
|
" linear-supported=" + (format_linear ? "yes" : "no") +
|
||||||
|
"; check Wo and Electron are using the same DRM render node")
|
||||||
.ThrowAsJavaScriptException();
|
.ThrowAsJavaScriptException();
|
||||||
return env.Null();
|
return env.Null();
|
||||||
}
|
}
|
||||||
|
|
@ -401,6 +448,122 @@ Napi::Value RecvFd(const Napi::CallbackInfo &info) {
|
||||||
return Napi::Number::New(env, -1);
|
return Napi::Number::New(env, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FdReadPacket {
|
||||||
|
std::vector<uint8_t> bytes;
|
||||||
|
std::vector<int> fds;
|
||||||
|
bool closed = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FdReader {
|
||||||
|
int fd;
|
||||||
|
std::atomic<bool> stopping{false};
|
||||||
|
Napi::ThreadSafeFunction callback;
|
||||||
|
std::thread worker;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::mutex g_fd_readers_mutex;
|
||||||
|
static std::map<int, std::shared_ptr<FdReader>> g_fd_readers;
|
||||||
|
|
||||||
|
static void stop_fd_reader(int fd) {
|
||||||
|
std::shared_ptr<FdReader> reader;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_fd_readers_mutex);
|
||||||
|
auto it = g_fd_readers.find(fd);
|
||||||
|
if (it == g_fd_readers.end()) return;
|
||||||
|
reader = it->second;
|
||||||
|
g_fd_readers.erase(it);
|
||||||
|
}
|
||||||
|
reader->stopping.store(true);
|
||||||
|
if (reader->worker.joinable()) reader->worker.join();
|
||||||
|
reader->callback.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
Napi::Value StartFdReader(const Napi::CallbackInfo &info) {
|
||||||
|
Napi::Env env = info.Env();
|
||||||
|
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsFunction()) {
|
||||||
|
Napi::TypeError::New(env, "startFdReader(socketFd, callback)")
|
||||||
|
.ThrowAsJavaScriptException();
|
||||||
|
return env.Undefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
const int fd = info[0].As<Napi::Number>().Int32Value();
|
||||||
|
stop_fd_reader(fd);
|
||||||
|
|
||||||
|
auto reader = std::make_shared<FdReader>();
|
||||||
|
reader->fd = fd;
|
||||||
|
reader->callback = Napi::ThreadSafeFunction::New(
|
||||||
|
env, info[1].As<Napi::Function>(), "wo-fd-reader", 0, 1);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_fd_readers_mutex);
|
||||||
|
g_fd_readers.emplace(fd, reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
reader->worker = std::thread([reader]() {
|
||||||
|
while (!reader->stopping.load()) {
|
||||||
|
pollfd pfd{reader->fd, POLLIN | POLLERR | POLLHUP, 0};
|
||||||
|
const int ready = ::poll(&pfd, 1, 50);
|
||||||
|
if (ready < 0) {
|
||||||
|
if (errno == EINTR) continue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ready == 0) continue;
|
||||||
|
|
||||||
|
uint8_t data[64 * 1024];
|
||||||
|
char control[CMSG_SPACE(sizeof(int) * 16)] = {};
|
||||||
|
iovec iov{data, sizeof(data)};
|
||||||
|
msghdr msg{};
|
||||||
|
msg.msg_iov = &iov;
|
||||||
|
msg.msg_iovlen = 1;
|
||||||
|
msg.msg_control = control;
|
||||||
|
msg.msg_controllen = sizeof(control);
|
||||||
|
const ssize_t count = ::recvmsg(reader->fd, &msg, MSG_DONTWAIT | MSG_CMSG_CLOEXEC);
|
||||||
|
if (count < 0) {
|
||||||
|
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *packet = new FdReadPacket();
|
||||||
|
packet->closed = count == 0;
|
||||||
|
if (count > 0) packet->bytes.assign(data, data + count);
|
||||||
|
for (cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
|
||||||
|
if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) continue;
|
||||||
|
const size_t payloadBytes = cmsg->cmsg_len - CMSG_LEN(0);
|
||||||
|
const size_t fdCount = payloadBytes / sizeof(int);
|
||||||
|
const int *received = reinterpret_cast<const int *>(CMSG_DATA(cmsg));
|
||||||
|
packet->fds.insert(packet->fds.end(), received, received + fdCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const napi_status status = reader->callback.NonBlockingCall(
|
||||||
|
packet, [](Napi::Env callbackEnv, Napi::Function callback, FdReadPacket *data) {
|
||||||
|
Napi::Buffer<uint8_t> bytes = data->bytes.empty()
|
||||||
|
? Napi::Buffer<uint8_t>::New(callbackEnv, 0)
|
||||||
|
: Napi::Buffer<uint8_t>::Copy(
|
||||||
|
callbackEnv, data->bytes.data(), data->bytes.size());
|
||||||
|
Napi::Array fds = Napi::Array::New(callbackEnv, data->fds.size());
|
||||||
|
for (size_t i = 0; i < data->fds.size(); ++i) {
|
||||||
|
fds.Set(i, Napi::Number::New(callbackEnv, data->fds[i]));
|
||||||
|
}
|
||||||
|
callback.Call({bytes, fds, Napi::Boolean::New(callbackEnv, data->closed)});
|
||||||
|
delete data;
|
||||||
|
});
|
||||||
|
if (status != napi_ok) {
|
||||||
|
for (int receivedFd : packet->fds) ::close(receivedFd);
|
||||||
|
delete packet;
|
||||||
|
}
|
||||||
|
if (count == 0) break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return env.Undefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
Napi::Value StopFdReader(const Napi::CallbackInfo &info) {
|
||||||
|
if (info.Length() >= 1 && info[0].IsNumber()) {
|
||||||
|
stop_fd_reader(info[0].As<Napi::Number>().Int32Value());
|
||||||
|
}
|
||||||
|
return info.Env().Undefined();
|
||||||
|
}
|
||||||
|
|
||||||
Napi::Value ImportDmabufTexture(const Napi::CallbackInfo &info) {
|
Napi::Value ImportDmabufTexture(const Napi::CallbackInfo &info) {
|
||||||
Napi::Env env = info.Env();
|
Napi::Env env = info.Env();
|
||||||
|
|
||||||
|
|
@ -565,10 +728,6 @@ Napi::Value MmapFd(const Napi::CallbackInfo &info) {
|
||||||
hint);
|
hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Persistent mmap cache ─────────────────────────────────────────────
|
|
||||||
// CopyMmapToSab and CopyMmapDamageToSab are called on every frame for
|
|
||||||
// the same fd+size. Avoid the mmap/munmap syscall pair per frame by
|
|
||||||
// caching the mapping. Evict when fd or size changes.
|
|
||||||
struct MmapCacheEntry {
|
struct MmapCacheEntry {
|
||||||
int fd;
|
int fd;
|
||||||
void *ptr;
|
void *ptr;
|
||||||
|
|
@ -683,6 +842,8 @@ Napi::Object ModuleInit(Napi::Env env, Napi::Object exports) {
|
||||||
exports.Set("releaseBuffer", Napi::Function::New(env, ReleaseBuffer));
|
exports.Set("releaseBuffer", Napi::Function::New(env, ReleaseBuffer));
|
||||||
exports.Set("sendFd", Napi::Function::New(env, SendFd));
|
exports.Set("sendFd", Napi::Function::New(env, SendFd));
|
||||||
exports.Set("recvFd", Napi::Function::New(env, RecvFd));
|
exports.Set("recvFd", Napi::Function::New(env, RecvFd));
|
||||||
|
exports.Set("startFdReader", Napi::Function::New(env, StartFdReader));
|
||||||
|
exports.Set("stopFdReader", Napi::Function::New(env, StopFdReader));
|
||||||
exports.Set("mmapFd", Napi::Function::New(env, MmapFd));
|
exports.Set("mmapFd", Napi::Function::New(env, MmapFd));
|
||||||
exports.Set("copyMmapToSab", Napi::Function::New(env, CopyMmapToSab));
|
exports.Set("copyMmapToSab", Napi::Function::New(env, CopyMmapToSab));
|
||||||
exports.Set("copyMmapDamageToSab", Napi::Function::New(env, CopyMmapDamageToSab));
|
exports.Set("copyMmapDamageToSab", Napi::Function::New(env, CopyMmapDamageToSab));
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron dist/main.js",
|
"start": "electron dist/main.js",
|
||||||
"dev": "bun src/main.ts",
|
"dev": "bun src/main.ts",
|
||||||
"build": "bun build src/main.ts src/preload.ts --outdir dist --target node --format esm --external electron",
|
"test": "bun test",
|
||||||
"build-native": "node-gyp rebuild --directory native",
|
"build": "bun build src/main.ts --outdir dist --target node --format esm --external electron && bun build src/preload.ts --outfile dist/preload.js --target node --format cjs --external electron",
|
||||||
|
"build-native": "node-gyp rebuild --directory native --jobs=1",
|
||||||
"postinstall": "bun run build && bun run build-native"
|
"postinstall": "bun run build && bun run build-native"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -17,9 +18,8 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/electron": "^1.6.12",
|
|
||||||
"@types/node": "^25.3.5",
|
"@types/node": "^25.3.5",
|
||||||
"electron": "^29.4.6",
|
"electron": "^45.0.0",
|
||||||
"node-gyp": "^10.3.1",
|
"node-gyp": "^10.3.1",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
149
electron/src/damage-buffer.test.ts
Normal file
149
electron/src/damage-buffer.test.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
import { closeSync, mkdtempSync, openSync, readFileSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import {
|
||||||
|
applyDamagePayload,
|
||||||
|
DamageBuffer,
|
||||||
|
DamageRegionTracker,
|
||||||
|
WoDmabufSender,
|
||||||
|
} from './damage-buffer.js';
|
||||||
|
import { MAGIC } from './protocol.js';
|
||||||
|
import { createRendererDamageHelper } from './renderer-damage-helper.js';
|
||||||
|
|
||||||
|
describe('damage buffer', () => {
|
||||||
|
test('copies padded full-frame rows into the destination stride', () => {
|
||||||
|
const buffer = new DamageBuffer(2, 2);
|
||||||
|
const source = Uint8Array.from([
|
||||||
|
1, 2, 3, 4, 5, 6, 7, 8, 90, 91, 92, 93,
|
||||||
|
9, 10, 11, 12, 13, 14, 15, 16, 94, 95, 96, 97,
|
||||||
|
]);
|
||||||
|
|
||||||
|
buffer.applyFullFrame(source, 12);
|
||||||
|
|
||||||
|
expect([...buffer.getFrameBuffer()]).toEqual([
|
||||||
|
1, 2, 3, 4, 5, 6, 7, 8,
|
||||||
|
9, 10, 11, 12, 13, 14, 15, 16,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clips patches without shifting source pixels', () => {
|
||||||
|
const buffer = new DamageBuffer(2, 2);
|
||||||
|
buffer.applyFullFrame(new Uint8Array(16));
|
||||||
|
|
||||||
|
buffer.applyPatch({
|
||||||
|
rect: { x: -1, y: -1, width: 3, height: 3 },
|
||||||
|
stride: 12,
|
||||||
|
rgba: Uint8Array.from([
|
||||||
|
1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3,
|
||||||
|
4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6,
|
||||||
|
7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect([...buffer.getFrameBuffer()]).toEqual([
|
||||||
|
5, 5, 5, 5, 6, 6, 6, 6,
|
||||||
|
8, 8, 8, 8, 9, 9, 9, 9,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tracks touching damage and ignores empty rectangles', () => {
|
||||||
|
const tracker = new DamageRegionTracker();
|
||||||
|
tracker.addAll([
|
||||||
|
{ x: 0, y: 0, width: 2, height: 2 },
|
||||||
|
{ x: 2, y: 0, width: 1, height: 2 },
|
||||||
|
{ x: 8, y: 8, width: 0, height: 1 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(tracker.take()).toEqual([{ x: 0, y: 0, width: 3, height: 2 }]);
|
||||||
|
expect(tracker.take()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renderer damage helper emits patches that reconstruct the updated frame', async () => {
|
||||||
|
const payloads: Parameters<typeof applyDamagePayload>[1][] = [];
|
||||||
|
const helper = createRendererDamageHelper(async (payload) => {
|
||||||
|
payloads.push(payload);
|
||||||
|
return { ok: true };
|
||||||
|
}, { width: 3, height: 2, fullFrameThreshold: 1 });
|
||||||
|
|
||||||
|
helper.updatePatch(
|
||||||
|
{ x: -1, y: 0, width: 3, height: 1 },
|
||||||
|
Uint8Array.from([1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3]),
|
||||||
|
);
|
||||||
|
await helper.flush();
|
||||||
|
|
||||||
|
const rendered = new DamageBuffer(3, 2);
|
||||||
|
const payload = payloads[0];
|
||||||
|
expect(payload?.patches).toEqual([{
|
||||||
|
rect: { x: 0, y: 0, width: 2, height: 1 },
|
||||||
|
rgba: Uint8Array.from([2, 2, 2, 2, 3, 3, 3, 3]),
|
||||||
|
stride: 8,
|
||||||
|
}]);
|
||||||
|
if (payload) applyDamagePayload(rendered, payload);
|
||||||
|
expect([...rendered.getFrameBuffer()]).toEqual([
|
||||||
|
2, 2, 2, 2, 3, 3, 3, 3,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shared DMA-BUF frame forwarding', () => {
|
||||||
|
test('holds a GBM buffer until the compositor acknowledges its frame', () => {
|
||||||
|
const directory = mkdtempSync(join(tmpdir(), 'wo-buffer-'));
|
||||||
|
const file = join(directory, 'frame');
|
||||||
|
const socketFd = openSync(file, 'w+');
|
||||||
|
const released: number[] = [];
|
||||||
|
try {
|
||||||
|
const socket = { _handle: { fd: socketFd } } as never;
|
||||||
|
const native = {
|
||||||
|
importRgba: () => ({ fd: 31, offset: 0, stride: 8, modifier_hi: 0, modifier_lo: 0, token: 7 }),
|
||||||
|
sendFd: () => {},
|
||||||
|
releaseBuffer: (token: number) => released.push(token),
|
||||||
|
} as never;
|
||||||
|
const sender = new WoDmabufSender(socket, 'web', native);
|
||||||
|
const seq = sender.send(new DamageBuffer(2, 2));
|
||||||
|
expect(released).toEqual([]);
|
||||||
|
sender.acknowledge(seq);
|
||||||
|
expect(released).toEqual([7]);
|
||||||
|
} finally {
|
||||||
|
closeSync(socketFd);
|
||||||
|
rmSync(directory, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writes plane metadata and forwards original file descriptors', async () => {
|
||||||
|
const directory = mkdtempSync(join(tmpdir(), 'wo-frame-'));
|
||||||
|
const file = join(directory, 'frame');
|
||||||
|
const socketFd = openSync(file, 'w+');
|
||||||
|
const sentFds: number[] = [];
|
||||||
|
try {
|
||||||
|
const socket = { _handle: { fd: socketFd } } as never;
|
||||||
|
const sender = new WoDmabufSender(socket, 'web', { sendFd: (_fd: number, fd: number) => sentFds.push(fd) } as never);
|
||||||
|
let released = 0;
|
||||||
|
const seq = sender.sendSharedTexture({
|
||||||
|
pixelFormat: 'bgra',
|
||||||
|
codedSize: { width: 2, height: 3 },
|
||||||
|
handle: { nativePixmap: { modifier: '0', planes: [
|
||||||
|
{ fd: 21, stride: 8, offset: 0 },
|
||||||
|
{ fd: 22, stride: 8, offset: 24 },
|
||||||
|
] } },
|
||||||
|
}, () => { released += 1; });
|
||||||
|
|
||||||
|
const bytes = readFileSync(file);
|
||||||
|
const planeCountOffset = 8 + Buffer.byteLength('web') + 8 + 4 + 4 + 4;
|
||||||
|
expect(bytes.readUInt32LE(0)).toBe(MAGIC.FRAME);
|
||||||
|
expect(bytes.readUInt32LE(planeCountOffset)).toBe(2);
|
||||||
|
expect(bytes.readUInt32LE(planeCountOffset + 4)).toBe(0);
|
||||||
|
expect(bytes.readUInt32LE(planeCountOffset + 8)).toBe(8);
|
||||||
|
expect(bytes.readUInt32LE(planeCountOffset + 20)).toBe(24);
|
||||||
|
expect(sentFds).toEqual([21, 22]);
|
||||||
|
expect(released).toBe(0);
|
||||||
|
sender.acknowledge(seq);
|
||||||
|
expect(released).toBe(1);
|
||||||
|
} finally {
|
||||||
|
closeSync(socketFd);
|
||||||
|
rmSync(directory, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -59,6 +59,8 @@ type NativeDmabufModule = {
|
||||||
releaseBuffer: (token: number) => void;
|
releaseBuffer: (token: number) => void;
|
||||||
sendFd: (socketFd: number, dmabufFd: number) => void;
|
sendFd: (socketFd: number, dmabufFd: number) => void;
|
||||||
recvFd: (socketFd: number) => number;
|
recvFd: (socketFd: number) => number;
|
||||||
|
startFdReader: (socketFd: number, callback: (chunk: Buffer, fds: number[], closed: boolean) => void) => void;
|
||||||
|
stopFdReader: (socketFd: number) => void;
|
||||||
mmapFd: (fd: number, size: number) => Buffer;
|
mmapFd: (fd: number, size: number) => Buffer;
|
||||||
copyMmapToSab: (fd: number, sab: SharedArrayBuffer, size: number, srcOff?: number, dstOff?: number) => void;
|
copyMmapToSab: (fd: number, sab: SharedArrayBuffer, size: number, srcOff?: number, dstOff?: number) => void;
|
||||||
copyMmapDamageToSab: (fd: number, sab: SharedArrayBuffer, stride: number, rects: Array<{y: number; h: number}>) => void;
|
copyMmapDamageToSab: (fd: number, sab: SharedArrayBuffer, stride: number, rects: Array<{y: number; h: number}>) => void;
|
||||||
|
|
@ -67,6 +69,24 @@ type NativeDmabufModule = {
|
||||||
releaseTexture: (windowName: string) => void;
|
releaseTexture: (windowName: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface OffscreenNativePixmapPlane {
|
||||||
|
fd: number;
|
||||||
|
stride: number;
|
||||||
|
offset: number;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OffscreenSharedTextureInfo {
|
||||||
|
pixelFormat: 'bgra' | 'rgba' | 'rgbaf16' | 'nv12';
|
||||||
|
codedSize: { width: number; height: number };
|
||||||
|
handle: {
|
||||||
|
nativePixmap: {
|
||||||
|
planes: OffscreenNativePixmapPlane[];
|
||||||
|
modifier: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const ARGB8888 = fourccCode('A', 'R', '2', '4');
|
const ARGB8888 = fourccCode('A', 'R', '2', '4');
|
||||||
|
|
||||||
export class DamageBuffer {
|
export class DamageBuffer {
|
||||||
|
|
@ -107,7 +127,6 @@ export class DamageBuffer {
|
||||||
|
|
||||||
applyFullFrame(frame: Uint8Array, frameStride: number = this.width * 4): void {
|
applyFullFrame(frame: Uint8Array, frameStride: number = this.width * 4): void {
|
||||||
if (frameStride === this.stride) {
|
if (frameStride === this.stride) {
|
||||||
// Fast path: direct typed-array copy, no intermediate Buffer wrapper
|
|
||||||
this.frame.set(new Uint8Array(frame.buffer, frame.byteOffset, this.frame.length));
|
this.frame.set(new Uint8Array(frame.buffer, frame.byteOffset, this.frame.length));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -129,12 +148,14 @@ export class DamageBuffer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const patchStride = patch.stride ?? rect.width * 4;
|
const patchStride = patch.stride ?? patch.rect.width * 4;
|
||||||
|
const sourceX = rect.x - patch.rect.x;
|
||||||
|
const sourceY = rect.y - patch.rect.y;
|
||||||
const dstStartX = rect.x * 4;
|
const dstStartX = rect.x * 4;
|
||||||
const rowBytes = rect.width * 4;
|
const rowBytes = rect.width * 4;
|
||||||
|
|
||||||
for (let row = 0; row < rect.height; row += 1) {
|
for (let row = 0; row < rect.height; row += 1) {
|
||||||
const srcStart = row * patchStride;
|
const srcStart = (sourceY + row) * patchStride + sourceX * 4;
|
||||||
const dstStart = (rect.y + row) * this.stride + dstStartX;
|
const dstStart = (rect.y + row) * this.stride + dstStartX;
|
||||||
this.frame.set(
|
this.frame.set(
|
||||||
new Uint8Array(patch.rgba.buffer, patch.rgba.byteOffset + srcStart, rowBytes),
|
new Uint8Array(patch.rgba.buffer, patch.rgba.byteOffset + srcStart, rowBytes),
|
||||||
|
|
@ -152,29 +173,28 @@ export class DamageBuffer {
|
||||||
|
|
||||||
export class WoDmabufSender {
|
export class WoDmabufSender {
|
||||||
private readonly socket: Socket;
|
private readonly socket: Socket;
|
||||||
private readonly windowName: string;
|
|
||||||
private readonly native: NativeDmabufModule;
|
private readonly native: NativeDmabufModule;
|
||||||
private seq = 1n;
|
private seq = 1n;
|
||||||
// Pre-allocated wire buffer: header (variable) + plane (16 bytes).
|
|
||||||
// Layout: MAGIC(4) + nameLen(4) + name(N) + seq(8) + w(4) + h(4) + fmt(4) + planes(4) + plane(16)
|
|
||||||
private readonly wireBuf: Buffer;
|
private readonly wireBuf: Buffer;
|
||||||
|
private readonly sharedWireBuf: Buffer;
|
||||||
|
private readonly nameBuf: Buffer;
|
||||||
private readonly nameLen: number;
|
private readonly nameLen: number;
|
||||||
private readonly seqOffset: number;
|
private readonly seqOffset: number;
|
||||||
private readonly widthOffset: number;
|
private readonly widthOffset: number;
|
||||||
private readonly heightOffset: number;
|
private readonly heightOffset: number;
|
||||||
|
private readonly pendingReleases = new Map<string, () => void>();
|
||||||
|
|
||||||
constructor(socket: Socket, windowName: string, native: NativeDmabufModule) {
|
constructor(socket: Socket, windowName: string, native: NativeDmabufModule) {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.windowName = windowName;
|
|
||||||
this.native = native;
|
this.native = native;
|
||||||
|
|
||||||
const nameBuf = Buffer.from(windowName, 'utf8');
|
const nameBuf = Buffer.from(windowName, 'utf8');
|
||||||
|
this.nameBuf = nameBuf;
|
||||||
this.nameLen = nameBuf.length;
|
this.nameLen = nameBuf.length;
|
||||||
// header = 4+4+nameLen+8+4+4+4+4 = 32+nameLen, plus 16 bytes for plane
|
|
||||||
const totalLen = 32 + this.nameLen + 16;
|
const totalLen = 32 + this.nameLen + 16;
|
||||||
this.wireBuf = Buffer.alloc(totalLen);
|
this.wireBuf = Buffer.alloc(totalLen);
|
||||||
|
this.sharedWireBuf = Buffer.alloc(32 + this.nameLen + 4 * 16);
|
||||||
|
|
||||||
// Static fields that never change
|
|
||||||
let off = 0;
|
let off = 0;
|
||||||
this.wireBuf.writeUInt32LE(MAGIC.FRAME, off); off += 4;
|
this.wireBuf.writeUInt32LE(MAGIC.FRAME, off); off += 4;
|
||||||
this.wireBuf.writeUInt32LE(this.nameLen, off); off += 4;
|
this.wireBuf.writeUInt32LE(this.nameLen, off); off += 4;
|
||||||
|
|
@ -183,10 +203,16 @@ export class WoDmabufSender {
|
||||||
this.widthOffset = off; off += 4;
|
this.widthOffset = off; off += 4;
|
||||||
this.heightOffset = off; off += 4;
|
this.heightOffset = off; off += 4;
|
||||||
this.wireBuf.writeUInt32LE(ARGB8888, off); off += 4;
|
this.wireBuf.writeUInt32LE(ARGB8888, off); off += 4;
|
||||||
this.wireBuf.writeUInt32LE(1, off); // planes count = 1
|
this.wireBuf.writeUInt32LE(1, off);
|
||||||
}
|
}
|
||||||
|
|
||||||
send(buffer: DamageBuffer): bigint {
|
send(buffer: DamageBuffer): bigint {
|
||||||
|
if (buffer.getWidth() <= 0 || buffer.getHeight() <= 0
|
||||||
|
|| buffer.getStride() < buffer.getWidth() * 4
|
||||||
|
|| buffer.getFrameBuffer().byteLength < buffer.getStride() * buffer.getHeight()) {
|
||||||
|
throw new RangeError('Cannot send an incomplete or zero-sized DMA-BUF frame');
|
||||||
|
}
|
||||||
|
|
||||||
const imported = this.native.importRgba(
|
const imported = this.native.importRgba(
|
||||||
buffer.getFrameBuffer(),
|
buffer.getFrameBuffer(),
|
||||||
buffer.getWidth(),
|
buffer.getWidth(),
|
||||||
|
|
@ -194,15 +220,15 @@ export class WoDmabufSender {
|
||||||
buffer.getStride(),
|
buffer.getStride(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let sent = false;
|
||||||
|
const sentSeq = this.seq;
|
||||||
try {
|
try {
|
||||||
const socketFd = getSocketFd(this.socket);
|
const socketFd = getSocketFd(this.socket);
|
||||||
|
|
||||||
// Write per-frame fields into the pre-allocated buffer
|
|
||||||
this.wireBuf.writeBigUInt64LE(this.seq, this.seqOffset);
|
this.wireBuf.writeBigUInt64LE(this.seq, this.seqOffset);
|
||||||
this.wireBuf.writeUInt32LE(buffer.getWidth(), this.widthOffset);
|
this.wireBuf.writeUInt32LE(buffer.getWidth(), this.widthOffset);
|
||||||
this.wireBuf.writeUInt32LE(buffer.getHeight(), this.heightOffset);
|
this.wireBuf.writeUInt32LE(buffer.getHeight(), this.heightOffset);
|
||||||
|
|
||||||
// Plane data (last 16 bytes of wireBuf)
|
|
||||||
const planeOff = this.wireBuf.length - 16;
|
const planeOff = this.wireBuf.length - 16;
|
||||||
this.wireBuf.writeUInt32LE(imported.offset, planeOff);
|
this.wireBuf.writeUInt32LE(imported.offset, planeOff);
|
||||||
this.wireBuf.writeUInt32LE(imported.stride, planeOff + 4);
|
this.wireBuf.writeUInt32LE(imported.stride, planeOff + 4);
|
||||||
|
|
@ -212,13 +238,91 @@ export class WoDmabufSender {
|
||||||
writeSync(socketFd, this.wireBuf, 0, this.wireBuf.length);
|
writeSync(socketFd, this.wireBuf, 0, this.wireBuf.length);
|
||||||
this.native.sendFd(socketFd, imported.fd);
|
this.native.sendFd(socketFd, imported.fd);
|
||||||
|
|
||||||
const sentSeq = this.seq;
|
this.pendingReleases.set(sentSeq.toString(), () => this.native.releaseBuffer(imported.token));
|
||||||
this.seq += 1n;
|
this.seq += 1n;
|
||||||
|
sent = true;
|
||||||
return sentSeq;
|
return sentSeq;
|
||||||
} finally {
|
} finally {
|
||||||
this.native.releaseBuffer(imported.token);
|
if (!sent) this.native.releaseBuffer(imported.token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendSharedTexture(texture: OffscreenSharedTextureInfo, release: () => void): bigint {
|
||||||
|
const pixmap = texture.handle?.nativePixmap;
|
||||||
|
const planes = pixmap?.planes;
|
||||||
|
if (!planes || planes.length < 1 || planes.length > 4) {
|
||||||
|
throw new Error('shared texture has no supported DMA-BUF planes');
|
||||||
|
}
|
||||||
|
const fourcc = texture.pixelFormat === 'bgra'
|
||||||
|
? fourccCode('A', 'R', '2', '4')
|
||||||
|
: texture.pixelFormat === 'rgba'
|
||||||
|
? fourccCode('A', 'B', '2', '4')
|
||||||
|
: texture.pixelFormat === 'nv12'
|
||||||
|
? fourccCode('N', 'V', '1', '2')
|
||||||
|
: 0;
|
||||||
|
if (fourcc === 0) throw new Error(`unsupported shared texture pixel format: ${texture.pixelFormat}`);
|
||||||
|
|
||||||
|
const width = texture.codedSize.width;
|
||||||
|
const height = texture.codedSize.height;
|
||||||
|
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
|
||||||
|
throw new Error('shared texture has invalid coded dimensions');
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerLength = 32 + this.nameLen + planes.length * 16;
|
||||||
|
let off = 0;
|
||||||
|
this.sharedWireBuf.writeUInt32LE(MAGIC.FRAME, off); off += 4;
|
||||||
|
this.sharedWireBuf.writeUInt32LE(this.nameLen, off); off += 4;
|
||||||
|
this.nameBuf.copy(this.sharedWireBuf, off); off += this.nameLen;
|
||||||
|
this.sharedWireBuf.writeBigUInt64LE(this.seq, off); off += 8;
|
||||||
|
this.sharedWireBuf.writeUInt32LE(width, off); off += 4;
|
||||||
|
this.sharedWireBuf.writeUInt32LE(height, off); off += 4;
|
||||||
|
this.sharedWireBuf.writeUInt32LE(fourcc, off); off += 4;
|
||||||
|
this.sharedWireBuf.writeUInt32LE(planes.length, off); off += 4;
|
||||||
|
const modifier = BigInt(pixmap.modifier || '0');
|
||||||
|
for (const plane of planes) {
|
||||||
|
this.sharedWireBuf.writeUInt32LE(plane.offset >>> 0, off); off += 4;
|
||||||
|
this.sharedWireBuf.writeUInt32LE(plane.stride >>> 0, off); off += 4;
|
||||||
|
this.sharedWireBuf.writeUInt32LE(Number((modifier >> 32n) & 0xffff_ffffn), off); off += 4;
|
||||||
|
this.sharedWireBuf.writeUInt32LE(Number(modifier & 0xffff_ffffn), off); off += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const socketFd = getSocketFd(this.socket);
|
||||||
|
for (const plane of planes) {
|
||||||
|
if (!Number.isInteger(plane.fd) || plane.fd < 0 || !Number.isInteger(plane.stride) || plane.stride <= 0 || !Number.isInteger(plane.offset) || plane.offset < 0) {
|
||||||
|
throw new Error('shared texture has invalid DMA-BUF plane metadata');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeAllSync(socketFd, this.sharedWireBuf, headerLength);
|
||||||
|
for (const plane of planes) this.native.sendFd(socketFd, plane.fd);
|
||||||
|
|
||||||
|
const sentSeq = this.seq;
|
||||||
|
this.pendingReleases.set(sentSeq.toString(), release);
|
||||||
|
this.seq += 1n;
|
||||||
|
return sentSeq;
|
||||||
|
}
|
||||||
|
|
||||||
|
acknowledge(seq: string | bigint): void {
|
||||||
|
const key = seq.toString();
|
||||||
|
const release = this.pendingReleases.get(key);
|
||||||
|
if (!release) return;
|
||||||
|
this.pendingReleases.delete(key);
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseAll(): void {
|
||||||
|
for (const release of this.pendingReleases.values()) release();
|
||||||
|
this.pendingReleases.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeAllSync(fd: number, buffer: Buffer, length: number): void {
|
||||||
|
let offset = 0;
|
||||||
|
while (offset < length) {
|
||||||
|
const written = writeSync(fd, buffer, offset, length - offset);
|
||||||
|
if (written <= 0) throw new Error('IPC socket closed while sending a DMA-BUF frame');
|
||||||
|
offset += written;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyDamagePayload(buffer: DamageBuffer, payload: DamageFramePayload): void {
|
export function applyDamagePayload(buffer: DamageBuffer, payload: DamageFramePayload): void {
|
||||||
|
|
@ -257,13 +361,12 @@ function clampRect(rect: DamageRect, width: number, height: number): DamageRect
|
||||||
}
|
}
|
||||||
|
|
||||||
function coalesceDamageRects(input: DamageRect[], maxRects: number): DamageRect[] {
|
function coalesceDamageRects(input: DamageRect[], maxRects: number): DamageRect[] {
|
||||||
if (input.length <= 1) {
|
|
||||||
return input.slice();
|
|
||||||
}
|
|
||||||
|
|
||||||
const rects = input
|
const rects = input
|
||||||
.map((r) => ({ ...r }))
|
.map((r) => ({ ...r }))
|
||||||
.filter((r) => r.width > 0 && r.height > 0);
|
.filter((r) => r.width > 0 && r.height > 0);
|
||||||
|
if (rects.length <= 1) {
|
||||||
|
return rects;
|
||||||
|
}
|
||||||
|
|
||||||
const out: DamageRect[] = [];
|
const out: DamageRect[] = [];
|
||||||
while (rects.length > 0) {
|
while (rects.length > 0) {
|
||||||
|
|
|
||||||
23
electron/src/frame-pacing.test.ts
Normal file
23
electron/src/frame-pacing.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
import { chooseFrameRate, frameIntervalMs } from './frame-pacing';
|
||||||
|
|
||||||
|
describe('per-display frame pacing', () => {
|
||||||
|
test('follows the display refresh when no FPS override is configured', () => {
|
||||||
|
expect(chooseFrameRate(0, 144)).toBe(144);
|
||||||
|
expect(chooseFrameRate(undefined, 360)).toBe(360);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('caps a per-window override at its display refresh', () => {
|
||||||
|
expect(chooseFrameRate(240, 144)).toBe(144);
|
||||||
|
expect(chooseFrameRate(120, 360)).toBe(120);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not impose a global FPS ceiling above high-refresh displays', () => {
|
||||||
|
expect(chooseFrameRate(1000, 360)).toBe(360);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to 60 Hz when display information is unavailable', () => {
|
||||||
|
expect(chooseFrameRate(0, 0)).toBe(60);
|
||||||
|
expect(frameIntervalMs(144)).toBeCloseTo(1000 / 144);
|
||||||
|
});
|
||||||
|
});
|
||||||
13
electron/src/frame-pacing.ts
Normal file
13
electron/src/frame-pacing.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
export function chooseFrameRate(configuredFps: number | undefined, displayFps: number | undefined): number {
|
||||||
|
const display = Number.isFinite(displayFps) && (displayFps ?? 0) > 0
|
||||||
|
? Math.round(displayFps!)
|
||||||
|
: 60;
|
||||||
|
const configured = Number.isFinite(configuredFps) && (configuredFps ?? 0) > 0
|
||||||
|
? Math.round(configuredFps!)
|
||||||
|
: display;
|
||||||
|
return Math.max(1, Math.min(configured, display));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function frameIntervalMs(fps: number): number {
|
||||||
|
return 1000 / Math.max(1, fps);
|
||||||
|
}
|
||||||
1844
electron/src/main.ts
1844
electron/src/main.ts
File diff suppressed because it is too large
Load diff
71
electron/src/native-fd-reader.test.ts
Normal file
71
electron/src/native-fd-reader.test.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
import { closeSync, existsSync, fstatSync, mkdtempSync, openSync, rmSync, writeSync } from 'fs';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { createConnection, createServer, type Socket } from 'net';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const nativePath = fileURLToPath(new URL('../native/build/Release/wo_dmabuf.node', import.meta.url));
|
||||||
|
|
||||||
|
describe('native SCM_RIGHTS receiver', () => {
|
||||||
|
if (!existsSync(nativePath) || process.env.WO_RUN_NATIVE_SOCKET_TESTS !== '1') {
|
||||||
|
test.skip('reads socket bytes and ancillary file descriptors with recvmsg (set WO_RUN_NATIVE_SOCKET_TESTS=1)', () => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('preserves SCM_RIGHTS descriptors alongside the Unix stream bytes', async () => {
|
||||||
|
const native = require(nativePath) as {
|
||||||
|
startFdReader: (fd: number, callback: (bytes: Buffer, fds: number[], closed: boolean) => void) => void;
|
||||||
|
stopFdReader: (fd: number) => void;
|
||||||
|
sendFd: (socketFd: number, fd: number) => void;
|
||||||
|
};
|
||||||
|
const directory = mkdtempSync(join(tmpdir(), 'wo-fd-reader-'));
|
||||||
|
const socketPath = join(directory, 'ipc.sock');
|
||||||
|
const filePath = join(directory, 'source');
|
||||||
|
const sourceFd = openSync(filePath, 'w+');
|
||||||
|
writeSync(sourceFd, Buffer.from('dma-buf-fd'));
|
||||||
|
|
||||||
|
const server = createServer();
|
||||||
|
const acceptedPromise = new Promise<Socket>((resolve) => server.once('connection', resolve));
|
||||||
|
let client: Socket | null = null;
|
||||||
|
let accepted: Socket | null = null;
|
||||||
|
let readerFd: number | null = null;
|
||||||
|
try {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.once('error', reject);
|
||||||
|
server.listen(socketPath, resolve);
|
||||||
|
});
|
||||||
|
client = createConnection(socketPath);
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
client!.once('connect', resolve);
|
||||||
|
client!.once('error', reject);
|
||||||
|
});
|
||||||
|
accepted = await acceptedPromise;
|
||||||
|
accepted.pause();
|
||||||
|
readerFd = (accepted as any)._handle.fd as number;
|
||||||
|
const clientFd = (client as any)._handle.fd as number;
|
||||||
|
|
||||||
|
const packet = new Promise<{ bytes: Buffer; fd: number }>((resolve, reject) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
native.startFdReader(readerFd!, (bytes, fds, closed) => {
|
||||||
|
chunks.push(bytes);
|
||||||
|
if (fds.length > 0) resolve({ bytes: Buffer.concat(chunks), fd: fds[0]! });
|
||||||
|
else if (closed) reject(new Error('socket closed before the descriptor arrived'));
|
||||||
|
});
|
||||||
|
client!.write(Buffer.from([0x57, 0x4f, 0x46, 0x44]));
|
||||||
|
native.sendFd(clientFd, sourceFd);
|
||||||
|
});
|
||||||
|
const received = await packet;
|
||||||
|
expect(received.bytes.includes(Buffer.from([0x57, 0x4f, 0x46, 0x44]))).toBe(true);
|
||||||
|
expect(fstatSync(received.fd).size).toBe(Buffer.byteLength('dma-buf-fd'));
|
||||||
|
closeSync(received.fd);
|
||||||
|
} finally {
|
||||||
|
if (readerFd !== null) native.stopFdReader(readerFd);
|
||||||
|
accepted?.destroy();
|
||||||
|
client?.destroy();
|
||||||
|
server.close();
|
||||||
|
closeSync(sourceFd);
|
||||||
|
rmSync(directory, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -6,15 +6,54 @@ import { contextBridge, ipcRenderer } from 'electron';
|
||||||
import type {
|
import type {
|
||||||
CompositorAPI,
|
CompositorAPI,
|
||||||
WoClientAPI,
|
WoClientAPI,
|
||||||
|
WoPortals,
|
||||||
WindowMetadataEvent,
|
WindowMetadataEvent,
|
||||||
FocusChangeEvent,
|
FocusChangeEvent,
|
||||||
WoWindow,
|
WoWindow,
|
||||||
DamageFramePayload,
|
DamageFramePayload,
|
||||||
RendererDamageHelper,
|
RendererDamageHelper,
|
||||||
RendererDamageHelperOptions,
|
RendererDamageHelperOptions,
|
||||||
|
PortalRequestEvent,
|
||||||
|
DBusBus,
|
||||||
|
DBusCallOptions,
|
||||||
|
DBusPropertyOptions,
|
||||||
|
DBusInterfaceOptions,
|
||||||
|
ApplicationIcon,
|
||||||
|
ApplicationInfo,
|
||||||
|
SyscallDbusGetAllPropertiesResult,
|
||||||
|
SyscallDbusListNamesResult,
|
||||||
|
SyscallLaunchResult,
|
||||||
|
SyscallXdgDirectoriesResult,
|
||||||
|
WoAudio,
|
||||||
|
WoAudioSource,
|
||||||
|
WoAudioChunk,
|
||||||
|
WoCapability,
|
||||||
|
WoPermissionState,
|
||||||
|
WoPerformanceStatus,
|
||||||
|
WoMonitor,
|
||||||
} from 'wo-types';
|
} from 'wo-types';
|
||||||
import { createRendererDamageHelper } from './renderer-damage-helper.js';
|
import { createRendererDamageHelper } from './renderer-damage-helper.js';
|
||||||
|
|
||||||
|
// The resource approval document is an internal data URL. Expose only the
|
||||||
|
// one-shot response bridge there; the main process validates its request ID
|
||||||
|
// and token before it accepts a decision.
|
||||||
|
try {
|
||||||
|
const consentUrl = new URL(globalThis.location.href);
|
||||||
|
if (consentUrl.protocol === 'data:' && consentUrl.hash === '#wo-resource-consent') {
|
||||||
|
contextBridge.exposeInMainWorld('woResourceConsent', {
|
||||||
|
respond: (requestId: string, token: string, decision: 'deny' | 'allow' | 'allow-all') =>
|
||||||
|
ipcRenderer.invoke('wo:resource-consent-respond', requestId, token, decision),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// The bridge is absent from ordinary pages and unavailable if the URL is malformed.
|
||||||
|
}
|
||||||
|
|
||||||
|
let monitorCache: WoMonitor[] = [];
|
||||||
|
ipcRenderer.on('wo:monitors', (_event, monitors: WoMonitor[]) => {
|
||||||
|
monitorCache = Array.isArray(monitors) ? monitors : [];
|
||||||
|
});
|
||||||
|
|
||||||
const compositorAPI: CompositorAPI = {
|
const compositorAPI: CompositorAPI = {
|
||||||
action: async (name: string, payload?: Record<string, unknown>) => {
|
action: async (name: string, payload?: Record<string, unknown>) => {
|
||||||
return await ipcRenderer.invoke('action', name, payload ?? {});
|
return await ipcRenderer.invoke('action', name, payload ?? {});
|
||||||
|
|
@ -26,7 +65,6 @@ const compositorAPI: CompositorAPI = {
|
||||||
reload: () => ipcRenderer.invoke('action', 'reload', {}),
|
reload: () => ipcRenderer.invoke('action', 'reload', {}),
|
||||||
toggleDevTools: () => ipcRenderer.invoke('action', 'devtools', {}),
|
toggleDevTools: () => ipcRenderer.invoke('action', 'devtools', {}),
|
||||||
|
|
||||||
// New: Focus change notifications
|
|
||||||
onFocusChange: (callback: (window: string, focused: boolean) => void) => {
|
onFocusChange: (callback: (window: string, focused: boolean) => void) => {
|
||||||
const handler = (_event: Electron.IpcRendererEvent, data: FocusChangeEvent) =>
|
const handler = (_event: Electron.IpcRendererEvent, data: FocusChangeEvent) =>
|
||||||
callback(data.window, data.focused);
|
callback(data.window, data.focused);
|
||||||
|
|
@ -34,24 +72,75 @@ const compositorAPI: CompositorAPI = {
|
||||||
return () => ipcRenderer.removeListener('wo:focus-change', handler);
|
return () => ipcRenderer.removeListener('wo:focus-change', handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
// New: Window metadata
|
|
||||||
onWindowMetadata: (callback: (metadata: WindowMetadataEvent) => void) => {
|
onWindowMetadata: (callback: (metadata: WindowMetadataEvent) => void) => {
|
||||||
const handler = (_event: Electron.IpcRendererEvent, metadata: WindowMetadataEvent) => callback(metadata);
|
const handler = (_event: Electron.IpcRendererEvent, metadata: WindowMetadataEvent) => callback(metadata);
|
||||||
ipcRenderer.on('wo:window-metadata', handler);
|
ipcRenderer.on('wo:window-metadata', handler);
|
||||||
return () => ipcRenderer.removeListener('wo:window-metadata', handler);
|
return () => ipcRenderer.removeListener('wo:window-metadata', handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
// New: Syscall API
|
onMonitors: (callback: (monitors: WoMonitor[]) => void) => {
|
||||||
syscall: async (type: string, params: Record<string, unknown>) => {
|
const handler = (_event: Electron.IpcRendererEvent, monitors: WoMonitor[]) => callback(monitors);
|
||||||
|
ipcRenderer.on('wo:monitors', handler);
|
||||||
|
if (monitorCache.length) callback(monitorCache);
|
||||||
|
return () => ipcRenderer.removeListener('wo:monitors', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
syscall: async (type: string, params?: any) => {
|
||||||
return await ipcRenderer.invoke('wo:syscall', type, params);
|
return await ipcRenderer.invoke('wo:syscall', type, params);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
dbus: {
|
||||||
|
call: (options: DBusCallOptions) => ipcRenderer.invoke('wo:syscall', 'dbus_call', options),
|
||||||
|
getProperty: (options: DBusPropertyOptions) => ipcRenderer.invoke('wo:syscall', 'dbus_get_property', options),
|
||||||
|
getAllProperties: (options: DBusInterfaceOptions) => ipcRenderer.invoke('wo:syscall', 'dbus_get_all_properties', options) as Promise<SyscallDbusGetAllPropertiesResult>,
|
||||||
|
listNames: (bus: DBusBus = 'user') => ipcRenderer.invoke('wo:syscall', 'dbus_list_names', { bus }) as Promise<SyscallDbusListNamesResult>,
|
||||||
|
},
|
||||||
|
|
||||||
|
desktop: {
|
||||||
|
applications: {
|
||||||
|
list: (options?: { locale?: string }) => ipcRenderer.invoke('wo:syscall', 'list_applications', options ?? {}) as Promise<ApplicationInfo[]>,
|
||||||
|
launch: (desktopId: string, files?: string[]) => ipcRenderer.invoke('wo:syscall', 'launch_desktop', { desktopId, files }) as Promise<SyscallLaunchResult>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
icons: {
|
||||||
|
resolve: (name: string, options?: { size?: number; theme?: string }) => ipcRenderer.invoke('wo:syscall', 'resolve_icon', { name, ...options }) as Promise<ApplicationIcon | null>,
|
||||||
|
},
|
||||||
|
|
||||||
|
xdg: {
|
||||||
|
directories: () => ipcRenderer.invoke('wo:syscall', 'xdg_directories', {}) as Promise<SyscallXdgDirectoriesResult>,
|
||||||
|
},
|
||||||
|
|
||||||
|
audio: {
|
||||||
|
sources: () => ipcRenderer.invoke('wo:syscall', 'audio_sources') as Promise<{ ok: boolean; sources: WoAudioSource[]; error?: string }>,
|
||||||
|
startDesktopCapture: () => ipcRenderer.invoke('wo:syscall', 'audio_capture_start', {}),
|
||||||
|
startCapture: (sourceId: string) => ipcRenderer.invoke('wo:syscall', 'audio_capture_start', { sourceId }),
|
||||||
|
stopCapture: () => ipcRenderer.invoke('wo:syscall', 'audio_capture_stop', {}),
|
||||||
|
onData: (callback) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, chunk: WoAudioChunk) => callback(chunk);
|
||||||
|
ipcRenderer.on('wo:audio-data', handler);
|
||||||
|
return () => ipcRenderer.removeListener('wo:audio-data', handler);
|
||||||
|
},
|
||||||
|
onState: (callback) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, state: { active: boolean; error?: string }) => callback(state);
|
||||||
|
ipcRenderer.on('wo:audio-state', handler);
|
||||||
|
return () => ipcRenderer.removeListener('wo:audio-state', handler);
|
||||||
|
},
|
||||||
|
} satisfies WoAudio,
|
||||||
|
|
||||||
|
portals: {
|
||||||
|
onRequest: (callback) => subscribePortalRequests(callback),
|
||||||
|
contextMenu: (options) => ipcRenderer.invoke('wo:portal-context-menu', options),
|
||||||
|
respond: async (requestId, response) => ipcRenderer.invoke('wo:syscall', 'portal_respond', {
|
||||||
|
requestId,
|
||||||
|
...response,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
submitDamageFrame: async (payload: DamageFramePayload) => {
|
submitDamageFrame: async (payload: DamageFramePayload) => {
|
||||||
return await ipcRenderer.invoke('wo:submit-damage-frame', payload);
|
return await ipcRenderer.invoke('wo:submit-damage-frame', payload);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Surface buffer: Wayland window pixel content from compositor
|
|
||||||
// Legacy path for backward compatibility (full pixel copy via IPC)
|
|
||||||
onSurfaceBuffer: (callback: (data: { name: string; width: number; height: number; stride: number; pixels: Buffer; damageRects?: Array<{x: number; y: number; width: number; height: number}> }) => void) => {
|
onSurfaceBuffer: (callback: (data: { name: string; width: number; height: number; stride: number; pixels: Buffer; damageRects?: Array<{x: number; y: number; width: number; height: number}> }) => void) => {
|
||||||
const handler = (_event: Electron.IpcRendererEvent, data: { name: string; width: number; height: number; stride: number; pixels: Buffer; damageRects?: Array<{x: number; y: number; width: number; height: number}> }) =>
|
const handler = (_event: Electron.IpcRendererEvent, data: { name: string; width: number; height: number; stride: number; pixels: Buffer; damageRects?: Array<{x: number; y: number; width: number; height: number}> }) =>
|
||||||
callback(data);
|
callback(data);
|
||||||
|
|
@ -59,9 +148,6 @@ const compositorAPI: CompositorAPI = {
|
||||||
return () => ipcRenderer.removeListener('wo:surface-buffer', handler);
|
return () => ipcRenderer.removeListener('wo:surface-buffer', handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Zero-copy surface buffer via SharedArrayBuffer.
|
|
||||||
// 'wo:surface-sab' delivers the SAB once per window (or on resize).
|
|
||||||
// 'wo:surface-update' is a lightweight signal (no pixels) when new data is ready.
|
|
||||||
onSurfaceSab: (callback: (data: { name: string; sab: SharedArrayBuffer; width: number; height: number; stride: number }) => void) => {
|
onSurfaceSab: (callback: (data: { name: string; sab: SharedArrayBuffer; width: number; height: number; stride: number }) => void) => {
|
||||||
const handler = (_event: Electron.IpcRendererEvent, data: { name: string; sab: SharedArrayBuffer; width: number; height: number; stride: number }) =>
|
const handler = (_event: Electron.IpcRendererEvent, data: { name: string; sab: SharedArrayBuffer; width: number; height: number; stride: number }) =>
|
||||||
callback(data);
|
callback(data);
|
||||||
|
|
@ -83,7 +169,6 @@ const compositorAPI: CompositorAPI = {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Notification events from compositor
|
|
||||||
onNotification: (callback: (data: { id?: string; title: string; body: string; icon?: string; timeout?: number; timestamp?: number }) => void) => {
|
onNotification: (callback: (data: { id?: string; title: string; body: string; icon?: string; timeout?: number; timestamp?: number }) => void) => {
|
||||||
const handler = (_event: Electron.IpcRendererEvent, data: { id?: string; title: string; body: string; icon?: string; timeout?: number; timestamp?: number }) =>
|
const handler = (_event: Electron.IpcRendererEvent, data: { id?: string; title: string; body: string; icon?: string; timeout?: number; timestamp?: number }) =>
|
||||||
callback(data);
|
callback(data);
|
||||||
|
|
@ -91,17 +176,30 @@ const compositorAPI: CompositorAPI = {
|
||||||
return () => ipcRenderer.removeListener('wo:notification', handler);
|
return () => ipcRenderer.removeListener('wo:notification', handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Forward keyboard event from canvas to compositor (evdev keycode)
|
|
||||||
forwardKeyboard: (windowName: string, evdevKey: number, pressed: boolean, time: number) => {
|
forwardKeyboard: (windowName: string, evdevKey: number, pressed: boolean, time: number) => {
|
||||||
ipcRenderer.send('wo:forward-keyboard', windowName, evdevKey, pressed, time);
|
ipcRenderer.send('wo:forward-keyboard', windowName, evdevKey, pressed, time);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Forward relative mouse motion
|
forwardPointerMotion: (windowName: string, x: number, y: number) => {
|
||||||
|
ipcRenderer.send('wo:forward-pointer', windowName, x, y);
|
||||||
|
},
|
||||||
|
|
||||||
|
forwardPointerButton: (windowName: string, x: number, y: number, button: number, pressed: boolean, time: number) => {
|
||||||
|
ipcRenderer.send('wo:forward-pointer-button', windowName, x, y, button, pressed, time);
|
||||||
|
},
|
||||||
|
|
||||||
|
forwardPointerScroll: (windowName: string, x: number, y: number, dx: number, dy: number, time: number) => {
|
||||||
|
ipcRenderer.send('wo:forward-pointer-scroll', windowName, x, y, dx, dy, time);
|
||||||
|
},
|
||||||
|
|
||||||
|
forwardPointerLeave: (windowName: string) => {
|
||||||
|
ipcRenderer.send('wo:forward-pointer-leave', windowName);
|
||||||
|
},
|
||||||
|
|
||||||
forwardRelativePointer: (windowName: string, dx: number, dy: number) => {
|
forwardRelativePointer: (windowName: string, dx: number, dy: number) => {
|
||||||
ipcRenderer.send('wo:forward-relative-pointer', windowName, dx, dy);
|
ipcRenderer.send('wo:forward-relative-pointer', windowName, dx, dy);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Pointer lock request from compositor
|
|
||||||
onPointerLockRequest: (callback: (data: { window: string; lock: boolean }) => void) => {
|
onPointerLockRequest: (callback: (data: { window: string; lock: boolean }) => void) => {
|
||||||
const handler = (_event: Electron.IpcRendererEvent, data: { window: string; lock: boolean }) =>
|
const handler = (_event: Electron.IpcRendererEvent, data: { window: string; lock: boolean }) =>
|
||||||
callback(data);
|
callback(data);
|
||||||
|
|
@ -109,7 +207,6 @@ const compositorAPI: CompositorAPI = {
|
||||||
return () => ipcRenderer.removeListener('wo:pointer-lock-request', handler);
|
return () => ipcRenderer.removeListener('wo:pointer-lock-request', handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Environment variable updates from compositor (e.g. DISPLAY after XWayland ready)
|
|
||||||
onEnvUpdate: (callback: (vars: Record<string, string>) => void) => {
|
onEnvUpdate: (callback: (vars: Record<string, string>) => void) => {
|
||||||
const handler = (_event: Electron.IpcRendererEvent, vars: Record<string, string>) =>
|
const handler = (_event: Electron.IpcRendererEvent, vars: Record<string, string>) =>
|
||||||
callback(vars);
|
callback(vars);
|
||||||
|
|
@ -117,22 +214,12 @@ const compositorAPI: CompositorAPI = {
|
||||||
return () => ipcRenderer.removeListener('wo:env-update', handler);
|
return () => ipcRenderer.removeListener('wo:env-update', handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Generic portal request event for client-provided popup/approval handlers
|
|
||||||
onPortalRequest: (callback: (data: {
|
onPortalRequest: (callback: (data: {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
payload: any;
|
payload: any;
|
||||||
kind?: string;
|
kind?: string;
|
||||||
}) => void) => {
|
}) => void) => subscribePortalRequests(callback),
|
||||||
const handler = (_event: Electron.IpcRendererEvent, data: {
|
|
||||||
requestId: string;
|
|
||||||
payload: any;
|
|
||||||
kind?: string;
|
|
||||||
}) => callback(data);
|
|
||||||
ipcRenderer.on('wo:portal-request', handler);
|
|
||||||
return () => ipcRenderer.removeListener('wo:portal-request', handler);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Screencopy capture event notifications
|
|
||||||
onScreencopyEvent: (callback: (data: { active: boolean; clientCount: number }) => void) => {
|
onScreencopyEvent: (callback: (data: { active: boolean; clientCount: number }) => void) => {
|
||||||
const handler = (_event: Electron.IpcRendererEvent, data: { active: boolean; clientCount: number }) =>
|
const handler = (_event: Electron.IpcRendererEvent, data: { active: boolean; clientCount: number }) =>
|
||||||
callback(data);
|
callback(data);
|
||||||
|
|
@ -141,6 +228,41 @@ const compositorAPI: CompositorAPI = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function subscribePortalRequests(callback: Parameters<WoPortals['onRequest']>[0]) {
|
||||||
|
portalRequestListeners.add(callback);
|
||||||
|
for (const data of pendingPortalRequests.splice(0)) {
|
||||||
|
callback(data);
|
||||||
|
ipcRenderer.send('wo:portal-ui-delivered', data.requestId);
|
||||||
|
}
|
||||||
|
return () => portalRequestListeners.delete(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
const portalRequestListeners = new Set<(data: PortalRequestEvent) => void>();
|
||||||
|
const pendingPortalRequests: PortalRequestEvent[] = [];
|
||||||
|
ipcRenderer.on('wo:portal-request', (_event, data: PortalRequestEvent) => {
|
||||||
|
if (portalRequestListeners.size === 0) {
|
||||||
|
pendingPortalRequests.push(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const callback of portalRequestListeners) callback(data);
|
||||||
|
ipcRenderer.send('wo:portal-ui-delivered', data.requestId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The desktop shell owns the system portal UI. Keep its consent response path
|
||||||
|
// separate from website capability grants so portal prompts still work when a
|
||||||
|
// site's Wo APIs have not been approved.
|
||||||
|
if (process.argv.includes('--wo-system-portal-ui=1')) {
|
||||||
|
// The main process holds portal requests until this preload has installed
|
||||||
|
// its queue listener. React can subscribe after that without losing an
|
||||||
|
// early xdg-desktop-portal request during shell startup.
|
||||||
|
ipcRenderer.send('wo:portal-ui-ready');
|
||||||
|
contextBridge.exposeInMainWorld('woPortalUI', {
|
||||||
|
onRequest: (callback: Parameters<WoPortals['onRequest']>[0]) => subscribePortalRequests(callback),
|
||||||
|
respond: (requestId: string, response: unknown) =>
|
||||||
|
ipcRenderer.invoke('wo:portal-ui-respond', requestId, response),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const woClientAPI: WoClientAPI = {
|
const woClientAPI: WoClientAPI = {
|
||||||
getWindows: async () => ipcRenderer.invoke('wo:get-windows') as Promise<WoWindow[]>,
|
getWindows: async () => ipcRenderer.invoke('wo:get-windows') as Promise<WoWindow[]>,
|
||||||
onWindows: (callback: (windows: WoWindow[]) => void) => {
|
onWindows: (callback: (windows: WoWindow[]) => void) => {
|
||||||
|
|
@ -151,9 +273,157 @@ const woClientAPI: WoClientAPI = {
|
||||||
sendAction: (action: string) => {
|
sendAction: (action: string) => {
|
||||||
ipcRenderer.send('wo:keybind-action', action);
|
ipcRenderer.send('wo:keybind-action', action);
|
||||||
},
|
},
|
||||||
|
portals: compositorAPI.portals,
|
||||||
|
dbus: compositorAPI.dbus,
|
||||||
|
desktop: compositorAPI.desktop,
|
||||||
|
icons: compositorAPI.icons,
|
||||||
|
xdg: compositorAPI.xdg,
|
||||||
|
audio: compositorAPI.audio,
|
||||||
};
|
};
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('compositor', compositorAPI);
|
function readCapabilityArgument(name: string): WoCapability[] {
|
||||||
contextBridge.exposeInMainWorld('woClient', woClientAPI);
|
const prefix = `--wo-api-${name}=`;
|
||||||
|
const argument = process.argv.find((value) => value.startsWith(prefix));
|
||||||
|
if (!argument) return [];
|
||||||
|
try {
|
||||||
|
const value = JSON.parse(decodeURIComponent(argument.slice(prefix.length)));
|
||||||
|
return Array.isArray(value) ? value.filter((item): item is WoCapability => typeof item === 'string') : [];
|
||||||
|
} catch { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function capabilityPageMatches(): boolean {
|
||||||
|
const argument = process.argv.find((value) => value.startsWith('--wo-api-page='));
|
||||||
|
if (!argument) return false;
|
||||||
|
try {
|
||||||
|
const approved = new URL(decodeURIComponent(argument.slice('--wo-api-page='.length)));
|
||||||
|
const current = new URL((globalThis as any).location.href);
|
||||||
|
approved.hash = '';
|
||||||
|
current.hash = '';
|
||||||
|
return approved.href === current.href;
|
||||||
|
} catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedCapabilities = readCapabilityArgument('requested');
|
||||||
|
const grantedCapabilities = new Set(capabilityPageMatches() ? readCapabilityArgument('granted') : []);
|
||||||
|
const permissions: WoPermissionState = Object.freeze({
|
||||||
|
requested: Object.freeze([...requestedCapabilities]),
|
||||||
|
granted: Object.freeze([...grantedCapabilities]),
|
||||||
|
has: (capability: WoCapability) => grantedCapabilities.has(capability),
|
||||||
|
});
|
||||||
|
|
||||||
|
const compositorExposure: Record<string, unknown> = { ...compositorAPI, permissions };
|
||||||
|
const hide = (...keys: string[]) => keys.forEach((key) => { delete compositorExposure[key]; });
|
||||||
|
if (!grantedCapabilities.has('window-management')) hide(
|
||||||
|
'action', 'actionSync', 'onFocusChange', 'onWindowMetadata', 'onMonitors', 'forwardKeyboard',
|
||||||
|
'forwardPointerMotion', 'forwardPointerButton', 'forwardPointerScroll', 'forwardPointerLeave',
|
||||||
|
'forwardRelativePointer', 'onPointerLockRequest',
|
||||||
|
);
|
||||||
|
if (!grantedCapabilities.has('overlay-rendering')) hide(
|
||||||
|
'createDamageHelper', 'submitDamageFrame', 'onSurfaceBuffer', 'onSurfaceSab', 'onSurfaceUpdate',
|
||||||
|
);
|
||||||
|
if (!grantedCapabilities.has('desktop-applications')) hide('desktop');
|
||||||
|
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('notifications')) hide('onNotification');
|
||||||
|
if (grantedCapabilities.size > 0) {
|
||||||
|
contextBridge.exposeInMainWorld('compositor', compositorExposure);
|
||||||
|
const clientExposure: Record<string, unknown> = { ...woClientAPI, permissions };
|
||||||
|
if (!grantedCapabilities.has('window-management')) {
|
||||||
|
delete clientExposure.getWindows;
|
||||||
|
delete clientExposure.onWindows;
|
||||||
|
delete clientExposure.sendAction;
|
||||||
|
}
|
||||||
|
if (!grantedCapabilities.has('portals')) delete clientExposure.portals;
|
||||||
|
if (!grantedCapabilities.has('dbus')) delete clientExposure.dbus;
|
||||||
|
if (!grantedCapabilities.has('desktop-applications')) delete clientExposure.desktop;
|
||||||
|
if (!grantedCapabilities.has('xdg')) delete clientExposure.icons;
|
||||||
|
if (!grantedCapabilities.has('xdg')) delete clientExposure.xdg;
|
||||||
|
if (!grantedCapabilities.has('audio-capture')) delete clientExposure.audio;
|
||||||
|
contextBridge.exposeInMainWorld('woClient', clientExposure);
|
||||||
|
|
||||||
|
const woExposure: Record<string, unknown> = {
|
||||||
|
permissions,
|
||||||
|
getPerformanceStatus: () => ipcRenderer.invoke('wo:performance-status') as Promise<WoPerformanceStatus>,
|
||||||
|
onPerformanceStatus: (callback: (status: WoPerformanceStatus) => void) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, status: WoPerformanceStatus) => callback(status);
|
||||||
|
ipcRenderer.on('wo:performance-status', handler);
|
||||||
|
return () => ipcRenderer.removeListener('wo:performance-status', handler);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (grantedCapabilities.has('window-management')) {
|
||||||
|
woExposure.windows = {
|
||||||
|
get: woClientAPI.getWindows,
|
||||||
|
on: woClientAPI.onWindows,
|
||||||
|
action: compositorAPI.action,
|
||||||
|
onFocusChange: compositorAPI.onFocusChange,
|
||||||
|
onMetadata: compositorAPI.onWindowMetadata,
|
||||||
|
monitors: compositorAPI.onMonitors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (grantedCapabilities.has('overlay-rendering')) {
|
||||||
|
woExposure.overlay = {
|
||||||
|
createDamageHelper: compositorAPI.createDamageHelper,
|
||||||
|
submitDamageFrame: compositorAPI.submitDamageFrame,
|
||||||
|
onSurfaceBuffer: compositorAPI.onSurfaceBuffer,
|
||||||
|
onSurfaceSab: compositorAPI.onSurfaceSab,
|
||||||
|
onSurfaceUpdate: compositorAPI.onSurfaceUpdate,
|
||||||
|
mount: (element: HTMLElement, monitorId: string) => {
|
||||||
|
const monitor = monitorCache.find((entry) => entry.id === monitorId);
|
||||||
|
if (!monitor) throw new Error(`Unknown Wo monitor: ${monitorId}`);
|
||||||
|
const style = element.style;
|
||||||
|
const previous = {
|
||||||
|
position: style.position,
|
||||||
|
left: style.left,
|
||||||
|
top: style.top,
|
||||||
|
width: style.width,
|
||||||
|
height: style.height,
|
||||||
|
transform: style.transform,
|
||||||
|
};
|
||||||
|
const previousMonitor = element.dataset.woMonitor;
|
||||||
|
style.position = 'absolute';
|
||||||
|
style.left = `${monitor.x}px`;
|
||||||
|
style.top = `${monitor.y}px`;
|
||||||
|
style.width = `${monitor.width}px`;
|
||||||
|
style.height = `${monitor.height}px`;
|
||||||
|
style.transform = 'translateZ(0)';
|
||||||
|
element.dataset.woMonitor = monitor.id;
|
||||||
|
return () => {
|
||||||
|
style.position = previous.position;
|
||||||
|
style.left = previous.left;
|
||||||
|
style.top = previous.top;
|
||||||
|
style.width = previous.width;
|
||||||
|
style.height = previous.height;
|
||||||
|
style.transform = previous.transform;
|
||||||
|
if (previousMonitor === undefined) delete element.dataset.woMonitor;
|
||||||
|
else element.dataset.woMonitor = previousMonitor;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (grantedCapabilities.has('desktop-applications')) woExposure.desktop = compositorAPI.desktop;
|
||||||
|
if (grantedCapabilities.has('xdg')) {
|
||||||
|
woExposure.xdg = compositorAPI.xdg;
|
||||||
|
woExposure.icons = compositorAPI.icons;
|
||||||
|
}
|
||||||
|
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('notifications')) woExposure.onNotification = compositorAPI.onNotification;
|
||||||
|
woExposure.syscall = compositorAPI.syscall;
|
||||||
|
contextBridge.exposeInMainWorld('wo', woExposure);
|
||||||
|
}
|
||||||
|
if (grantedCapabilities.size === 0) {
|
||||||
|
contextBridge.exposeInMainWorld('wo', {
|
||||||
|
permissions,
|
||||||
|
getPerformanceStatus: () => ipcRenderer.invoke('wo:performance-status') as Promise<WoPerformanceStatus>,
|
||||||
|
onPerformanceStatus: (callback: (status: WoPerformanceStatus) => void) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, status: WoPerformanceStatus) => callback(status);
|
||||||
|
ipcRenderer.on('wo:performance-status', handler);
|
||||||
|
return () => ipcRenderer.removeListener('wo:performance-status', handler);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export type { CompositorAPI, WoClientAPI };
|
export type { CompositorAPI, WoClientAPI };
|
||||||
|
|
|
||||||
|
|
@ -11,27 +11,32 @@
|
||||||
* IPC Protocol Magic Numbers (little-endian u32)
|
* IPC Protocol Magic Numbers (little-endian u32)
|
||||||
*/
|
*/
|
||||||
export const MAGIC = {
|
export const MAGIC = {
|
||||||
HELLO: 0x574F484C, // "WOHL"
|
HELLO: 0x574F484C,
|
||||||
FRAME: 0x574F4652, // "WOFR"
|
FRAME: 0x574F4652,
|
||||||
MOUSE_MOVE: 0x574F4D4D, // "WOMM"
|
MOUSE_MOVE: 0x574F4D4D,
|
||||||
MOUSE_BUTTON: 0x574F4D42, // "WOMB"
|
MOUSE_BUTTON: 0x574F4D42,
|
||||||
KEYBOARD: 0x574F4B42, // "WOKB"
|
KEYBOARD: 0x574F4B42,
|
||||||
SCROLL: 0x574F5343, // "WOSC"
|
SCROLL: 0x574F5343,
|
||||||
ACTION: 0x574F4341, // "WOAC"
|
ACTION: 0x574F4341,
|
||||||
FOCUS_CHANGE: 0x574F4643, // "WOFC"
|
FOCUS_CHANGE: 0x574F4643,
|
||||||
WINDOW_META: 0x574F574D, // "WOWM"
|
WINDOW_META: 0x574F574D,
|
||||||
WINDOW_POS: 0x574F5750, // "WOWP"
|
WINDOW_POS: 0x574F5750,
|
||||||
SYSCALL: 0x574F5359, // "WOSY"
|
SYSCALL: 0x574F5359,
|
||||||
FRAME_ACK: 0x574F4641, // "WOFA"
|
FRAME_ACK: 0x574F4641,
|
||||||
SURFACE_BUFFER: 0x574F5342, // "WOSB" — Wayland surface pixels from compositor
|
SURFACE_BUFFER: 0x574F5342,
|
||||||
SHM_BUFFER: 0x574F534D, // "WOSM" — Wayland SHM surface sent to Electron via process FD
|
SHM_BUFFER: 0x574F534D,
|
||||||
DMABUF_FRAME: 0x574F4446, // "WODF" — Wayland surface DMABUF FDs from compositor
|
DMABUF_FRAME: 0x574F4446,
|
||||||
FORWARD_POINTER: 0x574F5045, // "WOPE" — forwarded pointer from web UI canvas
|
FORWARD_POINTER: 0x574F5045,
|
||||||
FORWARD_KEYBOARD: 0x574F4B45, // "WOKE" — forwarded keyboard from web UI canvas
|
FORWARD_KEYBOARD: 0x574F4B45,
|
||||||
FORWARD_RELATIVE_POINTER: 0x574F5245, // "WORE" — forwarded relative pointer (dx/dy)
|
FORWARD_RELATIVE_POINTER: 0x574F5245,
|
||||||
POINTER_LOCK_REQUEST: 0x574F504C, // "WOPL" — server-to-client pointer lock request
|
FORWARD_POINTER_BUTTON: 0x574F5042,
|
||||||
ENV_UPDATE: 0x574F4555, // "WOEU" — environment variable update broadcast
|
FORWARD_POINTER_SCROLL: 0x574F5053,
|
||||||
SCREENCOPY_EVENT: 0x574F5345, // "WOSE" — screencopy capture event notification
|
FORWARD_POINTER_LEAVE: 0x574F504E,
|
||||||
|
POINTER_LOCK_REQUEST: 0x574F504C,
|
||||||
|
ENV_UPDATE: 0x574F4555,
|
||||||
|
SCREENCOPY_EVENT: 0x574F5345,
|
||||||
|
WINDOW_SIZE: 0x574F525A,
|
||||||
|
SITE_HASHES: 0x574F5348,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -83,7 +88,7 @@ export interface FocusChangeEvent {
|
||||||
|
|
||||||
export interface WindowMetadataEvent {
|
export interface WindowMetadataEvent {
|
||||||
type: 'windowMetadata';
|
type: 'windowMetadata';
|
||||||
metadata: string; // JSON string
|
metadata: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PointerLockRequestEvent {
|
export interface PointerLockRequestEvent {
|
||||||
|
|
@ -137,7 +142,7 @@ export interface FrameInfo {
|
||||||
seq: number;
|
seq: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
format: string; // DRM fourcc as string
|
format: string;
|
||||||
planes: PlaneInfo[];
|
planes: PlaneInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,10 +159,10 @@ export interface DmabufFrameInfo {
|
||||||
name: string;
|
name: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
format: number; // DRM fourcc as u32
|
format: number;
|
||||||
numPlanes: number;
|
numPlanes: number;
|
||||||
planes: DmabufPlaneInfo[];
|
planes: DmabufPlaneInfo[];
|
||||||
fds: number[]; // File descriptors passed with this message
|
fds: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DmabufPlaneInfo {
|
export interface DmabufPlaneInfo {
|
||||||
|
|
@ -212,7 +217,6 @@ export function bufferToF64LE(buf: Buffer, offset: number = 0): number {
|
||||||
|
|
||||||
export function bufferToI32LE(buf: Buffer, offset: number = 0): number {
|
export function bufferToI32LE(buf: Buffer, offset: number = 0): number {
|
||||||
const val = bufferToU32LE(buf, offset);
|
const val = bufferToU32LE(buf, offset);
|
||||||
// Convert unsigned to signed
|
|
||||||
return val > 0x7fffffff ? val - 0x100000000 : val;
|
return val > 0x7fffffff ? val - 0x100000000 : val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,9 +74,11 @@ export function createRendererDamageHelper(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rowBytes = clamped.width * 4;
|
const rowBytes = clamped.width * 4;
|
||||||
|
const sourceX = clamped.x - rect.x;
|
||||||
|
const sourceY = clamped.y - rect.y;
|
||||||
const startX = clamped.x * 4;
|
const startX = clamped.x * 4;
|
||||||
for (let row = 0; row < clamped.height; row += 1) {
|
for (let row = 0; row < clamped.height; row += 1) {
|
||||||
const srcStart = row * srcStride;
|
const srcStart = (sourceY + row) * srcStride + sourceX * 4;
|
||||||
const dstStart = (clamped.y + row) * stride + startX;
|
const dstStart = (clamped.y + row) * stride + startX;
|
||||||
frame.set(rgba.subarray(srcStart, srcStart + rowBytes), dstStart);
|
frame.set(rgba.subarray(srcStart, srcStart + rowBytes), dstStart);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
106
electron/src/wo-capabilities.test.ts
Normal file
106
electron/src/wo-capabilities.test.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { afterEach, describe, expect, test } from 'bun:test';
|
||||||
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { pathToFileURL } from 'node:url';
|
||||||
|
import { prefetchWoSiteAssets, sha256, validateWoManifest } from './wo-capabilities.js';
|
||||||
|
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const path of tempDirs.splice(0)) rmSync(path, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Wo site metadata', () => {
|
||||||
|
test('requires the manifest to be tied to the exact index and origin', () => {
|
||||||
|
const page = 'https://example.test/app/index.html';
|
||||||
|
const indexSha256 = 'a'.repeat(64);
|
||||||
|
const valid = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
appId: 'org.example.app',
|
||||||
|
origin: 'https://example.test',
|
||||||
|
indexSha256,
|
||||||
|
capabilities: ['window-management'],
|
||||||
|
componentsCss: { url: './_wo.components.css', sha256: 'b'.repeat(64) },
|
||||||
|
styles: [],
|
||||||
|
scripts: [],
|
||||||
|
};
|
||||||
|
expect(validateWoManifest(valid, indexSha256, page)).not.toBeNull();
|
||||||
|
expect(validateWoManifest(valid, 'c'.repeat(64), page)).toBeNull();
|
||||||
|
expect(validateWoManifest({ ...valid, origin: 'https://attacker.test' }, indexSha256, page)).toBeNull();
|
||||||
|
expect(validateWoManifest({ ...valid, capabilities: ['system-root'] }, indexSha256, page)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('prefetches and hashes page assets before accepting capability requests', async () => {
|
||||||
|
const page = 'https://example.test/app/index.html';
|
||||||
|
const indexBytes = new TextEncoder().encode('<!doctype html><script src="./app.js"></script>');
|
||||||
|
const cssBytes = new TextEncoder().encode(':root { --wo-accent: #abc; }');
|
||||||
|
const appCssBytes = new TextEncoder().encode('body { color: white; }');
|
||||||
|
const jsBytes = new TextEncoder().encode('window.appReady = true;');
|
||||||
|
const indexSha256 = sha256(indexBytes);
|
||||||
|
const manifest = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
appId: 'org.example.app',
|
||||||
|
origin: 'https://example.test',
|
||||||
|
indexSha256,
|
||||||
|
capabilities: ['window-management'],
|
||||||
|
componentsCss: { url: './_wo.components.css', sha256: sha256(cssBytes) },
|
||||||
|
styles: [{ url: './assets/app.css', sha256: sha256(appCssBytes) }],
|
||||||
|
scripts: [{ url: './app.js', sha256: sha256(jsBytes) }],
|
||||||
|
};
|
||||||
|
const assets = new Map([
|
||||||
|
[page, indexBytes],
|
||||||
|
['https://example.test/app/_wo.capabilities.json', new TextEncoder().encode(JSON.stringify(manifest))],
|
||||||
|
['https://example.test/app/_wo.components.css', cssBytes],
|
||||||
|
['https://example.test/app/assets/app.css', appCssBytes],
|
||||||
|
['https://example.test/app/app.js', jsBytes],
|
||||||
|
]);
|
||||||
|
const fetcher = async (url: string) => {
|
||||||
|
const bytes = assets.get(url);
|
||||||
|
return { ok: !!bytes, arrayBuffer: async () => bytes!.buffer.slice(bytes!.byteOffset, bytes!.byteOffset + bytes!.byteLength) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const site = await prefetchWoSiteAssets(page, fetcher);
|
||||||
|
expect(site.trusted).toBe(true);
|
||||||
|
expect(site.capabilities).toEqual(['window-management']);
|
||||||
|
expect(site.styles).toContain('--wo-accent');
|
||||||
|
expect(site.styleBodies.map((style) => style.url)).toEqual(['https://example.test/app/assets/app.css']);
|
||||||
|
expect(site.scripts).toEqual(['https://example.test/app/app.js']);
|
||||||
|
|
||||||
|
assets.set('https://example.test/app/bad.js', new TextEncoder().encode('changed script'));
|
||||||
|
assets.set('https://example.test/app/_wo.capabilities.json', new TextEncoder().encode(JSON.stringify({
|
||||||
|
...manifest,
|
||||||
|
scripts: [{ url: './bad.js', sha256: sha256(jsBytes) }],
|
||||||
|
})));
|
||||||
|
const tampered = await prefetchWoSiteAssets(page, fetcher);
|
||||||
|
expect(tampered.trusted).toBe(false);
|
||||||
|
expect(tampered.capabilities).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loads an exact local build and its relative assets from file URLs', async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'wo-site-'));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const index = new TextEncoder().encode('<html><script type="module" src="./assets/app.js"></script></html>');
|
||||||
|
const css = new TextEncoder().encode(':root { --wo-accent: red; }');
|
||||||
|
const js = new TextEncoder().encode('export const app = true;');
|
||||||
|
const assetsDir = join(root, 'assets');
|
||||||
|
mkdirSync(assetsDir);
|
||||||
|
writeFileSync(join(root, 'index.html'), index);
|
||||||
|
writeFileSync(join(root, '_wo.components.css'), css);
|
||||||
|
writeFileSync(join(assetsDir, 'app.js'), js);
|
||||||
|
writeFileSync(join(root, '_wo.capabilities.json'), JSON.stringify({
|
||||||
|
schemaVersion: 1,
|
||||||
|
appId: 'org.example.local',
|
||||||
|
indexSha256: sha256(index),
|
||||||
|
capabilities: ['overlay-rendering'],
|
||||||
|
componentsCss: { url: './_wo.components.css', sha256: sha256(css) },
|
||||||
|
scripts: [{ url: './assets/app.js', sha256: sha256(js) }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await prefetchWoSiteAssets(pathToFileURL(join(root, 'index.html')).href);
|
||||||
|
expect(result.trusted).toBe(true);
|
||||||
|
expect(result.appId).toBe('org.example.local');
|
||||||
|
expect(result.scripts).toHaveLength(1);
|
||||||
|
expect(result.styles).toContain('--wo-accent');
|
||||||
|
});
|
||||||
|
});
|
||||||
182
electron/src/wo-capabilities.ts
Normal file
182
electron/src/wo-capabilities.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
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',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type WoCapability = typeof WO_CAPABILITIES[number];
|
||||||
|
|
||||||
|
export interface WoSiteManifest {
|
||||||
|
schemaVersion: 1;
|
||||||
|
appId: string;
|
||||||
|
origin?: string;
|
||||||
|
indexSha256: string;
|
||||||
|
capabilities: WoCapability[];
|
||||||
|
componentsCss: { url: string; sha256: string };
|
||||||
|
styles?: Array<{ url: string; sha256: string }>;
|
||||||
|
scripts: Array<{ url: string; sha256: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WoSiteAssets {
|
||||||
|
pageUrl: string;
|
||||||
|
appId: string;
|
||||||
|
capabilities: WoCapability[];
|
||||||
|
styles: string;
|
||||||
|
stylesUrl: string;
|
||||||
|
stylesHash: string;
|
||||||
|
styleBodies: Array<{ url: string; hash: string; bytes: Uint8Array }>;
|
||||||
|
scripts: string[];
|
||||||
|
scriptBodies: Array<{ url: string; hash: string; bytes: Uint8Array }>;
|
||||||
|
indexSha256: string;
|
||||||
|
indexBytes: Uint8Array;
|
||||||
|
stylesBytes: Uint8Array;
|
||||||
|
trusted: boolean;
|
||||||
|
trustError?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FetchLike = (url: string) => Promise<{ ok: boolean; arrayBuffer(): Promise<ArrayBuffer> }>;
|
||||||
|
const MAX_INDEX_BYTES = 4 * 1024 * 1024;
|
||||||
|
const MAX_ASSET_BYTES = 16 * 1024 * 1024;
|
||||||
|
|
||||||
|
export function sha256(bytes: Uint8Array): string {
|
||||||
|
return createHash('sha256').update(bytes).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHash(value: unknown): value is string {
|
||||||
|
return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateWoManifest(value: unknown, indexHash: string, pageUrl: string): WoSiteManifest | null {
|
||||||
|
if (!value || typeof value !== 'object') return null;
|
||||||
|
const m = value as Partial<WoSiteManifest>;
|
||||||
|
let page: URL;
|
||||||
|
try { page = new URL(pageUrl); } catch { return null; }
|
||||||
|
if (m.schemaVersion !== 1 || typeof m.appId !== 'string' || !/^[a-zA-Z0-9._-]{1,128}$/.test(m.appId)) return null;
|
||||||
|
if (m.indexSha256 !== indexHash || (m.origin !== undefined && m.origin !== page.origin)) return null;
|
||||||
|
if (!Array.isArray(m.capabilities) || !m.capabilities.every((cap) => WO_CAPABILITIES.includes(cap))) return null;
|
||||||
|
if (!m.componentsCss || typeof m.componentsCss.url !== 'string' || !isHash(m.componentsCss.sha256)) return null;
|
||||||
|
if (!Array.isArray(m.scripts) || m.scripts.length > 256) return null;
|
||||||
|
if (m.styles !== undefined && (!Array.isArray(m.styles) || m.styles.length > 256)) return null;
|
||||||
|
const trustedPath = (path: string): boolean => {
|
||||||
|
try {
|
||||||
|
const resolved = new URL(path, page);
|
||||||
|
return resolved.origin === page.origin && (resolved.protocol === 'https:' || resolved.protocol === 'http:' || resolved.protocol === 'file:');
|
||||||
|
} catch { return false; }
|
||||||
|
};
|
||||||
|
if (!trustedPath(m.componentsCss.url)) return null;
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const script of m.scripts) {
|
||||||
|
if (!script || typeof script.url !== 'string' || !isHash(script.sha256) || !trustedPath(script.url)) return null;
|
||||||
|
const absolute = new URL(script.url, page).href;
|
||||||
|
if (seen.has(absolute)) return null;
|
||||||
|
seen.add(absolute);
|
||||||
|
}
|
||||||
|
for (const style of m.styles ?? []) {
|
||||||
|
if (!style || typeof style.url !== 'string' || !isHash(style.sha256) || !trustedPath(style.url)) return null;
|
||||||
|
const absolute = new URL(style.url, page).href;
|
||||||
|
if (seen.has(absolute)) return null;
|
||||||
|
seen.add(absolute);
|
||||||
|
}
|
||||||
|
return m as WoSiteManifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readUrl(url: string, maxBytes: number, fetcher: FetchLike): Promise<Uint8Array | null> {
|
||||||
|
try {
|
||||||
|
let bytes: Uint8Array;
|
||||||
|
if (new URL(url).protocol === 'file:') {
|
||||||
|
bytes = await readFile(fileURLToPath(url));
|
||||||
|
} else {
|
||||||
|
const response = await fetcher(url);
|
||||||
|
if (!response.ok) return null;
|
||||||
|
bytes = new Uint8Array(await response.arrayBuffer());
|
||||||
|
}
|
||||||
|
return bytes.byteLength <= maxBytes ? bytes : null;
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetCache = new Map<string, Uint8Array>();
|
||||||
|
|
||||||
|
async function loadAsset(url: string, hash: string, maxBytes: number, fetcher: FetchLike): Promise<Uint8Array | null> {
|
||||||
|
const cacheKey = `${url}#${hash}`;
|
||||||
|
const cached = assetCache.get(cacheKey);
|
||||||
|
if (cached) return cached;
|
||||||
|
const bytes = await readUrl(url, maxBytes, fetcher);
|
||||||
|
if (!bytes || sha256(bytes) !== hash) return null;
|
||||||
|
assetCache.set(cacheKey, bytes);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prefetchWoSiteAssets(
|
||||||
|
pageUrl: string,
|
||||||
|
fetcher: FetchLike = async (url) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (response.ok) return response;
|
||||||
|
const parsed = new URL(url);
|
||||||
|
if (parsed.hostname !== 'localhost' || parsed.protocol !== 'http:') return response;
|
||||||
|
parsed.hostname = '127.0.0.1';
|
||||||
|
return await fetch(parsed.href);
|
||||||
|
} catch (error) {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
if (parsed.hostname !== 'localhost' || parsed.protocol !== 'http:') throw error;
|
||||||
|
parsed.hostname = '127.0.0.1';
|
||||||
|
return await fetch(parsed.href);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
): Promise<WoSiteAssets> {
|
||||||
|
const empty = (trustError: string, indexSha256 = ''): WoSiteAssets => ({
|
||||||
|
pageUrl, appId: 'website', capabilities: [], styles: '', stylesUrl: '', stylesHash: '', styleBodies: [], scripts: [], scriptBodies: [], indexSha256, indexBytes: new Uint8Array(), stylesBytes: new Uint8Array(), trusted: false, trustError,
|
||||||
|
});
|
||||||
|
let page: URL;
|
||||||
|
try { page = new URL(pageUrl); } catch { return empty('invalid page URL'); }
|
||||||
|
if (!['http:', 'https:', 'file:'].includes(page.protocol)) return empty(`unsupported page protocol: ${page.protocol}`);
|
||||||
|
const index = await readUrl(page.href, MAX_INDEX_BYTES, fetcher);
|
||||||
|
if (!index) return empty(`could not read page: ${page.href}`);
|
||||||
|
const indexSha256 = sha256(index);
|
||||||
|
const manifestUrl = new URL('_wo.capabilities.json', new URL('.', page)).href;
|
||||||
|
const manifestBytes = await readUrl(manifestUrl, 256 * 1024, fetcher);
|
||||||
|
if (!manifestBytes) return empty(`could not read capability manifest: ${manifestUrl}`, indexSha256);
|
||||||
|
let parsed: unknown;
|
||||||
|
try { parsed = JSON.parse(new TextDecoder().decode(manifestBytes)); } catch { return empty('capability manifest is not valid JSON', indexSha256); }
|
||||||
|
const manifest = validateWoManifest(parsed, indexSha256, page.href);
|
||||||
|
if (!manifest) return empty('capability manifest does not match this page or contains invalid fields', indexSha256);
|
||||||
|
|
||||||
|
const cssUrl = new URL(manifest.componentsCss.url, page).href;
|
||||||
|
const css = await loadAsset(cssUrl, manifest.componentsCss.sha256, 1024 * 1024, fetcher);
|
||||||
|
if (!css) return empty(`component stylesheet failed SHA-256 verification: ${cssUrl}`, indexSha256);
|
||||||
|
const scripts: string[] = [];
|
||||||
|
const scriptBodies: Array<{ url: string; hash: string; bytes: Uint8Array }> = [];
|
||||||
|
for (const script of manifest.scripts) {
|
||||||
|
const scriptUrl = new URL(script.url, page).href;
|
||||||
|
const bytes = await loadAsset(scriptUrl, script.sha256, MAX_ASSET_BYTES, fetcher);
|
||||||
|
if (!bytes) return empty(`script failed SHA-256 verification: ${scriptUrl}`, indexSha256);
|
||||||
|
scripts.push(scriptUrl);
|
||||||
|
scriptBodies.push({ url: scriptUrl, hash: script.sha256, bytes });
|
||||||
|
}
|
||||||
|
const styleBodies: Array<{ url: string; hash: string; bytes: Uint8Array }> = [];
|
||||||
|
for (const style of manifest.styles ?? []) {
|
||||||
|
const styleUrl = new URL(style.url, page).href;
|
||||||
|
const bytes = await loadAsset(styleUrl, style.sha256, MAX_ASSET_BYTES, fetcher);
|
||||||
|
if (!bytes) return empty(`stylesheet failed SHA-256 verification: ${styleUrl}`, indexSha256);
|
||||||
|
styleBodies.push({ url: styleUrl, hash: style.sha256, bytes });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pageUrl: page.href,
|
||||||
|
appId: manifest.appId,
|
||||||
|
capabilities: [...new Set(manifest.capabilities)],
|
||||||
|
styles: new TextDecoder().decode(css),
|
||||||
|
stylesUrl: cssUrl,
|
||||||
|
stylesHash: manifest.componentsCss.sha256,
|
||||||
|
styleBodies,
|
||||||
|
scripts,
|
||||||
|
scriptBodies,
|
||||||
|
indexSha256,
|
||||||
|
indexBytes: index,
|
||||||
|
stylesBytes: css,
|
||||||
|
trusted: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,47 +1,10 @@
|
||||||
/**
|
|
||||||
* Wo Composer Input Event Receiver for Electron
|
|
||||||
*
|
|
||||||
* This module provides utilities for Electron apps to receive input events
|
|
||||||
* from the Wo compositor via the IPC protocol.
|
|
||||||
*
|
|
||||||
* Usage in your Electron renderer process:
|
|
||||||
*
|
|
||||||
* const { WoInputReceiver } = require('./wo-input-receiver');
|
|
||||||
* const receiver = new WoInputReceiver();
|
|
||||||
*
|
|
||||||
* receiver.on('mouseMove', (x, y) => {
|
|
||||||
* console.log(`Mouse moved to: ${x}, ${y}`);
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* receiver.on('mouseButton', (button, pressed, time) => {
|
|
||||||
* console.log(`Button ${button} ${pressed ? 'pressed' : 'released'}`);
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* receiver.on('keyboard', (key, pressed, time) => {
|
|
||||||
* console.log(`Key ${key} ${pressed ? 'pressed' : 'released'}`);
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* receiver.on('scroll', (vertical, horizontal, time) => {
|
|
||||||
* console.log(`Scrolled V:${vertical} H:${horizontal}`);
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* receiver.startListening();
|
|
||||||
*
|
|
||||||
* Or in the client app:
|
|
||||||
*
|
|
||||||
* // Access via window.__WO_INPUT_RECEIVER__
|
|
||||||
* window.__WO_INPUT_RECEIVER__.on('mouseMove', (x, y) => {
|
|
||||||
* // Handle mouse move
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
|
|
||||||
const MAGIC_MOUSE_MOVE = 0x574F4D4D; // "WOMM"
|
const MAGIC_MOUSE_MOVE = 0x574F4D4D;
|
||||||
const MAGIC_MOUSE_BUTTON = 0x574F4D42; // "WOMB"
|
const MAGIC_MOUSE_BUTTON = 0x574F4D42;
|
||||||
const MAGIC_KEYBOARD = 0x574F4B42; // "WOKB"
|
const MAGIC_KEYBOARD = 0x574F4B42;
|
||||||
const MAGIC_SCROLL = 0x574F5343; // "WOSC"
|
const MAGIC_SCROLL = 0x574F5343;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receives input events from the Wo compositor via IPC socket.
|
* Receives input events from the Wo compositor via IPC socket.
|
||||||
|
|
@ -81,8 +44,6 @@ class WoInputReceiver extends EventEmitter {
|
||||||
this.socket.destroy();
|
this.socket.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new socket connection just for receiving input events
|
|
||||||
// This reuses the DMABUF connection through parameter passing
|
|
||||||
this.socket = net.createConnection(this.ipcSocket);
|
this.socket = net.createConnection(this.ipcSocket);
|
||||||
|
|
||||||
this.socket.on('connect', () => {
|
this.socket.on('connect', () => {
|
||||||
|
|
@ -119,8 +80,7 @@ class WoInputReceiver extends EventEmitter {
|
||||||
let eventSize = 0;
|
let eventSize = 0;
|
||||||
let parsed = false;
|
let parsed = false;
|
||||||
|
|
||||||
// Mouse move: 4 (magic) + 8 (x) + 8 (y) = 20 bytes
|
if (magic === MAGIC_MOUSE_MOVE) {
|
||||||
if (magic === MAGIC_MOUSE_MOVE) { // "WOMM"
|
|
||||||
if (this.buffer.length >= 20) {
|
if (this.buffer.length >= 20) {
|
||||||
const x = this.buffer.readDoubleLE(4);
|
const x = this.buffer.readDoubleLE(4);
|
||||||
const y = this.buffer.readDoubleLE(12);
|
const y = this.buffer.readDoubleLE(12);
|
||||||
|
|
@ -129,8 +89,7 @@ class WoInputReceiver extends EventEmitter {
|
||||||
parsed = true;
|
parsed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Mouse button: 4 (magic) + 4 (button) + 4 (pressed) + 4 (time) = 16 bytes
|
else if (magic === MAGIC_MOUSE_BUTTON) {
|
||||||
else if (magic === MAGIC_MOUSE_BUTTON) { // "WOMB"
|
|
||||||
if (this.buffer.length >= 16) {
|
if (this.buffer.length >= 16) {
|
||||||
const button = this.buffer.readUInt32LE(4);
|
const button = this.buffer.readUInt32LE(4);
|
||||||
const pressed = this.buffer.readUInt32LE(8) !== 0;
|
const pressed = this.buffer.readUInt32LE(8) !== 0;
|
||||||
|
|
@ -140,8 +99,7 @@ class WoInputReceiver extends EventEmitter {
|
||||||
parsed = true;
|
parsed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Keyboard: 4 (magic) + 4 (key) + 4 (pressed) + 4 (time) = 16 bytes
|
else if (magic === MAGIC_KEYBOARD) {
|
||||||
else if (magic === MAGIC_KEYBOARD) { // "WOKB"
|
|
||||||
if (this.buffer.length >= 16) {
|
if (this.buffer.length >= 16) {
|
||||||
const key = this.buffer.readUInt32LE(4);
|
const key = this.buffer.readUInt32LE(4);
|
||||||
const pressed = this.buffer.readUInt32LE(8) !== 0;
|
const pressed = this.buffer.readUInt32LE(8) !== 0;
|
||||||
|
|
@ -151,8 +109,7 @@ class WoInputReceiver extends EventEmitter {
|
||||||
parsed = true;
|
parsed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Scroll: 4 (magic) + 4 (vertical) + 4 (horizontal) + 4 (time) = 16 bytes
|
else if (magic === MAGIC_SCROLL) {
|
||||||
else if (magic === MAGIC_SCROLL) { // "WOSC"
|
|
||||||
if (this.buffer.length >= 16) {
|
if (this.buffer.length >= 16) {
|
||||||
const vertical = this.buffer.readInt32LE(4);
|
const vertical = this.buffer.readInt32LE(4);
|
||||||
const horizontal = this.buffer.readInt32LE(8);
|
const horizontal = this.buffer.readInt32LE(8);
|
||||||
|
|
@ -163,13 +120,11 @@ class WoInputReceiver extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Unknown magic, skip this byte and try again
|
|
||||||
console.warn('[Wo] Unknown message magic: 0x' + magic.toString(16));
|
console.warn('[Wo] Unknown message magic: 0x' + magic.toString(16));
|
||||||
this.buffer = this.buffer.slice(1);
|
this.buffer = this.buffer.slice(1);
|
||||||
parsed = true;
|
parsed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we couldn't parse a complete event, wait for more data
|
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
91
electron/src/xdg.test.ts
Normal file
91
electron/src/xdg.test.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { afterEach, describe, expect, test } from 'bun:test';
|
||||||
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { getXdgDirectories, listDesktopApplications, resolveDesktopIcon } from './xdg.js';
|
||||||
|
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
function fixture() {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'wo-xdg-'));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const home = join(root, 'home');
|
||||||
|
const dataHome = join(home, '.local', 'share');
|
||||||
|
const applications = join(dataHome, 'applications');
|
||||||
|
const systemData = join(root, 'system-data');
|
||||||
|
mkdirSync(applications, { recursive: true });
|
||||||
|
mkdirSync(join(systemData, 'applications'), { recursive: true });
|
||||||
|
return {
|
||||||
|
root,
|
||||||
|
home,
|
||||||
|
applications,
|
||||||
|
systemData,
|
||||||
|
env: {
|
||||||
|
HOME: home,
|
||||||
|
XDG_DATA_HOME: dataHome,
|
||||||
|
XDG_DATA_DIRS: systemData,
|
||||||
|
XDG_CONFIG_HOME: join(home, '.config'),
|
||||||
|
LANG: 'en_US.UTF-8',
|
||||||
|
PATH: process.env.PATH,
|
||||||
|
} as NodeJS.ProcessEnv,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('XDG desktop helpers', () => {
|
||||||
|
test('reads XDG paths and user directories from the config file', () => {
|
||||||
|
const f = fixture();
|
||||||
|
mkdirSync(join(f.home, '.config'), { recursive: true });
|
||||||
|
writeFileSync(join(f.home, '.config', 'user-dirs.dirs'), 'XDG_DOWNLOAD_DIR="$HOME/Inbox"\n');
|
||||||
|
|
||||||
|
const dirs = getXdgDirectories(f.env);
|
||||||
|
expect(dirs.dataHome).toBe(join(f.home, '.local', 'share'));
|
||||||
|
expect(dirs.dataDirs).toEqual([f.systemData]);
|
||||||
|
expect(dirs.downloads).toBe(join(f.home, 'Inbox'));
|
||||||
|
expect(dirs.runtimeDir).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lists localized desktop entries and resolves themed icons', () => {
|
||||||
|
const f = fixture();
|
||||||
|
const iconFile = join(f.home, '.local', 'share', 'icons', 'hicolor', '64x64', 'apps', 'wo-test.png');
|
||||||
|
mkdirSync(join(iconFile, '..'), { recursive: true });
|
||||||
|
writeFileSync(iconFile, Buffer.from([1, 2, 3, 4]));
|
||||||
|
writeFileSync(join(f.applications, 'wo-test.desktop'), [
|
||||||
|
'[Desktop Entry]',
|
||||||
|
'Type=Application',
|
||||||
|
'Name=Test App',
|
||||||
|
'Name[en_US]=Localized Test App',
|
||||||
|
'Exec=/usr/bin/env -- %F',
|
||||||
|
'Icon=wo-test',
|
||||||
|
'Categories=Utility;Development;',
|
||||||
|
'Terminal=false',
|
||||||
|
].join('\n'));
|
||||||
|
writeFileSync(join(f.applications, 'hidden.desktop'), [
|
||||||
|
'[Desktop Entry]',
|
||||||
|
'Type=Application',
|
||||||
|
'Name=Hidden App',
|
||||||
|
'Exec=/usr/bin/env',
|
||||||
|
'Hidden=true',
|
||||||
|
].join('\n'));
|
||||||
|
|
||||||
|
const apps = listDesktopApplications({ env: f.env });
|
||||||
|
expect(apps).toHaveLength(1);
|
||||||
|
expect(apps[0]).toMatchObject({
|
||||||
|
name: 'Localized Test App',
|
||||||
|
command: '/usr/bin/env',
|
||||||
|
desktopId: 'wo-test.desktop',
|
||||||
|
categories: ['Utility', 'Development'],
|
||||||
|
terminal: false,
|
||||||
|
});
|
||||||
|
expect(apps[0].icon?.mimeType).toBe('image/png');
|
||||||
|
expect(Buffer.from(apps[0].icon?.data || '', 'base64')).toEqual(Buffer.from([1, 2, 3, 4]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for icons that are not present in active XDG icon trees', () => {
|
||||||
|
const f = fixture();
|
||||||
|
expect(resolveDesktopIcon('no-such-wo-icon', { env: f.env })).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
292
electron/src/xdg.ts
Normal file
292
electron/src/xdg.ts
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
||||||
|
import { basename, delimiter, extname, join, resolve } from 'node:path';
|
||||||
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
|
|
||||||
|
export interface XdgDirectories {
|
||||||
|
home: string;
|
||||||
|
configHome: string;
|
||||||
|
configDirs: string[];
|
||||||
|
dataHome: string;
|
||||||
|
dataDirs: string[];
|
||||||
|
cacheHome: string;
|
||||||
|
stateHome: string;
|
||||||
|
runtimeDir: string | null;
|
||||||
|
desktop: string;
|
||||||
|
documents: string;
|
||||||
|
downloads: string;
|
||||||
|
music: string;
|
||||||
|
pictures: string;
|
||||||
|
publicShare: string;
|
||||||
|
templates: string;
|
||||||
|
videos: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DesktopApplication {
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
desktopId: string;
|
||||||
|
desktopFile: string;
|
||||||
|
icon?: { type: 'base64'; data: string; mimeType: string };
|
||||||
|
genericName?: string;
|
||||||
|
comment?: string;
|
||||||
|
categories?: string[];
|
||||||
|
keywords?: string[];
|
||||||
|
startupWMClass?: string;
|
||||||
|
terminal?: boolean;
|
||||||
|
startupNotify?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getXdgDirectories(env: NodeJS.ProcessEnv = process.env): XdgDirectories {
|
||||||
|
const home = env.HOME || '/';
|
||||||
|
const configHome = env.XDG_CONFIG_HOME || join(home, '.config');
|
||||||
|
const dataHome = env.XDG_DATA_HOME || join(home, '.local', 'share');
|
||||||
|
const dataDirs = splitDirs(env.XDG_DATA_DIRS || '/usr/local/share:/usr/share');
|
||||||
|
const configDirs = splitDirs(env.XDG_CONFIG_DIRS || '/etc/xdg');
|
||||||
|
const userDirsFile = join(configHome, 'user-dirs.dirs');
|
||||||
|
const userDirs = parseUserDirs(userDirsFile, home);
|
||||||
|
const userDir = (key: string, fallback: string) => userDirs[key] || join(home, fallback);
|
||||||
|
|
||||||
|
return {
|
||||||
|
home,
|
||||||
|
configHome,
|
||||||
|
configDirs,
|
||||||
|
dataHome,
|
||||||
|
dataDirs,
|
||||||
|
cacheHome: env.XDG_CACHE_HOME || join(home, '.cache'),
|
||||||
|
stateHome: env.XDG_STATE_HOME || join(home, '.local', 'state'),
|
||||||
|
runtimeDir: env.XDG_RUNTIME_DIR || null,
|
||||||
|
desktop: userDir('XDG_DESKTOP_DIR', 'Desktop'),
|
||||||
|
documents: userDir('XDG_DOCUMENTS_DIR', 'Documents'),
|
||||||
|
downloads: userDir('XDG_DOWNLOAD_DIR', 'Downloads'),
|
||||||
|
music: userDir('XDG_MUSIC_DIR', 'Music'),
|
||||||
|
pictures: userDir('XDG_PICTURES_DIR', 'Pictures'),
|
||||||
|
publicShare: userDir('XDG_PUBLICSHARE_DIR', 'Public'),
|
||||||
|
templates: userDir('XDG_TEMPLATES_DIR', 'Templates'),
|
||||||
|
videos: userDir('XDG_VIDEOS_DIR', 'Videos'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitDirs(value: string): string[] {
|
||||||
|
return [...new Set(value.split(delimiter).filter(Boolean).map((dir) => resolve(dir)))];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUserDirs(file: string, home: string): Record<string, string> {
|
||||||
|
if (!existsSync(file)) return {};
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
|
||||||
|
const match = line.match(/^\s*(XDG_[A-Z]+_DIR)="((?:\\.|[^"])*)"\s*$/);
|
||||||
|
if (!match) continue;
|
||||||
|
const value = match[2].replace(/\\"/g, '"').replace(/\\\\/g, '\\').replace(/^\$HOME(?=\/|$)/, home);
|
||||||
|
if (value.startsWith('/')) result[match[1]] = resolve(value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeDesktopValue(value: string): string {
|
||||||
|
return value.replace(/\\([snrt\\])/g, (_match, code: string) => ({
|
||||||
|
s: ' ', n: '\n', r: '\r', t: '\t', '\\': '\\',
|
||||||
|
}[code] ?? code));
|
||||||
|
}
|
||||||
|
|
||||||
|
function desktopKeyValues(content: string, locale: string): Map<string, string> {
|
||||||
|
const values = new Map<string, string>();
|
||||||
|
let inEntry = false;
|
||||||
|
const localized = new Map<string, string>();
|
||||||
|
const language = locale.split(/[.@]/)[0];
|
||||||
|
for (const line of content.split(/\r?\n/)) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed.startsWith('[')) {
|
||||||
|
inEntry = trimmed === '[Desktop Entry]';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!inEntry || !trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
const splitAt = trimmed.indexOf('=');
|
||||||
|
if (splitAt < 1) continue;
|
||||||
|
const key = trimmed.slice(0, splitAt);
|
||||||
|
const value = decodeDesktopValue(trimmed.slice(splitAt + 1));
|
||||||
|
const localizedKey = key.match(/^Name\[(.+)\]$/);
|
||||||
|
if (localizedKey) localized.set(localizedKey[1], value);
|
||||||
|
else values.set(key, value);
|
||||||
|
}
|
||||||
|
if (language && localized.has(language)) values.set('Name', localized.get(language)!);
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
function availableCommand(command: string): boolean {
|
||||||
|
if (!command) return false;
|
||||||
|
if (command.includes('/')) return existsSync(command);
|
||||||
|
const result = spawnSync('sh', ['-c', 'command -v "$1" >/dev/null 2>&1', 'wo', command], {
|
||||||
|
stdio: 'ignore',
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
return result.status === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDesktopIcon(
|
||||||
|
name: string,
|
||||||
|
options: { size?: number; theme?: string; env?: NodeJS.ProcessEnv } = {},
|
||||||
|
): DesktopApplication['icon'] | undefined {
|
||||||
|
const xdg = getXdgDirectories(options.env);
|
||||||
|
const size = Math.max(16, options.size ?? 48);
|
||||||
|
const theme = options.theme || options.env?.XDG_ICON_THEME || process.env.XDG_ICON_THEME || 'hicolor';
|
||||||
|
const suffixes = extname(name) ? [''] : ['.png', '.svg'];
|
||||||
|
const candidates: string[] = [];
|
||||||
|
|
||||||
|
if (name.startsWith('/')) candidates.push(name);
|
||||||
|
for (const suffix of suffixes) {
|
||||||
|
for (const root of [xdg.dataHome, ...xdg.dataDirs]) {
|
||||||
|
for (const iconTheme of [theme, 'hicolor']) {
|
||||||
|
const themeRoot = join(root, 'icons', iconTheme);
|
||||||
|
for (const directory of iconSizeDirectories(themeRoot, size)) {
|
||||||
|
candidates.push(join(themeRoot, directory, 'apps', `${name}${suffix}`));
|
||||||
|
}
|
||||||
|
candidates.push(join(themeRoot, 'apps', `${name}${suffix}`));
|
||||||
|
}
|
||||||
|
candidates.push(join(root, 'pixmaps', `${name}${suffix}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = candidates.find((candidate) => existsSync(candidate));
|
||||||
|
if (!file) return undefined;
|
||||||
|
const extension = extname(file).toLowerCase();
|
||||||
|
const mimeType = extension === '.svg' ? 'image/svg+xml' : 'image/png';
|
||||||
|
try {
|
||||||
|
return { type: 'base64', data: readFileSync(file).toString('base64'), mimeType };
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconSizeDirectories(themeRoot: string, target: number): string[] {
|
||||||
|
try {
|
||||||
|
return readdirSync(themeRoot, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.filter((name) => /^(?:scalable|\d+x\d+(?:@\d+(?:\.\d+)?x)?)$/.test(name))
|
||||||
|
.sort((a, b) => iconDirectoryDistance(a, target) - iconDirectoryDistance(b, target));
|
||||||
|
} catch {
|
||||||
|
return ['scalable', `${target}x${target}`, '48x48', '32x32', '24x24', '16x16'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconDirectoryDistance(directory: string, target: number): number {
|
||||||
|
if (directory === 'scalable') return 0;
|
||||||
|
const match = directory.match(/^(\d+)x\d+/);
|
||||||
|
return match ? Math.abs(Number(match[1]) - target) + 1 : Number.MAX_SAFE_INTEGER;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listDesktopApplications(options: { env?: NodeJS.ProcessEnv; locale?: string } = {}): DesktopApplication[] {
|
||||||
|
const env = options.env || process.env;
|
||||||
|
const xdg = getXdgDirectories(env);
|
||||||
|
const locale = options.locale || env.LC_ALL || env.LC_MESSAGES || env.LANG || 'C';
|
||||||
|
const dirs = [join(xdg.dataHome, 'applications'), ...xdg.dataDirs.map((dir) => join(dir, 'applications'))];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const apps: DesktopApplication[] = [];
|
||||||
|
|
||||||
|
for (const dir of dirs) {
|
||||||
|
let entries: string[];
|
||||||
|
try {
|
||||||
|
entries = readdirSync(dir).filter((entry) => entry.endsWith('.desktop')).sort();
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
const file = join(dir, entry);
|
||||||
|
let keys: Map<string, string>;
|
||||||
|
try {
|
||||||
|
const content = readFileSync(file, 'utf8');
|
||||||
|
if (content.length > 512_000) continue;
|
||||||
|
keys = desktopKeyValues(content, locale);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const id = entry;
|
||||||
|
if (seen.has(id) || keys.get('Type') !== 'Application') continue;
|
||||||
|
seen.add(id);
|
||||||
|
if (keys.get('Hidden') === 'true' || keys.get('NoDisplay') === 'true') continue;
|
||||||
|
|
||||||
|
const onlyShowIn = (keys.get('OnlyShowIn') || '').split(';').filter(Boolean);
|
||||||
|
const notShowIn = (keys.get('NotShowIn') || '').split(';').filter(Boolean);
|
||||||
|
const desktops = (env.XDG_CURRENT_DESKTOP || '').split(':').filter(Boolean);
|
||||||
|
if (onlyShowIn.length && !onlyShowIn.some((desktop) => desktops.includes(desktop))) continue;
|
||||||
|
if (notShowIn.some((desktop) => desktops.includes(desktop))) continue;
|
||||||
|
|
||||||
|
const execLine = keys.get('Exec') || '';
|
||||||
|
const command = parseDesktopExec(execLine).argv[0] || '';
|
||||||
|
if (!command || (keys.has('TryExec') && !availableCommand(keys.get('TryExec')!))) continue;
|
||||||
|
const name = keys.get('Name') || basename(entry, '.desktop');
|
||||||
|
const iconName = keys.get('Icon');
|
||||||
|
const icon = iconName ? resolveDesktopIcon(iconName, { env }) : undefined;
|
||||||
|
apps.push({
|
||||||
|
name,
|
||||||
|
command,
|
||||||
|
desktopId: id,
|
||||||
|
desktopFile: file,
|
||||||
|
...(icon ? { icon } : {}),
|
||||||
|
...(keys.has('GenericName') ? { genericName: keys.get('GenericName') } : {}),
|
||||||
|
...(keys.has('Comment') ? { comment: keys.get('Comment') } : {}),
|
||||||
|
...(keys.has('Categories') ? { categories: keys.get('Categories')!.split(';').filter(Boolean) } : {}),
|
||||||
|
...(keys.has('Keywords') ? { keywords: keys.get('Keywords')!.split(';').filter(Boolean) } : {}),
|
||||||
|
...(keys.has('StartupWMClass') ? { startupWMClass: keys.get('StartupWMClass') } : {}),
|
||||||
|
...(keys.has('Terminal') ? { terminal: keys.get('Terminal') === 'true' } : {}),
|
||||||
|
...(keys.has('StartupNotify') ? { startupNotify: keys.get('StartupNotify') === 'true' } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function launchDesktopApplication(
|
||||||
|
desktopId: string,
|
||||||
|
files: string[] = [],
|
||||||
|
): Promise<{ ok: boolean; pid?: number; error?: string }> {
|
||||||
|
const app = listDesktopApplications().find((entry) => entry.desktopId === desktopId || entry.name === desktopId);
|
||||||
|
if (!app) return { ok: false, error: `Desktop application not found: ${desktopId}` };
|
||||||
|
const keys = desktopKeyValues(readFileSync(app.desktopFile, 'utf8'), process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || 'C');
|
||||||
|
const parsed = parseDesktopExec(keys.get('Exec') || '', { ...app, iconName: keys.get('Icon') }, files);
|
||||||
|
const [command, ...args] = parsed.argv;
|
||||||
|
if (!command) return { ok: false, error: `No executable in desktop entry: ${desktopId}` };
|
||||||
|
|
||||||
|
return new Promise((resolveResult) => {
|
||||||
|
try {
|
||||||
|
const child = spawn(command, args, { detached: true, stdio: 'ignore', env: process.env });
|
||||||
|
child.once('error', (error) => resolveResult({ ok: false, error: error.message }));
|
||||||
|
child.once('spawn', () => {
|
||||||
|
child.unref();
|
||||||
|
resolveResult({ ok: true, pid: child.pid });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
resolveResult({ ok: false, error: String(error) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDesktopExec(
|
||||||
|
exec: string,
|
||||||
|
app?: Pick<DesktopApplication, 'name' | 'desktopFile'> & { iconName?: string },
|
||||||
|
files: string[] = [],
|
||||||
|
): { argv: string[] } {
|
||||||
|
const tokens = exec.match(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s]+/g) || [];
|
||||||
|
const argv: string[] = [];
|
||||||
|
for (const token of tokens) {
|
||||||
|
const unquoted = token.replace(/^['"]|['"]$/g, '').replace(/\\(["'\\`$])/g, '$1');
|
||||||
|
if (unquoted === '%i') {
|
||||||
|
if (app?.iconName) argv.push('--icon', app.iconName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (unquoted === '%F' || unquoted === '%U') {
|
||||||
|
argv.push(...files);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const value = unquoted.replace(/%%|%[fFuUdDnNickvm]/g, (code) => {
|
||||||
|
if (code === '%%') return '%';
|
||||||
|
if (code === '%c') return app?.name || '';
|
||||||
|
if (code === '%k') return app?.desktopFile || '';
|
||||||
|
if (code === '%f' || code === '%u') return files[0] || '';
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
if (value) argv.push(value);
|
||||||
|
}
|
||||||
|
return { argv };
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,26 @@ export type {
|
||||||
WindowsCallback,
|
WindowsCallback,
|
||||||
WoClient,
|
WoClient,
|
||||||
WoClientAPI,
|
WoClientAPI,
|
||||||
|
WoCapability,
|
||||||
|
WoPermissionState,
|
||||||
|
WoGlobalAPI,
|
||||||
|
WoPerformanceStatus,
|
||||||
|
WoPortals,
|
||||||
|
WoDBus,
|
||||||
|
WoDesktop,
|
||||||
|
WoIcons,
|
||||||
|
WoXdg,
|
||||||
|
WoAudio,
|
||||||
|
WoAudioSource,
|
||||||
|
WoAudioChunk,
|
||||||
|
XdgDirectories,
|
||||||
|
DBusBus,
|
||||||
|
DBusCallOptions,
|
||||||
|
DBusPropertyOptions,
|
||||||
|
DBusInterfaceOptions,
|
||||||
|
DBusResult,
|
||||||
WoWindow,
|
WoWindow,
|
||||||
|
WoMonitor,
|
||||||
ApplicationIcon,
|
ApplicationIcon,
|
||||||
ApplicationInfo,
|
ApplicationInfo,
|
||||||
CompositorActionConfig,
|
CompositorActionConfig,
|
||||||
|
|
@ -23,6 +42,21 @@ export type {
|
||||||
RendererDamageHelperOptions,
|
RendererDamageHelperOptions,
|
||||||
PortalRequestEvent,
|
PortalRequestEvent,
|
||||||
ScreencopyEvent,
|
ScreencopyEvent,
|
||||||
|
SyscallXdgDirectoriesResult,
|
||||||
|
SyscallListApplicationsParams,
|
||||||
|
SyscallLaunchDesktopParams,
|
||||||
|
SyscallLaunchResult,
|
||||||
|
SyscallDbusGetAllPropertiesParams,
|
||||||
|
SyscallDbusCallParams,
|
||||||
|
SyscallDbusCallResult,
|
||||||
|
SyscallDbusGetPropertyParams,
|
||||||
|
SyscallDbusGetPropertyResult,
|
||||||
|
SyscallDbusListNamesParams,
|
||||||
|
SyscallDbusGetAllPropertiesResult,
|
||||||
|
SyscallDbusListNamesResult,
|
||||||
|
SyscallAudioCaptureParams,
|
||||||
|
SyscallAudioCaptureResult,
|
||||||
|
SyscallResolveIconParams,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export { InteractionDispatcher } from './dispatcher';
|
export { InteractionDispatcher } from './dispatcher';
|
||||||
|
|
|
||||||
249
electron/types/types.d.ts
vendored
249
electron/types/types.d.ts
vendored
|
|
@ -6,61 +6,60 @@
|
||||||
* Represents a single window managed by the compositor
|
* Represents a single window managed by the compositor
|
||||||
*/
|
*/
|
||||||
export interface WoWindow {
|
export interface WoWindow {
|
||||||
/** Unique identifier for this window */
|
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
/** Window title/label */
|
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
/** X coordinate of window in pixels */
|
|
||||||
x: number;
|
x: number;
|
||||||
|
|
||||||
/** Y coordinate of window in pixels */
|
|
||||||
y: number;
|
y: number;
|
||||||
|
|
||||||
/** Width of window in pixels */
|
|
||||||
width: number;
|
width: number;
|
||||||
|
|
||||||
/** Height of window in pixels */
|
|
||||||
height: number;
|
height: number;
|
||||||
|
|
||||||
/** Z-order (stacking) of the window (higher = on top) */
|
content_width?: number;
|
||||||
|
content_height?: number;
|
||||||
|
|
||||||
z_order?: number;
|
z_order?: number;
|
||||||
|
|
||||||
/** Whether this window is currently mapped/visible */
|
|
||||||
mapped?: boolean;
|
mapped?: boolean;
|
||||||
|
|
||||||
/** Whether this window is currently focused */
|
|
||||||
focused?: boolean;
|
focused?: boolean;
|
||||||
|
|
||||||
/** Frames per second being rendered */
|
|
||||||
fps?: number;
|
fps?: number;
|
||||||
|
|
||||||
/** Process ID of the application (if available) */
|
|
||||||
pid?: number;
|
pid?: number;
|
||||||
|
|
||||||
/** Window source: 'electron', 'wayland', or 'x11' (XWayland clients). */
|
|
||||||
source?: 'electron' | 'wayland' | 'x11';
|
source?: 'electron' | 'wayland' | 'x11';
|
||||||
|
|
||||||
/** True when server-side decorations/chrome should be drawn by comraw. */
|
|
||||||
ssd?: boolean;
|
ssd?: boolean;
|
||||||
|
|
||||||
/** Application ID (for Wayland windows) */
|
fullscreen?: boolean;
|
||||||
|
|
||||||
app_id?: string;
|
app_id?: string;
|
||||||
|
|
||||||
/** True if this is a dialog window (xdg_toplevel with set_parent) */
|
|
||||||
dialog?: boolean;
|
dialog?: boolean;
|
||||||
|
|
||||||
/** Name of the parent window for dialog windows (e.g. "wayland-0") */
|
|
||||||
parent_name?: string;
|
parent_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WoMonitor {
|
||||||
|
id: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
scale: number;
|
||||||
|
primary: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action types for compositor window control
|
* Action types for compositor window control
|
||||||
*/
|
*/
|
||||||
export type CompositorActionType =
|
export type CompositorActionType =
|
||||||
| 'focus' | 'minimize' | 'close' | 'resize' | 'move' | 'maximize'
|
| 'focus' | 'minimize' | 'close' | 'resize' | 'move' | 'maximize'
|
||||||
| 'pointer_motion' | 'pointer_button' | 'pointer_leave' | 'pointer_scroll' | 'keyboard_key';
|
| 'fullscreen' | 'pointer_motion' | 'pointer_button' | 'pointer_leave' | 'pointer_scroll' | 'keyboard_key';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for compositor actions
|
* Configuration for compositor actions
|
||||||
|
|
@ -74,9 +73,9 @@ export interface CompositorActionConfig {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stronger syscall typing
|
|
||||||
|
|
||||||
export interface SyscallListApplicationsParams { [key: string]: any; }
|
export interface SyscallListApplicationsParams { locale?: string; }
|
||||||
|
export interface SyscallLaunchDesktopParams { desktopId: string; files?: string[]; }
|
||||||
export interface SyscallLaunchParams { command: string; [key: string]: any; }
|
export interface SyscallLaunchParams { command: string; [key: string]: any; }
|
||||||
export interface SyscallExecParams { command: string; [key: string]: any; }
|
export interface SyscallExecParams { command: string; [key: string]: any; }
|
||||||
export interface SyscallListDirParams { path?: string; [key: string]: any; }
|
export interface SyscallListDirParams { path?: string; [key: string]: any; }
|
||||||
|
|
@ -91,11 +90,15 @@ export interface SyscallNotifyParams { id?: string; title: string; body?: string
|
||||||
export interface SyscallPortalRespondParams { requestId: string; response?: any; allowed?: boolean; type?: string; windowName?: string | null; [key: string]: any; }
|
export interface SyscallPortalRespondParams { requestId: string; response?: any; allowed?: boolean; type?: string; windowName?: string | null; [key: string]: any; }
|
||||||
export interface SyscallDbusCallParams { service: string; objectPath: string; interface: string; method: string; signature?: string; args?: any[]; bus?: 'user' | 'system'; [key: string]: any; }
|
export interface SyscallDbusCallParams { service: string; objectPath: string; interface: string; method: string; signature?: string; args?: any[]; bus?: 'user' | 'system'; [key: string]: any; }
|
||||||
export interface SyscallDbusGetPropertyParams { service: string; objectPath: string; interface: string; property: string; bus?: 'user' | 'system'; [key: string]: any; }
|
export interface SyscallDbusGetPropertyParams { service: string; objectPath: string; interface: string; property: string; bus?: 'user' | 'system'; [key: string]: any; }
|
||||||
|
export interface SyscallDbusGetAllPropertiesParams { service: string; objectPath: string; interface: string; bus?: 'user' | 'system'; }
|
||||||
|
export interface SyscallDbusListNamesParams { bus?: 'user' | 'system'; }
|
||||||
export interface SyscallLoadLocalFileParams { path: string; [key: string]: any; }
|
export interface SyscallLoadLocalFileParams { path: string; [key: string]: any; }
|
||||||
|
export interface SyscallResolveIconParams { name: string; size?: number; theme?: string; }
|
||||||
|
|
||||||
export interface SyscallLaunchResult { ok: boolean; pid?: number; error?: string; }
|
export interface SyscallLaunchResult { ok: boolean; pid?: number; error?: string; }
|
||||||
export interface SyscallExecResult { ok: boolean; pid?: number; error?: string; }
|
export interface SyscallExecResult { ok: boolean; pid?: number; error?: string; }
|
||||||
export interface SyscallListDirResult { ok: boolean; path?: string; entries?: { name: string; type: 'file'|'dir' }[]; error?: string; }
|
export interface SyscallListDirResult { ok: boolean; path?: string; entries?: { name: string; type: 'file'|'dir' }[]; error?: string; }
|
||||||
|
export interface SyscallXdgDirectoriesResult { ok: boolean; directories?: XdgDirectories; error?: string; }
|
||||||
export interface SyscallReadResult { ok: boolean; content?: string; size?: number; error?: string; }
|
export interface SyscallReadResult { ok: boolean; content?: string; size?: number; error?: string; }
|
||||||
export interface SyscallWriteResult { ok: boolean; bytesWritten?: number; error?: string; }
|
export interface SyscallWriteResult { ok: boolean; bytesWritten?: number; error?: string; }
|
||||||
export interface SyscallShutdownResult { ok: boolean; error?: string; }
|
export interface SyscallShutdownResult { ok: boolean; error?: string; }
|
||||||
|
|
@ -107,6 +110,10 @@ export interface SyscallNotifyResult { ok: boolean; error?: string; }
|
||||||
export interface SyscallPortalRespondResult { ok: boolean; error?: string; }
|
export interface SyscallPortalRespondResult { ok: boolean; error?: string; }
|
||||||
export interface SyscallDbusCallResult { ok: boolean; stdout?: string; stderr?: string; exitCode?: number; error?: string; }
|
export interface SyscallDbusCallResult { ok: boolean; stdout?: string; stderr?: string; exitCode?: number; error?: string; }
|
||||||
export interface SyscallDbusGetPropertyResult { ok: boolean; stdout?: string; stderr?: string; exitCode?: number; error?: string; }
|
export interface SyscallDbusGetPropertyResult { ok: boolean; stdout?: string; stderr?: string; exitCode?: number; error?: string; }
|
||||||
|
export interface SyscallDbusListNamesResult { ok: boolean; names?: string[]; stdout?: string; stderr?: string; exitCode?: number; error?: string; }
|
||||||
|
export interface SyscallDbusGetAllPropertiesResult { ok: boolean; properties?: Record<string, string>; stdout?: string; stderr?: string; exitCode?: number; error?: string; }
|
||||||
|
export interface SyscallAudioCaptureParams { sourceId?: string; }
|
||||||
|
export interface SyscallAudioCaptureResult { ok: boolean; source?: WoAudioSource; error?: string; }
|
||||||
export interface SyscallLoadLocalFileResult { ok: boolean; error?: string; }
|
export interface SyscallLoadLocalFileResult { ok: boolean; error?: string; }
|
||||||
|
|
||||||
export interface PortalRequestEvent {
|
export interface PortalRequestEvent {
|
||||||
|
|
@ -115,6 +122,49 @@ export interface PortalRequestEvent {
|
||||||
kind?: string;
|
kind?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WoCapability =
|
||||||
|
| 'window-management' | 'overlay-rendering' | 'desktop-applications' | 'xdg' | 'dbus'
|
||||||
|
| 'portals' | 'filesystem' | 'process-launch' | 'system-control' | 'notifications' | 'audio-capture';
|
||||||
|
|
||||||
|
export interface WoPermissionState {
|
||||||
|
requested: readonly WoCapability[];
|
||||||
|
granted: readonly WoCapability[];
|
||||||
|
has(capability: WoCapability): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WoPerformanceStatus {
|
||||||
|
limited: boolean;
|
||||||
|
gpuCompositing: string;
|
||||||
|
webgl: string;
|
||||||
|
sharedDmabuf: boolean | null;
|
||||||
|
reasons: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WoGlobalAPI {
|
||||||
|
permissions: WoPermissionState;
|
||||||
|
getPerformanceStatus(): Promise<WoPerformanceStatus>;
|
||||||
|
onPerformanceStatus(callback: (status: WoPerformanceStatus) => void): Unsubscribe;
|
||||||
|
windows?: {
|
||||||
|
get(): Promise<WoWindow[]>;
|
||||||
|
on(callback: WindowsCallback): Unsubscribe;
|
||||||
|
action: Compositor['action'];
|
||||||
|
onFocusChange: Compositor['onFocusChange'];
|
||||||
|
onMetadata: Compositor['onWindowMetadata'];
|
||||||
|
monitors: Compositor['onMonitors'];
|
||||||
|
};
|
||||||
|
overlay?: Pick<Compositor, 'createDamageHelper' | 'submitDamageFrame' | 'onSurfaceBuffer' | 'onSurfaceSab' | 'onSurfaceUpdate'> & {
|
||||||
|
mount(element: HTMLElement, monitorId: string): Unsubscribe;
|
||||||
|
};
|
||||||
|
desktop?: WoDesktop;
|
||||||
|
xdg?: WoXdg;
|
||||||
|
icons?: WoIcons;
|
||||||
|
dbus?: WoDBus;
|
||||||
|
portals?: WoPortals;
|
||||||
|
audio?: WoAudio;
|
||||||
|
syscall?: Compositor['syscall'];
|
||||||
|
onNotification?: Compositor['onNotification'];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ScreencopyEvent {
|
export interface ScreencopyEvent {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
clientCount: number;
|
clientCount: number;
|
||||||
|
|
@ -128,19 +178,113 @@ export interface ApplicationInfo {
|
||||||
command: string;
|
command: string;
|
||||||
multi_instance?: boolean;
|
multi_instance?: boolean;
|
||||||
icon?: ApplicationIcon;
|
icon?: ApplicationIcon;
|
||||||
|
desktopId?: string;
|
||||||
|
desktopFile?: string;
|
||||||
|
genericName?: string;
|
||||||
|
comment?: string;
|
||||||
|
categories?: string[];
|
||||||
|
keywords?: string[];
|
||||||
|
startupWMClass?: string;
|
||||||
|
terminal?: boolean;
|
||||||
|
startupNotify?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface XdgDirectories {
|
||||||
|
home: string;
|
||||||
|
configHome: string;
|
||||||
|
configDirs: string[];
|
||||||
|
dataHome: string;
|
||||||
|
dataDirs: string[];
|
||||||
|
cacheHome: string;
|
||||||
|
stateHome: string;
|
||||||
|
runtimeDir: string | null;
|
||||||
|
desktop: string;
|
||||||
|
documents: string;
|
||||||
|
downloads: string;
|
||||||
|
music: string;
|
||||||
|
pictures: string;
|
||||||
|
publicShare: string;
|
||||||
|
templates: string;
|
||||||
|
videos: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DBusBus = 'user' | 'system';
|
||||||
|
export interface DBusCallOptions {
|
||||||
|
service: string;
|
||||||
|
objectPath: string;
|
||||||
|
interface: string;
|
||||||
|
method: string;
|
||||||
|
signature?: string;
|
||||||
|
args?: Array<string | number | boolean>;
|
||||||
|
bus?: DBusBus;
|
||||||
|
}
|
||||||
|
export interface DBusPropertyOptions {
|
||||||
|
service: string;
|
||||||
|
objectPath: string;
|
||||||
|
interface: string;
|
||||||
|
property: string;
|
||||||
|
bus?: DBusBus;
|
||||||
|
}
|
||||||
|
export interface DBusInterfaceOptions {
|
||||||
|
service: string;
|
||||||
|
objectPath: string;
|
||||||
|
interface: string;
|
||||||
|
bus?: DBusBus;
|
||||||
|
}
|
||||||
|
export interface DBusResult {
|
||||||
|
ok: boolean;
|
||||||
|
stdout?: string;
|
||||||
|
stderr?: string;
|
||||||
|
exitCode?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
export interface WoDBus {
|
||||||
|
call(options: DBusCallOptions): Promise<DBusResult>;
|
||||||
|
getProperty(options: DBusPropertyOptions): Promise<DBusResult>;
|
||||||
|
getAllProperties(options: DBusInterfaceOptions): Promise<SyscallDbusGetAllPropertiesResult>;
|
||||||
|
listNames(bus?: DBusBus): Promise<SyscallDbusListNamesResult>;
|
||||||
|
}
|
||||||
|
export interface WoDesktop {
|
||||||
|
applications: {
|
||||||
|
list(options?: SyscallListApplicationsParams): Promise<ApplicationInfo[]>;
|
||||||
|
launch(desktopId: string, files?: string[]): Promise<SyscallLaunchResult>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface WoIcons {
|
||||||
|
resolve(name: string, options?: { size?: number; theme?: string }): Promise<ApplicationIcon | null>;
|
||||||
|
}
|
||||||
|
export interface WoXdg {
|
||||||
|
directories(): Promise<SyscallXdgDirectoriesResult>;
|
||||||
|
}
|
||||||
|
export interface WoAudioSource {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
monitor: boolean;
|
||||||
|
}
|
||||||
|
export interface WoAudioChunk {
|
||||||
|
format: 's16le';
|
||||||
|
sampleRate: 48000;
|
||||||
|
channels: 2;
|
||||||
|
data: Uint8Array;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
export interface WoAudio {
|
||||||
|
sources(): Promise<{ ok: boolean; sources: WoAudioSource[]; error?: string }>;
|
||||||
|
startDesktopCapture(): Promise<{ ok: boolean; source?: WoAudioSource; error?: string }>;
|
||||||
|
startCapture(sourceId: string): Promise<{ ok: boolean; source?: WoAudioSource; error?: string }>;
|
||||||
|
stopCapture(): Promise<{ ok: boolean; error?: string }>;
|
||||||
|
onData(callback: (chunk: WoAudioChunk) => void): Unsubscribe;
|
||||||
|
onState(callback: (state: { active: boolean; error?: string }) => void): Unsubscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Icon data for applications - supports multiple formats
|
* Icon data for applications - supports multiple formats
|
||||||
*/
|
*/
|
||||||
export interface ApplicationIcon {
|
export interface ApplicationIcon {
|
||||||
/** Type of icon: 'iconify', 'base64', 'url', or 'path' */
|
|
||||||
type: 'iconify' | 'base64' | 'url' | 'path';
|
type: 'iconify' | 'base64' | 'url' | 'path';
|
||||||
/** Icon data or reference */
|
|
||||||
data: string;
|
data: string;
|
||||||
/** Optional MIME type (e.g., 'image/png', 'image/svg+xml') */
|
|
||||||
mimeType?: string;
|
mimeType?: string;
|
||||||
/** Optional fallback icon if primary fails */
|
|
||||||
fallback?: ApplicationIcon;
|
fallback?: ApplicationIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,6 +312,7 @@ export type Unsubscribe = () => void;
|
||||||
* Wayland client interface for receiving window updates
|
* Wayland client interface for receiving window updates
|
||||||
*/
|
*/
|
||||||
export interface WoClient {
|
export interface WoClient {
|
||||||
|
permissions?: WoPermissionState;
|
||||||
/**
|
/**
|
||||||
* Get the current window list once
|
* Get the current window list once
|
||||||
*/
|
*/
|
||||||
|
|
@ -187,15 +332,47 @@ export interface WoClient {
|
||||||
* Send a keybind/management action to the compositor
|
* Send a keybind/management action to the compositor
|
||||||
*/
|
*/
|
||||||
sendAction(action: string): void;
|
sendAction(action: string): void;
|
||||||
|
|
||||||
|
portals: WoPortals;
|
||||||
|
dbus: WoDBus;
|
||||||
|
desktop: WoDesktop;
|
||||||
|
icons: WoIcons;
|
||||||
|
xdg: WoXdg;
|
||||||
|
audio: WoAudio;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WoPortals {
|
||||||
|
onRequest(callback: (data: PortalRequestEvent) => void): Unsubscribe;
|
||||||
|
/** Open a native desktop context menu through the platform popup path. */
|
||||||
|
contextMenu(options: WoContextMenuOptions): Promise<{ ok: boolean; selectedId?: string; error?: string }>;
|
||||||
|
respond(requestId: string, response: {
|
||||||
|
allowed: boolean;
|
||||||
|
type?: 'window' | 'monitor' | 'virtual';
|
||||||
|
windowName?: string | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}): Promise<SyscallPortalRespondResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WoContextMenuOptions {
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
items: Array<{
|
||||||
|
id?: string;
|
||||||
|
label?: string;
|
||||||
|
type?: 'normal' | 'separator' | 'checkbox' | 'radio';
|
||||||
|
enabled?: boolean;
|
||||||
|
checked?: boolean;
|
||||||
|
accelerator?: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Alias for WoClient to match preload.ts imports */
|
|
||||||
export type WoClientAPI = WoClient;
|
export type WoClientAPI = WoClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compositor interface for controlling windows and the compositor
|
* Compositor interface for controlling windows and the compositor
|
||||||
*/
|
*/
|
||||||
export interface Compositor {
|
export interface Compositor {
|
||||||
|
permissions?: WoPermissionState;
|
||||||
/**
|
/**
|
||||||
* Send an action to modify a window
|
* Send an action to modify a window
|
||||||
*/
|
*/
|
||||||
|
|
@ -210,6 +387,9 @@ export interface Compositor {
|
||||||
* Execute a syscall (filesystem, process, etc.) with strong typings
|
* Execute a syscall (filesystem, process, etc.) with strong typings
|
||||||
*/
|
*/
|
||||||
syscall(type: 'list_applications', params?: SyscallListApplicationsParams): Promise<ApplicationInfo[]>;
|
syscall(type: 'list_applications', params?: SyscallListApplicationsParams): Promise<ApplicationInfo[]>;
|
||||||
|
syscall(type: 'launch_desktop', params: SyscallLaunchDesktopParams): Promise<SyscallLaunchResult>;
|
||||||
|
syscall(type: 'xdg_directories', params?: Record<string, never>): Promise<SyscallXdgDirectoriesResult>;
|
||||||
|
syscall(type: 'resolve_icon', params: SyscallResolveIconParams): Promise<ApplicationIcon | null>;
|
||||||
syscall(type: 'launch', params: SyscallLaunchParams): Promise<SyscallLaunchResult>;
|
syscall(type: 'launch', params: SyscallLaunchParams): Promise<SyscallLaunchResult>;
|
||||||
syscall(type: 'exec', params: SyscallExecParams): Promise<SyscallExecResult>;
|
syscall(type: 'exec', params: SyscallExecParams): Promise<SyscallExecResult>;
|
||||||
syscall(type: 'listdir', params?: SyscallListDirParams): Promise<SyscallListDirResult>;
|
syscall(type: 'listdir', params?: SyscallListDirParams): Promise<SyscallListDirResult>;
|
||||||
|
|
@ -224,9 +404,22 @@ export interface Compositor {
|
||||||
syscall(type: 'portal_respond', params: SyscallPortalRespondParams): Promise<SyscallPortalRespondResult>;
|
syscall(type: 'portal_respond', params: SyscallPortalRespondParams): Promise<SyscallPortalRespondResult>;
|
||||||
syscall(type: 'dbus_call', params: SyscallDbusCallParams): Promise<SyscallDbusCallResult>;
|
syscall(type: 'dbus_call', params: SyscallDbusCallParams): Promise<SyscallDbusCallResult>;
|
||||||
syscall(type: 'dbus_get_property', params: SyscallDbusGetPropertyParams): Promise<SyscallDbusGetPropertyResult>;
|
syscall(type: 'dbus_get_property', params: SyscallDbusGetPropertyParams): Promise<SyscallDbusGetPropertyResult>;
|
||||||
|
syscall(type: 'dbus_get_all_properties', params: SyscallDbusGetAllPropertiesParams): Promise<SyscallDbusGetAllPropertiesResult>;
|
||||||
|
syscall(type: 'dbus_list_names', params?: SyscallDbusListNamesParams): Promise<SyscallDbusListNamesResult>;
|
||||||
|
syscall(type: 'audio_sources', params?: Record<string, never>): Promise<{ ok: boolean; sources: WoAudioSource[]; error?: string }>;
|
||||||
|
syscall(type: 'audio_capture_start', params?: SyscallAudioCaptureParams): Promise<SyscallAudioCaptureResult>;
|
||||||
|
syscall(type: 'audio_capture_stop', params?: Record<string, never>): Promise<{ ok: boolean; error?: string }>;
|
||||||
syscall(type: 'load_local_file', params: SyscallLoadLocalFileParams): Promise<SyscallLoadLocalFileResult>;
|
syscall(type: 'load_local_file', params: SyscallLoadLocalFileParams): Promise<SyscallLoadLocalFileResult>;
|
||||||
syscall(type: string, params?: any): Promise<any>;
|
syscall(type: string, params?: any): Promise<any>;
|
||||||
|
|
||||||
|
/** XDG desktop entries, D-Bus and icon helpers. */
|
||||||
|
dbus: WoDBus;
|
||||||
|
portals: WoPortals;
|
||||||
|
desktop: WoDesktop;
|
||||||
|
icons: WoIcons;
|
||||||
|
xdg: WoXdg;
|
||||||
|
audio: WoAudio;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to focus changes
|
* Subscribe to focus changes
|
||||||
*/
|
*/
|
||||||
|
|
@ -236,6 +429,7 @@ export interface Compositor {
|
||||||
* Subscribe to raw window metadata broadcasts from the compositor
|
* Subscribe to raw window metadata broadcasts from the compositor
|
||||||
*/
|
*/
|
||||||
onWindowMetadata(callback: (metadata: WindowMetadataEvent) => void): Unsubscribe;
|
onWindowMetadata(callback: (metadata: WindowMetadataEvent) => void): Unsubscribe;
|
||||||
|
onMonitors(callback: (monitors: WoMonitor[]) => void): Unsubscribe;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a damage helper for optimized rendering
|
* Create a damage helper for optimized rendering
|
||||||
|
|
@ -248,6 +442,11 @@ export interface Compositor {
|
||||||
*/
|
*/
|
||||||
forwardKeyboard(windowName: string, evdevKey: number, pressed: boolean, time: number): void;
|
forwardKeyboard(windowName: string, evdevKey: number, pressed: boolean, time: number): void;
|
||||||
|
|
||||||
|
forwardPointerMotion(windowName: string, x: number, y: number): void;
|
||||||
|
forwardPointerButton(windowName: string, x: number, y: number, button: number, pressed: boolean, time: number): void;
|
||||||
|
forwardPointerScroll(windowName: string, x: number, y: number, dx: number, dy: number, time: number): void;
|
||||||
|
forwardPointerLeave(windowName: string): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quit the compositor
|
* Quit the compositor
|
||||||
*/
|
*/
|
||||||
|
|
@ -321,7 +520,6 @@ export interface Compositor {
|
||||||
onSurfaceUpdate(callback: (data: SurfaceUpdateData) => void): Unsubscribe;
|
onSurfaceUpdate(callback: (data: SurfaceUpdateData) => void): Unsubscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Alias for Compositor to match preload.ts imports */
|
|
||||||
export type CompositorAPI = Compositor;
|
export type CompositorAPI = Compositor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -375,6 +573,7 @@ export interface NotificationData {
|
||||||
declare global {
|
declare global {
|
||||||
window.woClient = {} as WoClient;
|
window.woClient = {} as WoClient;
|
||||||
window.compositor = {} as Compositor;
|
window.compositor = {} as Compositor;
|
||||||
|
window.wo = {} as WoGlobalAPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
127
justfile
Normal file
127
justfile
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
set shell := ["bash", "-eu", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
|
# List available build and development recipes.
|
||||||
|
default:
|
||||||
|
@just --list
|
||||||
|
|
||||||
|
# Build the Rust executables in release mode.
|
||||||
|
build-rust:
|
||||||
|
cargo build --release --locked --bins
|
||||||
|
|
||||||
|
# Build the Electron application and its native addon.
|
||||||
|
build-electron:
|
||||||
|
cd electron && bun install --frozen-lockfile --ignore-scripts && bun run build && bun run build-native
|
||||||
|
cd electron && if [ ! -x node_modules/electron/dist/electron ] && ! command -v electron >/dev/null 2>&1; then node node_modules/electron/install.js; fi
|
||||||
|
cd electron && if [ ! -x node_modules/electron/dist/electron ] && ! command -v electron >/dev/null 2>&1; then echo "Electron runtime download failed; install Electron or retry with network access" >&2; exit 1; fi
|
||||||
|
|
||||||
|
# Install the local Wo types package before building comraw.
|
||||||
|
build-types:
|
||||||
|
cd electron/types && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# Build the compositor UI using the types shipped with this checkout.
|
||||||
|
build-comraw:
|
||||||
|
cd comraw && bun install --frozen-lockfile
|
||||||
|
mkdir -p comraw/node_modules/@wl-wo
|
||||||
|
rm -rf comraw/node_modules/@wl-wo/wo-types
|
||||||
|
cp -a electron/types comraw/node_modules/@wl-wo/wo-types
|
||||||
|
cd comraw && bun run build
|
||||||
|
|
||||||
|
# Build every runtime component in dependency order.
|
||||||
|
build: build-rust build-electron build-types build-comraw
|
||||||
|
|
||||||
|
# Build and install Wo for the current user under ~/.local and XDG directories.
|
||||||
|
# An existing user config is kept; a first install gets a config.toml derived
|
||||||
|
# from config.example.toml and serves the packaged comraw build on localhost.
|
||||||
|
install: build
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eu
|
||||||
|
repo_root="$(pwd)"
|
||||||
|
: "${HOME:?HOME must be set for a per-user install}"
|
||||||
|
xdg_data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||||
|
data_home="$xdg_data_home/wo"
|
||||||
|
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/wo"
|
||||||
|
user_bin="$HOME/.local/bin"
|
||||||
|
electron_runtime="$data_home/electron-runtime"
|
||||||
|
electron_app="$data_home/electron"
|
||||||
|
comraw_dir="$data_home/comraw"
|
||||||
|
electron_source="$repo_root/electron/node_modules/electron/dist/electron"
|
||||||
|
install_tag="$(date +%Y%m%d%H%M%S).$$"
|
||||||
|
electron_runtime_stage="$data_home/.electron-runtime.new.$install_tag"
|
||||||
|
electron_app_stage="$data_home/.electron.new.$install_tag"
|
||||||
|
comraw_stage="$data_home/.comraw.new.$install_tag"
|
||||||
|
trap 'rm -rf "$electron_runtime_stage" "$electron_app_stage" "$comraw_stage"' EXIT
|
||||||
|
|
||||||
|
install -d "$data_home/bin" "$config_dir" "$user_bin"
|
||||||
|
install -m755 "$repo_root/target/release/wo" "$data_home/bin/wo"
|
||||||
|
install -m755 "$repo_root/target/release/wo-portal" "$data_home/bin/wo-portal"
|
||||||
|
install -m755 "$repo_root/target/release/xdg-desktop-portal-wo" "$data_home/bin/xdg-desktop-portal-wo"
|
||||||
|
if [ -x "$electron_source" ]; then
|
||||||
|
install -d "$electron_runtime_stage"
|
||||||
|
cp -a "$repo_root/electron/node_modules/electron/dist/." "$electron_runtime_stage/"
|
||||||
|
test -x "$electron_runtime_stage/electron" || { echo "Electron runtime copy is incomplete" >&2; exit 1; }
|
||||||
|
electron_bin="$electron_runtime/electron"
|
||||||
|
elif command -v electron >/dev/null 2>&1; then
|
||||||
|
electron_bin="$(command -v electron)"
|
||||||
|
else
|
||||||
|
echo "Electron runtime is missing. Install Electron or run: cd electron && node node_modules/electron/install.js" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
install -d "$electron_app_stage/dist" "$electron_app_stage/native" "$comraw_stage"
|
||||||
|
cp -a "$repo_root/electron/dist/." "$electron_app_stage/dist/"
|
||||||
|
cp -a "$repo_root/electron/native/." "$electron_app_stage/native/"
|
||||||
|
cp -a "$repo_root/comraw/dist/." "$comraw_stage/"
|
||||||
|
|
||||||
|
replace_installed_dir() {
|
||||||
|
stage="$1"
|
||||||
|
destination="$2"
|
||||||
|
if [ -e "$destination" ] || [ -L "$destination" ]; then
|
||||||
|
# Keep the prior tree available to processes that were already running.
|
||||||
|
mv "$destination" "$destination.previous.$install_tag"
|
||||||
|
fi
|
||||||
|
mv "$stage" "$destination"
|
||||||
|
}
|
||||||
|
if [ -d "$electron_runtime_stage" ]; then
|
||||||
|
replace_installed_dir "$electron_runtime_stage" "$electron_runtime"
|
||||||
|
fi
|
||||||
|
replace_installed_dir "$electron_app_stage" "$electron_app"
|
||||||
|
replace_installed_dir "$comraw_stage" "$comraw_dir"
|
||||||
|
|
||||||
|
cat > "$user_bin/wo" <<EOF
|
||||||
|
#!/bin/sh
|
||||||
|
export WO_DATA_DIR="$data_home"
|
||||||
|
export WO_ELECTRON_PATH="$electron_bin"
|
||||||
|
export WO_ELECTRON_APP_DIR="$electron_app"
|
||||||
|
exec "$data_home/bin/wo" "\$@"
|
||||||
|
EOF
|
||||||
|
chmod 755 "$user_bin/wo"
|
||||||
|
install -m755 "$data_home/bin/wo-portal" "$user_bin/wo-portal"
|
||||||
|
install -m755 "$data_home/bin/xdg-desktop-portal-wo" "$user_bin/xdg-desktop-portal-wo"
|
||||||
|
|
||||||
|
if [ ! -e "$config_dir/config.toml" ]; then
|
||||||
|
cp "$repo_root/config.example.toml" "$config_dir/config.toml"
|
||||||
|
fi
|
||||||
|
# Existing user configuration is preserved byte-for-byte. Keep any
|
||||||
|
# migrations explicit so installation cannot silently change launch
|
||||||
|
# commands, display variables, URLs, or user preferences.
|
||||||
|
|
||||||
|
install -D -m644 "$repo_root/packaging/wayland-sessions/wo.desktop" "$xdg_data_home/wayland-sessions/wo.desktop"
|
||||||
|
sed -i "s#Exec=wo#Exec=$user_bin/wo#; s#TryExec=wo#TryExec=$user_bin/wo#" "$xdg_data_home/wayland-sessions/wo.desktop"
|
||||||
|
install -D -m644 "$repo_root/packaging/portals/wo.portal" "$xdg_data_home/xdg-desktop-portal/portals/wo.portal"
|
||||||
|
install -D -m644 "$repo_root/packaging/dbus-services/org.freedesktop.impl.portal.desktop.wo.service" "$xdg_data_home/dbus-1/services/org.freedesktop.impl.portal.desktop.wo.service"
|
||||||
|
sed -i "s#Exec=/usr/bin/xdg-desktop-portal-wo#Exec=$user_bin/xdg-desktop-portal-wo#" "$xdg_data_home/dbus-1/services/org.freedesktop.impl.portal.desktop.wo.service"
|
||||||
|
echo "Installed Wo for the current user. Ensure $user_bin is on PATH and select Wo from your session manager."
|
||||||
|
|
||||||
|
# Run the Rust and Electron test suites.
|
||||||
|
test:
|
||||||
|
cargo test --locked
|
||||||
|
cd electron && bun run test
|
||||||
|
|
||||||
|
# Build a Debian package for the current Ubuntu/Debian architecture.
|
||||||
|
ubuntu-deb:
|
||||||
|
packaging/ubuntu/build-deb.sh
|
||||||
|
|
||||||
|
# Check formatting and compile the Rust binaries without producing a release build.
|
||||||
|
check:
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
cargo check --locked --bins
|
||||||
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
[D-BUS Service]
|
[D-BUS Service]
|
||||||
Name=org.freedesktop.impl.portal.desktop.wo
|
Name=org.freedesktop.impl.portal.desktop.wo
|
||||||
Exec=/usr/bin/wo-portal
|
Exec=/usr/bin/xdg-desktop-portal-wo
|
||||||
|
|
|
||||||
23
packaging/engines/README.md
Normal file
23
packaging/engines/README.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Upstream engine patches
|
||||||
|
|
||||||
|
Wo keeps its Electron integration patch against Electron `v45.0.0`
|
||||||
|
(`54f961b44b83e4ece81065ab60a182fd0c38c01e`) in `wo-electron-v45.patch`.
|
||||||
|
The CEF patch targets source commit
|
||||||
|
`e537569ecda2e3f23ffdac559ecf7f80b8e59467` and is stored in `wo-cef.patch`.
|
||||||
|
|
||||||
|
The Electron patch removes the old 240 FPS offscreen ceiling; Wo supplies each
|
||||||
|
Electron window's current output refresh rate to `webContents.setFrameRate()`.
|
||||||
|
The CEF patch prevents the compositor interval from rounding to zero at high
|
||||||
|
rates and documents updating the per-browser rate when a window changes
|
||||||
|
displays. Both engines already expose shared/native-pixmap offscreen callbacks;
|
||||||
|
Wo's native addon transfers their DMA-BUF descriptors using `SCM_RIGHTS`.
|
||||||
|
|
||||||
|
Apply both patches to their corresponding source checkouts with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
packaging/engines/apply-wo-patches.sh /path/to/electron-v45 /path/to/cef
|
||||||
|
```
|
||||||
|
|
||||||
|
The Electron and CEF repositories need their matching Chromium source and
|
||||||
|
toolchains to build. These patches were reviewed against the checked-out source
|
||||||
|
but a full Chromium/Electron/CEF build was not run.
|
||||||
16
packaging/engines/apply-wo-patches.sh
Executable file
16
packaging/engines/apply-wo-patches.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $# -ne 2 ]]; then
|
||||||
|
echo "Usage: $0 /path/to/electron-v45 /path/to/cef" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
electron_source="$1"
|
||||||
|
cef_source="$2"
|
||||||
|
|
||||||
|
git -C "$electron_source" apply --check "$repo_root/packaging/engines/wo-electron-v45.patch"
|
||||||
|
git -C "$cef_source" apply --check "$repo_root/packaging/engines/wo-cef.patch"
|
||||||
|
git -C "$electron_source" apply "$repo_root/packaging/engines/wo-electron-v45.patch"
|
||||||
|
git -C "$cef_source" apply "$repo_root/packaging/engines/wo-cef.patch"
|
||||||
10
packaging/engines/cef-wo.gn
Normal file
10
packaging/engines/cef-wo.gn
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
is_debug = false
|
||||||
|
is_official_build = true
|
||||||
|
is_component_build = false
|
||||||
|
symbol_level = 0
|
||||||
|
blink_symbol_level = 0
|
||||||
|
v8_symbol_level = 0
|
||||||
|
enable_printing = false
|
||||||
|
enable_pdf = false
|
||||||
|
enable_pdf_viewer = false
|
||||||
|
enable_spellcheck = false
|
||||||
9
packaging/engines/electron-wo.gn
Normal file
9
packaging/engines/electron-wo.gn
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import("//electron/build/args/release.gn")
|
||||||
|
|
||||||
|
enable_printing = false
|
||||||
|
enable_pdf = false
|
||||||
|
enable_pdf_viewer = false
|
||||||
|
enable_spellcheck = false
|
||||||
|
symbol_level = 0
|
||||||
|
blink_symbol_level = 0
|
||||||
|
v8_symbol_level = 0
|
||||||
55
packaging/engines/wo-cef.patch
Normal file
55
packaging/engines/wo-cef.patch
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
diff --git a/include/cef_browser.h b/include/cef_browser.h
|
||||||
|
index 2d31eca..21f40eb 100644
|
||||||
|
--- a/include/cef_browser.h
|
||||||
|
+++ b/include/cef_browser.h
|
||||||
|
@@ -814,8 +814,10 @@ class CefBrowserHost : public virtual CefBaseRefCounted {
|
||||||
|
/// Set the maximum rate in frames per second (fps) that CefRenderHandler::
|
||||||
|
/// OnPaint will be called for a windowless browser. The actual fps may be
|
||||||
|
/// lower if the browser cannot generate frames at the requested rate. The
|
||||||
|
- /// minimum value is 1 and the default value is 30. Can also be set at browser
|
||||||
|
- /// creation via CefBrowserSettings.windowless_frame_rate.
|
||||||
|
+ /// minimum value is 1 and the default value is 30. There is no fixed upper
|
||||||
|
+ /// frame-rate cap; callers should provide the refresh rate of the display
|
||||||
|
+ /// containing the window and update it when that rate changes. Can also be
|
||||||
|
+ /// set at browser creation via CefBrowserSettings.windowless_frame_rate.
|
||||||
|
///
|
||||||
|
/*--cef()--*/
|
||||||
|
virtual void SetWindowlessFrameRate(int frame_rate) = 0;
|
||||||
|
diff --git a/include/internal/cef_types.h b/include/internal/cef_types.h
|
||||||
|
index aef516c..fe916b4 100644
|
||||||
|
--- a/include/internal/cef_types.h
|
||||||
|
+++ b/include/internal/cef_types.h
|
||||||
|
@@ -605,8 +605,10 @@ typedef struct _cef_browser_settings_t {
|
||||||
|
/// The maximum rate in frames per second (fps) that CefRenderHandler::OnPaint
|
||||||
|
/// will be called for a windowless browser. The actual fps may be lower if
|
||||||
|
/// the browser cannot generate frames at the requested rate. The minimum
|
||||||
|
- /// value is 1 and the default value is 30. This value can also be changed
|
||||||
|
- /// dynamically via CefBrowserHost::SetWindowlessFrameRate.
|
||||||
|
+ /// value is 1 and the default value is 30. Set a per-window display refresh
|
||||||
|
+ /// rate and update it when that window moves to a display with a different
|
||||||
|
+ /// refresh rate. This value can also be changed dynamically via
|
||||||
|
+ /// CefBrowserHost::SetWindowlessFrameRate.
|
||||||
|
///
|
||||||
|
int windowless_frame_rate;
|
||||||
|
|
||||||
|
diff --git a/libcef/browser/osr/render_widget_host_view_osr.cc b/libcef/browser/osr/render_widget_host_view_osr.cc
|
||||||
|
index 8135ee0..4e334ed 100644
|
||||||
|
--- a/libcef/browser/osr/render_widget_host_view_osr.cc
|
||||||
|
+++ b/libcef/browser/osr/render_widget_host_view_osr.cc
|
||||||
|
@@ -7,6 +7,7 @@
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
+#include <algorithm>
|
||||||
|
#include <memory>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
@@ -1820,7 +1821,7 @@ void CefRenderWidgetHostViewOSR::SetFrameRate() {
|
||||||
|
int frame_rate =
|
||||||
|
osr_util::ClampFrameRate(browser->settings().windowless_frame_rate);
|
||||||
|
|
||||||
|
- frame_rate_threshold_us_ = 1000000 / frame_rate;
|
||||||
|
+ frame_rate_threshold_us_ = std::max(1, 1000000 / frame_rate);
|
||||||
|
|
||||||
|
if (compositor_) {
|
||||||
|
compositor_->SetDisplayVSyncParameters(
|
||||||
30
packaging/engines/wo-electron-v45.patch
Normal file
30
packaging/engines/wo-electron-v45.patch
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md
|
||||||
|
index 935ded4..7b68e69 100644
|
||||||
|
--- a/docs/api/web-contents.md
|
||||||
|
+++ b/docs/api/web-contents.md
|
||||||
|
@@ -2263,7 +2263,10 @@ Returns `boolean` - If _offscreen rendering_ is enabled returns whether it is cu
|
||||||
|
* `fps` Integer
|
||||||
|
|
||||||
|
If _offscreen rendering_ is enabled sets the frame rate to the specified number.
|
||||||
|
-When `webPreferences.offscreen.useSharedTexture` is `false` only values between 1 and 240 are accepted.
|
||||||
|
+The requested rate is not globally capped. Applications should set it to the
|
||||||
|
+refresh rate of the display containing the window and update it when that
|
||||||
|
+display's refresh rate changes. The actual rate may be lower when the renderer
|
||||||
|
+cannot produce frames quickly enough.
|
||||||
|
|
||||||
|
#### `contents.getFrameRate()`
|
||||||
|
|
||||||
|
diff --git a/shell/browser/osr/osr_render_widget_host_view.cc b/shell/browser/osr/osr_render_widget_host_view.cc
|
||||||
|
index dff1fcc..6536102 100644
|
||||||
|
--- a/shell/browser/osr/osr_render_widget_host_view.cc
|
||||||
|
+++ b/shell/browser/osr/osr_render_widget_host_view.cc
|
||||||
|
@@ -899,9 +899,6 @@ void OffScreenRenderWidgetHostView::SetFrameRate(int frame_rate) {
|
||||||
|
} else {
|
||||||
|
if (frame_rate <= 0)
|
||||||
|
frame_rate = 1;
|
||||||
|
- if (!offscreen_use_shared_texture_ && frame_rate > 240)
|
||||||
|
- frame_rate = 240;
|
||||||
|
-
|
||||||
|
frame_rate_ = frame_rate;
|
||||||
|
}
|
||||||
|
|
||||||
22
packaging/ubuntu/README.md
Normal file
22
packaging/ubuntu/README.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Ubuntu package
|
||||||
|
|
||||||
|
Build an architecture-native `.deb` from the checkout with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
packaging/ubuntu/build-deb.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script builds the Wo compositor and portal binaries, builds the Electron
|
||||||
|
application and its DMA-BUF addon, builds `comraw`, and stages the Electron
|
||||||
|
runtime with the package. It writes the package to `target/ubuntu-package/`.
|
||||||
|
Set `WO_PACKAGE_VERSION` to override the version or `WO_DEB_BUILD_ROOT` to
|
||||||
|
choose another output directory.
|
||||||
|
|
||||||
|
Build dependencies are Rust/Cargo, Bun, a C/C++ toolchain, `dpkg-dev`, and the
|
||||||
|
development headers for libinput, libudev, libseat, libdrm, GBM, EGL, Wayland,
|
||||||
|
PipeWire, and XKBCommon. The build also needs network access for locked Bun
|
||||||
|
dependencies and Electron's Linux runtime archive.
|
||||||
|
|
||||||
|
Install the result with `sudo apt install ./target/ubuntu-package/wo_*.deb`.
|
||||||
|
The package installs the Wo Wayland session, its xdg-desktop-portal
|
||||||
|
implementation, and a launcher that selects the bundled Electron runtime.
|
||||||
74
packaging/ubuntu/build-deb.sh
Executable file
74
packaging/ubuntu/build-deb.sh
Executable file
|
|
@ -0,0 +1,74 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
package_version="${WO_PACKAGE_VERSION:-$(sed -n 's/^version = "\([^"]*\)"/\1/p' "$repo_root/Cargo.toml" | head -n1)}"
|
||||||
|
package_arch="$(dpkg --print-architecture)"
|
||||||
|
build_root="${WO_DEB_BUILD_ROOT:-$repo_root/target/ubuntu-package}"
|
||||||
|
stage="$build_root/root"
|
||||||
|
deb="$build_root/wo_${package_version}_${package_arch}.deb"
|
||||||
|
|
||||||
|
for tool in cargo bun dpkg dpkg-deb; do
|
||||||
|
command -v "$tool" >/dev/null 2>&1 || {
|
||||||
|
echo "Missing build tool: $tool" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -d "$repo_root/comraw" ]]; then
|
||||||
|
echo "Missing comraw source tree at $repo_root/comraw" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building Wo release binaries"
|
||||||
|
(cd "$repo_root" && cargo build --release --locked --bins)
|
||||||
|
|
||||||
|
echo "Building Electron app and native DMA-BUF addon"
|
||||||
|
(cd "$repo_root/electron" && bun install --frozen-lockfile && bun run build && bun run build-native)
|
||||||
|
|
||||||
|
echo "Building comraw"
|
||||||
|
(cd "$repo_root/comraw" && bun install --frozen-lockfile)
|
||||||
|
rm -rf "$repo_root/comraw/node_modules/@wl-wo/wo-types"
|
||||||
|
mkdir -p "$repo_root/comraw/node_modules/@wl-wo"
|
||||||
|
cp -a "$repo_root/electron/types" "$repo_root/comraw/node_modules/@wl-wo/wo-types"
|
||||||
|
(cd "$repo_root/comraw" && bun run build)
|
||||||
|
|
||||||
|
rm -rf "$stage"
|
||||||
|
install -d "$stage/DEBIAN" "$stage/usr/bin" "$stage/usr/lib/wo/bin" \
|
||||||
|
"$stage/usr/lib/wo/electron" "$stage/usr/lib/wo/electron-runtime" \
|
||||||
|
"$stage/usr/lib/wo/comraw" "$stage/usr/share/wayland-sessions" \
|
||||||
|
"$stage/usr/share/xdg-desktop-portal/portals" "$stage/usr/share/dbus-1/services" \
|
||||||
|
"$stage/usr/share/doc/wo"
|
||||||
|
|
||||||
|
install -m755 "$repo_root/target/release/wo" "$stage/usr/lib/wo/bin/wo"
|
||||||
|
install -m755 "$repo_root/target/release/wo-portal" "$stage/usr/bin/wo-portal"
|
||||||
|
install -m755 "$repo_root/target/release/xdg-desktop-portal-wo" "$stage/usr/bin/xdg-desktop-portal-wo"
|
||||||
|
|
||||||
|
install -m755 "$repo_root/electron/node_modules/electron/dist/electron" \
|
||||||
|
"$stage/usr/lib/wo/electron-runtime/electron"
|
||||||
|
cp -a "$repo_root/electron/node_modules/electron/dist/." "$stage/usr/lib/wo/electron-runtime/"
|
||||||
|
cp -a "$repo_root/electron/dist" "$stage/usr/lib/wo/electron/"
|
||||||
|
cp -a "$repo_root/electron/native" "$stage/usr/lib/wo/electron/"
|
||||||
|
cp -a "$repo_root/comraw/dist" "$stage/usr/lib/wo/comraw/"
|
||||||
|
|
||||||
|
cat > "$stage/usr/bin/wo" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
export WO_DATA_DIR=/usr/lib/wo
|
||||||
|
export WO_ELECTRON_PATH=/usr/lib/wo/electron-runtime/electron
|
||||||
|
export WO_ELECTRON_APP_DIR=/usr/lib/wo/electron
|
||||||
|
exec /usr/lib/wo/bin/wo "$@"
|
||||||
|
EOF
|
||||||
|
chmod 755 "$stage/usr/bin/wo"
|
||||||
|
|
||||||
|
install -m644 "$repo_root/packaging/wayland-sessions/wo.desktop" \
|
||||||
|
"$stage/usr/share/wayland-sessions/wo.desktop"
|
||||||
|
install -m644 "$repo_root/packaging/portals/wo.portal" \
|
||||||
|
"$stage/usr/share/xdg-desktop-portal/portals/wo.portal"
|
||||||
|
install -m644 "$repo_root/packaging/dbus-services/org.freedesktop.impl.portal.desktop.wo.service" \
|
||||||
|
"$stage/usr/share/dbus-1/services/org.freedesktop.impl.portal.desktop.wo.service"
|
||||||
|
install -m644 "$repo_root/config.example.toml" "$stage/usr/share/doc/wo/config.example.toml"
|
||||||
|
|
||||||
|
sed -e "s/@VERSION@/$package_version/" -e "s/@ARCH@/$package_arch/" \
|
||||||
|
"$repo_root/packaging/ubuntu/control.in" > "$stage/DEBIAN/control"
|
||||||
|
dpkg-deb --build --root-owner-group "$stage" "$deb"
|
||||||
|
echo "Built $deb"
|
||||||
16
packaging/ubuntu/control.in
Normal file
16
packaging/ubuntu/control.in
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
Package: wo
|
||||||
|
Version: @VERSION@
|
||||||
|
Section: x11
|
||||||
|
Priority: optional
|
||||||
|
Architecture: @ARCH@
|
||||||
|
Maintainer: Stephen E. Hellings <stephen@hellings.cc>
|
||||||
|
Description: Electron based Wayland compositor
|
||||||
|
Wo is a Wayland compositor that hosts Electron surfaces and provides a
|
||||||
|
desktop portal implementation for screen sharing.
|
||||||
|
Depends: libc6, libstdc++6, libgcc-s1, libdrm2, libinput10, libseat1,
|
||||||
|
libxkbcommon0, libgbm1, libegl1, libwayland-client0, libwayland-server0,
|
||||||
|
libpipewire-0.3-0 | libpipewire-0.3-0t64, libnss3,
|
||||||
|
libgtk-3-0 | libgtk-3-0t64, libatk1.0-0 | libatk1.0-0t64,
|
||||||
|
libatk-bridge2.0-0 | libatk-bridge-2.0-0t64,
|
||||||
|
libx11-xcb1, libxcb-dri3-0, libxss1, libasound2 | libasound2t64,
|
||||||
|
xdg-desktop-portal, dbus, xwayland, python3
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
//! This module provides a trait-based abstraction for rendering that works
|
//! This module provides a trait-based abstraction for rendering that works
|
||||||
//! with both Nested (GLES via winit) and DRM/KMS backends.
|
//! with both Nested (GLES via winit) and DRM/KMS backends.
|
||||||
|
|
||||||
|
use crate::dmabuf::ElectronTexture;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use smithay::utils::{Physical, Rectangle};
|
use smithay::utils::{Physical, Rectangle};
|
||||||
use crate::dmabuf::ElectronTexture;
|
|
||||||
|
|
||||||
/// Common interface for rendering frames across different backends
|
/// Common interface for rendering frames across different backends
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
/// xdg-desktop-portal-wo - Standalone portal service for Wo compositor
|
|
||||||
///
|
|
||||||
/// This is a standalone D-Bus service that implements the xdg-desktop-portal
|
|
||||||
/// interfaces for screen capture. Applications can use this to request screen
|
|
||||||
/// or window capture without direct compositor integration.
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
|
|
@ -11,17 +5,19 @@ use std::sync::Arc;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||||
use zbus::{Connection, interface, fdo};
|
use zbus::{fdo, interface, Connection};
|
||||||
|
|
||||||
use wo::state::portal::{WoPortal, CaptureRequest, SourceType};
|
use wo::state::portal::{CaptureRequest, SourceType, WoPortal};
|
||||||
|
|
||||||
fn request_screen_share_consent(
|
fn request_screen_share_consent(
|
||||||
app_id: &str,
|
app_id: &str,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
requested_source: SourceType,
|
requested_source: SourceType,
|
||||||
) -> Result<Option<(SourceType, Option<String>)>> {
|
) -> Result<Option<(SourceType, Option<String>)>> {
|
||||||
let mut stream = UnixStream::connect("/tmp/wo-portal-ui.sock")?;
|
let runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string());
|
||||||
|
let mut stream =
|
||||||
|
UnixStream::connect(std::path::Path::new(&runtime_dir).join("wo-portal-ui.sock"))?;
|
||||||
stream.set_read_timeout(Some(Duration::from_secs(95)))?;
|
stream.set_read_timeout(Some(Duration::from_secs(95)))?;
|
||||||
|
|
||||||
let requested = match requested_source {
|
let requested = match requested_source {
|
||||||
|
|
@ -49,12 +45,19 @@ fn request_screen_share_consent(
|
||||||
}
|
}
|
||||||
|
|
||||||
let json: serde_json::Value = serde_json::from_str(response.trim())?;
|
let json: serde_json::Value = serde_json::from_str(response.trim())?;
|
||||||
let allowed = json.get("allowed").and_then(|v| v.as_bool()).unwrap_or(false);
|
let allowed = json
|
||||||
|
.get("allowed")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
if !allowed {
|
if !allowed {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let source = match json.get("sourceType").and_then(|v| v.as_str()).unwrap_or("Monitor") {
|
let source = match json
|
||||||
|
.get("sourceType")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("Monitor")
|
||||||
|
{
|
||||||
"Window" => SourceType::Window,
|
"Window" => SourceType::Window,
|
||||||
"Virtual" => SourceType::Virtual,
|
"Virtual" => SourceType::Virtual,
|
||||||
_ => SourceType::Monitor,
|
_ => SourceType::Monitor,
|
||||||
|
|
@ -79,12 +82,15 @@ impl ScreenCastImpl {
|
||||||
async fn create_session(
|
async fn create_session(
|
||||||
&self,
|
&self,
|
||||||
#[zbus(header)] _hdr: zbus::message::Header<'_>,
|
#[zbus(header)] _hdr: zbus::message::Header<'_>,
|
||||||
_handle: zbus::zvariant::ObjectPath<'_>,
|
_handle: zbus::ObjectPath<'_>,
|
||||||
session_handle: zbus::zvariant::ObjectPath<'_>,
|
session_handle: zbus::ObjectPath<'_>,
|
||||||
app_id: &str,
|
app_id: &str,
|
||||||
_options: std::collections::HashMap<String, zbus::zvariant::Value<'_>>,
|
_options: std::collections::HashMap<String, zbus::Variant<'_>>,
|
||||||
) -> fdo::Result<(u32, std::collections::HashMap<String, zbus::zvariant::OwnedValue>)> {
|
) -> fdo::Result<(u32, std::collections::HashMap<String, zbus::OwnedVariant>)> {
|
||||||
info!("CreateSession called: app_id={}, session={:?}", app_id, session_handle);
|
info!(
|
||||||
|
"CreateSession called: app_id={}, session={:?}",
|
||||||
|
app_id, session_handle
|
||||||
|
);
|
||||||
|
|
||||||
let internal_session_id = self.portal.create_session();
|
let internal_session_id = self.portal.create_session();
|
||||||
let external_session_id = session_handle
|
let external_session_id = session_handle
|
||||||
|
|
@ -98,7 +104,7 @@ impl ScreenCastImpl {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert(external_session_id, internal_session_id);
|
.insert(external_session_id, internal_session_id);
|
||||||
|
|
||||||
let response: std::collections::HashMap<String, zbus::zvariant::OwnedValue> =
|
let response: std::collections::HashMap<String, zbus::OwnedVariant> =
|
||||||
std::collections::HashMap::new();
|
std::collections::HashMap::new();
|
||||||
|
|
||||||
Ok((0, response)) // 0 = success
|
Ok((0, response)) // 0 = success
|
||||||
|
|
@ -108,11 +114,11 @@ impl ScreenCastImpl {
|
||||||
async fn select_sources(
|
async fn select_sources(
|
||||||
&self,
|
&self,
|
||||||
#[zbus(header)] _hdr: zbus::message::Header<'_>,
|
#[zbus(header)] _hdr: zbus::message::Header<'_>,
|
||||||
_handle: zbus::zvariant::ObjectPath<'_>,
|
_handle: zbus::ObjectPath<'_>,
|
||||||
session_handle: zbus::zvariant::ObjectPath<'_>,
|
session_handle: zbus::ObjectPath<'_>,
|
||||||
app_id: &str,
|
app_id: &str,
|
||||||
options: std::collections::HashMap<String, zbus::zvariant::Value<'_>>,
|
options: std::collections::HashMap<String, zbus::Variant<'_>>,
|
||||||
) -> fdo::Result<(u32, std::collections::HashMap<String, zbus::zvariant::OwnedValue>)> {
|
) -> fdo::Result<(u32, std::collections::HashMap<String, zbus::OwnedVariant>)> {
|
||||||
info!("SelectSources called for session: {:?}", session_handle);
|
info!("SelectSources called for session: {:?}", session_handle);
|
||||||
|
|
||||||
// Extract source type from options
|
// Extract source type from options
|
||||||
|
|
@ -154,8 +160,11 @@ impl ScreenCastImpl {
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some((selected_source, selected_target)) = consent else {
|
let Some((selected_source, selected_target)) = consent else {
|
||||||
info!("Screen share request denied/cancelled for session: {}", session_id);
|
info!(
|
||||||
let response: std::collections::HashMap<String, zbus::zvariant::OwnedValue> =
|
"Screen share request denied/cancelled for session: {}",
|
||||||
|
session_id
|
||||||
|
);
|
||||||
|
let response: std::collections::HashMap<String, zbus::OwnedVariant> =
|
||||||
std::collections::HashMap::new();
|
std::collections::HashMap::new();
|
||||||
return Ok((1, response));
|
return Ok((1, response));
|
||||||
};
|
};
|
||||||
|
|
@ -170,10 +179,13 @@ impl ScreenCastImpl {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = self.portal.select_sources(&session_id, request) {
|
if let Err(e) = self.portal.select_sources(&session_id, request) {
|
||||||
return Err(fdo::Error::Failed(format!("Failed to select sources: {}", e)));
|
return Err(fdo::Error::Failed(format!(
|
||||||
|
"Failed to select sources: {}",
|
||||||
|
e
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let response: std::collections::HashMap<String, zbus::zvariant::OwnedValue> =
|
let response: std::collections::HashMap<String, zbus::OwnedVariant> =
|
||||||
std::collections::HashMap::new();
|
std::collections::HashMap::new();
|
||||||
|
|
||||||
Ok((0, response))
|
Ok((0, response))
|
||||||
|
|
@ -183,12 +195,12 @@ impl ScreenCastImpl {
|
||||||
async fn start(
|
async fn start(
|
||||||
&self,
|
&self,
|
||||||
#[zbus(header)] _hdr: zbus::message::Header<'_>,
|
#[zbus(header)] _hdr: zbus::message::Header<'_>,
|
||||||
_handle: zbus::zvariant::ObjectPath<'_>,
|
_handle: zbus::ObjectPath<'_>,
|
||||||
session_handle: zbus::zvariant::ObjectPath<'_>,
|
session_handle: zbus::ObjectPath<'_>,
|
||||||
_app_id: &str,
|
_app_id: &str,
|
||||||
_parent_window: &str,
|
_parent_window: &str,
|
||||||
_options: std::collections::HashMap<String, zbus::zvariant::Value<'_>>,
|
_options: std::collections::HashMap<String, zbus::Variant<'_>>,
|
||||||
) -> fdo::Result<(u32, std::collections::HashMap<String, zbus::zvariant::OwnedValue>)> {
|
) -> fdo::Result<(u32, std::collections::HashMap<String, zbus::OwnedVariant>)> {
|
||||||
info!("Start called for session: {:?}", session_handle);
|
info!("Start called for session: {:?}", session_handle);
|
||||||
|
|
||||||
let external_session_id = session_handle
|
let external_session_id = session_handle
|
||||||
|
|
@ -210,40 +222,59 @@ impl ScreenCastImpl {
|
||||||
|
|
||||||
match self.portal.start_session(&session_id, width, height) {
|
match self.portal.start_session(&session_id, width, height) {
|
||||||
Ok(capture_response) => {
|
Ok(capture_response) => {
|
||||||
use zbus::zvariant::OwnedValue;
|
use zbus::{OwnedVariant, Variant};
|
||||||
|
|
||||||
let mut response: std::collections::HashMap<String, OwnedValue> =
|
let mut response: std::collections::HashMap<String, OwnedVariant> =
|
||||||
std::collections::HashMap::new();
|
std::collections::HashMap::new();
|
||||||
|
let mut streams = Vec::with_capacity(capture_response.streams.len());
|
||||||
|
|
||||||
|
|
||||||
// Log stream activation and frame queueing begins
|
|
||||||
for stream in &capture_response.streams {
|
for stream in &capture_response.streams {
|
||||||
let src_type = match stream.source_type {
|
let src_type = match stream.source_type {
|
||||||
wo::state::portal::SourceType::Monitor => "Monitor",
|
wo::state::portal::SourceType::Monitor => "Monitor",
|
||||||
wo::state::portal::SourceType::Window => "Window",
|
wo::state::portal::SourceType::Window => "Window",
|
||||||
wo::state::portal::SourceType::Virtual => "Virtual",
|
wo::state::portal::SourceType::Virtual => "Virtual",
|
||||||
};
|
};
|
||||||
info!("Activated stream {}: {} [{}x{}] with frame queueing",
|
info!(
|
||||||
stream.node_id, src_type, stream.width, stream.height);
|
"Activated stream {}: {} [{}x{}] with frame queueing",
|
||||||
}
|
stream.node_id, src_type, stream.width, stream.height
|
||||||
|
);
|
||||||
// Return success status - actual streaming happens via frame queueing
|
|
||||||
let stream_count = capture_response.streams.len() as i32;
|
let source_type = match stream.source_type {
|
||||||
response.insert("status".to_string(), OwnedValue::from(0i32));
|
wo::state::portal::SourceType::Monitor => 1u32,
|
||||||
response.insert("stream_count".to_string(), OwnedValue::from(stream_count));
|
wo::state::portal::SourceType::Window => 2u32,
|
||||||
if let Some(first) = capture_response.streams.first() {
|
wo::state::portal::SourceType::Virtual => 4u32,
|
||||||
response.insert("primary_node_id".to_string(), OwnedValue::from(first.node_id as i32));
|
};
|
||||||
}
|
let mut properties = std::collections::HashMap::new();
|
||||||
|
properties.insert(
|
||||||
for stream in &capture_response.streams {
|
"size".to_string(),
|
||||||
info!(" Frame queue for stream {}: 0/{} frames queued", stream.node_id, 30);
|
OwnedVariant::try_from(Variant::new((
|
||||||
|
stream.width as i32,
|
||||||
|
stream.height as i32,
|
||||||
|
)))
|
||||||
|
.map_err(|error| fdo::Error::Failed(error.to_string()))?,
|
||||||
|
);
|
||||||
|
properties.insert("source_type".to_string(), OwnedVariant::from(source_type));
|
||||||
|
if stream.source_type == wo::state::portal::SourceType::Monitor {
|
||||||
|
properties.insert(
|
||||||
|
"position".to_string(),
|
||||||
|
OwnedVariant::try_from(Variant::new((0i32, 0i32)))
|
||||||
|
.map_err(|error| fdo::Error::Failed(error.to_string()))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
streams.push((stream.node_id, properties));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response.insert(
|
||||||
|
"streams".to_string(),
|
||||||
|
OwnedVariant::try_from(Variant::new(streams))
|
||||||
|
.map_err(|error| fdo::Error::Failed(error.to_string()))?,
|
||||||
|
);
|
||||||
Ok((0, response))
|
Ok((0, response))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => Err(fdo::Error::Failed(format!(
|
||||||
Err(fdo::Error::Failed(format!("Failed to start session: {}", e)))
|
"Failed to start session: {}",
|
||||||
}
|
e
|
||||||
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -257,16 +288,9 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
info!("xdg-desktop-portal-wo starting");
|
info!("xdg-desktop-portal-wo starting");
|
||||||
|
|
||||||
// Load config to get compositor IPC socket path
|
// Wo streams the compositor's rendered frames directly into PipeWire.
|
||||||
let config = wo::config::Config::load().unwrap_or_else(|_| {
|
// The binary compositor IPC socket is not a JSON control channel.
|
||||||
info!("Using default config");
|
let portal = Arc::new(WoPortal::new());
|
||||||
wo::config::Config::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create the portal backend with compositor socket
|
|
||||||
let portal = Arc::new(
|
|
||||||
WoPortal::new().with_compositor_socket(config.compositor.ipc_socket.clone())
|
|
||||||
);
|
|
||||||
|
|
||||||
// Connect to the session bus
|
// Connect to the session bus
|
||||||
let connection = Connection::session().await?;
|
let connection = Connection::session().await?;
|
||||||
|
|
@ -289,7 +313,6 @@ async fn main() -> Result<()> {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
info!("Portal service registered as org.freedesktop.impl.portal.desktop.wo");
|
info!("Portal service registered as org.freedesktop.impl.portal.desktop.wo");
|
||||||
info!("Compositor IPC socket: {}", config.compositor.ipc_socket);
|
|
||||||
info!("Listening for screen capture requests...");
|
info!("Listening for screen capture requests...");
|
||||||
|
|
||||||
// Keep the service running
|
// Keep the service running
|
||||||
|
|
|
||||||
163
src/config.rs
163
src/config.rs
|
|
@ -1,7 +1,7 @@
|
||||||
|
use anyhow::{Context, Result};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use anyhow::{Context, Result};
|
|
||||||
|
|
||||||
/// Compositor configuration
|
/// Compositor configuration
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
|
@ -83,7 +83,7 @@ pub struct CompositorConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub applications: Vec<ApplicationConfig>,
|
pub applications: Vec<ApplicationConfig>,
|
||||||
|
|
||||||
/// XCursor theme name (default: "breeze_cursors")
|
/// XCursor theme name (defaults to XCURSOR_THEME, then Adwaita)
|
||||||
#[serde(default = "default_cursor_theme")]
|
#[serde(default = "default_cursor_theme")]
|
||||||
pub cursor_theme: String,
|
pub cursor_theme: String,
|
||||||
|
|
||||||
|
|
@ -92,10 +92,9 @@ pub struct CompositorConfig {
|
||||||
pub cursor_size: u32,
|
pub cursor_size: u32,
|
||||||
|
|
||||||
/// Extra environment variables to set before spawning child processes.
|
/// Extra environment variables to set before spawning child processes.
|
||||||
/// Defaults include DISPLAY=":0" so X11 applications (e.g. Steam) can
|
/// DISPLAY is deliberately not guessed: Wo publishes the actual XWayland
|
||||||
/// fall back to a sensible value when XWayland hasn't started yet.
|
/// display after its server is ready.
|
||||||
/// XWayland will override DISPLAY with the actual value once ready.
|
#[serde(default)]
|
||||||
#[serde(default = "default_environment")]
|
|
||||||
pub environment: HashMap<String, String>,
|
pub environment: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,7 +117,7 @@ impl Default for CompositorConfig {
|
||||||
applications: vec![],
|
applications: vec![],
|
||||||
cursor_theme: default_cursor_theme(),
|
cursor_theme: default_cursor_theme(),
|
||||||
cursor_size: default_cursor_size(),
|
cursor_size: default_cursor_size(),
|
||||||
environment: default_environment(),
|
environment: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +125,10 @@ impl Default for CompositorConfig {
|
||||||
#[derive(Debug, Clone, Deserialize, serde::Serialize)]
|
#[derive(Debug, Clone, Deserialize, serde::Serialize)]
|
||||||
pub struct WindowConfig {
|
pub struct WindowConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// Set internally for compositor-owned root surfaces. Never read from
|
||||||
|
/// user configuration or serialized to the Electron window payload.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub system_shell: bool,
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
pub html: Option<String>,
|
pub html: Option<String>,
|
||||||
pub css: Option<String>,
|
pub css: Option<String>,
|
||||||
|
|
@ -264,9 +267,15 @@ impl ApplicationConfig {
|
||||||
|
|
||||||
// Defaults
|
// Defaults
|
||||||
|
|
||||||
fn default_socket_name() -> String { "wo-0".into() }
|
fn default_socket_name() -> String {
|
||||||
fn default_electron_path() -> String { "electron".into() }
|
"wo-0".into()
|
||||||
fn default_background() -> [f32; 4] { [0.1, 0.1, 0.1, 1.0] }
|
}
|
||||||
|
fn default_electron_path() -> String {
|
||||||
|
std::env::var("WO_ELECTRON_PATH").unwrap_or_else(|_| "electron".into())
|
||||||
|
}
|
||||||
|
fn default_background() -> [f32; 4] {
|
||||||
|
[0.1, 0.1, 0.1, 1.0]
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns $XDG_RUNTIME_DIR, falling back to /run/user/<uid>.
|
/// Returns $XDG_RUNTIME_DIR, falling back to /run/user/<uid>.
|
||||||
fn runtime_dir() -> String {
|
fn runtime_dir() -> String {
|
||||||
|
|
@ -274,23 +283,41 @@ fn runtime_dir() -> String {
|
||||||
.unwrap_or_else(|_| format!("/run/user/{}", unsafe { libc::getuid() }))
|
.unwrap_or_else(|_| format!("/run/user/{}", unsafe { libc::getuid() }))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_ipc_socket() -> String { format!("{}/wo-ipc.sock", runtime_dir()) }
|
fn default_ipc_socket() -> String {
|
||||||
fn default_portal_socket() -> String { format!("{}/wo-portal.sock", runtime_dir()) }
|
format!("{}/wo-ipc.sock", runtime_dir())
|
||||||
fn default_width() -> u32 { 1920 }
|
}
|
||||||
fn default_height() -> u32 { 1080 }
|
fn default_portal_socket() -> String {
|
||||||
fn default_nested_width() -> u32 { 1920 }
|
format!("{}/wo-portal.sock", runtime_dir())
|
||||||
fn default_nested_height() -> u32 { 1080 }
|
}
|
||||||
fn default_fps() -> u32 { 60 }
|
fn default_width() -> u32 {
|
||||||
fn default_format() -> String { "ARGB8888".into() }
|
1920
|
||||||
fn default_asset_prefix() -> String { "file".into() }
|
}
|
||||||
fn default_true() -> bool { true }
|
fn default_height() -> u32 {
|
||||||
fn default_cursor_theme() -> String { "breeze_cursors".into() }
|
1080
|
||||||
fn default_cursor_size() -> u32 { 24 }
|
}
|
||||||
|
fn default_nested_width() -> u32 {
|
||||||
fn default_environment() -> HashMap<String, String> {
|
1920
|
||||||
let mut env = HashMap::new();
|
}
|
||||||
env.insert("DISPLAY".into(), ":0".into());
|
fn default_nested_height() -> u32 {
|
||||||
env
|
1080
|
||||||
|
}
|
||||||
|
fn default_fps() -> u32 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
fn default_format() -> String {
|
||||||
|
"ARGB8888".into()
|
||||||
|
}
|
||||||
|
fn default_asset_prefix() -> String {
|
||||||
|
"file".into()
|
||||||
|
}
|
||||||
|
fn default_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn default_cursor_theme() -> String {
|
||||||
|
std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "Adwaita".into())
|
||||||
|
}
|
||||||
|
fn default_cursor_size() -> u32 {
|
||||||
|
24
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand_path(s: &str) -> String {
|
fn expand_path(s: &str) -> String {
|
||||||
|
|
@ -305,11 +332,13 @@ impl Config {
|
||||||
// 1. WO_CONFIG environment variable
|
// 1. WO_CONFIG environment variable
|
||||||
// 2. ./config.toml (current directory)
|
// 2. ./config.toml (current directory)
|
||||||
// 3. ~/.config/wo/config.toml (user config)
|
// 3. ~/.config/wo/config.toml (user config)
|
||||||
|
// 4. Installed package defaults (Ubuntu/Debian)
|
||||||
|
|
||||||
let paths = vec![
|
let paths = vec![
|
||||||
std::env::var("WO_CONFIG").ok().map(PathBuf::from),
|
std::env::var("WO_CONFIG").ok().map(PathBuf::from),
|
||||||
Some(PathBuf::from("./config.toml")),
|
Some(PathBuf::from("./config.toml")),
|
||||||
Some(config_path()),
|
Some(config_path()),
|
||||||
|
Some(PathBuf::from("/usr/share/doc/wo/config.example.toml")),
|
||||||
];
|
];
|
||||||
|
|
||||||
for path_opt in paths.into_iter().flatten() {
|
for path_opt in paths.into_iter().flatten() {
|
||||||
|
|
@ -320,10 +349,8 @@ impl Config {
|
||||||
|
|
||||||
let mut config: Self = toml::from_str(&text)
|
let mut config: Self = toml::from_str(&text)
|
||||||
.with_context(|| format!("parsing config {path_opt:?}"))?;
|
.with_context(|| format!("parsing config {path_opt:?}"))?;
|
||||||
config.compositor.ipc_socket =
|
config.compositor.ipc_socket = expand_path(&config.compositor.ipc_socket);
|
||||||
expand_path(&config.compositor.ipc_socket);
|
config.compositor.portal_socket = expand_path(&config.compositor.portal_socket);
|
||||||
config.compositor.portal_socket =
|
|
||||||
expand_path(&config.compositor.portal_socket);
|
|
||||||
return Ok(config);
|
return Ok(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -337,16 +364,15 @@ impl Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply configured environment variables to the current process so all
|
/// Apply configured environment variables to the current process so all
|
||||||
/// subsequently-spawned children inherit them. Existing env vars are
|
/// subsequently-spawned children inherit them. Existing env vars are
|
||||||
/// **not** overwritten so that, e.g., a DISPLAY already set by the
|
/// preserved, including the active parent-session DISPLAY.
|
||||||
/// parent session or XWayland takes priority.
|
|
||||||
pub fn apply_environment(&self) {
|
pub fn apply_environment(&self) {
|
||||||
for (key, value) in &self.compositor.environment {
|
for (key, value) in &self.compositor.environment {
|
||||||
if std::env::var(key).is_err() {
|
if std::env::var(key).is_err() {
|
||||||
tracing::info!("Setting environment {key}={value} (from config default)");
|
tracing::info!("Setting environment {key}={value} (from config)");
|
||||||
std::env::set_var(key, value);
|
std::env::set_var(key, value);
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!("Environment {key} already set, skipping config default");
|
tracing::debug!("Environment {key} already set, preserving it");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -357,6 +383,9 @@ fn config_path() -> PathBuf {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dirs_config() -> PathBuf {
|
fn dirs_config() -> PathBuf {
|
||||||
|
if let Some(config_home) = std::env::var_os("XDG_CONFIG_HOME") {
|
||||||
|
return PathBuf::from(config_home).join("wo");
|
||||||
|
}
|
||||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
|
||||||
PathBuf::from(home).join(".config/wo")
|
PathBuf::from(home).join(".config/wo")
|
||||||
}
|
}
|
||||||
|
|
@ -372,3 +401,65 @@ impl Default for Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::Config;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_config_uses_headless_safe_defaults() {
|
||||||
|
let config: Config = toml::from_str("").expect("empty config should deserialize");
|
||||||
|
|
||||||
|
assert!(config.windows.is_empty());
|
||||||
|
assert!(config.root.is_empty());
|
||||||
|
assert!(!config.compositor.enable_portal);
|
||||||
|
assert_eq!(config.compositor.nested_width, 1920);
|
||||||
|
assert_eq!(config.compositor.nested_height, 1080);
|
||||||
|
assert_eq!(config.compositor.socket_name, "wo-0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn window_defaults_and_explicit_portal_options_deserialize() {
|
||||||
|
let config: Config = toml::from_str(
|
||||||
|
r#"
|
||||||
|
[compositor]
|
||||||
|
enable_portal = true
|
||||||
|
nested = true
|
||||||
|
nested_width = 1280
|
||||||
|
nested_height = 720
|
||||||
|
|
||||||
|
[[windows]]
|
||||||
|
name = "overlay"
|
||||||
|
html = "<main>ready</main>"
|
||||||
|
width = 640
|
||||||
|
height = 360
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.expect("valid headless config should deserialize");
|
||||||
|
|
||||||
|
assert!(config.compositor.enable_portal);
|
||||||
|
assert_eq!(config.compositor.nested, Some(true));
|
||||||
|
assert_eq!(config.compositor.nested_width, 1280);
|
||||||
|
assert_eq!(config.compositor.nested_height, 720);
|
||||||
|
assert_eq!(config.windows.len(), 1);
|
||||||
|
let window = &config.windows[0];
|
||||||
|
assert_eq!(window.name, "overlay");
|
||||||
|
assert_eq!(window.width, 640);
|
||||||
|
assert_eq!(window.height, 360);
|
||||||
|
assert!(window.interactive);
|
||||||
|
assert!(!window.focusable);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_window_dimensions_are_rejected() {
|
||||||
|
let error = toml::from_str::<Config>(
|
||||||
|
r#"
|
||||||
|
[[windows]]
|
||||||
|
name = "invalid"
|
||||||
|
width = "wide"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(error.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
216
src/cursor.rs
216
src/cursor.rs
|
|
@ -4,12 +4,12 @@
|
||||||
//! GLES textures on first use, caching the result for subsequent frames.
|
//! GLES textures on first use, caching the result for subsequent frames.
|
||||||
|
|
||||||
use smithay::backend::allocator::Fourcc;
|
use smithay::backend::allocator::Fourcc;
|
||||||
|
use smithay::backend::renderer::ImportMem;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::backend::renderer::gles::GlesTexture;
|
use smithay::backend::renderer::gles::GlesTexture;
|
||||||
use smithay::backend::renderer::ImportMem;
|
|
||||||
use smithay::utils::Size;
|
use smithay::utils::Size;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tracing::{info, warn};
|
use tracing::info;
|
||||||
|
|
||||||
/// Cached cursor texture with its hotspot offset.
|
/// Cached cursor texture with its hotspot offset.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -23,7 +23,7 @@ pub struct CursorImage {
|
||||||
|
|
||||||
/// Manages cursor theme loading and GL texture caching.
|
/// Manages cursor theme loading and GL texture caching.
|
||||||
pub struct CursorThemeManager {
|
pub struct CursorThemeManager {
|
||||||
theme: xcursor::CursorTheme,
|
themes: Vec<xcursor::CursorTheme>,
|
||||||
size: u32,
|
size: u32,
|
||||||
cache: HashMap<String, Option<CursorImage>>,
|
cache: HashMap<String, Option<CursorImage>>,
|
||||||
}
|
}
|
||||||
|
|
@ -31,13 +31,24 @@ pub struct CursorThemeManager {
|
||||||
impl CursorThemeManager {
|
impl CursorThemeManager {
|
||||||
/// Load an XCursor theme by name with the given nominal cursor size.
|
/// Load an XCursor theme by name with the given nominal cursor size.
|
||||||
pub fn new(theme_name: &str, size: u32) -> Self {
|
pub fn new(theme_name: &str, size: u32) -> Self {
|
||||||
info!(
|
let mut names = vec![theme_name.to_owned()];
|
||||||
"Loading cursor theme '{}' (size {})",
|
for name in ["default", "Adwaita", "DMZ-White", "breeze"] {
|
||||||
theme_name, size
|
if !names
|
||||||
);
|
.iter()
|
||||||
let theme = xcursor::CursorTheme::load(theme_name);
|
.any(|candidate| candidate.eq_ignore_ascii_case(name))
|
||||||
|
{
|
||||||
|
names.push(name.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let themes = names
|
||||||
|
.iter()
|
||||||
|
.map(|name| {
|
||||||
|
info!("Loading cursor theme '{}' (size {})", name, size);
|
||||||
|
xcursor::CursorTheme::load(name)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
Self {
|
Self {
|
||||||
theme,
|
themes,
|
||||||
size,
|
size,
|
||||||
cache: HashMap::new(),
|
cache: HashMap::new(),
|
||||||
}
|
}
|
||||||
|
|
@ -47,11 +58,7 @@ impl CursorThemeManager {
|
||||||
///
|
///
|
||||||
/// Returns `None` if the icon cannot be found in the theme or the texture
|
/// Returns `None` if the icon cannot be found in the theme or the texture
|
||||||
/// import fails.
|
/// import fails.
|
||||||
pub fn get_cursor(
|
pub fn get_cursor(&mut self, name: &str, renderer: &mut GlesRenderer) -> Option<&CursorImage> {
|
||||||
&mut self,
|
|
||||||
name: &str,
|
|
||||||
renderer: &mut GlesRenderer,
|
|
||||||
) -> Option<&CursorImage> {
|
|
||||||
if !self.cache.contains_key(name) {
|
if !self.cache.contains_key(name) {
|
||||||
let img = self.load_cursor(name, renderer);
|
let img = self.load_cursor(name, renderer);
|
||||||
self.cache.insert(name.to_string(), img);
|
self.cache.insert(name.to_string(), img);
|
||||||
|
|
@ -59,60 +66,78 @@ impl CursorThemeManager {
|
||||||
self.cache.get(name).and_then(|opt| opt.as_ref())
|
self.cache.get(name).and_then(|opt| opt.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_cursor(
|
fn load_cursor(&self, name: &str, renderer: &mut GlesRenderer) -> Option<CursorImage> {
|
||||||
&self,
|
let icon_path = self
|
||||||
name: &str,
|
.themes
|
||||||
renderer: &mut GlesRenderer,
|
.iter()
|
||||||
) -> Option<CursorImage> {
|
.find_map(|theme| theme.load_icon(name))
|
||||||
let icon_path = self.theme.load_icon(name).or_else(|| {
|
.or_else(|| {
|
||||||
// Try common XCursor aliases
|
// Try common XCursor aliases
|
||||||
let fallback = match name {
|
let fallback = match name {
|
||||||
"default" => Some("left_ptr"),
|
"default" => Some("left_ptr"),
|
||||||
"pointer" => Some("hand2"),
|
"pointer" => Some("hand2"),
|
||||||
"text" => Some("xterm"),
|
"text" => Some("xterm"),
|
||||||
"wait" => Some("watch"),
|
"wait" => Some("watch"),
|
||||||
"progress" => Some("left_ptr_watch"),
|
"progress" => Some("left_ptr_watch"),
|
||||||
"help" => Some("question_arrow"),
|
"help" => Some("question_arrow"),
|
||||||
"crosshair" => Some("cross"),
|
"crosshair" => Some("cross"),
|
||||||
"move" => Some("fleur"),
|
"move" => Some("fleur"),
|
||||||
"not-allowed" => Some("crossed_circle"),
|
"not-allowed" => Some("crossed_circle"),
|
||||||
"no-drop" => Some("crossed_circle"),
|
"no-drop" => Some("crossed_circle"),
|
||||||
"grab" => Some("openhand"),
|
"grab" => Some("openhand"),
|
||||||
"grabbing" => Some("closedhand"),
|
"grabbing" => Some("closedhand"),
|
||||||
"e-resize" => Some("right_side"),
|
"e-resize" => Some("right_side"),
|
||||||
"w-resize" => Some("left_side"),
|
"w-resize" => Some("left_side"),
|
||||||
"n-resize" => Some("top_side"),
|
"n-resize" => Some("top_side"),
|
||||||
"s-resize" => Some("bottom_side"),
|
"s-resize" => Some("bottom_side"),
|
||||||
"ne-resize" => Some("top_right_corner"),
|
"ne-resize" => Some("top_right_corner"),
|
||||||
"nw-resize" => Some("top_left_corner"),
|
"nw-resize" => Some("top_left_corner"),
|
||||||
"se-resize" => Some("bottom_right_corner"),
|
"se-resize" => Some("bottom_right_corner"),
|
||||||
"sw-resize" => Some("bottom_left_corner"),
|
"sw-resize" => Some("bottom_left_corner"),
|
||||||
"ew-resize" => Some("sb_h_double_arrow"),
|
"ew-resize" => Some("sb_h_double_arrow"),
|
||||||
"ns-resize" => Some("sb_v_double_arrow"),
|
"ns-resize" => Some("sb_v_double_arrow"),
|
||||||
"nesw-resize" => Some("fd_double_arrow"),
|
"nesw-resize" => Some("fd_double_arrow"),
|
||||||
"nwse-resize" => Some("bd_double_arrow"),
|
"nwse-resize" => Some("bd_double_arrow"),
|
||||||
"col-resize" => Some("sb_h_double_arrow"),
|
"col-resize" => Some("sb_h_double_arrow"),
|
||||||
"row-resize" => Some("sb_v_double_arrow"),
|
"row-resize" => Some("sb_v_double_arrow"),
|
||||||
"all-resize" | "all-scroll" => Some("fleur"),
|
"all-resize" | "all-scroll" => Some("fleur"),
|
||||||
"context-menu" => Some("left_ptr"),
|
"context-menu" => Some("left_ptr"),
|
||||||
"vertical-text" => Some("xterm"),
|
"vertical-text" => Some("xterm"),
|
||||||
"alias" => Some("link"),
|
"alias" => Some("link"),
|
||||||
"copy" => Some("copy"),
|
"copy" => Some("copy"),
|
||||||
"cell" => Some("plus"),
|
"cell" => Some("plus"),
|
||||||
"zoom-in" => Some("zoom_in"),
|
"zoom-in" => Some("zoom_in"),
|
||||||
"zoom-out" => Some("zoom_out"),
|
"zoom-out" => Some("zoom_out"),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
fallback.and_then(|fb| self.theme.load_icon(fb))
|
fallback.and_then(|fb| self.themes.iter().find_map(|theme| theme.load_icon(fb)))
|
||||||
})?;
|
});
|
||||||
|
|
||||||
let data = std::fs::read(&icon_path).ok()?;
|
// A root DRM session has no host window-system cursor to fall back to.
|
||||||
let images = xcursor::parser::parse_xcursor(&data)?;
|
// Keep a small built-in pointer available when the configured theme is
|
||||||
|
// missing (common in minimal installs) or lacks an individual shape.
|
||||||
|
let Some(icon_path) = icon_path else {
|
||||||
|
info!(
|
||||||
|
"Cursor '{}' is missing from installed themes; using built-in pointer",
|
||||||
|
name
|
||||||
|
);
|
||||||
|
return self.load_builtin_pointer(renderer);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(data) = std::fs::read(&icon_path).ok() else {
|
||||||
|
return self.load_builtin_pointer(renderer);
|
||||||
|
};
|
||||||
|
let Some(images) = xcursor::parser::parse_xcursor(&data) else {
|
||||||
|
return self.load_builtin_pointer(renderer);
|
||||||
|
};
|
||||||
|
|
||||||
// Pick the image closest to our target size.
|
// Pick the image closest to our target size.
|
||||||
let best = images
|
let best = images
|
||||||
.iter()
|
.iter()
|
||||||
.min_by_key(|img| (img.size as i32 - self.size as i32).unsigned_abs())?;
|
.min_by_key(|img| (img.size as i32 - self.size as i32).unsigned_abs());
|
||||||
|
let Some(best) = best else {
|
||||||
|
return self.load_builtin_pointer(renderer);
|
||||||
|
};
|
||||||
|
|
||||||
// XCursor pixels are ARGB (native byte order). Convert to RGBA for
|
// XCursor pixels are ARGB (native byte order). Convert to RGBA for
|
||||||
// GL import with Fourcc::Abgr8888 (which is RGBA in memory on little-endian).
|
// GL import with Fourcc::Abgr8888 (which is RGBA in memory on little-endian).
|
||||||
|
|
@ -147,9 +172,70 @@ impl CursorThemeManager {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to import cursor texture for '{}': {e}", name);
|
info!(
|
||||||
None
|
"Could not import themed cursor '{}': {e}; using built-in pointer",
|
||||||
|
name
|
||||||
|
);
|
||||||
|
self.load_builtin_pointer(renderer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_builtin_pointer(&self, renderer: &mut GlesRenderer) -> Option<CursorImage> {
|
||||||
|
const WIDTH: usize = 16;
|
||||||
|
const HEIGHT: usize = 24;
|
||||||
|
const POLYGON: [(f32, f32); 7] = [
|
||||||
|
(0.5, 0.5),
|
||||||
|
(0.5, 18.0),
|
||||||
|
(4.4, 14.0),
|
||||||
|
(8.2, 23.0),
|
||||||
|
(11.6, 21.3),
|
||||||
|
(7.8, 12.0),
|
||||||
|
(14.0, 12.0),
|
||||||
|
];
|
||||||
|
let inside = |x: f32, y: f32| {
|
||||||
|
let mut hit = false;
|
||||||
|
let mut j = POLYGON.len() - 1;
|
||||||
|
for i in 0..POLYGON.len() {
|
||||||
|
let (xi, yi) = POLYGON[i];
|
||||||
|
let (xj, yj) = POLYGON[j];
|
||||||
|
if (yi > y) != (yj > y) && x < (xj - xi) * (y - yi) / (yj - yi) + xi {
|
||||||
|
hit = !hit;
|
||||||
|
}
|
||||||
|
j = i;
|
||||||
|
}
|
||||||
|
hit
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rgba = vec![0u8; WIDTH * HEIGHT * 4];
|
||||||
|
for y in 0..HEIGHT {
|
||||||
|
for x in 0..WIDTH {
|
||||||
|
if !inside(x as f32 + 0.5, y as f32 + 0.5) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let edge = [(-1.0, 0.0), (1.0, 0.0), (0.0, -1.0), (0.0, 1.0)]
|
||||||
|
.iter()
|
||||||
|
.any(|(dx, dy)| !inside(x as f32 + 0.5 + dx, y as f32 + 0.5 + dy));
|
||||||
|
let value = if edge { 0 } else { 255 };
|
||||||
|
let offset = (y * WIDTH + x) * 4;
|
||||||
|
rgba[offset..offset + 4].copy_from_slice(&[value, value, value, 255]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer
|
||||||
|
.import_memory(
|
||||||
|
&rgba,
|
||||||
|
Fourcc::Abgr8888,
|
||||||
|
Size::from((WIDTH as i32, HEIGHT as i32)),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.map(|texture| CursorImage {
|
||||||
|
texture,
|
||||||
|
width: WIDTH as u32,
|
||||||
|
height: HEIGHT as u32,
|
||||||
|
xhot: 0,
|
||||||
|
yhot: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,18 @@
|
||||||
/// On the export side (serving Wayland clients) the compositor creates a
|
/// On the export side (serving Wayland clients) the compositor creates a
|
||||||
/// `zwp_linux_dmabuf_v1` buffer from the same backing GBM BO so that other
|
/// `zwp_linux_dmabuf_v1` buffer from the same backing GBM BO so that other
|
||||||
/// Wayland clients can access the pixel data zero-copy.
|
/// Wayland clients can access the pixel data zero-copy.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use smithay::backend::{
|
use smithay::backend::{
|
||||||
allocator::{dmabuf::{Dmabuf, DmabufFlags}, Fourcc, Modifier},
|
allocator::{
|
||||||
renderer::{gles::{GlesRenderer, GlesTexture}, ImportDma},
|
dmabuf::{Dmabuf, DmabufFlags},
|
||||||
|
Fourcc, Modifier,
|
||||||
|
},
|
||||||
|
renderer::{
|
||||||
|
gles::{GlesRenderer, GlesTexture},
|
||||||
|
ImportDma,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::electron::ElectronFrame;
|
use crate::electron::ElectronFrame;
|
||||||
|
|
@ -22,12 +27,12 @@ use crate::electron::ElectronFrame;
|
||||||
/// In-flight texture imported from an Electron DMABUF frame.
|
/// In-flight texture imported from an Electron DMABUF frame.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct ElectronTexture {
|
pub struct ElectronTexture {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub texture: GlesTexture,
|
pub texture: GlesTexture,
|
||||||
/// Keep-alive handle so the underlying DMABUF fds stay open.
|
/// Keep-alive handle so the underlying DMABUF fds stay open.
|
||||||
pub dmabuf: Dmabuf,
|
pub dmabuf: Dmabuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cached DMABUF for a window with mutable pixel storage.
|
/// Cached DMABUF for a window with mutable pixel storage.
|
||||||
|
|
@ -55,8 +60,7 @@ impl WindowDmabufCache {
|
||||||
.context("seeking memfd to start")?;
|
.context("seeking memfd to start")?;
|
||||||
|
|
||||||
// Write new pixel data
|
// Write new pixel data
|
||||||
unistd::write(&self.memfd_fd, pixels)
|
unistd::write(&self.memfd_fd, pixels).context("writing pixels to memfd")?;
|
||||||
.context("writing pixels to memfd")?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -65,10 +69,10 @@ impl WindowDmabufCache {
|
||||||
/// Cached DMABUF data for Electron frames (texture is re-imported each frame to avoid
|
/// Cached DMABUF data for Electron frames (texture is re-imported each frame to avoid
|
||||||
/// lifetime issues with GlesRenderer contexts).
|
/// lifetime issues with GlesRenderer contexts).
|
||||||
pub struct CachedDmabufFrame {
|
pub struct CachedDmabufFrame {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub dmabuf: Dmabuf,
|
pub dmabuf: Dmabuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a cached DMABUF for window pixel storage with in-place update capability.
|
/// Create a cached DMABUF for window pixel storage with in-place update capability.
|
||||||
|
|
@ -86,18 +90,16 @@ pub fn create_window_dmabuf_cache(
|
||||||
// Create a memfd for the pixel data
|
// Create a memfd for the pixel data
|
||||||
let memfd_fd = nix::sys::memfd::memfd_create(
|
let memfd_fd = nix::sys::memfd::memfd_create(
|
||||||
std::ffi::CStr::from_bytes_with_nul(b"wayland_window\0").unwrap(),
|
std::ffi::CStr::from_bytes_with_nul(b"wayland_window\0").unwrap(),
|
||||||
nix::sys::memfd::MFdFlags::MFD_CLOEXEC
|
nix::sys::memfd::MFdFlags::MFD_CLOEXEC,
|
||||||
)
|
)
|
||||||
.context("creating memfd for window DMABUF")?;
|
.context("creating memfd for window DMABUF")?;
|
||||||
|
|
||||||
// Truncate to size
|
// Truncate to size
|
||||||
let size = pixels.len() as u64;
|
let size = pixels.len() as u64;
|
||||||
nix::unistd::ftruncate(&memfd_fd, size as i64)
|
nix::unistd::ftruncate(&memfd_fd, size as i64).context("truncating memfd")?;
|
||||||
.context("truncating memfd")?;
|
|
||||||
|
|
||||||
// Write initial pixel data
|
// Write initial pixel data
|
||||||
nix::unistd::write(&memfd_fd, pixels)
|
nix::unistd::write(&memfd_fd, pixels).context("writing initial pixels to memfd")?;
|
||||||
.context("writing initial pixels to memfd")?;
|
|
||||||
|
|
||||||
// Seek back to start for DMABUF builder
|
// Seek back to start for DMABUF builder
|
||||||
nix::unistd::lseek(&memfd_fd, 0, nix::unistd::Whence::SeekSet)
|
nix::unistd::lseek(&memfd_fd, 0, nix::unistd::Whence::SeekSet)
|
||||||
|
|
@ -138,8 +140,13 @@ pub fn import_electron_frame(
|
||||||
) -> Result<(ElectronTexture, CachedDmabufFrame)> {
|
) -> Result<(ElectronTexture, CachedDmabufFrame)> {
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
trace!("Importing DMABUF frame: {}x{} format=0x{:08X} planes={}",
|
trace!(
|
||||||
frame.width, frame.height, frame.format, frame.planes.len());
|
"Importing DMABUF frame: {}x{} format=0x{:08X} planes={}",
|
||||||
|
frame.width,
|
||||||
|
frame.height,
|
||||||
|
frame.format,
|
||||||
|
frame.planes.len()
|
||||||
|
);
|
||||||
|
|
||||||
let fourcc: Fourcc = fourcc_from_u32(frame.format)
|
let fourcc: Fourcc = fourcc_from_u32(frame.format)
|
||||||
.with_context(|| format!("unknown fourcc 0x{:08X}", frame.format))?;
|
.with_context(|| format!("unknown fourcc 0x{:08X}", frame.format))?;
|
||||||
|
|
@ -169,7 +176,12 @@ pub fn import_electron_frame(
|
||||||
debug!("Failed to add plane {} to DMABUF", idx);
|
debug!("Failed to add plane {} to DMABUF", idx);
|
||||||
anyhow::bail!("too many planes in DMABUF frame");
|
anyhow::bail!("too many planes in DMABUF frame");
|
||||||
}
|
}
|
||||||
trace!("Plane {}: offset={} stride={}", idx, plane.offset, plane.stride);
|
trace!(
|
||||||
|
"Plane {}: offset={} stride={}",
|
||||||
|
idx,
|
||||||
|
plane.offset,
|
||||||
|
plane.stride
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let dmabuf = builder.build().context("building DMABUF")?;
|
let dmabuf = builder.build().context("building DMABUF")?;
|
||||||
|
|
@ -193,8 +205,8 @@ pub fn import_electron_frame(
|
||||||
dmabuf,
|
dmabuf,
|
||||||
},
|
},
|
||||||
CachedDmabufFrame {
|
CachedDmabufFrame {
|
||||||
name: frame.name,
|
name: frame.name,
|
||||||
width: frame.width,
|
width: frame.width,
|
||||||
height: frame.height,
|
height: frame.height,
|
||||||
dmabuf: cache_dmabuf,
|
dmabuf: cache_dmabuf,
|
||||||
},
|
},
|
||||||
|
|
@ -232,20 +244,16 @@ pub fn create_temp_dmabuf_from_pixels(
|
||||||
use smithay::backend::allocator::Modifier;
|
use smithay::backend::allocator::Modifier;
|
||||||
|
|
||||||
// Create a memfd for the pixel data
|
// Create a memfd for the pixel data
|
||||||
let memfd_fd = nix::sys::memfd::memfd_create(
|
let memfd_fd =
|
||||||
"wayland_window",
|
nix::sys::memfd::memfd_create("wayland_window", nix::sys::memfd::MFdFlags::MFD_CLOEXEC)
|
||||||
nix::sys::memfd::MFdFlags::MFD_CLOEXEC
|
.context("creating memfd for DMABUF pixels")?;
|
||||||
)
|
|
||||||
.context("creating memfd for DMABUF pixels")?;
|
|
||||||
|
|
||||||
// Truncate to size
|
// Truncate to size
|
||||||
let size = pixels.len() as u64;
|
let size = pixels.len() as u64;
|
||||||
nix::unistd::ftruncate(&memfd_fd, size as i64)
|
nix::unistd::ftruncate(&memfd_fd, size as i64).context("truncating memfd")?;
|
||||||
.context("truncating memfd")?;
|
|
||||||
|
|
||||||
// Write pixel data directly using nix::unistd::write (no File wrapper)
|
// Write pixel data directly using nix::unistd::write (no File wrapper)
|
||||||
nix::unistd::write(&memfd_fd, pixels)
|
nix::unistd::write(&memfd_fd, pixels).context("writing pixels to memfd")?;
|
||||||
.context("writing pixels to memfd")?;
|
|
||||||
|
|
||||||
// Seek back to start
|
// Seek back to start
|
||||||
nix::unistd::lseek(&memfd_fd, 0, nix::unistd::Whence::SeekSet)
|
nix::unistd::lseek(&memfd_fd, 0, nix::unistd::Whence::SeekSet)
|
||||||
|
|
@ -265,8 +273,7 @@ pub fn create_temp_dmabuf_from_pixels(
|
||||||
builder.build().context("building DMABUF from pixels")
|
builder.build().context("building DMABUF from pixels")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Export an offscreen texture as a DMABUF for zero-copy transmission.
|
/// Direct EGL texture export is currently unsupported.
|
||||||
/// Used to export Wayland window textures to comraw via IPC using individual DMABUFs.
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn export_texture_as_dmabuf(
|
pub fn export_texture_as_dmabuf(
|
||||||
_renderer: &GlesRenderer,
|
_renderer: &GlesRenderer,
|
||||||
|
|
@ -274,11 +281,6 @@ pub fn export_texture_as_dmabuf(
|
||||||
_width: u32,
|
_width: u32,
|
||||||
_height: u32,
|
_height: u32,
|
||||||
) -> Result<Dmabuf> {
|
) -> Result<Dmabuf> {
|
||||||
// For now, we'll read pixels and create a DMABUF from them
|
|
||||||
// A more optimal approach would use EGLImage to back the DMABUF, but
|
|
||||||
// this is simpler and still zero-copy at the IPC boundary
|
|
||||||
// This would need a mutable reference, which we don't have
|
|
||||||
// So instead, for each window we'll create DMABUF on-demand after pixelreads
|
|
||||||
anyhow::bail!("texture export not directly supported; use create_temp_dmabuf_from_pixels after reading pixels")
|
anyhow::bail!("texture export not directly supported; use create_temp_dmabuf_from_pixels after reading pixels")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -318,11 +320,11 @@ fn fourcc_from_u32(v: u32) -> Option<Fourcc> {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn format_str_to_fourcc(s: &str) -> Option<u32> {
|
pub fn format_str_to_fourcc(s: &str) -> Option<u32> {
|
||||||
match s.to_uppercase().as_str() {
|
match s.to_uppercase().as_str() {
|
||||||
"ARGB8888" => Some(u32::from_le_bytes(*b"AR24")), // DRM_FORMAT_ARGB8888
|
"ARGB8888" => Some(u32::from_le_bytes(*b"AR24")), // DRM_FORMAT_ARGB8888
|
||||||
"BGRA8888" => Some(u32::from_le_bytes(*b"BA24")), // DRM_FORMAT_BGRA8888
|
"BGRA8888" => Some(u32::from_le_bytes(*b"BA24")), // DRM_FORMAT_BGRA8888
|
||||||
"XRGB8888" => Some(u32::from_le_bytes(*b"XR24")), // DRM_FORMAT_XRGB8888
|
"XRGB8888" => Some(u32::from_le_bytes(*b"XR24")), // DRM_FORMAT_XRGB8888
|
||||||
"ABGR8888" => Some(u32::from_le_bytes(*b"AB24")), // DRM_FORMAT_ABGR8888
|
"ABGR8888" => Some(u32::from_le_bytes(*b"AB24")), // DRM_FORMAT_ABGR8888
|
||||||
"XBGR8888" => Some(u32::from_le_bytes(*b"XB24")), // DRM_FORMAT_XBGR8888
|
"XBGR8888" => Some(u32::from_le_bytes(*b"XB24")), // DRM_FORMAT_XBGR8888
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
688
src/electron.rs
688
src/electron.rs
File diff suppressed because it is too large
Load diff
|
|
@ -28,11 +28,16 @@ impl CompositorHandler for WoState {
|
||||||
if let Some(data) = client.get_data::<crate::state::ClientData>() {
|
if let Some(data) = client.get_data::<crate::state::ClientData>() {
|
||||||
return &data.compositor_state;
|
return &data.compositor_state;
|
||||||
}
|
}
|
||||||
static LOGGED_ONCE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
static LOGGED_ONCE: std::sync::atomic::AtomicBool =
|
||||||
|
std::sync::atomic::AtomicBool::new(false);
|
||||||
if !LOGGED_ONCE.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
if !LOGGED_ONCE.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
||||||
tracing::info!("Client missing ClientData (likely XWayland), using fallback compositor state");
|
tracing::info!(
|
||||||
|
"Client missing ClientData (likely XWayland), using fallback compositor state"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!("Client missing ClientData (likely XWayland), using fallback compositor state");
|
tracing::debug!(
|
||||||
|
"Client missing ClientData (likely XWayland), using fallback compositor state"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
static FALLBACK: std::sync::OnceLock<CompositorClientState> = std::sync::OnceLock::new();
|
static FALLBACK: std::sync::OnceLock<CompositorClientState> = std::sync::OnceLock::new();
|
||||||
FALLBACK.get_or_init(CompositorClientState::default)
|
FALLBACK.get_or_init(CompositorClientState::default)
|
||||||
|
|
@ -52,7 +57,11 @@ impl CompositorHandler for WoState {
|
||||||
if let Some(window) = self
|
if let Some(window) = self
|
||||||
.space
|
.space
|
||||||
.elements()
|
.elements()
|
||||||
.find(|w| w.toplevel().map(|t| t.wl_surface() == &root).unwrap_or(false))
|
.find(|w| {
|
||||||
|
w.toplevel()
|
||||||
|
.map(|t| t.wl_surface() == &root)
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
{
|
{
|
||||||
window.on_commit();
|
window.on_commit();
|
||||||
|
|
@ -71,11 +80,14 @@ impl CompositorHandler for WoState {
|
||||||
window.on_commit();
|
window.on_commit();
|
||||||
self.dirty_surfaces.insert(root.clone());
|
self.dirty_surfaces.insert(root.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.popup_manager.commit(surface);
|
self.popup_manager.commit(surface);
|
||||||
crate::handlers::xdg_shell::ensure_initial_configure(surface, &self.space, &mut self.popup_manager);
|
crate::handlers::xdg_shell::ensure_initial_configure(
|
||||||
|
surface,
|
||||||
|
&self.space,
|
||||||
|
&mut self.popup_manager,
|
||||||
|
);
|
||||||
if self.debug_new_toplevel {
|
if self.debug_new_toplevel {
|
||||||
tracing::info!("commit handler EXIT");
|
tracing::info!("commit handler EXIT");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::allocator::dmabuf::Dmabuf,
|
backend::allocator::dmabuf::Dmabuf,
|
||||||
delegate_dmabuf,
|
delegate_dmabuf,
|
||||||
wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, DmabufFeedback, ImportNotifier}
|
wayland::dmabuf::{DmabufFeedback, DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier},
|
||||||
};
|
};
|
||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
|
||||||
|
|
||||||
use crate::state::WoState;
|
use crate::state::WoState;
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ impl DmabufHandler for WoState {
|
||||||
_dmabuf: Dmabuf,
|
_dmabuf: Dmabuf,
|
||||||
notifier: ImportNotifier,
|
notifier: ImportNotifier,
|
||||||
) {
|
) {
|
||||||
tracing::info!("dmabuf_imported: accepting DMABUF unconditionally");
|
tracing::debug!("dmabuf_imported: accepting renderer-advertised DMABUF");
|
||||||
// Frames from Electron arrive via the custom IPC channel, not via the
|
// Frames from Electron arrive via the custom IPC channel, not via the
|
||||||
// Wayland DMABUF protocol. However, native Wayland clients (and Electron's
|
// Wayland DMABUF protocol. However, native Wayland clients (and Electron's
|
||||||
// own GPU subprocess when it connects as a Wayland client) import DMABUFs
|
// own GPU subprocess when it connects as a Wayland client) import DMABUFs
|
||||||
|
|
@ -29,7 +29,6 @@ impl DmabufHandler for WoState {
|
||||||
// failure response to the client, preventing it from creating DMABUF-backed
|
// failure response to the client, preventing it from creating DMABUF-backed
|
||||||
// wl_buffers entirely and causing the app (and rendering) to freeze.
|
// wl_buffers entirely and causing the app (and rendering) to freeze.
|
||||||
let _ = notifier.successful::<WoState>();
|
let _ = notifier.successful::<WoState>();
|
||||||
tracing::info!("dmabuf_imported: done");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_surface_feedback(
|
fn new_surface_feedback(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use smithay::{
|
||||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
|
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
|
||||||
KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
|
KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
|
||||||
},
|
},
|
||||||
|
desktop::WindowSurfaceType,
|
||||||
input::keyboard::FilterResult,
|
input::keyboard::FilterResult,
|
||||||
utils::SERIAL_COUNTER,
|
utils::SERIAL_COUNTER,
|
||||||
};
|
};
|
||||||
|
|
@ -155,11 +156,8 @@ impl WoState {
|
||||||
let delta = (event.delta_x(), event.delta_y());
|
let delta = (event.delta_x(), event.delta_y());
|
||||||
|
|
||||||
let current = self.pointer_location;
|
let current = self.pointer_location;
|
||||||
let new_pos: smithay::utils::Point<f64, smithay::utils::Logical> = (
|
let new_pos: smithay::utils::Point<f64, smithay::utils::Logical> =
|
||||||
(current.x + delta.0).clamp(0.0, self.output_size.0 as f64),
|
self.clamp_pointer_to_monitors((current.x + delta.0, current.y + delta.1).into());
|
||||||
(current.y + delta.1).clamp(0.0, self.output_size.1 as f64),
|
|
||||||
)
|
|
||||||
.into();
|
|
||||||
|
|
||||||
// Hardware is the sole source of truth for pointer position.
|
// Hardware is the sole source of truth for pointer position.
|
||||||
self.pointer_location = new_pos;
|
self.pointer_location = new_pos;
|
||||||
|
|
@ -241,11 +239,7 @@ impl WoState {
|
||||||
.unwrap_or(initial_window_location);
|
.unwrap_or(initial_window_location);
|
||||||
self.space
|
self.space
|
||||||
.map_element(window.clone(), (new_x, new_y), false);
|
.map_element(window.clone(), (new_x, new_y), false);
|
||||||
self.translate_dialog_children(
|
self.translate_dialog_children(&window, new_x - prev_loc.x, new_y - prev_loc.y);
|
||||||
&window,
|
|
||||||
new_x - prev_loc.x,
|
|
||||||
new_y - prev_loc.y,
|
|
||||||
);
|
|
||||||
self.metadata_dirty = true;
|
self.metadata_dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -256,9 +250,16 @@ impl WoState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_pointer_motion_abs<B: InputBackend>(&mut self, event: B::PointerMotionAbsoluteEvent) {
|
fn on_pointer_motion_abs<B: InputBackend>(&mut self, event: B::PointerMotionAbsoluteEvent) {
|
||||||
let new_pos: smithay::utils::Point<f64, smithay::utils::Logical> = event
|
let bounds = self.desktop_bounds();
|
||||||
.position_transformed((self.output_size.0 as i32, self.output_size.1 as i32).into())
|
let transformed = event.position_transformed(bounds.size);
|
||||||
.into();
|
let new_pos: smithay::utils::Point<f64, smithay::utils::Logical> = self
|
||||||
|
.clamp_pointer_to_monitors(
|
||||||
|
(
|
||||||
|
transformed.x + bounds.loc.x as f64,
|
||||||
|
transformed.y + bounds.loc.y as f64,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
// Hardware is the sole source of truth for pointer position.
|
// Hardware is the sole source of truth for pointer position.
|
||||||
self.pointer_location = new_pos;
|
self.pointer_location = new_pos;
|
||||||
|
|
@ -339,11 +340,7 @@ impl WoState {
|
||||||
.unwrap_or(initial_window_location);
|
.unwrap_or(initial_window_location);
|
||||||
self.space
|
self.space
|
||||||
.map_element(window.clone(), (new_x, new_y), false);
|
.map_element(window.clone(), (new_x, new_y), false);
|
||||||
self.translate_dialog_children(
|
self.translate_dialog_children(&window, new_x - prev_loc.x, new_y - prev_loc.y);
|
||||||
&window,
|
|
||||||
new_x - prev_loc.x,
|
|
||||||
new_y - prev_loc.y,
|
|
||||||
);
|
|
||||||
self.metadata_dirty = true;
|
self.metadata_dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -405,12 +402,16 @@ impl WoState {
|
||||||
let local_x = self.pointer_location.x - win_loc.x as f64;
|
let local_x = self.pointer_location.x - win_loc.x as f64;
|
||||||
let local_y = self.pointer_location.y - win_loc.y as f64;
|
let local_y = self.pointer_location.y - win_loc.y as f64;
|
||||||
window
|
window
|
||||||
.surface_under(Point::<f64, Logical>::from((local_x, local_y)), WindowSurfaceType::ALL)
|
.surface_under(
|
||||||
|
Point::<f64, Logical>::from((local_x, local_y)),
|
||||||
|
WindowSurfaceType::ALL,
|
||||||
|
)
|
||||||
.map(|(s, offset)| {
|
.map(|(s, offset)| {
|
||||||
let surface_global: Point<f64, Logical> = (
|
let surface_global: Point<f64, Logical> = (
|
||||||
win_loc.x as f64 + offset.x as f64,
|
win_loc.x as f64 + offset.x as f64,
|
||||||
win_loc.y as f64 + offset.y as f64,
|
win_loc.y as f64 + offset.y as f64,
|
||||||
).into();
|
)
|
||||||
|
.into();
|
||||||
(s, surface_global)
|
(s, surface_global)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
@ -452,7 +453,9 @@ impl WoState {
|
||||||
// pointer_button action will arrive shortly and set the
|
// pointer_button action will arrive shortly and set the
|
||||||
// correct Wayland surface focus. Clearing it here would
|
// correct Wayland surface focus. Clearing it here would
|
||||||
// cause focus to bounce (clear → set → clear → set).
|
// cause focus to bounce (clear → set → clear → set).
|
||||||
let has_native_focus = self.seat.get_keyboard()
|
let has_native_focus = self
|
||||||
|
.seat
|
||||||
|
.get_keyboard()
|
||||||
.map(|kb| kb.current_focus().is_some())
|
.map(|kb| kb.current_focus().is_some())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if !has_native_focus {
|
if !has_native_focus {
|
||||||
|
|
@ -488,17 +491,6 @@ impl WoState {
|
||||||
let keyboard = self.seat.get_keyboard().unwrap();
|
let keyboard = self.seat.get_keyboard().unwrap();
|
||||||
keyboard.set_focus(self, Some(surface.clone()), serial);
|
keyboard.set_focus(self, Some(surface.clone()), serial);
|
||||||
self.keyboard_window_focus = None;
|
self.keyboard_window_focus = None;
|
||||||
|
|
||||||
let pointer = self.seat.get_pointer().unwrap();
|
|
||||||
pointer.button(
|
|
||||||
self,
|
|
||||||
&smithay::input::pointer::ButtonEvent {
|
|
||||||
button: button_code,
|
|
||||||
state: button_state,
|
|
||||||
serial,
|
|
||||||
time,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Clicked empty space: clear focus.
|
// Clicked empty space: clear focus.
|
||||||
|
|
@ -507,9 +499,61 @@ impl WoState {
|
||||||
keyboard.set_focus(self, None, serial);
|
keyboard.set_focus(self, None, serial);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(pointer) = self.seat.get_pointer() {
|
||||||
|
pointer.button(
|
||||||
|
self,
|
||||||
|
&smithay::input::pointer::ButtonEvent {
|
||||||
|
button: button_code,
|
||||||
|
state: button_state,
|
||||||
|
serial,
|
||||||
|
time,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pointer.frame(self);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if button_state == ButtonState::Pressed {
|
||||||
|
use smithay::utils::{Logical, Point};
|
||||||
|
let pos: Point<f64, Logical> = self.pointer_location;
|
||||||
|
let under_surface = self.space.element_under(pos);
|
||||||
|
|
||||||
|
if let Some((window, _)) = under_surface {
|
||||||
|
let surface = window
|
||||||
|
.x11_surface()
|
||||||
|
.and_then(|x11| x11.wl_surface())
|
||||||
|
.or_else(|| {
|
||||||
|
window
|
||||||
|
.toplevel()
|
||||||
|
.map(|toplevel| toplevel.wl_surface().clone())
|
||||||
|
});
|
||||||
|
if let (Some(surface), Some(keyboard)) = (surface, self.seat.get_keyboard()) {
|
||||||
|
keyboard.set_focus(self, Some(surface), serial);
|
||||||
|
self.keyboard_window_focus = None;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.keyboard_window_focus = None;
|
||||||
|
if let Some(keyboard) = self.seat.get_keyboard() {
|
||||||
|
keyboard.set_focus(self, None, serial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pointer) = self.seat.get_pointer() {
|
||||||
|
pointer.button(
|
||||||
|
self,
|
||||||
|
&smithay::input::pointer::ButtonEvent {
|
||||||
|
button: button_code,
|
||||||
|
state: button_state,
|
||||||
|
serial,
|
||||||
|
time,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pointer.frame(self);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_pointer_axis<B: InputBackend>(&mut self, event: B::PointerAxisEvent) {
|
fn on_pointer_axis<B: InputBackend>(&mut self, event: B::PointerAxisEvent) {
|
||||||
|
|
@ -539,17 +583,54 @@ impl WoState {
|
||||||
if vertical != 0 || horizontal != 0 {
|
if vertical != 0 || horizontal != 0 {
|
||||||
if let Some(client) = ipc.clients.lock().unwrap().get(win_name).cloned() {
|
if let Some(client) = ipc.clients.lock().unwrap().get(win_name).cloned() {
|
||||||
// Non-blocking send.
|
// Non-blocking send.
|
||||||
let _ = client.try_send_input_event(&ElectronInputEvent::Scroll {
|
if client
|
||||||
vertical,
|
.try_send_input_event(&ElectronInputEvent::Scroll {
|
||||||
horizontal,
|
vertical,
|
||||||
time,
|
horizontal,
|
||||||
});
|
time,
|
||||||
|
})
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.dispatch_native_pointer_axis::<B>(event, time);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.dispatch_native_pointer_axis::<B>(event, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_native_pointer_axis<B: InputBackend>(
|
||||||
|
&mut self,
|
||||||
|
event: B::PointerAxisEvent,
|
||||||
|
time: u32,
|
||||||
|
) {
|
||||||
|
use smithay::input::pointer::AxisFrame;
|
||||||
|
|
||||||
|
let Some(pointer) = self.seat.get_pointer() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut frame = AxisFrame::new(time).source(event.source());
|
||||||
|
for axis in [Axis::Horizontal, Axis::Vertical] {
|
||||||
|
if let Some(value) = event.amount(axis) {
|
||||||
|
frame = frame.value(axis, value);
|
||||||
|
}
|
||||||
|
if let Some(v120) = event.amount_v120(axis) {
|
||||||
|
frame = frame.v120(axis, v120.round() as i32);
|
||||||
|
if event.amount(axis).is_none() {
|
||||||
|
frame = frame.value(axis, v120 / 120.0 * 15.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frame = frame.relative_direction(axis, event.relative_direction(axis));
|
||||||
|
}
|
||||||
|
|
||||||
|
pointer.axis(self, frame);
|
||||||
|
pointer.frame(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forward a pointer motion event from the client (comraw/Electron) to the
|
/// Forward a pointer motion event from the client (comraw/Electron) to the
|
||||||
|
|
@ -579,21 +660,29 @@ impl WoState {
|
||||||
let win_loc = self.space.element_location(&window).unwrap_or_default();
|
let win_loc = self.space.element_location(&window).unwrap_or_default();
|
||||||
|
|
||||||
// Compute a synthetic global location from the client-provided
|
// Compute a synthetic global location from the client-provided
|
||||||
// surface-local coords. This is used ONLY for the MotionEvent so
|
// client-area coords. This is used only for MotionEvent so Smithay
|
||||||
// Smithay derives the correct wl_pointer surface-local coordinates.
|
// derives the correct wl_pointer surface-local coordinates.
|
||||||
// pointer_location itself is NOT modified.
|
// pointer_location itself is NOT modified.
|
||||||
let dispatch_location: Point<f64, Logical> = (
|
// The shell reports coordinates relative to the visible surface area.
|
||||||
win_loc.x as f64 + x,
|
// Convert them to window-element coordinates; geometry.loc marks the
|
||||||
win_loc.y as f64 + y,
|
// content inset inside that element (for example client shadow pixels).
|
||||||
).into();
|
let geometry_offset = window.geometry().loc;
|
||||||
|
let surface_x = x + geometry_offset.x as f64;
|
||||||
|
let surface_y = y + geometry_offset.y as f64;
|
||||||
|
let dispatch_location: Point<f64, Logical> =
|
||||||
|
(win_loc.x as f64 + surface_x, win_loc.y as f64 + surface_y).into();
|
||||||
|
|
||||||
let focus = window
|
let focus = window
|
||||||
.surface_under(Point::<f64, Logical>::from((x, y)), WindowSurfaceType::ALL)
|
.surface_under(
|
||||||
|
Point::<f64, Logical>::from((surface_x, surface_y)),
|
||||||
|
WindowSurfaceType::ALL,
|
||||||
|
)
|
||||||
.map(|(s, offset)| {
|
.map(|(s, offset)| {
|
||||||
let focus_pos: Point<f64, Logical> = (
|
let focus_pos: Point<f64, Logical> = (
|
||||||
win_loc.x as f64 + offset.x as f64,
|
win_loc.x as f64 + offset.x as f64,
|
||||||
win_loc.y as f64 + offset.y as f64,
|
win_loc.y as f64 + offset.y as f64,
|
||||||
).into();
|
)
|
||||||
|
.into();
|
||||||
(s, focus_pos)
|
(s, focus_pos)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -757,19 +846,32 @@ impl WoState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forward a pointer scroll event from the web UI.
|
/// Forward a pointer scroll event from the web UI.
|
||||||
pub fn handle_forwarded_pointer_scroll(&mut self, window_name: &str, dx: f64, dy: f64) {
|
pub fn handle_forwarded_pointer_scroll(
|
||||||
|
&mut self,
|
||||||
|
window_name: &str,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
dx: f64,
|
||||||
|
dy: f64,
|
||||||
|
time: u32,
|
||||||
|
) {
|
||||||
use smithay::input::pointer::AxisFrame;
|
use smithay::input::pointer::AxisFrame;
|
||||||
|
|
||||||
if self.find_wayland_window(window_name).is_none() {
|
if self.find_wayland_window(window_name).is_none() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Position the Wayland pointer from the same wheel event. Keeping
|
||||||
|
// motion+axis in one IPC message prevents a wheel from arriving while
|
||||||
|
// Smithay still has focus on the previous surface.
|
||||||
|
self.handle_forwarded_pointer_event(window_name, x, y);
|
||||||
|
|
||||||
let pointer = match self.seat.get_pointer() {
|
let pointer = match self.seat.get_pointer() {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut frame = AxisFrame::new(0).source(AxisSource::Wheel);
|
let mut frame = AxisFrame::new(time).source(AxisSource::Wheel);
|
||||||
if dx != 0.0 {
|
if dx != 0.0 {
|
||||||
frame = frame.value(Axis::Horizontal, dx);
|
frame = frame.value(Axis::Horizontal, dx);
|
||||||
}
|
}
|
||||||
|
|
@ -780,30 +882,83 @@ impl WoState {
|
||||||
pointer.frame(self);
|
pointer.frame(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// End forwarded pointer focus after the web shell leaves a native window.
|
||||||
|
pub fn handle_forwarded_pointer_leave(&mut self, window_name: &str) {
|
||||||
|
let Some(window) = self.find_wayland_window(window_name) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(pointer) = self.seat.get_pointer() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let surface = window
|
||||||
|
.toplevel()
|
||||||
|
.map(|toplevel| toplevel.wl_surface().clone())
|
||||||
|
.or_else(|| window.x11_surface().and_then(|x11| x11.wl_surface()));
|
||||||
|
let pointer_locked = surface.as_ref().is_some_and(|surface| {
|
||||||
|
smithay::wayland::pointer_constraints::with_pointer_constraint(
|
||||||
|
surface,
|
||||||
|
&pointer,
|
||||||
|
|constraint| {
|
||||||
|
constraint.is_some_and(|constraint| {
|
||||||
|
matches!(
|
||||||
|
&*constraint,
|
||||||
|
smithay::wayland::pointer_constraints::PointerConstraint::Locked(_)
|
||||||
|
) && constraint.is_active()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let serial = SERIAL_COUNTER.next_serial();
|
||||||
|
pointer.motion(
|
||||||
|
self,
|
||||||
|
None,
|
||||||
|
&smithay::input::pointer::MotionEvent {
|
||||||
|
location: self.pointer_location,
|
||||||
|
serial,
|
||||||
|
time: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pointer.frame(self);
|
||||||
|
// X11/Wayland applications can leave the shared cursor set to Hidden
|
||||||
|
// after the shell retakes pointer dispatch. Restore the normal desktop
|
||||||
|
// cursor unless a client still owns an active pointer lock.
|
||||||
|
if !pointer_locked {
|
||||||
|
self.cursor_status = smithay::input::pointer::CursorImageStatus::default_named();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Forward relative motion received from the web UI to the Wayland surface.
|
/// Forward relative motion received from the web UI to the Wayland surface.
|
||||||
pub fn handle_forwarded_relative_pointer_event(&mut self, window_name: &str, dx: f64, dy: f64) {
|
pub fn handle_forwarded_relative_pointer_event(&mut self, window_name: &str, dx: f64, dy: f64) {
|
||||||
use smithay::input::pointer::RelativeMotionEvent;
|
use smithay::input::pointer::RelativeMotionEvent;
|
||||||
|
use smithay::utils::{Logical, Point};
|
||||||
|
|
||||||
let window = match self.find_wayland_window(window_name) {
|
let window = match self.find_wayland_window(window_name) {
|
||||||
Some(w) => w,
|
Some(w) => w,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let surface = window
|
let Some(pointer) = self.seat.get_pointer() else {
|
||||||
.toplevel()
|
return;
|
||||||
.map(|t| t.wl_surface().clone())
|
};
|
||||||
.or_else(|| window.x11_surface().and_then(|x| x.wl_surface().clone()));
|
let Some(window_location) = self.space.element_location(&window) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let pointer_in_window: Point<f64, Logical> =
|
||||||
|
self.pointer_location - window_location.to_f64() + window.geometry().loc.to_f64();
|
||||||
|
let Some((surface, surface_offset)) =
|
||||||
|
window.surface_under(pointer_in_window, WindowSurfaceType::ALL)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(surface) = surface {
|
let surface_origin: Point<f64, Logical> =
|
||||||
if let Some(pointer) = self.seat.get_pointer() {
|
window_location.to_f64() + surface_offset.to_f64();
|
||||||
let event = RelativeMotionEvent {
|
let event = RelativeMotionEvent {
|
||||||
utime: 0,
|
utime: 0,
|
||||||
delta: (dx, dy).into(),
|
delta: (dx, dy).into(),
|
||||||
delta_unaccel: (dx, dy).into(),
|
delta_unaccel: (dx, dy).into(),
|
||||||
};
|
};
|
||||||
pointer.relative_motion(self, Some((surface, (0.0, 0.0).into())), &event);
|
pointer.relative_motion(self, Some((surface, surface_origin)), &event);
|
||||||
pointer.frame(self);
|
pointer.frame(self);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,15 @@ impl WoState {
|
||||||
let output = Output::new(
|
let output = Output::new(
|
||||||
"wo-output".to_string(),
|
"wo-output".to_string(),
|
||||||
PhysicalProperties {
|
PhysicalProperties {
|
||||||
size: (0, 0).into(),
|
size: (0, 0).into(),
|
||||||
subpixel: Subpixel::Unknown,
|
subpixel: Subpixel::Unknown,
|
||||||
make: "Wo".into(),
|
make: "Wo".into(),
|
||||||
model: "Virtual".into(),
|
model: "Virtual".into(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let mode = smithay::output::Mode {
|
let mode = smithay::output::Mode {
|
||||||
size: (width, height).into(),
|
size: (width, height).into(),
|
||||||
refresh: refresh_mhz,
|
refresh: refresh_mhz,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
///
|
///
|
||||||
/// Implements `zwlr_screencopy_manager_v1` allowing clients like grim, OBS, etc.
|
/// Implements `zwlr_screencopy_manager_v1` allowing clients like grim, OBS, etc.
|
||||||
/// to request frame copies of outputs or output regions.
|
/// to request frame copies of outputs or output regions.
|
||||||
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
use wayland_protocols_wlr::screencopy::v1::server::{
|
use wayland_protocols_wlr::screencopy::v1::server::{
|
||||||
|
|
@ -10,8 +9,8 @@ use wayland_protocols_wlr::screencopy::v1::server::{
|
||||||
zwlr_screencopy_manager_v1::{self, ZwlrScreencopyManagerV1},
|
zwlr_screencopy_manager_v1::{self, ZwlrScreencopyManagerV1},
|
||||||
};
|
};
|
||||||
use wayland_server::{
|
use wayland_server::{
|
||||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
|
||||||
protocol::{wl_buffer::WlBuffer, wl_shm},
|
protocol::{wl_buffer::WlBuffer, wl_shm},
|
||||||
|
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::state::WoState;
|
use crate::state::WoState;
|
||||||
|
|
@ -124,7 +123,9 @@ impl Dispatch<ZwlrScreencopyManagerV1, ()> for WoState {
|
||||||
send_buffer_info(&frame_resource, w, h);
|
send_buffer_info(&frame_resource, w, h);
|
||||||
debug!(
|
debug!(
|
||||||
"CaptureOutput: {}x{}, cursor_overlay={}",
|
"CaptureOutput: {}x{}, cursor_overlay={}",
|
||||||
w, h, overlay_cursor != 0
|
w,
|
||||||
|
h,
|
||||||
|
overlay_cursor != 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
zwlr_screencopy_manager_v1::Request::CaptureOutputRegion {
|
zwlr_screencopy_manager_v1::Request::CaptureOutputRegion {
|
||||||
|
|
@ -156,7 +157,11 @@ impl Dispatch<ZwlrScreencopyManagerV1, ()> for WoState {
|
||||||
send_buffer_info(&frame_resource, rw as u32, rh as u32);
|
send_buffer_info(&frame_resource, rw as u32, rh as u32);
|
||||||
debug!(
|
debug!(
|
||||||
"CaptureOutputRegion: ({},{}) {}x{}, cursor_overlay={}",
|
"CaptureOutputRegion: ({},{}) {}x{}, cursor_overlay={}",
|
||||||
rx, ry, rw, rh, overlay_cursor != 0
|
rx,
|
||||||
|
ry,
|
||||||
|
rw,
|
||||||
|
rh,
|
||||||
|
overlay_cursor != 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
zwlr_screencopy_manager_v1::Request::Destroy => {
|
zwlr_screencopy_manager_v1::Request::Destroy => {
|
||||||
|
|
@ -172,20 +177,11 @@ fn send_buffer_info(frame: &ZwlrScreencopyFrameV1, width: u32, height: u32) {
|
||||||
let stride = width * 4; // ARGB8888 / BGRx
|
let stride = width * 4; // ARGB8888 / BGRx
|
||||||
|
|
||||||
// SHM buffer support (always available)
|
// SHM buffer support (always available)
|
||||||
frame.buffer(
|
frame.buffer(wl_shm::Format::Argb8888, width, height, stride);
|
||||||
wl_shm::Format::Argb8888,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
stride,
|
|
||||||
);
|
|
||||||
|
|
||||||
// DMABUF support (v3)
|
// DMABUF support (v3)
|
||||||
if frame.version() >= 3 {
|
if frame.version() >= 3 {
|
||||||
frame.linux_dmabuf(
|
frame.linux_dmabuf(drm_fourcc::DrmFourcc::Argb8888 as u32, width, height);
|
||||||
drm_fourcc::DrmFourcc::Argb8888 as u32,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
);
|
|
||||||
frame.buffer_done();
|
frame.buffer_done();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use crate::state::WoState;
|
||||||
impl XdgDecorationHandler for WoState {
|
impl XdgDecorationHandler for WoState {
|
||||||
fn new_decoration(&mut self, toplevel: ToplevelSurface) {
|
fn new_decoration(&mut self, toplevel: ToplevelSurface) {
|
||||||
tracing::info!("new_decoration: client attached xdg_decoration");
|
tracing::info!("new_decoration: client attached xdg_decoration");
|
||||||
|
self.metadata_dirty = true;
|
||||||
|
|
||||||
let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32);
|
let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32);
|
||||||
let suggest_w = (ow * 2 / 3).max(640).min(1600);
|
let suggest_w = (ow * 2 / 3).max(640).min(1600);
|
||||||
|
|
@ -44,6 +45,7 @@ impl XdgDecorationHandler for WoState {
|
||||||
self.ssd_windows.remove(&toplevel.wl_surface().id());
|
self.ssd_windows.remove(&toplevel.wl_surface().id());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.metadata_dirty = true;
|
||||||
toplevel.send_configure();
|
toplevel.send_configure();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,6 +63,7 @@ impl XdgDecorationHandler for WoState {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
self.ssd_windows.remove(&toplevel.wl_surface().id());
|
self.ssd_windows.remove(&toplevel.wl_surface().id());
|
||||||
|
self.metadata_dirty = true;
|
||||||
toplevel.send_configure();
|
toplevel.send_configure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
|
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::ResizeEdge;
|
||||||
use smithay::{
|
use smithay::{
|
||||||
delegate_xdg_shell,
|
delegate_xdg_shell,
|
||||||
desktop::{PopupManager, Space, Window},
|
desktop::{PopupManager, Space, Window},
|
||||||
reexports::wayland_server::{protocol::{wl_output, wl_seat}, Resource},
|
reexports::wayland_server::{
|
||||||
|
protocol::{wl_output, wl_seat},
|
||||||
|
Resource,
|
||||||
|
},
|
||||||
utils::Serial,
|
utils::Serial,
|
||||||
wayland::{
|
wayland::{
|
||||||
compositor::with_states,
|
compositor::with_states,
|
||||||
|
|
@ -11,7 +15,6 @@ use smithay::{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::ResizeEdge;
|
|
||||||
|
|
||||||
use crate::state::WoState;
|
use crate::state::WoState;
|
||||||
|
|
||||||
|
|
@ -31,7 +34,8 @@ impl XdgShellHandler for WoState {
|
||||||
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
||||||
tracing::info!("new_toplevel ENTER");
|
tracing::info!("new_toplevel ENTER");
|
||||||
let app_id = with_states(surface.wl_surface(), |states| {
|
let app_id = with_states(surface.wl_surface(), |states| {
|
||||||
states.data_map
|
states
|
||||||
|
.data_map
|
||||||
.get::<XdgToplevelSurfaceData>()
|
.get::<XdgToplevelSurfaceData>()
|
||||||
.and_then(|d| d.lock().ok().and_then(|data| data.app_id.clone()))
|
.and_then(|d| d.lock().ok().and_then(|data| data.app_id.clone()))
|
||||||
});
|
});
|
||||||
|
|
@ -42,8 +46,10 @@ impl XdgShellHandler for WoState {
|
||||||
let oid = surface.wl_surface().id();
|
let oid = surface.wl_surface().id();
|
||||||
let stable_name = format!("wayland-{}", self.next_wayland_id);
|
let stable_name = format!("wayland-{}", self.next_wayland_id);
|
||||||
self.next_wayland_id += 1;
|
self.next_wayland_id += 1;
|
||||||
self.wayland_window_names.insert(oid.clone(), stable_name.clone());
|
self.wayland_window_names
|
||||||
self.wayland_name_to_id.insert(stable_name.clone(), oid.clone());
|
.insert(oid.clone(), stable_name.clone());
|
||||||
|
self.wayland_name_to_id
|
||||||
|
.insert(stable_name.clone(), oid.clone());
|
||||||
tracing::info!(name = %stable_name, "assigned stable window name");
|
tracing::info!(name = %stable_name, "assigned stable window name");
|
||||||
// Mark this Wayland window as mapped/visible for metadata and frontend
|
// Mark this Wayland window as mapped/visible for metadata and frontend
|
||||||
self.window_mapped.insert(stable_name.clone(), true);
|
self.window_mapped.insert(stable_name.clone(), true);
|
||||||
|
|
@ -63,7 +69,6 @@ impl XdgShellHandler for WoState {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32);
|
let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32);
|
||||||
|
|
||||||
let location = if let Some(ref parent_wl) = parent_surface {
|
let location = if let Some(ref parent_wl) = parent_surface {
|
||||||
|
|
@ -115,7 +120,8 @@ impl XdgShellHandler for WoState {
|
||||||
// absolute popup rectangle and clamp it back into [0, output_size].
|
// absolute popup rectangle and clamp it back into [0, output_size].
|
||||||
let parent_loc: Option<smithay::utils::Point<i32, smithay::utils::Logical>> =
|
let parent_loc: Option<smithay::utils::Point<i32, smithay::utils::Logical>> =
|
||||||
smithay::wayland::compositor::with_states(surface.wl_surface(), |states| {
|
smithay::wayland::compositor::with_states(surface.wl_surface(), |states| {
|
||||||
states.data_map
|
states
|
||||||
|
.data_map
|
||||||
.get::<smithay::wayland::shell::xdg::XdgPopupSurfaceData>()
|
.get::<smithay::wayland::shell::xdg::XdgPopupSurfaceData>()
|
||||||
.and_then(|d| d.lock().ok().and_then(|locked| locked.parent.clone()))
|
.and_then(|d| d.lock().ok().and_then(|locked| locked.parent.clone()))
|
||||||
})
|
})
|
||||||
|
|
@ -151,7 +157,10 @@ impl XdgShellHandler for WoState {
|
||||||
if let Err(e) = surface.send_configure() {
|
if let Err(e) = surface.send_configure() {
|
||||||
tracing::warn!("failed to send initial popup configure: {e}");
|
tracing::warn!("failed to send initial popup configure: {e}");
|
||||||
}
|
}
|
||||||
if let Err(e) = self.popup_manager.track_popup(smithay::desktop::PopupKind::Xdg(surface)) {
|
if let Err(e) = self
|
||||||
|
.popup_manager
|
||||||
|
.track_popup(smithay::desktop::PopupKind::Xdg(surface))
|
||||||
|
{
|
||||||
tracing::warn!("failed tracking popup: {e}");
|
tracing::warn!("failed tracking popup: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -171,7 +180,9 @@ impl XdgShellHandler for WoState {
|
||||||
|
|
||||||
fn move_request(&mut self, surface: ToplevelSurface, _seat: wl_seat::WlSeat, _serial: Serial) {
|
fn move_request(&mut self, surface: ToplevelSurface, _seat: wl_seat::WlSeat, _serial: Serial) {
|
||||||
// Find the window element for this surface and start tracking a move.
|
// Find the window element for this surface and start tracking a move.
|
||||||
let window = self.space.elements()
|
let window = self
|
||||||
|
.space
|
||||||
|
.elements()
|
||||||
.find(|w| w.toplevel().map(|t| t == &surface).unwrap_or(false))
|
.find(|w| w.toplevel().map(|t| t == &surface).unwrap_or(false))
|
||||||
.cloned();
|
.cloned();
|
||||||
|
|
||||||
|
|
@ -188,11 +199,13 @@ impl XdgShellHandler for WoState {
|
||||||
fn resize_request(
|
fn resize_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
surface: ToplevelSurface,
|
surface: ToplevelSurface,
|
||||||
_seat: wl_seat::WlSeat,
|
_seat: wl_seat::WlSeat,
|
||||||
_serial: Serial,
|
_serial: Serial,
|
||||||
edges: ResizeEdge,
|
edges: ResizeEdge,
|
||||||
) {
|
) {
|
||||||
let window = self.space.elements()
|
let window = self
|
||||||
|
.space
|
||||||
|
.elements()
|
||||||
.find(|w| w.toplevel().map(|t| t == &surface).unwrap_or(false))
|
.find(|w| w.toplevel().map(|t| t == &surface).unwrap_or(false))
|
||||||
.cloned();
|
.cloned();
|
||||||
|
|
||||||
|
|
@ -210,28 +223,43 @@ impl XdgShellHandler for WoState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) { }
|
fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {}
|
||||||
|
|
||||||
fn fullscreen_request(&mut self, surface: ToplevelSurface, _output: Option<wl_output::WlOutput>) {
|
fn fullscreen_request(
|
||||||
let window = self.space.elements()
|
&mut self,
|
||||||
|
surface: ToplevelSurface,
|
||||||
|
_output: Option<wl_output::WlOutput>,
|
||||||
|
) {
|
||||||
|
let window = self
|
||||||
|
.space
|
||||||
|
.elements()
|
||||||
.find(|w| w.toplevel().map(|t| t == &surface).unwrap_or(false))
|
.find(|w| w.toplevel().map(|t| t == &surface).unwrap_or(false))
|
||||||
.cloned();
|
.cloned();
|
||||||
|
|
||||||
if let Some(window) = window {
|
if let Some(window) = window {
|
||||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState;
|
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState;
|
||||||
|
|
||||||
let th = self.title_h_for_window(&window);
|
|
||||||
let y_offset = th.max(PANEL_H);
|
|
||||||
let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32);
|
let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32);
|
||||||
let content_h = (oh - y_offset).max(1);
|
let name = self.wayland_window_name(&window).cloned();
|
||||||
|
if let Some(ref name) = name {
|
||||||
|
if !self.fullscreen_windows.contains(name) {
|
||||||
|
let loc = self.space.element_location(&window).unwrap_or_default();
|
||||||
|
let size = window.geometry().size;
|
||||||
|
self.fullscreen_restore.insert(
|
||||||
|
name.clone(),
|
||||||
|
(loc.x, loc.y, size.w.max(1) as u32, size.h.max(1) as u32),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.fullscreen_windows.insert(name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
surface.with_pending_state(|state| {
|
surface.with_pending_state(|state| {
|
||||||
state.states.set(XdgState::Fullscreen);
|
state.states.set(XdgState::Fullscreen);
|
||||||
state.states.unset(XdgState::Maximized);
|
state.states.unset(XdgState::Maximized);
|
||||||
state.size = Some((ow, content_h).into());
|
state.size = Some((ow, oh).into());
|
||||||
});
|
});
|
||||||
surface.send_configure();
|
surface.send_configure();
|
||||||
self.space.map_element(window.clone(), (0, y_offset), false);
|
self.space.map_element(window.clone(), (0, 0), false);
|
||||||
self.metadata_dirty = true;
|
self.metadata_dirty = true;
|
||||||
|
|
||||||
// Automatically give keyboard focus to fullscreen windows (important for games)
|
// Automatically give keyboard focus to fullscreen windows (important for games)
|
||||||
|
|
@ -243,24 +271,33 @@ impl XdgShellHandler for WoState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
|
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
|
||||||
let window = self.space.elements()
|
let window = self
|
||||||
|
.space
|
||||||
|
.elements()
|
||||||
.find(|w| w.toplevel().map(|t| t == &surface).unwrap_or(false))
|
.find(|w| w.toplevel().map(|t| t == &surface).unwrap_or(false))
|
||||||
.cloned();
|
.cloned();
|
||||||
|
|
||||||
if let Some(_window) = window {
|
if let Some(window) = window {
|
||||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState;
|
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState;
|
||||||
|
|
||||||
let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32);
|
let name = self.wayland_window_name(&window).cloned();
|
||||||
let suggest_w = (ow * 2 / 3).max(640).min(1600);
|
let restore = name
|
||||||
let suggest_h = (oh * 2 / 3).max(480).min(1000);
|
.as_ref()
|
||||||
|
.and_then(|name| self.fullscreen_restore.remove(name));
|
||||||
|
if let Some(ref name) = name {
|
||||||
|
self.fullscreen_windows.remove(name);
|
||||||
|
}
|
||||||
|
let (x, y, width, height) = restore.unwrap_or_else(|| {
|
||||||
|
let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32);
|
||||||
|
((ow / 6), (oh / 6), (ow * 2 / 3) as u32, (oh * 2 / 3) as u32)
|
||||||
|
});
|
||||||
|
|
||||||
surface.with_pending_state(|state| {
|
surface.with_pending_state(|state| {
|
||||||
state.states.unset(XdgState::Fullscreen);
|
state.states.unset(XdgState::Fullscreen);
|
||||||
if state.size.is_none() {
|
state.size = Some((width as i32, height as i32).into());
|
||||||
state.size = Some((suggest_w, suggest_h).into());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
surface.send_configure();
|
surface.send_configure();
|
||||||
|
self.space.map_element(window, (x, y), false);
|
||||||
self.metadata_dirty = true;
|
self.metadata_dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -273,6 +310,8 @@ impl XdgShellHandler for WoState {
|
||||||
let oid = surface.wl_surface().id();
|
let oid = surface.wl_surface().id();
|
||||||
if let Some(name) = self.wayland_window_names.remove(&oid) {
|
if let Some(name) = self.wayland_window_names.remove(&oid) {
|
||||||
self.wayland_name_to_id.remove(&name);
|
self.wayland_name_to_id.remove(&name);
|
||||||
|
self.fullscreen_restore.remove(&name);
|
||||||
|
self.fullscreen_windows.remove(&name);
|
||||||
if self.keyboard_window_focus.as_deref() == Some(&name) {
|
if self.keyboard_window_focus.as_deref() == Some(&name) {
|
||||||
self.keyboard_window_focus = None;
|
self.keyboard_window_focus = None;
|
||||||
}
|
}
|
||||||
|
|
@ -293,11 +332,7 @@ impl XdgShellHandler for WoState {
|
||||||
let to_remove = self
|
let to_remove = self
|
||||||
.all_windows
|
.all_windows
|
||||||
.iter()
|
.iter()
|
||||||
.find(|w| {
|
.find(|w| w.toplevel().map(|t| t == &surface).unwrap_or(false))
|
||||||
w.toplevel()
|
|
||||||
.map(|t| t == &surface)
|
|
||||||
.unwrap_or(false)
|
|
||||||
})
|
|
||||||
.cloned();
|
.cloned();
|
||||||
|
|
||||||
if let Some(window) = to_remove {
|
if let Some(window) = to_remove {
|
||||||
|
|
@ -331,10 +366,9 @@ pub enum GrabState {
|
||||||
/// Send the initial configure to a surface if it hasn't received one yet.
|
/// Send the initial configure to a surface if it hasn't received one yet.
|
||||||
pub fn ensure_initial_configure(
|
pub fn ensure_initial_configure(
|
||||||
surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
|
surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||||
space: &Space<Window>,
|
space: &Space<Window>,
|
||||||
popups: &mut PopupManager,
|
popups: &mut PopupManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
if let Some(window) = space
|
if let Some(window) = space
|
||||||
.elements()
|
.elements()
|
||||||
.find(|w| {
|
.find(|w| {
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,18 @@
|
||||||
//! Implements [`XwmHandler`] and [`XWaylandShellHandler`] so that X11 clients
|
//! Implements [`XwmHandler`] and [`XWaylandShellHandler`] so that X11 clients
|
||||||
//! running under XWayland can be managed by the compositor.
|
//! running under XWayland can be managed by the compositor.
|
||||||
|
|
||||||
|
use smithay::reexports::wayland_server::Resource;
|
||||||
|
use smithay::xwayland::xwm::X11Window;
|
||||||
use smithay::{
|
use smithay::{
|
||||||
desktop::Window,
|
desktop::Window,
|
||||||
utils::{Logical, Rectangle},
|
utils::{Logical, Rectangle},
|
||||||
wayland::xwayland_shell::{XWaylandShellHandler, XWaylandShellState},
|
wayland::xwayland_shell::{XWaylandShellHandler, XWaylandShellState},
|
||||||
xwayland::xwm::{
|
xwayland::xwm::{Reorder, ResizeEdge as X11ResizeEdge, X11Surface, X11Wm, XwmHandler, XwmId},
|
||||||
Reorder, ResizeEdge as X11ResizeEdge, X11Surface, X11Wm, XwmHandler, XwmId,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use smithay::xwayland::xwm::X11Window;
|
|
||||||
use smithay::reexports::wayland_server::Resource;
|
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::state::WoState;
|
|
||||||
use crate::handlers::xdg_shell::PANEL_H;
|
use crate::handlers::xdg_shell::PANEL_H;
|
||||||
|
use crate::state::WoState;
|
||||||
|
|
||||||
impl XwmHandler for WoState {
|
impl XwmHandler for WoState {
|
||||||
fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm {
|
fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm {
|
||||||
|
|
@ -51,7 +49,8 @@ impl XwmHandler for WoState {
|
||||||
}
|
}
|
||||||
|
|
||||||
let stable_name = format!("x11-{}", window.window_id());
|
let stable_name = format!("x11-{}", window.window_id());
|
||||||
self.x11_window_names.insert(window.window_id(), stable_name.clone());
|
self.x11_window_names
|
||||||
|
.insert(window.window_id(), stable_name.clone());
|
||||||
|
|
||||||
let parent_x11_id = window.is_transient_for();
|
let parent_x11_id = window.is_transient_for();
|
||||||
let geo = window.geometry();
|
let geo = window.geometry();
|
||||||
|
|
@ -122,10 +121,7 @@ impl XwmHandler for WoState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unmapped_window(&mut self, _xwm: XwmId, window: X11Surface) {
|
fn unmapped_window(&mut self, _xwm: XwmId, window: X11Surface) {
|
||||||
info!(
|
info!(window_id = window.window_id(), "XWayland: window unmapped");
|
||||||
window_id = window.window_id(),
|
|
||||||
"XWayland: window unmapped"
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(name) = self.x11_window_names.get(&window.window_id()).cloned() {
|
if let Some(name) = self.x11_window_names.get(&window.window_id()).cloned() {
|
||||||
if self.keyboard_window_focus.as_deref() == Some(&name) {
|
if self.keyboard_window_focus.as_deref() == Some(&name) {
|
||||||
|
|
@ -145,11 +141,15 @@ impl XwmHandler for WoState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let to_remove = self.space.elements().find(|w| {
|
let to_remove = self
|
||||||
w.x11_surface()
|
.space
|
||||||
.map(|x| x.window_id() == window.window_id())
|
.elements()
|
||||||
.unwrap_or(false)
|
.find(|w| {
|
||||||
}).cloned();
|
w.x11_surface()
|
||||||
|
.map(|x| x.window_id() == window.window_id())
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.cloned();
|
||||||
|
|
||||||
if let Some(w) = to_remove {
|
if let Some(w) = to_remove {
|
||||||
self.space.unmap_elem(&w);
|
self.space.unmap_elem(&w);
|
||||||
|
|
@ -160,10 +160,7 @@ impl XwmHandler for WoState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn destroyed_window(&mut self, _xwm: XwmId, window: X11Surface) {
|
fn destroyed_window(&mut self, _xwm: XwmId, window: X11Surface) {
|
||||||
info!(
|
info!(window_id = window.window_id(), "XWayland: window destroyed");
|
||||||
window_id = window.window_id(),
|
|
||||||
"XWayland: window destroyed"
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(name) = self.x11_window_names.get(&window.window_id()).cloned() {
|
if let Some(name) = self.x11_window_names.get(&window.window_id()).cloned() {
|
||||||
if self.keyboard_window_focus.as_deref() == Some(&name) {
|
if self.keyboard_window_focus.as_deref() == Some(&name) {
|
||||||
|
|
@ -183,13 +180,20 @@ impl XwmHandler for WoState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.x11_window_names.remove(&window.window_id());
|
if let Some(name) = self.x11_window_names.remove(&window.window_id()) {
|
||||||
|
self.fullscreen_restore.remove(&name);
|
||||||
|
self.fullscreen_windows.remove(&name);
|
||||||
|
}
|
||||||
|
|
||||||
let to_remove = self.space.elements().find(|w| {
|
let to_remove = self
|
||||||
w.x11_surface()
|
.space
|
||||||
.map(|x| x.window_id() == window.window_id())
|
.elements()
|
||||||
.unwrap_or(false)
|
.find(|w| {
|
||||||
}).cloned();
|
w.x11_surface()
|
||||||
|
.map(|x| x.window_id() == window.window_id())
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.cloned();
|
||||||
if let Some(w) = to_remove {
|
if let Some(w) = to_remove {
|
||||||
self.space.unmap_elem(&w);
|
self.space.unmap_elem(&w);
|
||||||
self.all_windows.retain(|win| win != &w);
|
self.all_windows.retain(|win| win != &w);
|
||||||
|
|
@ -219,7 +223,8 @@ impl XwmHandler for WoState {
|
||||||
_window: X11Surface,
|
_window: X11Surface,
|
||||||
_geometry: Rectangle<i32, Logical>,
|
_geometry: Rectangle<i32, Logical>,
|
||||||
_above: Option<X11Window>,
|
_above: Option<X11Window>,
|
||||||
) {}
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
fn resize_request(
|
fn resize_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -227,7 +232,8 @@ impl XwmHandler for WoState {
|
||||||
_window: X11Surface,
|
_window: X11Surface,
|
||||||
_button: u32,
|
_button: u32,
|
||||||
_resize_edge: X11ResizeEdge,
|
_resize_edge: X11ResizeEdge,
|
||||||
) {}
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
fn move_request(&mut self, _xwm: XwmId, _window: X11Surface, _button: u32) {}
|
fn move_request(&mut self, _xwm: XwmId, _window: X11Surface, _button: u32) {}
|
||||||
|
|
||||||
|
|
@ -242,32 +248,36 @@ impl XwmHandler for WoState {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let th = if let Some(wl_surface) = window.wl_surface() {
|
|
||||||
if self.ssd_windows.contains(&wl_surface.id()) {
|
|
||||||
30
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
let y_offset = th.max(PANEL_H);
|
|
||||||
let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32);
|
let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32);
|
||||||
let content_h = (oh - y_offset).max(1);
|
let name = self.x11_window_names.get(&window.window_id()).cloned();
|
||||||
|
let win = self
|
||||||
|
.space
|
||||||
|
.elements()
|
||||||
|
.find(|w| {
|
||||||
|
w.x11_surface()
|
||||||
|
.map(|x| x.window_id() == window.window_id())
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.cloned();
|
||||||
|
if let (Some(name), Some(win)) = (name.as_ref(), win.as_ref()) {
|
||||||
|
if !self.fullscreen_windows.contains(name) {
|
||||||
|
let loc = self.space.element_location(win).unwrap_or_default();
|
||||||
|
let size = window.geometry().size;
|
||||||
|
self.fullscreen_restore.insert(
|
||||||
|
name.clone(),
|
||||||
|
(loc.x, loc.y, size.w.max(1) as u32, size.h.max(1) as u32),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.fullscreen_windows.insert(name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
if let Err(e) = window.configure(Some(Rectangle::from_loc_and_size((0, y_offset), (ow, content_h)))) {
|
if let Err(e) = window.configure(Some(Rectangle::from_loc_and_size((0, 0), (ow, oh)))) {
|
||||||
warn!("XWayland: fullscreen configure failed: {e}");
|
warn!("XWayland: fullscreen configure failed: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let win = self.space.elements().find(|w| {
|
|
||||||
w.x11_surface()
|
|
||||||
.map(|x| x.window_id() == window.window_id())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}).cloned();
|
|
||||||
|
|
||||||
if let Some(w) = win {
|
if let Some(w) = win {
|
||||||
self.space.map_element(w, (0, y_offset), false);
|
self.space.map_element(w, (0, 0), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically give keyboard focus to fullscreen windows (important for games)
|
// Automatically give keyboard focus to fullscreen windows (important for games)
|
||||||
|
|
@ -297,14 +307,37 @@ impl XwmHandler for WoState {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32);
|
let name = self.x11_window_names.get(&window.window_id()).cloned();
|
||||||
let suggest_w = (ow * 2 / 3).max(640).min(1600);
|
let restore = name
|
||||||
let suggest_h = (oh * 2 / 3).max(480).min(1000);
|
.as_ref()
|
||||||
|
.and_then(|name| self.fullscreen_restore.remove(name));
|
||||||
|
if let Some(ref name) = name {
|
||||||
|
self.fullscreen_windows.remove(name);
|
||||||
|
}
|
||||||
|
let (x, y, width, height) = restore.unwrap_or_else(|| {
|
||||||
|
let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32);
|
||||||
|
(ow / 6, oh / 6, (ow * 2 / 3) as u32, (oh * 2 / 3) as u32)
|
||||||
|
});
|
||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
if let Err(e) = window.configure(Some(Rectangle::from_loc_and_size((0, 0), (suggest_w, suggest_h)))) {
|
if let Err(e) = window.configure(Some(Rectangle::from_loc_and_size(
|
||||||
|
(x, y),
|
||||||
|
(width as i32, height as i32),
|
||||||
|
))) {
|
||||||
warn!("XWayland: unfullscreen configure failed: {e}");
|
warn!("XWayland: unfullscreen configure failed: {e}");
|
||||||
}
|
}
|
||||||
|
let mapped_window = self
|
||||||
|
.space
|
||||||
|
.elements()
|
||||||
|
.find(|w| {
|
||||||
|
w.x11_surface()
|
||||||
|
.map(|x| x.window_id() == window.window_id())
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.cloned();
|
||||||
|
if let Some(win) = mapped_window {
|
||||||
|
self.space.map_element(win, (x, y), false);
|
||||||
|
}
|
||||||
self.metadata_dirty = true;
|
self.metadata_dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1625
src/main.rs
1625
src/main.rs
File diff suppressed because it is too large
Load diff
544
src/nested.rs
544
src/nested.rs
|
|
@ -22,14 +22,12 @@ use smithay::{
|
||||||
},
|
},
|
||||||
output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel},
|
output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel},
|
||||||
reexports::wayland_server::{Display, ListeningSocket},
|
reexports::wayland_server::{Display, ListeningSocket},
|
||||||
utils::{
|
utils::{Logical, Physical, Point, Rectangle, Scale as ScaleF, Size, Transform},
|
||||||
Logical, Physical, Point, Rectangle,
|
|
||||||
Scale as ScaleF, Size, Transform,
|
|
||||||
},
|
|
||||||
wayland::{seat::WaylandFocus, xdg_activation::XdgActivationHandler},
|
wayland::{seat::WaylandFocus, xdg_activation::XdgActivationHandler},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
os::unix::fs::FileTypeExt,
|
||||||
path::Path,
|
path::Path,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
@ -67,6 +65,93 @@ fn find_render_node() -> DrmNode {
|
||||||
.unwrap_or_else(|_| panic!("Could not find or create any render node"))
|
.unwrap_or_else(|_| panic!("Could not find or create any render node"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn refresh_millihz(backend: &winit::WinitGraphicsBackend<GlesRenderer>) -> u32 {
|
||||||
|
backend
|
||||||
|
.window()
|
||||||
|
.current_monitor()
|
||||||
|
.and_then(|monitor| monitor.refresh_rate_millihertz())
|
||||||
|
.filter(|rate| *rate > 0)
|
||||||
|
.unwrap_or(60_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame_duration(refresh_millihz: u32) -> Duration {
|
||||||
|
Duration::from_secs_f64(1_000.0 / f64::from(refresh_millihz.max(1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_wayland_display() -> Option<std::ffi::OsString> {
|
||||||
|
let runtime_dir = std::env::var_os("XDG_RUNTIME_DIR")?;
|
||||||
|
let runtime_path = Path::new(&runtime_dir);
|
||||||
|
|
||||||
|
if let Some(display) = std::env::var_os("WAYLAND_DISPLAY") {
|
||||||
|
let socket = Path::new(&display);
|
||||||
|
let socket_path = if socket.is_absolute() {
|
||||||
|
socket.to_path_buf()
|
||||||
|
} else {
|
||||||
|
runtime_path.join(socket)
|
||||||
|
};
|
||||||
|
if socket_path.metadata().is_ok_and(|metadata| metadata.file_type().is_socket()) {
|
||||||
|
return Some(display);
|
||||||
|
}
|
||||||
|
warn!(path = %socket_path.display(), "WAYLAND_DISPLAY does not name an accessible socket; searching runtime directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sockets = std::fs::read_dir(runtime_path)
|
||||||
|
.ok()?
|
||||||
|
.filter_map(|entry| entry.ok())
|
||||||
|
.filter(|entry| {
|
||||||
|
entry.file_name().to_string_lossy().starts_with("wayland-")
|
||||||
|
&& entry.file_type().is_ok_and(|kind| kind.is_socket())
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
sockets.sort_by_key(|entry| entry.file_name());
|
||||||
|
sockets.first().map(|entry| entry.file_name())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_nested_electron_windows(
|
||||||
|
state: &mut WoState,
|
||||||
|
config: &Config,
|
||||||
|
app_dir: &Path,
|
||||||
|
render_node: &str,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
ozone_platform: &str,
|
||||||
|
) {
|
||||||
|
for root_cfg in &config.root {
|
||||||
|
let mut full = root_cfg.clone();
|
||||||
|
full.system_shell = true;
|
||||||
|
full.x = 0;
|
||||||
|
full.y = 0;
|
||||||
|
full.width = width;
|
||||||
|
full.height = height;
|
||||||
|
full.z_order = i32::MIN / 2;
|
||||||
|
match ElectronProcess::spawn(
|
||||||
|
&full,
|
||||||
|
&config.compositor.electron_path,
|
||||||
|
app_dir,
|
||||||
|
&config.compositor.ipc_socket,
|
||||||
|
render_node,
|
||||||
|
ozone_platform,
|
||||||
|
) {
|
||||||
|
Ok(proc) => state.electron_processes.push(proc),
|
||||||
|
Err(e) => error!(window = %full.name, "failed to spawn Electron root: {e:#}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for win_cfg in &config.windows {
|
||||||
|
match ElectronProcess::spawn(
|
||||||
|
win_cfg,
|
||||||
|
&config.compositor.electron_path,
|
||||||
|
app_dir,
|
||||||
|
&config.compositor.ipc_socket,
|
||||||
|
render_node,
|
||||||
|
ozone_platform,
|
||||||
|
) {
|
||||||
|
Ok(proc) => state.electron_processes.push(proc),
|
||||||
|
Err(e) => error!(window = %win_cfg.name, "failed to spawn Electron: {e:#}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn is_egl_context_or_surface_lost(err: &str) -> bool {
|
fn is_egl_context_or_surface_lost(err: &str) -> bool {
|
||||||
let lowered = err.to_ascii_lowercase();
|
let lowered = err.to_ascii_lowercase();
|
||||||
lowered.contains("context has been lost")
|
lowered.contains("context has been lost")
|
||||||
|
|
@ -77,15 +162,15 @@ fn is_egl_context_or_surface_lost(err: &str) -> bool {
|
||||||
|| lowered.contains("failed to allocate resources")
|
|| lowered.contains("failed to allocate resources")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_nested(config: Config) -> Result<()> {
|
pub fn run_nested(mut config: Config) -> Result<()> {
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::signal(libc::SIGPIPE, libc::SIG_IGN);
|
libc::signal(libc::SIGPIPE, libc::SIG_IGN);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Starting nested compositor mode");
|
info!("Starting nested compositor mode");
|
||||||
|
|
||||||
// Apply configured environment variables (e.g. DISPLAY=:0) early so that
|
// Apply explicit environment overrides early. DISPLAY is set only after
|
||||||
// all children inherit them. XWayland will override DISPLAY later.
|
// this compositor's XWayland server is ready.
|
||||||
config.apply_environment();
|
config.apply_environment();
|
||||||
|
|
||||||
if config.compositor.exit_on_escape {
|
if config.compositor.exit_on_escape {
|
||||||
|
|
@ -101,7 +186,28 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
.with_context(|| format!("binding Wayland socket {}", config.compositor.socket_name))?;
|
.with_context(|| format!("binding Wayland socket {}", config.compositor.socket_name))?;
|
||||||
info!(socket = %config.compositor.socket_name, "Wayland socket created");
|
info!(socket = %config.compositor.socket_name, "Wayland socket created");
|
||||||
|
|
||||||
let (mut backend, mut winit_evt_loop) = match winit::init::<GlesRenderer>() {
|
// Some session launchers and old configs provide DISPLAY but omit
|
||||||
|
// WAYLAND_DISPLAY. Discover the host compositor socket in that case so
|
||||||
|
// nested Wo uses a native Wayland host window instead of failing XOpenDisplay
|
||||||
|
// and unnecessarily dropping into DRM/KMS root mode.
|
||||||
|
let host_display = std::env::var_os("DISPLAY");
|
||||||
|
let host_wayland = host_wayland_display();
|
||||||
|
if let Some(wayland_display) = host_wayland {
|
||||||
|
std::env::set_var("WAYLAND_DISPLAY", &wayland_display);
|
||||||
|
info!(socket = %wayland_display.to_string_lossy(), "Using host Wayland display for nested startup");
|
||||||
|
if host_display.is_some() {
|
||||||
|
info!("Temporarily hiding DISPLAY so winit selects the host Wayland backend");
|
||||||
|
}
|
||||||
|
std::env::remove_var("DISPLAY");
|
||||||
|
} else {
|
||||||
|
info!("No host Wayland socket found; nested startup will use X11 if DISPLAY is available");
|
||||||
|
}
|
||||||
|
let backend_result = winit::init::<GlesRenderer>();
|
||||||
|
if let Some(display) = host_display {
|
||||||
|
std::env::set_var("DISPLAY", display);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut backend, mut winit_evt_loop) = match backend_result {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to create winit backend: {:?}", e);
|
error!("Failed to create winit backend: {:?}", e);
|
||||||
|
|
@ -124,9 +230,10 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut refresh_rate = refresh_millihz(&backend);
|
||||||
let output_mode = OutputMode {
|
let output_mode = OutputMode {
|
||||||
size: (width as i32, height as i32).into(),
|
size: (width as i32, height as i32).into(),
|
||||||
refresh: 60_000,
|
refresh: refresh_rate as i32,
|
||||||
};
|
};
|
||||||
|
|
||||||
output.change_current_state(
|
output.change_current_state(
|
||||||
|
|
@ -164,18 +271,34 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
dmabuf_formats: Some(dmabuf_formats),
|
dmabuf_formats: Some(dmabuf_formats),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
crate::spawn_immediate_autostarts(&mut config);
|
||||||
let mut state = WoState::new(&mut display, config.clone(), render_node, backend_data);
|
let mut state = WoState::new(&mut display, config.clone(), render_node, backend_data);
|
||||||
|
|
||||||
let (ipc, mut msg_rx) =
|
let (ipc, mut msg_rx) =
|
||||||
ElectronIpc::listen(&config.compositor.ipc_socket).context("IPC socket")?;
|
ElectronIpc::listen(&config.compositor.ipc_socket).context("IPC socket")?;
|
||||||
state.electron_ipc = Some(ipc);
|
state.electron_ipc = Some(ipc);
|
||||||
|
|
||||||
|
let app_dir = crate::electron::app_dir();
|
||||||
|
// Nested mode has no DRM card; use the first available render node.
|
||||||
|
let nested_render_node = std::fs::read_dir("/dev/dri")
|
||||||
|
.ok()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|e| e.filter_map(Result::ok))
|
||||||
|
.find(|e| {
|
||||||
|
e.file_name()
|
||||||
|
.to_str()
|
||||||
|
.map_or(false, |n| n.starts_with("renderD"))
|
||||||
|
})
|
||||||
|
.map(|e| e.path().to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| "/dev/dri/renderD128".to_string());
|
||||||
|
|
||||||
let mut calloop_loop =
|
let mut calloop_loop =
|
||||||
calloop::EventLoop::<WoState>::try_new().context("calloop EventLoop creation")?;
|
calloop::EventLoop::<WoState>::try_new().context("calloop EventLoop creation")?;
|
||||||
let calloop_handle = calloop_loop.handle();
|
let calloop_handle = calloop_loop.handle();
|
||||||
|
|
||||||
state.xwayland_shell_state = Some(XWaylandShellState::new::<WoState>(&dh));
|
state.xwayland_shell_state = Some(XWaylandShellState::new::<WoState>(&dh));
|
||||||
|
|
||||||
|
let mut xwayland_source_registered = false;
|
||||||
match XWayland::spawn(
|
match XWayland::spawn(
|
||||||
&dh,
|
&dh,
|
||||||
None, // auto-detect display number
|
None, // auto-detect display number
|
||||||
|
|
@ -189,6 +312,10 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
let wm_handle = calloop_handle.clone();
|
let wm_handle = calloop_handle.clone();
|
||||||
let xw_client = std::rc::Rc::new(std::cell::RefCell::new(Some(xw_client)));
|
let xw_client = std::rc::Rc::new(std::cell::RefCell::new(Some(xw_client)));
|
||||||
let xw_client_cb = xw_client.clone();
|
let xw_client_cb = xw_client.clone();
|
||||||
|
let startup_config = config.clone();
|
||||||
|
let startup_app_dir = app_dir.clone();
|
||||||
|
let startup_render_node = nested_render_node.clone();
|
||||||
|
let mut electron_started = false;
|
||||||
|
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
calloop_handle.insert_source(xwayland, move |event, _, state: &mut WoState| {
|
calloop_handle.insert_source(xwayland, move |event, _, state: &mut WoState| {
|
||||||
|
|
@ -216,15 +343,40 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
Err(e) => error!("X11Wm::start_wm failed: {e:#}"),
|
Err(e) => error!("X11Wm::start_wm failed: {e:#}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !electron_started {
|
||||||
|
spawn_nested_electron_windows(
|
||||||
|
state,
|
||||||
|
&startup_config,
|
||||||
|
&startup_app_dir,
|
||||||
|
&startup_render_node,
|
||||||
|
width as u32,
|
||||||
|
height as u32,
|
||||||
|
"wayland",
|
||||||
|
);
|
||||||
|
electron_started = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
XWaylandEvent::Error => {
|
XWaylandEvent::Error => {
|
||||||
warn!("XWayland server encountered an error during startup");
|
warn!("XWayland server encountered an error during startup");
|
||||||
|
if !electron_started {
|
||||||
|
spawn_nested_electron_windows(
|
||||||
|
state,
|
||||||
|
&startup_config,
|
||||||
|
&startup_app_dir,
|
||||||
|
&startup_render_node,
|
||||||
|
width as u32,
|
||||||
|
height as u32,
|
||||||
|
"wayland",
|
||||||
|
);
|
||||||
|
electron_started = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
error!("Failed to register XWayland event source: {e:#}");
|
error!("Failed to register XWayland event source: {e:#}");
|
||||||
} else {
|
} else {
|
||||||
|
xwayland_source_registered = true;
|
||||||
info!("XWayland server starting");
|
info!("XWayland server starting");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -236,47 +388,16 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
std::env::set_var("WAYLAND_DISPLAY", &config.compositor.socket_name);
|
std::env::set_var("WAYLAND_DISPLAY", &config.compositor.socket_name);
|
||||||
info!("Set WAYLAND_DISPLAY={}", config.compositor.socket_name);
|
info!("Set WAYLAND_DISPLAY={}", config.compositor.socket_name);
|
||||||
crate::state::propagate_environment(&["WAYLAND_DISPLAY"]);
|
crate::state::propagate_environment(&["WAYLAND_DISPLAY"]);
|
||||||
|
if !xwayland_source_registered {
|
||||||
let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron");
|
spawn_nested_electron_windows(
|
||||||
// Nested mode has no DRM card; use the first available render node.
|
&mut state,
|
||||||
let nested_render_node = std::fs::read_dir("/dev/dri")
|
&config,
|
||||||
.ok()
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|e| e.filter_map(Result::ok))
|
|
||||||
.find(|e| e.file_name().to_str().map_or(false, |n| n.starts_with("renderD")))
|
|
||||||
.map(|e| e.path().to_string_lossy().into_owned())
|
|
||||||
.unwrap_or_else(|| "/dev/dri/renderD128".to_string());
|
|
||||||
|
|
||||||
for root_cfg in &config.root {
|
|
||||||
let mut full = root_cfg.clone();
|
|
||||||
full.x = 0;
|
|
||||||
full.y = 0;
|
|
||||||
full.width = width as u32;
|
|
||||||
full.height = height as u32;
|
|
||||||
full.z_order = std::i32::MIN / 2;
|
|
||||||
match ElectronProcess::spawn(
|
|
||||||
&full,
|
|
||||||
&config.compositor.electron_path,
|
|
||||||
&app_dir,
|
&app_dir,
|
||||||
&config.compositor.ipc_socket,
|
|
||||||
&nested_render_node,
|
&nested_render_node,
|
||||||
) {
|
width as u32,
|
||||||
Ok(proc) => state.electron_processes.push(proc),
|
height as u32,
|
||||||
Err(e) => error!(window = %full.name, "failed to spawn Electron root: {e:#}"),
|
"wayland",
|
||||||
}
|
);
|
||||||
}
|
|
||||||
|
|
||||||
for win_cfg in &config.windows {
|
|
||||||
match ElectronProcess::spawn(
|
|
||||||
win_cfg,
|
|
||||||
&config.compositor.electron_path,
|
|
||||||
&app_dir,
|
|
||||||
&config.compositor.ipc_socket,
|
|
||||||
&nested_render_node,
|
|
||||||
) {
|
|
||||||
Ok(proc) => state.electron_processes.push(proc),
|
|
||||||
Err(e) => error!(window = %win_cfg.name, "failed to spawn Electron: {e:#}"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut autostart_tasks: Vec<(crate::config::AutostartConfig, std::time::Instant)> = Vec::new();
|
let mut autostart_tasks: Vec<(crate::config::AutostartConfig, std::time::Instant)> = Vec::new();
|
||||||
|
|
@ -287,18 +408,20 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let bg = Color32F::from(config.compositor.background);
|
let bg = Color32F::from(config.compositor.background);
|
||||||
let frame_time = Duration::from_millis(16);
|
let mut frame_time = frame_duration(refresh_rate);
|
||||||
let mut last_render = std::time::Instant::now();
|
let mut last_render = std::time::Instant::now();
|
||||||
let event_start_time = std::time::Instant::now();
|
let event_start_time = std::time::Instant::now();
|
||||||
|
|
||||||
let mut gles_tex_cache: HashMap<String, GlesTexture> = HashMap::new();
|
let mut gles_tex_cache: HashMap<String, GlesTexture> = HashMap::new();
|
||||||
let mut offscreen_tex_cache: HashMap<String, (GlesTexture, i32, i32)> = HashMap::new();
|
let mut offscreen_tex_cache: HashMap<String, (GlesTexture, i32, i32)> = HashMap::new();
|
||||||
|
let mut logged_first_frame_import = false;
|
||||||
|
let mut logged_first_texture_draw = false;
|
||||||
|
let mut logged_first_output_submit = false;
|
||||||
use crate::electron::ElectronFrame;
|
use crate::electron::ElectronFrame;
|
||||||
let mut latest_frames: HashMap<String, ElectronFrame> = HashMap::new();
|
let mut latest_frames: HashMap<String, ElectronFrame> = HashMap::new();
|
||||||
let mut last_wayland_elem_count: usize = 0;
|
let mut last_wayland_elem_count: usize = 0;
|
||||||
// Frame tracking for backpressure management
|
// Frame tracking for backpressure management
|
||||||
let mut frame_timestamps: HashMap<String, Instant> = HashMap::new();
|
let mut frame_timestamps: HashMap<String, Instant> = HashMap::new();
|
||||||
let _max_frame_age = Duration::from_millis(33); // Drop frames older than 2 frames @60Hz
|
|
||||||
// Track previous cursor position to detect movement
|
// Track previous cursor position to detect movement
|
||||||
let mut last_cursor_pos: (f64, f64) = (0.0, 0.0);
|
let mut last_cursor_pos: (f64, f64) = (0.0, 0.0);
|
||||||
|
|
||||||
|
|
@ -315,6 +438,22 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let current_refresh = refresh_millihz(&backend);
|
||||||
|
if current_refresh != refresh_rate {
|
||||||
|
refresh_rate = current_refresh;
|
||||||
|
frame_time = frame_duration(refresh_rate);
|
||||||
|
let mode = OutputMode {
|
||||||
|
size: (state.output_size.0 as i32, state.output_size.1 as i32).into(),
|
||||||
|
refresh: refresh_rate as i32,
|
||||||
|
};
|
||||||
|
output.change_current_state(Some(mode), None, None, None);
|
||||||
|
output.set_preferred(mode);
|
||||||
|
info!(
|
||||||
|
refresh_hz = refresh_rate as f64 / 1000.0,
|
||||||
|
"nested display refresh changed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let winit_t0 = std::time::Instant::now();
|
let winit_t0 = std::time::Instant::now();
|
||||||
let _dispatch_result = winit_evt_loop.dispatch_new_events(|event| {
|
let _dispatch_result = winit_evt_loop.dispatch_new_events(|event| {
|
||||||
match event {
|
match event {
|
||||||
|
|
@ -322,7 +461,7 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
info!("Window resized to {}x{}", size.w, size.h);
|
info!("Window resized to {}x{}", size.w, size.h);
|
||||||
let new_mode = OutputMode {
|
let new_mode = OutputMode {
|
||||||
size: (size.w as i32, size.h as i32).into(),
|
size: (size.w as i32, size.h as i32).into(),
|
||||||
refresh: 60_000,
|
refresh: refresh_rate as i32,
|
||||||
};
|
};
|
||||||
output.change_current_state(Some(new_mode), None, None, None);
|
output.change_current_state(Some(new_mode), None, None, None);
|
||||||
}
|
}
|
||||||
|
|
@ -409,6 +548,7 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
}
|
}
|
||||||
state.debug_new_toplevel = false;
|
state.debug_new_toplevel = false;
|
||||||
|
|
||||||
|
let dispatch_elapsed = dispatch_t0.elapsed();
|
||||||
let calloop_t0 = std::time::Instant::now();
|
let calloop_t0 = std::time::Instant::now();
|
||||||
if let Err(e) = calloop_loop.dispatch(Duration::ZERO, &mut state) {
|
if let Err(e) = calloop_loop.dispatch(Duration::ZERO, &mut state) {
|
||||||
warn!("calloop dispatch error: {e}");
|
warn!("calloop dispatch error: {e}");
|
||||||
|
|
@ -426,7 +566,6 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
);
|
);
|
||||||
last_wayland_elem_count = cur_wayland_count;
|
last_wayland_elem_count = cur_wayland_count;
|
||||||
}
|
}
|
||||||
let dispatch_elapsed = dispatch_t0.elapsed();
|
|
||||||
if dispatch_elapsed.as_millis() > 50 {
|
if dispatch_elapsed.as_millis() > 50 {
|
||||||
warn!(
|
warn!(
|
||||||
"dispatch_clients+flush took {}ms!",
|
"dispatch_clients+flush took {}ms!",
|
||||||
|
|
@ -450,7 +589,8 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
| ElectronMessage::ForwardedKeyboard { .. }
|
| ElectronMessage::ForwardedKeyboard { .. }
|
||||||
| ElectronMessage::ForwardedPointerButton { .. }
|
| ElectronMessage::ForwardedPointerButton { .. }
|
||||||
| ElectronMessage::ForwardedRelativePointer { .. }
|
| ElectronMessage::ForwardedRelativePointer { .. }
|
||||||
| ElectronMessage::ForwardedPointerScroll { .. } => {
|
| ElectronMessage::ForwardedPointerScroll { .. }
|
||||||
|
| ElectronMessage::ForwardedPointerLeave { .. } => {
|
||||||
input_batch.push(msg);
|
input_batch.push(msg);
|
||||||
input_events_pending = true;
|
input_events_pending = true;
|
||||||
}
|
}
|
||||||
|
|
@ -472,16 +612,22 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
|
|
||||||
// Drop frames that are too old (backpressure)
|
// Drop frames that are too old (backpressure)
|
||||||
if let Some(last_ts) = frame_timestamps.get(&nm) {
|
if let Some(last_ts) = frame_timestamps.get(&nm) {
|
||||||
if now.duration_since(*last_ts) < Duration::from_millis(8) {
|
if now.duration_since(*last_ts) < frame_time {
|
||||||
// Skip frame if less than 8ms since last update (>120fps throttle)
|
|
||||||
trace!("Dropping frame {} due to backpressure", nm);
|
trace!("Dropping frame {} due to backpressure", nm);
|
||||||
|
if let Some(ref ipc) = state.electron_ipc {
|
||||||
|
let _ = ipc.send_frame_ack(&nm, frame.seq);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
frame_timestamps.insert(nm.clone(), now);
|
frame_timestamps.insert(nm.clone(), now);
|
||||||
// Only keep the latest frame per window
|
// Only keep the latest frame per window
|
||||||
latest_frames.insert(nm, frame);
|
if let Some(stale) = latest_frames.insert(nm.clone(), frame) {
|
||||||
|
if let Some(ref ipc) = state.electron_ipc {
|
||||||
|
let _ = ipc.send_frame_ack(&nm, stale.seq);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -533,6 +679,7 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
Ok(status) => {
|
Ok(status) => {
|
||||||
let name = state.electron_processes[idx].name.clone();
|
let name = state.electron_processes[idx].name.clone();
|
||||||
info!(window = %name, pid = pid, "Electron process exited: {:?}", status);
|
info!(window = %name, pid = pid, "Electron process exited: {:?}", status);
|
||||||
|
state.current_frame_seq.remove(&name);
|
||||||
let mut proc = state.electron_processes.remove(idx);
|
let mut proc = state.electron_processes.remove(idx);
|
||||||
let _ = proc.child.try_wait();
|
let _ = proc.child.try_wait();
|
||||||
state.metadata_dirty = true;
|
state.metadata_dirty = true;
|
||||||
|
|
@ -609,19 +756,37 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
|
|
||||||
for (_, frame) in latest_frames.drain() {
|
for (_, frame) in latest_frames.drain() {
|
||||||
let win_name = frame.name.clone();
|
let win_name = frame.name.clone();
|
||||||
|
let frame_seq = frame.seq;
|
||||||
let (width, height) = (frame.width, frame.height);
|
let (width, height) = (frame.width, frame.height);
|
||||||
|
let mut imported = false;
|
||||||
|
|
||||||
// Import each new frame - each has unique pixel data even if size unchanged
|
// Import each new frame - each has unique pixel data even if size unchanged
|
||||||
match import_electron_frame(renderer, frame) {
|
match import_electron_frame(renderer, frame) {
|
||||||
Ok((tex, cached)) => {
|
Ok((tex, cached)) => {
|
||||||
trace!("Frame imported for {} ({}x{})", win_name, width, height);
|
trace!("Frame imported for {} ({}x{})", win_name, width, height);
|
||||||
|
if !logged_first_frame_import {
|
||||||
|
info!(window = %win_name, width, height, seq = frame_seq, "first Electron frame imported as a DMA-BUF texture");
|
||||||
|
logged_first_frame_import = true;
|
||||||
|
}
|
||||||
state.texture_cache.insert_dmabuf(cached);
|
state.texture_cache.insert_dmabuf(cached);
|
||||||
gles_tex_cache.insert(win_name.clone(), tex.texture);
|
gles_tex_cache.insert(win_name.clone(), tex.texture);
|
||||||
|
imported = true;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("DMABUF import failed for {}: {e:#}", win_name);
|
warn!("DMABUF import failed for {}: {e:#}", win_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if imported {
|
||||||
|
if let Some(previous_seq) =
|
||||||
|
state.current_frame_seq.insert(win_name.clone(), frame_seq)
|
||||||
|
{
|
||||||
|
if let Some(ref ipc) = state.electron_ipc {
|
||||||
|
let _ = ipc.send_frame_ack(&win_name, previous_seq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(ref ipc) = state.electron_ipc {
|
||||||
|
let _ = ipc.send_frame_ack(&win_name, frame_seq);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evict textures for windows no longer in the DMABUF cache.
|
// Evict textures for windows no longer in the DMABUF cache.
|
||||||
|
|
@ -633,15 +798,22 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
if frame_dirty {
|
if frame_dirty {
|
||||||
// Prepare cursor elements before starting the frame
|
// Prepare cursor elements before starting the frame
|
||||||
let cursor_elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> =
|
let cursor_elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> =
|
||||||
if let smithay::input::pointer::CursorImageStatus::Surface(ref cursor_surface) = state.cursor_status {
|
if let smithay::input::pointer::CursorImageStatus::Surface(ref cursor_surface) =
|
||||||
use smithay::backend::renderer::element::{surface::render_elements_from_surface_tree, Kind};
|
state.cursor_status
|
||||||
|
{
|
||||||
|
use smithay::backend::renderer::element::{
|
||||||
|
surface::render_elements_from_surface_tree, Kind,
|
||||||
|
};
|
||||||
use smithay::utils::Scale as ScaleF;
|
use smithay::utils::Scale as ScaleF;
|
||||||
|
|
||||||
let scale = ScaleF::from(1.0);
|
let scale = ScaleF::from(1.0);
|
||||||
render_elements_from_surface_tree(
|
render_elements_from_surface_tree(
|
||||||
renderer,
|
renderer,
|
||||||
cursor_surface,
|
cursor_surface,
|
||||||
(state.pointer_location.x as i32, state.pointer_location.y as i32),
|
(
|
||||||
|
state.pointer_location.x as i32,
|
||||||
|
state.pointer_location.y as i32,
|
||||||
|
),
|
||||||
scale,
|
scale,
|
||||||
1.0,
|
1.0,
|
||||||
Kind::Cursor,
|
Kind::Cursor,
|
||||||
|
|
@ -651,17 +823,29 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load themed cursor texture for Named cursor status
|
// Load themed cursor texture for Named cursor status
|
||||||
let themed_cursor = if let smithay::input::pointer::CursorImageStatus::Named(ref icon) = state.cursor_status {
|
let themed_cursor =
|
||||||
state.cursor_theme_manager.get_cursor(icon.name(), renderer).cloned()
|
if let smithay::input::pointer::CursorImageStatus::Named(ref icon) =
|
||||||
} else if cursor_elements.is_empty() {
|
state.cursor_status
|
||||||
if !matches!(state.cursor_status, smithay::input::pointer::CursorImageStatus::Hidden) {
|
{
|
||||||
state.cursor_theme_manager.get_cursor("default", renderer).cloned()
|
state
|
||||||
|
.cursor_theme_manager
|
||||||
|
.get_cursor(icon.name(), renderer)
|
||||||
|
.cloned()
|
||||||
|
} else if cursor_elements.is_empty() {
|
||||||
|
if !matches!(
|
||||||
|
state.cursor_status,
|
||||||
|
smithay::input::pointer::CursorImageStatus::Hidden
|
||||||
|
) {
|
||||||
|
state
|
||||||
|
.cursor_theme_manager
|
||||||
|
.get_cursor("default", renderer)
|
||||||
|
.cloned()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Wayland windows are exclusively rendered by comraw via off-screen buffers.
|
// Wayland windows are exclusively rendered by comraw via off-screen buffers.
|
||||||
// The native space.elements() rendering pass has been removed here.
|
// The native space.elements() rendering pass has been removed here.
|
||||||
|
|
@ -716,7 +900,12 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
1.0,
|
1.0,
|
||||||
) {
|
) {
|
||||||
warn!("Failed to render texture for {}: {}", win.name, e);
|
warn!("Failed to render texture for {}: {}", win.name, e);
|
||||||
|
} else if !logged_first_texture_draw {
|
||||||
|
info!(window = %win.name, x = win.x, y = win.y, output_width = size.w, output_height = size.h, "first Electron texture rendered into the output frame");
|
||||||
|
logged_first_texture_draw = true;
|
||||||
}
|
}
|
||||||
|
} else if win.name == "main" && !logged_first_frame_import {
|
||||||
|
trace!(window = %win.name, mapped, "desktop root has no imported texture yet");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -829,29 +1018,32 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
|
|
||||||
// Extract Wayland damage rects for partial-update optimization.
|
// Extract Wayland damage rects for partial-update optimization.
|
||||||
// Empty damage = full frame (surfaces in dirty list always get exported).
|
// Empty damage = full frame (surfaces in dirty list always get exported).
|
||||||
let damage_rects: Vec<crate::state::DamageRect> = with_states(&surface, |states| {
|
let damage_rects: Vec<crate::state::DamageRect> =
|
||||||
let mut attrs = states.cached_state.get::<SurfaceAttributes>();
|
with_states(&surface, |states| {
|
||||||
let current = attrs.current();
|
let mut attrs = states.cached_state.get::<SurfaceAttributes>();
|
||||||
current.damage.iter().map(|d| {
|
let current = attrs.current();
|
||||||
match d {
|
current
|
||||||
Damage::Buffer(rect) => crate::state::DamageRect {
|
.damage
|
||||||
x: rect.loc.x,
|
.iter()
|
||||||
y: rect.loc.y,
|
.map(|d| match d {
|
||||||
width: rect.size.w,
|
Damage::Buffer(rect) => crate::state::DamageRect {
|
||||||
height: rect.size.h,
|
x: rect.loc.x,
|
||||||
},
|
y: rect.loc.y,
|
||||||
Damage::Surface(rect) => {
|
width: rect.size.w,
|
||||||
let scale = current.buffer_scale;
|
height: rect.size.h,
|
||||||
crate::state::DamageRect {
|
},
|
||||||
x: rect.loc.x * scale,
|
Damage::Surface(rect) => {
|
||||||
y: rect.loc.y * scale,
|
let scale = current.buffer_scale;
|
||||||
width: rect.size.w * scale,
|
crate::state::DamageRect {
|
||||||
height: rect.size.h * scale,
|
x: rect.loc.x * scale,
|
||||||
|
y: rect.loc.y * scale,
|
||||||
|
width: rect.size.w * scale,
|
||||||
|
height: rect.size.h * scale,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
.collect()
|
||||||
}).collect()
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate bbox (bounding box) for CSD windows with shadows/decorations
|
// Calculate bbox (bounding box) for CSD windows with shadows/decorations
|
||||||
let bbox_w = (geo.size.w + geo.loc.x.max(0) * 2) as u32;
|
let bbox_w = (geo.size.w + geo.loc.x.max(0) * 2) as u32;
|
||||||
|
|
@ -914,8 +1106,7 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
let src = unsafe { std::slice::from_raw_parts(ptr, pixel_bytes) };
|
let src = unsafe { std::slice::from_raw_parts(ptr, pixel_bytes) };
|
||||||
|
|
||||||
// Determine whether to do a partial (damage-rect) or full copy.
|
// Determine whether to do a partial (damage-rect) or full copy.
|
||||||
let do_partial = !damage_rects.is_empty()
|
let do_partial = !damage_rects.is_empty() && damage_rects.len() < 64;
|
||||||
&& damage_rects.len() < 64;
|
|
||||||
|
|
||||||
if do_partial {
|
if do_partial {
|
||||||
// Partial update: only copy damaged rows with BGRA→RGBA swap
|
// Partial update: only copy damaged rows with BGRA→RGBA swap
|
||||||
|
|
@ -931,9 +1122,9 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
let si = src_row_off + col * 4;
|
let si = src_row_off + col * 4;
|
||||||
let di = dst_row_off + col * 4;
|
let di = dst_row_off + col * 4;
|
||||||
// BGRA → RGBA: swap B↔R
|
// BGRA → RGBA: swap B↔R
|
||||||
dst[di] = src[si + 2]; // R
|
dst[di] = src[si + 2]; // R
|
||||||
dst[di + 1] = src[si + 1]; // G
|
dst[di + 1] = src[si + 1]; // G
|
||||||
dst[di + 2] = src[si]; // B
|
dst[di + 2] = src[si]; // B
|
||||||
dst[di + 3] = src[si + 3]; // A
|
dst[di + 3] = src[si + 3]; // A
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -946,9 +1137,9 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
for col in 0..bw as usize {
|
for col in 0..bw as usize {
|
||||||
let si = src_row_off + col * 4;
|
let si = src_row_off + col * 4;
|
||||||
let di = dst_row_off + col * 4;
|
let di = dst_row_off + col * 4;
|
||||||
dst[di] = src[si + 2]; // R
|
dst[di] = src[si + 2]; // R
|
||||||
dst[di + 1] = src[si + 1]; // G
|
dst[di + 1] = src[si + 1]; // G
|
||||||
dst[di + 2] = src[si]; // B
|
dst[di + 2] = src[si]; // B
|
||||||
dst[di + 3] = src[si + 3]; // A
|
dst[di + 3] = src[si + 3]; // A
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -972,13 +1163,18 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
let rss_data = states.data_map.get::<RendererSurfaceStateUserData>();
|
let rss_data = states.data_map.get::<RendererSurfaceStateUserData>();
|
||||||
rss_data
|
rss_data
|
||||||
.and_then(|d| d.lock().ok())
|
.and_then(|d| d.lock().ok())
|
||||||
.and_then(|rss| rss.buffer().map(|b| {
|
.and_then(|rss| {
|
||||||
let mut r = (0u32, 0u32);
|
rss.buffer().map(|b| {
|
||||||
let _ = with_buffer_contents(&b, |_ptr, _len, spec| {
|
let mut r = (0u32, 0u32);
|
||||||
r = (spec.width.max(0) as u32, spec.height.max(0) as u32);
|
let _ = with_buffer_contents(&b, |_ptr, _len, spec| {
|
||||||
});
|
r = (
|
||||||
r
|
spec.width.max(0) as u32,
|
||||||
}))
|
spec.height.max(0) as u32,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
r
|
||||||
|
})
|
||||||
|
})
|
||||||
.unwrap_or((w, h))
|
.unwrap_or((w, h))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1107,7 +1303,6 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
state.dirty_surfaces.clear();
|
state.dirty_surfaces.clear();
|
||||||
} // inner bind scope ends here, backend borrow is released
|
} // inner bind scope ends here, backend borrow is released
|
||||||
|
|
||||||
|
|
||||||
let did_bind = render_ok;
|
let did_bind = render_ok;
|
||||||
|
|
||||||
if did_bind {
|
if did_bind {
|
||||||
|
|
@ -1124,6 +1319,10 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
trace!("Frame submitted successfully");
|
trace!("Frame submitted successfully");
|
||||||
|
if !logged_first_output_submit {
|
||||||
|
info!("first nested output frame submitted successfully");
|
||||||
|
logged_first_output_submit = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let submit_ms = submit_t0.elapsed().as_millis();
|
let submit_ms = submit_t0.elapsed().as_millis();
|
||||||
if submit_ms > 50 {
|
if submit_ms > 50 {
|
||||||
|
|
@ -1154,7 +1353,11 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
// some of the budget; sleep the rest so the next iteration starts close
|
// some of the budget; sleep the rest so the next iteration starts close
|
||||||
// to the target frame boundary.
|
// to the target frame boundary.
|
||||||
let elapsed = loop_t0.elapsed();
|
let elapsed = loop_t0.elapsed();
|
||||||
let target = if input_events_pending { Duration::from_millis(8) } else { frame_time };
|
let target = if input_events_pending {
|
||||||
|
Duration::from_millis(8)
|
||||||
|
} else {
|
||||||
|
frame_time
|
||||||
|
};
|
||||||
if elapsed < target {
|
if elapsed < target {
|
||||||
let remaining = target - elapsed;
|
let remaining = target - elapsed;
|
||||||
// Don't sleep more than 4ms to stay responsive to new Wayland/IPC events
|
// Don't sleep more than 4ms to stay responsive to new Wayland/IPC events
|
||||||
|
|
@ -1164,7 +1367,15 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
// Log if the entire loop iteration was slow.
|
// Log if the entire loop iteration was slow.
|
||||||
let total_loop_ms = loop_t0.elapsed().as_millis();
|
let total_loop_ms = loop_t0.elapsed().as_millis();
|
||||||
if total_loop_ms > 50 {
|
if total_loop_ms > 50 {
|
||||||
warn!("SLOW LOOP iter={} total={}ms", loop_iter, total_loop_ms);
|
warn!(
|
||||||
|
"SLOW LOOP iter={} total={}ms wayland_dispatch={}ms calloop={}ms electron_messages={}ms render_callbacks={}ms",
|
||||||
|
loop_iter,
|
||||||
|
total_loop_ms,
|
||||||
|
dispatch_elapsed.as_millis(),
|
||||||
|
calloop_ms,
|
||||||
|
emsg_ms,
|
||||||
|
render_total_ms,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1189,7 +1400,11 @@ fn execute_autostart_task(
|
||||||
.ok()
|
.ok()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|e| e.filter_map(Result::ok))
|
.flat_map(|e| e.filter_map(Result::ok))
|
||||||
.find(|e| e.file_name().to_str().map_or(false, |n| n.starts_with("renderD")))
|
.find(|e| {
|
||||||
|
e.file_name()
|
||||||
|
.to_str()
|
||||||
|
.map_or(false, |n| n.starts_with("renderD"))
|
||||||
|
})
|
||||||
.map(|e| e.path().to_string_lossy().into_owned())
|
.map(|e| e.path().to_string_lossy().into_owned())
|
||||||
.unwrap_or_else(|| "/dev/dri/renderD128".to_string());
|
.unwrap_or_else(|| "/dev/dri/renderD128".to_string());
|
||||||
match ElectronProcess::spawn(
|
match ElectronProcess::spawn(
|
||||||
|
|
@ -1198,23 +1413,21 @@ fn execute_autostart_task(
|
||||||
app_dir,
|
app_dir,
|
||||||
&state.config.compositor.ipc_socket,
|
&state.config.compositor.ipc_socket,
|
||||||
&render_node,
|
&render_node,
|
||||||
|
"wayland",
|
||||||
) {
|
) {
|
||||||
Ok(proc) => state.electron_processes.push(proc),
|
Ok(proc) => state.electron_processes.push(proc),
|
||||||
Err(e) => error!(window = %name, "autostart spawn failed: {e:#}"),
|
Err(e) => error!(window = %name, "autostart spawn failed: {e:#}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if cmd == "wo://portal/start" {
|
} else if cmd == "wo://portal/start" {
|
||||||
let portal_bin = std::env::current_exe()
|
let portal_bin = crate::companion_binary("wo-portal");
|
||||||
.ok()
|
|
||||||
.and_then(|p| p.parent().map(|d| d.join("wo-portal")))
|
|
||||||
.unwrap_or_else(|| std::path::PathBuf::from("wo-portal"));
|
|
||||||
match std::process::Command::new(&portal_bin).spawn() {
|
match std::process::Command::new(&portal_bin).spawn() {
|
||||||
Ok(_) => info!("Spawned portal service: {:?}", portal_bin),
|
Ok(_) => info!("Spawned portal service: {:?}", portal_bin),
|
||||||
Err(e) => error!("Failed to spawn portal: {e:#}"),
|
Err(e) => error!("Failed to spawn portal: {e:#}"),
|
||||||
}
|
}
|
||||||
} else if let Some(path) = cmd.strip_prefix("file://") {
|
} else if let Some(path) = cmd.strip_prefix("file://") {
|
||||||
let _ = std::process::Command::new(path).spawn();
|
let _ = std::process::Command::new(path).spawn();
|
||||||
} else if cmd.contains(' ') {
|
} else if crate::command_uses_shell(cmd) {
|
||||||
let _ = std::process::Command::new("sh").arg("-c").arg(cmd).spawn();
|
let _ = std::process::Command::new("sh").arg("-c").arg(cmd).spawn();
|
||||||
} else {
|
} else {
|
||||||
let _ = std::process::Command::new(cmd).spawn();
|
let _ = std::process::Command::new(cmd).spawn();
|
||||||
|
|
@ -1241,6 +1454,23 @@ fn process_electron_message(
|
||||||
);
|
);
|
||||||
state.metadata_dirty = true;
|
state.metadata_dirty = true;
|
||||||
}
|
}
|
||||||
|
ElectronMessage::SiteHashes {
|
||||||
|
window_name,
|
||||||
|
update,
|
||||||
|
} => {
|
||||||
|
use sha2::Digest;
|
||||||
|
if !update.index_bytes.is_empty() {
|
||||||
|
let hashes = crate::electron::SiteHashes {
|
||||||
|
page_url: update.page_url,
|
||||||
|
index_sha256: format!("{:x}", sha2::Sha256::digest(&update.index_bytes)),
|
||||||
|
styles_sha256: format!("{:x}", sha2::Sha256::digest(&update.styles_bytes)),
|
||||||
|
};
|
||||||
|
info!(window = %window_name, page = %hashes.page_url, "computed and registered document and stylesheet hashes");
|
||||||
|
state.site_hashes.insert(window_name, hashes);
|
||||||
|
} else {
|
||||||
|
warn!(window = %window_name, "rejected empty site assets");
|
||||||
|
}
|
||||||
|
}
|
||||||
ElectronMessage::Syscall(req) => {
|
ElectronMessage::Syscall(req) => {
|
||||||
info!(window = %req.window_name, payload = %req.payload, "syscall received");
|
info!(window = %req.window_name, payload = %req.payload, "syscall received");
|
||||||
if let Some(ref handler) = state.syscall_handler {
|
if let Some(ref handler) = state.syscall_handler {
|
||||||
|
|
@ -1310,7 +1540,8 @@ fn process_electron_message(
|
||||||
.unwrap_or(0) as i32;
|
.unwrap_or(0) as i32;
|
||||||
// comraw reports the MacWindow origin (title bar top);
|
// comraw reports the MacWindow origin (title bar top);
|
||||||
// the Wayland surface sits th pixels below that.
|
// the Wayland surface sits th pixels below that.
|
||||||
let prev_loc = state.space.element_location(&window).unwrap_or_default();
|
let prev_loc =
|
||||||
|
state.space.element_location(&window).unwrap_or_default();
|
||||||
state.space.map_element(window.clone(), (x, y + th), false);
|
state.space.map_element(window.clone(), (x, y + th), false);
|
||||||
state.translate_dialog_children(
|
state.translate_dialog_children(
|
||||||
&window,
|
&window,
|
||||||
|
|
@ -1436,11 +1667,20 @@ fn process_electron_message(
|
||||||
smithay::utils::user_data::UserDataMap::new(),
|
smithay::utils::user_data::UserDataMap::new(),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
state.request_activation(token, token_data, surface.clone());
|
state.request_activation(
|
||||||
|
token,
|
||||||
|
token_data,
|
||||||
|
surface.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(keyboard) = state.seat.get_keyboard() {
|
if let Some(keyboard) = state.seat.get_keyboard() {
|
||||||
let serial = smithay::utils::SERIAL_COUNTER.next_serial();
|
let serial =
|
||||||
keyboard.set_focus(state, Some(surface.clone()), serial);
|
smithay::utils::SERIAL_COUNTER.next_serial();
|
||||||
|
keyboard.set_focus(
|
||||||
|
state,
|
||||||
|
Some(surface.clone()),
|
||||||
|
serial,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(name) = target_name.as_ref() {
|
if let Some(name) = target_name.as_ref() {
|
||||||
|
|
@ -1489,25 +1729,27 @@ fn process_electron_message(
|
||||||
if window.wl_surface().is_none() {
|
if window.wl_surface().is_none() {
|
||||||
// Surface not ready yet.
|
// Surface not ready yet.
|
||||||
} else if let Some(loc) = state.space.element_location(&window) {
|
} else if let Some(loc) = state.space.element_location(&window) {
|
||||||
let px = payload_json
|
let mut px = payload_json
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|p| p.get("x"))
|
.and_then(|p| p.get("x"))
|
||||||
.and_then(|v| v.as_f64())
|
.and_then(|v| v.as_f64())
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
let py = payload_json
|
let mut py = payload_json
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|p| p.get("y"))
|
.and_then(|p| p.get("y"))
|
||||||
.and_then(|v| v.as_f64())
|
.and_then(|v| v.as_f64())
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
let geometry_offset = window.geometry().loc;
|
||||||
|
px += geometry_offset.x as f64;
|
||||||
|
py += geometry_offset.y as f64;
|
||||||
|
|
||||||
// Client dictates event dispatch; compositor
|
// Client dictates event dispatch; compositor
|
||||||
// owns pointer_location (hardware). Compute
|
// owns pointer_location (hardware). Compute
|
||||||
// a synthetic location for Smithay without
|
// a synthetic location for Smithay without
|
||||||
// overwriting pointer_location.
|
// overwriting pointer_location.
|
||||||
let dispatch_location: Point<f64, Logical> = (
|
let dispatch_location: Point<f64, Logical> =
|
||||||
loc.x as f64 + px,
|
(loc.x as f64 + px, loc.y as f64 + py).into();
|
||||||
loc.y as f64 + py,
|
|
||||||
).into();
|
|
||||||
|
|
||||||
let under = window.surface_under(
|
let under = window.surface_under(
|
||||||
(px, py),
|
(px, py),
|
||||||
|
|
@ -1552,22 +1794,24 @@ fn process_electron_message(
|
||||||
.and_then(|p| p.get("pressed"))
|
.and_then(|p| p.get("pressed"))
|
||||||
.and_then(|v| v.as_bool())
|
.and_then(|v| v.as_bool())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let px = payload_json
|
let mut px = payload_json
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|p| p.get("x"))
|
.and_then(|p| p.get("x"))
|
||||||
.and_then(|v| v.as_f64())
|
.and_then(|v| v.as_f64())
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
let py = payload_json
|
let mut py = payload_json
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|p| p.get("y"))
|
.and_then(|p| p.get("y"))
|
||||||
.and_then(|v| v.as_f64())
|
.and_then(|v| v.as_f64())
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
let geometry_offset = window.geometry().loc;
|
||||||
|
px += geometry_offset.x as f64;
|
||||||
|
py += geometry_offset.y as f64;
|
||||||
|
|
||||||
// Update pointer_location consistently
|
// Update pointer_location consistently
|
||||||
state.pointer_location = (
|
state.pointer_location =
|
||||||
loc.x as f64 + px,
|
(loc.x as f64 + px, loc.y as f64 + py).into();
|
||||||
loc.y as f64 + py,
|
|
||||||
).into();
|
|
||||||
|
|
||||||
// Send motion first so focus is correct for the button
|
// Send motion first so focus is correct for the button
|
||||||
let under = window.surface_under(
|
let under = window.surface_under(
|
||||||
|
|
@ -1578,7 +1822,8 @@ fn process_electron_message(
|
||||||
let focus_pos: Point<f64, Logical> = (
|
let focus_pos: Point<f64, Logical> = (
|
||||||
loc.x as f64 + off.x as f64,
|
loc.x as f64 + off.x as f64,
|
||||||
loc.y as f64 + off.y as f64,
|
loc.y as f64 + off.y as f64,
|
||||||
).into();
|
)
|
||||||
|
.into();
|
||||||
(s, focus_pos)
|
(s, focus_pos)
|
||||||
});
|
});
|
||||||
let time = event_start_time.elapsed().as_millis() as u32;
|
let time = event_start_time.elapsed().as_millis() as u32;
|
||||||
|
|
@ -1754,6 +1999,7 @@ fn process_electron_message(
|
||||||
proc.kill();
|
proc.kill();
|
||||||
}
|
}
|
||||||
state.texture_cache.remove(&action_msg.window_name);
|
state.texture_cache.remove(&action_msg.window_name);
|
||||||
|
state.current_frame_seq.remove(&action_msg.window_name);
|
||||||
state.window_positions.remove(&action_msg.window_name);
|
state.window_positions.remove(&action_msg.window_name);
|
||||||
state.window_mapped.remove(&action_msg.window_name);
|
state.window_mapped.remove(&action_msg.window_name);
|
||||||
if state.keyboard_window_focus.as_deref()
|
if state.keyboard_window_focus.as_deref()
|
||||||
|
|
@ -1804,10 +2050,16 @@ fn process_electron_message(
|
||||||
}
|
}
|
||||||
ElectronMessage::ForwardedPointerScroll {
|
ElectronMessage::ForwardedPointerScroll {
|
||||||
window_name,
|
window_name,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
dx,
|
dx,
|
||||||
dy,
|
dy,
|
||||||
|
time,
|
||||||
} => {
|
} => {
|
||||||
state.handle_forwarded_pointer_scroll(&window_name, dx, dy);
|
state.handle_forwarded_pointer_scroll(&window_name, x, y, dx, dy, time);
|
||||||
|
}
|
||||||
|
ElectronMessage::ForwardedPointerLeave { window_name } => {
|
||||||
|
state.handle_forwarded_pointer_leave(&window_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1836,11 +2088,14 @@ fn send_window_metadata(state: &WoState) {
|
||||||
"y": y,
|
"y": y,
|
||||||
"width": w,
|
"width": w,
|
||||||
"height": h,
|
"height": h,
|
||||||
|
"content_width": w,
|
||||||
|
"content_height": h,
|
||||||
"z_order": win_cfg.z_order,
|
"z_order": win_cfg.z_order,
|
||||||
"focused": is_focused,
|
"focused": is_focused,
|
||||||
"mapped": mapped,
|
"mapped": mapped,
|
||||||
"source": "electron",
|
"source": "electron",
|
||||||
"ssd": true,
|
"ssd": true,
|
||||||
|
"fullscreen": state.fullscreen_windows.contains(&win_cfg.name),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1887,20 +2142,28 @@ fn send_window_metadata(state: &WoState) {
|
||||||
let bbox_w = geo.size.w + geo.loc.x.max(0) * 2;
|
let bbox_w = geo.size.w + geo.loc.x.max(0) * 2;
|
||||||
let bbox_h = geo.size.h + geo.loc.y.max(0) * 2;
|
let bbox_h = geo.size.h + geo.loc.y.max(0) * 2;
|
||||||
|
|
||||||
let th = state.title_h_for_window(window);
|
let fullscreen = state.fullscreen_windows.contains(&window_name);
|
||||||
|
let th = if fullscreen {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
state.title_h_for_window(window)
|
||||||
|
};
|
||||||
let is_ssd = th > 0;
|
let is_ssd = th > 0;
|
||||||
|
|
||||||
let (parent_surface, parent_x11_id) = if let Some(x11) = window.x11_surface() {
|
let (parent_surface, parent_x11_id) = if let Some(x11) = window.x11_surface() {
|
||||||
(None, x11.is_transient_for())
|
(None, x11.is_transient_for())
|
||||||
} else {
|
} else {
|
||||||
(window.toplevel().and_then(|t| {
|
(
|
||||||
smithay::wayland::compositor::with_states(t.wl_surface(), |states| {
|
window.toplevel().and_then(|t| {
|
||||||
states
|
smithay::wayland::compositor::with_states(t.wl_surface(), |states| {
|
||||||
.data_map
|
states
|
||||||
.get::<smithay::wayland::shell::xdg::XdgToplevelSurfaceData>()
|
.data_map
|
||||||
.and_then(|d| d.lock().ok().and_then(|data| data.parent.clone()))
|
.get::<smithay::wayland::shell::xdg::XdgToplevelSurfaceData>()
|
||||||
})
|
.and_then(|d| d.lock().ok().and_then(|data| data.parent.clone()))
|
||||||
}), None)
|
})
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let parent_name = if let Some(parent_wl) = parent_surface.as_ref() {
|
let parent_name = if let Some(parent_wl) = parent_surface.as_ref() {
|
||||||
|
|
@ -1941,11 +2204,14 @@ fn send_window_metadata(state: &WoState) {
|
||||||
"y": loc.y - th - geo.loc.y.max(0),
|
"y": loc.y - th - geo.loc.y.max(0),
|
||||||
"width": bbox_w,
|
"width": bbox_w,
|
||||||
"height": bbox_h + th,
|
"height": bbox_h + th,
|
||||||
|
"content_width": geo.size.w,
|
||||||
|
"content_height": geo.size.h,
|
||||||
"z_order": space_idx,
|
"z_order": space_idx,
|
||||||
"focused": is_focused,
|
"focused": is_focused,
|
||||||
"mapped": mapped,
|
"mapped": mapped,
|
||||||
"source": if window.x11_surface().is_some() { "x11" } else { "wayland" },
|
"source": if window.x11_surface().is_some() { "x11" } else { "wayland" },
|
||||||
"ssd": is_ssd,
|
"ssd": is_ssd,
|
||||||
|
"fullscreen": fullscreen,
|
||||||
"dialog": is_dialog,
|
"dialog": is_dialog,
|
||||||
"parent_name": parent_name,
|
"parent_name": parent_name,
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
274
src/state.rs
274
src/state.rs
|
|
@ -8,7 +8,7 @@ use smithay::{
|
||||||
},
|
},
|
||||||
utils::{Logical, Point, SERIAL_COUNTER},
|
utils::{Logical, Point, SERIAL_COUNTER},
|
||||||
wayland::{
|
wayland::{
|
||||||
compositor::{CompositorClientState, CompositorState, with_states}, cursor_shape::CursorShapeManagerState, dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufState}, fractional_scale::{
|
compositor::{CompositorClientState, CompositorState, with_states}, cursor_shape::CursorShapeManagerState, dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal, DmabufState}, fractional_scale::{
|
||||||
self as fractional_scale_mod,
|
self as fractional_scale_mod,
|
||||||
FractionalScaleHandler, FractionalScaleManagerState,
|
FractionalScaleHandler, FractionalScaleManagerState,
|
||||||
}, output::OutputManagerState, selection::{
|
}, output::OutputManagerState, selection::{
|
||||||
|
|
@ -87,7 +87,11 @@ impl MappedShmSlot {
|
||||||
anyhow::bail!("mmap failed for MappedShmSlot");
|
anyhow::bail!("mmap failed for MappedShmSlot");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self { fd, ptr: ptr as *mut u8, len: size })
|
Ok(Self {
|
||||||
|
fd,
|
||||||
|
ptr: ptr as *mut u8,
|
||||||
|
len: size,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resize the slot, remapping if the new size differs.
|
/// Resize the slot, remapping if the new size differs.
|
||||||
|
|
@ -200,6 +204,72 @@ pub struct BackendData {
|
||||||
pub dmabuf_formats: Option<Vec<drm_fourcc::DrmFormat>>,
|
pub dmabuf_formats: Option<Vec<drm_fourcc::DrmFormat>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One compositor output in the global desktop coordinate space.
|
||||||
|
///
|
||||||
|
/// DRM and nested backends can publish multiple entries. The primary output
|
||||||
|
/// remains available through `WoState::output` for code paths that have not
|
||||||
|
/// yet been made output-aware.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MonitorOutput {
|
||||||
|
pub id: String,
|
||||||
|
pub output: Output,
|
||||||
|
pub location: Point<i32, Logical>,
|
||||||
|
pub size: (u32, u32),
|
||||||
|
pub primary: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MonitorOutput {
|
||||||
|
pub fn contains(&self, point: Point<f64, Logical>) -> bool {
|
||||||
|
let x = point.x - self.location.x as f64;
|
||||||
|
let y = point.y - self.location.y as f64;
|
||||||
|
x >= 0.0 && y >= 0.0 && x < self.size.0 as f64 && y < self.size.1 as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clamp_point_to_monitors(
|
||||||
|
monitors: &[MonitorOutput],
|
||||||
|
point: Point<f64, Logical>,
|
||||||
|
) -> Point<f64, Logical> {
|
||||||
|
if monitors.is_empty() || monitors.iter().any(|monitor| monitor.contains(point)) {
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
monitors
|
||||||
|
.iter()
|
||||||
|
.map(|monitor| {
|
||||||
|
let max_x = monitor.location.x as f64 + monitor.size.0.saturating_sub(1) as f64;
|
||||||
|
let max_y = monitor.location.y as f64 + monitor.size.1.saturating_sub(1) as f64;
|
||||||
|
let x = point.x.clamp(monitor.location.x as f64, max_x);
|
||||||
|
let y = point.y.clamp(monitor.location.y as f64, max_y);
|
||||||
|
let dx = point.x - x;
|
||||||
|
let dy = point.y - y;
|
||||||
|
(x, y, dx * dx + dy * dy)
|
||||||
|
})
|
||||||
|
.min_by(|a, b| a.2.total_cmp(&b.2))
|
||||||
|
.map(|(x, y, _)| (x, y).into())
|
||||||
|
.unwrap_or(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn monitor_desktop_bounds(monitors: &[MonitorOutput]) -> smithay::utils::Rectangle<i32, Logical> {
|
||||||
|
let Some(first) = monitors.first() else {
|
||||||
|
return smithay::utils::Rectangle::new((0, 0).into(), (0, 0).into());
|
||||||
|
};
|
||||||
|
let extent = |pixels: u32| pixels.min(i32::MAX as u32) as i32;
|
||||||
|
let mut left = first.location.x;
|
||||||
|
let mut top = first.location.y;
|
||||||
|
let mut right = left.saturating_add(extent(first.size.0));
|
||||||
|
let mut bottom = top.saturating_add(extent(first.size.1));
|
||||||
|
for monitor in monitors.iter().skip(1) {
|
||||||
|
left = left.min(monitor.location.x);
|
||||||
|
top = top.min(monitor.location.y);
|
||||||
|
right = right.max(monitor.location.x.saturating_add(extent(monitor.size.0)));
|
||||||
|
bottom = bottom.max(monitor.location.y.saturating_add(extent(monitor.size.1)));
|
||||||
|
}
|
||||||
|
smithay::utils::Rectangle::new(
|
||||||
|
(left, top).into(),
|
||||||
|
(right.saturating_sub(left), bottom.saturating_sub(top)).into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
impl BackendData {
|
impl BackendData {
|
||||||
pub fn renderer(&self) -> Option<std::sync::Arc<std::sync::Mutex<GlesRenderer>>> {
|
pub fn renderer(&self) -> Option<std::sync::Arc<std::sync::Mutex<GlesRenderer>>> {
|
||||||
self.renderer.clone()
|
self.renderer.clone()
|
||||||
|
|
@ -213,7 +283,7 @@ pub struct WoState {
|
||||||
pub xdg_decoration_state: XdgDecorationState,
|
pub xdg_decoration_state: XdgDecorationState,
|
||||||
pub shm_state: ShmState,
|
pub shm_state: ShmState,
|
||||||
pub dmabuf_state: DmabufState,
|
pub dmabuf_state: DmabufState,
|
||||||
// pub dmabuf_global: DmabufGlobal,
|
pub dmabuf_global: DmabufGlobal,
|
||||||
pub dmabuf_default_feedback: DmabufFeedback,
|
pub dmabuf_default_feedback: DmabufFeedback,
|
||||||
pub dmabuf_scanout_feedback: DmabufFeedback,
|
pub dmabuf_scanout_feedback: DmabufFeedback,
|
||||||
pub seat_state: SeatState<Self>,
|
pub seat_state: SeatState<Self>,
|
||||||
|
|
@ -238,10 +308,19 @@ pub struct WoState {
|
||||||
|
|
||||||
pub space: Space<Window>,
|
pub space: Space<Window>,
|
||||||
pub output: Output,
|
pub output: Output,
|
||||||
|
/// Outputs ordered with the primary monitor first, in global desktop coordinates.
|
||||||
|
pub monitors: Vec<MonitorOutput>,
|
||||||
|
|
||||||
pub electron_processes: Vec<ElectronProcess>,
|
pub electron_processes: Vec<ElectronProcess>,
|
||||||
pub texture_cache: TextureCache,
|
pub texture_cache: TextureCache,
|
||||||
|
/// Sequence of the Electron DMA-BUF currently retained for each window.
|
||||||
|
/// It is acknowledged only after a newer frame replaces it.
|
||||||
|
pub current_frame_seq: std::collections::HashMap<String, u64>,
|
||||||
pub window_positions: std::collections::HashMap<String, (i32, i32, u32, u32)>,
|
pub window_positions: std::collections::HashMap<String, (i32, i32, u32, u32)>,
|
||||||
|
/// Hashes of the verified document and theme stylesheet bound to each Electron window.
|
||||||
|
pub site_hashes: std::collections::HashMap<String, crate::electron::SiteHashes>,
|
||||||
|
pub fullscreen_restore: std::collections::HashMap<String, (i32, i32, u32, u32)>,
|
||||||
|
pub fullscreen_windows: std::collections::HashSet<String>,
|
||||||
pub electron_ipc: Option<ElectronIpc>,
|
pub electron_ipc: Option<ElectronIpc>,
|
||||||
pub syscall_handler: Option<SyscallHandler>,
|
pub syscall_handler: Option<SyscallHandler>,
|
||||||
pub portal: Option<std::sync::Arc<WoPortal>>,
|
pub portal: Option<std::sync::Arc<WoPortal>>,
|
||||||
|
|
@ -350,7 +429,7 @@ impl WoState {
|
||||||
let screencopy_state = Some(ScreencopyManagerState::new::<Self>(&dh));
|
let screencopy_state = Some(ScreencopyManagerState::new::<Self>(&dh));
|
||||||
|
|
||||||
let data_device_state = DataDeviceState::new::<Self>(&dh);
|
let data_device_state = DataDeviceState::new::<Self>(&dh);
|
||||||
let dmabuf_state = DmabufState::new();
|
let mut dmabuf_state = DmabufState::new();
|
||||||
|
|
||||||
// Get DMABUF formats from renderer if available
|
// Get DMABUF formats from renderer if available
|
||||||
let (default_feedback, scanout_feedback) = if let Some(renderer_arc) = &backend.renderer {
|
let (default_feedback, scanout_feedback) = if let Some(renderer_arc) = &backend.renderer {
|
||||||
|
|
@ -394,13 +473,8 @@ impl WoState {
|
||||||
.expect("building empty scanout dmabuf feedback");
|
.expect("building empty scanout dmabuf feedback");
|
||||||
(default_fb, scanout_fb)
|
(default_fb, scanout_fb)
|
||||||
};
|
};
|
||||||
// Disable DMABUF advertising to force clients to use SHM for the pixel export pipeline.
|
let dmabuf_global =
|
||||||
/*
|
dmabuf_state.create_global_with_default_feedback::<Self>(&dh, &default_feedback);
|
||||||
let dmabuf_global = dmabuf_state.create_global_with_default_feedback::<Self>(
|
|
||||||
&dh,
|
|
||||||
&default_feedback,
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
let popup_manager = PopupManager::default();
|
let popup_manager = PopupManager::default();
|
||||||
let space = Space::default();
|
let space = Space::default();
|
||||||
|
|
||||||
|
|
@ -416,7 +490,7 @@ impl WoState {
|
||||||
xdg_decoration_state,
|
xdg_decoration_state,
|
||||||
shm_state,
|
shm_state,
|
||||||
dmabuf_state,
|
dmabuf_state,
|
||||||
// dmabuf_global,
|
dmabuf_global,
|
||||||
dmabuf_default_feedback: default_feedback,
|
dmabuf_default_feedback: default_feedback,
|
||||||
dmabuf_scanout_feedback: scanout_feedback,
|
dmabuf_scanout_feedback: scanout_feedback,
|
||||||
seat_state,
|
seat_state,
|
||||||
|
|
@ -438,10 +512,21 @@ impl WoState {
|
||||||
keyboard_window_focus: None,
|
keyboard_window_focus: None,
|
||||||
output_size,
|
output_size,
|
||||||
space,
|
space,
|
||||||
output,
|
output: output.clone(),
|
||||||
|
monitors: vec![MonitorOutput {
|
||||||
|
id: "wo-output".to_string(),
|
||||||
|
output,
|
||||||
|
location: (0, 0).into(),
|
||||||
|
size: output_size,
|
||||||
|
primary: true,
|
||||||
|
}],
|
||||||
electron_processes: vec![],
|
electron_processes: vec![],
|
||||||
texture_cache: TextureCache::default(),
|
texture_cache: TextureCache::default(),
|
||||||
|
current_frame_seq: std::collections::HashMap::new(),
|
||||||
window_positions: std::collections::HashMap::new(),
|
window_positions: std::collections::HashMap::new(),
|
||||||
|
site_hashes: std::collections::HashMap::new(),
|
||||||
|
fullscreen_restore: std::collections::HashMap::new(),
|
||||||
|
fullscreen_windows: std::collections::HashSet::new(),
|
||||||
electron_ipc: None,
|
electron_ipc: None,
|
||||||
syscall_handler,
|
syscall_handler,
|
||||||
portal,
|
portal,
|
||||||
|
|
@ -473,14 +558,33 @@ impl WoState {
|
||||||
// Map the output into the space at origin.
|
// Map the output into the space at origin.
|
||||||
state.space.map_output(&state.output, (0, 0));
|
state.space.map_output(&state.output, (0, 0));
|
||||||
|
|
||||||
// Initialize mapping state for configured windows as mapped (visible).
|
// Initialize configured windows and desktop roots as mapped (visible).
|
||||||
for win in &state.config.windows {
|
for win in &state.config.windows {
|
||||||
state.window_mapped.insert(win.name.clone(), true);
|
state.window_mapped.insert(win.name.clone(), true);
|
||||||
}
|
}
|
||||||
|
for root in &state.config.root {
|
||||||
|
state.window_mapped.insert(root.name.clone(), true);
|
||||||
|
}
|
||||||
|
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the output under a global logical desktop coordinate.
|
||||||
|
pub fn monitor_at(&self, point: Point<f64, Logical>) -> Option<&MonitorOutput> {
|
||||||
|
self.monitors.iter().find(|monitor| monitor.contains(point))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clamp a global pointer position to a real output, including layouts
|
||||||
|
/// with negative origins or gaps between monitors.
|
||||||
|
pub fn clamp_pointer_to_monitors(&self, point: Point<f64, Logical>) -> Point<f64, Logical> {
|
||||||
|
clamp_point_to_monitors(&self.monitors, point)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the union of all output rectangles in global logical coordinates.
|
||||||
|
pub fn desktop_bounds(&self) -> smithay::utils::Rectangle<i32, Logical> {
|
||||||
|
monitor_desktop_bounds(&self.monitors)
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the effective title-bar height for a Wayland window.
|
/// Return the effective title-bar height for a Wayland window.
|
||||||
/// SSD windows get TITLE_H (comraw draws the title bar); CSD windows get 0
|
/// SSD windows get TITLE_H (comraw draws the title bar); CSD windows get 0
|
||||||
/// (the client draws its own decorations).
|
/// (the client draws its own decorations).
|
||||||
|
|
@ -619,19 +723,28 @@ impl WoState {
|
||||||
|
|
||||||
/// Find a Wayland or X11 Window element by its stable name (even if minimized).
|
/// Find a Wayland or X11 Window element by its stable name (even if minimized).
|
||||||
pub fn find_wayland_window(&self, name: &str) -> Option<Window> {
|
pub fn find_wayland_window(&self, name: &str) -> Option<Window> {
|
||||||
self.all_windows.iter().find(|w| {
|
self.all_windows
|
||||||
if let Some(toplevel) = w.toplevel() {
|
.iter()
|
||||||
if let Some(win_name) = self.wayland_window_names.get(&toplevel.wl_surface().id()) {
|
.find(|w| {
|
||||||
if win_name == name { return true; }
|
if let Some(toplevel) = w.toplevel() {
|
||||||
|
if let Some(win_name) =
|
||||||
|
self.wayland_window_names.get(&toplevel.wl_surface().id())
|
||||||
|
{
|
||||||
|
if win_name == name {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if let Some(x11) = w.x11_surface() {
|
||||||
if let Some(x11) = w.x11_surface() {
|
if let Some(win_name) = self.x11_window_names.get(&x11.window_id()) {
|
||||||
if let Some(win_name) = self.x11_window_names.get(&x11.window_id()) {
|
if win_name == name {
|
||||||
if win_name == name { return true; }
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
false
|
||||||
false
|
})
|
||||||
}).cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Focus the topmost mapped native window in `space`.
|
/// Focus the topmost mapped native window in `space`.
|
||||||
|
|
@ -703,7 +816,8 @@ impl WoState {
|
||||||
// Translate global space coordinate to surface-local coordinate
|
// Translate global space coordinate to surface-local coordinate
|
||||||
// window.geometry().loc is the offset from surface origin to content area.
|
// window.geometry().loc is the offset from surface origin to content area.
|
||||||
let geo = window.geometry();
|
let geo = window.geometry();
|
||||||
let surface_local: Point<f64, Logical> = (local.x + geo.loc.x as f64, local.y + geo.loc.y as f64).into();
|
let surface_local: Point<f64, Logical> =
|
||||||
|
(local.x + geo.loc.x as f64, local.y + geo.loc.y as f64).into();
|
||||||
window
|
window
|
||||||
.surface_under(surface_local, smithay::desktop::WindowSurfaceType::ALL)
|
.surface_under(surface_local, smithay::desktop::WindowSurfaceType::ALL)
|
||||||
.map(|(s, p)| (s, p.to_f64() + window_pos.to_f64()))
|
.map(|(s, p)| (s, p.to_f64() + window_pos.to_f64()))
|
||||||
|
|
@ -711,6 +825,70 @@ impl WoState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod monitor_layout_tests {
|
||||||
|
use super::*;
|
||||||
|
use smithay::output::{PhysicalProperties, Subpixel};
|
||||||
|
|
||||||
|
fn monitor(id: &str, x: i32, y: i32, width: u32, height: u32) -> MonitorOutput {
|
||||||
|
MonitorOutput {
|
||||||
|
id: id.to_string(),
|
||||||
|
output: Output::new(
|
||||||
|
id.to_string(),
|
||||||
|
PhysicalProperties {
|
||||||
|
size: (0, 0).into(),
|
||||||
|
subpixel: Subpixel::Unknown,
|
||||||
|
make: "test".into(),
|
||||||
|
model: id.into(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
location: (x, y).into(),
|
||||||
|
size: (width, height),
|
||||||
|
primary: x == 0 && y == 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn monitor_hit_test_uses_global_coordinates_and_excludes_far_edge() {
|
||||||
|
let displays = vec![monitor("left", -100, 20, 100, 80)];
|
||||||
|
assert!(displays[0].contains((-100.0, 20.0).into()));
|
||||||
|
assert!(displays[0].contains((-0.5, 99.5).into()));
|
||||||
|
assert!(!displays[0].contains((0.0, 20.0).into()));
|
||||||
|
assert!(!displays[0].contains((-1.0, 100.0).into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pointer_clamps_to_nearest_output_across_layout_gaps() {
|
||||||
|
let displays = vec![
|
||||||
|
monitor("left", -100, 0, 100, 100),
|
||||||
|
monitor("right", 20, 0, 100, 100),
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
clamp_point_to_monitors(&displays, (-110.0, 40.0).into()),
|
||||||
|
(-100.0, 40.0).into()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
clamp_point_to_monitors(&displays, (0.0, 40.0).into()),
|
||||||
|
(-1.0, 40.0).into()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
clamp_point_to_monitors(&displays, (300.0, 120.0).into()),
|
||||||
|
(119.0, 99.0).into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn desktop_bounds_include_negative_origins_and_output_gaps() {
|
||||||
|
let displays = vec![
|
||||||
|
monitor("left", -100, -20, 100, 80),
|
||||||
|
monitor("right", 20, 10, 120, 90),
|
||||||
|
];
|
||||||
|
let bounds = monitor_desktop_bounds(&displays);
|
||||||
|
assert_eq!(bounds.loc, (-100, -20).into());
|
||||||
|
assert_eq!(bounds.size, (240, 120).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SeatHandler for WoState {
|
impl SeatHandler for WoState {
|
||||||
type KeyboardFocus = smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
type KeyboardFocus = smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
type PointerFocus = smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
type PointerFocus = smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
|
|
@ -800,14 +978,34 @@ smithay::delegate_cursor_shape!(WoState);
|
||||||
impl PointerConstraintsHandler for WoState {
|
impl PointerConstraintsHandler for WoState {
|
||||||
fn cursor_position_hint(
|
fn cursor_position_hint(
|
||||||
&mut self,
|
&mut self,
|
||||||
_surface: &WlSurface,
|
surface: &WlSurface,
|
||||||
_pointer: &PointerHandle<Self>,
|
pointer: &PointerHandle<Self>,
|
||||||
_location: Point<f64, Logical>,
|
location: Point<f64, Logical>,
|
||||||
) {
|
) {
|
||||||
|
let window = self.space.elements().find(|window| {
|
||||||
|
window
|
||||||
|
.toplevel()
|
||||||
|
.map(|toplevel| toplevel.wl_surface() == surface)
|
||||||
|
.unwrap_or(false)
|
||||||
|
|| window
|
||||||
|
.x11_surface()
|
||||||
|
.and_then(|x11| x11.wl_surface())
|
||||||
|
.map(|wl_surface| wl_surface == *surface)
|
||||||
|
.unwrap_or(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(window) = window {
|
||||||
|
if let Some(origin) = self.space.element_location(window) {
|
||||||
|
let content_origin = origin + window.geometry().loc;
|
||||||
|
let hinted_location = content_origin.to_f64() + location;
|
||||||
|
self.pointer_location = hinted_location;
|
||||||
|
pointer.set_location(hinted_location);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_constraint(&mut self, surface: &WlSurface, pointer: &PointerHandle<Self>) {
|
fn new_constraint(&mut self, surface: &WlSurface, pointer: &PointerHandle<Self>) {
|
||||||
// When a new constraint is created, check if it's a lock and notify Electron.
|
// Notify Electron when a native client requests a pointer lock.
|
||||||
pointer_constraints::with_pointer_constraint(surface, pointer, |constraint| {
|
pointer_constraints::with_pointer_constraint(surface, pointer, |constraint| {
|
||||||
if let Some(constraint) = constraint {
|
if let Some(constraint) = constraint {
|
||||||
if let smithay::wayland::pointer_constraints::PointerConstraint::Locked(_) =
|
if let smithay::wayland::pointer_constraints::PointerConstraint::Locked(_) =
|
||||||
|
|
@ -868,9 +1066,7 @@ pub fn propagate_environment(vars: &[&str]) {
|
||||||
|
|
||||||
let var_args: Vec<String> = vars
|
let var_args: Vec<String> = vars
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|&name| {
|
.filter_map(|&name| std::env::var(name).ok().map(|val| format!("{name}={val}")))
|
||||||
std::env::var(name).ok().map(|val| format!("{name}={val}"))
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if var_args.is_empty() {
|
if var_args.is_empty() {
|
||||||
|
|
@ -889,7 +1085,11 @@ pub fn propagate_environment(vars: &[&str]) {
|
||||||
for arg in &var_args {
|
for arg in &var_args {
|
||||||
dbus_cmd.arg(arg);
|
dbus_cmd.arg(arg);
|
||||||
}
|
}
|
||||||
match dbus_cmd.stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).spawn() {
|
match dbus_cmd
|
||||||
|
.stdout(std::process::Stdio::null())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
Ok(mut child) => {
|
Ok(mut child) => {
|
||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
info!("Propagated {:?} to DBus activation environment", names);
|
info!("Propagated {:?} to DBus activation environment", names);
|
||||||
|
|
@ -903,7 +1103,11 @@ pub fn propagate_environment(vars: &[&str]) {
|
||||||
for name in &names {
|
for name in &names {
|
||||||
systemctl_cmd.arg(name);
|
systemctl_cmd.arg(name);
|
||||||
}
|
}
|
||||||
match systemctl_cmd.stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).spawn() {
|
match systemctl_cmd
|
||||||
|
.stdout(std::process::Stdio::null())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
Ok(mut child) => {
|
Ok(mut child) => {
|
||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
info!("Propagated {:?} to systemd user environment", names);
|
info!("Propagated {:?} to systemd user environment", names);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
/// xdg-desktop-portal implementation for Wo compositor
|
/// xdg-desktop-portal implementation for Wo compositor
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use pipewire as pw;
|
use pipewire as pw;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -13,7 +12,7 @@ use tracing::{debug, info, warn};
|
||||||
pub struct PortalSession {
|
pub struct PortalSession {
|
||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
pub source_type: SourceType,
|
pub source_type: SourceType,
|
||||||
pub target: Option<String>, // "screen" or window name
|
pub target: Option<String>, // "screen" or window name
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,7 +54,6 @@ pub struct WoPortal {
|
||||||
sessions: Arc<Mutex<HashMap<String, PortalSession>>>,
|
sessions: Arc<Mutex<HashMap<String, PortalSession>>>,
|
||||||
next_session_id: Arc<Mutex<u64>>,
|
next_session_id: Arc<Mutex<u64>>,
|
||||||
streams: Arc<Mutex<HashMap<u32, Arc<ScreenCaptureStream>>>>,
|
streams: Arc<Mutex<HashMap<u32, Arc<ScreenCaptureStream>>>>,
|
||||||
compositor_socket: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WoPortal {
|
impl WoPortal {
|
||||||
|
|
@ -64,38 +62,9 @@ impl WoPortal {
|
||||||
sessions: Arc::new(Mutex::new(HashMap::new())),
|
sessions: Arc::new(Mutex::new(HashMap::new())),
|
||||||
next_session_id: Arc::new(Mutex::new(1)),
|
next_session_id: Arc::new(Mutex::new(1)),
|
||||||
streams: Arc::new(Mutex::new(HashMap::new())),
|
streams: Arc::new(Mutex::new(HashMap::new())),
|
||||||
compositor_socket: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_compositor_socket(mut self, socket: String) -> Self {
|
|
||||||
self.compositor_socket = Some(socket);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request capture from compositor via IPC
|
|
||||||
fn request_compositor_capture(&self, window: &Option<String>, source_type: SourceType) -> Result<()> {
|
|
||||||
if let Some(socket_path) = &self.compositor_socket {
|
|
||||||
use std::os::unix::net::UnixStream;
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
// Connect to compositor IPC
|
|
||||||
if let Ok(mut stream) = UnixStream::connect(socket_path) {
|
|
||||||
let request = serde_json::json!({
|
|
||||||
"type": "portal_capture_request",
|
|
||||||
"source_type": source_type,
|
|
||||||
"window": window,
|
|
||||||
});
|
|
||||||
let msg = serde_json::to_string(&request)?;
|
|
||||||
let _ = stream.write_all(msg.as_bytes());
|
|
||||||
info!("Requested capture from compositor: {:?}", source_type);
|
|
||||||
} else {
|
|
||||||
warn!("Could not connect to compositor socket: {}", socket_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new portal session
|
/// Create a new portal session
|
||||||
pub fn create_session(&self) -> String {
|
pub fn create_session(&self) -> String {
|
||||||
let mut next_id = self.next_session_id.lock().unwrap();
|
let mut next_id = self.next_session_id.lock().unwrap();
|
||||||
|
|
@ -109,21 +78,18 @@ impl WoPortal {
|
||||||
active: false,
|
active: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.sessions.lock().unwrap().insert(session_id.clone(), session);
|
self.sessions
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(session_id.clone(), session);
|
||||||
info!("Created portal session: {}", session_id);
|
info!("Created portal session: {}", session_id);
|
||||||
session_id
|
session_id
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select sources for a session
|
/// Select sources for a session
|
||||||
pub fn select_sources(
|
pub fn select_sources(&self, session_id: &str, request: CaptureRequest) -> Result<()> {
|
||||||
&self,
|
|
||||||
session_id: &str,
|
|
||||||
request: CaptureRequest,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut sessions = self.sessions.lock().unwrap();
|
let mut sessions = self.sessions.lock().unwrap();
|
||||||
let session = sessions
|
let session = sessions.get_mut(session_id).context("session not found")?;
|
||||||
.get_mut(session_id)
|
|
||||||
.context("session not found")?;
|
|
||||||
|
|
||||||
session.source_type = request.source_type;
|
session.source_type = request.source_type;
|
||||||
session.target = request.target.clone();
|
session.target = request.target.clone();
|
||||||
|
|
@ -143,9 +109,7 @@ impl WoPortal {
|
||||||
height: u32,
|
height: u32,
|
||||||
) -> Result<CaptureResponse> {
|
) -> Result<CaptureResponse> {
|
||||||
let mut sessions = self.sessions.lock().unwrap();
|
let mut sessions = self.sessions.lock().unwrap();
|
||||||
let session = sessions
|
let session = sessions.get_mut(session_id).context("session not found")?;
|
||||||
.get_mut(session_id)
|
|
||||||
.context("session not found")?;
|
|
||||||
|
|
||||||
session.active = true;
|
session.active = true;
|
||||||
|
|
||||||
|
|
@ -155,7 +119,8 @@ impl WoPortal {
|
||||||
.filter(|c| c.is_ascii_digit())
|
.filter(|c| c.is_ascii_digit())
|
||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
.parse::<u32>()
|
.parse::<u32>()
|
||||||
.unwrap_or(1) + 100; // Offset to avoid conflicts
|
.unwrap_or(1)
|
||||||
|
+ 100; // Offset to avoid conflicts
|
||||||
|
|
||||||
// Create the actual PipeWire stream
|
// Create the actual PipeWire stream
|
||||||
let stream = Arc::new(ScreenCaptureStream::new(
|
let stream = Arc::new(ScreenCaptureStream::new(
|
||||||
|
|
@ -172,9 +137,6 @@ impl WoPortal {
|
||||||
let mut streams = self.streams.lock().unwrap();
|
let mut streams = self.streams.lock().unwrap();
|
||||||
streams.insert(real_node_id, stream);
|
streams.insert(real_node_id, stream);
|
||||||
|
|
||||||
// Request capture from compositor
|
|
||||||
self.request_compositor_capture(&session.target, session.source_type)?;
|
|
||||||
|
|
||||||
let stream_info = StreamInfo {
|
let stream_info = StreamInfo {
|
||||||
node_id: real_node_id,
|
node_id: real_node_id,
|
||||||
width,
|
width,
|
||||||
|
|
@ -262,17 +224,13 @@ impl WoPortal {
|
||||||
|
|
||||||
/// Push raw pixel data (ARGB8888/BGRx) to matching portal streams.
|
/// Push raw pixel data (ARGB8888/BGRx) to matching portal streams.
|
||||||
/// Called from the compositor after offscreen capture or full-screen compositing.
|
/// Called from the compositor after offscreen capture or full-screen compositing.
|
||||||
pub fn push_pixels_to_streams(
|
pub fn push_pixels_to_streams(&self, window_name: Option<&str>, pixels: &[u8]) {
|
||||||
&self,
|
|
||||||
window_name: Option<&str>,
|
|
||||||
pixels: &[u8],
|
|
||||||
) {
|
|
||||||
let streams = self.streams.lock().unwrap();
|
let streams = self.streams.lock().unwrap();
|
||||||
for stream in streams.values() {
|
for stream in streams.values() {
|
||||||
let matches = match (&stream.window_name, window_name) {
|
let matches = match (&stream.window_name, window_name) {
|
||||||
(Some(stream_win), Some(win)) => stream_win == win,
|
(Some(stream_win), Some(win)) => stream_win == win,
|
||||||
(None, None) => true, // Monitor capture gets full screen
|
(None, None) => true, // Monitor capture gets full screen
|
||||||
(None, Some(_)) => true, // Monitor capture sees all windows
|
(None, Some(_)) => true, // Monitor capture sees all windows
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
if matches && stream.is_active() {
|
if matches && stream.is_active() {
|
||||||
|
|
@ -280,6 +238,47 @@ impl WoPortal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Publish a window crop from the already-composited desktop frame.
|
||||||
|
/// The output is scaled to the negotiated PipeWire size so captures remain
|
||||||
|
/// valid when a window changes size after portal Start.
|
||||||
|
pub fn push_window_capture(
|
||||||
|
&self,
|
||||||
|
window_name: &str,
|
||||||
|
pixels: &[u8],
|
||||||
|
screen_width: u32,
|
||||||
|
screen_height: u32,
|
||||||
|
bounds: (i32, i32, u32, u32),
|
||||||
|
) {
|
||||||
|
let streams = self.streams.lock().unwrap();
|
||||||
|
let (x, y, width, height) = bounds;
|
||||||
|
if screen_width == 0 || screen_height == 0 || width == 0 || height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for stream in streams.values().filter(|stream| {
|
||||||
|
stream.window_name.as_deref() == Some(window_name) && stream.is_active()
|
||||||
|
}) {
|
||||||
|
let mut output = vec![0; stream.width as usize * stream.height as usize * 4];
|
||||||
|
for dy in 0..stream.height {
|
||||||
|
let sy = y + (dy as u64 * height as u64 / stream.height.max(1) as u64) as i32;
|
||||||
|
if sy < 0 || sy >= screen_height as i32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for dx in 0..stream.width {
|
||||||
|
let sx = x + (dx as u64 * width as u64 / stream.width.max(1) as u64) as i32;
|
||||||
|
if sx < 0 || sx >= screen_width as i32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let src = (sy as usize * screen_width as usize + sx as usize) * 4;
|
||||||
|
let dst = (dy as usize * stream.width as usize + dx as usize) * 4;
|
||||||
|
if src + 4 <= pixels.len() {
|
||||||
|
output[dst..dst + 4].copy_from_slice(&pixels[src..src + 4]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream.push_pixels(&output);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for WoPortal {
|
impl Default for WoPortal {
|
||||||
|
|
@ -316,7 +315,10 @@ impl PortalDBusHandler {
|
||||||
}
|
}
|
||||||
("org.freedesktop.portal.ScreenCast", "SelectSources") => {
|
("org.freedesktop.portal.ScreenCast", "SelectSources") => {
|
||||||
let request: CaptureRequest = serde_json::from_value(args)?;
|
let request: CaptureRequest = serde_json::from_value(args)?;
|
||||||
let session_id = request.restore_token.clone().unwrap_or_else(|| "default".to_string());
|
let session_id = request
|
||||||
|
.restore_token
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "default".to_string());
|
||||||
self.portal.select_sources(&session_id, request)?;
|
self.portal.select_sources(&session_id, request)?;
|
||||||
Ok(serde_json::json!({ "success": true }))
|
Ok(serde_json::json!({ "success": true }))
|
||||||
}
|
}
|
||||||
|
|
@ -358,7 +360,7 @@ pub struct ScreenCaptureStream {
|
||||||
/// Frame data for PipeWire streaming
|
/// Frame data for PipeWire streaming
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct PipeWireFrame {
|
pub struct PipeWireFrame {
|
||||||
pub pts: u64, // Presentation timestamp
|
pub pts: u64, // Presentation timestamp
|
||||||
pub dmabuf_fd: i32,
|
pub dmabuf_fd: i32,
|
||||||
pub stride: u32,
|
pub stride: u32,
|
||||||
pub offset: u32,
|
pub offset: u32,
|
||||||
|
|
@ -387,7 +389,13 @@ impl ScreenCaptureStream {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push a frame to the PipeWire stream with DMABUF
|
/// Push a frame to the PipeWire stream with DMABUF
|
||||||
pub fn push_frame(&self, dmabuf_fd: i32, stride: u32, offset: u32, modifier: u64) -> Result<()> {
|
pub fn push_frame(
|
||||||
|
&self,
|
||||||
|
dmabuf_fd: i32,
|
||||||
|
stride: u32,
|
||||||
|
offset: u32,
|
||||||
|
modifier: u64,
|
||||||
|
) -> Result<()> {
|
||||||
if !*self.active.lock().unwrap() {
|
if !*self.active.lock().unwrap() {
|
||||||
return Ok(()); // Stream not active
|
return Ok(()); // Stream not active
|
||||||
}
|
}
|
||||||
|
|
@ -411,7 +419,8 @@ impl ScreenCaptureStream {
|
||||||
queue.push(frame.clone());
|
queue.push(frame.clone());
|
||||||
|
|
||||||
// Keep queue bounded to prevent memory issues
|
// Keep queue bounded to prevent memory issues
|
||||||
if queue.len() > 30 { // 30 frame buffer
|
if queue.len() > 30 {
|
||||||
|
// 30 frame buffer
|
||||||
queue.remove(0);
|
queue.remove(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -462,13 +471,9 @@ impl ScreenCaptureStream {
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
.name(format!("pw-capture-{}", self.node_id))
|
.name(format!("pw-capture-{}", self.node_id))
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
if let Err(e) = run_pw_output_stream(
|
if let Err(e) =
|
||||||
width,
|
run_pw_output_stream(width, height, active.clone(), latest_pixels, node_tx)
|
||||||
height,
|
{
|
||||||
active.clone(),
|
|
||||||
latest_pixels,
|
|
||||||
node_tx,
|
|
||||||
) {
|
|
||||||
warn!("PipeWire stream thread failed: {e:#}");
|
warn!("PipeWire stream thread failed: {e:#}");
|
||||||
// Mark inactive on failure so the stream reports itself as dead.
|
// Mark inactive on failure so the stream reports itself as dead.
|
||||||
*active.lock().unwrap() = false;
|
*active.lock().unwrap() = false;
|
||||||
|
|
@ -516,7 +521,11 @@ impl ScreenCaptureStream {
|
||||||
impl Drop for ScreenCaptureStream {
|
impl Drop for ScreenCaptureStream {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.disconnect();
|
self.disconnect();
|
||||||
debug!("Stream {}: dropped (pushed {} frames)", self.node_id, self.frame_count());
|
debug!(
|
||||||
|
"Stream {}: dropped (pushed {} frames)",
|
||||||
|
self.node_id,
|
||||||
|
self.frame_count()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -665,10 +674,7 @@ fn run_pw_output_stream(
|
||||||
Fraction,
|
Fraction,
|
||||||
pw::spa::utils::Fraction { num: 60, denom: 1 },
|
pw::spa::utils::Fraction { num: 60, denom: 1 },
|
||||||
pw::spa::utils::Fraction { num: 1, denom: 1 },
|
pw::spa::utils::Fraction { num: 1, denom: 1 },
|
||||||
pw::spa::utils::Fraction {
|
pw::spa::utils::Fraction { num: 120, denom: 1 }
|
||||||
num: 120,
|
|
||||||
denom: 1
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
/// Dynamic syscall handling for IPC
|
/// Dynamic syscall handling for IPC
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::os::unix::process::CommandExt;
|
use std::os::unix::process::CommandExt;
|
||||||
|
|
@ -49,19 +48,13 @@ pub enum SyscallRequest {
|
||||||
},
|
},
|
||||||
/// List directory contents
|
/// List directory contents
|
||||||
#[serde(rename = "listdir")]
|
#[serde(rename = "listdir")]
|
||||||
ListDir {
|
ListDir { path: String },
|
||||||
path: String,
|
|
||||||
},
|
|
||||||
/// Get file/directory metadata
|
/// Get file/directory metadata
|
||||||
#[serde(rename = "stat")]
|
#[serde(rename = "stat")]
|
||||||
Stat {
|
Stat { path: String },
|
||||||
path: String,
|
|
||||||
},
|
|
||||||
/// Delete file
|
/// Delete file
|
||||||
#[serde(rename = "delete")]
|
#[serde(rename = "delete")]
|
||||||
Delete {
|
Delete { path: String },
|
||||||
path: String,
|
|
||||||
},
|
|
||||||
/// Custom extension syscall
|
/// Custom extension syscall
|
||||||
#[serde(rename = "custom")]
|
#[serde(rename = "custom")]
|
||||||
Custom {
|
Custom {
|
||||||
|
|
@ -99,12 +92,8 @@ pub trait ExtensionHandler: Send + Sync {
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
#[serde(tag = "status")]
|
#[serde(tag = "status")]
|
||||||
pub enum SyscallResponse {
|
pub enum SyscallResponse {
|
||||||
Success {
|
Success { data: serde_json::Value },
|
||||||
data: serde_json::Value,
|
Error { message: String },
|
||||||
},
|
|
||||||
Error {
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Syscall handler with security policies
|
/// Syscall handler with security policies
|
||||||
|
|
@ -176,10 +165,10 @@ impl SyscallHandler {
|
||||||
SyscallRequest::ListApplications => {
|
SyscallRequest::ListApplications => {
|
||||||
Ok(serde_json::Value::Array(self.applications.clone()))
|
Ok(serde_json::Value::Array(self.applications.clone()))
|
||||||
}
|
}
|
||||||
SyscallRequest::BrowseApplications => {
|
SyscallRequest::BrowseApplications => browse_applications(),
|
||||||
browse_applications()
|
SyscallRequest::Launch { command, args } => {
|
||||||
|
self.handle_launch(&command, args.as_deref())
|
||||||
}
|
}
|
||||||
SyscallRequest::Launch { command, args } => self.handle_launch(&command, args.as_deref()),
|
|
||||||
SyscallRequest::Exec {
|
SyscallRequest::Exec {
|
||||||
command,
|
command,
|
||||||
args,
|
args,
|
||||||
|
|
@ -198,12 +187,8 @@ impl SyscallHandler {
|
||||||
SyscallRequest::Quit => {
|
SyscallRequest::Quit => {
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
SyscallRequest::Shutdown => {
|
SyscallRequest::Shutdown => handle_power_command("systemctl", &["poweroff"]),
|
||||||
handle_power_command("systemctl", &["poweroff"])
|
SyscallRequest::Restart => handle_power_command("systemctl", &["reboot"]),
|
||||||
}
|
|
||||||
SyscallRequest::Restart => {
|
|
||||||
handle_power_command("systemctl", &["reboot"])
|
|
||||||
}
|
|
||||||
SyscallRequest::Logout => {
|
SyscallRequest::Logout => {
|
||||||
if let Ok(session_id) = std::env::var("XDG_SESSION_ID") {
|
if let Ok(session_id) = std::env::var("XDG_SESSION_ID") {
|
||||||
handle_power_command("loginctl", &["terminate-session", &session_id])
|
handle_power_command("loginctl", &["terminate-session", &session_id])
|
||||||
|
|
@ -212,25 +197,21 @@ impl SyscallHandler {
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SyscallRequest::Lock => {
|
SyscallRequest::Lock => handle_power_command("loginctl", &["lock-session"]),
|
||||||
handle_power_command("loginctl", &["lock-session"])
|
SyscallRequest::Sleep => handle_power_command("systemctl", &["suspend"]),
|
||||||
}
|
|
||||||
SyscallRequest::Sleep => {
|
|
||||||
handle_power_command("systemctl", &["suspend"])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_launch(
|
fn handle_launch(&self, command: &str, args: Option<&[String]>) -> Result<serde_json::Value> {
|
||||||
&self,
|
|
||||||
command: &str,
|
|
||||||
args: Option<&[String]>,
|
|
||||||
) -> Result<serde_json::Value> {
|
|
||||||
if !self.allow_exec {
|
if !self.allow_exec {
|
||||||
bail!("exec syscalls are disabled");
|
bail!("exec syscalls are disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!("Launching command (fire-and-forget): {} {:?}", command, args);
|
tracing::info!(
|
||||||
|
"Launching command (fire-and-forget): {} {:?}",
|
||||||
|
command,
|
||||||
|
args
|
||||||
|
);
|
||||||
|
|
||||||
let mut cmd = Command::new("sh");
|
let mut cmd = Command::new("sh");
|
||||||
let shell_cmd = if let Some(args) = args {
|
let shell_cmd = if let Some(args) = args {
|
||||||
|
|
@ -302,12 +283,10 @@ impl SyscallHandler {
|
||||||
.stderr(Stdio::null());
|
.stderr(Stdio::null());
|
||||||
|
|
||||||
match cmd.spawn() {
|
match cmd.spawn() {
|
||||||
Ok(child) => {
|
Ok(child) => Ok(serde_json::json!({
|
||||||
Ok(serde_json::json!({
|
"pid": child.id(),
|
||||||
"pid": child.id(),
|
"launched": true,
|
||||||
"launched": true,
|
})),
|
||||||
}))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
bail!("failed to execute '{}': {}", command, e);
|
bail!("failed to execute '{}': {}", command, e);
|
||||||
}
|
}
|
||||||
|
|
@ -436,10 +415,7 @@ impl SyscallHandler {
|
||||||
.context("HOME not set")?;
|
.context("HOME not set")?;
|
||||||
let canonical = path.canonicalize().context("canonicalizing path")?;
|
let canonical = path.canonicalize().context("canonicalizing path")?;
|
||||||
if !canonical.starts_with(&home) && !canonical.starts_with("/proc") {
|
if !canonical.starts_with(&home) && !canonical.starts_with("/proc") {
|
||||||
bail!(
|
bail!("path outside home directory: {}", canonical.display());
|
||||||
"path outside home directory: {}",
|
|
||||||
canonical.display()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -460,10 +436,7 @@ impl SyscallHandler {
|
||||||
if parent.exists() {
|
if parent.exists() {
|
||||||
let canonical = parent.canonicalize().context("canonicalizing parent")?;
|
let canonical = parent.canonicalize().context("canonicalizing parent")?;
|
||||||
if !canonical.starts_with(&home) {
|
if !canonical.starts_with(&home) {
|
||||||
bail!(
|
bail!("write path outside home directory: {}", canonical.display());
|
||||||
"write path outside home directory: {}",
|
|
||||||
canonical.display()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -489,7 +462,9 @@ fn handle_power_command(program: &str, args: &[&str]) -> Result<serde_json::Valu
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null());
|
.stderr(Stdio::null());
|
||||||
match cmd.spawn() {
|
match cmd.spawn() {
|
||||||
Ok(_child) => Ok(serde_json::json!({ "ok": true, "command": format!("{} {}", program, args.join(" ")) })),
|
Ok(_child) => Ok(
|
||||||
|
serde_json::json!({ "ok": true, "command": format!("{} {}", program, args.join(" ")) }),
|
||||||
|
),
|
||||||
Err(e) => bail!("failed to execute '{}': {}", program, e),
|
Err(e) => bail!("failed to execute '{}': {}", program, e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
360
whatever.txt
360
whatever.txt
|
|
@ -1,222 +1,154 @@
|
||||||
Compiling wo v0.1.0 (/home/hellings/Documents/wo)
|
[2m2026-10-26T03:36:18.813061Z[0m [32m INFO[0m [2mwo[0m[2m:[0m wo compositor starting
|
||||||
Compiling wo v0.1.0 (/home/hellings/Documents/wo)
|
[2m2026-10-26T03:36:18.813121Z[0m [32m INFO[0m [2mwo::config[0m[2m:[0m Loading config from "/home/hellings/.config/wo/config.toml"
|
||||||
Finished `release` profile [optimized] target(s) in 2m 21s
|
[2m2026-10-26T03:36:18.813191Z[0m [32m INFO[0m [2mwo[0m[2m:[0m Attempting nested mode (gles renderer)
|
||||||
Finished `release` profile [optimized] target(s) in 2m 21s
|
[2m2026-10-26T03:36:18.813218Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Starting nested compositor mode
|
||||||
Running `target/release/wo`
|
[2m2026-10-26T03:36:18.813231Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Press ESC to exit
|
||||||
Running `target/release/wo`
|
[2m2026-10-26T03:36:18.813449Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Wayland socket created [3msocket[0m[2m=[0mwo-0
|
||||||
Xwayland glamor: GBM Wayland interfaces not available
|
[2m2026-10-26T03:36:18.813504Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Using host Wayland display for nested startup [3msocket[0m[2m=[0mwayland-0
|
||||||
Failed to initialize glamor, falling back to sw
|
[2m2026-10-26T03:36:18.856187Z[0m [31mERROR[0m [2msmithay::backend::egl::ffi[0m[2m:[0m [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
|
||||||
Xwayland glamor: GBM Wayland interfaces not available
|
|
||||||
Failed to initialize glamor, falling back to sw
|
[2m2026-10-26T03:36:18.911103Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Nested window size [3mwidth[0m[2m=[0m1280 [3mheight[0m[2m=[0m800
|
||||||
(II) IGLX: Loaded and initialized swrast
|
[2m2026-10-26T03:36:18.911169Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Using render node: /dev/dri/renderD128
|
||||||
(II) GLX: Initialized DRISWRAST GL provider for screen 0
|
[2m2026-10-26T03:36:18.911238Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Collected 672 dmabuf formats from renderer
|
||||||
(II) IGLX: Loaded and initialized swrast
|
[2m2026-10-26T03:36:18.911502Z[0m [32m INFO[0m [2mwo[0m[2m:[0m Started configured service before root windows [3mname[0m[2m=[0mvite [3mpid[0m[2m=[0m1378486
|
||||||
(II) GLX: Initialized DRISWRAST GL provider for screen 0
|
[2m2026-10-26T03:36:18.913053Z[0m [32m INFO[0m [2mwo::handlers::screencopy[0m[2m:[0m Registered zwlr_screencopy_manager_v1 global (v3)
|
||||||
|
[2m2026-10-26T03:36:18.913289Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loading cursor theme 'Adwaita' (size 24)
|
||||||
|
[2m2026-10-26T03:36:18.913344Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loading cursor theme 'default' (size 24)
|
||||||
|
[2m2026-10-26T03:36:18.913378Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loading cursor theme 'DMZ-White' (size 24)
|
||||||
|
[2m2026-10-26T03:36:18.913400Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loading cursor theme 'breeze' (size 24)
|
||||||
|
[2m2026-10-26T03:36:18.913478Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m IPC socket listening [3msocket[0m[2m=[0m"/run/user/1000/wo-ipc.sock"
|
||||||
|
[2m2026-10-26T03:36:18.916655Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m XWayland server starting
|
||||||
|
[2m2026-10-26T03:36:18.916688Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Set WAYLAND_DISPLAY=wo-0
|
||||||
|
[2m2026-10-26T03:36:18.919374Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m Propagated ["WAYLAND_DISPLAY"] to DBus activation environment
|
||||||
|
[2m2026-10-26T03:36:18.922988Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m Propagated ["WAYLAND_DISPLAY"] to systemd user environment
|
||||||
|
[2m2026-10-26T03:36:18.923014Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Nested compositor running
|
||||||
|
[2m2026-10-26T03:36:18.923039Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Window resized to 1280x800
|
||||||
|
[2m2026-10-26T03:36:18.923058Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Window resized to 1280x800
|
||||||
|
[2m2026-10-26T03:36:18.962626Z[0m [32m INFO[0m [2mwo::handlers::compositor[0m[2m:[0m Client missing ClientData (likely XWayland), using fallback compositor state
|
||||||
|
[2m2026-10-26T03:36:19.023297Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Spawned portal service: "/home/hellings/.local/share/wo/bin/wo-portal"
|
||||||
|
[2m2026-10-26T03:36:19.025666Z[0m [32m INFO[0m [2mwo_portal[0m[2m:[0m xdg-desktop-portal-wo starting
|
||||||
|
[2m2026-10-26T03:36:19.026881Z[0m [32m INFO[0m [2mwo_portal[0m[2m:[0m Portal service registered as org.freedesktop.impl.portal.desktop.wo
|
||||||
|
[2m2026-10-26T03:36:19.026938Z[0m [32m INFO[0m [2mwo_portal[0m[2m:[0m Listening for screen capture requests...
|
||||||
|
(II) GLX: Initialized glamor GL provider for screen 0
|
||||||
The XKEYBOARD keymap compiler (xkbcomp) reports:
|
The XKEYBOARD keymap compiler (xkbcomp) reports:
|
||||||
> Warning: Multiple symbols for level 1/group 1 on key <FK23>
|
> Warning: Could not resolve keysym XF86CameraAccessEnable
|
||||||
> Using F23, ignoring XF86TouchpadOff
|
> Warning: Could not resolve keysym XF86CameraAccessDisable
|
||||||
> Warning: Symbol map for key <FK23> redefined
|
> Warning: Could not resolve keysym XF86CameraAccessToggle
|
||||||
> Using last definition for conflicting fields
|
> Warning: Could not resolve keysym XF86NextElement
|
||||||
> Warning: Symbol map for key <FK24> redefined
|
> Warning: Could not resolve keysym XF86PreviousElement
|
||||||
> Using last definition for conflicting fields
|
> Warning: Could not resolve keysym XF86AutopilotEngageToggle
|
||||||
The XKEYBOARD keymap compiler (xkbcomp) reports:
|
> Warning: Could not resolve keysym XF86MarkWaypoint
|
||||||
> Warning: Multiple symbols for level 1/group 1 on key <FK23>
|
> Warning: Could not resolve keysym XF86Sos
|
||||||
> Using F23, ignoring XF86TouchpadOff
|
> Warning: Could not resolve keysym XF86NavChart
|
||||||
> Warning: Symbol map for key <FK23> redefined
|
> Warning: Could not resolve keysym XF86FishingChart
|
||||||
> Using last definition for conflicting fields
|
> Warning: Could not resolve keysym XF86SingleRangeRadar
|
||||||
> Warning: Symbol map for key <FK24> redefined
|
> Warning: Could not resolve keysym XF86DualRangeRadar
|
||||||
> Using last definition for conflicting fields
|
> Warning: Could not resolve keysym XF86RadarOverlay
|
||||||
Errors from xkbcomp are not fatal to the X server
|
> Warning: Could not resolve keysym XF86TraditionalSonar
|
||||||
|
> Warning: Could not resolve keysym XF86ClearvuSonar
|
||||||
|
> Warning: Could not resolve keysym XF86SidevuSonar
|
||||||
|
> Warning: Could not resolve keysym XF86NavInfo
|
||||||
Errors from xkbcomp are not fatal to the X server
|
Errors from xkbcomp are not fatal to the X server
|
||||||
|
[2m2026-10-26T03:36:19.106508Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m XWayland ready on DISPLAY :1
|
||||||
|
[2m2026-10-26T03:36:19.108904Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m Propagated ["DISPLAY", "WAYLAND_DISPLAY"] to DBus activation environment
|
||||||
|
[2m2026-10-26T03:36:19.112684Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m Propagated ["DISPLAY", "WAYLAND_DISPLAY"] to systemd user environment
|
||||||
The XKEYBOARD keymap compiler (xkbcomp) reports:
|
The XKEYBOARD keymap compiler (xkbcomp) reports:
|
||||||
> Warning: Unsupported maximum keycode 709, clipping.
|
> Warning: Unsupported maximum keycode 708, clipping.
|
||||||
> X11 cannot support keycodes above 255.
|
> X11 cannot support keycodes above 255.
|
||||||
The XKEYBOARD keymap compiler (xkbcomp) reports:
|
|
||||||
> Warning: Unsupported maximum keycode 709, clipping.
|
|
||||||
> X11 cannot support keycodes above 255.
|
|
||||||
> Warning: Virtual modifier Hyper multiply defined
|
|
||||||
> Using 0, ignoring 0
|
|
||||||
> Warning: Virtual modifier ScrollLock multiply defined
|
|
||||||
> Using 0, ignoring 0
|
|
||||||
> Warning: Virtual modifier Hyper multiply defined
|
|
||||||
> Using 0, ignoring 0
|
|
||||||
> Warning: Virtual modifier ScrollLock multiply defined
|
> Warning: Virtual modifier ScrollLock multiply defined
|
||||||
> Using 0, ignoring 0
|
> Using 0, ignoring 0
|
||||||
Errors from xkbcomp are not fatal to the X server
|
Errors from xkbcomp are not fatal to the X server
|
||||||
Errors from xkbcomp are not fatal to the X server
|
[2m2026-10-26T03:36:19.116806Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m X11 window manager attached
|
||||||
Authorization required, but no authorization protocol specified
|
[2m2026-10-26T03:36:19.116829Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m 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 }
|
||||||
|
[2m2026-10-26T03:36:19.116868Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m 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}
|
||||||
|
[2m2026-10-26T03:36:19.117076Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m spawned Electron process [3mwindow[0m[2m=[0mmain [3mpid[0m[2m=[0m1378527 [3mrender_node[0m[2m=[0m"/dev/dri/renderD128" [3mozone[0m[2m=[0mwayland
|
||||||
|
[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] 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 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
|
||||||
|
[2m2026-10-26T03:36:19.251134Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m Electron window connected [3mwindow[0m[2m=[0mmain
|
||||||
|
[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
|
||||||
|
[Wo] Granted verified comraw system-shell capabilities
|
||||||
|
[Wo] preloadPath=/home/hellings/.local/share/wo/electron/dist/preload.js
|
||||||
|
[2m2026-10-26T03:36:19.290019Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m new_fractional_scale: setting preferred_scale(1.0)
|
||||||
|
[2m2026-10-26T03:36:19.290063Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m new_fractional_scale: done
|
||||||
|
[2m2026-10-26T03:36:19.290115Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m computed and registered document and stylesheet hashes [3mwindow[0m[2m=[0mmain [3mpage[0m[2m=[0mhttp://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 -
|
||||||
|
[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] 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] 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] 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}
|
||||||
|
[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 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] SKIPPED paint due to frame backpressure inFlight=2
|
||||||
|
[2m2026-10-26T03:36:19.634388Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m first Electron frame imported as a DMA-BUF texture [3mwindow[0m[2m=[0mmain [3mwidth[0m[2m=[0m1280 [3mheight[0m[2m=[0m800 [3mseq[0m[2m=[0m1
|
||||||
|
[2m2026-10-26T03:36:19.634880Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loaded cursor 'default' (24x24, hotspot 3,1) from /usr/share/icons/Adwaita/cursors/default
|
||||||
|
[2m2026-10-26T03:36:19.635561Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m first Electron texture rendered into the output frame [3mwindow[0m[2m=[0mmain [3mx[0m[2m=[0m0 [3my[0m[2m=[0m0 [3moutput_width[0m[2m=[0m1280 [3moutput_height[0m[2m=[0m800
|
||||||
|
[2m2026-10-26T03:36:19.663496Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m 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
|
||||||
|
Serving HTTP on 127.0.0.1 port 5173 (http://127.0.0.1:5173/) ...
|
||||||
|
|
||||||
Authorization required, but no authorization protocol specified
|
Keyboard interrupt received, exiting.
|
||||||
|
[Wo] IPC connection closed
|
||||||
Authorization required, but no authorization protocol specified
|
Gdk-Message: 23:36:25.499: Error reading events from display: Broken pipe
|
||||||
|
|
||||||
Authorization required, but no authorization protocol specified
|
|
||||||
|
|
||||||
Gtk-Message: 13:58:16.908: Failed to load module "appmenu-gtk-module"
|
|
||||||
Gtk-Message: 13:58:16.908: Failed to load module "appmenu-gtk-module"
|
|
||||||
Authorization required, but no authorization protocol specified
|
|
||||||
|
|
||||||
Authorization required, but no authorization protocol specified
|
|
||||||
|
|
||||||
[24415:0311/135817.250839:ERROR:ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc:250] '--ozone-platform=wayland' is not compatible with Vulkan. Consider switching to '--ozone-platform=x11' or disabling Vulkan
|
|
||||||
[24415:0311/135817.250839:ERROR:ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc:250] '--ozone-platform=wayland' is not compatible with Vulkan. Consider switching to '--ozone-platform=x11' or disabling Vulkan
|
|
||||||
[24415:0311/135817.264878:ERROR:components/viz/service/display_embedder/skia_output_device_vulkan.cc:301] Failed to create vulkan surface.
|
|
||||||
[24415:0311/135817.264878:ERROR:components/viz/service/display_embedder/skia_output_device_vulkan.cc:301] Failed to create vulkan surface.
|
|
||||||
[24330:0311/135939.036070:ERROR:content/browser/network_service_instance_impl.cc:613] Network service crashed, restarting service.
|
|
||||||
[24330:0311/135939.036070:ERROR:content/browser/network_service_instance_impl.cc:613] Network service crashed, restarting service.
|
|
||||||
[24330:0311/135939.185963:ERROR:content/browser/gpu/gpu_process_host.cc:999] GPU process exited unexpectedly: exit_code=15
|
|
||||||
[24330:0311/135939.185963:ERROR:content/browser/gpu/gpu_process_host.cc:999] GPU process exited unexpectedly: exit_code=15
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1213:36)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1213:36)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1235:40)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1235:40)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Authorization required, but no authorization protocol specified
|
|
||||||
|
|
||||||
Authorization required, but no authorization protocol specified
|
|
||||||
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1213:36)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1213:36)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1235:40)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1235:40)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Authorization required, but no authorization protocol specified
|
|
||||||
|
|
||||||
Authorization required, but no authorization protocol specified
|
|
||||||
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1213:36)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1213:36)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1235:40)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1235:40)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1213:36)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1213:36)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1235:40)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
Error sending from webFrameMain: Error: Render frame was disposed before WebFrameMain could be accessed
|
|
||||||
at s.send (node:electron/js2c/browser_init:2:100266)
|
|
||||||
at b.send (node:electron/js2c/browser_init:2:84687)
|
|
||||||
at Socket.<anonymous> (file:///home/hellings/Documents/wo/electron/dist/main.js:1235:40)
|
|
||||||
at Socket.emit (node:events:519:28)
|
|
||||||
at addChunk (node:internal/streams/readable:561:12)
|
|
||||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
|
||||||
at Readable.push (node:internal/streams/readable:392:5)
|
|
||||||
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
|
|
||||||
(EE) failed to read Wayland events: Broken pipe
|
|
||||||
(EE) failed to read Wayland events: Broken pipe
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue