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:
David Vornholt 2026-04-24 16:50:24 +02:00 committed by GitHub
parent 8d8d63d231
commit 0988c2d8c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 641 additions and 3 deletions

2
.gitignore vendored
View file

@ -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

View file

@ -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,

View file

@ -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
View 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",
]

View 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"

View file

@ -0,0 +1,2 @@
[toolchain]
channel = "stable"

View 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);
}
}

View file

@ -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",

View file

@ -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`,

View 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();
}