mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-13 01:55:50 -07:00
fix(overlay): handle X11 display scaling across monitors (#193)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Fixed X11/XWayland overlays being oversized and offset from mpv under fractional or mixed-monitor display scaling.
|
||||
@@ -405,8 +405,9 @@ On any Wayland session that is not Hyprland or Sway (KDE Plasma, GNOME, and othe
|
||||
|
||||
SubMiner handles this automatically:
|
||||
|
||||
- It launches its own window under XWayland (it sets `--ozone-platform-hint=x11`).
|
||||
- It launches its own window under XWayland (it sets `--ozone-platform=x11`).
|
||||
- Every mpv it launches (via the `subminer` launcher, Jellyfin, or YouTube) is pinned to XWayland too - Wayland environment hints are stripped and an X11 GPU context (`--gpu-context=x11vk,x11egl,x11`) is applied. Only the window context is overridden; your `vo`/`gpu-api` and user shaders are left alone.
|
||||
- Fractional and mixed-monitor display scaling is handled per screen when SubMiner maps XWayland mpv coordinates to the overlay.
|
||||
- While mpv is windowed, the overlay is a managed X11 window owned by the tracked mpv window (`WM_TRANSIENT_FOR`), so it stays above mpv while other foreground X11/Xwayland apps can still cover both windows.
|
||||
- While tracked mpv is fullscreen, SubMiner swaps the visible overlay to a focusable-false X11 override-redirect window. That path can stay above the active fullscreen mpv window without requiring a KDE/KWin-specific rule, and SubMiner hides/releases it when mpv is no longer the active X11/Xwayland window.
|
||||
- The visible overlay is shown inactive on Linux, so normal hover should not steal keyboard focus from mpv.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { shouldForceX11ElectronBackend } from './electron-backend';
|
||||
import { resolveX11ElectronRelaunchArgs, shouldForceX11ElectronBackend } from './electron-backend';
|
||||
|
||||
function withPlatform(platform: NodeJS.Platform, run: () => void): void {
|
||||
const original = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
@@ -32,3 +32,70 @@ test('shouldForceX11ElectronBackend is false off Linux', () => {
|
||||
assert.equal(shouldForceX11ElectronBackend({}), false);
|
||||
});
|
||||
});
|
||||
|
||||
test('resolveX11ElectronRelaunchArgs adds the raw X11 Ozone argument on unsupported Linux', () => {
|
||||
assert.deepEqual(
|
||||
resolveX11ElectronRelaunchArgs(
|
||||
['--start'],
|
||||
{
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_CURRENT_DESKTOP: 'KDE',
|
||||
},
|
||||
'linux',
|
||||
),
|
||||
['--start', '--ozone-platform=x11'],
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveX11ElectronRelaunchArgs avoids loops and preserves native Wayland backends', () => {
|
||||
const kdeWayland = {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_CURRENT_DESKTOP: 'KDE',
|
||||
};
|
||||
assert.equal(
|
||||
resolveX11ElectronRelaunchArgs(['--start', '--ozone-platform=x11'], kdeWayland, 'linux'),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
resolveX11ElectronRelaunchArgs(
|
||||
['--start'],
|
||||
{ ...kdeWayland, HYPRLAND_INSTANCE_SIGNATURE: 'hypr' },
|
||||
'linux',
|
||||
),
|
||||
null,
|
||||
);
|
||||
assert.equal(resolveX11ElectronRelaunchArgs(['--start'], kdeWayland, 'darwin'), null);
|
||||
assert.equal(
|
||||
resolveX11ElectronRelaunchArgs(
|
||||
[],
|
||||
{
|
||||
...kdeWayland,
|
||||
SUBMINER_APP_ARGC: '1',
|
||||
SUBMINER_APP_ARG_0: '--start',
|
||||
},
|
||||
'linux',
|
||||
)?.at(-1),
|
||||
'--ozone-platform=x11',
|
||||
);
|
||||
assert.equal(
|
||||
resolveX11ElectronRelaunchArgs([], { ...kdeWayland, SUBMINER_X11_BOOTSTRAPPED: '1' }, 'linux'),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveX11ElectronRelaunchArgs replaces an explicit unsupported Wayland argument', () => {
|
||||
assert.deepEqual(
|
||||
resolveX11ElectronRelaunchArgs(
|
||||
['--start', '--ozone-platform', 'wayland'],
|
||||
{
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_CURRENT_DESKTOP: 'KDE',
|
||||
},
|
||||
'linux',
|
||||
),
|
||||
['--start', '--ozone-platform=x11'],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,9 @@ import { isSupportedWaylandCompositor } from '../../shared/mpv-x11-backend';
|
||||
|
||||
const logger = createLogger('core:electron-backend');
|
||||
|
||||
export const X11_ELECTRON_BOOTSTRAP_ENV = 'SUBMINER_X11_BOOTSTRAPPED';
|
||||
const X11_ELECTRON_OZONE_ARG = '--ozone-platform=x11';
|
||||
|
||||
function getElectronOzonePlatformHint(env: NodeJS.ProcessEnv = process.env): string | null {
|
||||
const hint = env.ELECTRON_OZONE_PLATFORM_HINT?.trim().toLowerCase();
|
||||
if (hint) return hint;
|
||||
@@ -24,11 +27,42 @@ function getElectronOzonePlatformHint(env: NodeJS.ProcessEnv = process.env): str
|
||||
* Electron Wayland backend is unsupported); the Hyprland/Sway case is left untouched so
|
||||
* {@link enforceUnsupportedWaylandMode} can report it.
|
||||
*/
|
||||
export function shouldForceX11ElectronBackend(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
if (process.platform !== 'linux') return false;
|
||||
export function shouldForceX11ElectronBackend(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): boolean {
|
||||
if (platform !== 'linux') return false;
|
||||
return !isSupportedWaylandCompositor(env);
|
||||
}
|
||||
|
||||
export function resolveX11ElectronRelaunchArgs(
|
||||
args: string[],
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): string[] | null {
|
||||
if (!shouldForceX11ElectronBackend(env, platform)) return null;
|
||||
if (env[X11_ELECTRON_BOOTSTRAP_ENV] === '1') return null;
|
||||
|
||||
const retainedArgs: string[] = [];
|
||||
let alreadyForced = false;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === '--ozone-platform') {
|
||||
const value = args[index + 1];
|
||||
alreadyForced = value?.trim().toLowerCase() === 'x11';
|
||||
if (value && !value.startsWith('--')) index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg?.startsWith('--ozone-platform=')) {
|
||||
alreadyForced = arg.slice('--ozone-platform='.length).trim().toLowerCase() === 'x11';
|
||||
continue;
|
||||
}
|
||||
if (arg) retainedArgs.push(arg);
|
||||
}
|
||||
|
||||
return alreadyForced ? null : [...retainedArgs, X11_ELECTRON_OZONE_ARG];
|
||||
}
|
||||
|
||||
export function forceX11Backend(args: CliArgs): void {
|
||||
if (!shouldStartApp(args)) return;
|
||||
if (!shouldForceX11ElectronBackend()) return;
|
||||
|
||||
@@ -25,8 +25,24 @@ import {
|
||||
applyBackgroundBootstrapCommandLineSwitches,
|
||||
applyEarlyLinuxCommandLineSwitches,
|
||||
resolveLinuxPasswordStoreValue,
|
||||
spawnDetachedApp,
|
||||
} from './main-entry-runtime';
|
||||
|
||||
test('detached app launch policy stays in the startup runtime utilities', () => {
|
||||
const entrySource = fs.readFileSync(path.join(process.cwd(), 'src/main-entry.ts'), 'utf8');
|
||||
const runtimeSource = fs.readFileSync(
|
||||
path.join(process.cwd(), 'src/main-entry-runtime.ts'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
assert.equal(typeof spawnDetachedApp, 'function');
|
||||
assert.doesNotMatch(entrySource, /function spawnDetachedApp/);
|
||||
assert.match(
|
||||
runtimeSource,
|
||||
/child\.once\('error', \(error\) => \{\s*console\.error\([^;]*error\);\s*\}\);\s*child\.unref\(\)/,
|
||||
);
|
||||
});
|
||||
|
||||
test('background bootstrap exits through Electron so Chromium children shut down', () => {
|
||||
const exitCodes: number[] = [];
|
||||
exitBackgroundBootstrap({ exit: (code) => exitCodes.push(code) });
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { CliArgs, hasExplicitCommand, parseArgs, shouldStartApp } from './cli/args';
|
||||
import { resolveConfigDir } from './config/path-resolution';
|
||||
import { resolveAppImageMountKeepaliveInvocation } from './main/appimage-mount-keepalive';
|
||||
|
||||
const BACKGROUND_ARG = '--background';
|
||||
const START_ARG = '--start';
|
||||
@@ -265,6 +267,25 @@ export function exitBackgroundBootstrap(app: BackgroundBootstrapAppLike): void {
|
||||
app.exit(0);
|
||||
}
|
||||
|
||||
export function spawnDetachedApp(childArgs: string[], env: NodeJS.ProcessEnv): void {
|
||||
const keepalive = resolveAppImageMountKeepaliveInvocation(env);
|
||||
const child = keepalive
|
||||
? spawn(keepalive.command, [...keepalive.args, ...childArgs], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env,
|
||||
})
|
||||
: spawn(process.execPath, childArgs, {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env,
|
||||
});
|
||||
child.once('error', (error) => {
|
||||
console.error('Failed to spawn detached SubMiner app:', error);
|
||||
});
|
||||
child.unref();
|
||||
}
|
||||
|
||||
export function shouldHandleHelpOnlyAtEntry(argv: string[], env: NodeJS.ProcessEnv): boolean {
|
||||
if (env.ELECTRON_RUN_AS_NODE === '1') return false;
|
||||
const args = parseCliArgs(argv);
|
||||
|
||||
+19
-16
@@ -1,5 +1,4 @@
|
||||
import os from 'node:os';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { app, dialog, shell } from 'electron';
|
||||
import { printHelp } from './cli/help';
|
||||
import {
|
||||
@@ -20,9 +19,9 @@ import {
|
||||
shouldHandleHelpOnlyAtEntry,
|
||||
shouldHandleLaunchMpvAtEntry,
|
||||
shouldHandleStatsDaemonCommandAtEntry,
|
||||
spawnDetachedApp,
|
||||
} from './main-entry-runtime';
|
||||
import { requestSingleInstanceLockEarly } from './main/early-single-instance';
|
||||
import { resolveAppImageMountKeepaliveInvocation } from './main/appimage-mount-keepalive';
|
||||
import { readConfiguredWindowsMpvLaunch } from './main-entry-launch-config';
|
||||
import { isAppControlServerAvailable, sendAppControlCommand } from './shared/app-control-client';
|
||||
import {
|
||||
@@ -44,6 +43,10 @@ import {
|
||||
resolveDefaultLogFilePath,
|
||||
type LogRotation,
|
||||
} from './shared/log-files';
|
||||
import {
|
||||
resolveX11ElectronRelaunchArgs,
|
||||
X11_ELECTRON_BOOTSTRAP_ENV,
|
||||
} from './core/utils/electron-backend';
|
||||
|
||||
const DEFAULT_TEXTHOOKER_PORT = 5174;
|
||||
|
||||
@@ -296,26 +299,26 @@ async function runEntryProcess(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const childArgs = hasTransportedStartupArgs(process.env) ? [] : process.argv.slice(1);
|
||||
const x11ChildArgs = resolveX11ElectronRelaunchArgs(childArgs, process.env);
|
||||
|
||||
if (shouldDetachBackgroundLaunch(process.argv, process.env)) {
|
||||
const childArgs = hasTransportedStartupArgs(process.env) ? [] : process.argv.slice(1);
|
||||
const keepalive = resolveAppImageMountKeepaliveInvocation(process.env);
|
||||
const child = keepalive
|
||||
? spawn(keepalive.command, [...keepalive.args, ...childArgs], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env: sanitizeBackgroundEnv(process.env),
|
||||
})
|
||||
: spawn(process.execPath, childArgs, {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env: sanitizeBackgroundEnv(process.env),
|
||||
});
|
||||
child.unref();
|
||||
const childEnv = sanitizeBackgroundEnv(process.env);
|
||||
if (x11ChildArgs) childEnv[X11_ELECTRON_BOOTSTRAP_ENV] = '1';
|
||||
spawnDetachedApp(x11ChildArgs ?? childArgs, childEnv);
|
||||
// Let Electron stop bootstrap Chromium children before its AppImage mount is released.
|
||||
exitBackgroundBootstrap(app);
|
||||
return;
|
||||
}
|
||||
|
||||
if (x11ChildArgs) {
|
||||
const childEnv = sanitizeStartupEnv(process.env);
|
||||
childEnv[X11_ELECTRON_BOOTSTRAP_ENV] = '1';
|
||||
spawnDetachedApp(x11ChildArgs, childEnv);
|
||||
exitBackgroundBootstrap(app);
|
||||
return;
|
||||
}
|
||||
|
||||
startMainProcess();
|
||||
}
|
||||
|
||||
|
||||
-10
@@ -256,7 +256,6 @@ import {
|
||||
import {
|
||||
enforceUnsupportedWaylandMode,
|
||||
forceX11Backend,
|
||||
shouldForceX11ElectronBackend,
|
||||
generateDefaultConfigFile,
|
||||
resolveConfiguredShortcuts,
|
||||
resolveKeybindings,
|
||||
@@ -597,15 +596,6 @@ if (process.platform === 'linux') {
|
||||
);
|
||||
app.commandLine.appendSwitch('password-store', passwordStore);
|
||||
createLogger('main').debug(`Applied --password-store ${passwordStore}`);
|
||||
// Pin the overlay to XWayland on unsupported Wayland sessions (everything except
|
||||
// Hyprland/Sway). `setAlwaysOnTop`/`moveTop` are no-ops under a native Wayland surface,
|
||||
// so the overlay can only stay above mpv under X11/XWayland. The command-line switch is
|
||||
// applied at module load (before app init) so it reliably wins over the late env-var
|
||||
// fallback in forceX11Backend().
|
||||
if (shouldForceX11ElectronBackend(process.env)) {
|
||||
app.commandLine.appendSwitch('ozone-platform-hint', 'x11');
|
||||
createLogger('main').debug('Forced ozone-platform-hint=x11 for XWayland overlay stacking');
|
||||
}
|
||||
}
|
||||
|
||||
app.setName('SubMiner');
|
||||
|
||||
@@ -47,6 +47,29 @@ WINDOW=44040194
|
||||
assert.deepEqual(reader.getCursorScreenPoint({ x: 877, y: 718 }), { x: 1700, y: 1050 });
|
||||
});
|
||||
|
||||
test('createLinuxX11CursorPointReader converts physical X11 coordinates to Electron DIP', async () => {
|
||||
const convertedPoints: Array<{ x: number; y: number }> = [];
|
||||
const reader = createLinuxX11CursorPointReader({
|
||||
env: { DISPLAY: ':1' },
|
||||
platform: 'linux',
|
||||
runCommand: async () => `X=1424
|
||||
Y=697
|
||||
SCREEN=0
|
||||
WINDOW=44040194
|
||||
`,
|
||||
screenToDipPoint: (point) => {
|
||||
convertedPoints.push(point);
|
||||
return { x: 1139, y: 557 };
|
||||
},
|
||||
});
|
||||
|
||||
reader.refresh();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.deepEqual(reader.getCursorScreenPoint({ x: 0, y: 0 }), { x: 1139, y: 557 });
|
||||
assert.deepEqual(convertedPoints, [{ x: 1424, y: 697 }]);
|
||||
});
|
||||
|
||||
test('createLinuxX11CursorPointReader does not spawn off X11 Linux', () => {
|
||||
const calls: string[] = [];
|
||||
const reader = createLinuxX11CursorPointReader({
|
||||
|
||||
@@ -36,11 +36,13 @@ export function createLinuxX11CursorPointReader(options?: {
|
||||
now?: () => number;
|
||||
platform?: NodeJS.Platform;
|
||||
runCommand?: CommandRunner;
|
||||
screenToDipPoint?: (point: PointerPoint) => PointerPoint;
|
||||
}) {
|
||||
const env = options?.env ?? process.env;
|
||||
const now = options?.now ?? (() => Date.now());
|
||||
const platform = options?.platform ?? process.platform;
|
||||
const runCommand = options?.runCommand ?? execFileUtf8;
|
||||
const screenToDipPoint = options?.screenToDipPoint ?? ((point: PointerPoint) => point);
|
||||
let latest: { point: PointerPoint; updatedAtMs: number } | null = null;
|
||||
let inFlight = false;
|
||||
let retryAfterMs = 0;
|
||||
@@ -63,7 +65,7 @@ export function createLinuxX11CursorPointReader(options?: {
|
||||
retryAfterMs = now() + COMMAND_FAILURE_RETRY_DELAY_MS;
|
||||
return;
|
||||
}
|
||||
latest = { point, updatedAtMs: now() };
|
||||
latest = { point: screenToDipPoint(point), updatedAtMs: now() };
|
||||
retryAfterMs = 0;
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -289,7 +289,9 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter
|
||||
if (initialArgs && isHeadlessInitialCommand(initialArgs)) {
|
||||
return null;
|
||||
}
|
||||
return createWindowTrackerCore(override, targetMpvSocketPath);
|
||||
return createWindowTrackerCore(override, targetMpvSocketPath, (point) =>
|
||||
screen.screenToDipPoint(point),
|
||||
);
|
||||
}
|
||||
|
||||
function bindVisibleOverlayOwner(): void {
|
||||
@@ -627,7 +629,9 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter
|
||||
|
||||
ensureWindowsVisibleOverlayForegroundPollLoop();
|
||||
|
||||
const linuxX11CursorPointReader = createLinuxX11CursorPointReader();
|
||||
const linuxX11CursorPointReader = createLinuxX11CursorPointReader({
|
||||
screenToDipPoint: (point) => screen.screenToDipPoint(point),
|
||||
});
|
||||
|
||||
function getLinuxOverlayPointerMeasurement() {
|
||||
const measurement = overlayContentMeasurementStore.getLatestByLayer('visible');
|
||||
|
||||
@@ -20,6 +20,7 @@ import { BaseWindowTracker } from './base-tracker';
|
||||
import { HyprlandWindowTracker } from './hyprland-tracker';
|
||||
import { SwayWindowTracker } from './sway-tracker';
|
||||
import { X11WindowTracker } from './x11-tracker';
|
||||
import type { ScreenToDipPoint } from './x11-tracker';
|
||||
import { MacOSWindowTracker } from './macos-tracker';
|
||||
import { WindowsWindowTracker } from './windows-tracker';
|
||||
import { createLogger } from '../logger';
|
||||
@@ -51,6 +52,7 @@ function normalizeCompositor(value: string): Compositor | null {
|
||||
export function createWindowTracker(
|
||||
override?: string | null,
|
||||
targetMpvSocketPath?: string | null,
|
||||
screenToDipPoint?: ScreenToDipPoint,
|
||||
): BaseWindowTracker | null {
|
||||
let compositor = detectCompositor();
|
||||
|
||||
@@ -70,7 +72,11 @@ export function createWindowTracker(
|
||||
case 'sway':
|
||||
return new SwayWindowTracker(targetMpvSocketPath?.trim() || undefined);
|
||||
case 'x11':
|
||||
return new X11WindowTracker(targetMpvSocketPath?.trim() || undefined);
|
||||
return new X11WindowTracker(
|
||||
targetMpvSocketPath?.trim() || undefined,
|
||||
undefined,
|
||||
screenToDipPoint,
|
||||
);
|
||||
case 'macos':
|
||||
return new MacOSWindowTracker(targetMpvSocketPath?.trim() || undefined);
|
||||
case 'windows':
|
||||
|
||||
@@ -82,6 +82,47 @@ Height: 360`;
|
||||
});
|
||||
});
|
||||
|
||||
test('X11WindowTracker converts both physical rectangle corners to Electron DIP', async () => {
|
||||
const convertedPoints: Array<{ x: number; y: number }> = [];
|
||||
const tracker = new X11WindowTracker(
|
||||
undefined,
|
||||
async (command, args) => {
|
||||
if (command === 'xdotool' && args[0] === 'search') {
|
||||
return '123';
|
||||
}
|
||||
if (command === 'xdotool' && args[0] === 'getactivewindow') {
|
||||
return '123';
|
||||
}
|
||||
if (command === 'xwininfo') {
|
||||
return `Absolute upper-left X: 2000
|
||||
Absolute upper-left Y: 125
|
||||
Width: 1000
|
||||
Height: 750`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
(point) => {
|
||||
convertedPoints.push(point);
|
||||
if (point.x === 2000) return { x: 1600, y: 100 };
|
||||
return { x: 2400, y: 700 };
|
||||
},
|
||||
);
|
||||
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
assert.deepEqual(convertedPoints, [
|
||||
{ x: 2000, y: 125 },
|
||||
{ x: 3000, y: 875 },
|
||||
]);
|
||||
assert.deepEqual(tracker.getGeometry(), {
|
||||
x: 1600,
|
||||
y: 100,
|
||||
width: 800,
|
||||
height: 600,
|
||||
});
|
||||
});
|
||||
|
||||
test('X11WindowTracker updates target focus from active X11 window', async () => {
|
||||
let activeWindowId = '999';
|
||||
const tracker = new X11WindowTracker(undefined, async (command, args) => {
|
||||
|
||||
@@ -20,6 +20,9 @@ import { execFile } from 'child_process';
|
||||
import { BaseWindowTracker } from './base-tracker';
|
||||
|
||||
type CommandRunner = (command: string, args: string[]) => Promise<string>;
|
||||
export type ScreenToDipPoint = (point: { x: number; y: number }) => { x: number; y: number };
|
||||
|
||||
const preservePoint: ScreenToDipPoint = (point) => point;
|
||||
|
||||
function execFileUtf8(command: string, args: string[]): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -87,16 +90,22 @@ export class X11WindowTracker extends BaseWindowTracker {
|
||||
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private readonly targetMpvSocketPath: string | null;
|
||||
private readonly runCommand: CommandRunner;
|
||||
private readonly screenToDipPoint: ScreenToDipPoint;
|
||||
private targetWindowId: string | null = null;
|
||||
private targetWindowPid: number | null = null;
|
||||
private pollInFlight = false;
|
||||
private currentPollIntervalMs = 750;
|
||||
private readonly stablePollIntervalMs = 250;
|
||||
|
||||
constructor(targetMpvSocketPath?: string, runCommand: CommandRunner = execFileUtf8) {
|
||||
constructor(
|
||||
targetMpvSocketPath?: string,
|
||||
runCommand: CommandRunner = execFileUtf8,
|
||||
screenToDipPoint: ScreenToDipPoint = preservePoint,
|
||||
) {
|
||||
super();
|
||||
this.targetMpvSocketPath = targetMpvSocketPath?.trim() || null;
|
||||
this.runCommand = runCommand;
|
||||
this.screenToDipPoint = screenToDipPoint;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
@@ -196,11 +205,25 @@ export class X11WindowTracker extends BaseWindowTracker {
|
||||
this.targetWindowPid = targetPid;
|
||||
|
||||
const winInfo = await this.runCommand('xwininfo', ['-id', windowId]);
|
||||
const geometry = parseX11WindowGeometry(winInfo);
|
||||
if (!geometry) {
|
||||
const physicalGeometry = parseX11WindowGeometry(winInfo);
|
||||
if (!physicalGeometry) {
|
||||
this.updateGeometry(null);
|
||||
return;
|
||||
}
|
||||
const topLeft = this.screenToDipPoint({
|
||||
x: physicalGeometry.x,
|
||||
y: physicalGeometry.y,
|
||||
});
|
||||
const bottomRight = this.screenToDipPoint({
|
||||
x: physicalGeometry.x + physicalGeometry.width,
|
||||
y: physicalGeometry.y + physicalGeometry.height,
|
||||
});
|
||||
const geometry = {
|
||||
x: topLeft.x,
|
||||
y: topLeft.y,
|
||||
width: bottomRight.x - topLeft.x,
|
||||
height: bottomRight.y - topLeft.y,
|
||||
};
|
||||
|
||||
const focused = await this.isWindowActive(windowId, targetPid);
|
||||
this.updateGeometry(geometry, focused);
|
||||
|
||||
Reference in New Issue
Block a user