Audited bar, dock, quick settings, date menu and Settings for three things: a setting reachable in one UI but not another, a setting that exists but is unreachable anywhere, and UI that states something false. Night Light was fully exposed in Quick Settings and had no control anywhere in Settings. It now has a card on Displays, where GNOME also puts it, with on/off, schedule, times and temperature. Adding those controls would have shipped the exact defect this audit exists to find. NightLight declared enabled, temperature and automatic as bindings on the store, but toggle() assigns to them, and an assignment destroys a QML binding permanently -- so the service wrote to the store and never read from it again. The Settings controls would have written values the service ignored, while Quick Settings kept working. It now follows the store. Every other service was swept for the same pattern; this was the only one. The night light schedule was two hardcoded literals, so the hours could not be changed. They are schema keys now, with a row that renders 17.5 as "5:30 PM" and honours the 24-hour preference rather than showing a decimal nobody reads as a time. keyboardLayout was in the schema and read by input.lua but had no control anywhere: configurable in principle, unreachable in practice. It is surfaced on Input & Shortcuts as read-only, with the reason, because it needs a compositor reload and a control implying instant apply would be a smaller lie but still a lie. Caffeine was a Quick Settings toggle mentioned only in a subtitle in Settings. It has a real control now. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
135 lines
5.6 KiB
QML
135 lines
5.6 KiB
QML
pragma Singleton
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Night light — a thin wrapper around hyprsunset (0.4.0).
|
|
//
|
|
// hyprsunset is a daemon: `hyprsunset -t <kelvin>` grabs wlr-gamma-control and
|
|
// holds it until it exits, and `hyprctl hyprsunset <request>` 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)
|
|
}
|