About
Inhibit Idle While Watching
Keeps your session awake while you are sitting in front of the camera. When Noctalia's idle chain is about to act (dim, screen-off, lock), this plugin captures one webcam frame, looks for a face, and on a face restarts the idle countdown with one synthetic input tap — a gate, not a daemon: nothing is held, no timer is kept. Walking away (or looking away) lets the normal idle chain proceed, silently and exactly as configured.
Plugin
| Field | Value |
|---|---|
| ID | zhangdm/inhibit-idle-while-watching |
| Entries | Service: face-watch |
Requirements
noctalia,python3,ffmpeg,v4l2-ctl,wtypeonPATH(all independencies).- Python module for the helper:
cv2(OpenCV, e.g.python-opencv). Thednndetector additionally needsassets/face-dnn.onnx(not bundled — see Detectors under Notes). - A V4L2 webcam. The idle handshake below is compositor-agnostic (Noctalia owns idle); the reset path is verified on umbriel (and Niri before it).
Usage
The plugin cannot observe idle by itself (the plugin API has no idle hook), so
it is triggered by a Noctalia idle behaviour that messages it. Add a command
idle behaviour that fires a few seconds before the first action you want
postponed — example for postponing dim at 50 s:
# ~/.local/state/noctalia/settings.toml (or ~/.config/noctalia/config.toml)
[idle.behavior.presence-check]
action = "command"
command = "noctalia msg plugin zhangdm/inhibit-idle-while-watching:face-watch all idle-begin"
timeout = 45.0
The lead matters: the reset tap must land before the next behaviour's timer
expires, and a check costs about a second end-to-end (measured ~0.3 s capture +
~15 ms Haar on a 640px frame). A timeout equal to the behaviour you are
postponing loses that race every time — that action has already fired by the
time the face is detected.
Then noctalia msg config-reload. On each trigger the plugin captures one
frame; with a face present it sends one synthetic Shift tap (wtype), which
reads as user activity: the shell re-arms every idle behaviour and restarts
the countdown, so the check fires again on the next timeout while you keep
watching. Without a face nothing happens and your dim/screen-off/lock chain
runs untouched.
Manual control:
noctalia msg plugin zhangdm/inhibit-idle-while-watching:face-watch all status # log current state
Settings
| Setting | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
true |
Master switch; off means the camera is never touched. |
camera_path |
string |
"" |
Video device such as /dev/video0; empty auto-detects. |
detector |
string |
"haar" |
haar (cheap) or dnn (accurate, needs the ONNX model). |
sensitivity |
int |
5 |
1 lenient … 10 strict. |
capture_width |
int |
640 |
Frame width per check (height 4:3); detection downscales to 320 px. |
cooldown_s |
int |
10 |
Minimum seconds between gate runs (camera wake-up cap). |
debug_log |
bool |
false |
Helper activity log in the plugin data dir (verdicts, never images). |
IPC
noctalia msg plugin zhangdm/inhibit-idle-while-watching:face-watch all <event> [payload]
| Event | Effect |
|---|---|
idle-begin |
Run the presence gate (this is what the idle behaviour above sends). |
status |
Append the helper's status file to the log for debugging. |
Security
This plugin sees a webcam image — the most sensitive data on the desk. Threat model and guarantees:
- Image lifetime: one frame per check, captured by
ffmpegstraight into a stdout pipe, decoded in RAM (cv2.imdecode), and dropped when the check ends. There is no code path that writes a frame to disk (checked including debug and error paths). The camera activity LED lights during every capture — that is hardware, and it is your visible signal. - What is persisted:
face-watch-status.json(state, a verdict string such asface (1), check count, timestamps) and — only withdebug_log = true—face-watch.log(camera path and one-line verdicts). Neither contains image data. Both live in the plugin data directory, are written with mode0600(O_NOFOLLOW), and tighten any pre-existing looser file. - No capture while locked — fail closed: the privacy gate captures only on
an explicit "unlocked" from
noctalia msg status. A lock screen, a missing or malformed status answer, or a schema drift all refuse capture ("skipped"). The lock is checked at spawn (service side, also fail-closed), at check start, and again immediately before the frame is taken. What is guaranteed: capture is never started while the session is locked or its state unknown. A capture already in flight when the session locks cannot be cancelled mid-frame — that single frame still follows the image-lifetime rule above (RAM only, dropped with the check). - The tap never reaches a lockscreen: synthetic input on a lockscreen
would wake it, so the lock state is re-checked one last time immediately
before the
wtypetap — after capture and detection, which can span up to ~15 s during which the session can lock. Locked or unknown at that moment refuses the tap (the gate then reportslocked/skipped) and the configured idle chain proceeds untouched. - No shell, fixed binaries:
ffmpeg,v4l2-ctl,wtypeandnoctaliaare invoked with argument lists (never through a shell). Settings such ascamera_pathare passed as discrete arguments; there is no string interpolation into a command line. Defaults come fromv4l2-ctlautodetection (/dev/videoNonly). - Two failure postures, deliberately different: the privacy gate fails closed (above); the chain fails open — any check error, a missing detector asset, or a lock means no tap, and the configured idle chain proceeds untouched. A broken camera can never keep the session awake.
- Poke scope: on a confirmed face the plugin sends exactly one synthetic
Shift tap (
wtype -M shift -m shift) — a bare modifier, so nothing is typed into applications. Synthetic input is still real input: it can affect drag states or game keybinds (low but nonzero). The tap is what re-arms the idle chain (see Notes); the alternative pokes were measured and cannot. - Detector weights:
assets/face-dnn.onnxis fetched, not bundled, and is pinned by sha256 — only the exact reviewedversion-RFB-320.onnx(upstream MIT repository in Notes) loads; anything else is refused before reaching OpenCV's ONNX parser.
Known limits: a local attacker with your user's privileges can read the same camera; the plugin cannot defend against that.
Notes
- What "presence" means: a face toward the camera. Measured 2026-10-22 at reclined desk distance: facing the screen is detected 12/12 (Haar, YuNet and the DNN model agree); looking down at a phone is missed by all three (the camera sees mostly the top of the head). By design a look-away means no tap — this plugin inhibits idle while you are watching, not merely occupying the chair.
- Why a synthetic tap (measured 2026-10-22, umbriel): only real input takes
the shell's
resumedpath that re-arms a fired idle behaviour. A caffeine inhibit/release pair restarts the timers but leaves the fired behaviour spent (no re-fire within 90 s); inhibitor holds need a daemon. The tap re-arms everything and the check fires again at the nexttimeout. - Timing: the check fires at
timeoutplus Noctalia's globalpre_action_fade_seconds, and any input during that fade cancels the whole idle chain. With the default 30 s fade the camera only wakes at 75 s of perfect stillness; setpre_action_fade_seconds = 0for a check at exactlytimeout. - Detectors:
haar(default, no assets needed) ordnn(assets/face-dnn.onnx, Ultra-Light-Fast-Generic-Face-Detector-1MBversion-RFB-320.onnx, MIT — drop the file in to enable; detections are counted after non-maximum suppression). - Privacy: see Security above (image lifetime, persistence, lock guard).
- Power: one short capture+detect per check (~0.5 s of camera) plus one
wtypecall per confirmed face; never a continuous stream, no polling loop, no process outlives the check. Tunecapture_widthto taste. - What is spawned and written: the service spawns
python3 face-watch gate …(which runsffmpeg,v4l2-ctl,wtype,noctalia msg status); the helper writesface-watch-status.jsonand, withdebug_log,face-watch.login the plugin data directory. No network access. - Failure mode is fail-open: any capture/detect error means no tap and the idle chain proceeds — a broken camera never keeps the session awake.
Versions
| Version | Plugin API | Updated |
|---|---|---|
| v0.2.0 latest | 24 | Oct 22, 2026 |
Older versions stay installable on a Noctalia release whose plugin API is below the latest version's.