mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
291 lines
8.8 KiB
QML
291 lines
8.8 KiB
QML
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;
|
|
}
|
|
}
|
|
}
|
|
}
|