// Copyright (C) 2026 Danilo M. // // 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 } } } }