pragma Singleton // ───────────────────────────────────────────────────────────────────────────── // Night light — a thin wrapper around hyprsunset (0.4.0). // // hyprsunset is a daemon: `hyprsunset -t ` grabs wlr-gamma-control and // holds it until it exits, and `hyprctl hyprsunset ` re-tunes the // running instance over its socket. So the shape here is: // * `active` drives whether the daemon process runs at all, // * temperature changes are pushed to the *running* daemon rather than // restarting it, which would flash the screen back to 6500K. // // Gamma is restored by the compositor when the daemon's gamma-control object // dies, so stopping the Process is a complete "off" — no `-i` pass needed. // // Only works under Hyprland; there is no gamma protocol on GNOME, so the // daemon exits immediately there. That is reported once, not retried. // ───────────────────────────────────────────────────────────────────────────── import Quickshell import Quickshell.Io import QtQuick import qs.config Singleton { id: root property bool initialized: false // The manual switch. Ignored while `automatic` is on. property bool enabled: Settings.nightLightEnabledByDefault property int temperature: Settings.nightLightTemperature // Follow Settings.nightLightFrom .. nightLightTo instead of the manual // switch. Off by default because GNOME's schedule was disabled. property bool automatic: DesktopPreferences.get("nightLightAutomatic") // What is actually applied right now. readonly property bool active: root.automatic ? root.scheduled : root.enabled // True while the wall clock is inside the scheduled window. readonly property bool scheduled: root.inWindow(clock.hours + clock.minutes / 60) // A manual toggle always wins: it drops out of the schedule rather than // being silently reverted a minute later. Same as GNOME's behaviour when // you flip night light off during a scheduled evening. function toggle(): void { if (root.automatic) { root.automatic = false; root.enabled = !root.scheduled; } else { root.enabled = !root.enabled; } } // The window wraps midnight (17:00 → 10:00), so the comparison flips when // `from` is later in the day than `to`. function inWindow(hour: real): bool { const from = Settings.nightLightFrom; const to = Settings.nightLightTo; return from <= to ? (hour >= from && hour < to) : (hour >= from || hour < to); } // Only ticks while the schedule is in charge — no idle work otherwise. SystemClock { id: clock precision: SystemClock.Minutes enabled: root.automatic } Process { id: daemon command: ["hyprsunset", "-t", String(root.temperature)] running: root.active onExited: (code, status) => { if (root.active) console.warn("NightLight: hyprsunset exited with", code, "- is Hyprland running?"); } } // Re-tune in place; restarting the daemon would flash the display. onTemperatureChanged: { DesktopPreferences.set("nightLightTemperature", root.temperature); if (daemon.running) Quickshell.execDetached(["hyprctl", "hyprsunset", "temperature", String(root.temperature)]); } onEnabledChanged: DesktopPreferences.set("nightLightEnabled", root.enabled) onAutomaticChanged: DesktopPreferences.set("nightLightAutomatic", root.automatic) // `enabled`, `temperature`, and `automatic` are declared as bindings on the // store, but a binding is destroyed the moment anything assigns to the // property -- which toggle() does. Without this, the service would write to // the store and never read from it again: Quick Settings would keep working // while the same settings on the Displays page silently did nothing, which // is worse than not offering them there at all. // // No loop: set() is a no-op when the value is unchanged, and assigning a // property its current value emits nothing, so this converges immediately. Connections { target: DesktopPreferences function onRevisionChanged(): void { root.syncFromStore(); } } function syncFromStore(): void { const storedEnabled = DesktopPreferences.get("nightLightEnabled") === true; if (root.enabled !== storedEnabled) root.enabled = storedEnabled; const storedAutomatic = DesktopPreferences.get("nightLightAutomatic") === true; if (root.automatic !== storedAutomatic) root.automatic = storedAutomatic; const storedTemperature = DesktopPreferences.get("nightLightTemperature"); if (typeof storedTemperature === "number" && root.temperature !== storedTemperature) root.temperature = storedTemperature; } onActiveChanged: { if (!root.initialized) return; StatusEvents.publish({ key: "display-night-light", glyph: "\u{F0F31}", title: root.active ? "Night Light on" : "Night Light off", detail: root.active ? String(root.temperature) + " K" : "Display colors restored", tone: root.active ? "warn" : "accent", priority: StatusEvents.ambientPriority }); } Component.onCompleted: Qt.callLater(() => root.initialized = true) }