From 32788fafdc03ea496b7791b522b1aa984494bed0 Mon Sep 17 00:00:00 2001 From: Twest2 Date: Fri, 26 Jun 2026 15:48:09 -0500 Subject: [PATCH] fixed ubuntu (Wayland) recording workflow --- app/capture.js | 30 +++++++++++++++++++++++++++--- scripts/start-electron.js | 8 +++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/app/capture.js b/app/capture.js index eebe825..e6080c3 100644 --- a/app/capture.js +++ b/app/capture.js @@ -107,6 +107,15 @@ function hasBinary(name) { } } +// On Wayland, xinput only sees XWayland (X11-bridge) events — native Wayland +// app clicks are delivered via the Wayland protocol and never reach xinput. +// Treating it as "available" would leave the session with no click capture AND +// no interval fallback, so zero steps get captured. +function isWayland() { + return process.platform === 'linux' + && (process.env.XDG_SESSION_TYPE === 'wayland' || Boolean(process.env.WAYLAND_DISPLAY)); +} + class CaptureService { constructor({ store, @@ -184,7 +193,11 @@ class CaptureService { clickCaptureAvailable() { if (this._clickAvail === undefined) { - this._clickAvail = process.platform === 'win32' || (process.platform === 'linux' && hasBinary('xinput')); + // Wayland: xinput connects via XWayland and only sees X11-bridge clicks. + // Native Wayland app events are invisible to it, so we can't reliably + // detect clicks — disable this path and fall back to interval capture. + this._clickAvail = process.platform === 'win32' + || (process.platform === 'linux' && !isWayland() && hasBinary('xinput')); } return this._clickAvail; } @@ -300,6 +313,7 @@ class CaptureService { showWindow() { const win = this.getWindow(); if (win && !win.isDestroyed()) { + if (win.isMinimized()) win.restore(); win.show(); win.focus(); } @@ -383,7 +397,16 @@ class CaptureService { if (!this.session || this.session.paused) { this.warmingUp = false; return; } } if (wantHide && win && !win.isDestroyed() && win.isVisible()) { - win.hide(); + // On Linux without a working system tray (common on Wayland GNOME + // without AppIndicator) the user has no way to bring back a hidden + // window mid-session. Minimize instead: the window disappears from + // screen (so it won't appear in fullscreen screenshots) but remains + // accessible via the taskbar. + if (process.platform === 'linux' && (!this.tray || this.tray.isDestroyed())) { + win.minimize(); + } else { + win.hide(); + } // Let a couple of frames of the now-unobscured screen land before // the user's first click, so that frame shows their work, not the // app window that was just dismissed. @@ -775,13 +798,14 @@ class CaptureService { try { this.clickWatcherBuf = ''; this.linuxEvent = null; - if (process.platform === 'linux' && hasBinary('xinput')) { + if (process.platform === 'linux' && !isWayland() && hasBinary('xinput')) { // Stream raw button events from the X server; one capture per press. // xinput block-buffers stdout when piped, so a press event can sit // in its buffer until later motion events flush it — by then the // cursor read in onOsClick lands where the mouse moved *after* the // click. stdbuf -oL forces line-buffering so events (and the cursor // read) line up with the actual click instant. + // (Skipped on Wayland: xinput only sees XWayland events — see isWayland.) const argv = hasBinary('stdbuf') ? ['stdbuf', '-oL', 'xinput', 'test-xi2', '--root'] : ['xinput', 'test-xi2', '--root']; diff --git a/scripts/start-electron.js b/scripts/start-electron.js index cec232f..0065906 100644 --- a/scripts/start-electron.js +++ b/scripts/start-electron.js @@ -14,7 +14,13 @@ try { } const env = sanitizeElectronEnv(); -const child = spawn(electronPath, ['.'], { +// On Linux/Wayland, enable PipeWire-based screen capture so desktopCapturer +// can go through the XDG Desktop Portal when XWayland isn't the only option. +const extraArgs = process.platform === 'linux' + ? ['--enable-features=WebRTCPipeWireCapturer'] + : []; + +const child = spawn(electronPath, [...extraArgs, '.'], { stdio: 'inherit', env, windowsHide: false,