Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/power.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@ Reference machine for every number below: ASUS ROG Zephyrus G14 GA402XV, Ryzen 9
`power-profiles-daemon` running, kernel 7.1. Numbers are specific to that
chassis; the *shape* of the conclusions holds for AMD laptops generally.

## Low-battery alerts

Open **Win+, → Graphics & Power → Battery** to enable alerts, set the warning
and critical percentages, enable either level independently, or send a test notification. Defaults are 25% and
10%. Both levels default to enabled; turn either one off for warning-only or
critical-only reminders. Warning accepts 5–50%; critical accepts 1–49% and must be below warning.
These settings are independent of hardware support for a charge ceiling.

While discharging, Ryoku sends “Battery low” at the warning level and “Battery
critical” at the critical level, with “Battery at N%. Connect your charger.”
Each level alerts once until AC power reconnects. Starting below the critical
level sends only the critical alert. Delivery failures can retry without
consuming that alert. The service runs regardless of the selected bar style
or whether its battery indicator is visible.

Alerts use the normal notification system and respect Do Not Disturb. The
native service replaces the Python bar's separate low-battery reminders while
preserving its full-charge notification. If you installed the Battery Alerts
community add-on, disable its alerts when enabling the native service to avoid
duplicate reminders.

## The layers, from the one that matters most

1. **Which GPU renders your desktop.** On a hybrid laptop this dominates
Expand Down
6 changes: 6 additions & 0 deletions ryoku/hub/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

### Added
- Battery settings include low-battery alerts, independent warning and critical
switches and percentages, and a test notification. Either reminder can run
alone. Changes apply immediately and critical always stays
below warning (`quickshell/pages/BatteryAlertsSettings.qml`).

### Changed
- **Add-ons is a library of what you installed, not a wall of toggles.**
Desktop widgets no longer appear there; they are managed in Desktop. Shell
Expand Down
165 changes: 165 additions & 0 deletions ryoku/hub/quickshell/pages/BatteryAlertsSettings.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
pragma ComponentBehavior: Bound

import QtQuick
import Ryoku.Ui
import Ryoku.Ui.Singletons
import "../Singletons"

