/*
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 { execSync } from 'child_process';
import { BaseWindowTracker } from './base-tracker';
interface SwayRect {
x: number;
y: number;
width: number;
height: number;
}
interface SwayNode {
pid?: number;
app_id?: string;
window_properties?: { class?: string };
rect?: SwayRect;
nodes?: SwayNode[];
floating_nodes?: SwayNode[];
}
export class SwayWindowTracker extends BaseWindowTracker {
private pollInterval: ReturnType | null = null;
private readonly targetMpvSocketPath: string | null;
constructor(targetMpvSocketPath?: string) {
super();
this.targetMpvSocketPath = targetMpvSocketPath?.trim() || null;
}
start(): void {
this.pollInterval = setInterval(() => this.pollGeometry(), 250);
this.pollGeometry();
}
stop(): void {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
}
private collectMpvWindows(node: SwayNode): SwayNode[] {
const windows: SwayNode[] = [];
if (node.app_id === 'mpv' || node.window_properties?.class === 'mpv') {
windows.push(node);
}
if (node.nodes) {
for (const child of node.nodes) {
windows.push(...this.collectMpvWindows(child));
}
}
if (node.floating_nodes) {
for (const child of node.floating_nodes) {
windows.push(...this.collectMpvWindows(child));
}
}
return windows;
}
private findTargetSocketWindow(node: SwayNode): SwayNode | null {
const windows = this.collectMpvWindows(node);
if (!this.targetMpvSocketPath) {
return windows[0] || null;
}
return windows.find((candidate) => this.isWindowForTargetSocket(candidate)) || null;
}
private isWindowForTargetSocket(node: SwayNode): boolean {
if (!node.pid) {
return false;
}
const commandLine = this.getWindowCommandLine(node.pid);
if (!commandLine) {
return false;
}
return (
commandLine.includes(`--input-ipc-server=${this.targetMpvSocketPath}`) ||
commandLine.includes(`--input-ipc-server ${this.targetMpvSocketPath}`)
);
}
private getWindowCommandLine(pid: number): string | null {
const commandLine = execSync(`ps -p ${pid} -o args=`, {
encoding: 'utf-8',
}).trim();
return commandLine || null;
}
private pollGeometry(): void {
try {
const output = execSync('swaymsg -t get_tree', { encoding: 'utf-8' });
const tree: SwayNode = JSON.parse(output);
const mpvWindow = this.findTargetSocketWindow(tree);
if (mpvWindow && mpvWindow.rect) {
this.updateGeometry({
x: mpvWindow.rect.x,
y: mpvWindow.rect.y,
width: mpvWindow.rect.width,
height: mpvWindow.rect.height,
});
} else {
this.updateGeometry(null);
}
} catch (err) {
// swaymsg not available or failed - silent fail
}
}
}