mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-04 00:41:33 -07:00
fix: align Hyprland fullscreen overlays
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
id: TASK-336
|
||||||
|
title: Fix Hyprland fullscreen overlay downward offset
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-05-04 05:42'
|
||||||
|
updated_date: '2026-05-04 05:56'
|
||||||
|
labels:
|
||||||
|
- linux
|
||||||
|
- hyprland
|
||||||
|
- overlay
|
||||||
|
- bug
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- src/window-trackers/hyprland-tracker.ts
|
||||||
|
- src/core/services/overlay-window-bounds.ts
|
||||||
|
- src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
SubMiner visible overlay is slightly below mpv when mpv is fullscreen on Linux Hyprland. Align overlay bounds with mpv fullscreen client/monitor bounds.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 Hyprland fullscreen mpv overlay uses top-aligned geometry instead of inheriting a downward offset.
|
||||||
|
- [x] #2 Regression coverage captures the fullscreen Hyprland geometry case.
|
||||||
|
- [x] #3 Targeted tests pass.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Added follow-up Hyprland placement handling after the fullscreenClient geometry fix. SubMiner overlay/stats windows now get stable titles and, on Hyprland, are resolved from `hyprctl -j clients` by current PID/title, then set floating and pinned before bounds are applied. The stats overlay reapplies bounds after showing because Hyprland cannot see the hidden window before it is mapped.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Summary:
|
||||||
|
- Treated Hyprland `fullscreenClient` as a fullscreen signal when resolving mpv overlay geometry.
|
||||||
|
- Added Hyprland window placement handling so SubMiner overlay/stats windows are set floating and pinned before bounds are applied.
|
||||||
|
- Gave overlay/stats windows stable titles for Hyprland client matching, and reapplied stats bounds after show.
|
||||||
|
- Added regression coverage for the 28px fullscreen geometry shape and Hyprland placement dispatches.
|
||||||
|
- Added a changelog fragment for the overlay fix.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
- `bun test src/core/services/hyprland-window-placement.test.ts src/core/services/overlay-window-config.test.ts src/core/services/stats-window.test.ts src/core/services/overlay-window-bounds.test.ts src/window-trackers/hyprland-tracker.test.ts`
|
||||||
|
- `bun run typecheck`
|
||||||
|
- `bun run changelog:lint`
|
||||||
|
- `bun run test:fast`
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Overlay: Aligned the Hyprland fullscreen overlay with mpv when mpv reports client-requested fullscreen.
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
buildHyprlandPlacementDispatches,
|
||||||
|
ensureHyprlandWindowFloatingByTitle,
|
||||||
|
findHyprlandWindowForPlacement,
|
||||||
|
shouldAttemptHyprlandWindowPlacement,
|
||||||
|
} from './hyprland-window-placement';
|
||||||
|
|
||||||
|
test('shouldAttemptHyprlandWindowPlacement only enables on Hyprland Linux sessions', () => {
|
||||||
|
assert.equal(
|
||||||
|
shouldAttemptHyprlandWindowPlacement('linux', {
|
||||||
|
HYPRLAND_INSTANCE_SIGNATURE: 'abc',
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
shouldAttemptHyprlandWindowPlacement('linux', {
|
||||||
|
WAYLAND_DISPLAY: 'wayland-1',
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
shouldAttemptHyprlandWindowPlacement('darwin', {
|
||||||
|
HYPRLAND_INSTANCE_SIGNATURE: 'abc',
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('findHyprlandWindowForPlacement matches current process by title', () => {
|
||||||
|
const client = findHyprlandWindowForPlacement(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
address: '0xother',
|
||||||
|
pid: 123,
|
||||||
|
title: 'SubMiner Stats',
|
||||||
|
mapped: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: '0xmatch',
|
||||||
|
pid: 456,
|
||||||
|
title: 'SubMiner Stats',
|
||||||
|
mapped: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
pid: 456,
|
||||||
|
title: 'SubMiner Stats',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(client?.address, '0xmatch');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildHyprlandPlacementDispatches floats and pins tiled overlay windows', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
buildHyprlandPlacementDispatches({
|
||||||
|
address: '0xabc',
|
||||||
|
floating: false,
|
||||||
|
pinned: false,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
['dispatch', 'setfloating', 'address:0xabc'],
|
||||||
|
['dispatch', 'pin', 'address:0xabc'],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildHyprlandPlacementDispatches skips already floating and pinned windows', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
buildHyprlandPlacementDispatches({
|
||||||
|
address: '0xabc',
|
||||||
|
floating: true,
|
||||||
|
pinned: true,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ensureHyprlandWindowFloatingByTitle dispatches placement for matching tiled window', () => {
|
||||||
|
const calls: unknown[][] = [];
|
||||||
|
const placed = ensureHyprlandWindowFloatingByTitle({
|
||||||
|
title: 'SubMiner Stats',
|
||||||
|
platform: 'linux',
|
||||||
|
env: {
|
||||||
|
HYPRLAND_INSTANCE_SIGNATURE: 'abc',
|
||||||
|
},
|
||||||
|
pid: 456,
|
||||||
|
execFileSync: ((command: string, args: string[], options: unknown) => {
|
||||||
|
calls.push([command, args, options]);
|
||||||
|
if (args.join(' ') === '-j clients') {
|
||||||
|
return JSON.stringify([
|
||||||
|
{
|
||||||
|
address: '0xmatch',
|
||||||
|
pid: 456,
|
||||||
|
title: 'SubMiner Stats',
|
||||||
|
mapped: true,
|
||||||
|
floating: false,
|
||||||
|
pinned: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}) as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(placed, true);
|
||||||
|
assert.deepEqual(
|
||||||
|
calls.map(([, args]) => args),
|
||||||
|
[
|
||||||
|
['-j', 'clients'],
|
||||||
|
['dispatch', 'setfloating', 'address:0xmatch'],
|
||||||
|
['dispatch', 'pin', 'address:0xmatch'],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
|
||||||
|
export interface HyprlandPlacementClient {
|
||||||
|
address?: string;
|
||||||
|
floating?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
|
initialTitle?: string;
|
||||||
|
mapped?: boolean;
|
||||||
|
pid?: number;
|
||||||
|
pinned?: boolean;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecFileSync = typeof execFileSync;
|
||||||
|
|
||||||
|
export function shouldAttemptHyprlandWindowPlacement(
|
||||||
|
platform: NodeJS.Platform = process.platform,
|
||||||
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
|
): boolean {
|
||||||
|
return platform === 'linux' && Boolean(env.HYPRLAND_INSTANCE_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHyprlandClients(output: string): HyprlandPlacementClient[] {
|
||||||
|
const payloadStart = output.indexOf('[');
|
||||||
|
if (payloadStart < 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = JSON.parse(output.slice(payloadStart)) as unknown;
|
||||||
|
return Array.isArray(parsed) ? (parsed as HyprlandPlacementClient[]) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findHyprlandWindowForPlacement(
|
||||||
|
clients: HyprlandPlacementClient[],
|
||||||
|
options: {
|
||||||
|
pid: number;
|
||||||
|
title: string;
|
||||||
|
},
|
||||||
|
): HyprlandPlacementClient | null {
|
||||||
|
const title = options.title.trim();
|
||||||
|
if (!title) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
clients.find(
|
||||||
|
(client) =>
|
||||||
|
client.pid === options.pid &&
|
||||||
|
client.address &&
|
||||||
|
client.mapped !== false &&
|
||||||
|
client.hidden !== true &&
|
||||||
|
(client.title === title || client.initialTitle === title),
|
||||||
|
) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildHyprlandPlacementDispatches(
|
||||||
|
client: HyprlandPlacementClient,
|
||||||
|
): string[][] {
|
||||||
|
if (!client.address) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowAddress = `address:${client.address}`;
|
||||||
|
const dispatches: string[][] = [];
|
||||||
|
if (client.floating !== true) {
|
||||||
|
dispatches.push(['dispatch', 'setfloating', windowAddress]);
|
||||||
|
}
|
||||||
|
if (client.pinned !== true) {
|
||||||
|
dispatches.push(['dispatch', 'pin', windowAddress]);
|
||||||
|
}
|
||||||
|
return dispatches;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureHyprlandWindowFloatingByTitle(options: {
|
||||||
|
title: string;
|
||||||
|
platform?: NodeJS.Platform;
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
|
pid?: number;
|
||||||
|
execFileSync?: ExecFileSync;
|
||||||
|
}): boolean {
|
||||||
|
if (!shouldAttemptHyprlandWindowPlacement(options.platform, options.env)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = options.execFileSync ?? execFileSync;
|
||||||
|
try {
|
||||||
|
const clients = parseHyprlandClients(
|
||||||
|
String(run('hyprctl', ['-j', 'clients'], { encoding: 'utf-8' })),
|
||||||
|
);
|
||||||
|
const client = findHyprlandWindowForPlacement(clients, {
|
||||||
|
pid: options.pid ?? process.pid,
|
||||||
|
title: options.title,
|
||||||
|
});
|
||||||
|
if (!client) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatches = buildHyprlandPlacementDispatches(client);
|
||||||
|
for (const args of dispatches) {
|
||||||
|
run('hyprctl', args, { stdio: 'ignore' });
|
||||||
|
}
|
||||||
|
return dispatches.length > 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,7 @@ test('overlay manager applies bounds for main and modal windows', () => {
|
|||||||
const visibleCalls: Electron.Rectangle[] = [];
|
const visibleCalls: Electron.Rectangle[] = [];
|
||||||
const visibleWindow = {
|
const visibleWindow = {
|
||||||
isDestroyed: () => false,
|
isDestroyed: () => false,
|
||||||
|
getTitle: () => 'SubMiner Overlay',
|
||||||
setBounds: (bounds: Electron.Rectangle) => {
|
setBounds: (bounds: Electron.Rectangle) => {
|
||||||
visibleCalls.push(bounds);
|
visibleCalls.push(bounds);
|
||||||
},
|
},
|
||||||
@@ -84,6 +85,7 @@ test('overlay manager applies bounds for main and modal windows', () => {
|
|||||||
const modalCalls: Electron.Rectangle[] = [];
|
const modalCalls: Electron.Rectangle[] = [];
|
||||||
const modalWindow = {
|
const modalWindow = {
|
||||||
isDestroyed: () => false,
|
isDestroyed: () => false,
|
||||||
|
getTitle: () => 'SubMiner Overlay Modal',
|
||||||
setBounds: (bounds: Electron.Rectangle) => {
|
setBounds: (bounds: Electron.Rectangle) => {
|
||||||
modalCalls.push(bounds);
|
modalCalls.push(bounds);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ test('overlay window config explicitly disables renderer sandbox for preload com
|
|||||||
yomitanSession: null,
|
yomitanSession: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assert.equal(options.title, 'SubMiner Overlay');
|
||||||
assert.equal(options.backgroundColor, '#00000000');
|
assert.equal(options.backgroundColor, '#00000000');
|
||||||
assert.equal(options.webPreferences?.sandbox, false);
|
assert.equal(options.webPreferences?.sandbox, false);
|
||||||
assert.equal(options.webPreferences?.backgroundThrottling, false);
|
assert.equal(options.webPreferences?.backgroundThrottling, false);
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import type { BrowserWindowConstructorOptions, Session } from 'electron';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import type { OverlayWindowKind } from './overlay-window-input';
|
import type { OverlayWindowKind } from './overlay-window-input';
|
||||||
|
|
||||||
|
export const OVERLAY_WINDOW_TITLES: Record<OverlayWindowKind, string> = {
|
||||||
|
visible: 'SubMiner Overlay',
|
||||||
|
modal: 'SubMiner Overlay Modal',
|
||||||
|
};
|
||||||
|
|
||||||
export function buildOverlayWindowOptions(
|
export function buildOverlayWindowOptions(
|
||||||
kind: OverlayWindowKind,
|
kind: OverlayWindowKind,
|
||||||
options: {
|
options: {
|
||||||
@@ -14,6 +19,7 @@ export function buildOverlayWindowOptions(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
show: false,
|
show: false,
|
||||||
|
title: OVERLAY_WINDOW_TITLES[kind],
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 600,
|
height: 600,
|
||||||
x: 0,
|
x: 0,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BrowserWindow, screen, type Session } from 'electron';
|
import electron from 'electron';
|
||||||
|
import type { BrowserWindow, Session } from 'electron';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { WindowGeometry } from '../../types';
|
import { WindowGeometry } from '../../types';
|
||||||
import { createLogger } from '../../logger';
|
import { createLogger } from '../../logger';
|
||||||
@@ -8,12 +9,14 @@ import {
|
|||||||
handleOverlayWindowBlurred,
|
handleOverlayWindowBlurred,
|
||||||
type OverlayWindowKind,
|
type OverlayWindowKind,
|
||||||
} from './overlay-window-input';
|
} from './overlay-window-input';
|
||||||
|
import { ensureHyprlandWindowFloatingByTitle } from './hyprland-window-placement';
|
||||||
import { buildOverlayWindowOptions } from './overlay-window-options';
|
import { buildOverlayWindowOptions } from './overlay-window-options';
|
||||||
import { normalizeOverlayWindowBoundsForPlatform } from './overlay-window-bounds';
|
import { normalizeOverlayWindowBoundsForPlatform } from './overlay-window-bounds';
|
||||||
import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
|
import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
|
||||||
export { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
|
export { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
|
||||||
|
|
||||||
const logger = createLogger('main:overlay-window');
|
const logger = createLogger('main:overlay-window');
|
||||||
|
const { BrowserWindow: ElectronBrowserWindow, screen } = electron;
|
||||||
const overlayWindowLayerByInstance = new WeakMap<BrowserWindow, OverlayWindowKind>();
|
const overlayWindowLayerByInstance = new WeakMap<BrowserWindow, OverlayWindowKind>();
|
||||||
const overlayWindowContentReady = new WeakSet<BrowserWindow>();
|
const overlayWindowContentReady = new WeakSet<BrowserWindow>();
|
||||||
|
|
||||||
@@ -50,6 +53,7 @@ export function updateOverlayWindowBounds(
|
|||||||
window: BrowserWindow | null,
|
window: BrowserWindow | null,
|
||||||
): void {
|
): void {
|
||||||
if (!geometry || !window || window.isDestroyed()) return;
|
if (!geometry || !window || window.isDestroyed()) return;
|
||||||
|
ensureHyprlandWindowFloatingByTitle({ title: window.getTitle() });
|
||||||
window.setBounds(normalizeOverlayWindowBoundsForPlatform(geometry, process.platform, screen));
|
window.setBounds(normalizeOverlayWindowBoundsForPlatform(geometry, process.platform, screen));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +72,7 @@ export function ensureOverlayWindowLevel(window: BrowserWindow): void {
|
|||||||
}
|
}
|
||||||
window.setAlwaysOnTop(true);
|
window.setAlwaysOnTop(true);
|
||||||
window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
||||||
|
ensureHyprlandWindowFloatingByTitle({ title: window.getTitle() });
|
||||||
window.moveTop();
|
window.moveTop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +104,7 @@ export function createOverlayWindow(
|
|||||||
yomitanSession?: Session | null;
|
yomitanSession?: Session | null;
|
||||||
},
|
},
|
||||||
): BrowserWindow {
|
): BrowserWindow {
|
||||||
const window = new BrowserWindow(buildOverlayWindowOptions(kind, options));
|
const window = new ElectronBrowserWindow(buildOverlayWindowOptions(kind, options));
|
||||||
(window as BrowserWindow & { [OVERLAY_WINDOW_CONTENT_READY_FLAG]?: boolean })[
|
(window as BrowserWindow & { [OVERLAY_WINDOW_CONTENT_READY_FLAG]?: boolean })[
|
||||||
OVERLAY_WINDOW_CONTENT_READY_FLAG
|
OVERLAY_WINDOW_CONTENT_READY_FLAG
|
||||||
] = false;
|
] = false;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { WindowGeometry } from '../../types';
|
|||||||
|
|
||||||
const DEFAULT_STATS_WINDOW_WIDTH = 900;
|
const DEFAULT_STATS_WINDOW_WIDTH = 900;
|
||||||
const DEFAULT_STATS_WINDOW_HEIGHT = 700;
|
const DEFAULT_STATS_WINDOW_HEIGHT = 700;
|
||||||
|
export const STATS_WINDOW_TITLE = 'SubMiner Stats';
|
||||||
|
|
||||||
type StatsWindowLevelController = Pick<BrowserWindow, 'setAlwaysOnTop' | 'moveTop'> &
|
type StatsWindowLevelController = Pick<BrowserWindow, 'setAlwaysOnTop' | 'moveTop'> &
|
||||||
Partial<Pick<BrowserWindow, 'setVisibleOnAllWorkspaces' | 'setFullScreenable'>>;
|
Partial<Pick<BrowserWindow, 'setVisibleOnAllWorkspaces' | 'setFullScreenable'>>;
|
||||||
@@ -30,6 +31,7 @@ export function buildStatsWindowOptions(options: {
|
|||||||
bounds?: WindowGeometry | null;
|
bounds?: WindowGeometry | null;
|
||||||
}): BrowserWindowConstructorOptions {
|
}): BrowserWindowConstructorOptions {
|
||||||
return {
|
return {
|
||||||
|
title: STATS_WINDOW_TITLE,
|
||||||
x: options.bounds?.x,
|
x: options.bounds?.x,
|
||||||
y: options.bounds?.y,
|
y: options.bounds?.y,
|
||||||
width: options.bounds?.width ?? DEFAULT_STATS_WINDOW_WIDTH,
|
width: options.bounds?.width ?? DEFAULT_STATS_WINDOW_WIDTH,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ test('buildStatsWindowOptions uses tracked overlay bounds and preload-friendly w
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assert.equal(options.title, 'SubMiner Stats');
|
||||||
assert.equal(options.x, 120);
|
assert.equal(options.x, 120);
|
||||||
assert.equal(options.y, 80);
|
assert.equal(options.y, 80);
|
||||||
assert.equal(options.width, 1440);
|
assert.equal(options.width, 1440);
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import {
|
|||||||
buildStatsWindowOptions,
|
buildStatsWindowOptions,
|
||||||
promoteStatsWindowLevel,
|
promoteStatsWindowLevel,
|
||||||
shouldHideStatsWindowForInput,
|
shouldHideStatsWindowForInput,
|
||||||
|
STATS_WINDOW_TITLE,
|
||||||
} from './stats-window-runtime.js';
|
} from './stats-window-runtime.js';
|
||||||
|
import { ensureHyprlandWindowFloatingByTitle } from './hyprland-window-placement.js';
|
||||||
|
|
||||||
let statsWindow: BrowserWindow | null = null;
|
let statsWindow: BrowserWindow | null = null;
|
||||||
let toggleRegistered = false;
|
let toggleRegistered = false;
|
||||||
@@ -41,6 +43,9 @@ function showStatsWindow(window: BrowserWindow, options: StatsWindowOptions): vo
|
|||||||
syncStatsWindowBounds(window, options.resolveBounds());
|
syncStatsWindowBounds(window, options.resolveBounds());
|
||||||
promoteStatsWindowLevel(window);
|
promoteStatsWindowLevel(window);
|
||||||
window.show();
|
window.show();
|
||||||
|
if (ensureHyprlandWindowFloatingByTitle({ title: STATS_WINDOW_TITLE })) {
|
||||||
|
syncStatsWindowBounds(window, options.resolveBounds());
|
||||||
|
}
|
||||||
window.focus();
|
window.focus();
|
||||||
options.onVisibilityChanged?.(true);
|
options.onVisibilityChanged?.(true);
|
||||||
promoteStatsWindowLevel(window);
|
promoteStatsWindowLevel(window);
|
||||||
|
|||||||
@@ -149,3 +149,23 @@ test('resolveHyprlandWindowGeometry uses monitor bounds for fullscreen clients',
|
|||||||
height: 1440,
|
height: 1440,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('resolveHyprlandWindowGeometry uses monitor bounds for client-requested fullscreen', () => {
|
||||||
|
const geometry = resolveHyprlandWindowGeometry(
|
||||||
|
makeClient({
|
||||||
|
at: [0, 28],
|
||||||
|
size: [1920, 1052],
|
||||||
|
monitor: 0,
|
||||||
|
fullscreen: 0,
|
||||||
|
fullscreenClient: 2,
|
||||||
|
}),
|
||||||
|
[makeMonitor({ id: 0, x: 0, y: 0, width: 1920, height: 1080 })],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(geometry, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export function parseHyprctlMonitors(output: string): HyprlandMonitor[] | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isHyprlandFullscreenClient(client: HyprlandClient): boolean {
|
function isHyprlandFullscreenClient(client: HyprlandClient): boolean {
|
||||||
return (client.fullscreen ?? 0) > 0;
|
return (client.fullscreen ?? 0) > 0 || (client.fullscreenClient ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveHyprlandWindowGeometry(
|
export function resolveHyprlandWindowGeometry(
|
||||||
|
|||||||
Reference in New Issue
Block a user