Column {
id: root

property bool busy: false
property string feedback: ""
readonly property bool alertsEnabled: {
Settings.revision;
return Settings.get("batteryAlerts.enabled") !== false;
}
readonly property bool warningEnabled: {
Settings.revision;
return Settings.get("batteryAlerts.warningEnabled") !== false;
}
readonly property bool criticalEnabled: {
Settings.revision;
return Settings.get("batteryAlerts.criticalEnabled") !== false;
}
readonly property int warningPercent: {
Settings.revision;
var value = Settings.get("batteryAlerts.warningPercent");
return typeof value === "number" ? Math.max(5, Math.min(50, Math.round(value))) : 25;
}
readonly property int criticalPercent: {
Settings.revision;
var value = Settings.get("batteryAlerts.criticalPercent");
return Math.max(1, Math.min(root.warningPercent - 1, typeof value === "number" ? Math.round(value) : 10));
}

function save(key, value) {
root.busy = true;
root.feedback = "";
Settings.patch("batteryAlerts." + key, value, function(ok, error) {
root.busy = false;
root.feedback = ok ? "" : I18n.tr(error);
});
}

function testNotification() {
root.busy = true;
root.feedback = "";
Settings.send("battery-alerts.test", {}, function(ok, error) {
root.busy = false;
root.feedback = ok ? I18n.tr("Test notification requested.") : I18n.tr(error);
});
}

SettingRow {
width: parent.width
label: I18n.tr("Low-battery alerts")
desc: I18n.tr("Warn while the battery is discharging. Alerts respect Do Not Disturb.")
controlWidth: 54
enabled: Settings.ready && !root.busy
Sw {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
on: root.alertsEnabled
onToggled: value => root.save("enabled", value)
}
}

SettingRow {
width: parent.width
divider: true
label: I18n.tr("Warning alerts")
desc: I18n.tr("Send the first reminder to plug in.")
controlWidth: 54
enabled: Settings.ready && root.alertsEnabled && !root.busy
Sw {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
on: root.warningEnabled
onToggled: value => root.save("warningEnabled", value)
}
}

SettingRow {
width: parent.width
divider: true
label: I18n.tr("Warning percentage")
desc: I18n.tr("First reminder to plug in. Must be above the critical percentage.")
controlWidth: 58
unit: "%"
value: String(root.warningPercent)
editableValue: true
enabled: Settings.ready && root.alertsEnabled && root.warningEnabled && !root.busy
onValueCommitted: text => {
var value = Number(text);
if (text.trim() !== "" && isFinite(value))
root.save("warningPercent", Math.max(Math.max(5, root.criticalPercent + 1), Math.min(50, Math.round(value))));
}
Step {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
from: Math.max(5, root.criticalPercent + 1)
to: 50
stepBy: 1
value: root.warningPercent
onModified: value => root.save("warningPercent", value)
}
}

SettingRow {
width: parent.width
divider: true
label: I18n.tr("Critical alerts")
desc: I18n.tr("Send the urgent reminder to plug in.")
controlWidth: 54
enabled: Settings.ready && root.alertsEnabled && !root.busy
Sw {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
on: root.criticalEnabled
onToggled: value => root.save("criticalEnabled", value)
}
}

SettingRow {
width: parent.width
divider: true
label: I18n.tr("Critical percentage")
desc: I18n.tr("Urgent reminder to plug in. Must be below the warning percentage.")
controlWidth: 58
unit: "%"
value: String(root.criticalPercent)
editableValue: true
enabled: Settings.ready && root.alertsEnabled && root.criticalEnabled && !root.busy
onValueCommitted: text => {
var value = Number(text);
if (text.trim() !== "" && isFinite(value))
root.save("criticalPercent", Math.max(1, Math.min(root.warningPercent - 1, Math.round(value))));
}
Step {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
from: 1
to: root.warningPercent - 1
stepBy: 1
value: root.criticalPercent
onModified: value => root.save("criticalPercent", value)
}
}

SettingRow {
width: parent.width
divider: true
label: I18n.tr("Test notification")
desc: root.feedback !== "" ? root.feedback : I18n.tr("Preview a battery alert without changing your battery level.")
controlWidth: testButton.implicitWidth
Btn {
id: testButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Test")
armed: Settings.ready && root.alertsEnabled && !root.busy
onAct: root.testNotification()
}
}
}
5 changes: 4 additions & 1 deletion ryoku/hub/quickshell/pages/GpuPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -984,9 +984,12 @@ done
// ── BATTERY ──
SettingCard {
width: gfxCol.colWidth
visible: pg.batteryTune.length > 0
title: I18n.tr("BATTERY")

BatteryAlertsSettings {
width: parent.width
}

Repeater {
model: pg.batteryTune
delegate: TuneCell {
Expand Down
6 changes: 6 additions & 0 deletions ryoku/shell/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
## Unreleased

### Added
- Warning and critical battery alerts can each be enabled independently in
Graphics & Power → Battery, including warning-only or critical-only modes.
- **Native battery alerts.** Every bar style warns once per discharge session at
configurable warning and critical levels (25% and 10% by default). Configure
or test them in Ryoku Settings → Graphics & Power → Battery. Alerts respect
Do Not Disturb and retry notification delivery failures.
- **One dock for every bar style, in any design.** The dock is no longer tied
to a bar style: pick the Ryoku, Python or Shima dock (or none) and it runs
the same under QS Bar, Kairos, Shima, Python, Sumi and Nomarchy, on both
Expand Down
41 changes: 41 additions & 0 deletions ryoku/shell/ipc/batteryalerts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package main

import (
"encoding/json"
"fmt"
"strings"
)

type batteryAlertsSettings struct {
WarningEnabled bool `json:"warningEnabled"`
CriticalEnabled bool `json:"criticalEnabled"`
Enabled bool `json:"enabled"`
WarningPercent int `json:"warningPercent"`
CriticalPercent int `json:"criticalPercent"`
}

func (b *batteryAlertsSettings) normalize(v *validator) {
v.rangeI("batteryAlerts.warningPercent", &b.WarningPercent, 5, 50)
v.rangeI("batteryAlerts.criticalPercent", &b.CriticalPercent, 1, 49)
if v.err != nil || b.CriticalPercent < b.WarningPercent {
return
}
if v.strict {
v.err = fmt.Errorf("batteryAlerts.criticalPercent: must be below warningPercent")
return
}
b.CriticalPercent = b.WarningPercent - 1
}

var batteryAlertsTestIPC = func() string {
return ipcCall("shell", "battery-alerts-native", "test", "")
}

func (d *daemon) registerBatteryAlertsCalls() {
d.registerCall("battery-alerts.test", func(json.RawMessage) (any, error) {
if result := batteryAlertsTestIPC(); result != "ok" {
return nil, fmt.Errorf("%s", strings.TrimPrefix(result, "err "))
}
return nil, nil
})
}
Loading
Loading