diff --git a/README.md b/README.md index efeeb9d..2724654 100644 --- a/README.md +++ b/README.md @@ -62,3 +62,20 @@ comments and reloads while quick-visor is running. "radius": 12 } ``` + +## Persisting the layout + +Applying a layout runs `hyprctl keyword monitor` for each display, which takes +effect immediately but is lost when Hyprland restarts. + +To make changes survive a restart, quick-visor also rewrites +`~/.config/hypr/monitors.conf` **only if that file already exists** (the same +file generated by tools like nwg-displays and typically sourced from +`hyprland.conf`). If the file is absent, quick-visor leaves it alone and applies +the layout at runtime only. Add the following to your `hyprland.conf` to enable +persistence: + +```conf +source = ~/.config/hypr/monitors.conf +``` + diff --git a/VERSION b/VERSION index 81340c7..bbdeab6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.4 +0.0.5 diff --git a/qml/VisorService.qml b/qml/VisorService.qml index 4bc668f..047bfc2 100644 --- a/qml/VisorService.qml +++ b/qml/VisorService.qml @@ -320,26 +320,60 @@ QtObject { return "'" + String(value).replace(/'/g, "'\\''") + "'"; } + readonly property string monitorConfPath: "${XDG_CONFIG_HOME:-$HOME/.config}/hypr/monitors.conf" + + function monitorSpec(m) { + if (monitorEnabled(m)) { + return m.name + "," + modeForHyprland(monitorMode(m)) + "," + + monitorX(m) + "x" + monitorY(m) + "," + monitorScale(m); + } + return m.name + ",disable"; + } + + function pad2(value) { + return value < 10 ? "0" + value : String(value); + } + + function monitorConfContent() { + const now = new Date(); + const stamp = now.getFullYear() + "-" + pad2(now.getMonth() + 1) + "-" + pad2(now.getDate()) + + " at " + pad2(now.getHours()) + ":" + pad2(now.getMinutes()) + ":" + pad2(now.getSeconds()); + let body = "# Generated by quick-visor on " + stamp + ". Do not edit manually.\n\n"; + for (let i = 0; i < monitors.length; i++) { + body += "monitor=" + monitorSpec(monitors[i]) + "\n"; + } + return body; + } + function applyLayout() { if (!dirty || monitors.length === 0) return; const enableCommands = []; const disableCommands = []; + const dpmsCommands = []; for (let i = 0; i < monitors.length; i++) { const m = monitors[i]; + const command = "hyprctl keyword monitor " + shQuote(monitorSpec(m)); if (monitorEnabled(m)) { - const spec = m.name + "," + modeForHyprland(monitorMode(m)) + "," - + monitorX(m) + "x" + monitorY(m) + "," + monitorScale(m); - enableCommands.push("hyprctl keyword monitor " + shQuote(spec)); + enableCommands.push(command); + // Enabling a monitor that was previously off leaves DPMS off + // (blank/grey panel, invisible cursor), so force it on. + dpmsCommands.push("hyprctl dispatch dpms on " + shQuote(m.name)); } else { - disableCommands.push("hyprctl keyword monitor " + shQuote(m.name + ",disable")); + disableCommands.push(command); } } - const commands = enableCommands.concat(disableCommands); + const commands = enableCommands.concat(disableCommands).concat(dpmsCommands); if (commands.length === 0) return; + + // Persist to the Hyprland-sourced monitors.conf, but only if it already exists. + const persist = "CONF=\"" + monitorConfPath + "\"; " + + "if [ -f \"$CONF\" ]; then printf '%s' " + shQuote(monitorConfContent()) + " > \"$CONF\"; fi"; + const script = commands.join(" && ") + " && { " + persist + "; }"; + status = "Applying layout..."; - applyProc.command = ["sh", "-c", commands.join(" && ")]; + applyProc.command = ["sh", "-c", script]; applyProc.running = true; }