/* SubMiner - All-in-one sentence mining overlay Copyright (C) 2024 sudacode This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import { execFile } from 'child_process'; import { BaseWindowTracker } from './base-tracker'; type CommandRunner = (command: string, args: string[]) => Promise; function execFileUtf8(command: string, args: string[]): Promise { return new Promise((resolve, reject) => { execFile(command, args, { encoding: 'utf-8' }, (error, stdout) => { if (error) { reject(error); return; } resolve(stdout); }); }); } export function parseX11WindowGeometry(winInfo: string): { x: number; y: number; width: number; height: number; } | null { const xMatch = winInfo.match(/Absolute upper-left X:\s*(\d+)/); const yMatch = winInfo.match(/Absolute upper-left Y:\s*(\d+)/); const widthMatch = winInfo.match(/Width:\s*(\d+)/); const heightMatch = winInfo.match(/Height:\s*(\d+)/); if (!xMatch || !yMatch || !widthMatch || !heightMatch) { return null; } return { x: parseInt(xMatch[1]!, 10), y: parseInt(yMatch[1]!, 10), width: parseInt(widthMatch[1]!, 10), height: parseInt(heightMatch[1]!, 10), }; } export function parseX11WindowPid(raw: string): number | null { const pidMatch = raw.match(/= (\d+)/); if (!pidMatch) { return null; } const pid = Number.parseInt(pidMatch[1]!, 10); return Number.isInteger(pid) ? pid : null; } export class X11WindowTracker extends BaseWindowTracker { private pollInterval: ReturnType | null = null; private readonly targetMpvSocketPath: string | null; private readonly runCommand: CommandRunner; private pollInFlight = false; private currentPollIntervalMs = 750; private readonly stablePollIntervalMs = 250; constructor(targetMpvSocketPath?: string, runCommand: CommandRunner = execFileUtf8) { super(); this.targetMpvSocketPath = targetMpvSocketPath?.trim() || null; this.runCommand = runCommand; } start(): void { this.resetPollInterval(this.currentPollIntervalMs); this.pollGeometry(); } stop(): void { if (this.pollInterval) { clearInterval(this.pollInterval); this.pollInterval = null; } } private resetPollInterval(intervalMs: number): void { if (this.pollInterval) { clearInterval(this.pollInterval); this.pollInterval = null; } this.pollInterval = setInterval(() => this.pollGeometry(), intervalMs); } private pollGeometry(): void { if (this.pollInFlight) { return; } this.pollInFlight = true; void this.pollGeometryAsync() .catch(() => { this.updateGeometry(null); }) .finally(() => { this.pollInFlight = false; }); } private async pollGeometryAsync(): Promise { const windowIdsOutput = await this.runCommand('xdotool', ['search', '--class', 'mpv']); const windowIds = windowIdsOutput.trim(); if (!windowIds) { this.updateGeometry(null); return; } const windowIdList = windowIds.split(/\s+/).filter(Boolean); if (windowIdList.length === 0) { this.updateGeometry(null); return; } const windowId = await this.findTargetWindowId(windowIdList); if (!windowId) { this.updateGeometry(null); return; } const winInfo = await this.runCommand('xwininfo', ['-id', windowId]); const geometry = parseX11WindowGeometry(winInfo); if (!geometry) { this.updateGeometry(null); return; } this.updateGeometry(geometry); if (this.pollInterval && this.currentPollIntervalMs !== this.stablePollIntervalMs) { this.currentPollIntervalMs = this.stablePollIntervalMs; this.resetPollInterval(this.currentPollIntervalMs); } } private async findTargetWindowId(windowIds: string[]): Promise { if (!this.targetMpvSocketPath) { return windowIds[0] ?? null; } for (const windowId of windowIds) { if (await this.isWindowForTargetSocket(windowId)) { return windowId; } } return null; } private async isWindowForTargetSocket(windowId: string): Promise { const pid = await this.getWindowPid(windowId); if (pid === null) { return false; } const commandLine = await this.getWindowCommandLine(pid); if (!commandLine) { return false; } return ( commandLine.includes(`--input-ipc-server=${this.targetMpvSocketPath}`) || commandLine.includes(`--input-ipc-server ${this.targetMpvSocketPath}`) ); } private async getWindowPid(windowId: string): Promise { let windowPid: string; try { windowPid = await this.runCommand('xprop', ['-id', windowId, '_NET_WM_PID']); } catch { return null; } return parseX11WindowPid(windowPid); } private async getWindowCommandLine(pid: number): Promise { let raw: string; try { raw = await this.runCommand('ps', ['-p', String(pid), '-o', 'args=']); } catch { return null; } const commandLine = raw.trim(); return commandLine || null; } }