mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -07:00
fix(tracker): follow active hyprland and visible x11 windows
This commit is contained in:
@@ -30,8 +30,8 @@
|
|||||||
"test:plugin:src": "lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-binary-windows.lua",
|
"test:plugin:src": "lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-binary-windows.lua",
|
||||||
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
||||||
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/mpv.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src",
|
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/mpv.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src",
|
||||||
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/setup-gate.test.ts",
|
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/setup-gate.test.ts",
|
||||||
"test:core:dist": "bun test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js",
|
"test:core:dist": "bun test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js",
|
||||||
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
||||||
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
||||||
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
|
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
|
||||||
|
|||||||
72
src/window-trackers/hyprland-tracker.test.ts
Normal file
72
src/window-trackers/hyprland-tracker.test.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
selectHyprlandMpvWindow,
|
||||||
|
type HyprlandClient,
|
||||||
|
} from './hyprland-tracker';
|
||||||
|
|
||||||
|
function makeClient(overrides: Partial<HyprlandClient> = {}): HyprlandClient {
|
||||||
|
return {
|
||||||
|
address: '0x1',
|
||||||
|
class: 'mpv',
|
||||||
|
at: [0, 0],
|
||||||
|
size: [1280, 720],
|
||||||
|
mapped: true,
|
||||||
|
hidden: false,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('selectHyprlandMpvWindow ignores hidden and unmapped mpv clients', () => {
|
||||||
|
const selected = selectHyprlandMpvWindow(
|
||||||
|
[
|
||||||
|
makeClient({
|
||||||
|
address: '0xhidden',
|
||||||
|
hidden: true,
|
||||||
|
}),
|
||||||
|
makeClient({
|
||||||
|
address: '0xunmapped',
|
||||||
|
mapped: false,
|
||||||
|
}),
|
||||||
|
makeClient({
|
||||||
|
address: '0xvisible',
|
||||||
|
at: [100, 200],
|
||||||
|
size: [1920, 1080],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
targetMpvSocketPath: null,
|
||||||
|
activeWindowAddress: null,
|
||||||
|
getWindowCommandLine: () => null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(selected?.address, '0xvisible');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selectHyprlandMpvWindow prefers active visible window among socket matches', () => {
|
||||||
|
const commandLines = new Map<string, string>([
|
||||||
|
['10', 'mpv --input-ipc-server=/tmp/subminer.sock first.mkv'],
|
||||||
|
['20', 'mpv --input-ipc-server=/tmp/subminer.sock second.mkv'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selected = selectHyprlandMpvWindow(
|
||||||
|
[
|
||||||
|
makeClient({
|
||||||
|
address: '0xfirst',
|
||||||
|
pid: 10,
|
||||||
|
}),
|
||||||
|
makeClient({
|
||||||
|
address: '0xsecond',
|
||||||
|
pid: 20,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
targetMpvSocketPath: '/tmp/subminer.sock',
|
||||||
|
activeWindowAddress: '0xsecond',
|
||||||
|
getWindowCommandLine: (pid) => commandLines.get(String(pid)) ?? null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(selected?.address, '0xsecond');
|
||||||
|
});
|
||||||
@@ -23,17 +23,77 @@ import { createLogger } from '../logger';
|
|||||||
|
|
||||||
const log = createLogger('tracker').child('hyprland');
|
const log = createLogger('tracker').child('hyprland');
|
||||||
|
|
||||||
interface HyprlandClient {
|
export interface HyprlandClient {
|
||||||
|
address?: string;
|
||||||
class: string;
|
class: string;
|
||||||
at: [number, number];
|
at: [number, number];
|
||||||
size: [number, number];
|
size: [number, number];
|
||||||
pid?: number;
|
pid?: number;
|
||||||
|
mapped?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectHyprlandMpvWindowOptions {
|
||||||
|
targetMpvSocketPath: string | null;
|
||||||
|
activeWindowAddress: string | null;
|
||||||
|
getWindowCommandLine: (pid: number) => string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesTargetSocket(commandLine: string, targetMpvSocketPath: string): boolean {
|
||||||
|
return (
|
||||||
|
commandLine.includes(`--input-ipc-server=${targetMpvSocketPath}`) ||
|
||||||
|
commandLine.includes(`--input-ipc-server ${targetMpvSocketPath}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function preferActiveHyprlandWindow(
|
||||||
|
clients: HyprlandClient[],
|
||||||
|
activeWindowAddress: string | null,
|
||||||
|
): HyprlandClient | null {
|
||||||
|
if (activeWindowAddress) {
|
||||||
|
const activeClient = clients.find((client) => client.address === activeWindowAddress);
|
||||||
|
if (activeClient) {
|
||||||
|
return activeClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clients[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectHyprlandMpvWindow(
|
||||||
|
clients: HyprlandClient[],
|
||||||
|
options: SelectHyprlandMpvWindowOptions,
|
||||||
|
): HyprlandClient | null {
|
||||||
|
const visibleMpvWindows = clients.filter(
|
||||||
|
(client) => client.class === 'mpv' && client.mapped !== false && client.hidden !== true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!options.targetMpvSocketPath) {
|
||||||
|
return preferActiveHyprlandWindow(visibleMpvWindows, options.activeWindowAddress);
|
||||||
|
}
|
||||||
|
const targetMpvSocketPath = options.targetMpvSocketPath;
|
||||||
|
|
||||||
|
const matchingWindows = visibleMpvWindows.filter((client) => {
|
||||||
|
if (!client.pid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandLine = options.getWindowCommandLine(client.pid);
|
||||||
|
if (!commandLine) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchesTargetSocket(commandLine, targetMpvSocketPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
return preferActiveHyprlandWindow(matchingWindows, options.activeWindowAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HyprlandWindowTracker extends BaseWindowTracker {
|
export class HyprlandWindowTracker extends BaseWindowTracker {
|
||||||
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
private eventSocket: net.Socket | null = null;
|
private eventSocket: net.Socket | null = null;
|
||||||
private readonly targetMpvSocketPath: string | null;
|
private readonly targetMpvSocketPath: string | null;
|
||||||
|
private activeWindowAddress: string | null = null;
|
||||||
|
|
||||||
constructor(targetMpvSocketPath?: string) {
|
constructor(targetMpvSocketPath?: string) {
|
||||||
super();
|
super();
|
||||||
@@ -75,15 +135,7 @@ export class HyprlandWindowTracker extends BaseWindowTracker {
|
|||||||
this.eventSocket.on('data', (data: Buffer) => {
|
this.eventSocket.on('data', (data: Buffer) => {
|
||||||
const events = data.toString().split('\n');
|
const events = data.toString().split('\n');
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
if (
|
this.handleSocketEvent(event);
|
||||||
event.includes('movewindow') ||
|
|
||||||
event.includes('windowtitle') ||
|
|
||||||
event.includes('openwindow') ||
|
|
||||||
event.includes('closewindow') ||
|
|
||||||
event.includes('fullscreen')
|
|
||||||
) {
|
|
||||||
this.pollGeometry();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,6 +150,39 @@ export class HyprlandWindowTracker extends BaseWindowTracker {
|
|||||||
this.eventSocket.connect(socketPath);
|
this.eventSocket.connect(socketPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleSocketEvent(event: string): void {
|
||||||
|
const trimmedEvent = event.trim();
|
||||||
|
if (!trimmedEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [name, rawData = ''] = trimmedEvent.split('>>', 2);
|
||||||
|
const data = rawData.trim();
|
||||||
|
|
||||||
|
if (name === 'activewindowv2') {
|
||||||
|
this.activeWindowAddress = data || null;
|
||||||
|
this.pollGeometry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'closewindow' && data === this.activeWindowAddress) {
|
||||||
|
this.activeWindowAddress = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
name === 'movewindow' ||
|
||||||
|
name === 'movewindowv2' ||
|
||||||
|
name === 'windowtitle' ||
|
||||||
|
name === 'windowtitlev2' ||
|
||||||
|
name === 'openwindow' ||
|
||||||
|
name === 'closewindow' ||
|
||||||
|
name === 'fullscreen' ||
|
||||||
|
name === 'changefloatingmode'
|
||||||
|
) {
|
||||||
|
this.pollGeometry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private pollGeometry(): void {
|
private pollGeometry(): void {
|
||||||
try {
|
try {
|
||||||
const output = execSync('hyprctl clients -j', { encoding: 'utf-8' });
|
const output = execSync('hyprctl clients -j', { encoding: 'utf-8' });
|
||||||
@@ -120,30 +205,11 @@ export class HyprlandWindowTracker extends BaseWindowTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private findTargetWindow(clients: HyprlandClient[]): HyprlandClient | null {
|
private findTargetWindow(clients: HyprlandClient[]): HyprlandClient | null {
|
||||||
const mpvWindows = clients.filter((client) => client.class === 'mpv');
|
return selectHyprlandMpvWindow(clients, {
|
||||||
if (!this.targetMpvSocketPath) {
|
targetMpvSocketPath: this.targetMpvSocketPath,
|
||||||
return mpvWindows[0] || null;
|
activeWindowAddress: this.activeWindowAddress,
|
||||||
}
|
getWindowCommandLine: (pid) => this.getWindowCommandLine(pid),
|
||||||
|
});
|
||||||
for (const mpvWindow of mpvWindows) {
|
|
||||||
if (!mpvWindow.pid) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const commandLine = this.getWindowCommandLine(mpvWindow.pid);
|
|
||||||
if (!commandLine) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
commandLine.includes(`--input-ipc-server=${this.targetMpvSocketPath}`) ||
|
|
||||||
commandLine.includes(`--input-ipc-server ${this.targetMpvSocketPath}`)
|
|
||||||
) {
|
|
||||||
return mpvWindow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getWindowCommandLine(pid: number): string | null {
|
private getWindowCommandLine(pid: number): string | null {
|
||||||
|
|||||||
@@ -18,11 +18,51 @@ Height: 720
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parseX11WindowGeometry preserves negative coordinates', () => {
|
||||||
|
const geometry = parseX11WindowGeometry(`
|
||||||
|
Absolute upper-left X: -1920
|
||||||
|
Absolute upper-left Y: -24
|
||||||
|
Width: 1920
|
||||||
|
Height: 1080
|
||||||
|
`);
|
||||||
|
assert.deepEqual(geometry, {
|
||||||
|
x: -1920,
|
||||||
|
y: -24,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('parseX11WindowPid parses xprop output', () => {
|
test('parseX11WindowPid parses xprop output', () => {
|
||||||
assert.equal(parseX11WindowPid('_NET_WM_PID(CARDINAL) = 4242'), 4242);
|
assert.equal(parseX11WindowPid('_NET_WM_PID(CARDINAL) = 4242'), 4242);
|
||||||
assert.equal(parseX11WindowPid('_NET_WM_PID(CARDINAL) = not-a-number'), null);
|
assert.equal(parseX11WindowPid('_NET_WM_PID(CARDINAL) = not-a-number'), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('X11WindowTracker searches only visible mpv windows', async () => {
|
||||||
|
const commands: Array<{ command: string; args: string[] }> = [];
|
||||||
|
const tracker = new X11WindowTracker(undefined, async (command, args) => {
|
||||||
|
commands.push({ command, args });
|
||||||
|
if (command === 'xdotool') {
|
||||||
|
return '123';
|
||||||
|
}
|
||||||
|
if (command === 'xwininfo') {
|
||||||
|
return `Absolute upper-left X: 0
|
||||||
|
Absolute upper-left Y: 0
|
||||||
|
Width: 640
|
||||||
|
Height: 360`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
assert.deepEqual(commands[0], {
|
||||||
|
command: 'xdotool',
|
||||||
|
args: ['search', '--onlyvisible', '--class', 'mpv'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('X11WindowTracker skips overlapping polls while one command is in flight', async () => {
|
test('X11WindowTracker skips overlapping polls while one command is in flight', async () => {
|
||||||
let commandCalls = 0;
|
let commandCalls = 0;
|
||||||
let release: (() => void) | undefined;
|
let release: (() => void) | undefined;
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ export function parseX11WindowGeometry(winInfo: string): {
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
} | null {
|
} | null {
|
||||||
const xMatch = winInfo.match(/Absolute upper-left X:\s*(\d+)/);
|
const xMatch = winInfo.match(/Absolute upper-left X:\s*(-?\d+)/);
|
||||||
const yMatch = winInfo.match(/Absolute upper-left Y:\s*(\d+)/);
|
const yMatch = winInfo.match(/Absolute upper-left Y:\s*(-?\d+)/);
|
||||||
const widthMatch = winInfo.match(/Width:\s*(\d+)/);
|
const widthMatch = winInfo.match(/Width:\s*(\d+)/);
|
||||||
const heightMatch = winInfo.match(/Height:\s*(\d+)/);
|
const heightMatch = winInfo.match(/Height:\s*(\d+)/);
|
||||||
if (!xMatch || !yMatch || !widthMatch || !heightMatch) {
|
if (!xMatch || !yMatch || !widthMatch || !heightMatch) {
|
||||||
@@ -112,7 +112,12 @@ export class X11WindowTracker extends BaseWindowTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async pollGeometryAsync(): Promise<void> {
|
private async pollGeometryAsync(): Promise<void> {
|
||||||
const windowIdsOutput = await this.runCommand('xdotool', ['search', '--class', 'mpv']);
|
const windowIdsOutput = await this.runCommand('xdotool', [
|
||||||
|
'search',
|
||||||
|
'--onlyvisible',
|
||||||
|
'--class',
|
||||||
|
'mpv',
|
||||||
|
]);
|
||||||
const windowIds = windowIdsOutput.trim();
|
const windowIds = windowIdsOutput.trim();
|
||||||
if (!windowIds) {
|
if (!windowIds) {
|
||||||
this.updateGeometry(null);
|
this.updateGeometry(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user