quickshell: quick-settings — night light with sunset auto, brightness + kbd backlight sliders

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-07-08 13:26:08 +01:00
parent 05c09cfad2
commit 0342cbe59a
2 changed files with 261 additions and 12 deletions

View file

@ -128,7 +128,7 @@ in
*,*) lat=''${loc%,*}; lon=''${loc#*,} ;;
*) lat=51.51; lon=-0.13 ;;
esac
${pkgs.curl}/bin/curl -sf --max-time 10 "https://api.open-meteo.com/v1/forecast?latitude=$lat&longitude=$lon&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto&forecast_days=7"
${pkgs.curl}/bin/curl -sf --max-time 10 "https://api.open-meteo.com/v1/forecast?latitude=$lat&longitude=$lon&daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset&timezone=auto&forecast_days=7"
'';
in {
"quickshell/qmldir" = {
@ -155,6 +155,7 @@ in
readonly property color base04: "#${c.base04}"
readonly property color base05: "#${c.base05}"
readonly property color base08: "#${c.base08}"
readonly property color base09: "#${c.base09}"
readonly property color base0A: "#${c.base0A}"
readonly property color base0B: "#${c.base0B}"
readonly property color base0C: "#${c.base0C}"
@ -446,6 +447,57 @@ in
function unpin(): void { if (root.mainBar) root.mainBar.setScreenshotPin(false); }
}
// Night light: quickshell owns the schedule, the hyprsunset
// daemon just applies the gamma (hyprctl hyprsunset ).
// sunrise/sunset default to 7:00/21:00 and are replaced with
// real times by the bar's open-meteo weather fetch.
property int sunriseMins: 7 * 60
property int sunsetMins: 21 * 60
readonly property var nightlight: nlState
FileView {
id: nlFile
path: Quickshell.env("HOME") + "/.local/state/quickshell-nightlight.json"
printErrors: false
adapter: JsonAdapter {
id: nlState
property bool autoOn: true
property real warmth: 0.5 // 0 = 6500K 1 = 2500K
}
onLoaded: root.applyNightlight()
onLoadFailed: writeAdapter() // first run: create with defaults
}
function setNightlight(autoOn, warmth) {
nlState.autoOn = autoOn;
nlState.warmth = warmth;
applyNightlight();
nlFile.writeAdapter();
}
function applyNightlight() {
let w = nlState.warmth;
if (nlState.autoOn) {
let now = new Date();
let mins = now.getHours() * 60 + now.getMinutes();
if (mins >= sunriseMins && mins < sunsetMins) w = 0; // daytime
}
nlApply.command = w < 0.02
? ["hyprctl", "hyprsunset", "identity"]
: ["hyprctl", "hyprsunset", "temperature", String(Math.round(6500 - w * 4000))];
nlApply.running = true;
}
Process { id: nlApply }
Timer {
// Re-assert once a minute: crosses the sunrise/sunset
// boundaries and heals a hyprsunset daemon that (re)started
// after us.
interval: 60000; running: true; repeat: true; triggeredOnStart: true
onTriggered: root.applyNightlight()
}
NotificationServer {
id: _notifServer
bodySupported: true
@ -678,6 +730,85 @@ in
}
}
// ToggleSwitch: small on/off pill.
component ToggleSwitch: Rectangle {
property bool on: false
signal toggled(bool v)
width: 34; height: 18; radius: 9
color: on ? Theme.base0D : Theme.base02
Behavior on color { ColorAnimation { duration: Theme.animFade } }
Rectangle {
width: 12; height: 12; radius: 6
anchors.verticalCenter: parent.verticalCenter
x: parent.on ? parent.width - width - 3 : 3
color: Theme.base05
Behavior on x { NumberAnimation { duration: Theme.animContent; easing.type: Easing.OutExpo } }
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: parent.toggled(!parent.on)
}
}
// NightlightCard: colour-temperature control backed by
// shell.qml's night-light state. Auto on: follows
// sunrise/sunset and the slider sets the *warmest* colour the
// night reaches. Auto off: the slider is the temperature, now.
component NightlightCard: Card {
id: _nlc
cardSpacing: 6
property var nl: bar.shellRoot.nightlight
Item {
width: parent.width
height: 20
Row {
spacing: 6
anchors.verticalCenter: parent.verticalCenter
SIcon { anchors.verticalCenter: parent.verticalCenter; text: "nightlight"; font.pixelSize: 15 }
SText { anchors.verticalCenter: parent.verticalCenter; text: "Night Light"; font.weight: Font.Medium }
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 6
SText { anchors.verticalCenter: parent.verticalCenter; text: "Auto"; color: Theme.base04; font.pixelSize: 11 }
ToggleSwitch {
anchors.verticalCenter: parent.verticalCenter
on: _nlc.nl.autoOn
onToggled: (v) => bar.shellRoot.setNightlight(v, _nlc.nl.warmth)
}
}
}
SText {
text: _nlc.nl.autoOn ? "Warmest colour (at night)" : "Colour temperature"
color: Theme.base04
font.pixelSize: 10
}
Row {
width: parent.width
spacing: 8
PillSlider {
width: parent.width - nlTempLabel.width - 8
anchors.verticalCenter: parent.verticalCenter
value: _nlc.nl.warmth
fillColor: Theme.base09
onMoved: (v) => bar.shellRoot.setNightlight(_nlc.nl.autoOn, v)
}
SText {
id: nlTempLabel
width: 38
text: Math.round(6500 - _nlc.nl.warmth * 4000) + "K"
font.pixelSize: 11
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
}
}
}
// NotifContent: summary + body + action chips for one
// notification, shared by the calendar list and the toast.
// Callers supply the container, dismiss button and sizes.
@ -1609,6 +1740,7 @@ in
bar.toggleDropdown(batteryDropdown, function() {
let pos = batteryWidget.mapToItem(bar.contentItem, batteryWidget.width / 2, 0);
batteryDropdown.dropdownX = pos.x;
brightRead.running = true; // refresh sliders on open
});
}
@ -1625,6 +1757,41 @@ in
}
''}
${lib.optionalString (!isMacbook) ''
// Quick settings (night light) no battery widget on the
// desktop, so the card gets its own bar icon.
Item {
id: qsWidget
width: qsIcon.width
height: Theme.barHeight
SIcon {
id: qsIcon
anchors.verticalCenter: parent.verticalCenter
text: "nightlight"
font.pixelSize: 16
}
function openQsDropdown() {
bar.toggleDropdown(qsDropdown, function() {
let pos = qsWidget.mapToItem(bar.contentItem, qsWidget.width / 2, 0);
qsDropdown.dropdownX = pos.x;
});
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: qsWidget.openQsDropdown()
onEntered: {
if (bar.activeDropdown) {
if (bar.activeDropdown !== qsDropdown) qsWidget.openQsDropdown();
}
}
}
}
''}
// Tray icons
Row {
id: trayArea
@ -2386,6 +2553,86 @@ in
}
}
}
// Brightness card screen + keyboard backlight.
// Values are read on dropdown open (brightRead); writes
// are debounced latest-wins so a drag doesn't spawn a
// brightnessctl per pixel.
Card {
id: brightCard
width: parent.width
cardSpacing: 6
property real screenB: 0.5
property real kbdB: 0
SText { text: "Brightness"; color: Theme.base04; font.pixelSize: 11 }
Row {
width: parent.width
spacing: 8
SIcon { width: 18; anchors.verticalCenter: parent.verticalCenter; text: "brightness_6"; font.pixelSize: 16 }
PillSlider {
width: parent.width - 26
anchors.verticalCenter: parent.verticalCenter
value: brightCard.screenB
onMoved: (v) => { brightCard.screenB = v; screenBTimer.restart(); }
}
}
Row {
width: parent.width
spacing: 8
SIcon { width: 18; anchors.verticalCenter: parent.verticalCenter; text: "keyboard"; font.pixelSize: 16 }
PillSlider {
width: parent.width - 26
anchors.verticalCenter: parent.verticalCenter
value: brightCard.kbdB
onMoved: (v) => { brightCard.kbdB = v; kbdBTimer.restart(); }
}
}
Timer { id: screenBTimer; interval: 80; onTriggered: screenBSet.running = true }
// floor 1%: `set 0%` turns the panel fully off
Process { id: screenBSet; command: ["brightnessctl", "set", Math.max(1, Math.round(brightCard.screenB * 100)) + "%"] }
Timer { id: kbdBTimer; interval: 80; onTriggered: kbdBSet.running = true }
Process { id: kbdBSet; command: ["brightnessctl", "-d", "smc::kbd_backlight", "set", Math.round(brightCard.kbdB * 100) + "%"] }
Process {
id: brightRead
command: ["sh", "-c", "brightnessctl -m; brightnessctl -m -d smc::kbd_backlight"]
stdout: StdioCollector {
// machine-readable: device,class,current,percent%,max
onStreamFinished: {
for (let l of text.trim().split("\n")) {
let f = l.split(",");
if (f.length < 5) continue;
let pct = parseInt(f[3]) / 100;
if (f[0].indexOf("kbd") !== -1) brightCard.kbdB = pct;
else brightCard.screenB = pct;
}
}
}
}
}
NightlightCard { width: parent.width }
}
}
''}
${lib.optionalString (!isMacbook) ''
// Quick settings dropdown (desktop) just the night light.
BarDropdown {
id: qsDropdown
alignRight: true
fullWidth: qsCol.width + 28
fullHeight: qsCol.height + 20
Column {
id: qsCol
anchors.centerIn: parent
width: 240
spacing: 8
NightlightCard { width: parent.width }
}
}
''}
@ -2438,6 +2685,15 @@ in
});
}
if (out.length > 0) calPopup.weatherDays = out;
// Feed today's real sunrise/sunset to the
// night-light schedule (shell.qml).
if (j.daily.sunrise && j.daily.sunset) {
let sr = new Date(j.daily.sunrise[0]);
let ss = new Date(j.daily.sunset[0]);
bar.shellRoot.sunriseMins = sr.getHours() * 60 + sr.getMinutes();
bar.shellRoot.sunsetMins = ss.getHours() * 60 + ss.getMinutes();
bar.shellRoot.applyNightlight();
}
} catch (e) { /* keep previous forecast */ }
}
}