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_RDONLY on 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.so in 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.acline0 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 TIMEOUT seconds 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.suspend owns lid close : suspend via acpiconf -k 0
  • blight owns idle : backlight via /dev/backlight/backlight0, suspend via blight-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