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.
|
||||
# Adjust live: hyprctl hyprsunset temperature <K>
|
||||
services.hyprsunset = {
|
||||
enable = true;
|
||||
settings = {
|
||||
profile = [
|
||||
{ time = "7:00"; identity = true; }
|
||||
{ time = "21:00"; temperature = 4000; }
|
||||
];
|
||||
};
|
||||
};
|
||||
# Night light — hyprsunset applies the gamma; the schedule and
|
||||
# temperature are driven by quickshell's quick-settings menu
|
||||
# (settings/quickshell.nix) via `hyprctl hyprsunset`.
|
||||
services.hyprsunset.enable = true;
|
||||
|
||||
# Lock screen is quickshell's WlSessionLock (settings/quickshell.nix),
|
||||
# triggered via `qs ipc call lock lock`.
|
||||
|
|
|
|||
|
|
@ -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 */ }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue