mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
Add ext-idle-notify backend for Wayland idle detection (#7337)
* fix(electron): support Wayland idle detection via ext-idle-notify * fix(electron): address wayland idle helper review --------- Co-authored-by: Johannes Millan <johannesjo@users.noreply.github.com>
This commit is contained in:
parent
8d8d63d231
commit
0988c2d8c6
10 changed files with 641 additions and 3 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -77,6 +77,8 @@ build/hub
|
|||
electron/**/*.js
|
||||
!electron/scripts/bundle-preload.js
|
||||
electron/**/*.js.map
|
||||
electron/bin/wayland-idle-helper
|
||||
electron/wayland-idle-helper/target/
|
||||
|
||||
# mac os deployment
|
||||
*.provisionprofile
|
||||
|
|
|
|||
|
|
@ -1,14 +1,22 @@
|
|||
import { exec } from 'child_process';
|
||||
import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { createInterface } from 'readline';
|
||||
import { promisify } from 'util';
|
||||
import { powerMonitor } from 'electron';
|
||||
import { app, powerMonitor } from 'electron';
|
||||
import electronLog from 'electron-log/main';
|
||||
import { CONFIG } from './CONFIG';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const log = electronLog.scope('IdleTimeHandler');
|
||||
const WAYLAND_IDLE_HELPER_READY_TIMEOUT_MS = 3000;
|
||||
const WAYLAND_IDLE_HELPER_KILL_TIMEOUT_MS = 500;
|
||||
const WAYLAND_IDLE_HELPER_FAILURE_THRESHOLD = 3;
|
||||
|
||||
type IdleDetectionMethod =
|
||||
| 'powerMonitor'
|
||||
| 'gnomeDBus'
|
||||
| 'waylandIdleNotify'
|
||||
| 'xprintidle'
|
||||
| 'loginctl'
|
||||
| 'none';
|
||||
|
|
@ -32,10 +40,20 @@ export class IdleTimeHandler {
|
|||
private readonly _environment: EnvironmentInfo;
|
||||
private _methodDetectionPromise: Promise<IdleDetectionMethod> | null = null;
|
||||
private _workingMethod: IdleDetectionMethod = 'none';
|
||||
private _waylandIdleHelperProcess: ChildProcessWithoutNullStreams | null = null;
|
||||
private _waylandIdleHelperReady: boolean = false;
|
||||
private _waylandIdleHelperReadyPromise: Promise<boolean> | null = null;
|
||||
private _waylandIdleSinceMs: number | null = null;
|
||||
private _waylandIdleHelperFailureCount: number = 0;
|
||||
private readonly _waylandHelperTimeoutMs = CONFIG.MIN_IDLE_TIME;
|
||||
private readonly _resetWaylandIdleAfterResume = (): void => {
|
||||
this._waylandIdleSinceMs = null;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this._environment = this._detectEnvironment();
|
||||
this._methodDetectionPromise = this._initializeWorkingMethod();
|
||||
powerMonitor.on('resume', this._resetWaylandIdleAfterResume);
|
||||
}
|
||||
|
||||
private _detectEnvironment(): EnvironmentInfo {
|
||||
|
|
@ -71,6 +89,16 @@ export class IdleTimeHandler {
|
|||
return this._workingMethod;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
powerMonitor.off('resume', this._resetWaylandIdleAfterResume);
|
||||
|
||||
const child = this._waylandIdleHelperProcess;
|
||||
if (child) {
|
||||
this._stopWaylandIdleHelperProcess(child);
|
||||
}
|
||||
this._resetWaylandIdleHelperState();
|
||||
}
|
||||
|
||||
async getIdleTime(): Promise<number> {
|
||||
const methodUsed = await this._ensureWorkingMethod();
|
||||
|
||||
|
|
@ -92,6 +120,23 @@ export class IdleTimeHandler {
|
|||
return 0;
|
||||
}
|
||||
|
||||
case 'waylandIdleNotify':
|
||||
try {
|
||||
const isReady = await this._ensureWaylandIdleHelperStarted();
|
||||
if (!isReady || this._waylandIdleSinceMs === null) {
|
||||
if (!isReady) {
|
||||
this._handleWaylandIdleHelperFailure();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
this._waylandIdleHelperFailureCount = 0;
|
||||
return Math.max(0, Date.now() - this._waylandIdleSinceMs);
|
||||
} catch (error) {
|
||||
this._logError('Wayland idle-notify helper error', error);
|
||||
this._handleWaylandIdleHelperFailure();
|
||||
return 0;
|
||||
}
|
||||
|
||||
case 'xprintidle':
|
||||
try {
|
||||
const result = await this._getXprintidleTime();
|
||||
|
|
@ -185,6 +230,11 @@ export class IdleTimeHandler {
|
|||
});
|
||||
}
|
||||
|
||||
candidates.push({
|
||||
name: 'waylandIdleNotify',
|
||||
test: async () => this._ensureWaylandIdleHelperStarted(),
|
||||
});
|
||||
|
||||
candidates.push({
|
||||
name: 'xprintidle',
|
||||
test: async () => {
|
||||
|
|
@ -261,6 +311,177 @@ export class IdleTimeHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
private async _ensureWaylandIdleHelperStarted(): Promise<boolean> {
|
||||
if (this._waylandIdleHelperReady && this._waylandIdleHelperProcess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this._waylandIdleHelperReadyPromise) {
|
||||
return this._waylandIdleHelperReadyPromise;
|
||||
}
|
||||
|
||||
const helperPath = this._resolveWaylandIdleHelperPath();
|
||||
if (!helperPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._waylandIdleHelperReadyPromise = new Promise<boolean>((resolve) => {
|
||||
let isSettled = false;
|
||||
const settle = (value: boolean): void => {
|
||||
if (isSettled) {
|
||||
return;
|
||||
}
|
||||
isSettled = true;
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
const child = spawn(
|
||||
helperPath,
|
||||
['--timeout-ms', String(this._waylandHelperTimeoutMs)],
|
||||
{
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
);
|
||||
child.unref();
|
||||
|
||||
this._waylandIdleHelperProcess = child;
|
||||
this._waylandIdleHelperReady = false;
|
||||
this._waylandIdleSinceMs = null;
|
||||
|
||||
const readyTimeout = setTimeout(() => {
|
||||
log.warn('Wayland idle helper did not become ready in time');
|
||||
this._stopWaylandIdleHelperProcess(child);
|
||||
if (this._waylandIdleHelperProcess === child) {
|
||||
this._resetWaylandIdleHelperState();
|
||||
}
|
||||
settle(false);
|
||||
}, WAYLAND_IDLE_HELPER_READY_TIMEOUT_MS);
|
||||
|
||||
const stdout = createInterface({ input: child.stdout });
|
||||
const stderr = createInterface({ input: child.stderr });
|
||||
let isOutputClosed = false;
|
||||
const closeOutput = (): void => {
|
||||
if (isOutputClosed) {
|
||||
return;
|
||||
}
|
||||
isOutputClosed = true;
|
||||
stdout.close();
|
||||
stderr.close();
|
||||
};
|
||||
|
||||
stdout.on('line', (line) => {
|
||||
const message = line.trim();
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message === 'ready') {
|
||||
clearTimeout(readyTimeout);
|
||||
this._waylandIdleHelperReady = true;
|
||||
this._waylandIdleHelperFailureCount = 0;
|
||||
settle(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message === 'idle') {
|
||||
this._waylandIdleSinceMs = Date.now() - this._waylandHelperTimeoutMs;
|
||||
return;
|
||||
}
|
||||
|
||||
if (message === 'resumed') {
|
||||
this._waylandIdleSinceMs = null;
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Wayland idle helper sent unexpected message: ${message}`);
|
||||
});
|
||||
|
||||
stderr.on('line', (line) => {
|
||||
log.debug(`Wayland idle helper stderr: ${line}`);
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
clearTimeout(readyTimeout);
|
||||
closeOutput();
|
||||
log.warn('Failed to start Wayland idle helper', error);
|
||||
if (this._waylandIdleHelperProcess === child) {
|
||||
this._resetWaylandIdleHelperState();
|
||||
}
|
||||
settle(false);
|
||||
});
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
clearTimeout(readyTimeout);
|
||||
closeOutput();
|
||||
if (this._waylandIdleHelperProcess !== child) {
|
||||
settle(false);
|
||||
return;
|
||||
}
|
||||
if (this._waylandIdleHelperReady) {
|
||||
log.warn(
|
||||
`Wayland idle helper exited (code=${code ?? 'null'}, signal=${signal ?? 'null'})`,
|
||||
);
|
||||
}
|
||||
this._resetWaylandIdleHelperState();
|
||||
settle(false);
|
||||
});
|
||||
}).finally(() => {
|
||||
this._waylandIdleHelperReadyPromise = null;
|
||||
});
|
||||
|
||||
return this._waylandIdleHelperReadyPromise;
|
||||
}
|
||||
|
||||
private _resetWaylandIdleHelperState(): void {
|
||||
this._waylandIdleHelperProcess = null;
|
||||
this._waylandIdleHelperReady = false;
|
||||
this._waylandIdleSinceMs = null;
|
||||
}
|
||||
|
||||
private _stopWaylandIdleHelperProcess(child: ChildProcessWithoutNullStreams): void {
|
||||
if (child.exitCode !== null || child.signalCode !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let didExit = false;
|
||||
child.once('exit', () => {
|
||||
didExit = true;
|
||||
});
|
||||
child.kill('SIGTERM');
|
||||
|
||||
const killTimer = setTimeout(() => {
|
||||
if (!didExit && child.exitCode === null && child.signalCode === null) {
|
||||
child.kill('SIGKILL');
|
||||
}
|
||||
}, WAYLAND_IDLE_HELPER_KILL_TIMEOUT_MS);
|
||||
killTimer.unref();
|
||||
}
|
||||
|
||||
private _handleWaylandIdleHelperFailure(): void {
|
||||
this._waylandIdleHelperFailureCount += 1;
|
||||
if (this._waylandIdleHelperFailureCount < WAYLAND_IDLE_HELPER_FAILURE_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn('Wayland idle helper failed repeatedly; re-detecting idle method');
|
||||
this._waylandIdleHelperFailureCount = 0;
|
||||
this._workingMethod = 'none';
|
||||
this._methodDetectionPromise = null;
|
||||
}
|
||||
|
||||
private _resolveWaylandIdleHelperPath(): string | null {
|
||||
const helperPath = app.isPackaged
|
||||
? path.join(path.dirname(process.execPath), 'wayland-idle-helper')
|
||||
: path.join(__dirname, 'bin', 'wayland-idle-helper');
|
||||
|
||||
if (!existsSync(helperPath)) {
|
||||
log.debug(`Wayland idle helper binary not found at ${helperPath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return helperPath;
|
||||
}
|
||||
|
||||
private async _getLoginctlIdleTime(): Promise<number | null> {
|
||||
const { stdout } = await execAsync('loginctl show-session -p IdleSinceHint', {
|
||||
timeout: 3000,
|
||||
|
|
|
|||
|
|
@ -418,6 +418,7 @@ export const startApp = (): void => {
|
|||
return;
|
||||
}
|
||||
// isQuiting=true: all before-close IPC work is complete — safe to clean up.
|
||||
idleTimeHandler?.dispose();
|
||||
destroyTaskWidget();
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
|
|
|
|||
203
electron/wayland-idle-helper/Cargo.lock
generated
Normal file
203
electron/wayland-idle-helper/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.185"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.39.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
"rustix",
|
||||
"smallvec",
|
||||
"wayland-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-client"
|
||||
version = "0.31.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"rustix",
|
||||
"wayland-backend",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-idle-helper"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.32.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-scanner"
|
||||
version = "0.31.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-sys"
|
||||
version = "0.31.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
|
||||
dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
14
electron/wayland-idle-helper/Cargo.toml
Normal file
14
electron/wayland-idle-helper/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "wayland-idle-helper"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
wayland-client = "0.31.11"
|
||||
wayland-protocols = { version = "0.32.9", features = ["client", "staging"] }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
panic = "abort"
|
||||
2
electron/wayland-idle-helper/rust-toolchain.toml
Normal file
2
electron/wayland-idle-helper/rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "stable"
|
||||
114
electron/wayland-idle-helper/src/main.rs
Normal file
114
electron/wayland-idle-helper/src/main.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::io::{self, ErrorKind, Write};
|
||||
use std::process;
|
||||
|
||||
use wayland_client::globals::{registry_queue_init, GlobalListContents};
|
||||
use wayland_client::protocol::wl_registry::WlRegistry;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::{delegate_noop, Connection, Dispatch, EventQueue, QueueHandle};
|
||||
use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notification_v1::{
|
||||
Event as IdleNotificationEvent, ExtIdleNotificationV1,
|
||||
};
|
||||
use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notifier_v1::ExtIdleNotifierV1;
|
||||
|
||||
struct AppState {
|
||||
stdout: io::Stdout,
|
||||
_seat: WlSeat,
|
||||
_notifier: ExtIdleNotifierV1,
|
||||
_notification: ExtIdleNotificationV1,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
fn emit(&mut self, event: &str) {
|
||||
let mut lock = self.stdout.lock();
|
||||
if let Err(err) = writeln!(lock, "{event}").and_then(|_| lock.flush()) {
|
||||
if err.kind() == ErrorKind::BrokenPipe {
|
||||
process::exit(0);
|
||||
}
|
||||
eprintln!("failed to write event: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate_noop!(AppState: ignore WlSeat);
|
||||
delegate_noop!(AppState: ignore ExtIdleNotifierV1);
|
||||
|
||||
impl Dispatch<WlRegistry, GlobalListContents> for AppState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &WlRegistry,
|
||||
_event: wayland_client::protocol::wl_registry::Event,
|
||||
_data: &GlobalListContents,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ExtIdleNotificationV1, ()> for AppState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_proxy: &ExtIdleNotificationV1,
|
||||
event: IdleNotificationEvent,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
IdleNotificationEvent::Idled => state.emit("idle"),
|
||||
IdleNotificationEvent::Resumed => state.emit("resumed"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_timeout_ms() -> Result<u32, Box<dyn Error>> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
match args.as_slice() {
|
||||
[_, flag, value] if flag == "--timeout-ms" => Ok(value.parse::<u32>()?),
|
||||
_ => Err("usage: wayland-idle-helper --timeout-ms <ms>".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn init_wayland(
|
||||
timeout_ms: u32,
|
||||
) -> Result<(Connection, EventQueue<AppState>, AppState), Box<dyn Error>> {
|
||||
let conn = Connection::connect_to_env()?;
|
||||
let (globals, event_queue) = registry_queue_init::<AppState>(&conn)?;
|
||||
let qh = event_queue.handle();
|
||||
|
||||
let seat: WlSeat = globals.bind(&qh, 1..=9, ())?;
|
||||
let notifier: ExtIdleNotifierV1 = globals.bind(&qh, 1..=2, ())?;
|
||||
let notification = notifier.get_idle_notification(timeout_ms, &seat, &qh, ());
|
||||
|
||||
Ok((
|
||||
conn,
|
||||
event_queue,
|
||||
// Keep these Wayland objects alive for the lifetime of the connection.
|
||||
AppState {
|
||||
stdout: io::stdout(),
|
||||
_seat: seat,
|
||||
_notifier: notifier,
|
||||
_notification: notification,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn run() -> Result<(), Box<dyn Error>> {
|
||||
let timeout_ms = parse_timeout_ms()?;
|
||||
let (_conn, mut event_queue, mut state) = init_wayland(timeout_ms)?;
|
||||
|
||||
state.emit("ready");
|
||||
|
||||
loop {
|
||||
event_queue.blocking_dispatch(&mut state)?;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
eprintln!("{err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
"e2e:docker:webdav": "docker compose -f docker-compose.e2e.yaml up -d app webdav && ./scripts/wait-for-app.sh && ./scripts/wait-for-webdav.sh && E2E_BASE_URL=http://localhost:${APP_PORT:-4242} npm run e2e:all -- --grep @webdav; docker compose -f docker-compose.e2e.yaml down",
|
||||
"e2e:docker:all": "docker compose -f docker-compose.e2e.yaml -f docker-compose.yaml -f docker-compose.supersync.yaml up -d app webdav db supersync && ./scripts/wait-for-app.sh && ./scripts/wait-for-webdav.sh && ./scripts/wait-for-supersync.sh && E2E_BASE_URL=http://localhost:${APP_PORT:-4242} npm run e2e:all -- --workers=6; EXIT_CODE=$?; docker compose -f docker-compose.e2e.yaml -f docker-compose.yaml -f docker-compose.supersync.yaml down; exit $EXIT_CODE",
|
||||
"electron": "NODE_ENV=PROD electron .",
|
||||
"electron:build": "tsc -p electron/tsconfig.electron.json && node electron/scripts/bundle-preload.js",
|
||||
"electron:build": "node ./tools/build-wayland-idle-helper.js && tsc -p electron/tsconfig.electron.json && node electron/scripts/bundle-preload.js",
|
||||
"electron:verify-asar": "node tools/verify-electron-requires.js .tmp/app-builds/linux-unpacked/resources/app.asar",
|
||||
"electron:watch": "tsc -p electron/tsconfig.electron.json --watch",
|
||||
"electronBuilderOnly": "electron-builder",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,14 @@ const { join } = require('path');
|
|||
const BIN_NAME = 'superproductivity'; // must match linux.executableName
|
||||
const RENAMED = 'superproductivity-bin';
|
||||
const WRAPPER_SRC = join(__dirname, '..', 'build', 'linux', 'snap-wrapper.sh');
|
||||
const WAYLAND_IDLE_HELPER_SRC = join(
|
||||
__dirname,
|
||||
'..',
|
||||
'electron',
|
||||
'bin',
|
||||
'wayland-idle-helper',
|
||||
);
|
||||
const WAYLAND_IDLE_HELPER_DEST = 'wayland-idle-helper';
|
||||
|
||||
async function afterPack(context) {
|
||||
if (context.electronPlatformName !== 'linux') return;
|
||||
|
|
@ -28,6 +36,20 @@ async function afterPack(context) {
|
|||
const { appOutDir } = context;
|
||||
const binPath = join(appOutDir, BIN_NAME);
|
||||
const renamedPath = join(appOutDir, RENAMED);
|
||||
const helperDestPath = join(appOutDir, WAYLAND_IDLE_HELPER_DEST);
|
||||
|
||||
const installWaylandIdleHelper = async () => {
|
||||
const helperStat = await fs.stat(WAYLAND_IDLE_HELPER_SRC).catch(() => null);
|
||||
if (!helperStat) {
|
||||
console.warn(
|
||||
`[afterPack] ${WAYLAND_IDLE_HELPER_SRC} not found; skipping Wayland idle helper copy`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.copyFile(WAYLAND_IDLE_HELPER_SRC, helperDestPath);
|
||||
await fs.chmod(helperDestPath, 0o755);
|
||||
};
|
||||
|
||||
// Read wrapper content BEFORE touching appOutDir. If the source file is
|
||||
// missing or unreadable we fail fast with the Electron binary still in
|
||||
|
|
@ -50,6 +72,7 @@ async function afterPack(context) {
|
|||
if (binStat && renamedStat) {
|
||||
const head = await fs.readFile(binPath, 'utf8').catch(() => '');
|
||||
if (head.startsWith('#!')) {
|
||||
await installWaylandIdleHelper();
|
||||
console.log(`[afterPack] wrapper already installed`);
|
||||
return;
|
||||
}
|
||||
|
|
@ -78,6 +101,7 @@ async function afterPack(context) {
|
|||
}
|
||||
|
||||
await fs.chmod(renamedPath, 0o755);
|
||||
await installWaylandIdleHelper();
|
||||
|
||||
console.log(
|
||||
`[afterPack] Installed argv wrapper: ${BIN_NAME} -> ${RENAMED} + shell wrapper`,
|
||||
|
|
|
|||
57
tools/build-wayland-idle-helper.js
Normal file
57
tools/build-wayland-idle-helper.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const CRATE_DIR = path.join(REPO_ROOT, 'electron', 'wayland-idle-helper');
|
||||
const TARGET_DIR = path.join(REPO_ROOT, '.tmp', 'rust-target');
|
||||
const OUTPUT_DIR = path.join(REPO_ROOT, 'electron', 'bin');
|
||||
const OUTPUT_PATH = path.join(OUTPUT_DIR, 'wayland-idle-helper');
|
||||
|
||||
const run = (command, args, options = {}) => {
|
||||
const result = spawnSync(command, args, {
|
||||
stdio: 'inherit',
|
||||
...options,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
};
|
||||
|
||||
const hasCargo = () => {
|
||||
const result = spawnSync('cargo', ['--version'], {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
return result.status === 0;
|
||||
};
|
||||
|
||||
const buildHelper = () => {
|
||||
if (!hasCargo()) {
|
||||
console.warn(
|
||||
'[build-wayland-idle-helper] Rust toolchain not found -- skipping Wayland idle helper. Install via https://rustup.rs if you need it.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
run('cargo', [
|
||||
'build',
|
||||
'--release',
|
||||
'--locked',
|
||||
'--manifest-path',
|
||||
path.join(CRATE_DIR, 'Cargo.toml'),
|
||||
'--target-dir',
|
||||
TARGET_DIR,
|
||||
]);
|
||||
|
||||
const builtBinaryPath = path.join(TARGET_DIR, 'release', 'wayland-idle-helper');
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
const binaryContents = fs.readFileSync(builtBinaryPath);
|
||||
const tempOutputPath = `${OUTPUT_PATH}.tmp`;
|
||||
fs.writeFileSync(tempOutputPath, binaryContents);
|
||||
fs.chmodSync(tempOutputPath, 0o755);
|
||||
fs.renameSync(tempOutputPath, OUTPUT_PATH);
|
||||
};
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
buildHelper();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue