Files
imperial-command-center/electron/services/ScreenManager.ts

230 lines
8.3 KiB
TypeScript

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 idleTimeout: number = 300000; // 5 minutes default
private idleTimer: NodeJS.Timeout | null = null;
private isScreenOn: boolean = true;
private lastActivity: number = Date.now();
private screenControlAvailable: boolean = true;
private isWayland: boolean = false;
constructor(window: BrowserWindow) {
this.window = window;
this.detectDisplayServer();
this.setupActivityListeners();
this.startIdleMonitor();
}
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 {
this.lastActivity = Date.now();
// Always try to wake the screen on touch/click - user is actively interacting
if (!this.isScreenOn) {
this.wakeScreen();
}
}
private startIdleMonitor(): void {
this.idleTimer = setInterval(() => {
const idleTime = Date.now() - this.lastActivity;
if (idleTime >= this.idleTimeout && this.isScreenOn && this.screenControlAvailable) {
this.sleepScreen();
}
}, 30000); // Check every 30 seconds (not 10) to reduce spam
}
private resetIdleTimer(): void {
this.lastActivity = Date.now();
if (!this.isScreenOn) {
this.wakeScreen();
}
}
setIdleTimeout(timeout: number): void {
this.idleTimeout = timeout;
}
async wakeScreen(): Promise<void> {
// 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 <x> <y> (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.lastActivity = Date.now();
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<void> {
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;
}
}
}