A lightweight Rust daemon for managing systemd-logind idle inhibitor locks with per-session D-Bus event system.
📚 Wiki - Comprehensive guides and integration examples:
logind-idle-control provides per-GUI-session idle inhibition control through systemd-logind's native D-Bus API. Each graphical session (TTY) gets its own isolated daemon instance.
Key Features:
- 🎯 Per-session isolation - Each GUI session (TTY) has independent control
- ✨ Native systemd-logind integration via D-Bus
- 📡 Event-driven D-Bus interface - consumers listen to signals directly
- 💾 Session-specific persistent state
- 🔒 Auto-disable on screen lock (configurable, listens to logind Lock signal)
- 🎨 Pure D-Bus interface - UI consumers listen directly, no wrapper scripts
Critical Design: Each graphical session runs its own daemon instance:
TTY1 (Session 2): /com/logind/IdleControl/session_2
→ State: enabled
→ Inhibitor: active
TTY2 (Session 3): /com/logind/IdleControl/session_3
→ State: disabled
→ Inhibitor: inactive
Sessions don't interfere with each other.
- Service:
com.logind.IdleControl(shared session bus) - Object Path:
/com/logind/IdleControl/session_<SESSION_ID> - Interface:
com.logind.IdleControl
| Method | Returns | Description |
|---|---|---|
Enable |
boolean enabled |
Enable idle inhibitor for this session |
Disable |
boolean enabled |
Disable idle inhibitor for this session |
Toggle |
boolean enabled |
Toggle idle inhibitor state for this session |
Missing daemon is an error: the CLI and tray call these methods on com.logind.IdleControl and do not report success unless the daemon applied the change.
| Property | Type | Description |
|---|---|---|
Enabled |
boolean |
Whether this daemon currently holds the logind idle inhibitor |
| Signal | Parameters | Description |
|---|---|---|
StateChanged |
boolean enabled |
Emitted when inhibitor state changes |
Add the [mason] repo to /etc/pacman.conf:
[mason]
# Import the signing key first: https://github.com/MasonRhodesDev/arch-repo#use-it
SigLevel = Required DatabaseRequired
Server = https://masonrhodesdev.github.io/arch-repo/x86_64sudo pacman -Syu logind-idle-controlsudo dnf copr enable solaris765/logind-idle-control
sudo dnf install logind-idle-controlmake # builds daemon + tray
sudo make install # DESTDIR/PREFIX-correct; default PREFIX=/usr/localThe package ships both user units and the tray icons; enable per user:
systemctl --user enable --now logind-idle-control.service
systemctl --user enable --now logind-idle-control-tray.service # optionalThe tray is optional and requires an SNI-compatible tray host that provides org.kde.StatusNotifierWatcher on the session bus. If the watcher is not available during login, the tray service waits for it instead of restart-looping.
The CLI automatically detects which graphical session you're in:
logind-idle-control enable # Enable idle inhibitor
logind-idle-control disable # Disable idle inhibitor
logind-idle-control toggle # Toggle state
logind-idle-control status # Check current status
logind-idle-control monitor # Monitor state changes via D-Bus
logind-idle-control daemon # Run daemon (typically started by systemd)Config file: ~/.config/logind-idle-control/config.toml
state_on_start = false # Enable inhibitor when daemon starts
disable_on_lock = true # Auto-disable when screen locked
log_level = "info" # Logging verbosityUI applications can monitor idle inhibitor state via D-Bus signals directly.
The bundled tray icon in Waybar: inhibitor enabled (full cup) vs disabled (crossed-out cup).
Your UI module should:
- Detect current session ID via
loginctl - Connect to session-specific D-Bus path
- Listen for
StateChangedsignals - Read initial state from:
$XDG_RUNTIME_DIR/logind-idle-control-session-<ID>.state
session_id = get_session_id()
object_path = "/com/logind/IdleControl/session_" + session_id
# Read initial state
state_file = "$XDG_RUNTIME_DIR/logind-idle-control-session-{session_id}.state"
display_icon(read(state_file))
# Listen for changes
dbus_monitor("com.logind.IdleControl", object_path, "StateChanged", callback)
#!/bin/bash
SESSION=$(loginctl session-status | head -1 | awk '{print $1}')
STATE_FILE="$XDG_RUNTIME_DIR/logind-idle-control-session-${SESSION}.state"
if [[ -f "$STATE_FILE" ]] && [[ "$(cat $STATE_FILE)" == "1" ]]; then
echo "enabled"
else
echo "disabled"
fiFor real-time updates, use the monitor command or listen to D-Bus StateChanged signals directly.
$XDG_RUNTIME_DIR/logind-idle-control-session-2.state # Session 2
$XDG_RUNTIME_DIR/logind-idle-control-session-3.state # Session 3
#!/bin/bash
logind-idle-control disable
hyprlockThe daemon auto-disables on lock if disable_on_lock = true.
bind = $mainMod, I, exec, logind-idle-control toggle
# TTY1
$ loginctl session-status
2 - user (1000)
$ logind-idle-control enable
Idle inhibitor enabled
# TTY2 (same user, different session)
$ loginctl session-status
3 - user (1000)
$ logind-idle-control status
0 # Independent!# Check daemon
systemctl --user status logind-idle-control.service
# Check tray service
systemctl --user status logind-idle-control-tray.service
# Check session
loginctl session-status | head -1
# Check inhibitor lock
systemd-inhibit --list | grep logind-idle-control
# Monitor D-Bus
SESSION=$(loginctl session-status | head -1 | awk '{print $1}')
dbus-monitor --session "path='/com/logind/IdleControl/session_${SESSION}'"
# Check state file
cat $XDG_RUNTIME_DIR/logind-idle-control-session-${SESSION}.state
# Follow tray logs
journalctl --user -u logind-idle-control-tray.service -fflowchart TD
CLI["CLI: logind-idle-control enable / disable / toggle"]
BIND["Hyprland keybind (exec logind-idle-control toggle)"]
TRAY["Tray icon — ksni StatusNotifierItem (requires a StatusNotifierWatcher host, e.g. sni-watcher or the bar's own)"]
BIND --> CLI
CLI -->|"Enable / Disable / Toggle methods on com.logind.IdleControl at /com/logind/IdleControl/session_N"| CTRL
TRAY -->|"click: Toggle method on the same object path"| CTRL
subgraph DAEMON ["Per-session daemon"]
CTRL["control method handler"]
LOCK["InhibitorLock — RAII holder of the logind fd (Drop = close fd = release)"]
STATE["state file: $XDG_RUNTIME_DIR/logind-idle-control-session-N.state"]
CTRL --> LOCK
CTRL --> STATE
end
LOCK -->|"org.freedesktop.login1.Manager Inhibit('idle', ...) returns held fd"| LOGIND["systemd-logind"]
LOGIND -->|"Session Lock signal: auto-release inhibitor"| CTRL
LOGIND -->|"Session Unlock signal: re-acquire inhibitor"| CTRL
CTRL -->|"StateChanged(enabled) D-Bus signal: tray updates icon"| TRAY
TTY1 (Session 2):
systemd graphical-session.target
→ Starts logind-idle-control daemon instance 1
→ Detects session 2 via GetSessionByPID()
→ Creates D-Bus path: /com/logind/IdleControl/session_2
→ State file: .../session-2.state
→ Listens: /org/freedesktop/login1/session/_32
TTY2 (Session 3):
systemd graphical-session.target
→ Starts logind-idle-control daemon instance 2
→ Detects session 3 via GetSessionByPID()
→ Creates D-Bus path: /com/logind/IdleControl/session_3
→ State file: .../session-3.state
→ Listens: /org/freedesktop/login1/session/_33
Both instances run independently on the same session D-Bus.
- Daemon calls
org.freedesktop.login1.Manager.GetSessionByPID() - Verifies session type is
x11orwayland(rejects TTY/SSH) - Uses session ID for all paths
- Daemon calls
org.freedesktop.login1.Manager.Inhibit("idle", ...) - logind returns file descriptor (FD)
- FD held open = inhibitor active
- FD closed = inhibitor released (automatic cleanup)
When disable_on_lock = true, daemon listens to session-specific org.freedesktop.login1.Session.Lock signal and disables inhibitor before lock screen appears.
journalctl --user -u logind-idle-control.service -f
loginctl session-status # Must show Type: x11 or waylandThe tray binary requires an SNI-compatible tray host that owns org.kde.StatusNotifierWatcher. On login, the tray service may start before the watcher exists. That is now expected: the process stays alive and waits locally until the watcher appears.
Expected tray log messages:
StatusNotifierWatcher not available yet; waiting for tray hostStatusNotifierWatcher detected; registering tray iconTray icon registered
If the watcher disappears during registration, the tray retries and logs:
Tray registration raced with watcher disappearance; retrying
For actual tray startup failures unrelated to watcher availability, inspect:
systemctl --user status logind-idle-control-tray.service
journalctl --user -u logind-idle-control-tray.service -floginctl show-session $(loginctl session-status | head -1 | awk '{print $1}') -p Type
# Must be x11 or wayland, not ttySESSION=$(loginctl session-status | head -1 | awk '{print $1}')
dbus-monitor --session "interface='com.logind.IdleControl'"MIT

