mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-10 03:13:32 -07:00
fix(overlay): Linux X11/XWayland stacking, stale pause state, multi-copy selector (#101)
This commit is contained in:
@@ -39,6 +39,7 @@ export const IPC_CHANNELS = {
|
||||
refreshKnownWords: 'anki:refresh-known-words',
|
||||
kikuFieldGroupingRespond: 'kiku:field-grouping-respond',
|
||||
reportOverlayContentBounds: 'overlay-content-bounds:report',
|
||||
reportOverlayInteractive: 'overlay-interactive:report',
|
||||
overlayModalOpened: 'overlay:modal-opened',
|
||||
toggleStatsOverlay: 'stats:toggle-overlay',
|
||||
markActiveVideoWatched: 'immersion:mark-active-video-watched',
|
||||
@@ -50,6 +51,7 @@ export const IPC_CHANNELS = {
|
||||
getCurrentSubtitleRaw: 'get-current-subtitle-raw',
|
||||
getCurrentSubtitleAss: 'get-current-subtitle-ass',
|
||||
getSubtitleSidebarSnapshot: 'get-subtitle-sidebar-snapshot',
|
||||
getSubtitleSidebarOpen: 'get-subtitle-sidebar-open',
|
||||
getPlaybackPaused: 'get-playback-paused',
|
||||
getSubtitlePosition: 'get-subtitle-position',
|
||||
getSubtitleStyle: 'get-subtitle-style',
|
||||
@@ -64,6 +66,7 @@ export const IPC_CHANNELS = {
|
||||
getCurrentSecondarySub: 'get-current-secondary-sub',
|
||||
youtubePickerResolve: 'youtube:picker-resolve',
|
||||
focusMainWindow: 'focus-main-window',
|
||||
activatePlaybackWindowForOverlayInteraction: 'overlay:activate-playback-window',
|
||||
runSubsyncManual: 'subsync:run-manual',
|
||||
getAnkiConnectStatus: 'get-anki-connect-status',
|
||||
getRuntimeOptions: 'runtime-options:get',
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
MPV_X11_BACKEND_ARGS,
|
||||
applyX11EnvOverrides,
|
||||
isSupportedWaylandCompositor,
|
||||
shouldForceX11MpvBackend,
|
||||
shouldForceX11WaylandSession,
|
||||
} from './mpv-x11-backend';
|
||||
|
||||
function withPlatform(platform: NodeJS.Platform, run: () => void): void {
|
||||
const original = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: platform });
|
||||
try {
|
||||
run();
|
||||
} finally {
|
||||
if (original) Object.defineProperty(process, 'platform', original);
|
||||
}
|
||||
}
|
||||
|
||||
const KDE_WAYLAND = {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
XDG_CURRENT_DESKTOP: 'KDE',
|
||||
XDG_SESSION_DESKTOP: 'plasma',
|
||||
};
|
||||
|
||||
test('isSupportedWaylandCompositor detects Hyprland and Sway via env or xdg desktop', () => {
|
||||
assert.equal(isSupportedWaylandCompositor({ HYPRLAND_INSTANCE_SIGNATURE: 'hypr' }), true);
|
||||
assert.equal(isSupportedWaylandCompositor({ SWAYSOCK: '/tmp/sway.sock' }), true);
|
||||
assert.equal(isSupportedWaylandCompositor({ XDG_CURRENT_DESKTOP: 'Hyprland' }), true);
|
||||
assert.equal(isSupportedWaylandCompositor({ XDG_SESSION_DESKTOP: 'sway' }), true);
|
||||
assert.equal(isSupportedWaylandCompositor(KDE_WAYLAND), false);
|
||||
});
|
||||
|
||||
test('shouldForceX11WaylandSession forces X11 for unsupported Wayland sessions only', () => {
|
||||
withPlatform('linux', () => {
|
||||
assert.equal(shouldForceX11WaylandSession(KDE_WAYLAND), true);
|
||||
// GNOME Wayland (also unsupported) → forced.
|
||||
assert.equal(
|
||||
shouldForceX11WaylandSession({
|
||||
DISPLAY: ':0',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_CURRENT_DESKTOP: 'GNOME',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
// Hyprland keeps native Wayland.
|
||||
assert.equal(
|
||||
shouldForceX11WaylandSession({ ...KDE_WAYLAND, HYPRLAND_INSTANCE_SIGNATURE: 'hypr' }),
|
||||
false,
|
||||
);
|
||||
// No X11 display to fall back to.
|
||||
assert.equal(shouldForceX11WaylandSession({ WAYLAND_DISPLAY: 'wayland-0' }), false);
|
||||
// Pure X11 session (no Wayland) → nothing to force.
|
||||
assert.equal(shouldForceX11WaylandSession({ DISPLAY: ':0', XDG_SESSION_TYPE: 'x11' }), false);
|
||||
});
|
||||
});
|
||||
|
||||
test('shouldForceX11WaylandSession is false off Linux', () => {
|
||||
withPlatform('darwin', () => {
|
||||
assert.equal(shouldForceX11WaylandSession(KDE_WAYLAND), false);
|
||||
});
|
||||
withPlatform('win32', () => {
|
||||
assert.equal(shouldForceX11WaylandSession(KDE_WAYLAND), false);
|
||||
});
|
||||
});
|
||||
|
||||
test('shouldForceX11MpvBackend honors explicit x11 and auto modes', () => {
|
||||
withPlatform('linux', () => {
|
||||
// Explicit x11 forces even without Wayland.
|
||||
assert.equal(shouldForceX11MpvBackend('x11', { DISPLAY: ':0' }), true);
|
||||
// Auto defers to the session check.
|
||||
assert.equal(shouldForceX11MpvBackend('auto', KDE_WAYLAND), true);
|
||||
assert.equal(
|
||||
shouldForceX11MpvBackend('auto', { ...KDE_WAYLAND, SWAYSOCK: '/tmp/sway.sock' }),
|
||||
false,
|
||||
);
|
||||
// No display at all.
|
||||
assert.equal(shouldForceX11MpvBackend('x11', {}), false);
|
||||
});
|
||||
});
|
||||
|
||||
test('applyX11EnvOverrides strips Wayland hints and pins session type to x11', () => {
|
||||
const env = {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
HYPRLAND_INSTANCE_SIGNATURE: 'hypr',
|
||||
SWAYSOCK: '/tmp/sway.sock',
|
||||
};
|
||||
const result = applyX11EnvOverrides(env);
|
||||
assert.equal(result, env); // mutates in place
|
||||
assert.equal(result.DISPLAY, ':1');
|
||||
assert.equal(result.WAYLAND_DISPLAY, undefined);
|
||||
assert.equal(result.HYPRLAND_INSTANCE_SIGNATURE, undefined);
|
||||
assert.equal(result.SWAYSOCK, undefined);
|
||||
assert.equal(result.XDG_SESSION_TYPE, 'x11');
|
||||
});
|
||||
|
||||
test('MPV_X11_BACKEND_ARGS pins the GPU stack to X11', () => {
|
||||
assert.deepEqual(
|
||||
[...MPV_X11_BACKEND_ARGS],
|
||||
['--vo=gpu', '--gpu-api=opengl', '--gpu-context=x11egl,x11'],
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
Shared XWayland/X11 backend forcing for mpv and the Electron app.
|
||||
|
||||
On Wayland sessions the SubMiner overlay can only be reliably kept above mpv when
|
||||
BOTH processes run under XWayland: the Wayland protocol forbids clients from
|
||||
controlling window stacking, so Electron's `setAlwaysOnTop`/`moveTop` become
|
||||
no-ops under a native Wayland surface. Hyprland and Sway are the exception — they
|
||||
are supported natively via compositor-specific window placement — so all forcing
|
||||
here is gated to "Linux + Wayland session + NOT Hyprland/Sway".
|
||||
|
||||
This module is shared between the `launcher/` bundle and the Electron `src/` build
|
||||
so the gate and the mpv backend args stay in one place.
|
||||
*/
|
||||
|
||||
/** mpv args that pin the GPU/windowing stack to X11/XWayland (libGL via EGL on X11). */
|
||||
export const MPV_X11_BACKEND_ARGS = [
|
||||
'--vo=gpu',
|
||||
'--gpu-api=opengl',
|
||||
'--gpu-context=x11egl,x11',
|
||||
] as const;
|
||||
|
||||
export type LinuxDesktopEnv = {
|
||||
xdgCurrentDesktop: string;
|
||||
xdgSessionDesktop: string;
|
||||
hasWayland: boolean;
|
||||
};
|
||||
|
||||
export function getLinuxDesktopEnv(env: NodeJS.ProcessEnv = process.env): LinuxDesktopEnv {
|
||||
const xdgCurrentDesktop = (env.XDG_CURRENT_DESKTOP || '').toLowerCase();
|
||||
const xdgSessionDesktop = (env.XDG_SESSION_DESKTOP || '').toLowerCase();
|
||||
const xdgSessionType = (env.XDG_SESSION_TYPE || '').toLowerCase();
|
||||
return {
|
||||
xdgCurrentDesktop,
|
||||
xdgSessionDesktop,
|
||||
hasWayland: Boolean(env.WAYLAND_DISPLAY) || xdgSessionType === 'wayland',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compositors that SubMiner supports natively on Wayland (no XWayland forcing).
|
||||
* Detected via their socket env vars or the XDG desktop identifiers.
|
||||
*/
|
||||
export function isSupportedWaylandCompositor(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
const desktop = getLinuxDesktopEnv(env);
|
||||
return (
|
||||
Boolean(env.HYPRLAND_INSTANCE_SIGNATURE || env.SWAYSOCK) ||
|
||||
desktop.xdgCurrentDesktop.includes('hyprland') ||
|
||||
desktop.xdgCurrentDesktop.includes('sway') ||
|
||||
desktop.xdgSessionDesktop.includes('hyprland') ||
|
||||
desktop.xdgSessionDesktop.includes('sway')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should this Linux session be pushed onto XWayland/X11? True for a Wayland session
|
||||
* that is not one of the natively-supported compositors and has an X11 display
|
||||
* available for the fallback. This is the "auto" decision shared by the Electron app
|
||||
* and SubMiner-managed mpv launches.
|
||||
*/
|
||||
export function shouldForceX11WaylandSession(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
if (process.platform !== 'linux') return false;
|
||||
if (!env.DISPLAY?.trim()) return false;
|
||||
if (!getLinuxDesktopEnv(env).hasWayland) return false;
|
||||
return !isSupportedWaylandCompositor(env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launcher-facing decision that also honors an explicit `--backend` choice:
|
||||
* - `x11` forces the X11 stack whenever an X11 display exists,
|
||||
* - `auto` defers to {@link shouldForceX11WaylandSession}.
|
||||
*/
|
||||
export function shouldForceX11MpvBackend(
|
||||
backend: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
if (process.platform !== 'linux' || !env.DISPLAY?.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (backend === 'x11') return true;
|
||||
return backend === 'auto' && shouldForceX11WaylandSession(env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip Wayland/compositor hints and pin the session type to X11 on the given env
|
||||
* object (mutates in place and returns it) so a child mpv process picks XWayland.
|
||||
*/
|
||||
export function applyX11EnvOverrides(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
delete env.WAYLAND_DISPLAY;
|
||||
delete env.HYPRLAND_INSTANCE_SIGNATURE;
|
||||
delete env.SWAYSOCK;
|
||||
env.XDG_SESSION_TYPE = 'x11';
|
||||
return env;
|
||||
}
|
||||
Reference in New Issue
Block a user