pragma Singleton import QtQuick import Quickshell import Quickshell.Io import qs.Commons import qs.Services.UI Singleton { id: root // Night Light properties - directly bound to settings readonly property var params: Settings.data.nightLight property var lastCommand: [] // Crash tracking for auto-restart property int _crashCount: 0 property int _maxCrashes: 5 // Manual schedule tracking property bool _manualNightPhase: false // Kill any stale wlsunset processes on startup to prevent issues after shell restart Component.onCompleted: { killStaleProcess.running = true; } Process { id: killStaleProcess running: false command: ["pkill", "-x", "wlsunset"] onExited: function (code, status) { if (code === 0) { Logger.i("NightLight", "Killed stale wlsunset process from previous session"); } // Now apply the settings after cleanup root.apply(); } } Timer { id: restartTimer interval: 2000 repeat: false onTriggered: { if (root.params.enabled && !runner.running) { Logger.w("NightLight", "Restarting after crash..."); if (root.isManualMode()) { root.applyManualSchedule(); } else { runner.running = true; } } } } Timer { id: manualScheduleTimer repeat: false onTriggered: { Logger.i("NightLight", "Manual schedule: phase boundary reached"); root.applyManualSchedule(); } } function timeToMinutes(timeStr) { var parts = timeStr.split(":").map(Number); return parts[0] * 60 + parts[1]; } function isManualMode() { return !params.forced && !params.autoSchedule; } function isCurrentlyNight() { var now = new Date(); var nowMin = now.getHours() * 60 + now.getMinutes(); var sunsetMin = timeToMinutes(params.manualSunset); var sunriseMin = timeToMinutes(params.manualSunrise); if (sunsetMin < sunriseMin) { // Inverted: e.g. sunset=03:00, sunrise=07:00 → night is [03:00, 07:00) return nowMin >= sunsetMin && nowMin < sunriseMin; } else { // Normal: e.g. sunset=18:00, sunrise=06:00 → night is [18:00, 06:00) return nowMin >= sunsetMin || nowMin < sunriseMin; } } function msUntilNextBoundary() { var now = new Date(); var nowMin = now.getHours() * 60 + now.getMinutes(); var sunsetMin = timeToMinutes(params.manualSunset); var sunriseMin = timeToMinutes(params.manualSunrise); var targetMin = isCurrentlyNight() ? sunriseMin : sunsetMin; var diffMin = targetMin - nowMin; if (diffMin <= 0) diffMin += 1440; return diffMin * 60 * 1000 - now.getSeconds() * 1000 - now.getMilliseconds(); } function applyManualSchedule() { if (!params.enabled) { manualScheduleTimer.stop(); runner.running = false; return; } var night = isCurrentlyNight(); _manualNightPhase = night; if (night) { var cmd = ["wlsunset"]; cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`); cmd.push("-S", "23:59"); cmd.push("-s", "00:00"); cmd.push("-d", 1); if (JSON.stringify(cmd) !== JSON.stringify(lastCommand) || !runner.running) { lastCommand = cmd; runner.command = cmd; runner.running = false; runner.running = true; } Logger.i("NightLight", "Manual schedule: night phase - wlsunset forced on"); } else { lastCommand = []; runner.running = false; Logger.i("NightLight", "Manual schedule: day phase - wlsunset stopped"); } var ms = msUntilNextBoundary(); manualScheduleTimer.interval = Math.max(ms, 1000); manualScheduleTimer.restart(); Logger.i("NightLight", "Manual schedule: next boundary in " + Math.round(ms / 1000) + "s"); } function apply(force = false) { // If using LocationService, wait for it to be ready if (!params.forced && params.autoSchedule && !LocationService.coordinatesReady) { return; } // Manual mode: handle scheduling ourselves if (isManualMode() && params.enabled) { _crashCount = 0; restartTimer.stop(); applyManualSchedule(); return; } // Not in manual mode - clean up manual timer manualScheduleTimer.stop(); var command = buildCommand(); // Compare with previous command to avoid unnecessary restart if (force || JSON.stringify(command) !== JSON.stringify(lastCommand)) { lastCommand = command; runner.command = command; // Set running to false so it may restart below if still enabled runner.running = false; } runner.running = params.enabled; } function buildCommand() { var cmd = ["wlsunset"]; if (params.forced) { // Force immediate full night temperature regardless of time // Keep distinct day/night temps but set times so we're effectively always in "night" cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`); // Night spans from sunset (00:00) to sunrise (23:59) covering almost the full day cmd.push("-S", "23:59"); // sunrise very late cmd.push("-s", "00:00"); // sunset at midnight // Near-instant transition cmd.push("-d", 1); } else if (params.autoSchedule) { cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`); cmd.push("-l", `${LocationService.stableLatitude}`, "-L", `${LocationService.stableLongitude}`); cmd.push("-d", 60 * 15); // 15min progressive fade at sunset/sunrise } return cmd; } // Observe setting changes and location readiness Connections { target: Settings.data.nightLight function onEnabledChanged() { apply(); // Toast: night light toggled const enabled = !!Settings.data.nightLight.enabled; ToastService.showNotice(I18n.tr("common.night-light"), enabled ? I18n.tr("common.enabled") : I18n.tr("common.disabled"), enabled ? "nightlight-on" : "nightlight-off"); } function onForcedChanged() { apply(); if (Settings.data.nightLight.enabled) { ToastService.showNotice(I18n.tr("common.night-light"), Settings.data.nightLight.forced ? I18n.tr("toast.night-light.forced") : I18n.tr("toast.night-light.normal"), Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on"); } } function onNightTempChanged() { apply(); } function onDayTempChanged() { apply(); } function onManualSunriseChanged() { apply(); } function onManualSunsetChanged() { apply(); } function onAutoScheduleChanged() { apply(); } } Connections { target: LocationService function onCoordinatesReadyChanged() { if (LocationService.coordinatesReady) { root.apply(); } } } Timer { id: resumeRetryTimer interval: 2000 repeat: false onTriggered: { Logger.i("NightLight", "Resume retry - re-applying night light again"); root.apply(true); } } Connections { target: Time function onResumed() { Logger.i("NightLight", "System resumed - re-applying night light"); root.apply(true); resumeRetryTimer.restart(); } } // Foreground process runner Process { id: runner running: false onStarted: { Logger.i("NightLight", "Wlsunset started:", runner.command); // Reset crash count on successful start if (root._crashCount > 0) { root._crashCount = 0; } } onExited: function (code, status) { if (root.params.enabled && root.isManualMode()) { // Manual mode: only treat as crash if we're in the night phase if (root._manualNightPhase) { root._crashCount++; if (root._crashCount <= root._maxCrashes) { Logger.w("NightLight", "Wlsunset exited unexpectedly during manual night phase (code: " + code + "), restarting in 2s... (attempt " + root._crashCount + "/" + root._maxCrashes + ")"); restartTimer.start(); } else { Logger.e("NightLight", "Wlsunset crashed too many times (" + root._maxCrashes + "), giving up"); } } else { Logger.i("NightLight", "Wlsunset exited (manual day phase):", code, status); root._crashCount = 0; } } else if (root.params.enabled) { // Non-manual mode: any exit while enabled is a crash root._crashCount++; if (root._crashCount <= root._maxCrashes) { Logger.w("NightLight", "Wlsunset exited unexpectedly (code: " + code + "), restarting in 2s... (attempt " + root._crashCount + "/" + root._maxCrashes + ")"); restartTimer.start(); } else { Logger.e("NightLight", "Wlsunset crashed too many times (" + root._maxCrashes + "), giving up"); } } else { Logger.i("NightLight", "Wlsunset exited (disabled):", code, status); root._crashCount = 0; } } } }