# 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`.