Compare commits

...

1 Commits

3 changed files with 226 additions and 18 deletions
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Fixed Hyprland overlay placement on Hyprland 0.55+ Lua configs by using Lua dispatcher syntax when Hyprland reports `configProvider: "lua"`.
@@ -95,6 +95,54 @@ test('buildHyprlandPlacementDispatches force-aligns floating overlay windows to
); );
}); });
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.move({ x = 0, y = 0, window = "address:0xabc" })'],
['dispatch', 'hl.dsp.window.resize({ x = 1920, y = 1080, 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', () => { test('buildHyprlandPlacementDispatches does not pin already floating overlay windows', () => {
assert.deepEqual( assert.deepEqual(
buildHyprlandPlacementDispatches({ buildHyprlandPlacementDispatches({
@@ -177,6 +225,9 @@ test('ensureHyprlandWindowFloatingByTitle dispatches float-only placement for ma
}, },
]); ]);
} }
if (args.join(' ') === '-j status') {
return JSON.stringify({ configProvider: 'hyprlang' });
}
return ''; return '';
}) as never, }) as never,
}); });
@@ -186,6 +237,7 @@ test('ensureHyprlandWindowFloatingByTitle dispatches float-only placement for ma
calls.map(([, args]) => args), calls.map(([, args]) => args),
[ [
['-j', 'clients'], ['-j', 'clients'],
['-j', 'status'],
['dispatch', 'setfloating', 'address:0xmatch'], ['dispatch', 'setfloating', 'address:0xmatch'],
['dispatch', 'alterzorder', 'top,address:0xmatch'], ['dispatch', 'alterzorder', 'top,address:0xmatch'],
], ],
@@ -221,6 +273,9 @@ test('ensureHyprlandWindowFloatingByTitle dispatches exact Hyprland geometry whe
}, },
]); ]);
} }
if (args.join(' ') === '-j status') {
return JSON.stringify({ configProvider: 'hyprlang' });
}
return ''; return '';
}) as never, }) as never,
}); });
@@ -230,6 +285,7 @@ test('ensureHyprlandWindowFloatingByTitle dispatches exact Hyprland geometry whe
calls.map(([, args]) => args), calls.map(([, args]) => args),
[ [
['-j', 'clients'], ['-j', 'clients'],
['-j', 'status'],
['dispatch', 'movewindowpixel', 'exact 0 0,address:0xmatch'], ['dispatch', 'movewindowpixel', 'exact 0 0,address:0xmatch'],
['dispatch', 'resizewindowpixel', 'exact 1920 1080,address:0xmatch'], ['dispatch', 'resizewindowpixel', 'exact 1920 1080,address:0xmatch'],
['dispatch', 'setprop', 'address:0xmatch rounding 0'], ['dispatch', 'setprop', 'address:0xmatch rounding 0'],
@@ -241,3 +297,72 @@ test('ensureHyprlandWindowFloatingByTitle dispatches exact Hyprland geometry whe
], ],
); );
}); });
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);
assert.deepEqual(
calls.map(([, args]) => args),
[
['-j', 'clients'],
['-j', 'status'],
['dispatch', 'hl.dsp.window.move({ x = 0, y = 0, window = "address:0xmatch" })'],
['dispatch', 'hl.dsp.window.resize({ x = 1920, y = 1080, window = "address:0xmatch" })'],
[
'dispatch',
'hl.dsp.window.set_prop({ prop = "rounding", value = "0", window = "address:0xmatch" })',
],
[
'dispatch',
'hl.dsp.window.set_prop({ prop = "border_size", value = "0", window = "address:0xmatch" })',
],
[
'dispatch',
'hl.dsp.window.set_prop({ prop = "no_shadow", value = "1", window = "address:0xmatch" })',
],
[
'dispatch',
'hl.dsp.window.set_prop({ prop = "no_blur", value = "1", window = "address:0xmatch" })',
],
[
'dispatch',
'hl.dsp.window.set_prop({ prop = "decorate", value = "0", window = "address:0xmatch" })',
],
['dispatch', 'hl.dsp.window.alter_zorder({ mode = "top", window = "address:0xmatch" })'],
],
);
});
+97 -18
View File
@@ -19,10 +19,12 @@ export interface HyprlandPlacementBounds {
} }
export interface HyprlandPlacementDispatchOptions { export interface HyprlandPlacementDispatchOptions {
configProvider?: HyprlandConfigProvider;
promote?: boolean; promote?: boolean;
} }
type ExecFileSync = typeof execFileSync; type ExecFileSync = typeof execFileSync;
export type HyprlandConfigProvider = 'hyprlang' | 'lua';
export function shouldAttemptHyprlandWindowPlacement( export function shouldAttemptHyprlandWindowPlacement(
platform: NodeJS.Platform = process.platform, platform: NodeJS.Platform = process.platform,
@@ -75,37 +77,88 @@ export function buildHyprlandPlacementDispatches(
} }
const windowAddress = `address:${client.address}`; const windowAddress = `address:${client.address}`;
const configProvider = options.configProvider ?? 'hyprlang';
const dispatches: string[][] = []; const dispatches: string[][] = [];
if (client.floating !== true) { if (client.floating !== true) {
dispatches.push(['dispatch', 'setfloating', windowAddress]); dispatches.push(
configProvider === 'lua'
? luaWindowDispatch('float', windowAddress, ['action = "on"'])
: ['dispatch', 'setfloating', windowAddress],
);
} }
if (client.pinned === true) { if (client.pinned === true) {
dispatches.push(['dispatch', 'pin', windowAddress]); dispatches.push(
configProvider === 'lua'
? luaWindowDispatch('pin', windowAddress, ['action = "off"'])
: ['dispatch', 'pin', windowAddress],
);
} }
const roundedBounds = roundPlacementBounds(bounds); const roundedBounds = roundPlacementBounds(bounds);
if (roundedBounds) { if (roundedBounds) {
dispatches.push([ if (configProvider === 'lua') {
'dispatch', dispatches.push(
'movewindowpixel', luaWindowDispatch('move', windowAddress, [
`exact ${roundedBounds.x} ${roundedBounds.y},${windowAddress}`, `x = ${roundedBounds.x}`,
]); `y = ${roundedBounds.y}`,
dispatches.push([ ]),
'dispatch', );
'resizewindowpixel', dispatches.push(
`exact ${roundedBounds.width} ${roundedBounds.height},${windowAddress}`, luaWindowDispatch('resize', windowAddress, [
]); `x = ${roundedBounds.width}`,
dispatches.push(['dispatch', 'setprop', `${windowAddress} rounding 0`]); `y = ${roundedBounds.height}`,
dispatches.push(['dispatch', 'setprop', `${windowAddress} border_size 0`]); ]),
dispatches.push(['dispatch', 'setprop', `${windowAddress} no_shadow 1`]); );
dispatches.push(['dispatch', 'setprop', `${windowAddress} no_blur 1`]); dispatches.push(luaWindowSetProp(windowAddress, 'rounding', '0'));
dispatches.push(['dispatch', 'setprop', `${windowAddress} decorate 0`]); dispatches.push(luaWindowSetProp(windowAddress, 'border_size', '0'));
dispatches.push(luaWindowSetProp(windowAddress, 'no_shadow', '1'));
dispatches.push(luaWindowSetProp(windowAddress, 'no_blur', '1'));
dispatches.push(luaWindowSetProp(windowAddress, 'decorate', '0'));
} else {
dispatches.push([
'dispatch',
'movewindowpixel',
`exact ${roundedBounds.x} ${roundedBounds.y},${windowAddress}`,
]);
dispatches.push([
'dispatch',
'resizewindowpixel',
`exact ${roundedBounds.width} ${roundedBounds.height},${windowAddress}`,
]);
dispatches.push(['dispatch', 'setprop', `${windowAddress} rounding 0`]);
dispatches.push(['dispatch', 'setprop', `${windowAddress} border_size 0`]);
dispatches.push(['dispatch', 'setprop', `${windowAddress} no_shadow 1`]);
dispatches.push(['dispatch', 'setprop', `${windowAddress} no_blur 1`]);
dispatches.push(['dispatch', 'setprop', `${windowAddress} decorate 0`]);
}
} }
if (options.promote !== false) { if (options.promote !== false) {
dispatches.push(['dispatch', 'alterzorder', `top,${windowAddress}`]); dispatches.push(
configProvider === 'lua'
? luaWindowDispatch('alter_zorder', windowAddress, ['mode = "top"'])
: ['dispatch', 'alterzorder', `top,${windowAddress}`],
);
} }
return dispatches; return dispatches;
} }
function luaWindowDispatch(name: string, windowAddress: string, fields: string[]): string[] {
return [
'dispatch',
`hl.dsp.window.${name}({ ${[...fields, `window = ${luaString(windowAddress)}`].join(', ')} })`,
];
}
function luaWindowSetProp(windowAddress: string, prop: string, value: string): string[] {
return luaWindowDispatch('set_prop', windowAddress, [
`prop = ${luaString(prop)}`,
`value = ${luaString(value)}`,
]);
}
function luaString(value: string): string {
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
function roundPlacementBounds( function roundPlacementBounds(
bounds?: HyprlandPlacementBounds | null, bounds?: HyprlandPlacementBounds | null,
): HyprlandPlacementBounds | null { ): HyprlandPlacementBounds | null {
@@ -154,7 +207,9 @@ export function ensureHyprlandWindowFloatingByTitle(options: {
return false; return false;
} }
const configProvider = detectHyprlandConfigProvider(run);
const dispatches = buildHyprlandPlacementDispatches(client, options.bounds, { const dispatches = buildHyprlandPlacementDispatches(client, options.bounds, {
configProvider,
promote: options.promote, promote: options.promote,
}); });
for (const args of dispatches) { for (const args of dispatches) {
@@ -165,3 +220,27 @@ export function ensureHyprlandWindowFloatingByTitle(options: {
return false; return false;
} }
} }
function detectHyprlandConfigProvider(run: ExecFileSync): HyprlandConfigProvider {
try {
return parseHyprlandConfigProvider(
String(run('hyprctl', ['-j', 'status'], { encoding: 'utf-8' })),
);
} catch {
return 'hyprlang';
}
}
function parseHyprlandConfigProvider(output: string): HyprlandConfigProvider {
const payloadStart = output.indexOf('{');
if (payloadStart < 0) {
return 'hyprlang';
}
const parsed = JSON.parse(output.slice(payloadStart)) as unknown;
return isHyprlandStatusPayload(parsed) && parsed.configProvider === 'lua' ? 'lua' : 'hyprlang';
}
function isHyprlandStatusPayload(value: unknown): value is { configProvider?: unknown } {
return Boolean(value) && typeof value === 'object';
}