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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
// 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.
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import QtQuick
import "../.."
import "Mon.js" as M
// The connected monitors, read from hyprctl monitors -j into a draft. Apply
// runs each monitor's hl.monitor line through hyprctl eval, which applies it
// at once or refuses it (exit 7) and changes nothing; then Keep writes
// sections/monitors.lua, which this page owns whole, and Revert, or 15s with
// no answer, is hyprctl reload, which reapplies the untouched file. A
// setting that blanks a screen never reaches the file.
Module {
id: mm
name: "monitors"
label: "Monitors"
// nf-fa-desktop, present in Inconsolata Nerd Font.
icon: ""
settings: true
group: "Hyprland"
dirty: mm.changed
keywords: ["monitor", "display", "screen", "resolution", "refresh", "hz", "scale", "rotation", "vrr", "position", "layout"]
readonly property string file: (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config")
+ "/hypr/sections/monitors.lua"
// What hyprctl last reported, and the page's edit of it.
property var orig: []
property var draft: []
readonly property bool changed: JSON.stringify(mm.draft) !== JSON.stringify(mm.orig)
readonly property bool valid: mm.draft.every(d => /^\d+(\.\d+)?$/.test(d.scale) && +d.scale > 0)
// An Apply is live and waiting for Keep or Revert.
property bool pending: false
property int countdown: 0
property string error: ""
property string notice: ""
// The reload after a refusal must not replace the draft being fixed.
property bool skipRead: false
function refresh() { if (!mm.changed && !mm.pending) mm.read(); }
function read() {
mm.error = "";
readProc.running = false;
readProc.running = true;
}
property Process readProc: Process {
command: ["hyprctl", "monitors", "-j"]
stdout: StdioCollector {
onStreamFinished: {
let list;
try { list = JSON.parse(this.text); } catch (e) { return; }
mm.orig = list.map(M.draft);
mm.draft = list.map(M.draft);
}
}
}
// Keep's write, Revert's reload: both end in configreloaded.
property Connections reloads: Connections {
target: Hyprland
function onRawEvent(event) {
if (event.name !== "configreloaded" || mm.pending) return;
if (mm.skipRead) { mm.skipRead = false; return; }
mm.read();
}
}
// Changes one field of monitor i; a new array so bindings see it.
function set(i, key, value) {
const next = mm.draft.slice();
next[i] = Object.assign({}, next[i]);
next[i][key] = value;
mm.draft = next;
mm.notice = "";
}
function apply() {
mm.error = "";
mm.notice = "";
applyProc.command = ["sh", "-c", 'for l; do hyprctl eval "$l" || exit 7; done', "sh"]
.concat(mm.draft.map(M.lua));
applyProc.running = false;
applyProc.running = true;
}
property Process applyProc: Process {
stdout: StdioCollector { id: applyOut }
onExited: code => {
if (code !== 0) {
const line = applyOut.text.trim().split("\n").filter(l => l.startsWith("error:"))[0] || "";
mm.error = line.replace(/^error: .*?:1: /, "") || "Hyprland refused it.";
// An earlier monitor's eval may have gone through.
mm.skipRead = true;
Quickshell.execDetached(["hyprctl", "reload"]);
return;
}
mm.pending = true;
mm.countdown = 15;
tick.restart();
}
}
property Timer tick: Timer {
interval: 1000
repeat: true
onTriggered: if (--mm.countdown <= 0) mm.revert()
}
function keep() {
tick.stop();
mm.pending = false;
fv.setText(M.file(mm.draft));
mm.notice = "saved";
}
// Also what leaving the page while pending does.
function revert() {
tick.stop();
mm.pending = false;
Quickshell.execDetached(["hyprctl", "reload"]);
}
property FileView fv: FileView {
path: mm.file
atomicWrites: true
printErrors: false
}
Component.onCompleted: mm.read()
page: Component {
Page {
title: "Monitors"
actions: Component {
SaveBar { saveText: "Apply"; dirty: mm.changed && !mm.pending; valid: mm.valid
note: mm.valid ? "" : "Scale must be a positive number."
notice: mm.notice; error: mm.error; busy: mm.applyProc.running
onSave: mm.apply(); onDiscard: mm.read() }
}
MonitorsPage { width: parent.width; mod: mm }
}
}
}
|