Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
aef24d7
feat(battery-threshold): add translations and rule file
damian-ds7 Jul 13, 2026
57b4d6b
feat(battery-threshold): add setup script
damian-ds7 Jul 14, 2026
6c1dca8
feat(battery-threshold): add plugin manifest
damian-ds7 Jul 14, 2026
bc31310
feat(battery-threshold): implement battery-threshold service
damian-ds7 Jul 14, 2026
6286506
feat(battery-threshold): implement battery-threshold widget
damian-ds7 Jul 14, 2026
b99af28
feat(battery-threshold): implement battery-threshold panel
damian-ds7 Jul 14, 2026
7b5d31d
feat(battery-threshold): add readme
damian-ds7 Jul 14, 2026
e129131
chore(battery-threshold): add dependencies field
damian-ds7 Jul 14, 2026
0f4d986
chore(battery-threshold): add thumbnail
damian-ds7 Jul 14, 2026
a91cfb1
docs(battery-threshold): add thumbnail to readme
damian-ds7 Jul 14, 2026
f085bf3
Merge branch 'noctalia-dev:main' into feat/battery-threshold
damian-ds7 Jul 14, 2026
e59954a
fix(battery-threshold): add thumbnail from generator
damian-ds7 Jul 14, 2026
050e38f
chore(battery-threshold): add license and icon fields to plugin.toml
damian-ds7 Jul 14, 2026
c979626
style(battery-threshold): replace tabs with spaces
damian-ds7 Jul 14, 2026
1c6825e
style(battery-threshold): unflatten translation files
damian-ds7 Jul 14, 2026
8ffbe1e
refactor(battery-threshold): use translations for widget tooltip
damian-ds7 Jul 14, 2026
0048780
chore(battery-threshold): replace raw strings with translations
damian-ds7 Jul 14, 2026
e281b89
chore(battery-threshold): remove panel ipc event
damian-ds7 Jul 14, 2026
19129a3
chore(battery-threshold): check error on file read
damian-ds7 Jul 14, 2026
13a8547
chore(battery-threshold): remove `setUpdateInterval` calls
damian-ds7 Jul 14, 2026
7ec5f4e
style(battery-threshold): fix formatting
damian-ds7 Jul 14, 2026
e178b92
chore(battery-threshold): remove tags capital letters
damian-ds7 Jul 14, 2026
a836f13
Update plugin.toml
ItsLemmy Jul 15, 2026
62bee30
chore(battery-threshold): remove de fr and tr translations
damian-ds7 Jul 15, 2026
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
7 changes: 7 additions & 0 deletions battery-threshold/99-battery-threshold.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 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"

80 changes: 80 additions & 0 deletions battery-threshold/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Battery Threshold

![thumbnail](./thumbnail.webp)

A plugin for Noctalia Shell to control the battery threshold on laptops, helping
extend battery lifespan. This plugin only works if your laptop supports charge
threshold control (as exported by the kernel in sysfs). The plugin looks like
this in action:

## Features

- **Bar Widget**: Shows current battery threshold in the bar
- **Panel**: Adjust battery threshold with a slider (40-100%)
- **Persistent Settings**: Saves and restores threshold across reboots

## Usage

Add the bar widget to your bar. Click to open the panel and adjust the battery
threshold using the slider.

### Panel Controls

- Drag the slider to set battery threshold (40-100%)
- Changes are applied immediately
- Settings persist across reboots

## Setup (Required)

This plugin requires write access to the battery threshold sysfs file. You can
configure permissions in one of three ways:

### 1. Via UI (Recommended)

If the plugin is loaded and read-only, open the panel and click the **Configure
Permissions** button. This will trigger a `pkexec` dialog prompting for
authentication to install the udev rules automatically.

### 2. Via IPC Command

You can trigger the automated setup from the terminal:

```bash
noctalia msg plugin damian-ds7/battery-threshold setup
```

### 3. Manual Fallback

If you do not have a Polkit agent running, you can manually run the included
script:

```bash
sudo ./setup_rules.sh
```

**Note:** A logout/reboot is required for the new group membership
(`battery_ctl`) to take effect.

## IPC Commands

```bash
# Toggle panel
noctalia msg panel-toggle damian-ds7/battery-threshold:panel

# Set threshold
noctalia msg plugin damian-ds7/battery-threshold:service all set <value>
```

## Troubleshooting

- **Read-only mode**: Ensure udev rule is installed, and you're in the correct
group
- **Not available**: Your laptop may not support charge threshold control, or
select the correct battery in the settings menu
- **Changes not saving**: Check write permissions on the sysfs file

## Requirements

- Laptop with battery charge threshold support (ThinkPad, ASUS, etc.)
- Tested on Asus Zenbook 14 UX3405
- Tested on Asus TUF Gaming F15 FX506L
172 changes: 172 additions & 0 deletions battery-threshold/panel.luau
Original file line number Diff line number Diff line change
@@ -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)
43 changes: 43 additions & 0 deletions battery-threshold/plugin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
id = "damian-ds7/battery-threshold"
name = "Battery Threshold Control"
version = "1.0.0"
min_noctalia = "5.0.0"
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 = ["polkit"]

[[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
Loading