mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
158 lines
4.2 KiB
TypeScript
158 lines
4.2 KiB
TypeScript
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;
|
|
}
|
|
|
|
export interface HyprlandPlacementBounds {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
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,
|
|
bounds?: HyprlandPlacementBounds | null,
|
|
): 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]);
|
|
}
|
|
const roundedBounds = roundPlacementBounds(bounds);
|
|
if (roundedBounds) {
|
|
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`]);
|
|
}
|
|
dispatches.push(['dispatch', 'alterzorder', `top,${windowAddress}`]);
|
|
return dispatches;
|
|
}
|
|
|
|
function roundPlacementBounds(
|
|
bounds?: HyprlandPlacementBounds | null,
|
|
): HyprlandPlacementBounds | null {
|
|
if (!bounds) {
|
|
return null;
|
|
}
|
|
const rounded = {
|
|
x: Math.round(bounds.x),
|
|
y: Math.round(bounds.y),
|
|
width: Math.round(bounds.width),
|
|
height: Math.round(bounds.height),
|
|
};
|
|
return Number.isFinite(rounded.x) &&
|
|
Number.isFinite(rounded.y) &&
|
|
Number.isFinite(rounded.width) &&
|
|
Number.isFinite(rounded.height) &&
|
|
rounded.width > 0 &&
|
|
rounded.height > 0
|
|
? rounded
|
|
: null;
|
|
}
|
|
|
|
export function ensureHyprlandWindowFloatingByTitle(options: {
|
|
title: string;
|
|
bounds?: HyprlandPlacementBounds | null;
|
|
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, options.bounds);
|
|
for (const args of dispatches) {
|
|
run('hyprctl', args, { stdio: 'ignore' });
|
|
}
|
|
return dispatches.length > 0;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|