mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-29 00:55:15 -07:00
fix(overlay): use Lua dispatch syntax for Hyprland 0.55+ Lua configs
- Detect `configProvider: "lua"` via `hyprctl -j status` at placement time - Emit `hl.dsp.window.*` Lua dispatcher calls instead of legacy hyprlang syntax - Fall back to hyprlang if status call fails or returns non-lua provider
This commit is contained in:
@@ -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" })'],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user