Files
Gabriel Brown d96863b687 Convert British spellings to American across the repo
colour -> color, behaviour -> behavior, centre -> center, favourite ->
favorite, and about twenty other pairs, applied consistently across
comments, docs, error/UI copy, and a handful of QML identifiers that
used the British spelling as their actual name: SystemSettings'
serialiseValue/serialiseTable/normaliseGradient, Displays'
normaliseModes, Wallpaper's normalisePolicy, SettingsBackup's
serialiseHomeState, DateTime's ntpSynchronised property, Clipboard's
_normalise helper, and ShortcutCapture's cancelled signal (with its
onCancelled handler in ShortcutsPage.qml). Every call site and the two
tests that assert on the literal source text (settings-ownership and
settings-backup-live contracts) were updated in lockstep.

Left untouched: config/dot/espanso/match/packages/misspell-en/ is a
vendored third-party autocorrect dictionary -- its entries are typo
corrections, not our prose, and rewriting them would fight the
package's own purpose (and any future re-sync from upstream).

The already-American `favorites` property (Home page pinned
accessories) was never actually misspelled -- only nearby comments and
error strings said "favourites" -- so no data migration was needed
there.

Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
2026-08-19 08:07:55 -04:00

152 lines
6.3 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
property bool suppressStatusEvents: 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 behavior 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;
}
}
// Launcher actions already provide immediate Prism OSD feedback. Keep the
// ambient Signal Glass channel quiet for that path so one choice produces
// one confirmation instead of two competing transients.
function toggleQuietly(): void {
root.suppressStatusEvents = true;
root.toggle();
root.suppressStatusEvents = false;
}
function restore(manualEnabled: bool, automaticEnabled: bool): void {
root.suppressStatusEvents = true;
root.enabled = manualEnabled;
root.automatic = automaticEnabled;
root.suppressStatusEvents = false;
}
// 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 || root.suppressStatusEvents)
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)
}