Feat/battery threshold (#7)
* feat(battery-threshold): add translations and rule file * feat(battery-threshold): add setup script * feat(battery-threshold): add plugin manifest * feat(battery-threshold): implement battery-threshold service * feat(battery-threshold): implement battery-threshold widget * feat(battery-threshold): implement battery-threshold panel * feat(battery-threshold): add readme * chore(battery-threshold): add dependencies field * chore(battery-threshold): add thumbnail * docs(battery-threshold): add thumbnail to readme * fix(battery-threshold): add thumbnail from generator * chore(battery-threshold): add license and icon fields to plugin.toml * style(battery-threshold): replace tabs with spaces * style(battery-threshold): unflatten translation files * refactor(battery-threshold): use translations for widget tooltip * chore(battery-threshold): replace raw strings with translations * chore(battery-threshold): remove panel ipc event * chore(battery-threshold): check error on file read * chore(battery-threshold): remove `setUpdateInterval` calls * style(battery-threshold): fix formatting * chore(battery-threshold): remove tags capital letters * Update plugin.toml * chore(battery-threshold): remove de fr and tr translations * chore(battery-threshold): update thumbnail * fix(battery-threshold): add shell quoting for runAsync * chore(battery-threshold): embed rule file content in setup script * chore(battery-threshold): update manifest * refactor(battery-threshold): use noctalia.pluginDataDir, log read/write errors * docs(battery-threshold): update README to match template * chore(battery-threshold): add missing dependencies to manifest --------- Co-authored-by: Lemmy <studio@quadbyte.net>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# Battery Threshold Control
|
||||
|
||||
Control the battery charge threshold on laptop batteries to help extend overall
|
||||
battery lifespan. Someone would use this plugin to limit maximum charge levels
|
||||
while plugged in, reducing battery wear and heat.
|
||||
|
||||
## Plugin
|
||||
|
||||
| Field | Value |
|
||||
| ------- | ------------------------------------------------------------------- |
|
||||
| ID | `damian-ds7/battery-threshold` |
|
||||
| Entries | Bar widget: `battery-threshold`; panel: `panel`; service: `service` |
|
||||
|
||||
## Requirements
|
||||
|
||||
- `polkit` - required for interactive setup
|
||||
- Laptop hardware supporting battery charge threshold control in sysfs
|
||||
(`/sys/class/power_supply/*/charge_control_end_threshold`).
|
||||
- The following external programs must be available on `PATH`: `test`, `pkexec`,
|
||||
`bash`, `cat`, `getent`, `groupadd`, `usermod`, `udevadm`, `chgrp`, and
|
||||
`chmod`.
|
||||
|
||||
## Usage
|
||||
|
||||
- **Bar Widget (`battery-threshold`)**: Displays the current battery threshold
|
||||
in the bar. Click to toggle the panel.
|
||||
- **Panel (`panel`)**: Adjust the battery threshold using a slider (40–100%).
|
||||
Includes a **Configure Permissions** button if write access is missing. Toggle
|
||||
the panel using:
|
||||
|
||||
```sh
|
||||
noctalia msg panel-toggle damian-ds7/battery-threshold:panel
|
||||
```
|
||||
|
||||
## Settings
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| ------------------ | -------- | ------------------------------ | ---------------------------------------------- |
|
||||
| `battery_device` | `folder` | `/sys/class/power_supply/BAT0` | Path to the battery sysfs directory. |
|
||||
| `charge_threshold` | `int` | `80` | Default charge threshold percentage (40–100%). |
|
||||
|
||||
## IPC
|
||||
|
||||
```sh
|
||||
# Set charge threshold percentage (between 40 and 100)
|
||||
noctalia msg plugin damian-ds7/battery-threshold:service all set 80
|
||||
|
||||
# Trigger setup script for udev permissions
|
||||
noctalia msg plugin damian-ds7/battery-threshold:service all setup
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- **Permissions & Setup**: Requires write access to
|
||||
`/sys/class/power_supply/BAT0/charge_control_end_threshold`. Automated setup
|
||||
creates the `battery_ctl` group, adds the active user to it, and installs
|
||||
`99-battery-threshold.rules` to `/etc/udev/rules.d/`.
|
||||
- **Relogin / Reboot**: A logout or system reboot is required after running
|
||||
setup for `battery_ctl` group membership changes to take effect.
|
||||
- **Manual Setup Fallback**: If Polkit is not available, run
|
||||
`sudo ./setup_rules.sh` manually from the plugin directory.
|
||||
- **Persistence**: Threshold settings are stored in `threshold.txt` in plugin
|
||||
directory and restored across reboots.
|
||||
@@ -0,0 +1,172 @@
|
||||
--!nonstrict
|
||||
local is_open = false
|
||||
|
||||
local function render_panel()
|
||||
if not is_open then
|
||||
return
|
||||
end
|
||||
|
||||
local is_available = noctalia.state.get("is_available") == true
|
||||
local is_writable = noctalia.state.get("is_writable") == true
|
||||
local threshold = noctalia.state.get("current_threshold") or 0
|
||||
local model_name = noctalia.state.get("battery_model_name") or ""
|
||||
|
||||
local title_text = noctalia.tr("panel.title")
|
||||
local sub_text = ""
|
||||
if not is_available then
|
||||
sub_text = noctalia.tr("panel.not-available")
|
||||
else
|
||||
sub_text = model_name
|
||||
end
|
||||
|
||||
local hint_text = ""
|
||||
local hint_color = "on_surface_variant"
|
||||
if not is_writable then
|
||||
hint_text = noctalia.tr("panel.read-only")
|
||||
hint_color = "error"
|
||||
else
|
||||
hint_text = noctalia.tr("panel.adjust-limit")
|
||||
end
|
||||
|
||||
local children = {
|
||||
ui.row({ align = "center", justify = "space_between" }, {
|
||||
ui.column({ gap = 2 }, {
|
||||
ui.label({
|
||||
text = title_text,
|
||||
fontSize = 16,
|
||||
fontWeight = "bold",
|
||||
color = "primary",
|
||||
}),
|
||||
ui.label({
|
||||
text = sub_text,
|
||||
fontSize = 12,
|
||||
color = "on_surface_variant",
|
||||
}),
|
||||
}),
|
||||
ui.button({
|
||||
glyph = "close",
|
||||
variant = "ghost",
|
||||
onClick = "onCloseClicked",
|
||||
}),
|
||||
}),
|
||||
}
|
||||
|
||||
if is_available then
|
||||
table.insert(
|
||||
children,
|
||||
ui.separator({ thickness = 1, color = "outline/0.3", spacing = 8 })
|
||||
)
|
||||
|
||||
if is_writable then
|
||||
table.insert(
|
||||
children,
|
||||
ui.column({ gap = 8 }, {
|
||||
ui.row({ align = "center", justify = "space_between" }, {
|
||||
ui.label({
|
||||
text = title_text,
|
||||
fontSize = 14,
|
||||
color = "on_surface",
|
||||
}),
|
||||
ui.label({
|
||||
text = tostring(threshold) .. "%",
|
||||
fontSize = 16,
|
||||
fontWeight = "bold",
|
||||
color = "primary",
|
||||
}),
|
||||
}),
|
||||
ui.row({ align = "center", gap = 8 }, {
|
||||
ui.label({
|
||||
text = "40%",
|
||||
fontSize = 11,
|
||||
color = "on_surface_variant",
|
||||
}),
|
||||
ui.slider({
|
||||
min = 40,
|
||||
max = 100,
|
||||
step = 5,
|
||||
value = threshold,
|
||||
enabled = is_writable,
|
||||
onChange = "onSliderChange",
|
||||
}),
|
||||
ui.label({
|
||||
text = "100%",
|
||||
fontSize = 11,
|
||||
color = "on_surface_variant",
|
||||
}),
|
||||
}),
|
||||
ui.label({
|
||||
text = hint_text,
|
||||
fontSize = 11,
|
||||
color = hint_color,
|
||||
textAlign = "center",
|
||||
}),
|
||||
})
|
||||
)
|
||||
else
|
||||
table.insert(
|
||||
children,
|
||||
ui.column({ gap = 12, align = "center" }, {
|
||||
ui.label({
|
||||
text = hint_text,
|
||||
fontSize = 12,
|
||||
color = "error",
|
||||
textAlign = "center",
|
||||
maxLines = 2,
|
||||
}),
|
||||
ui.button({
|
||||
text = noctalia.tr("panel.button"),
|
||||
glyph = "shield-lock",
|
||||
variant = "primary",
|
||||
onClick = "onRunSetup",
|
||||
}),
|
||||
})
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
panel.render(ui.column({ gap = 12, padding = 12 }, children))
|
||||
end
|
||||
|
||||
function onOpen(context: string?)
|
||||
is_open = true
|
||||
render_panel()
|
||||
end
|
||||
|
||||
function onClose()
|
||||
is_open = false
|
||||
end
|
||||
|
||||
function onSliderChange(value: string)
|
||||
local num = tonumber(value)
|
||||
if num then
|
||||
noctalia.state.set("set_threshold_request", num)
|
||||
noctalia.state.set("current_threshold", num)
|
||||
render_panel()
|
||||
end
|
||||
end
|
||||
|
||||
function onCloseClicked()
|
||||
panel.close()
|
||||
end
|
||||
|
||||
function onRunSetup()
|
||||
noctalia.state.set("run_setup_request", true)
|
||||
end
|
||||
|
||||
noctalia.state.watch("current_threshold", function()
|
||||
if is_open then
|
||||
render_panel()
|
||||
end
|
||||
end)
|
||||
|
||||
noctalia.state.watch("is_writable", function()
|
||||
if is_open then
|
||||
render_panel()
|
||||
end
|
||||
end)
|
||||
|
||||
noctalia.state.watch("is_available", function()
|
||||
if is_open then
|
||||
render_panel()
|
||||
end
|
||||
end)
|
||||
@@ -0,0 +1,43 @@
|
||||
id = "damian-ds7/battery-threshold"
|
||||
name = "Battery Threshold Control"
|
||||
version = "1.0.0"
|
||||
plugin_api = 3
|
||||
author = "Damian D'Souza"
|
||||
description = "Set the battery threshold for laptop batteries to extend battery lifespan"
|
||||
license = "MIT"
|
||||
icon = "battery-eco"
|
||||
tags = ["hardware", "system", "utility", "bar", "panel"]
|
||||
dependencies = ["test", "pkexec", "bash", "cat", "getent", "groupadd", "usermod", "udevadm", "chgrp", "chmod"]
|
||||
|
||||
[[service]]
|
||||
id = "service"
|
||||
entry = "service.luau"
|
||||
|
||||
[[widget]]
|
||||
id = "battery-threshold"
|
||||
entry = "widget.luau"
|
||||
|
||||
[[panel]]
|
||||
id = "panel"
|
||||
entry = "panel.luau"
|
||||
width = 320
|
||||
height = 220
|
||||
placement = "floating"
|
||||
position = "center"
|
||||
open_near_click = true
|
||||
|
||||
[[setting]]
|
||||
key = "battery_device"
|
||||
type = "folder"
|
||||
label_key = "settings.battery-device"
|
||||
description_key = "settings.battery-device-desc"
|
||||
default = "/sys/class/power_supply/BAT0"
|
||||
|
||||
[[setting]]
|
||||
key = "charge_threshold"
|
||||
type = "int"
|
||||
label_key = "settings.charge-threshold"
|
||||
description_key = "settings.charge-threshold-desc"
|
||||
default = 80
|
||||
min = 40
|
||||
max = 100
|
||||
@@ -0,0 +1,228 @@
|
||||
--!nonstrict
|
||||
local function get_batteries(): { string }
|
||||
local batteries = {}
|
||||
local files, _ = noctalia.listDir("/sys/class/power_supply")
|
||||
|
||||
if files then
|
||||
for _, name in files do
|
||||
local path = "/sys/class/power_supply/" .. name
|
||||
if noctalia.fileExists(path .. "/charge_control_end_threshold") then
|
||||
table.insert(batteries, path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return batteries
|
||||
end
|
||||
|
||||
local function get_active_battery(): string?
|
||||
local configured = noctalia.getConfig("battery_device")
|
||||
|
||||
if
|
||||
typeof(configured) == "string"
|
||||
and configured ~= ""
|
||||
and noctalia.fileExists(configured .. "/charge_control_end_threshold")
|
||||
then
|
||||
return configured
|
||||
end
|
||||
|
||||
local list = get_batteries()
|
||||
return list[1]
|
||||
end
|
||||
|
||||
local function shell_quote(str: string): string
|
||||
return "'" .. string.gsub(str, "'", "'\\''") .. "'"
|
||||
end
|
||||
|
||||
local function check_writable(path: string, callback: (boolean) -> ())
|
||||
noctalia.runAsync("test -w " .. shell_quote(path), function(res)
|
||||
callback(res.exitCode == 0)
|
||||
end)
|
||||
end
|
||||
|
||||
local data_dir, data_dir_err = noctalia.pluginDataDir()
|
||||
if not data_dir or data_dir_err then
|
||||
noctalia.log(
|
||||
"Failed to get plugin data directory: "
|
||||
.. tostring(data_dir_err or "unknown error")
|
||||
)
|
||||
end
|
||||
local THRESHOLD_FILE_PATH: string? = if data_dir
|
||||
then data_dir .. "/threshold.txt"
|
||||
else nil
|
||||
|
||||
local function set_threshold(value: number)
|
||||
local battery = get_active_battery()
|
||||
if not battery then
|
||||
return
|
||||
end
|
||||
local threshold_file = battery .. "/charge_control_end_threshold"
|
||||
|
||||
local v = math.floor(value + 0.5)
|
||||
if v < 40 then
|
||||
v = 40
|
||||
end
|
||||
if v > 100 then
|
||||
v = 100
|
||||
end
|
||||
|
||||
noctalia.log("Setting charge threshold to " .. tostring(v) .. "% on " .. battery)
|
||||
|
||||
local ok, err = noctalia.writeFile(threshold_file, tostring(v) .. "\n")
|
||||
if ok then
|
||||
noctalia.state.set("current_threshold", v)
|
||||
if THRESHOLD_FILE_PATH then
|
||||
local save_ok, save_err =
|
||||
noctalia.writeFile(THRESHOLD_FILE_PATH, tostring(v))
|
||||
if not save_ok then
|
||||
noctalia.log(
|
||||
"Failed to write saved threshold to "
|
||||
.. THRESHOLD_FILE_PATH
|
||||
.. ": "
|
||||
.. tostring(save_err)
|
||||
)
|
||||
end
|
||||
end
|
||||
else
|
||||
noctalia.log("Failed to write threshold: " .. tostring(err))
|
||||
noctalia.notifyError(
|
||||
noctalia.tr("notification.error-title"),
|
||||
noctalia.tr("notification.error-msg", { file = threshold_file })
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local function check_status()
|
||||
local battery = get_active_battery()
|
||||
if not battery then
|
||||
noctalia.state.set("is_available", false)
|
||||
noctalia.state.set("is_writable", false)
|
||||
noctalia.state.set("current_threshold", 0)
|
||||
noctalia.state.set("battery_model_name", "")
|
||||
return
|
||||
end
|
||||
|
||||
noctalia.state.set("is_available", true)
|
||||
|
||||
local model_name = ""
|
||||
local content, err = noctalia.readFile(battery .. "/model_name")
|
||||
if content and not err then
|
||||
model_name = noctalia.string.trim(content)
|
||||
end
|
||||
noctalia.state.set("battery_model_name", model_name)
|
||||
|
||||
local threshold_file = battery .. "/charge_control_end_threshold"
|
||||
|
||||
local threshold_content = noctalia.readFile(threshold_file)
|
||||
local current_val = 0
|
||||
if threshold_content then
|
||||
current_val = tonumber(noctalia.string.trim(threshold_content)) or 0
|
||||
noctalia.state.set("current_threshold", current_val)
|
||||
end
|
||||
|
||||
check_writable(threshold_file, function(writable)
|
||||
noctalia.state.set("is_writable", writable)
|
||||
|
||||
if writable then
|
||||
local saved_val = nil
|
||||
if THRESHOLD_FILE_PATH and noctalia.fileExists(THRESHOLD_FILE_PATH) then
|
||||
local saved_content, read_err = noctalia.readFile(THRESHOLD_FILE_PATH)
|
||||
if saved_content and not read_err then
|
||||
saved_val = tonumber(noctalia.string.trim(saved_content))
|
||||
elseif read_err then
|
||||
noctalia.log(
|
||||
"Failed to read saved threshold from "
|
||||
.. THRESHOLD_FILE_PATH
|
||||
.. ": "
|
||||
.. tostring(read_err)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if not saved_val then
|
||||
local config_val = noctalia.getConfig("charge_threshold")
|
||||
if typeof(config_val) == "number" then
|
||||
saved_val = config_val
|
||||
end
|
||||
end
|
||||
|
||||
if saved_val and saved_val >= 40 and saved_val <= 100 then
|
||||
if saved_val ~= current_val then
|
||||
set_threshold(saved_val)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function run_setup()
|
||||
local user = noctalia.getenv("USER")
|
||||
if not user or user == "" then
|
||||
noctalia.log("Could not get USER environment variable")
|
||||
return
|
||||
end
|
||||
|
||||
local plugin_dir = noctalia.pluginDir()
|
||||
if not plugin_dir then
|
||||
noctalia.log("Could not get plugin directory")
|
||||
return
|
||||
end
|
||||
|
||||
local rules_script = plugin_dir .. "/setup_rules.sh"
|
||||
|
||||
local cmd =
|
||||
string.format("pkexec bash %s %s", shell_quote(rules_script), shell_quote(user))
|
||||
|
||||
noctalia.log("Running setup script via pkexec: " .. cmd)
|
||||
noctalia.runAsync(cmd, function(result)
|
||||
if result.exitCode == 0 then
|
||||
noctalia.log("Setup completed successfully")
|
||||
noctalia.notify(
|
||||
noctalia.tr("notification.setup-title"),
|
||||
noctalia.tr("notification.setup-success")
|
||||
)
|
||||
check_status()
|
||||
else
|
||||
noctalia.log("Setup failed with exit code " .. tostring(result.exitCode))
|
||||
noctalia.notifyError(
|
||||
noctalia.tr("notification.setup-title"),
|
||||
noctalia.tr("notification.setup-fail")
|
||||
)
|
||||
end
|
||||
noctalia.state.set("run_setup_request", false)
|
||||
end)
|
||||
end
|
||||
|
||||
noctalia.state.watch("run_setup_request", function(val)
|
||||
if val == true then
|
||||
run_setup()
|
||||
end
|
||||
end)
|
||||
|
||||
noctalia.state.watch("set_threshold_request", function(val)
|
||||
if val then
|
||||
local num = tonumber(val)
|
||||
if num then
|
||||
set_threshold(num)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function onIpc(event, payload)
|
||||
if event == "set" then
|
||||
if payload then
|
||||
local val = tonumber(payload)
|
||||
if val and val >= 40 and val <= 100 then
|
||||
set_threshold(val)
|
||||
end
|
||||
end
|
||||
elseif event == "setup" then
|
||||
run_setup()
|
||||
end
|
||||
end
|
||||
|
||||
function onConfigChanged()
|
||||
check_status()
|
||||
end
|
||||
|
||||
check_status()
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# ------------------------------
|
||||
# Battery Threshold Udev Setup
|
||||
# ------------------------------
|
||||
# This script sets up udev rules to allow a non-root user to write to
|
||||
# /sys/class/power_supply/BAT0/charge_control_end_threshold.
|
||||
# It creates a group 'battery_ctl' and adds the target user to this group.
|
||||
#
|
||||
# Usage:
|
||||
# $ sudo ./setup_rules.sh # uses SUDO_USER (with sudo)
|
||||
# $ ./setup_rules.sh username # use provided username (if ran as root)
|
||||
# ------------------------------
|
||||
set -e
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Error: This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine target user
|
||||
TARGET_USER=${SUDO_USER:-$1}
|
||||
|
||||
if [ -z "$TARGET_USER" ]; then
|
||||
echo "Error: No target user specified." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! getent group battery_ctl >/dev/null; then
|
||||
echo "Creating battery_ctl group..."
|
||||
groupadd battery_ctl
|
||||
fi
|
||||
|
||||
echo "Adding $TARGET_USER to battery_ctl group..."
|
||||
usermod -aG battery_ctl "$TARGET_USER"
|
||||
|
||||
echo "Writing udev rule to /etc/udev/rules.d/99-battery-threshold.rules..."
|
||||
|
||||
cat <<'EOF' >/etc/udev/rules.d/99-battery-threshold.rules
|
||||
# Battery Threshold Control - udev rule
|
||||
# Grants write access to charge_control_end_threshold for users in the
|
||||
# 'battery_ctl' group.
|
||||
SUBSYSTEM=="power_supply", KERNEL=="BAT*", \
|
||||
RUN+="/bin/chgrp battery_ctl /sys$devpath/charge_control_end_threshold", \
|
||||
RUN+="/bin/chmod g+w /sys$devpath/charge_control_end_threshold"
|
||||
EOF
|
||||
|
||||
echo "Reloading rules..."
|
||||
|
||||
udevadm control --reload-rules && udevadm trigger
|
||||
|
||||
echo "You may need a reboot for the plugin's write access to take effect"
|
||||
echo "Done!"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"settings": {
|
||||
"battery-device": "Battery Device",
|
||||
"battery-device-desc": "Battery to configure threshold for",
|
||||
"charge-threshold": "Charge Threshold",
|
||||
"charge-threshold-desc": "The percentage at which the battery should stop charging",
|
||||
"no-battery-device": "No configurable batteries are available on this system"
|
||||
},
|
||||
"notification": {
|
||||
"error-title": "Battery Threshold Error",
|
||||
"error-msg": "Failed to write battery threshold to {file}",
|
||||
"setup-title": "Battery Threshold Setup",
|
||||
"setup-fail": "Failed to configure permissions.",
|
||||
"setup-success": "Permissions configured. Please log out and back in for group changes to take effect."
|
||||
},
|
||||
"actions": {
|
||||
"widget-settings": "Widget Settings"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Battery Threshold",
|
||||
"not-available": "Not available on this system",
|
||||
"adjust-limit": "Drag slider to adjust limit",
|
||||
"read-only": "Read-only: Install udev rule for write access",
|
||||
"button": "Configure Permissions"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
--!nonstrict
|
||||
local function render_widget()
|
||||
local is_available = noctalia.state.get("is_available") == true
|
||||
local threshold = noctalia.state.get("current_threshold") or 0
|
||||
|
||||
local container = barWidget.isVertical() and ui.column or ui.row
|
||||
|
||||
if is_available then
|
||||
barWidget.render(container({ gap = 4, align = "center" }, {
|
||||
ui.glyph({ name = "charging-pile", size = 14 }),
|
||||
ui.label({ text = tostring(threshold) .. "%", fontWeight = "bold" }),
|
||||
}))
|
||||
barWidget.setTooltip(
|
||||
noctalia.tr("panel.title") .. " " .. tostring(threshold) .. "%"
|
||||
)
|
||||
else
|
||||
barWidget.render(container({ gap = 4, align = "center" }, {
|
||||
ui.glyph({ name = "charging-pile", size = 14, color = "on_surface/0.4" }),
|
||||
}))
|
||||
barWidget.setTooltip(noctalia.tr("settings.no-battery-device"))
|
||||
end
|
||||
end
|
||||
|
||||
noctalia.state.watch("current_threshold", render_widget)
|
||||
noctalia.state.watch("is_available", render_widget)
|
||||
|
||||
function onClick()
|
||||
noctalia.togglePanel("damian-ds7/battery-threshold:panel")
|
||||
end
|
||||
|
||||
function update()
|
||||
render_widget()
|
||||
end
|
||||
|
||||
render_widget()
|
||||
Reference in New Issue
Block a user