mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-20 17:01:24 -07:00
604 lines
18 KiB
TypeScript
604 lines
18 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
buildHyprlandPlacementDispatches,
|
|
ensureHyprlandWindowFloatingByTitle,
|
|
ensureHyprlandWindowFloatingByTitleWithStatus,
|
|
findHyprlandWindowForPlacement,
|
|
hasHyprlandWindowPlacementBoundsMismatch,
|
|
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 tiled overlay windows without pinning them', () => {
|
|
assert.deepEqual(
|
|
buildHyprlandPlacementDispatches({
|
|
address: '0xabc',
|
|
floating: false,
|
|
pinned: false,
|
|
}),
|
|
[
|
|
['dispatch', 'setfloating', 'address:0xabc'],
|
|
['dispatch', 'alterzorder', 'top,address:0xabc'],
|
|
],
|
|
);
|
|
});
|
|
|
|
test('buildHyprlandPlacementDispatches force-aligns floating overlay windows to target bounds', () => {
|
|
assert.deepEqual(
|
|
buildHyprlandPlacementDispatches(
|
|
{
|
|
address: '0xabc',
|
|
floating: true,
|
|
pinned: false,
|
|
},
|
|
{
|
|
x: 0,
|
|
y: 0,
|
|
width: 1920,
|
|
height: 1080,
|
|
},
|
|
),
|
|
[
|
|
['dispatch', 'resizewindowpixel', 'exact 1920 1080,address:0xabc'],
|
|
['dispatch', 'movewindowpixel', 'exact 0 0,address:0xabc'],
|
|
['dispatch', 'setprop', 'address:0xabc rounding 0'],
|
|
['dispatch', 'setprop', 'address:0xabc border_size 0'],
|
|
['dispatch', 'setprop', 'address:0xabc no_shadow 1'],
|
|
['dispatch', 'setprop', 'address:0xabc no_blur 1'],
|
|
['dispatch', 'setprop', 'address:0xabc decorate 0'],
|
|
['dispatch', 'alterzorder', 'top,address:0xabc'],
|
|
],
|
|
);
|
|
});
|
|
|
|
test('buildHyprlandPlacementDispatches emits Lua dispatchers for Lua-config Hyprland sessions', () => {
|
|
assert.deepEqual(
|
|
buildHyprlandPlacementDispatches(
|
|
{
|
|
address: '0xabc',
|
|
floating: false,
|
|
pinned: true,
|
|
},
|
|
{
|
|
x: 0,
|
|
y: 0,
|
|
width: 1920,
|
|
height: 1080,
|
|
},
|
|
{
|
|
configProvider: 'lua',
|
|
},
|
|
),
|
|
[
|
|
['dispatch', 'hl.dsp.window.float({ action = "on", window = "address:0xabc" })'],
|
|
['dispatch', 'hl.dsp.window.pin({ action = "off", window = "address:0xabc" })'],
|
|
['dispatch', 'hl.dsp.window.resize({ x = 1920, y = 1080, window = "address:0xabc" })'],
|
|
['dispatch', 'hl.dsp.window.move({ x = 0, y = 0, window = "address:0xabc" })'],
|
|
[
|
|
'dispatch',
|
|
'hl.dsp.window.set_prop({ prop = "rounding", value = "0", window = "address:0xabc" })',
|
|
],
|
|
[
|
|
'dispatch',
|
|
'hl.dsp.window.set_prop({ prop = "border_size", value = "0", window = "address:0xabc" })',
|
|
],
|
|
[
|
|
'dispatch',
|
|
'hl.dsp.window.set_prop({ prop = "no_shadow", value = "1", window = "address:0xabc" })',
|
|
],
|
|
[
|
|
'dispatch',
|
|
'hl.dsp.window.set_prop({ prop = "no_blur", value = "1", window = "address:0xabc" })',
|
|
],
|
|
[
|
|
'dispatch',
|
|
'hl.dsp.window.set_prop({ prop = "decorate", value = "0", window = "address:0xabc" })',
|
|
],
|
|
['dispatch', 'hl.dsp.window.alter_zorder({ mode = "top", window = "address:0xabc" })'],
|
|
],
|
|
);
|
|
});
|
|
|
|
test('buildHyprlandPlacementDispatches does not pin already floating overlay windows', () => {
|
|
assert.deepEqual(
|
|
buildHyprlandPlacementDispatches({
|
|
address: '0xabc',
|
|
floating: true,
|
|
pinned: false,
|
|
}),
|
|
[['dispatch', 'alterzorder', 'top,address:0xabc']],
|
|
);
|
|
});
|
|
|
|
test('Hyprland placement keeps a recovery dialog above the input-catching overlay', () => {
|
|
for (const configProvider of ['hyprlang', 'lua']) {
|
|
for (const retryBounds of [false, true]) {
|
|
// Bottom to top, as when Hyprland opens its recovery dialog over playback.
|
|
const stack = ['0xmpv', '0xoverlay', '0xdialog'];
|
|
const clients = [
|
|
{
|
|
address: '0xoverlay',
|
|
pid: 456,
|
|
title: 'SubMiner Overlay',
|
|
floating: true,
|
|
workspace: { id: 1 },
|
|
at: [10, 20],
|
|
size: [100, 100],
|
|
},
|
|
{
|
|
address: '0xdialog',
|
|
class: 'hyprland-dialog',
|
|
mapped: true,
|
|
hidden: false,
|
|
workspace: { id: 1 },
|
|
},
|
|
];
|
|
let clientReads = 0;
|
|
const status = ensureHyprlandWindowFloatingByTitleWithStatus({
|
|
title: 'SubMiner Overlay',
|
|
platform: 'linux',
|
|
env: { HYPRLAND_INSTANCE_SIGNATURE: 'abc' },
|
|
pid: 456,
|
|
bounds: retryBounds ? { x: 0, y: 0, width: 1280, height: 720 } : undefined,
|
|
execFileSync: (_command, args) => {
|
|
if (args.join(' ') === '-j clients') {
|
|
clientReads += 1;
|
|
return JSON.stringify(clients);
|
|
}
|
|
if (args.join(' ') === '-j status') return JSON.stringify({ configProvider });
|
|
if (args.join(' ').match(/alterzorder|alter_zorder/)) {
|
|
const address = args.join(' ').match(/address:(0x\w+)/)?.[1];
|
|
assert.ok(address);
|
|
stack.splice(stack.indexOf(address), 1);
|
|
stack.push(address);
|
|
}
|
|
return '';
|
|
},
|
|
});
|
|
assert.equal(status.dispatched, true);
|
|
assert.equal(clientReads, retryBounds ? 2 : 1);
|
|
assert.deepEqual(stack, ['0xmpv', '0xoverlay', '0xdialog'], configProvider);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('Hyprland placement only promotes mapped dialogs on the placed window workspace', () => {
|
|
const calls: string[] = [];
|
|
const dialog = {
|
|
class: 'hyprland-dialog',
|
|
mapped: true,
|
|
hidden: false,
|
|
workspace: { id: 1 },
|
|
};
|
|
const clients = [
|
|
{
|
|
address: '0xoverlay',
|
|
pid: 456,
|
|
title: 'SubMiner Overlay',
|
|
floating: true,
|
|
workspace: { id: 1 },
|
|
},
|
|
{ ...dialog, address: '0xhidden', hidden: true },
|
|
{ ...dialog, address: '0xunmapped', mapped: false },
|
|
{ ...dialog, address: '0xother', workspace: { id: 2 } },
|
|
{ ...dialog, address: '0xordinary', class: 'terminal' },
|
|
{ ...dialog, address: '0xinitial', class: '', initialClass: 'hyprland-dialog' },
|
|
];
|
|
for (const promote of [true, false]) {
|
|
calls.length = 0;
|
|
ensureHyprlandWindowFloatingByTitleWithStatus({
|
|
title: 'SubMiner Overlay',
|
|
platform: 'linux',
|
|
env: { HYPRLAND_INSTANCE_SIGNATURE: 'abc' },
|
|
pid: 456,
|
|
promote,
|
|
execFileSync: (_command, args) => {
|
|
if (args.join(' ') === '-j clients') return JSON.stringify(clients);
|
|
if (args.join(' ') === '-j status') return JSON.stringify({ configProvider: 'hyprlang' });
|
|
calls.push(args.join(' '));
|
|
return '';
|
|
},
|
|
});
|
|
assert.deepEqual(
|
|
calls,
|
|
promote
|
|
? [
|
|
'dispatch alterzorder top,address:0xoverlay',
|
|
'dispatch alterzorder top,address:0xinitial',
|
|
]
|
|
: [],
|
|
);
|
|
}
|
|
});
|
|
|
|
test('buildHyprlandPlacementDispatches can update placement without raising z-order', () => {
|
|
const buildDispatches = buildHyprlandPlacementDispatches as (
|
|
client: Parameters<typeof buildHyprlandPlacementDispatches>[0],
|
|
bounds: Parameters<typeof buildHyprlandPlacementDispatches>[1],
|
|
options: { promote: false },
|
|
) => string[][];
|
|
|
|
assert.deepEqual(
|
|
buildDispatches(
|
|
{
|
|
address: '0xabc',
|
|
floating: true,
|
|
pinned: false,
|
|
},
|
|
{
|
|
x: 0,
|
|
y: 0,
|
|
width: 1920,
|
|
height: 1080,
|
|
},
|
|
{ promote: false },
|
|
),
|
|
[
|
|
['dispatch', 'resizewindowpixel', 'exact 1920 1080,address:0xabc'],
|
|
['dispatch', 'movewindowpixel', 'exact 0 0,address:0xabc'],
|
|
['dispatch', 'setprop', 'address:0xabc rounding 0'],
|
|
['dispatch', 'setprop', 'address:0xabc border_size 0'],
|
|
['dispatch', 'setprop', 'address:0xabc no_shadow 1'],
|
|
['dispatch', 'setprop', 'address:0xabc no_blur 1'],
|
|
['dispatch', 'setprop', 'address:0xabc decorate 0'],
|
|
],
|
|
);
|
|
});
|
|
|
|
test('buildHyprlandPlacementDispatches unpins previously pinned overlay windows', () => {
|
|
assert.deepEqual(
|
|
buildHyprlandPlacementDispatches({
|
|
address: '0xabc',
|
|
floating: true,
|
|
pinned: true,
|
|
}),
|
|
[
|
|
['dispatch', 'pin', 'address:0xabc'],
|
|
['dispatch', 'alterzorder', 'top,address:0xabc'],
|
|
],
|
|
);
|
|
});
|
|
|
|
test('ensureHyprlandWindowFloatingByTitleWithStatus reports not-applicable off Hyprland', () => {
|
|
const status = ensureHyprlandWindowFloatingByTitleWithStatus({
|
|
title: 'SubMiner Overlay Modal',
|
|
platform: 'linux',
|
|
env: {},
|
|
execFileSync: (() => {
|
|
throw new Error('should not query the compositor when placement is not applicable');
|
|
}) as never,
|
|
});
|
|
|
|
assert.deepEqual(status, { applicable: false, clientFound: false, dispatched: false });
|
|
});
|
|
|
|
test('ensureHyprlandWindowFloatingByTitleWithStatus reports pending when the client is not yet mapped', () => {
|
|
// The window has not been mapped by the compositor yet, so no client matches. Callers use
|
|
// this to keep retrying until the modal is actually placed above fullscreen mpv.
|
|
const status = ensureHyprlandWindowFloatingByTitleWithStatus({
|
|
title: 'SubMiner Overlay Modal',
|
|
platform: 'linux',
|
|
env: { HYPRLAND_INSTANCE_SIGNATURE: 'abc' },
|
|
pid: 999,
|
|
execFileSync: ((command: string, args: string[]) => {
|
|
if (args.join(' ') === '-j clients') {
|
|
return JSON.stringify([]);
|
|
}
|
|
return '';
|
|
}) as never,
|
|
});
|
|
|
|
assert.deepEqual(status, { applicable: true, clientFound: false, dispatched: false });
|
|
});
|
|
|
|
test('ensureHyprlandWindowFloatingByTitleWithStatus reports the client found once mapped', () => {
|
|
const status = ensureHyprlandWindowFloatingByTitleWithStatus({
|
|
title: 'SubMiner Overlay Modal',
|
|
platform: 'linux',
|
|
env: { HYPRLAND_INSTANCE_SIGNATURE: 'abc' },
|
|
pid: 456,
|
|
execFileSync: ((command: string, args: string[]) => {
|
|
if (args.join(' ') === '-j clients') {
|
|
return JSON.stringify([
|
|
{
|
|
address: '0xmatch',
|
|
pid: 456,
|
|
title: 'SubMiner Overlay Modal',
|
|
mapped: true,
|
|
floating: false,
|
|
pinned: false,
|
|
},
|
|
]);
|
|
}
|
|
if (args.join(' ') === '-j status') {
|
|
return JSON.stringify({ configProvider: 'lua' });
|
|
}
|
|
return '';
|
|
}) as never,
|
|
});
|
|
|
|
assert.deepEqual(status, { applicable: true, clientFound: true, dispatched: true });
|
|
});
|
|
|
|
test('ensureHyprlandWindowFloatingByTitle dispatches float-only 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,
|
|
},
|
|
]);
|
|
}
|
|
if (args.join(' ') === '-j status') {
|
|
return JSON.stringify({ configProvider: 'hyprlang' });
|
|
}
|
|
return '';
|
|
}) as never,
|
|
});
|
|
|
|
assert.equal(placed, true);
|
|
assert.deepEqual(
|
|
calls.map(([, args]) => args),
|
|
[
|
|
['-j', 'clients'],
|
|
['-j', 'status'],
|
|
['dispatch', 'setfloating', 'address:0xmatch'],
|
|
['dispatch', 'alterzorder', 'top,address:0xmatch'],
|
|
],
|
|
);
|
|
});
|
|
|
|
test('ensureHyprlandWindowFloatingByTitle dispatches exact Hyprland geometry when bounds are provided', () => {
|
|
const calls: unknown[][] = [];
|
|
const placed = ensureHyprlandWindowFloatingByTitle({
|
|
title: 'SubMiner Stats',
|
|
platform: 'linux',
|
|
env: {
|
|
HYPRLAND_INSTANCE_SIGNATURE: 'abc',
|
|
},
|
|
pid: 456,
|
|
bounds: {
|
|
x: 0,
|
|
y: 0,
|
|
width: 1920,
|
|
height: 1080,
|
|
},
|
|
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: true,
|
|
pinned: false,
|
|
},
|
|
]);
|
|
}
|
|
if (args.join(' ') === '-j status') {
|
|
return JSON.stringify({ configProvider: 'hyprlang' });
|
|
}
|
|
return '';
|
|
}) as never,
|
|
});
|
|
|
|
assert.equal(placed, true);
|
|
assert.deepEqual(
|
|
calls.map(([, args]) => args),
|
|
[
|
|
['-j', 'clients'],
|
|
['-j', 'status'],
|
|
['dispatch', 'resizewindowpixel', 'exact 1920 1080,address:0xmatch'],
|
|
['dispatch', 'movewindowpixel', 'exact 0 0,address:0xmatch'],
|
|
['dispatch', 'setprop', 'address:0xmatch rounding 0'],
|
|
['dispatch', 'setprop', 'address:0xmatch border_size 0'],
|
|
['dispatch', 'setprop', 'address:0xmatch no_shadow 1'],
|
|
['dispatch', 'setprop', 'address:0xmatch no_blur 1'],
|
|
['dispatch', 'setprop', 'address:0xmatch decorate 0'],
|
|
['dispatch', 'alterzorder', 'top,address:0xmatch'],
|
|
],
|
|
);
|
|
});
|
|
|
|
test('ensureHyprlandWindowFloatingByTitle dispatches Lua syntax for Lua-config Hyprland sessions', () => {
|
|
const calls: unknown[][] = [];
|
|
const placed = ensureHyprlandWindowFloatingByTitle({
|
|
title: 'SubMiner Stats',
|
|
platform: 'linux',
|
|
env: {
|
|
HYPRLAND_INSTANCE_SIGNATURE: 'abc',
|
|
},
|
|
pid: 456,
|
|
bounds: {
|
|
x: 0,
|
|
y: 0,
|
|
width: 1920,
|
|
height: 1080,
|
|
},
|
|
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: true,
|
|
pinned: false,
|
|
},
|
|
]);
|
|
}
|
|
if (args.join(' ') === '-j status') {
|
|
return JSON.stringify({ configProvider: 'lua' });
|
|
}
|
|
return '';
|
|
}) as never,
|
|
});
|
|
|
|
assert.equal(placed, true);
|
|
const dispatches = calls
|
|
.map(([, args]) => args as string[])
|
|
.filter(([verb]) => verb === 'dispatch');
|
|
assert.ok(dispatches.length > 0);
|
|
for (const [, dispatcher] of dispatches) {
|
|
assert.ok(dispatcher!.startsWith('hl.dsp.window.'), dispatcher);
|
|
}
|
|
});
|
|
|
|
test('hasHyprlandWindowPlacementBoundsMismatch compares compositor client bounds', () => {
|
|
const mismatch = hasHyprlandWindowPlacementBoundsMismatch({
|
|
title: 'SubMiner Overlay',
|
|
platform: 'linux',
|
|
env: {
|
|
HYPRLAND_INSTANCE_SIGNATURE: 'abc',
|
|
},
|
|
pid: 456,
|
|
bounds: {
|
|
x: 0,
|
|
y: 0,
|
|
width: 3440,
|
|
height: 1440,
|
|
},
|
|
execFileSync: ((command: string, args: string[]) => {
|
|
assert.equal(command, 'hyprctl');
|
|
assert.deepEqual(args, ['-j', 'clients']);
|
|
return JSON.stringify([
|
|
{
|
|
address: '0xmatch',
|
|
pid: 456,
|
|
title: 'SubMiner Overlay',
|
|
mapped: true,
|
|
floating: true,
|
|
at: [0, 14],
|
|
size: [3440, 1426],
|
|
},
|
|
]);
|
|
}) as never,
|
|
});
|
|
|
|
assert.equal(mismatch, true);
|
|
});
|
|
|
|
test('ensureHyprlandWindowFloatingByTitle retries when compositor bounds stay misaligned', () => {
|
|
let clientReads = 0;
|
|
const calls: unknown[][] = [];
|
|
const placed = ensureHyprlandWindowFloatingByTitle({
|
|
title: 'SubMiner Overlay',
|
|
platform: 'linux',
|
|
env: {
|
|
HYPRLAND_INSTANCE_SIGNATURE: 'abc',
|
|
},
|
|
pid: 456,
|
|
bounds: {
|
|
x: 0,
|
|
y: 0,
|
|
width: 3440,
|
|
height: 1440,
|
|
},
|
|
execFileSync: ((command: string, args: string[], options: unknown) => {
|
|
calls.push([command, args, options]);
|
|
if (args.join(' ') === '-j clients') {
|
|
clientReads += 1;
|
|
return JSON.stringify([
|
|
{
|
|
address: '0xmatch',
|
|
pid: 456,
|
|
title: 'SubMiner Overlay',
|
|
mapped: true,
|
|
floating: true,
|
|
pinned: false,
|
|
at: clientReads === 1 ? [10, 58] : [0, 14],
|
|
size: clientReads === 1 ? [3420, 1372] : [3440, 1426],
|
|
},
|
|
]);
|
|
}
|
|
if (args.join(' ') === '-j status') {
|
|
return JSON.stringify({ configProvider: 'hyprlang' });
|
|
}
|
|
return '';
|
|
}) as never,
|
|
});
|
|
|
|
assert.equal(placed, true);
|
|
assert.equal(clientReads, 2);
|
|
assert.deepEqual(
|
|
calls
|
|
.map(([, args]) => args)
|
|
.filter(
|
|
(args) =>
|
|
Array.isArray(args) &&
|
|
args[0] === 'dispatch' &&
|
|
(args[1] === 'resizewindowpixel' || args[1] === 'movewindowpixel'),
|
|
),
|
|
[
|
|
['dispatch', 'resizewindowpixel', 'exact 3440 1440,address:0xmatch'],
|
|
['dispatch', 'movewindowpixel', 'exact 0 0,address:0xmatch'],
|
|
['dispatch', 'resizewindowpixel', 'exact 3440 1440,address:0xmatch'],
|
|
['dispatch', 'movewindowpixel', 'exact 0 0,address:0xmatch'],
|
|
],
|
|
);
|
|
});
|