Add NightLight, update README, format

This commit is contained in:
Ly-sec
2025-08-26 18:19:35 +02:00
parent 71cfbc8c0a
commit 634d78456d
11 changed files with 471 additions and 22 deletions

View File

@@ -129,7 +129,7 @@ Singleton {
widgets: JsonObject {
property list<string> left: ["SystemMonitor", "ActiveWindow", "MediaMini"]
property list<string> center: ["Workspace"]
property list<string> right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"]
property list<string> 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
}
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -49,8 +49,6 @@ NPanel {
searchText = ""
selectedIndex = 0
}
}
onClosed: {

View File

@@ -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)
}
}
}
}

View File

@@ -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
}

View File

@@ -105,10 +105,11 @@ nix run github:noctalia-dev/noctalia-shell
<details>
<summary><strong>For flakes</strong></summary>
```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
];
}
```
</details>
### Usage

View File

@@ -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 {}
}

View File

@@ -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())
}
}
}
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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