blight : Backlight Idle Daemon for FreeBSD/KDE/Plasma/Wayland

Host: thinkpadt480s | Date: 2026-05-04 | Status: Deployed and running

blight is a FreeBSD-native backlight idle python script (written with the goldfish brain) running as a user daemon for KDE Plasma on Wayland. It dims the screen to 0 after a configurable idle timeout and restores it on any trackpad or TrackPoint input : talking directly to /dev/psm0 and /dev/backlight/backlight0. No D-Bus. No logind. No powerdevil.

What it is

blight is a FreeBSD-native backlight idle daemon written in python for KDE Plasma on Wayland. It dims the screen to 0 after a configurable idle timeout and restores it on any trackpad or TrackPoint input.

It was built because every existing solution fails for a different reason: some are X11-only and simply don’t exist under Wayland, one is wlroots-only and won’t run with KWin, others are systemd-only and depend on Linux kernel interfaces, and the KDE-native option ships broken in FreeBSD ports.

  • 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. It’s a settings UI with no execution engine. Not fundamentally Linux-only, just broken on FreeBSD ports.
  • 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. Run fine on FreeBSD, but not available under KWin/Wayland.
  • swayidle : wlroots compositors only (Sway, Wayfire). Would work on FreeBSD with a wlroots compositor, but does not work with KDE KWin.
  • dimdaemon / YABD : systemd-only. Depend on /sys/class/backlight/ and logind, both Linux-specific.

blight bypasses all of this entirely. It talks directly to the kernel via two device nodes that work unconditionally:

  • /dev/psm0 : PS/2 trackpad and TrackPoint input stream
  • /dev/backlight/backlight0 : DRM backlight interface exposed by i915kms

No D-Bus. No logind. No powerdevil. No udev. Pure FreeBSD.


Why it works

Input detection: select() on /dev/psm0

The PS/2 trackpad (psm0) is a character device that streams raw input packets whenever the trackpad or TrackPoint is touched. blight opens it O_NONBLOCK and calls select() with a 2-second timeout in a tight loop.

  • If select() returns the fd as readable: activity happened. Drain the buffer, update last_activity timestamp, restore brightness if dimmed.
  • If select() times out: check elapsed time since last_activity. If >= IDLE_TIMEOUT, dim.

This is efficient : select() blocks in the kernel until input arrives or timeout expires. CPU usage is effectively zero.

Brightness control: /dev/backlight/backlight0

The backlight(8) utility shipped with FreeBSD base talks to /dev/backlight/backlight0, which is exposed by drm-kmod (i915kms). The device is owned by root:video with group write permission, so any user in the video group can control it without doas.

johnny is already in the video group (required for GPU access), so no additional permissions are needed.

Brightness save/restore

Before dimming, blight reads the current brightness and saves it in memory. On restore, it writes that saved value back. If the daemon is killed while dimmed, the SIGTERM handler restores brightness before exit.


Installation

1. Install Python 3

blight requires Python 3 (no third-party packages). Install from ports if not already present:

pkg install python3

Verify the shebang matches your install:

which python3
# should be /usr/local/bin/python3

2. Save the script

Copy the full script from the Script section at the bottom of this page and save it:

mkdir -p ~/bin
# paste the script content into ~/bin/blight
chmod +x ~/bin/blight

Ensure ~/bin is in your $PATH (it is by default in zsh on this system):

echo $PATH | grep -o "$HOME/bin"

3. Verify device access

# Should show crw-rw---- root video
ls -la /dev/backlight/backlight0

# Should show crw-rw-rw- root wheel (world readable)
ls -la /dev/psm0

4. Autostart with KDE session

cat > ~/.config/plasma-workspace/env/blight.sh << 'EOF'
#!/bin/sh
blight start
EOF
chmod +x ~/.config/plasma-workspace/env/blight.sh

KDE executes all scripts in ~/.config/plasma-workspace/env/ at session start, before the desktop loads.


Usage

blight                   # start (daemonizes)
blight start             # same
blight stop              # stop and restore brightness
blight status            # show running state and current brightness
blight -f                # run in foreground (no daemonize)
blight -f -v             # foreground + verbose logging to stderr
blight -t 60             # start with 60 second timeout
blight -f -v -t 15       # foreground, verbose, 15s timeout (testing)

Log file: /tmp/blight.log
PID file: /tmp/blight.pid


Configuration

Edit the constants at the top of blight.py:

IDLE_TIMEOUT  = 300           # seconds before blanking (default: 5 min)
BACKLIGHT     = "/dev/backlight/backlight0"
INPUT_DEVICES = ["/dev/psm0", "/dev/input/event5", "/dev/input/event10"]  # trackpad (PS/2), Logitech Bolt mouse
PIDFILE       = "/tmp/blight.pid"
LOGFILE       = "/tmp/blight.log"
SELECT_CHUNK  = 2.0           # select() polling interval in seconds

