feat(lid-guard): add lid-close suspension inhibitor (#30)

* feat(lid-guard): add lid-close suspension inhibitor

* docs(lid-guard): replace plugin thumbnail
This commit is contained in:
pedro
2026-07-23 23:46:04 -04:00
committed by GitHub
parent 79583e36de
commit 98e7cc1ea1
7 changed files with 330 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
# Lid Guard
Lid Guard keeps a laptop awake when its lid is closed, allowing AI agents,
builds, downloads, and other background jobs to continue without changing the
system-wide lid policy.
## Plugin
| Field | Value |
| --- | --- |
| ID | `8bury/lid-guard` |
| Entries | Bar widget: `lid-guard`; shortcut: `lid-guard-toggle`; service: `lid-guard-service` |
## Requirements
Install `systemctl`, `systemd-run`, and `systemd-inhibit` from systemd, plus
`sleep` from coreutils, on `PATH`. The active user session must use
`systemd-logind` and have a working user service manager.
## Usage
Add the `lid-guard` widget to a bar. Left-click the shield to toggle the mode;
right-click it to refresh the detected state. A highlighted shield means the
lid-close inhibitor is active.
To use the same toggle from the Control Center, add the `lid-guard-toggle`
shortcut in Settings → Control Center → Shortcuts.
The service entry `lid-guard-service` owns the inhibitor and can also be
controlled through IPC:
```sh
noctalia msg plugin 8bury/lid-guard:lid-guard-service all enable
noctalia msg plugin 8bury/lid-guard:lid-guard-service all disable
noctalia msg plugin 8bury/lid-guard:lid-guard-service all toggle
noctalia msg plugin 8bury/lid-guard:lid-guard-service all refresh
```
## IPC
The `enable`, `disable`, and `toggle` events change the inhibitor state.
`refresh` and `status` re-check the transient user service without changing it.
IPC events take no payload.
## Notes
Lid Guard makes no network requests and writes no files. It spawns
`systemctl`, `systemd-run`, `systemd-inhibit`, and `sleep` as the current user.
When enabled, it creates the transient user service
`noctalia-lid-guard.service`; disabling the mode stops that service.
The inhibitor covers only `handle-lid-switch`. It does not prevent suspension
requested by an idle daemon, a power menu, or another explicit command, and it
does not modify `logind.conf`. The mode ends when the user session or user
service manager stops. If Noctalia is unavailable, restore the normal lid
behavior with:
```sh
systemctl --user stop noctalia-lid-guard.service
```
+22
View File
@@ -0,0 +1,22 @@
id = "8bury/lid-guard"
name = "Lid Guard"
version = "1.0.0"
plugin_api = 3
author = "8bury"
license = "MIT"
icon = "shield-lock"
description = "Keep the laptop awake when its lid is closed, so background jobs can continue running."
tags = ["bar", "indicator", "productivity", "service", "shortcut", "system", "utility"]
dependencies = ["systemctl", "systemd-run", "systemd-inhibit", "sleep"]
[[service]]
id = "lid-guard-service"
entry = "service.luau"
[[widget]]
id = "lid-guard"
entry = "widget.luau"
[[shortcut]]
id = "lid-guard-toggle"
entry = "shortcut.luau"
+127
View File
@@ -0,0 +1,127 @@
--!nonstrict
-- Owns the systemd inhibitor. Widgets and shortcuts communicate with this
-- singleton through noctalia.state.
local UNIT = "noctalia-lid-guard.service"
local CHECK_COMMAND = `systemctl --user is-active --quiet {UNIT}`
local START_COMMAND = `systemctl --user reset-failed {UNIT} >/dev/null 2>&1 || true; ` .. table.concat({
`systemd-run --user --quiet --collect --unit={UNIT}`,
`--description='Noctalia Lid Guard'`,
`systemd-inhibit --what=handle-lid-switch`,
`--who='Noctalia Lid Guard'`,
`--why='Keep background jobs running while the laptop lid is closed'`,
`--mode=block sleep infinity`,
}, " ")
local STOP_COMMAND = `systemctl --user stop {UNIT}`
local active = false
local busy = false
local available = noctalia.commandExists("systemctl")
and noctalia.commandExists("systemd-run")
and noctalia.commandExists("systemd-inhibit")
and noctalia.commandExists("sleep")
local revision = 0
local function publish(errorMessage)
revision += 1
noctalia.state.set("lid_guard_status", {
active = active,
busy = busy,
available = available,
error = errorMessage or "",
revision = revision,
})
end
local function refresh(callback)
if not available then
active = false
publish(noctalia.tr("error.missing_dependencies"))
if callback then callback(false) end
return
end
noctalia.runAsync(CHECK_COMMAND, function(result)
active = result.exitCode == 0
publish()
if callback then callback(active) end
end)
end
local function finishToggle(expectedActive, result)
if result.exitCode ~= 0 then
busy = false
local detail = noctalia.string.trim(result.stderr or "")
if detail == "" then detail = noctalia.tr("error.command_failed") end
publish(detail)
noctalia.notifyError(noctalia.tr("title"), detail)
return
end
refresh(function(currentActive)
busy = false
active = currentActive
publish()
if currentActive ~= expectedActive then
noctalia.notifyError(noctalia.tr("title"), noctalia.tr("error.state_mismatch"))
elseif currentActive then
noctalia.notify(noctalia.tr("notification.enabled.title"), noctalia.tr("notification.enabled.body"))
else
noctalia.notify(noctalia.tr("notification.disabled.title"), noctalia.tr("notification.disabled.body"))
end
end)
end
local function setActive(nextActive)
if busy or not available then return end
busy = true
publish()
if nextActive then
noctalia.runAsync(START_COMMAND, function(result)
finishToggle(true, result)
end)
else
noctalia.runAsync(STOP_COMMAND, function(result)
finishToggle(false, result)
end)
end
end
noctalia.state.watch("lid_guard_command", function(command)
if type(command) ~= "table" then return end
if command.action == "toggle" then
refresh(function(currentActive)
setActive(not currentActive)
end)
elseif command.action == "enable" then
setActive(true)
elseif command.action == "disable" then
setActive(false)
elseif command.action == "refresh" then
refresh()
end
end)
noctalia.setUpdateInterval(5000)
refresh()
function update()
if not busy then refresh() end
end
function onIpc(event, _payload)
if event == "toggle" then
refresh(function(currentActive)
setActive(not currentActive)
end)
elseif event == "enable" then
setActive(true)
elseif event == "disable" then
setActive(false)
elseif event == "refresh" or event == "status" then
refresh()
end
end
+34
View File
@@ -0,0 +1,34 @@
--!nonstrict
local status = noctalia.state.get("lid_guard_status") or {
active = false,
busy = false,
available = true,
}
local requestId = 0
local function render()
local active = status.active == true
shortcut.setLabel(if active then noctalia.tr("shortcut.active") else noctalia.tr("shortcut.inactive"))
shortcut.setIcon("shield-lock", "shield-off")
shortcut.setActive(active)
shortcut.setEnabled(status.available ~= false and status.busy ~= true)
end
noctalia.state.watch("lid_guard_status", function(value)
if type(value) == "table" then
status = value
render()
end
end)
render()
function onClick()
if status.available == false or status.busy == true then return end
requestId += 1
noctalia.state.set("lid_guard_command", {
action = "toggle",
requestId = `shortcut-{requestId}`,
})
end
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

+28
View File
@@ -0,0 +1,28 @@
{
"title": "Lid Guard",
"tooltip": {
"active": "Lid Guard is on — closing the lid will not suspend the laptop",
"inactive": "Lid Guard is off — click to keep the laptop awake with the lid closed",
"changing": "Changing Lid Guard state…",
"unavailable": "Lid Guard requires systemd"
},
"shortcut": {
"active": "Lid Guard On",
"inactive": "Lid Guard Off"
},
"notification": {
"enabled": {
"title": "Lid Guard enabled",
"body": "Closing the laptop lid will no longer suspend it."
},
"disabled": {
"title": "Lid Guard disabled",
"body": "The normal lid-close behavior has been restored."
}
},
"error": {
"missing_dependencies": "systemctl, systemd-run, systemd-inhibit, and sleep are required.",
"command_failed": "The systemd command failed.",
"state_mismatch": "The requested state could not be confirmed."
}
}
+59
View File
@@ -0,0 +1,59 @@
--!nonstrict
local status = noctalia.state.get("lid_guard_status") or {
active = false,
busy = false,
available = true,
}
local requestId = 0
local function render()
if status.available == false then
barWidget.setGlyph("shield-x")
barWidget.setGlyphColor("error")
barWidget.setTooltip(noctalia.tr("tooltip.unavailable"))
elseif status.busy == true then
barWidget.setGlyph("loader-2")
barWidget.setGlyphColor("primary")
barWidget.setTooltip(noctalia.tr("tooltip.changing"))
elseif status.active == true then
barWidget.setGlyph("shield-lock")
barWidget.setGlyphColor("tertiary")
barWidget.setTooltip(noctalia.tr("tooltip.active"))
else
barWidget.setGlyph("shield-off")
barWidget.setGlyphColor("on_surface_variant")
barWidget.setTooltip(noctalia.tr("tooltip.inactive"))
end
end
noctalia.state.watch("lid_guard_status", function(value)
if type(value) == "table" then
status = value
render()
end
end)
render()
function onClick()
if status.available == false then
noctalia.notifyError(noctalia.tr("title"), status.error or noctalia.tr("error.missing_dependencies"))
return
end
if status.busy == true then return end
requestId += 1
noctalia.state.set("lid_guard_command", {
action = "toggle",
requestId = `widget-{requestId}`,
})
end
function onRightClick()
requestId += 1
noctalia.state.set("lid_guard_command", {
action = "refresh",
requestId = `widget-refresh-{requestId}`,
})
end