230 lines
8.3 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|