INPUT_DEVICES is a list — blight opens each one at startup and skips any that are unavailable. Add or remove entries to match your hardware. event5 and event10 are the Logitech Bolt receiver nodes on this machine; yours may differ.


Behavior with screen lock

blight does not know the screen is locked. It only watches for trackpad input. Behavior when locked:

  • Lock screen and walk away → screen goes dark after IDLE_TIMEOUT seconds of no trackpad input (default 5 minutes)
  • Lock screen and keep using trackpad → screen stays lit
  • Touch trackpad while screen is dark → brightness restores, lock screen is already visible

There is no “lock = immediate blank” behavior. That would require watching the KScreenLocker state, which is a future addition.

powerdevil DPMS (screen blank via KWin) is configured separately and may also fire on idle. Check ~/.config/powermanagementprofilesrc for DPMSControl settings. Both can coexist : powerdevil handles DPMS blanking through KWin, blight handles backlight control through the kernel.


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
  • No coordination needed : they operate on different events through different kernel interfaces

One known gap: if rc.suspend fires while blight has dimmed the screen, on resume the brightness will be restored by blight on next trackpad activity. If the lock screen appears before that, the screen will be dark until touched. Acceptable behavior.

A future improvement: add a devd rule that sends SIGUSR1 to blight on ACPI resume, triggering an immediate brightness restore.


Future additions

Keyboard wake support

/dev/atkbd0 is the PS/2 keyboard device. A devfs rule can grant group access but the kernel keyboard driver holds an exclusive lock — any attempt to open it returns [Errno 16] Device busy. Granting permission doesn’t help; the conflict is in the driver.

The practical path is evdev passthrough: FreeBSD’s evdev kernel module can expose a synthetic /dev/input/eventX node that mirrors atkbd0 events without conflicting with the existing driver. If loaded, the keyboard would appear as an additional event node and could be added to INPUT_DEVICES with no other changes. Check whether it’s already active:

kldstat | grep evdev
ls /dev/input/

If a new event node appears and responds to keypresses, add it to INPUT_DEVICES.

Immediate blank on lock

Watch for KScreenLocker activation via polling kscreenlocker_greet process presence, or add a signal hook. When lock detected, call set_brightness(0) immediately without waiting for idle timeout.

devd resume hook

Add to /etc/devd.conf:

notify 10 {
    match "system"    "ACPI";
    match "subsystem" "Resume";
    action "pkill -SIGUSR1 -F /tmp/blight.pid";
};

Then add a SIGUSR1 handler in blight that forces brightness restore.


Troubleshooting

Screen doesn’t dim

tail -f /tmp/blight.log
blight status

Check that blight is running and the timeout hasn’t been changed.

Brightness doesn’t restore on trackpad touch

blight stop
blight -f -v -t 15

Run in foreground and watch the log. If “Activity” lines don’t appear when touching the trackpad, psm0 may be exclusively held by another process.

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

blight python script

#!/usr/local/bin/python3
"""
blight - backlight idle daemon for FreeBSD KDE Plasma Wayland
Watches input devices for activity via select().
After IDLE_TIMEOUT seconds of no activity, dims backlight to 0.
On any activity while dimmed, restores previous brightness.

No D-Bus. No logind. No powerdevil. Just /dev/psm0 and /dev/backlight/.
"""

import os
import sys
import select
import time
import subprocess
import signal
import logging
import argparse

# --- config ---
IDLE_TIMEOUT  = 300           # seconds before blanking
BACKLIGHT     = "/dev/backlight/backlight0"
INPUT_DEVICES = ["/dev/psm0", "/dev/input/event5", "/dev/input/event10"]  # trackpad (PS/2), Logitech Bolt mouse
PIDFILE       = "/tmp/blight.pid"
LOGFILE       = "/tmp/blight.log"
SELECT_CHUNK  = 2.0           # select() timeout in seconds
# --------------

log = logging.getLogger("blight")


def setup_logging(verbose=False):
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        filename=LOGFILE,
        level=level,
        format="%(asctime)s %(levelname)s %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )
    if verbose:
        logging.getLogger().addHandler(logging.StreamHandler())


def get_brightness():
    try:
        out = subprocess.check_output(
            ["backlight", "-f", BACKLIGHT], text=True, stderr=subprocess.DEVNULL
        )
        return int(out.strip().split()[-1])
    except Exception as e:
        log.warning(f"get_brightness failed: {e}")
        return 100


