From 634d78456d683c4d0b69208acdbab442cf1b6b91 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 26 Aug 2025 18:19:35 +0200 Subject: [PATCH] Add NightLight, update README, format --- Commons/Settings.qml | 12 +- Modules/Bar/Widgets/NightLight.qml | 85 +++++++++++ Modules/Launcher/Launcher.qml | 2 - Modules/NightLight/NightLight.qml | 65 +++++++++ Modules/SettingsPanel/Tabs/DisplayTab.qml | 166 ++++++++++++++++++++++ README.md | 27 ++-- Services/BarWidgetRegistry.qml | 4 + Services/NightLightService.qml | 108 ++++++++++++++ Widgets/NPanel.qml | 12 +- Widgets/NPill.qml | 10 +- shell.qml | 2 + 11 files changed, 471 insertions(+), 22 deletions(-) create mode 100644 Modules/Bar/Widgets/NightLight.qml create mode 100644 Modules/NightLight/NightLight.qml create mode 100644 Services/NightLightService.qml diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 6c85b4b..6e37d8d 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -129,7 +129,7 @@ Singleton { widgets: JsonObject { property list left: ["SystemMonitor", "ActiveWindow", "MediaMini"] property list center: ["Workspace"] - property list right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"] + property list right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "NightLight", "Clock", "SidePanelToggle"] } } @@ -256,6 +256,16 @@ Singleton { // External app theming (GTK & Qt) property bool themeApps: false } + + // night light + property JsonObject nightLight: JsonObject { + property bool enabled: false + property real warmth: 0.0 + property real intensity: 0.8 + property string startTime: "20:00" + property string stopTime: "07:00" + property bool autoSchedule: false + } } } } diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml new file mode 100644 index 0000000..edea949 --- /dev/null +++ b/Modules/Bar/Widgets/NightLight.qml @@ -0,0 +1,85 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Modules.SettingsPanel +import qs.Services +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + + implicitWidth: pill.width + implicitHeight: pill.height + visible: true + + function getIcon() { + if (!NightLightService.enabled) { + return "light_mode" + } + return NightLightService.isActive ? "dark_mode" : "light_mode" + } + + function getTooltipText() { + if (!NightLightService.enabled) { + return "Night Light: Disabled\nLeft click to open settings.\nRight click to enable." + } + + var status = NightLightService.isActive ? "Active" : "Inactive (outside schedule)" + var warmth = Math.round(NightLightService.warmth * 10) + var schedule = NightLightService.autoSchedule ? `Schedule: ${NightLightService.startTime} - ${NightLightService.stopTime}` : "Manual mode" + + return `Night Light: ${status}\nWarmth: ${warmth}/10\n${schedule}\nLeft click to open settings.\nRight click to toggle.` + } + + NPill { + id: pill + icon: getIcon() + iconCircleColor: NightLightService.isActive ? Color.mSecondary : Color.mOnSurfaceVariant + collapsedIconColor: NightLightService.isActive ? Color.mOnSecondary : Color.mOnSurface + autoHide: false + text: NightLightService.enabled ? (NightLightService.isActive ? "ON" : "OFF") : "OFF" + tooltipText: getTooltipText() + + onClicked: { + // Left click - open settings + var settingsPanel = PanelService.getPanel("settingsPanel") + settingsPanel.requestedTab = SettingsPanel.Tab.Display + settingsPanel.open(screen) + } + + onRightClicked: { + // Right click - toggle night light + NightLightService.toggle() + } + } + + // Update when service state changes + Connections { + target: NightLightService + function onEnabledChanged() { + pill.icon = getIcon() + pill.text = NightLightService.enabled ? (NightLightService.isActive ? "ON" : "OFF") : "OFF" + pill.tooltipText = getTooltipText() + } + function onIsActiveChanged() { + pill.icon = getIcon() + pill.text = NightLightService.enabled ? (NightLightService.isActive ? "ON" : "OFF") : "OFF" + pill.tooltipText = getTooltipText() + } + function onWarmthChanged() { + pill.tooltipText = getTooltipText() + } + function onStartTimeChanged() { + pill.tooltipText = getTooltipText() + } + function onStopTimeChanged() { + pill.tooltipText = getTooltipText() + } + function onAutoScheduleChanged() { + pill.tooltipText = getTooltipText() + } + } +} diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 2f5f960..2ac193a 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -49,8 +49,6 @@ NPanel { searchText = "" selectedIndex = 0 } - - } onClosed: { diff --git a/Modules/NightLight/NightLight.qml b/Modules/NightLight/NightLight.qml new file mode 100644 index 0000000..e6557cf --- /dev/null +++ b/Modules/NightLight/NightLight.qml @@ -0,0 +1,65 @@ +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.enabled + + sourceComponent: PanelWindow { + id: nightlightWindow + + screen: modelData + visible: NightLightService.isActive + color: Color.transparent + + mask: Region {} + + anchors { + top: true + bottom: true + left: true + right: true + } + + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + WlrLayershell.namespace: "noctalia-nightlight" + + Rectangle { + anchors.fill: parent + color: NightLightService.overlayColor + } + + // Safe connection that checks if the window still exists + Connections { + target: NightLightService + function onIsActiveChanged() { + if (nightlightWindow && typeof nightlightWindow.visible !== 'undefined') { + nightlightWindow.visible = NightLightService.isActive + } + } + } + + // Cleanup when component is being destroyed + Component.onDestruction: { + Logger.log("NightLight", "PanelWindow being destroyed") + } + } + + // Safe state changes + onActiveChanged: { + if (!active) { + Logger.log("NightLight", "Loader deactivating for screen:", modelData.name) + } + } + } +} diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index b13aa0d..16c3dd4 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -14,6 +14,24 @@ Item { Layout.fillWidth: true Layout.fillHeight: true + // Time dropdown options (00:00 .. 23:30) + ListModel { + id: timeOptions + } + Component.onCompleted: { + for (var h = 0; h < 24; h++) { + for (var m = 0; m < 60; m += 30) { + var hh = ("0" + h).slice(-2) + var mm = ("0" + m).slice(-2) + var key = hh + ":" + mm + timeOptions.append({ + "key": key, + "name": key + }) + } + } + } + // Helper functions to update arrays immutably function addMonitor(list, name) { const arr = (list || []).slice() @@ -209,6 +227,154 @@ Item { } } } + + // Night Light Section + NText { + text: "Night Light" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.topMargin: Style.marginXL * scaling + } + + NText { + text: "Reduce blue light emission to help you sleep better and reduce eye strain." + font.pointSize: Style.fontSize * scaling + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + Layout.preferredWidth: parent.width - (Style.marginL * 2 * scaling) + } + + NToggle { + label: "Enable Night Light" + description: "Apply a warm color filter to reduce blue light emission." + checked: NightLightService.enabled + onToggled: checked => { + Settings.data.nightLight.enabled = checked + } + } + + NToggle { + label: "Auto Schedule" + description: "Automatically enable night light based on time schedule." + checked: NightLightService.autoSchedule + enabled: NightLightService.enabled + onToggled: checked => { + NightLightService.setAutoSchedule(checked) + } + } + + // Warmth settings + NText { + text: "Warmth" + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + enabled: NightLightService.enabled + } + + NText { + text: "Higher values create warmer (more orange) light, lower values create cooler (more blue) light." + font.pointSize: Style.fontSizeS * scaling + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + enabled: NightLightService.enabled + } + + RowLayout { + spacing: Style.marginS * scaling + Layout.fillWidth: true + enabled: NightLightService.enabled + + NSlider { + id: warmthSlider + from: 0 + to: 10 + stepSize: 1 + value: Math.round(NightLightService.warmth * 10) + onPressedChanged: { + if (!pressed) { + NightLightService.setWarmth(value / 10) + } + } + Layout.fillWidth: true + Layout.minimumWidth: 150 * scaling + } + + Connections { + target: NightLightService + function onWarmthChanged() { + if (!warmthSlider.pressed) { + warmthSlider.value = Math.round(NightLightService.warmth * 10) + } + } + } + + NText { + text: `${warmthSlider.value}` + Layout.alignment: Qt.AlignVCenter + Layout.minimumWidth: 60 * scaling + horizontalAlignment: Text.AlignRight + } + } + + // Schedule settings + NText { + text: "Schedule" + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + enabled: NightLightService.enabled + } + + RowLayout { + spacing: Style.marginL * scaling + Layout.fillWidth: true + enabled: NightLightService.enabled + + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + + NText { + text: "Start Time" + font.pointSize: Style.fontSizeS * scaling + color: Color.mOnSurfaceVariant + } + + NComboBox { + model: timeOptions + currentKey: NightLightService.startTime + placeholder: "Select time" + onSelected: function (key) { + NightLightService.setSchedule(key, NightLightService.stopTime) + } + } + } + + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + + NText { + text: "Stop Time" + font.pointSize: Style.fontSizeS * scaling + color: Color.mOnSurfaceVariant + } + + NComboBox { + model: timeOptions + currentKey: NightLightService.stopTime + placeholder: "Select time" + onSelected: function (key) { + NightLightService.setSchedule(NightLightService.startTime, key) + } + } + } + } + Item { Layout.fillHeight: true } diff --git a/README.md b/README.md index dab4c14..034a3eb 100644 --- a/README.md +++ b/README.md @@ -105,10 +105,11 @@ nix run github:noctalia-dev/noctalia-shell
For flakes -```nix -{ - description = "Example Nix flake with Noctalia + Quickshell"; +**Step 1**: Add quickshell and noctalia flakes +```nix + +{ inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; @@ -129,21 +130,29 @@ nix run github:noctalia-dev/noctalia-shell pkgs = import nixpkgs { inherit system; }; in { nixosConfigurations.my-host = nixpkgs.lib.nixosSystem { - system = system; modules = [ ./configuration.nix - # Add noctalia to system packages - ({ pkgs, ... }: { environment.systemPackages = [ - noctalia.packages.${system}.default - quickshell.packages.${system}.default ]; - }) ]; }; }; +}``` + +and in `configuration.nix` + +```nix + +# your configuration.nix +{ + environment.systemPackages = with pkgs; [ + noctalia.packages.${system}.default + quickshell.packages.${system}.default + ]; } ``` + +
### Usage diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index b6e735c..5ff44fd 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -17,6 +17,7 @@ Singleton { "Clock": clockComponent, "KeyboardLayout": keyboardLayoutComponent, "MediaMini": mediaMiniComponent, + "NightLight": nightLightComponent, "NotificationHistory": notificationHistoryComponent, "PowerProfile": powerProfileComponent, "ScreenRecorderIndicator": screenRecorderIndicatorComponent, @@ -53,6 +54,9 @@ Singleton { property Component mediaMiniComponent: Component { MediaMini {} } + property Component nightLightComponent: Component { + NightLight {} + } property Component notificationHistoryComponent: Component { NotificationHistory {} } diff --git a/Services/NightLightService.qml b/Services/NightLightService.qml new file mode 100644 index 0000000..0c781fe --- /dev/null +++ b/Services/NightLightService.qml @@ -0,0 +1,108 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + // Night Light properties - directly bound to settings + property bool enabled: Settings.data.nightLight?.enabled || false + property real warmth: (Settings.data.nightLight + && Settings.data.nightLight.warmth !== undefined) ? Settings.data.nightLight.warmth : 0.6 + property real intensity: (Settings.data.nightLight + && Settings.data.nightLight.intensity !== undefined) ? Settings.data.nightLight.intensity : 0.8 + property string startTime: Settings.data.nightLight?.startTime || "20:00" + property string stopTime: Settings.data.nightLight?.stopTime || "07:00" + property bool autoSchedule: Settings.data.nightLight?.autoSchedule !== false + + // Computed properties + property color overlayColor: enabled ? calculateOverlayColor() : "transparent" + property bool isActive: enabled && (autoSchedule ? isWithinSchedule() : true) + + Component.onCompleted: { + Logger.log("NightLight", "Service started") + } + + function toggle() { + Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled + Logger.log("NightLight", "Toggled:", Settings.data.nightLight.enabled) + } + + function setWarmth(value) { + Settings.data.nightLight.warmth = Math.max(0.0, Math.min(1.0, value)) + Logger.log("NightLight", "Warmth set to:", Settings.data.nightLight.warmth) + } + + function setIntensity(value) { + Settings.data.nightLight.intensity = Math.max(0.0, Math.min(1.0, value)) + Logger.log("NightLight", "Intensity set to:", Settings.data.nightLight.intensity) + } + + function setSchedule(start, stop) { + Settings.data.nightLight.startTime = start + Settings.data.nightLight.stopTime = stop + Logger.log("NightLight", "Schedule set to:", Settings.data.nightLight.startTime, "-", + Settings.data.nightLight.stopTime) + } + + function setAutoSchedule(auto) { + Settings.data.nightLight.autoSchedule = auto + Logger.log("NightLight", "Auto schedule set to:", Settings.data.nightLight.autoSchedule, "enabled:", enabled, + "isActive:", isActive, "withinSchedule:", isWithinSchedule()) + } + + function calculateOverlayColor() { + if (!isActive) + return "transparent" + + // More vibrant color formula - stronger effect at high warmth + var red = 1.0 + var green = 0.85 - warmth * 0.4 // More green reduction for stronger effect + var blue = 0.5 - warmth * 0.45 // More blue reduction for warmer feel + var alpha = 0.1 + warmth * 0.25 // Higher alpha for more noticeable effect + + // Apply intensity + red = red * intensity + green = green * intensity + blue = blue * intensity + + return Qt.rgba(red, green, blue, alpha) + } + + function isWithinSchedule() { + if (!autoSchedule) + return true + + var now = new Date() + var currentTime = now.getHours() * 60 + now.getMinutes() + + var startParts = startTime.split(":") + var stopParts = 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 + } else { + return currentTime >= startMinutes && currentTime <= stopMinutes + } + } + + // Timer to check schedule changes + Timer { + interval: 60000 // Check every minute + running: true + repeat: true + onTriggered: { + if (autoSchedule && enabled) { + // Force overlay update when schedule changes + Logger.log("NightLight", "Schedule check - enabled:", enabled, "autoSchedule:", autoSchedule, "isActive:", + isActive, "withinSchedule:", isWithinSchedule()) + } + } + } +} diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 42872c9..659ad5b 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -194,17 +194,13 @@ Loader { property int calculatedY: { if (panelAnchorVerticalCenter) { return (panelWindow.height - panelHeight) / 2 - } - else if (panelAnchorBottom) { + } else if (panelAnchorBottom) { return panelWindow.height - panelHeight - (Style.marginS * scaling) - } - else if (panelAnchorTop) { + } else if (panelAnchorTop) { return (Style.marginS * scaling) - } - else if (panelAnchorBottom) { + } else if (panelAnchorBottom) { panelWindow.height - panelHeight - (Style.marginS * scaling) - } - else if (!barAtBottom) { + } else if (!barAtBottom) { // Below the top bar return Style.marginS * scaling } else { diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index bbf334c..d361cee 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -27,6 +27,7 @@ Item { signal entered signal exited signal clicked + signal rightClicked signal wheel(int delta) // Internal state @@ -194,6 +195,7 @@ Item { MouseArea { anchors.fill: parent hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton onEntered: { root.entered() tooltip.show() @@ -211,8 +213,12 @@ Item { } tooltip.hide() } - onClicked: { - root.clicked() + onClicked: function (mouse) { + if (mouse.button === Qt.LeftButton) { + root.clicked() + } else if (mouse.button === Qt.RightButton) { + root.rightClicked() + } } onWheel: wheel => { root.wheel(wheel.angleDelta.y) diff --git a/shell.qml b/shell.qml index 17103e0..feaceff 100644 --- a/shell.qml +++ b/shell.qml @@ -21,6 +21,7 @@ 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 @@ -39,6 +40,7 @@ ShellRoot { ScreenCorners {} Bar {} Dock {} + NightLight {} Notification { id: notification