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.soin 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 byi915kms
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, updatelast_activitytimestamp, restore brightness if dimmed. - If
select()times out: check elapsed time sincelast_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_TIMEOUTseconds 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.suspendowns lid close → suspend viaacpiconf -k 0blightowns 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()