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:
parent
05c09cfad2
commit
0342cbe59a
2 changed files with 261 additions and 12 deletions
|
|
@ -351,17 +351,10 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
# Night light — hyprsunset daemon, warms screen on a schedule.
|
# Night light — hyprsunset applies the gamma; the schedule and
|
||||||
# Adjust live: hyprctl hyprsunset temperature <K>
|
# temperature are driven by quickshell's quick-settings menu
|
||||||
services.hyprsunset = {
|
# (settings/quickshell.nix) via `hyprctl hyprsunset`.
|
||||||
enable = true;
|
services.hyprsunset.enable = true;
|
||||||
settings = {
|
|
||||||
profile = [
|
|
||||||
{ time = "7:00"; identity = true; }
|
|
||||||
{ time = "21:00"; temperature = 4000; }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Lock screen is quickshell's WlSessionLock (settings/quickshell.nix),
|
# Lock screen is quickshell's WlSessionLock (settings/quickshell.nix),
|
||||||
# triggered via `qs ipc call lock lock`.
|
# triggered via `qs ipc call lock lock`.
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ in
|
||||||
*,*) lat=''${loc%,*}; lon=''${loc#*,} ;;
|
*,*) lat=''${loc%,*}; lon=''${loc#*,} ;;
|
||||||
*) lat=51.51; lon=-0.13 ;;
|
*) lat=51.51; lon=-0.13 ;;
|
||||||
esac
|
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 {
|
in {
|
||||||
"quickshell/qmldir" = {
|
"quickshell/qmldir" = {
|
||||||
|
|
@ -155,6 +155,7 @@ in
|
||||||
readonly property color base04: "#${c.base04}"
|
readonly property color base04: "#${c.base04}"
|
||||||
readonly property color base05: "#${c.base05}"
|
readonly property color base05: "#${c.base05}"
|
||||||
readonly property color base08: "#${c.base08}"
|
readonly property color base08: "#${c.base08}"
|
||||||
|
readonly property color base09: "#${c.base09}"
|
||||||
readonly property color base0A: "#${c.base0A}"
|
readonly property color base0A: "#${c.base0A}"
|
||||||
readonly property color base0B: "#${c.base0B}"
|
readonly property color base0B: "#${c.base0B}"
|
||||||
readonly property color base0C: "#${c.base0C}"
|
readonly property color base0C: "#${c.base0C}"
|
||||||
|
|
@ -446,6 +447,57 @@ in
|
||||||
function unpin(): void { if (root.mainBar) root.mainBar.setScreenshotPin(false); }
|
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 {
|
NotificationServer {
|
||||||
id: _notifServer
|
id: _notifServer
|
||||||
bodySupported: true
|
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
|
// ── NotifContent: summary + body + action chips for one
|
||||||
// notification, shared by the calendar list and the toast.
|
// notification, shared by the calendar list and the toast.
|
||||||
// Callers supply the container, dismiss button and sizes.
|
// Callers supply the container, dismiss button and sizes.
|
||||||
|
|
@ -1609,6 +1740,7 @@ in
|
||||||
bar.toggleDropdown(batteryDropdown, function() {
|
bar.toggleDropdown(batteryDropdown, function() {
|
||||||
let pos = batteryWidget.mapToItem(bar.contentItem, batteryWidget.width / 2, 0);
|
let pos = batteryWidget.mapToItem(bar.contentItem, batteryWidget.width / 2, 0);
|
||||||
batteryDropdown.dropdownX = pos.x;
|
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
|
// Tray icons
|
||||||
Row {
|
Row {
|
||||||
id: trayArea
|
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;
|
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 */ }
|
} catch (e) { /* keep previous forecast */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue