aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/monitors/Mon.js
blob: 865e155dd06bcfebf398e9af228b321aac008654 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
.pragma library
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// Pure helpers for the Monitors page, tested by test_mon.js under node.
// No String.matchAll, Object.fromEntries or flatMap: the shell's engine has
// none of them (AGENTS.md).

// Hyprland's automatic positions, as the card offers them.
const AUTO = ["auto", "auto-left", "auto-right", "auto-up", "auto-down",
              "auto-center-left", "auto-center-right", "auto-center-up", "auto-center-down"];

// availableModes ("1920x1080@120.00Hz") without the suffix, duplicates
// dropped: the ASUS lists 120Hz twice.
function modes(avail) {
    const out = [];
    for (const a of avail) {
        const m = a.replace(/Hz$/, "");
        if (!out.includes(m)) out.push(m);
    }
    return out;
}

// The entry of list at w x h with the refresh nearest r: refreshRate reads
// 99.943 against a listed 99.94. Built from the numbers when none matches.
function current(list, w, h, r) {
    let best = "", dist = Infinity;
    for (const m of list) {
        const p = /^(\d+)x(\d+)@([\d.]+)$/.exec(m);
        if (!p || +p[1] !== w || +p[2] !== h) continue;
        const e = Math.abs(+p[3] - r);
        if (e < dist) { dist = e; best = m; }
    }
    return best || w + "x" + h + "@" + r.toFixed(2);
}

// A monitor from hyprctl monitors -j as the page edits it. x and y are
// where Hyprland put it, kept to draw an auto position.
function draft(m) {
    const list = modes(m.availableModes);
    return { name: m.name, make: m.make, model: m.model, modes: list,
             mode: current(list, m.width, m.height, m.refreshRate),
             scale: String(m.scale), transform: m.transform, vrr: !!m.vrr,
             position: m.x + "x" + m.y, x: m.x, y: m.y };
}

// Logical size: an odd transform is a quarter turn, scale divides. A scale
// being typed ("", "1.") draws at 1 until it is a positive number.
function size(mode, scale, transform) {
    const p = /^(\d+)x(\d+)/.exec(mode);
    const k = +scale > 0 ? +scale : 1;
    let w = +p[1], h = +p[2];
    if (transform % 2) { const t = w; w = h; h = t; }
    return { w: w / k, h: h / k };
}

// The layout position: exact XxY, or where Hyprland last put an auto one.
function xy(d) {
    const p = /^(-?\d+)x(-?\d+)$/.exec(d.position);
    return p ? { x: +p[1], y: +p[2] } : { x: d.x, y: d.y };
}

// One monitor as one Lua expression, for hyprctl eval and the file alike.
function lua(d) {
    return 'hl.monitor({ output = "' + d.name + '", mode = "' + d.mode + '", position = "' + d.position
        + '", scale = "' + d.scale + '", transform = ' + d.transform + (d.vrr ? ", vrr = 1" : "") + " })";
}

const HEADER = "-- Monitors, written by the keystone settings window (Hyprland > Monitors).\n"
    + "-- Rewritten whole on Keep: change them there, an edit here is replaced.\n";

function file(list) {
    return HEADER + list.map(d => "\n-- " + d.name + ": " + d.make + " " + d.model + "\n" + lua(d) + "\n").join("");
}

// The candidate within t of v nearest to it, else v.
function nearest(v, cands, t) {
    let best = v, dist = t;
    for (const c of cands) {
        if (Math.abs(c - v) <= dist) { dist = Math.abs(c - v); best = c; }
    }
    return best;
}

// r ({x, y, w, h}, logical) moved so an edge within t lies on an edge of one
// of others: beside it, or aligned with it. Each axis on its own, rounded.
// ponytail: an axis snaps to a monitor however far off it is on the other
// axis, and overlaps are not refused; right for two monitors, check both
// axes together if a third is added.
function snap(r, others, t) {
    const xs = [], ys = [];
    for (const o of others) {
        xs.push(o.x - r.w, o.x + o.w, o.x, o.x + o.w - r.w);
        ys.push(o.y - r.h, o.y + o.h, o.y, o.y + o.h - r.h);
    }
    return { x: Math.round(nearest(r.x, xs, t)), y: Math.round(nearest(r.y, ys, t)) };
}