From 2a686b55c4e495f2325c949ba822474b0102076c Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 28 Aug 2025 15:34:47 +0200 Subject: [PATCH] Replace our NightLight solution with wlsunset. NightLight: add temperature solution NTextInput: add input hint support --- Commons/Settings.qml | 3 + Modules/Bar/Widgets/NightLight.qml | 23 ++-- Modules/NightLight/NightLightOverlay.qml | 46 -------- Modules/SettingsPanel/Tabs/DisplayTab.qml | 64 ++++++++++- README.md | 2 +- Services/NightLightService.qml | 129 ++++++++++++++-------- Widgets/NCheckbox.qml | 26 ++--- Widgets/NTextInput.qml | 1 + shell.qml | 3 +- 9 files changed, 167 insertions(+), 130 deletions(-) delete mode 100644 Modules/NightLight/NightLightOverlay.qml diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 8f8c65f..8f4b74b 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -275,6 +275,9 @@ Singleton { property string startTime: "20:00" property string stopTime: "07:00" property bool autoSchedule: false + // wlsunset temperatures (Kelvin) + property int lowTemp: 3500 + property int highTemp: 6500 } } } diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index 845e85f..5fe70da 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -17,21 +17,19 @@ Item { NPill { id: pill - icon: NightLightService.isActive ? "bedtime" : "bedtime_off" - iconCircleColor: NightLightService.isActive ? Color.mSecondary : Color.mOnSurfaceVariant - collapsedIconColor: NightLightService.isActive ? Color.mOnSecondary : Color.mOnSurface + icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off" + iconCircleColor: Settings.data.nightLight.enabled ? Color.mSecondary : Color.mOnSurfaceVariant + collapsedIconColor: Settings.data.nightLight.enabled ? Color.mOnSecondary : Color.mOnSurface autoHide: false - text: NightLightService.isActive ? "On" : "Off" + text: Settings.data.nightLight.enabled ? "On" : "Off" tooltipText: { if (!Settings.isLoaded || !Settings.data.nightLight.enabled) { return "Night Light: Disabled\nLeft click to open settings.\nRight click to enable." } - var status = NightLightService.isActive ? "Active" : "Inactive (outside schedule)" var intensity = Math.round(Settings.data.nightLight.intensity * 100) - var schedule = Settings.data.nightLight.autoSchedule ? `Schedule: ${Settings.data.nightLight.startTime} - ${Settings.data.nightLight.stopTime}` : "Manual mode" - - return `Intensity: ${intensity}%\n${schedule}\nLeft click to open settings.\nRight click to toggle.` + var schedule = Settings.data.nightLight.autoSchedule ? `Auto schedule` : `Manual: ${Settings.data.nightLight.startTime} - ${Settings.data.nightLight.stopTime}` + return `Night Light: Enabled\nIntensity: ${intensity}%\n${schedule}\nLeft click to open settings.\nRight click to toggle.` } onClicked: { @@ -42,14 +40,11 @@ Item { } onRightClicked: { - // Right click - toggle night light + // Right click - toggle night light (debounced apply handled by service) Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled + NightLightService.apply() } - onWheel: delta => { - var diff = delta > 0 ? 0.05 : -0.05 - Settings.data.nightLight.intensity = Math.max(0, Math.min(1.0, - Settings.data.nightLight.intensity + diff)) - } + // Wheel handler removed to avoid frequent rapid restarts/flicker } } diff --git a/Modules/NightLight/NightLightOverlay.qml b/Modules/NightLight/NightLightOverlay.qml deleted file mode 100644 index 8bb74bc..0000000 --- a/Modules/NightLight/NightLightOverlay.qml +++ /dev/null @@ -1,46 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Wayland -import qs.Commons -import qs.Services - -Variants { - model: Quickshell.screens - - delegate: Loader { - required property ShellScreen modelData - readonly property real scaling: ScalingService.scale(modelData) - - active: NightLightService.isActive - - sourceComponent: PanelWindow { - screen: modelData - color: Color.transparent - anchors { - top: true - bottom: true - left: true - right: true - } - - // Ensure a full click through - mask: Region {} - - WlrLayershell.layer: WlrLayershell.Overlay - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - WlrLayershell.namespace: "noctalia-nightlight" - - Rectangle { - anchors.fill: parent - color: NightLightService.overlayColor - - Behavior on color { - ColorAnimation { - duration: Style.animationSlow - } - } - } - } - } -} diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index 3236bcb..7f2c217 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -239,7 +239,10 @@ ColumnLayout { label: "Enable Night Light" description: "Apply a warm color filter to reduce blue light emission." checked: Settings.data.nightLight.enabled - onToggled: checked => Settings.data.nightLight.enabled = checked + onToggled: checked => { + Settings.data.nightLight.enabled = checked + NightLightService.apply() + } } // Intensity settings @@ -257,7 +260,10 @@ ColumnLayout { to: 1 stepSize: 0.01 value: Settings.data.nightLight.intensity - onMoved: Settings.data.nightLight.intensity = value + onMoved: { + Settings.data.nightLight.intensity = value + NightLightService.apply() + } Layout.fillWidth: true Layout.minimumWidth: 150 * scaling } @@ -271,11 +277,59 @@ ColumnLayout { } } + // Temperature settings (inline like schedule) + RowLayout { + visible: Settings.data.nightLight.enabled + Layout.fillWidth: false + spacing: Style.marginM * scaling + + NText { + text: "Low" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + } + NTextInput { + text: Settings.data.nightLight.lowTemp.toString() + inputMethodHints: Qt.ImhDigitsOnly + Layout.preferredWidth: 100 * scaling + onEditingFinished: { + var v = parseInt(text) + if (!isNaN(v)) { + Settings.data.nightLight.lowTemp = Math.max(1000, Math.min(6500, v)) + NightLightService.apply() + } + } + } + + Item {} + + NText { + text: "High" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + } + NTextInput { + text: Settings.data.nightLight.highTemp.toString() + inputMethodHints: Qt.ImhDigitsOnly + Layout.preferredWidth: 100 * scaling + onEditingFinished: { + var v = parseInt(text) + if (!isNaN(v)) { + Settings.data.nightLight.highTemp = Math.max(1000, Math.min(10000, v)) + NightLightService.apply() + } + } + } + } + NToggle { label: "Auto Schedule" description: "Automatically enable night light based on time schedule." checked: Settings.data.nightLight.autoSchedule - onToggled: checked => Settings.data.nightLight.autoSchedule = checked + onToggled: checked => { + Settings.data.nightLight.autoSchedule = checked + NightLightService.apply() + } visible: Settings.data.nightLight.enabled } @@ -303,7 +357,7 @@ ColumnLayout { model: timeOptions currentKey: Settings.data.nightLight.startTime placeholder: "Select start time" - onSelected: key => Settings.data.nightLight.startTime = key + onSelected: key => { Settings.data.nightLight.startTime = key; NightLightService.apply() } preferredWidth: 120 * scaling } @@ -319,7 +373,7 @@ ColumnLayout { model: timeOptions currentKey: Settings.data.nightLight.stopTime placeholder: "Select stop time" - onSelected: key => Settings.data.nightLight.stopTime = key + onSelected: key => { Settings.data.nightLight.stopTime = key; NightLightService.apply() } preferredWidth: 120 * scaling } } diff --git a/README.md b/README.md index f8649cf..916b2f5 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,6 @@ Features a modern modular architecture with a status bar, notification system, c - `gpu-screen-recorder` - Screen recording functionality - `brightnessctl` - For internal/laptop monitor brightness - `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors) -- `xdg-desktop-portal-gnome` - Desktop integration (or alternative portal) ### Optional @@ -79,6 +78,7 @@ Features a modern modular architecture with a status bar, notification system, c - `swww` - Wallpaper animations and effects - `matugen` - Material You color scheme generation - `cava` - Audio visualizer component +- `wlsunset` - To be able to use NightLight > There are 2 more optional dependencies. > Any `polkit agent` to be able to use the ArchUpdater widget. diff --git a/Services/NightLightService.qml b/Services/NightLightService.qml index f94e409..d2fe5a8 100644 --- a/Services/NightLightService.qml +++ b/Services/NightLightService.qml @@ -4,63 +4,106 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons +import qs.Services Singleton { id: root // Night Light properties - directly bound to settings readonly property var params: Settings.data.nightLight + // Deprecated overlay flag removed; service only manages wlsunset now + property bool isActive: false + property bool isRunning: false + property string lastCommand: "" + property var nextCommand: [] - // Computed properties - readonly property color overlayColor: params.enabled ? calculateOverlayColor() : "transparent" - property bool isActive: params.enabled && (params.autoSchedule ? isWithinSchedule() : true) + Component.onCompleted: apply() - Component.onCompleted: { - Logger.log("NightLight", "Service started") - } - - function calculateOverlayColor() { - if (!isActive) { - return "transparent" - } - - // More vibrant color formula - stronger effect at high warmth - var red = 1.0 - var green = 1.0 - (0.43 * params.intensity) - var blue = 1.0 - (0.84 * params.intensity) - var alpha = (params.intensity * 0.25) // Higher alpha for more noticeable effect - - return Qt.rgba(red, green, blue, alpha) - } - - function isWithinSchedule() { - if (!params.autoSchedule) { - return true - } - - var now = new Date() - var currentTime = now.getHours() * 60 + now.getMinutes() - - var startParts = params.startTime.split(":") - var stopParts = params.stopTime.split(":") - var startMinutes = parseInt(startParts[0]) * 60 + parseInt(startParts[1]) - var stopMinutes = parseInt(stopParts[0]) * 60 + parseInt(stopParts[1]) - - // Handle overnight schedule (e.g., 20:00 to 07:00) - if (stopMinutes < startMinutes) { - return currentTime >= startMinutes || currentTime <= stopMinutes + function buildCommand() { + var cmd = ["wlsunset"] + // Use user-configured temps; if intensity is used, bias lowTemp towards user low + var i = Math.max(0, Math.min(1, params.intensity)) + var loCfg = params.lowTemp || 3500 + var hiCfg = params.highTemp || 6500 + var lowTemp = Math.round(hiCfg - (hiCfg - loCfg) * Math.pow(i, 0.6)) + cmd.push("-t", lowTemp.toString()) + cmd.push("-T", hiCfg.toString()) + if (params.autoSchedule && LocationService.data.coordinatesReady && LocationService.data.stableLatitude !== "" && LocationService.data.stableLongitude !== "") { + cmd.push("-l", LocationService.data.stableLatitude) + cmd.push("-L", LocationService.data.stableLongitude) } else { - return currentTime >= startMinutes && currentTime <= stopMinutes + // Manual schedule + if (params.startTime && params.stopTime) { + cmd.push("-S", params.startTime) + cmd.push("-s", params.stopTime) + } + // Optional: do not pass duration, use wlsunset defaults } + return cmd } - // Timer to check schedule changes + function stopIfRunning() { + // Best-effort stop; wlsunset runs as foreground, so pkill is simplest + Quickshell.execDetached(["pkill", "-x", "wlsunset"]) + isRunning = false + } + + function apply() { + if (!params.enabled) { + // Disable immediately + debounceStart.stop() + nextCommand = [] + stopIfRunning() + return + } + // Debounce rapid changes (slider) + nextCommand = buildCommand() + lastCommand = nextCommand.join(" ") + stopIfRunning() + debounceStart.restart() + } + + // Observe setting changes and location readiness + Connections { + target: Settings.data.nightLight + function onEnabledChanged() { apply() } + function onIntensityChanged() { apply() } + function onAutoScheduleChanged() { apply() } + function onStartTimeChanged() { apply() } + function onStopTimeChanged() { apply() } + } + + Connections { + target: LocationService.data + function onCoordinatesReadyChanged() { if (params.enabled && params.autoSchedule) apply() } + function onStableLatitudeChanged() { if (params.enabled && params.autoSchedule) apply() } + function onStableLongitudeChanged() { if (params.enabled && params.autoSchedule) apply() } + } + + // Foreground process runner + Process { + id: runner + running: false + onStarted: { isRunning = true; Logger.log("NightLight", "Started wlsunset:", root.lastCommand) } + onExited: function (code, status) { + isRunning = false + Logger.log("NightLight", "wlsunset exited:", code, status) + // Do not auto-restart here; debounceStart handles starts + } + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + // Debounce timer to avoid flicker when moving sliders Timer { - interval: 60000 // Check every minute - running: params.enabled && params.autoSchedule - repeat: true + id: debounceStart + interval: 300 + repeat: false onTriggered: { - isActive = isWithinSchedule() + if (params.enabled && nextCommand.length > 0) { + runner.command = nextCommand + runner.running = true + } } } } diff --git a/Widgets/NCheckbox.qml b/Widgets/NCheckbox.qml index ee2beaf..3b85d0d 100644 --- a/Widgets/NCheckbox.qml +++ b/Widgets/NCheckbox.qml @@ -12,7 +12,7 @@ RowLayout { property bool checked: false property bool hovering: false // Smaller default footprint than NToggle - property int baseSize: Math.max(Style.baseWidgetSize * 0.8, Math.round(14 / scaling)) + property int baseSize: Math.max(Style.baseWidgetSize * 0.8, 14) signal toggled(bool checked) signal entered @@ -47,26 +47,14 @@ RowLayout { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true - onEntered: { - hovering = true - root.entered() - } - onExited: { - hovering = false - root.exited() - } + onEntered: { hovering = true; root.entered() } + onExited: { hovering = false; root.exited() } onClicked: root.toggled(!root.checked) } - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - Behavior on border.color { - ColorAnimation { - duration: Style.animationFast - } - } + Behavior on color { ColorAnimation { duration: Style.animationFast } } + Behavior on border.color { ColorAnimation { duration: Style.animationFast } } } } + + diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index bc1a955..6fd56a9 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -14,6 +14,7 @@ Item { property alias text: input.text property alias placeholderText: input.placeholderText + property alias inputMethodHints: input.inputMethodHints signal editingFinished diff --git a/shell.qml b/shell.qml index c62e8a2..4b5f314 100644 --- a/shell.qml +++ b/shell.qml @@ -21,7 +21,6 @@ import qs.Modules.Calendar import qs.Modules.Dock import qs.Modules.IPC import qs.Modules.LockScreen -import qs.Modules.NightLight import qs.Modules.Notification import qs.Modules.SettingsPanel import qs.Modules.PowerPanel @@ -51,7 +50,7 @@ ShellRoot { ToastOverlay {} - NightLightOverlay {} + // Night light handled by wlsunset via NightLightService; no overlay needed IPCManager {}