import { BrowserWindow } from 'electron'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); // DBus environment for GNOME Wayland - needed to communicate with session services const DBUS_ENV = { XDG_RUNTIME_DIR: '/run/user/1000', DBUS_SESSION_BUS_ADDRESS: 'unix:path=/run/user/1000/bus', }; export class ScreenManager { private window: BrowserWindow; private idleTimer: NodeJS.Timeout | null = null; private isScreenOn: boolean = true; private screenControlAvailable: boolean = true; private isWayland: boolean = false; constructor(window: BrowserWindow) { this.window = window; this.detectDisplayServer(); this.setupActivityListeners(); // Idle monitor intentionally disabled: monitor stays on; photo frame // handles the visual idle state instead. } private detectDisplayServer(): void { // Check if running under Wayland this.isWayland = !!(process.env.WAYLAND_DISPLAY || process.env.XDG_SESSION_TYPE === 'wayland'); console.log(`ScreenManager: Display server detected - ${this.isWayland ? 'Wayland' : 'X11'}`); } private setupActivityListeners(): void { // Monitor keyboard and mouse activity this.window.webContents.on('before-input-event', () => { this.resetIdleTimer(); }); // Also listen for any cursor/pointer events via IPC from renderer // Touch events on Wayland may not trigger before-input-event } // Called from renderer when touch/click detected public handleUserActivity(): void { if (!this.isScreenOn) { this.wakeScreen(); } } private resetIdleTimer(): void { if (!this.isScreenOn) { this.wakeScreen(); } } setIdleTimeout(_timeout: number): void { // no-op: idle monitor is disabled; monitor stays on } async wakeScreen(): Promise { // Always attempt to wake - the screen may have been externally put to sleep by GNOME if (!this.screenControlAvailable) { this.isScreenOn = true; return; } try { if (process.platform === 'linux') { if (this.isWayland) { // On GNOME Wayland, we need multiple approaches because: // 1. The screensaver/lock screen blocks DBus SetActive calls // 2. We need to wake DPMS separately from screensaver // 3. Simulating input is the most reliable way to wake let woke = false; // Method 1: Simulate mouse movement with ydotool (most reliable on Wayland) // This bypasses all the GNOME screensaver/DPMS complications try { // ydotool syntax: mousemove (relative movement) await execAsync('ydotool mousemove 1 0 && ydotool mousemove -- -1 0', { env: { ...process.env, ...DBUS_ENV } }); console.log('ScreenManager: Screen woke via ydotool mouse movement'); woke = true; } catch (e) { console.log('ScreenManager: ydotool failed:', e); } // Method 2: loginctl unlock-session (unlocks GNOME lock screen) if (!woke) { try { await execAsync('loginctl unlock-session', { env: { ...process.env, ...DBUS_ENV } }); console.log('ScreenManager: Session unlocked via loginctl'); woke = true; } catch (e) { console.log('ScreenManager: loginctl unlock failed:', e); } } // Method 3: gnome-screensaver-command (older GNOME) if (!woke) { try { await execAsync('gnome-screensaver-command --deactivate', { env: { ...process.env, ...DBUS_ENV } }); console.log('ScreenManager: Screen woke via gnome-screensaver-command'); woke = true; } catch { console.log('ScreenManager: gnome-screensaver-command not available'); } } // Method 4: DBus SetActive (works when screen is blanked but not locked) if (!woke) { try { await execAsync('dbus-send --session --dest=org.gnome.ScreenSaver --type=method_call /org/gnome/ScreenSaver org.gnome.ScreenSaver.SetActive boolean:false', { env: { ...process.env, ...DBUS_ENV } }); console.log('ScreenManager: Screen woke via dbus-send'); woke = true; } catch { console.log('ScreenManager: dbus-send SetActive failed'); } } // Method 5: Wake DPMS via wlr-randr or gnome-randr try { await execAsync('gnome-randr modify --on DP-1 2>/dev/null || wlr-randr --output DP-1 --on 2>/dev/null || true', { env: { ...process.env, ...DBUS_ENV } }); } catch { // Ignore - display names vary } if (!woke) { console.log('ScreenManager: All wake methods failed'); } } else { // X11 methods try { await execAsync('xset dpms force on'); } catch { try { await execAsync('xdotool key shift'); } catch { console.log('ScreenManager: No X11 screen control available, disabling feature'); this.screenControlAvailable = false; } } } } else if (process.platform === 'win32') { await execAsync( 'powershell -Command "(Add-Type -MemberDefinition \'[DllImport(\\"user32.dll\\")]public static extern int SendMessage(int hWnd,int hMsg,int wParam,int lParam);\' -Name a -Pas)::SendMessage(-1,0x0112,0xF170,-1)"' ); } else if (process.platform === 'darwin') { await execAsync('caffeinate -u -t 1'); } this.isScreenOn = true; this.window.webContents.send('screen:woke'); } catch (error) { console.error('Failed to wake screen:', error); this.isScreenOn = true; // Assume screen is on to prevent retry loops } } async sleepScreen(): Promise { if (!this.isScreenOn) return; if (!this.screenControlAvailable) return; try { if (process.platform === 'linux') { if (this.isWayland) { // Use busctl with DBUS session for GNOME on Wayland try { await execAsync('busctl --user call org.gnome.ScreenSaver /org/gnome/ScreenSaver org.gnome.ScreenSaver SetActive b true', { env: { ...process.env, ...DBUS_ENV } }); console.log('ScreenManager: Screen slept via GNOME ScreenSaver DBus'); } catch { try { // Fallback: Try wlopm await execAsync('wlopm --off \\*', { env: { ...process.env, ...DBUS_ENV } }); } catch { // No working method - disable screen control console.log('ScreenManager: Wayland screen sleep not available, disabling feature'); this.screenControlAvailable = false; return; } } } else { try { await execAsync('xset dpms force off'); } catch { console.log('ScreenManager: X11 screen sleep not available, disabling feature'); this.screenControlAvailable = false; return; } } } else if (process.platform === 'win32') { await execAsync( 'powershell -Command "(Add-Type -MemberDefinition \'[DllImport(\\"user32.dll\\")]public static extern int SendMessage(int hWnd,int hMsg,int wParam,int lParam);\' -Name a -Pas)::SendMessage(-1,0x0112,0xF170,2)"' ); } else if (process.platform === 'darwin') { await execAsync('pmset displaysleepnow'); } this.isScreenOn = false; this.window.webContents.send('screen:slept'); } catch (error) { console.error('Failed to sleep screen:', error); // Disable feature to prevent spam this.screenControlAvailable = false; } } destroy(): void { if (this.idleTimer) { clearInterval(this.idleTimer); this.idleTimer = null; } } }