aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-10-30-monitors-page-design.md
blob: 3d06740ae9ba036ec726f379a1de76e9dc2ed537 (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
# Monitors: a settings page for the outputs

Date: 2026-10-30

## Goal

Set up the connected monitors from the settings window: mode, scale,
rotation, VRR and position, with a to-scale layout to drag them into place.
A change is tried live and only written once kept, so a setting that blanks
a screen never reaches the config.

## Where it lives

`desktop/modules/monitors/`, a settings `Module` in the Hyprland group of the
sidebar, beside Keyboard shortcuts and Options:

- `MonitorsModule.qml`: reads `hyprctl monitors -j`, holds the draft, runs
  the eval, writes the file, runs the revert.
- `MonitorsPage.qml`: the layout canvas and one card per monitor.
- `Mon.js`: pure helpers, no QML: the Lua for a monitor and for the whole
  file, logical size, snapping, mode deduplication.
- `test_mon.js`: `node desktop/modules/monitors/test_mon.js`, the one check.
- `README.md`.

## The file

The page owns `~/.config/hypr/sections/monitors.lua` (in `hypr-theme`,
required by `hyprland.lua`) and regenerates it whole: a header saying the
keystone settings window writes it, then one `hl.monitor({ ... })` block per
connected output, in `hyprctl monitors` order, with the fields `output`,
`mode`, `position`, `scale`, `transform`, and `vrr` only when on. Only
connected outputs are written; a monitor that is unplugged when the page
saves drops out of the file. Hand comments in the current file (two) are
not kept.

## Draft

Read from `hyprctl monitors -j` each time the page opens and after every
`configreloaded` raw event, unless an Apply is pending. Per monitor:

- `mode`: `WxH@R` from `availableModes`, duplicates removed (the ASUS lists
  `1920x1080@120.00Hz` twice). Shown with refresh to two decimals. The
  current mode is matched against the list by width, height and the nearest
  refresh, since `refreshRate` reads `99.94300` against `99.94Hz`.
- `scale`: a field, any number Hyprland accepts.
- `transform`: 0, 90, 180, 270 and a flipped toggle, stored as 0..7.
- `vrr`: a toggle.
- `position`: one of `auto`, `auto-left`, `auto-right`, `auto-up`,
  `auto-down`, `auto-center-left`, `auto-center-right`, `auto-center-up`,
  `auto-center-down`, or exact `XxY`. hyprctl only reports the resolved
  `x`/`y`, so the page starts from the exact position; picking an auto value
  is a choice in the card, and a drag always sets exact coordinates.

Dirty when any field differs from what was read. SaveBar carries Apply and
Discard.

## Layout canvas

Each monitor is a rectangle at its logical size: width and height swapped
for an odd transform, both divided by scale. The whole arrangement is fitted
to the canvas with a margin. Each rectangle shows the output name and model.
Dragging moves one; on release its position snaps so that a near edge (within
a threshold in logical pixels) lies on an edge of another monitor, and it
becomes exact `XxY` in the draft, rounded to integers. A monitor set to an
auto position is drawn where Hyprland last put it, and dragging it switches
it to exact.

## Apply, keep, revert

1. Apply runs `hyprctl eval` with every monitor's `hl.monitor` line in one
   call. Exit 7 is a refusal: nothing changed, the error text shows in
   SaveBar's `error`, the draft stays dirty.
2. On success the change is live. A bar shows "Keep these settings?
   Reverting in 15s", counting down, with Keep and Revert.
3. Keep writes `monitors.lua` through `FileView.setText`; Hyprland's
   autoreload rereads it, which is now the same settings.
4. Revert, or the countdown reaching zero, runs `hyprctl reload`, which
   reapplies the untouched file. The draft is reread from hyprctl.

Closing the settings window while pending counts as Revert. If the shell dies
while pending, the file is still the old one and the next reload restores it.

## Not handled

Disabling an output, mirroring, bit depth and colour management; settings for
an output that is not connected; renaming outputs. The island and the window
switcher's hot corner are tied to DP-1 by name, which a move does not affect.

## Check

`test_mon.js` asserts: the Lua emitted for a monitor, including `vrr` only
when on; the logical size of a rotated and of a scaled monitor; a drag within
the threshold snapping to the other monitor's edge and one outside it not
snapping; duplicate modes collapsing; the nearest-refresh match picking
`99.94` for `99.943`. The eval refusal path was measured by hand: `transform
= 9` exits 7 with `value 9 is more than the maximum of 7`.