blight : Backlight Idle Daemon for FreeBSD/KDE/Plasma/Wayland
Host: thinkpadt480s | Date: 2026-05-10 | Status: Deployed and running
blight is a backlight idle ‘daemon’ I made to use on KDE Plasma/Wayland on FreeBSD 15 because I needed something to dim the screen. It blanks the screen after a configurable idle timeout, suspends on battery after extended idle, and restores brightness on any input activity : no D-Bus, no logind, no powerdevil, no device polling. It is a shell script wrapping swayidle, which speaks the org_kde_kwin_idle Wayland protocol directly to KWin.
This is a rewrite. The original was a Python daemon that polled input devices directly. That approach had fundamental problems on FreeBSD and was replaced entirely with a sh wrapper around swayidle.
Redux
The original implementation was a Python daemon that opened /dev/psm0 and /dev/input/event* directly, using select() to watch for input activity. It worked initially but had three problems that made it untenable:
- CPU spin : the Logitech Bolt receiver streams constant EV_SYN sync events on its event nodes, making
select()return immediately in a tight loop and pegging one CPU core. - Input stealing : opening input devices with
O_RDONLYon FreeBSD consumes events from the stream. The compositor loses input, causing cursor drift and erratic behavior. - Wrong abstraction : polling raw devices to infer idle time is solving the wrong problem. The compositor already knows when the user is idle.
The old approach also required devfs rules to grant video group access to input/event* and atkbd0. Neither ended up being useful — atkbd0 cannot be opened by userspace while the kernel driver holds it, and input/event* access is moot once device polling was dropped. No devfs rules are needed for blight.
The rewrite drops Python entirely and replaces the device polling loop with swayidle, which gets idle time directly from KWin via the org_kde_kwin_idle Wayland protocol. The compositor tells swayidle when the idle threshold is crossed and when input resumes. No device access, no polling, no CPU overhead. The script itself is a thin sh wrapper for start/stop/status lifecycle management around swayidle.
What else was tried and dropped
Graduated dimming (80% → 50% → 0%) was tested but dropped. swayidle does not reset elapsed timeouts on resume, so waking from a dim state immediately re-fires the earlier timeout instead of restoring to 100%. A single blank timeout avoids this entirely.
What it is
blight blanks the screen after a configurable idle timeout and restores it on any input. After extended idle on battery, it suspends via a helper script (blight-sleep). It is a shell script. The heavy lifting is done by swayidle, which queries KWin directly via the Wayland idle protocol. No Python. No device access. No competing with the compositor for input events.
Every existing solution failed (for me) for a different reason:
- powerdevil : ships without
kded_powerdevil.soin the FreeBSD ports package. No KDED module means no suspend backend, no idle detection, no brightness control through KDE. A settings UI with no execution engine. - D-Bus idle interfaces (
org.kde.KIdleTime,org.freedesktop.ScreenSaver.GetSessionIdleTime) : not supported on this FreeBSD build. All return “service does not exist” or “not supported on this platform.” - xscreensaver / xss-lock : X11-only. Not available under KWin/Wayland.
- dimdaemon / YABD : Linux/systemd-only. Depend on
/sys/class/backlight/and logind. - Device polling via
/dev/psm0: steals input from KWin. Causes CPU spin from evdev EV_SYN noise on USB HID devices and cursor drift as the compositor loses events. Not viable.
swayidle works because KWin implements org_kde_kwin_idle, the KDE Wayland idle protocol. swayidle speaks it natively and gets idle time straight from the compositor without touching input devices at all.
No D-Bus. No logind. No powerdevil. No device polling. Just swayidle and
/dev/backlight/.
It works like this
Idle detection : swayidle + org_kde_kwin_idle
swayidle is a Wayland idle management daemon. On wlroots compositors it uses ext-idle-notify-v1. On KDE it falls back to org_kde_kwin_idle, which KWin implements. This means swayidle can register idle and resume callbacks with the compositor directly : the compositor tracks input activity internally and notifies swayidle when the idle threshold is crossed or input resumes.
Zero device access. Zero polling. Zero CPU overhead. The kernel does the work.
Brightness control : /dev/backlight/backlight0
The backlight(8) utility shipped with FreeBSD base talks to /dev/backlight/backlight0, exposed by drm-kmod (i915kms). The device is owned by root:video with group write permission. Any user in the video group controls it without doas.
Installation
1. Install swayidle
pkg install swayidle
2. Verify device access
# Should show crw-rw---- root video
ls -la /dev/backlight/backlight0
Add yourself to the video group if needed:
doas pw groupmod video -m YOUR_USER
# log out and back in
3. Save the scripts
Copy the blight script from the Script section below:
mkdir -p ~/bin
# paste script content into ~/bin/blight
chmod +x ~/bin/blight
Ensure ~/bin is in your $PATH:
echo $PATH | grep "$HOME/bin"
Install the sleep helper:
doas cp blight-sleep /usr/local/bin/blight-sleep
doas chmod +x /usr/local/bin/blight-sleep
4. Autostart with KDE session
KDE executes scripts in ~/.config/plasma-workspace/env/ synchronously at session start, before the desktop loads. Calling blight start directly blocks startup if swayidle cannot connect to the compositor in time. The fix is to run the entire wait and launch in a background subshell so session startup is never blocked:
cat > ~/.config/plasma-workspace/env/blight.sh << 'BEOF'
#!/bin/sh
(
i=0
while [ $i -lt 30 ]; do
[ -S "/var/run/user/$(id -u)/wayland-0" ] && exec blight start
sleep 1
i=$((i + 1))
done
) &
BEOF
chmod +x ~/.config/plasma-workspace/env/blight.sh
The subshell polls for the Wayland socket up to 30 seconds, then hands off to blight start once it appears. KDE login is never blocked.
Usage
blight # start (default)
blight start # same
blight stop # stop
blight status # show running state and current brightness
Log file: /tmp/blight.log
PID file: /tmp/blight.pid
Configuration
Edit the constants at the top of the script:
TIMEOUT=300 # seconds before blanking to 0%
TIMEOUT_SLEEP=600 # seconds before sleep (battery only)
BACKLIGHT=/dev/backlight/backlight0
SLEEP_CMD=/usr/local/bin/blight-sleep
blight-sleep
Battery-conditional suspend is handled by a standalone script at /usr/local/bin/blight-sleep. swayidle calls it after TIMEOUT_SLEEP seconds of idle. It checks hw.acpi.acline – 0 means on battery, 1 means on AC.
On battery it suspends. On AC it blocks indefinitely (sleep 86400) until swayidle kills it on resume. This is intentional : if the script exits immediately on AC, swayidle interprets the process exit as a state change and fires a spurious resume callback, restoring brightness without any actual input.
#!/bin/sh
if [ "$(sysctl -n hw.acpi.acline)" = "0" ]; then
acpiconf -s 3
else
sleep 86400
fi
Behavior with screen lock
blight gets its idle time from KWin, which tracks all input. Any input resets the timer : keyboard, mouse, trackpad, TrackPoint. Behavior when locked:
- Lock screen and walk away : screen goes dark after
TIMEOUTseconds of no input (default 5 minutes) - Touch anything : brightness restores, lock screen is already visible
powerdevil DPMS may also fire on idle via KWin. Both can coexist : powerdevil handles DPMS blanking, blight handles backlight. Check ~/.config/powermanagementprofilesrc for DPMSControl settings if you see double-blanking behavior.
Interaction with rc.suspend
blight and rc.suspend are independent:
rc.suspendowns lid close : suspend viaacpiconf -k 0blightowns idle : backlight via/dev/backlight/backlight0, suspend viablight-sleep(battery only)- No coordination needed : different events, different kernel interfaces
One known gap: if rc.suspend fires while blight has dimmed the screen, brightness restores on next input after resume. If the lock screen appears before that, the screen will be dark until touched. Acceptable behavior.
Future improvement: add a devd rule that sends SIGUSR1 to blight’s swayidle pid on ACPI resume to force immediate restore.
Troubleshooting
Screen doesn’t dim
blight status
tail -f /tmp/blight.log
Confirm swayidle is running and WAYLAND_DISPLAY is set correctly in the environment.
Brightness doesn’t restore on input
swayidle’s resume callback fires on any input KWin sees. If it’s not firing, swayidle may have lost its Wayland connection. Check:
blight stop
blight start
tail -f /tmp/blight.log
blight not starting on login
ls -la ~/.config/plasma-workspace/env/blight.sh
cat ~/.config/plasma-workspace/env/blight.sh
Ensure the file is executable and contains blight start.
Restore brightness manually
backlight -f /dev/backlight/backlight0 100
Spurious resume after blank on AC
If the screen restores brightness immediately after blanking with no input, blight-sleep may be the original version that exits immediately on AC. Replace it with the blocking version shown in the blight-sleep section above.
blight script
#!/bin/sh
# blight - backlight idle daemon for FreeBSD KDE Plasma Wayland
# Uses swayidle + KWin org_kde_kwin_idle Wayland protocol.
# No device polling. No input stealing. No CPU spin.
#
# Usage: blight [start|stop|status]
TIMEOUT=300 # seconds before blanking to 0%
TIMEOUT_SLEEP=600 # seconds before sleep (battery only)
BACKLIGHT=/dev/backlight/backlight0
SLEEP_CMD=/usr/local/bin/blight-sleep
PIDFILE=/tmp/blight.pid
LOGFILE=/tmp/blight.log
export XDG_RUNTIME_DIR=/var/run/user/$(id -u)
export WAYLAND_DISPLAY=wayland-0
CMD=${1:-start}
case "$CMD" in
stop)
if [ -f "$PIDFILE" ]; then
kill "$(cat $PIDFILE)" 2>/dev/null
rm -f "$PIDFILE"
echo "blight stopped"
else
echo "blight not running"
fi
;;
status)
if [ -f "$PIDFILE" ] && kill -0 "$(cat $PIDFILE)" 2>/dev/null; then
echo "blight running (pid $(cat $PIDFILE))"
echo "brightness: $(backlight -f $BACKLIGHT 2>/dev/null | awk '{print $2}')"
else
echo "blight not running"
fi
;;
start)
if [ -f "$PIDFILE" ] && kill -0 "$(cat $PIDFILE)" 2>/dev/null; then
echo "blight already running (pid $(cat $PIDFILE))"
exit 1
fi
rm -f "$PIDFILE"
i=0
while [ $i -lt 30 ]; do
[ -S "/var/run/user/$(id -u)/wayland-0" ] && break
sleep 1
i=$((i + 1))
done
if [ ! -S "/var/run/user/$(id -u)/wayland-0" ]; then
echo "blight: timed out waiting for Wayland socket" >> "$LOGFILE"
exit 1
fi
swayidle -w \
timeout "$TIMEOUT" "backlight -f $BACKLIGHT 0" \
timeout "$TIMEOUT_SLEEP" "$SLEEP_CMD" \
resume "backlight -f $BACKLIGHT 100" \
>> "$LOGFILE" 2>&1 &
echo $! > "$PIDFILE"
echo "blight started (pid $!)"
;;
*)
echo "Usage: blight [start|stop|status]"
exit 1
;;
esac