def set_brightness(val):
    val = max(0, min(100, int(val)))
    try:
        subprocess.call(["backlight", "-f", BACKLIGHT, str(val)], stderr=subprocess.DEVNULL)
        log.debug(f"set_brightness -> {val}")
    except Exception as e:
        log.error(f"set_brightness({val}) failed: {e}")


def write_pidfile():
    with open(PIDFILE, "w") as f:
        f.write(str(os.getpid()) + "\n")


def remove_pidfile():
    try:
        os.unlink(PIDFILE)
    except FileNotFoundError:
        pass


def run(idle_timeout=IDLE_TIMEOUT, verbose=False):
    setup_logging(verbose)
    write_pidfile()
    log.info(f"Started : timeout={idle_timeout}s backlight={BACKLIGHT}")

    saved_brightness = None  # None = screen active

    def handle_signal(signum, frame):
        log.info("Caught signal, restoring brightness and exiting")
        if saved_brightness is not None:
            set_brightness(saved_brightness)
        remove_pidfile()
        sys.exit(0)

    signal.signal(signal.SIGTERM, handle_signal)
    signal.signal(signal.SIGINT, handle_signal)

    fds = []
    for dev in INPUT_DEVICES:
        try:
            fds.append(os.open(dev, os.O_RDONLY | os.O_NONBLOCK))
            log.info(f"Watching {dev}")
        except OSError as e:
            log.warning(f"Cannot open {dev}: {e} (skipping)")

    if not fds:
        log.error("No input devices available, exiting")
        remove_pidfile()
        sys.exit(1)

    last_activity = time.monotonic()

    while True:
        try:
            readable, _, _ = select.select(fds, [], [], SELECT_CHUNK)
        except Exception as e:
            log.error(f"select() error: {e}")
            time.sleep(1)
            continue

        if readable:
            for fd in readable:
                try:
                    os.read(fd, 4096)
                except OSError:
                    pass

            last_activity = time.monotonic()

            if saved_brightness is not None:
                log.info(f"Activity : restoring brightness to {saved_brightness}")
                set_brightness(saved_brightness)
                saved_brightness = None
        else:
            idle = time.monotonic() - last_activity
            if idle >= idle_timeout and saved_brightness is None:
                current = get_brightness()
                if current > 0:
                    saved_brightness = current
                    log.info(f"Idle {idle:.0f}s : blanking (saved={saved_brightness})")
                    set_brightness(0)


def stop():
    if not os.path.exists(PIDFILE):
        print("blight not running")
        return
    try:
        with open(PIDFILE) as f:
            pid = int(f.read().strip())
        os.kill(pid, signal.SIGTERM)
        print(f"blight stopped (pid {pid})")
    except (ValueError, ProcessLookupError, PermissionError) as e:
        print(f"Failed to stop blight: {e}")


def status():
    if not os.path.exists(PIDFILE):
        print("blight not running")
        return
    try:
        with open(PIDFILE) as f:
            pid = int(f.read().strip())
        os.kill(pid, 0)
        print(f"blight running (pid {pid})")
        print(f"brightness: {get_brightness()}")
    except (ValueError, ProcessLookupError):
        print("blight not running (stale pidfile)")
        remove_pidfile()


def main():
    parser = argparse.ArgumentParser(description="blight : backlight idle daemon")
    parser.add_argument("command", nargs="?", default="start",
                        choices=["start", "stop", "status"])
    parser.add_argument("-t", "--timeout", type=int, default=IDLE_TIMEOUT,
                        help=f"idle timeout in seconds (default: {IDLE_TIMEOUT})")
    parser.add_argument("-v", "--verbose", action="store_true")
    parser.add_argument("-f", "--foreground", action="store_true",
                        help="run in foreground, don't daemonize")
    args = parser.parse_args()

    if args.command == "stop":
        stop(); return
    if args.command == "status":
        status(); return

    if os.path.exists(PIDFILE):
        try:
            with open(PIDFILE) as f:
                pid = int(f.read().strip())
            os.kill(pid, 0)
            print(f"blight already running (pid {pid})")
            sys.exit(1)
        except (ValueError, ProcessLookupError):
            remove_pidfile()

    if args.foreground:
        run(idle_timeout=args.timeout, verbose=args.verbose)
    else:
        pid = os.fork()
        if pid > 0:
            print(f"blight started (pid {pid})")
            sys.exit(0)
        os.setsid()
        if os.fork() > 0:
            sys.exit(0)
        for fd_num, devnull_mode in ((0, "r"), (1, "w"), (2, "w")):
            with open(os.devnull, devnull_mode) as dn:
                os.dup2(dn.fileno(), fd_num)
        run(idle_timeout=args.timeout, verbose=args.verbose)


if __name__ == "__main__":
    main()