Files
community-plugins/ideapad-conservation-mode/conservation_mode.luau
T

145 lines
4.5 KiB
Luau

--!nonstrict
-- Control-center [[shortcut]] entry: toggles Lenovo IdeaPad/Legion battery
-- Conservation Mode (caps charging around ~60% to preserve battery health)
-- via the ideapad_acpi driver's conservation_mode sysfs attribute.
--
-- Writing that attribute needs root by default. The first time a write
-- fails, onClick() opens a terminal running a bundled one-time setup script
-- (scripts/setup-permissions.sh) via sudo, instead of just failing -- see
-- that script for what it changes. Every write after that is a plain,
-- unprivileged noctalia.writeFile().
local DRIVER_DIR = "/sys/bus/platform/drivers/ideapad_acpi"
local ATTR_NAME = "conservation_mode"
-- The ACPI instance name (e.g. "VPC2004:00") isn't hardcoded: we discover it
-- by scanning the driver's bound devices, so this keeps working if it ever
-- differs. Cached in plugin state under a generic key since other future
-- entries (fan mode, camera power, ...) live under the same device dir.
local function findDevicePath()
local cached = noctalia.state.get("ideapad_device_path")
if cached then
return cached
end
local entries = noctalia.listDir(DRIVER_DIR)
if not entries then
return nil
end
for _, name in ipairs(entries) do
if name ~= "uevent" and name ~= "bind" and name ~= "unbind" and name ~= "module" then
local path = DRIVER_DIR .. "/" .. name
if noctalia.fileExists(path .. "/" .. ATTR_NAME) then
noctalia.state.set("ideapad_device_path", path)
return path
end
end
end
return nil
end
local function attrPath()
local devicePath = findDevicePath()
if not devicePath then
return nil
end
return devicePath .. "/" .. ATTR_NAME
end
-- true/false for a known state, nil when the attribute couldn't be read.
local function readState()
local path = attrPath()
if not path then
return nil
end
local contents = noctalia.readFile(path)
if not contents then
return nil
end
return noctalia.string.trim(contents) == "1"
end
local function render(on, enabled)
shortcut.setLabel(noctalia.tr("shortcut.label"))
shortcut.setIcon("battery-charging", "battery")
shortcut.setActive(on)
shortcut.setEnabled(enabled)
end
local on = readState()
render(on == true, on ~= nil)
if on == nil then
shortcut.setLabel(noctalia.tr("shortcut.na"))
end
function onClick()
local path = attrPath()
if not path then
noctalia.notifyError(noctalia.tr("title"), noctalia.tr("error.device_not_found"))
return
end
local current = readState()
if current == nil then
noctalia.notifyError(noctalia.tr("title"), noctalia.tr("error.read_failed"))
return
end
local flipped = not current
local ok, err = noctalia.writeFile(path, if flipped then "1" else "0")
if ok then
render(flipped, true)
noctalia.notify(
noctalia.tr("title"),
noctalia.tr(if flipped then "notify.enabled" else "notify.disabled")
)
return
end
render(current, true)
runSetup(err)
end
-- POSIX single-quote escaping: wraps s in '...', turning any embedded ' into
-- '\'' so the result is safe to splice into a shell command string. Needed
-- because pluginDir and $USER are attacker/environment-influenced strings
-- spliced into a command that runs through sh -lc.
local function shQuote(s)
return "'" .. string.gsub(s, "'", "'\\''") .. "'"
end
-- Not set up yet (or permissions regressed): run the bundled setup script in
-- a terminal via sudo. Deliberately not pkexec -- minimal Wayland setups
-- (niri, sway, ...) commonly have no polkit GUI agent registered, so pkexec
-- would fail silently with no prompt at all. A terminal + sudo has no such
-- dependency.
function runSetup(writeErr)
local pluginDir = noctalia.pluginDir()
if not pluginDir then
noctalia.notifyError(
noctalia.tr("title"),
noctalia.tr("error.write_failed", { error = writeErr or "permission denied" })
)
return
end
local setupScript = pluginDir .. "/scripts/setup-permissions.sh"
local user = noctalia.string.trim(noctalia.getenv("USER") or "")
local cmd = "sudo "
.. shQuote(setupScript)
.. " "
.. shQuote(user)
.. " && echo && echo 'Setup complete. Log out and back in, then click the tile again.' && read -r -p 'Press Enter to close...'"
if noctalia.runInTerminal(cmd) then
noctalia.notify(noctalia.tr("title"), noctalia.tr("notify.setup_needed"))
else
noctalia.notifyError(
noctalia.tr("title"),
noctalia.tr("error.write_failed_manual", { error = writeErr or "permission denied" })
)
end
end