feat(hyprlua,hyprdrive,niri): switch presence detection from webcam motion to FIDO key
Replace the OpenCV motion-detection daemon with a fido2-token -L check — presence now means "a FIDO2/U2F key is plugged in", queried without any touch/tap. Adaptive poll: 20s under light load, 120s under moderate, 600s under heavy. Drops the old motion "grace window" since key presence doesn't flicker like camera motion did. Removes presence_detect.py, the presence camera config/test flow from enroll-biometrics.sh, and the python-opencv dependency (kept v4l-utils, still used by howdy's camera setup). Adds a standalone presence-test.sh instead of folding the FIDO check into enroll-biometrics.sh's dialog TUI — there's nothing to enroll, so it doesn't belong in that flow.main
parent
96f15c82af
commit
c16a17f10b
|
|
@ -3,9 +3,10 @@
|
||||||
#
|
#
|
||||||
# hypridle watches for user inactivity and triggers actions (lock, suspend)
|
# hypridle watches for user inactivity and triggers actions (lock, suspend)
|
||||||
# after configurable timeouts. It integrates with the presence-detection daemon
|
# after configurable timeouts. It integrates with the presence-detection daemon
|
||||||
# (presence-detect.sh) which resets the idle timer every 20s (backed off to 120s
|
# (presence-detect.sh) which resets the idle timer every 20s under light load
|
||||||
# under heavy CPU/RAM load) while the webcam detects motion, so these timeouts
|
# (backed off to 120s under moderate load, 600s under heavy CPU/RAM load) while
|
||||||
# only fire when the user has truly stepped away from the machine.
|
# a FIDO key is plugged in, so these timeouts only fire when the user has truly
|
||||||
|
# stepped away from the machine.
|
||||||
#
|
#
|
||||||
# Reference: https://wiki.hypr.land/Hypr-Ecosystem/hypridle/
|
# Reference: https://wiki.hypr.land/Hypr-Ecosystem/hypridle/
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -34,9 +35,9 @@ general {
|
||||||
ignore_dbus_inhibit = false # respect systemd-inhibit locks (presence-detect, caffeine)
|
ignore_dbus_inhibit = false # respect systemd-inhibit locks (presence-detect, caffeine)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Presence detection resets the idle timer every 20s (up to 120s under load)
|
# Presence detection resets the idle timer every 20s (backed off to 120s/600s
|
||||||
# while motion is detected, so these timeouts only run when you've actually
|
# under load) while a FIDO key is plugged in, so these timeouts only run when
|
||||||
# stepped away.
|
# you've actually stepped away.
|
||||||
|
|
||||||
# First idle listener: lock the screen after 30s of inactivity.
|
# First idle listener: lock the screen after 30s of inactivity.
|
||||||
listener {
|
listener {
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
# Toggle idle inhibit via systemd-inhibit (hypridle respects the logind idle hint).
|
# Toggle idle inhibit via systemd-inhibit (hypridle respects the logind idle hint).
|
||||||
# The PID file tracks the background sleep process used to hold the inhibitor lock.
|
# The PID file tracks the background sleep process used to hold the inhibitor lock.
|
||||||
# This lock is shared with presence-detect.sh, which also inhibits idle while
|
# This lock is shared with presence-detect.sh, which also inhibits idle while
|
||||||
# motion is detected — OWNED_FLAG marks lock ownership between the two, so clear
|
# a FIDO key is plugged in — OWNED_FLAG marks lock ownership between the two,
|
||||||
# it on every manual toggle: this action is always a manual takeover of the lock.
|
# so clear it on every manual toggle: this action is always a manual takeover
|
||||||
|
# of the lock.
|
||||||
PID_FILE="/tmp/caffeine-inhibit.pid"
|
PID_FILE="/tmp/caffeine-inhibit.pid"
|
||||||
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# enroll-biometrics.sh — TUI for face biometric setup.
|
# enroll-biometrics.sh — TUI for howdy face-auth enrollment + howdy/FIDO PAM 2FA setup.
|
||||||
#
|
#
|
||||||
# Two subsystems:
|
# For testing FIDO key presence detection (presence-detect.sh), see
|
||||||
# 1. Presence detection — configure/test the webcam used by presence-detect.sh
|
# presence-test.sh instead — kept separate since it's not an "enrollment"
|
||||||
# 2. Howdy face auth — enroll/manage/test face models for PAM authentication
|
# step (fido2-token needs no setup, it just checks the key over USB).
|
||||||
|
|
||||||
BACKTITLE="Biometric Enrollment"
|
BACKTITLE="Biometric Enrollment"
|
||||||
PRESENCE_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/presence-detect.conf"
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PYTHON_DETECT="$SCRIPT_DIR/python/presence_detect.py"
|
|
||||||
|
|
||||||
# howdy keeps its own camera setting in this INI, entirely separate from the
|
# howdy keeps its own camera setting in this INI, entirely separate from the
|
||||||
# presence-detect camera. Enrollment aborts in VideoCapture before touching the
|
# presence-detect camera. Enrollment aborts in VideoCapture before touching the
|
||||||
|
|
@ -106,21 +103,6 @@ list_cameras() {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
get_camera_id() {
|
|
||||||
if [[ -n "$PRESENCE_DETECT_CAMERA" ]]; then
|
|
||||||
echo "$PRESENCE_DETECT_CAMERA"
|
|
||||||
elif [[ -f "$PRESENCE_CFG" ]]; then
|
|
||||||
grep -oP 'CAMERA=\K[0-9]+' "$PRESENCE_CFG" 2>/dev/null || echo 0
|
|
||||||
else
|
|
||||||
echo 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
set_camera_id() {
|
|
||||||
mkdir -p "$(dirname "$PRESENCE_CFG")"
|
|
||||||
printf 'CAMERA=%s\n' "$1" > "$PRESENCE_CFG"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Grab a single still from a v4l2 node into $2. No sudo: /dev/video* is acl/group
|
# Grab a single still from a v4l2 node into $2. No sudo: /dev/video* is acl/group
|
||||||
# readable by the user. The first frame off a cold sensor is often black, so pull
|
# readable by the user. The first frame off a cold sensor is often black, so pull
|
||||||
# a few and keep the last.
|
# a few and keep the last.
|
||||||
|
|
@ -206,40 +188,6 @@ pick_camera_with_preview() {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Presence detection ────────────────────────────────────────────────────────
|
|
||||||
presence_configure_camera() {
|
|
||||||
local current; current=$(get_camera_id)
|
|
||||||
pick_camera_with_preview "Select Camera (Presence)" "/dev/video${current}" || return
|
|
||||||
|
|
||||||
set_camera_id "$PICKED_CAM"
|
|
||||||
msg "Camera Set" \
|
|
||||||
"Presence detection will use /dev/video${PICKED_CAM}.\n\nRestart presence-detect.sh for the change to take effect." \
|
|
||||||
9 62
|
|
||||||
}
|
|
||||||
|
|
||||||
presence_test_camera() {
|
|
||||||
local cam; cam=$(get_camera_id)
|
|
||||||
|
|
||||||
clear
|
|
||||||
printf "\n\033[1;35m Testing presence detection on /dev/video%s...\033[0m\n" "$cam"
|
|
||||||
printf "\033[35m ─────────────────────────────────────────\033[0m\n\n"
|
|
||||||
printf " Move around in front of the camera.\n\n"
|
|
||||||
|
|
||||||
python3 "$PYTHON_DETECT" "$cam" 2>/dev/null
|
|
||||||
local rc=$?
|
|
||||||
|
|
||||||
case $rc in
|
|
||||||
0) msg "Test Result" "Motion detected!\n\nPresence detection is working correctly." 8 52 ;;
|
|
||||||
1) msg "Test Result" \
|
|
||||||
"No motion detected.\n\nMake sure you are in front of the camera,\nmoving slightly, with adequate lighting." \
|
|
||||||
10 56 ;;
|
|
||||||
2) msg "Camera Error" \
|
|
||||||
"Could not open /dev/video${cam}.\n\nTry configuring a different camera first." \
|
|
||||||
9 56 ;;
|
|
||||||
*) msg "Error" "Unexpected exit code ($rc) from detection script." 7 52 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Howdy helpers ─────────────────────────────────────────────────────────────
|
# ── Howdy helpers ─────────────────────────────────────────────────────────────
|
||||||
# Detection must survive howdy 2.x's layout: its CLI lives under root-only
|
# Detection must survive howdy 2.x's layout: its CLI lives under root-only
|
||||||
# /usr/lib/security/howdy/ with /usr/bin/howdy symlinked into it, so a normal
|
# /usr/lib/security/howdy/ with /usr/bin/howdy symlinked into it, so a normal
|
||||||
|
|
@ -636,28 +584,24 @@ main_menu() {
|
||||||
local choice
|
local choice
|
||||||
choice=$(dialog --backtitle "$BACKTITLE" \
|
choice=$(dialog --backtitle "$BACKTITLE" \
|
||||||
--title " Biometric Enrollment " \
|
--title " Biometric Enrollment " \
|
||||||
--menu "\nSelect an option:" 23 70 10 \
|
--menu "\nSelect an option:" 20 70 8 \
|
||||||
"1" "Presence detection — configure camera" \
|
"1" "Howdy face auth — configure camera" \
|
||||||
"2" "Presence detection — test detection" \
|
"2" "Howdy face auth — add face model" \
|
||||||
"3" "Howdy face auth — configure camera" \
|
"3" "Howdy face auth — list enrolled models" \
|
||||||
"4" "Howdy face auth — add face model" \
|
"4" "Howdy face auth — remove face model" \
|
||||||
"5" "Howdy face auth — list enrolled models" \
|
"5" "Howdy face auth — test authentication" \
|
||||||
"6" "Howdy face auth — remove face model" \
|
"6" "PAM 2FA — set up howdy + FIDO key" \
|
||||||
"7" "Howdy face auth — test authentication" \
|
"7" "PAM 2FA — remove howdy + FIDO key" \
|
||||||
"8" "PAM 2FA — set up howdy + FIDO key" \
|
|
||||||
"9" "PAM 2FA — remove howdy + FIDO key" \
|
|
||||||
3>&1 1>&2 2>&3) || { clear; exit 0; }
|
3>&1 1>&2 2>&3) || { clear; exit 0; }
|
||||||
|
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
1) presence_configure_camera ;;
|
1) howdy_configure_camera ;;
|
||||||
2) presence_test_camera ;;
|
2) howdy_add ;;
|
||||||
3) howdy_configure_camera ;;
|
3) howdy_list ;;
|
||||||
4) howdy_add ;;
|
4) howdy_remove ;;
|
||||||
5) howdy_list ;;
|
5) howdy_test ;;
|
||||||
6) howdy_remove ;;
|
6) pam_setup ;;
|
||||||
7) howdy_test ;;
|
7) pam_teardown ;;
|
||||||
8) pam_setup ;;
|
|
||||||
9) pam_teardown ;;
|
|
||||||
esac
|
esac
|
||||||
main_menu
|
main_menu
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,42 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Webcam presence detection daemon.
|
# FIDO key presence detection daemon.
|
||||||
# Checks for motion at an adaptive interval (20s normally, backed off to 120s
|
# Checks whether a FIDO2/U2F security key is plugged in, at an adaptive
|
||||||
# under heavy system load) and shares caffeine's systemd-inhibit idle lock
|
# interval (20s under light load, 120s under moderate load, 600s under heavy
|
||||||
# while the user is detected, so hypridle never fires during an active session.
|
# load), and shares caffeine's systemd-inhibit idle lock while a key is
|
||||||
|
# present, so hypridle never fires during an active session.
|
||||||
#
|
#
|
||||||
# Reads the physical camera directly, but only briefly: each tick opens the
|
# Detection: `fido2-token -L` (libfido2) enumerates connected FIDO CTAP
|
||||||
# device, grabs a handful of frames, and releases it, so the camera stays free
|
# devices by USB HID usage page — no touch/tap required, so this is a pure
|
||||||
# for other apps (video calls, howdy) the rest of the time. If another app is
|
# presence check, not an authentication. Non-empty output = a key is present.
|
||||||
# already holding the camera when a tick fires, the grab fails — and that
|
|
||||||
# "busy" is itself proof the user is present (someone's on a call), so it counts
|
|
||||||
# as presence rather than an error. This replaced an earlier v4l2loopback-mirror
|
|
||||||
# design that held the camera open 24/7 and only served one reader at a time.
|
|
||||||
#
|
#
|
||||||
# Camera selection: set PRESENCE_DETECT_CAMERA env var or write
|
# This replaced an earlier webcam motion-detection design: simpler, no camera
|
||||||
# CAMERA=<id> to ~/.config/presence-detect.conf
|
# contention with video calls/howdy, and no OpenCV dependency.
|
||||||
#
|
|
||||||
# Exit codes from python helper:
|
|
||||||
# 0 = motion (present) 1 = no motion (away)
|
|
||||||
# 2 = camera unavailable -> skip the tick, leave inhibit state unchanged
|
|
||||||
# 3 = camera busy / in use -> treat as present (another app holds the camera)
|
|
||||||
|
|
||||||
# Resolve the script's real directory so the Python helper path stays valid
|
|
||||||
# even when invoked via a symlink or from a different cwd.
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PYTHON_DETECT="$SCRIPT_DIR/python/presence_detect.py"
|
|
||||||
# Shared with caffeine.sh: both the manual toggle and this daemon drive the
|
# Shared with caffeine.sh: both the manual toggle and this daemon drive the
|
||||||
# same systemd-inhibit lock, so caffeine-status.sh reflects either source.
|
# same systemd-inhibit lock, so caffeine-status.sh reflects either source.
|
||||||
PID_FILE="/tmp/caffeine-inhibit.pid"
|
PID_FILE="/tmp/caffeine-inhibit.pid"
|
||||||
# Marks that *this daemon* (not the manual caffeine toggle) currently holds
|
# Marks that *this daemon* (not the manual caffeine toggle) currently holds
|
||||||
# the lock, so a "no motion" tick never releases a manually-started session.
|
# the lock, so a "key removed" tick never releases a manually-started session.
|
||||||
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
||||||
# Records whether the camera currently SEES the user (motion, or the camera
|
# Records whether a FIDO key is present on the last tick. presence-status.sh
|
||||||
# being in use by another app, on the last tick). presence-status.sh reads this
|
# reads this so the Eww widget can show presence as a distinct signal from
|
||||||
# so the Eww widget can show presence as a distinct signal from the idle lock.
|
# the idle lock. During a MANUAL caffeine session the daemon still checks (the
|
||||||
# It reflects live camera state only while the daemon is actively watching:
|
# check is cheap and doesn't touch any device other apps might want), so this
|
||||||
# during a MANUAL caffeine session the daemon stops reading the camera entirely
|
# stays live even then — only the inhibit ownership differs.
|
||||||
# (to leave it free) and clears this flag, so presence reads false there.
|
|
||||||
PRESENCE_FLAG="/tmp/presence-detected"
|
PRESENCE_FLAG="/tmp/presence-detected"
|
||||||
PRESENCE_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/presence-detect.conf"
|
|
||||||
|
|
||||||
INTERVAL_FAST=2 # near-idle system (< LOAD_LIGHT): poll fast for responsiveness
|
INTERVAL_LIGHT=20 # seconds between checks when CPU-or-RAM usage < LOAD_LIGHT
|
||||||
INTERVAL_IDLE=20 # seconds between checks under normal load
|
INTERVAL_MID=120 # seconds between checks when usage is between the two thresholds
|
||||||
INTERVAL_BUSY=120 # seconds between checks when CPU or RAM is >= LOAD_BUSY
|
INTERVAL_HEAVY=600 # seconds between checks when usage >= LOAD_HEAVY
|
||||||
LOAD_BUSY=0.70 # CPU-or-RAM fraction at/above which we back off to INTERVAL_BUSY
|
LOAD_LIGHT=0.20 # CPU-or-RAM fraction below which we poll at INTERVAL_LIGHT
|
||||||
LOAD_LIGHT=0.30 # CPU-or-RAM fraction below which we speed up to INTERVAL_FAST
|
LOAD_HEAVY=0.50 # CPU-or-RAM fraction at/above which we back off to INTERVAL_HEAVY
|
||||||
NPROC="$(nproc)"
|
NPROC="$(nproc)"
|
||||||
|
|
||||||
# Grace window: keep reporting "present" for this long after the last detected
|
# True if any FIDO2/U2F authenticator is currently enumerable over USB.
|
||||||
# motion, so brief stillness (reading, thinking, a slow moment) never drops the
|
# fido2-token -L lists one line per connected device and needs no PIN, touch,
|
||||||
# lock the instant one tick sees nothing. Any motion tick resets the clock.
|
# or tap — it's a pure USB HID enumeration, so this never prompts the key.
|
||||||
GRACE_SECONDS=90
|
_fido_key_present() {
|
||||||
|
[[ -n "$(fido2-token -L 2>/dev/null)" ]]
|
||||||
# Resolve camera ID: env var takes highest priority, then config file, then default 0.
|
|
||||||
_camera_id() {
|
|
||||||
if [[ -n "$PRESENCE_DETECT_CAMERA" ]]; then
|
|
||||||
echo "$PRESENCE_DETECT_CAMERA"
|
|
||||||
elif [[ -f "$PRESENCE_CFG" ]]; then
|
|
||||||
# -oP 'CAMERA=\K[0-9]+': Perl-style look-behind strips "CAMERA=" prefix.
|
|
||||||
grep -oP 'CAMERA=\K[0-9]+' "$PRESENCE_CFG" 2>/dev/null || echo 0
|
|
||||||
else
|
|
||||||
echo 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# True if any process OTHER than this daemon currently holds the real camera
|
|
||||||
# open — a video call, howdy, etc. `fuser` lists same-user holders without root
|
|
||||||
# (every camera app runs as us), and we only ever call this when we aren't
|
|
||||||
# reading the camera ourselves, so any holder is another app. If fuser isn't
|
|
||||||
# installed we return false: the tick then just tries the camera and the Python
|
|
||||||
# helper's busy/in-use path (exit 3) still covers an already-held device.
|
|
||||||
_camera_in_use() {
|
|
||||||
command -v fuser >/dev/null 2>&1 || return 1
|
|
||||||
fuser -s "/dev/video${1}" 2>/dev/null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Prints current CPU-or-RAM usage as a fraction (0..1): the higher of the
|
# Prints current CPU-or-RAM usage as a fraction (0..1): the higher of the
|
||||||
|
|
@ -87,14 +52,14 @@ _resource_usage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Three-tier poll interval based on current resource usage:
|
# Three-tier poll interval based on current resource usage:
|
||||||
# >= LOAD_BUSY -> INTERVAL_BUSY (heavy load: back off, stay out of the way)
|
# >= LOAD_HEAVY -> INTERVAL_HEAVY (heavy load: back off, stay out of the way)
|
||||||
# < LOAD_LIGHT -> INTERVAL_FAST (near-idle: poll fast, cheap and responsive)
|
# < LOAD_LIGHT -> INTERVAL_LIGHT (near-idle: poll fast, cheap and responsive)
|
||||||
# otherwise -> INTERVAL_IDLE (normal load)
|
# otherwise -> INTERVAL_MID (moderate load)
|
||||||
_next_interval() {
|
_next_interval() {
|
||||||
local u; u="$(_resource_usage)"
|
local u; u="$(_resource_usage)"
|
||||||
awk -v u="$u" -v busy="$LOAD_BUSY" -v light="$LOAD_LIGHT" \
|
awk -v u="$u" -v heavy="$LOAD_HEAVY" -v light="$LOAD_LIGHT" \
|
||||||
-v fb="$INTERVAL_BUSY" -v ff="$INTERVAL_FAST" -v fi="$INTERVAL_IDLE" \
|
-v fh="$INTERVAL_HEAVY" -v fl="$INTERVAL_LIGHT" -v fm="$INTERVAL_MID" \
|
||||||
'BEGIN{ if (u >= busy) print fb; else if (u < light) print ff; else print fi }'
|
'BEGIN{ if (u >= heavy) print fh; else if (u < light) print fl; else print fm }'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Returns true if the inhibitor sentinel process is still alive.
|
# Returns true if the inhibitor sentinel process is still alive.
|
||||||
|
|
@ -109,12 +74,12 @@ _start_inhibit() {
|
||||||
# --what=idle: target the logind idle-inhibit lock that hypridle polls.
|
# --what=idle: target the logind idle-inhibit lock that hypridle polls.
|
||||||
# "sleep infinity" is the sentinel; its PID is saved so we can kill it later.
|
# "sleep infinity" is the sentinel; its PID is saved so we can kill it later.
|
||||||
systemd-inhibit --what=idle --who="presence-detect" \
|
systemd-inhibit --what=idle --who="presence-detect" \
|
||||||
--why="User presence detected" --mode=block \
|
--why="FIDO key present" --mode=block \
|
||||||
sleep infinity &
|
sleep infinity &
|
||||||
echo $! > "$PID_FILE"
|
echo $! > "$PID_FILE"
|
||||||
touch "$OWNED_FLAG"
|
touch "$OWNED_FLAG"
|
||||||
# logger writes to the system journal — visible via `journalctl -t presence-detect`.
|
# logger writes to the system journal — visible via `journalctl -t presence-detect`.
|
||||||
logger -t presence-detect "Motion detected — idle inhibited"
|
logger -t presence-detect "FIDO key present — idle inhibited"
|
||||||
}
|
}
|
||||||
|
|
||||||
_stop_inhibit() {
|
_stop_inhibit() {
|
||||||
|
|
@ -125,7 +90,7 @@ _stop_inhibit() {
|
||||||
# Killing the sleep process releases the systemd-inhibit lock automatically.
|
# Killing the sleep process releases the systemd-inhibit lock automatically.
|
||||||
kill "$(cat "$PID_FILE")" 2>/dev/null
|
kill "$(cat "$PID_FILE")" 2>/dev/null
|
||||||
rm -f "$PID_FILE" "$OWNED_FLAG"
|
rm -f "$PID_FILE" "$OWNED_FLAG"
|
||||||
logger -t presence-detect "No motion — idle inhibit released"
|
logger -t presence-detect "FIDO key removed — idle inhibit released"
|
||||||
}
|
}
|
||||||
|
|
||||||
_cleanup() {
|
_cleanup() {
|
||||||
|
|
@ -139,53 +104,23 @@ _cleanup() {
|
||||||
# Intercept termination signals to ensure the inhibitor PID is never orphaned.
|
# Intercept termination signals to ensure the inhibitor PID is never orphaned.
|
||||||
trap _cleanup SIGTERM SIGINT SIGHUP
|
trap _cleanup SIGTERM SIGINT SIGHUP
|
||||||
|
|
||||||
# Epoch seconds of the last tick that saw motion; drives the GRACE_SECONDS
|
|
||||||
# window below. 0 = never seen yet, so a no-motion tick at startup releases
|
|
||||||
# immediately (nothing is held anyway).
|
|
||||||
LAST_MOTION=0
|
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
# When idle is inhibited MANUALLY (a caffeine toggle, not this daemon), the
|
# When idle is inhibited MANUALLY (a caffeine toggle, not this daemon), the
|
||||||
# lock is already held regardless of presence, so there's no reason to look
|
# lock is already held regardless of presence. Detected the same way
|
||||||
# at the camera — skip the read entirely and leave the device free for other
|
# caffeine-manual-status.sh does it: the shared lock is alive but we don't
|
||||||
# apps. Detected the same way caffeine-manual-status.sh does it: the shared
|
# own it. We still update PRESENCE_FLAG below since checking is cheap and
|
||||||
# lock is alive but we don't own it. We clear PRESENCE_FLAG because we're no
|
# doesn't touch a device other apps care about — only inhibit ownership
|
||||||
# longer watching and must not report a stale "camera sees you".
|
# differs during a manual session.
|
||||||
if _inhibit_running && [[ ! -f "$OWNED_FLAG" ]]; then
|
if _fido_key_present; then
|
||||||
|
touch "$PRESENCE_FLAG"
|
||||||
|
# No-op if a manual caffeine session already holds the lock — only
|
||||||
|
# _stop_inhibit needs the ownership check; starting is always safe
|
||||||
|
# since _start_inhibit itself no-ops when a lock is already held.
|
||||||
|
_start_inhibit
|
||||||
|
else
|
||||||
rm -f "$PRESENCE_FLAG"
|
rm -f "$PRESENCE_FLAG"
|
||||||
sleep "$(_next_interval)"
|
_stop_inhibit
|
||||||
continue
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
DEVICE="$(_camera_id)"
|
|
||||||
now="$(date +%s)"
|
|
||||||
|
|
||||||
# Back-off: if another app already holds the camera (a video call, howdy,
|
|
||||||
# ...), don't even try to read it — attempting would be pointless (we'd just
|
|
||||||
# get EBUSY) and a simultaneous open could make the app itself fail. A camera
|
|
||||||
# in use is proof you're present, so mark presence and wait it out instead.
|
|
||||||
if _camera_in_use "$DEVICE"; then
|
|
||||||
LAST_MOTION="$now"; touch "$PRESENCE_FLAG"; _start_inhibit
|
|
||||||
sleep "$(_next_interval)"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run the OpenCV motion detector; stderr suppressed to keep the journal clean.
|
|
||||||
python3 "$PYTHON_DETECT" "$DEVICE" 2>/dev/null
|
|
||||||
rc=$?
|
|
||||||
case $rc in
|
|
||||||
# Motion (0) or camera busy/in use (3): user is present. rc=3 means
|
|
||||||
# another app (a video call, howdy) is holding the camera, which is
|
|
||||||
# itself proof you're here, so it keeps the session awake even though we
|
|
||||||
# can't read frames. Both refresh the grace clock and mark presence.
|
|
||||||
0|3) LAST_MOTION="$now"; touch "$PRESENCE_FLAG"; _start_inhibit ;;
|
|
||||||
# No motion: only actually release once we've been still for the whole
|
|
||||||
# grace window. Within it, leave the flag/lock exactly as they were so a
|
|
||||||
# brief pause in movement doesn't flicker presence off.
|
|
||||||
1) if (( now - LAST_MOTION >= GRACE_SECONDS )); then
|
|
||||||
rm -f "$PRESENCE_FLAG"; _stop_inhibit
|
|
||||||
fi ;;
|
|
||||||
# rc=2 = camera unavailable (unplugged/gone) — silently skip, state unchanged
|
|
||||||
esac
|
|
||||||
sleep "$(_next_interval)"
|
sleep "$(_next_interval)"
|
||||||
done
|
done
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Reports the PRESENCE input for the Eww caffeine widget: whether the webcam
|
# Reports the PRESENCE input for the Eww caffeine widget: whether the
|
||||||
# presence-detect daemon currently sees the user (motion on its last tick),
|
# presence-detect daemon currently sees a FIDO key plugged in (on its last
|
||||||
# INDEPENDENT of whether the idle lock is held or who holds it.
|
# tick), INDEPENDENT of whether the idle lock is held or who holds it.
|
||||||
#
|
#
|
||||||
# presence-detect.sh writes PRESENCE_FLAG on every "present" tick and removes it
|
# presence-detect.sh writes PRESENCE_FLAG on every "present" tick and removes
|
||||||
# on every "away" tick, so this is a clean, standalone signal. Note: during a
|
# it on every "away" tick, so this is a clean, standalone signal — it stays
|
||||||
# MANUAL caffeine session the daemon stops reading the camera (to leave it free)
|
# live even during a MANUAL caffeine session, since checking for the key is
|
||||||
# and clears the flag, so presence reads false there. For the MANUAL-inhibit
|
# cheap and doesn't contend with anything. For the MANUAL-inhibit input, see
|
||||||
# input, see caffeine-manual-status.sh.
|
# caffeine-manual-status.sh.
|
||||||
#
|
#
|
||||||
# Prints "true" when presence is detected, else "false".
|
# Prints "true" when presence is detected, else "false".
|
||||||
PRESENCE_FLAG="/tmp/presence-detected"
|
PRESENCE_FLAG="/tmp/presence-detected"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# presence-test.sh — standalone check for FIDO key presence detection.
|
||||||
|
#
|
||||||
|
# Kept separate from enroll-biometrics.sh: there's nothing to enroll or
|
||||||
|
# configure here (fido2-token needs no setup, it just enumerates the key over
|
||||||
|
# USB), so bundling it into the enrollment TUI was more confusing than useful.
|
||||||
|
#
|
||||||
|
# Runs the exact same check presence-detect.sh uses on every poll tick.
|
||||||
|
|
||||||
|
command -v fido2-token &>/dev/null || {
|
||||||
|
printf "\n\033[1;31m fido2-token not found\033[0m (from libfido2).\n"
|
||||||
|
printf " Install it: sudo pacman -S libfido2\n\n"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
printf "\n\033[1;35m Checking for a connected FIDO key...\033[0m\n"
|
||||||
|
printf "\033[35m ─────────────────────────────────────────\033[0m\n\n"
|
||||||
|
|
||||||
|
out=$(fido2-token -L 2>/dev/null)
|
||||||
|
if [[ -n "$out" ]]; then
|
||||||
|
printf " \033[1;32mFIDO key detected:\033[0m\n\n"
|
||||||
|
printf '%s\n' "$out" | sed 's/^/ /'
|
||||||
|
printf "\n Presence detection is working correctly.\n\n"
|
||||||
|
else
|
||||||
|
printf " \033[1;33mNo FIDO key detected.\033[0m\n\n"
|
||||||
|
printf " Make sure your key is plugged in.\n\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Webcam motion presence detector using frame differencing.
|
|
||||||
Exit codes:
|
|
||||||
0 = motion detected (user present)
|
|
||||||
1 = no motion (user likely away)
|
|
||||||
2 = camera unavailable (device node missing / gone) -> caller skips tick
|
|
||||||
3 = camera busy / in use (another app holds it) -> treat as present
|
|
||||||
Usage: presence_detect.py [camera_id]
|
|
||||||
|
|
||||||
Reads the physical camera directly, only for the ~0.5s it takes to grab a few
|
|
||||||
frames, then releases it — so a video call, howdy, etc. can use the camera the
|
|
||||||
rest of the time. When one of those apps IS holding the camera, we can't read
|
|
||||||
it; rather than mistake that for "no motion", we report it as busy/in-use (3),
|
|
||||||
which the daemon treats as presence: a camera in use means the user is here.
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
FRAMES_TO_CHECK = 8
|
|
||||||
DIFFS_NEEDED = 2 # require motion in at least N consecutive-frame diffs
|
|
||||||
PIXEL_DELTA_THRESHOLD = 18 # per-pixel grayscale delta to count as "changed"
|
|
||||||
MOTION_AREA_RATIO = 0.008 # fraction of pixels that must change to call it motion
|
|
||||||
BLUR_KSIZE = (21, 21) # Gaussian blur kernel to suppress sensor noise
|
|
||||||
# Spacing between the frames we diff. Without it the frames are grabbed
|
|
||||||
# back-to-back within a few milliseconds, so slow, small movements barely differ
|
|
||||||
# between adjacent frames. Spacing the grabs out gives slow motion a real
|
|
||||||
# temporal baseline and guarantees each compared frame is a fresh one.
|
|
||||||
INTER_FRAME_DELAY = 0.06 # seconds between grabs (~0.5s total observation window)
|
|
||||||
|
|
||||||
|
|
||||||
def detect(camera_id: int) -> int:
|
|
||||||
# A missing device node means the camera is genuinely gone (unplugged, module
|
|
||||||
# not loaded) -> "unavailable" (2), and the daemon leaves presence untouched.
|
|
||||||
# A node that exists but won't open (below) or won't yield frames (further
|
|
||||||
# down) means something else is streaming it -> "busy/in use" (3) = present.
|
|
||||||
if not os.path.exists(f"/dev/video{camera_id}"):
|
|
||||||
return 2
|
|
||||||
|
|
||||||
cap = cv2.VideoCapture(camera_id)
|
|
||||||
if not cap.isOpened():
|
|
||||||
# Node exists but we can't open it: another app owns the camera.
|
|
||||||
return 3
|
|
||||||
|
|
||||||
motion_diffs = 0
|
|
||||||
frames_read = 0
|
|
||||||
prev_gray = None
|
|
||||||
try:
|
|
||||||
for i in range(FRAMES_TO_CHECK):
|
|
||||||
if i > 0:
|
|
||||||
time.sleep(INTER_FRAME_DELAY)
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
continue
|
|
||||||
frames_read += 1
|
|
||||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
||||||
gray = cv2.GaussianBlur(gray, BLUR_KSIZE, 0)
|
|
||||||
|
|
||||||
if prev_gray is not None:
|
|
||||||
delta = cv2.absdiff(prev_gray, gray)
|
|
||||||
changed = np.count_nonzero(delta > PIXEL_DELTA_THRESHOLD)
|
|
||||||
if changed / delta.size >= MOTION_AREA_RATIO:
|
|
||||||
motion_diffs += 1
|
|
||||||
|
|
||||||
prev_gray = gray
|
|
||||||
finally:
|
|
||||||
cap.release()
|
|
||||||
|
|
||||||
# Opened but produced no frames at all: on V4L2 a second opener can succeed
|
|
||||||
# yet have its reads starved while another app streams the device. Treat that
|
|
||||||
# as busy/in-use (present) rather than "no motion", so an active video call
|
|
||||||
# keeps the session awake even though we never see a usable frame.
|
|
||||||
if frames_read == 0:
|
|
||||||
return 3
|
|
||||||
|
|
||||||
return 0 if motion_diffs >= DIFFS_NEEDED else 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
camera_id = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
|
||||||
sys.exit(detect(camera_id))
|
|
||||||
|
|
@ -3,9 +3,10 @@
|
||||||
#
|
#
|
||||||
# hypridle watches for user inactivity and triggers actions (lock, suspend)
|
# hypridle watches for user inactivity and triggers actions (lock, suspend)
|
||||||
# after configurable timeouts. It integrates with the presence-detection daemon
|
# after configurable timeouts. It integrates with the presence-detection daemon
|
||||||
# (presence-detect.sh) which resets the idle timer every 20s (backed off to 120s
|
# (presence-detect.sh) which resets the idle timer every 20s under light load
|
||||||
# under heavy CPU/RAM load) while the webcam detects motion, so these timeouts
|
# (backed off to 120s under moderate load, 600s under heavy CPU/RAM load) while
|
||||||
# only fire when the user has truly stepped away from the machine.
|
# a FIDO key is plugged in, so these timeouts only fire when the user has truly
|
||||||
|
# stepped away from the machine.
|
||||||
#
|
#
|
||||||
# Reference: https://wiki.hypr.land/Hypr-Ecosystem/hypridle/
|
# Reference: https://wiki.hypr.land/Hypr-Ecosystem/hypridle/
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -34,9 +35,9 @@ general {
|
||||||
ignore_dbus_inhibit = false # respect systemd-inhibit locks (presence-detect, caffeine)
|
ignore_dbus_inhibit = false # respect systemd-inhibit locks (presence-detect, caffeine)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Presence detection resets the idle timer every 20s (up to 120s under load)
|
# Presence detection resets the idle timer every 20s (backed off to 120s/600s
|
||||||
# while motion is detected, so these timeouts only run when you've actually
|
# under load) while a FIDO key is plugged in, so these timeouts only run when
|
||||||
# stepped away.
|
# you've actually stepped away.
|
||||||
|
|
||||||
# First idle listener: lock the screen after 30s of inactivity.
|
# First idle listener: lock the screen after 30s of inactivity.
|
||||||
listener {
|
listener {
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
# Toggle idle inhibit via systemd-inhibit (hypridle respects the logind idle hint).
|
# Toggle idle inhibit via systemd-inhibit (hypridle respects the logind idle hint).
|
||||||
# The PID file tracks the background sleep process used to hold the inhibitor lock.
|
# The PID file tracks the background sleep process used to hold the inhibitor lock.
|
||||||
# This lock is shared with presence-detect.sh, which also inhibits idle while
|
# This lock is shared with presence-detect.sh, which also inhibits idle while
|
||||||
# motion is detected — OWNED_FLAG marks lock ownership between the two, so clear
|
# a FIDO key is plugged in — OWNED_FLAG marks lock ownership between the two,
|
||||||
# it on every manual toggle: this action is always a manual takeover of the lock.
|
# so clear it on every manual toggle: this action is always a manual takeover
|
||||||
|
# of the lock.
|
||||||
PID_FILE="/tmp/caffeine-inhibit.pid"
|
PID_FILE="/tmp/caffeine-inhibit.pid"
|
||||||
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# enroll-biometrics.sh — TUI for face biometric setup.
|
# enroll-biometrics.sh — TUI for howdy face-auth enrollment + howdy/FIDO PAM 2FA setup.
|
||||||
#
|
#
|
||||||
# Two subsystems:
|
# For testing FIDO key presence detection (presence-detect.sh), see
|
||||||
# 1. Presence detection — configure/test the webcam used by presence-detect.sh
|
# presence-test.sh instead — kept separate since it's not an "enrollment"
|
||||||
# 2. Howdy face auth — enroll/manage/test face models for PAM authentication
|
# step (fido2-token needs no setup, it just checks the key over USB).
|
||||||
|
|
||||||
BACKTITLE="Biometric Enrollment"
|
BACKTITLE="Biometric Enrollment"
|
||||||
PRESENCE_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/presence-detect.conf"
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PYTHON_DETECT="$SCRIPT_DIR/python/presence_detect.py"
|
|
||||||
|
|
||||||
# howdy keeps its own camera setting in this INI, entirely separate from the
|
# howdy keeps its own camera setting in this INI, entirely separate from the
|
||||||
# presence-detect camera. Enrollment aborts in VideoCapture before touching the
|
# presence-detect camera. Enrollment aborts in VideoCapture before touching the
|
||||||
|
|
@ -106,21 +103,6 @@ list_cameras() {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
get_camera_id() {
|
|
||||||
if [[ -n "$PRESENCE_DETECT_CAMERA" ]]; then
|
|
||||||
echo "$PRESENCE_DETECT_CAMERA"
|
|
||||||
elif [[ -f "$PRESENCE_CFG" ]]; then
|
|
||||||
grep -oP 'CAMERA=\K[0-9]+' "$PRESENCE_CFG" 2>/dev/null || echo 0
|
|
||||||
else
|
|
||||||
echo 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
set_camera_id() {
|
|
||||||
mkdir -p "$(dirname "$PRESENCE_CFG")"
|
|
||||||
printf 'CAMERA=%s\n' "$1" > "$PRESENCE_CFG"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Grab a single still from a v4l2 node into $2. No sudo: /dev/video* is acl/group
|
# Grab a single still from a v4l2 node into $2. No sudo: /dev/video* is acl/group
|
||||||
# readable by the user. The first frame off a cold sensor is often black, so pull
|
# readable by the user. The first frame off a cold sensor is often black, so pull
|
||||||
# a few and keep the last.
|
# a few and keep the last.
|
||||||
|
|
@ -206,40 +188,6 @@ pick_camera_with_preview() {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Presence detection ────────────────────────────────────────────────────────
|
|
||||||
presence_configure_camera() {
|
|
||||||
local current; current=$(get_camera_id)
|
|
||||||
pick_camera_with_preview "Select Camera (Presence)" "/dev/video${current}" || return
|
|
||||||
|
|
||||||
set_camera_id "$PICKED_CAM"
|
|
||||||
msg "Camera Set" \
|
|
||||||
"Presence detection will use /dev/video${PICKED_CAM}.\n\nRestart presence-detect.sh for the change to take effect." \
|
|
||||||
9 62
|
|
||||||
}
|
|
||||||
|
|
||||||
presence_test_camera() {
|
|
||||||
local cam; cam=$(get_camera_id)
|
|
||||||
|
|
||||||
clear
|
|
||||||
printf "\n\033[1;35m Testing presence detection on /dev/video%s...\033[0m\n" "$cam"
|
|
||||||
printf "\033[35m ─────────────────────────────────────────\033[0m\n\n"
|
|
||||||
printf " Move around in front of the camera.\n\n"
|
|
||||||
|
|
||||||
python3 "$PYTHON_DETECT" "$cam" 2>/dev/null
|
|
||||||
local rc=$?
|
|
||||||
|
|
||||||
case $rc in
|
|
||||||
0) msg "Test Result" "Motion detected!\n\nPresence detection is working correctly." 8 52 ;;
|
|
||||||
1) msg "Test Result" \
|
|
||||||
"No motion detected.\n\nMake sure you are in front of the camera,\nmoving slightly, with adequate lighting." \
|
|
||||||
10 56 ;;
|
|
||||||
2) msg "Camera Error" \
|
|
||||||
"Could not open /dev/video${cam}.\n\nTry configuring a different camera first." \
|
|
||||||
9 56 ;;
|
|
||||||
*) msg "Error" "Unexpected exit code ($rc) from detection script." 7 52 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Howdy helpers ─────────────────────────────────────────────────────────────
|
# ── Howdy helpers ─────────────────────────────────────────────────────────────
|
||||||
# Detection must survive howdy 2.x's layout: its CLI lives under root-only
|
# Detection must survive howdy 2.x's layout: its CLI lives under root-only
|
||||||
# /usr/lib/security/howdy/ with /usr/bin/howdy symlinked into it, so a normal
|
# /usr/lib/security/howdy/ with /usr/bin/howdy symlinked into it, so a normal
|
||||||
|
|
@ -636,28 +584,24 @@ main_menu() {
|
||||||
local choice
|
local choice
|
||||||
choice=$(dialog --backtitle "$BACKTITLE" \
|
choice=$(dialog --backtitle "$BACKTITLE" \
|
||||||
--title " Biometric Enrollment " \
|
--title " Biometric Enrollment " \
|
||||||
--menu "\nSelect an option:" 23 70 10 \
|
--menu "\nSelect an option:" 20 70 8 \
|
||||||
"1" "Presence detection — configure camera" \
|
"1" "Howdy face auth — configure camera" \
|
||||||
"2" "Presence detection — test detection" \
|
"2" "Howdy face auth — add face model" \
|
||||||
"3" "Howdy face auth — configure camera" \
|
"3" "Howdy face auth — list enrolled models" \
|
||||||
"4" "Howdy face auth — add face model" \
|
"4" "Howdy face auth — remove face model" \
|
||||||
"5" "Howdy face auth — list enrolled models" \
|
"5" "Howdy face auth — test authentication" \
|
||||||
"6" "Howdy face auth — remove face model" \
|
"6" "PAM 2FA — set up howdy + FIDO key" \
|
||||||
"7" "Howdy face auth — test authentication" \
|
"7" "PAM 2FA — remove howdy + FIDO key" \
|
||||||
"8" "PAM 2FA — set up howdy + FIDO key" \
|
|
||||||
"9" "PAM 2FA — remove howdy + FIDO key" \
|
|
||||||
3>&1 1>&2 2>&3) || { clear; exit 0; }
|
3>&1 1>&2 2>&3) || { clear; exit 0; }
|
||||||
|
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
1) presence_configure_camera ;;
|
1) howdy_configure_camera ;;
|
||||||
2) presence_test_camera ;;
|
2) howdy_add ;;
|
||||||
3) howdy_configure_camera ;;
|
3) howdy_list ;;
|
||||||
4) howdy_add ;;
|
4) howdy_remove ;;
|
||||||
5) howdy_list ;;
|
5) howdy_test ;;
|
||||||
6) howdy_remove ;;
|
6) pam_setup ;;
|
||||||
7) howdy_test ;;
|
7) pam_teardown ;;
|
||||||
8) pam_setup ;;
|
|
||||||
9) pam_teardown ;;
|
|
||||||
esac
|
esac
|
||||||
main_menu
|
main_menu
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,42 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Webcam presence detection daemon.
|
# FIDO key presence detection daemon.
|
||||||
# Checks for motion at an adaptive interval (20s normally, backed off to 120s
|
# Checks whether a FIDO2/U2F security key is plugged in, at an adaptive
|
||||||
# under heavy system load) and shares caffeine's systemd-inhibit idle lock
|
# interval (20s under light load, 120s under moderate load, 600s under heavy
|
||||||
# while the user is detected, so hypridle never fires during an active session.
|
# load), and shares caffeine's systemd-inhibit idle lock while a key is
|
||||||
|
# present, so hypridle never fires during an active session.
|
||||||
#
|
#
|
||||||
# Reads the physical camera directly, but only briefly: each tick opens the
|
# Detection: `fido2-token -L` (libfido2) enumerates connected FIDO CTAP
|
||||||
# device, grabs a handful of frames, and releases it, so the camera stays free
|
# devices by USB HID usage page — no touch/tap required, so this is a pure
|
||||||
# for other apps (video calls, howdy) the rest of the time. If another app is
|
# presence check, not an authentication. Non-empty output = a key is present.
|
||||||
# already holding the camera when a tick fires, the grab fails — and that
|
|
||||||
# "busy" is itself proof the user is present (someone's on a call), so it counts
|
|
||||||
# as presence rather than an error. This replaced an earlier v4l2loopback-mirror
|
|
||||||
# design that held the camera open 24/7 and only served one reader at a time.
|
|
||||||
#
|
#
|
||||||
# Camera selection: set PRESENCE_DETECT_CAMERA env var or write
|
# This replaced an earlier webcam motion-detection design: simpler, no camera
|
||||||
# CAMERA=<id> to ~/.config/presence-detect.conf
|
# contention with video calls/howdy, and no OpenCV dependency.
|
||||||
#
|
|
||||||
# Exit codes from python helper:
|
|
||||||
# 0 = motion (present) 1 = no motion (away)
|
|
||||||
# 2 = camera unavailable -> skip the tick, leave inhibit state unchanged
|
|
||||||
# 3 = camera busy / in use -> treat as present (another app holds the camera)
|
|
||||||
|
|
||||||
# Resolve the script's real directory so the Python helper path stays valid
|
|
||||||
# even when invoked via a symlink or from a different cwd.
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PYTHON_DETECT="$SCRIPT_DIR/python/presence_detect.py"
|
|
||||||
# Shared with caffeine.sh: both the manual toggle and this daemon drive the
|
# Shared with caffeine.sh: both the manual toggle and this daemon drive the
|
||||||
# same systemd-inhibit lock, so caffeine-status.sh reflects either source.
|
# same systemd-inhibit lock, so caffeine-status.sh reflects either source.
|
||||||
PID_FILE="/tmp/caffeine-inhibit.pid"
|
PID_FILE="/tmp/caffeine-inhibit.pid"
|
||||||
# Marks that *this daemon* (not the manual caffeine toggle) currently holds
|
# Marks that *this daemon* (not the manual caffeine toggle) currently holds
|
||||||
# the lock, so a "no motion" tick never releases a manually-started session.
|
# the lock, so a "key removed" tick never releases a manually-started session.
|
||||||
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
||||||
# Records whether the camera currently SEES the user (motion, or the camera
|
# Records whether a FIDO key is present on the last tick. presence-status.sh
|
||||||
# being in use by another app, on the last tick). presence-status.sh reads this
|
# reads this so the Eww widget can show presence as a distinct signal from
|
||||||
# so the Eww widget can show presence as a distinct signal from the idle lock.
|
# the idle lock. During a MANUAL caffeine session the daemon still checks (the
|
||||||
# It reflects live camera state only while the daemon is actively watching:
|
# check is cheap and doesn't touch any device other apps might want), so this
|
||||||
# during a MANUAL caffeine session the daemon stops reading the camera entirely
|
# stays live even then — only the inhibit ownership differs.
|
||||||
# (to leave it free) and clears this flag, so presence reads false there.
|
|
||||||
PRESENCE_FLAG="/tmp/presence-detected"
|
PRESENCE_FLAG="/tmp/presence-detected"
|
||||||
PRESENCE_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/presence-detect.conf"
|
|
||||||
|
|
||||||
INTERVAL_FAST=2 # near-idle system (< LOAD_LIGHT): poll fast for responsiveness
|
INTERVAL_LIGHT=20 # seconds between checks when CPU-or-RAM usage < LOAD_LIGHT
|
||||||
INTERVAL_IDLE=20 # seconds between checks under normal load
|
INTERVAL_MID=120 # seconds between checks when usage is between the two thresholds
|
||||||
INTERVAL_BUSY=120 # seconds between checks when CPU or RAM is >= LOAD_BUSY
|
INTERVAL_HEAVY=600 # seconds between checks when usage >= LOAD_HEAVY
|
||||||
LOAD_BUSY=0.70 # CPU-or-RAM fraction at/above which we back off to INTERVAL_BUSY
|
LOAD_LIGHT=0.20 # CPU-or-RAM fraction below which we poll at INTERVAL_LIGHT
|
||||||
LOAD_LIGHT=0.30 # CPU-or-RAM fraction below which we speed up to INTERVAL_FAST
|
LOAD_HEAVY=0.50 # CPU-or-RAM fraction at/above which we back off to INTERVAL_HEAVY
|
||||||
NPROC="$(nproc)"
|
NPROC="$(nproc)"
|
||||||
|
|
||||||
# Grace window: keep reporting "present" for this long after the last detected
|
# True if any FIDO2/U2F authenticator is currently enumerable over USB.
|
||||||
# motion, so brief stillness (reading, thinking, a slow moment) never drops the
|
# fido2-token -L lists one line per connected device and needs no PIN, touch,
|
||||||
# lock the instant one tick sees nothing. Any motion tick resets the clock.
|
# or tap — it's a pure USB HID enumeration, so this never prompts the key.
|
||||||
GRACE_SECONDS=90
|
_fido_key_present() {
|
||||||
|
[[ -n "$(fido2-token -L 2>/dev/null)" ]]
|
||||||
# Resolve camera ID: env var takes highest priority, then config file, then default 0.
|
|
||||||
_camera_id() {
|
|
||||||
if [[ -n "$PRESENCE_DETECT_CAMERA" ]]; then
|
|
||||||
echo "$PRESENCE_DETECT_CAMERA"
|
|
||||||
elif [[ -f "$PRESENCE_CFG" ]]; then
|
|
||||||
# -oP 'CAMERA=\K[0-9]+': Perl-style look-behind strips "CAMERA=" prefix.
|
|
||||||
grep -oP 'CAMERA=\K[0-9]+' "$PRESENCE_CFG" 2>/dev/null || echo 0
|
|
||||||
else
|
|
||||||
echo 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# True if any process OTHER than this daemon currently holds the real camera
|
|
||||||
# open — a video call, howdy, etc. `fuser` lists same-user holders without root
|
|
||||||
# (every camera app runs as us), and we only ever call this when we aren't
|
|
||||||
# reading the camera ourselves, so any holder is another app. If fuser isn't
|
|
||||||
# installed we return false: the tick then just tries the camera and the Python
|
|
||||||
# helper's busy/in-use path (exit 3) still covers an already-held device.
|
|
||||||
_camera_in_use() {
|
|
||||||
command -v fuser >/dev/null 2>&1 || return 1
|
|
||||||
fuser -s "/dev/video${1}" 2>/dev/null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Prints current CPU-or-RAM usage as a fraction (0..1): the higher of the
|
# Prints current CPU-or-RAM usage as a fraction (0..1): the higher of the
|
||||||
|
|
@ -87,14 +52,14 @@ _resource_usage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Three-tier poll interval based on current resource usage:
|
# Three-tier poll interval based on current resource usage:
|
||||||
# >= LOAD_BUSY -> INTERVAL_BUSY (heavy load: back off, stay out of the way)
|
# >= LOAD_HEAVY -> INTERVAL_HEAVY (heavy load: back off, stay out of the way)
|
||||||
# < LOAD_LIGHT -> INTERVAL_FAST (near-idle: poll fast, cheap and responsive)
|
# < LOAD_LIGHT -> INTERVAL_LIGHT (near-idle: poll fast, cheap and responsive)
|
||||||
# otherwise -> INTERVAL_IDLE (normal load)
|
# otherwise -> INTERVAL_MID (moderate load)
|
||||||
_next_interval() {
|
_next_interval() {
|
||||||
local u; u="$(_resource_usage)"
|
local u; u="$(_resource_usage)"
|
||||||
awk -v u="$u" -v busy="$LOAD_BUSY" -v light="$LOAD_LIGHT" \
|
awk -v u="$u" -v heavy="$LOAD_HEAVY" -v light="$LOAD_LIGHT" \
|
||||||
-v fb="$INTERVAL_BUSY" -v ff="$INTERVAL_FAST" -v fi="$INTERVAL_IDLE" \
|
-v fh="$INTERVAL_HEAVY" -v fl="$INTERVAL_LIGHT" -v fm="$INTERVAL_MID" \
|
||||||
'BEGIN{ if (u >= busy) print fb; else if (u < light) print ff; else print fi }'
|
'BEGIN{ if (u >= heavy) print fh; else if (u < light) print fl; else print fm }'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Returns true if the inhibitor sentinel process is still alive.
|
# Returns true if the inhibitor sentinel process is still alive.
|
||||||
|
|
@ -109,12 +74,12 @@ _start_inhibit() {
|
||||||
# --what=idle: target the logind idle-inhibit lock that hypridle polls.
|
# --what=idle: target the logind idle-inhibit lock that hypridle polls.
|
||||||
# "sleep infinity" is the sentinel; its PID is saved so we can kill it later.
|
# "sleep infinity" is the sentinel; its PID is saved so we can kill it later.
|
||||||
systemd-inhibit --what=idle --who="presence-detect" \
|
systemd-inhibit --what=idle --who="presence-detect" \
|
||||||
--why="User presence detected" --mode=block \
|
--why="FIDO key present" --mode=block \
|
||||||
sleep infinity &
|
sleep infinity &
|
||||||
echo $! > "$PID_FILE"
|
echo $! > "$PID_FILE"
|
||||||
touch "$OWNED_FLAG"
|
touch "$OWNED_FLAG"
|
||||||
# logger writes to the system journal — visible via `journalctl -t presence-detect`.
|
# logger writes to the system journal — visible via `journalctl -t presence-detect`.
|
||||||
logger -t presence-detect "Motion detected — idle inhibited"
|
logger -t presence-detect "FIDO key present — idle inhibited"
|
||||||
}
|
}
|
||||||
|
|
||||||
_stop_inhibit() {
|
_stop_inhibit() {
|
||||||
|
|
@ -125,7 +90,7 @@ _stop_inhibit() {
|
||||||
# Killing the sleep process releases the systemd-inhibit lock automatically.
|
# Killing the sleep process releases the systemd-inhibit lock automatically.
|
||||||
kill "$(cat "$PID_FILE")" 2>/dev/null
|
kill "$(cat "$PID_FILE")" 2>/dev/null
|
||||||
rm -f "$PID_FILE" "$OWNED_FLAG"
|
rm -f "$PID_FILE" "$OWNED_FLAG"
|
||||||
logger -t presence-detect "No motion — idle inhibit released"
|
logger -t presence-detect "FIDO key removed — idle inhibit released"
|
||||||
}
|
}
|
||||||
|
|
||||||
_cleanup() {
|
_cleanup() {
|
||||||
|
|
@ -139,53 +104,23 @@ _cleanup() {
|
||||||
# Intercept termination signals to ensure the inhibitor PID is never orphaned.
|
# Intercept termination signals to ensure the inhibitor PID is never orphaned.
|
||||||
trap _cleanup SIGTERM SIGINT SIGHUP
|
trap _cleanup SIGTERM SIGINT SIGHUP
|
||||||
|
|
||||||
# Epoch seconds of the last tick that saw motion; drives the GRACE_SECONDS
|
|
||||||
# window below. 0 = never seen yet, so a no-motion tick at startup releases
|
|
||||||
# immediately (nothing is held anyway).
|
|
||||||
LAST_MOTION=0
|
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
# When idle is inhibited MANUALLY (a caffeine toggle, not this daemon), the
|
# When idle is inhibited MANUALLY (a caffeine toggle, not this daemon), the
|
||||||
# lock is already held regardless of presence, so there's no reason to look
|
# lock is already held regardless of presence. Detected the same way
|
||||||
# at the camera — skip the read entirely and leave the device free for other
|
# caffeine-manual-status.sh does it: the shared lock is alive but we don't
|
||||||
# apps. Detected the same way caffeine-manual-status.sh does it: the shared
|
# own it. We still update PRESENCE_FLAG below since checking is cheap and
|
||||||
# lock is alive but we don't own it. We clear PRESENCE_FLAG because we're no
|
# doesn't touch a device other apps care about — only inhibit ownership
|
||||||
# longer watching and must not report a stale "camera sees you".
|
# differs during a manual session.
|
||||||
if _inhibit_running && [[ ! -f "$OWNED_FLAG" ]]; then
|
if _fido_key_present; then
|
||||||
|
touch "$PRESENCE_FLAG"
|
||||||
|
# No-op if a manual caffeine session already holds the lock — only
|
||||||
|
# _stop_inhibit needs the ownership check; starting is always safe
|
||||||
|
# since _start_inhibit itself no-ops when a lock is already held.
|
||||||
|
_start_inhibit
|
||||||
|
else
|
||||||
rm -f "$PRESENCE_FLAG"
|
rm -f "$PRESENCE_FLAG"
|
||||||
sleep "$(_next_interval)"
|
_stop_inhibit
|
||||||
continue
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
DEVICE="$(_camera_id)"
|
|
||||||
now="$(date +%s)"
|
|
||||||
|
|
||||||
# Back-off: if another app already holds the camera (a video call, howdy,
|
|
||||||
# ...), don't even try to read it — attempting would be pointless (we'd just
|
|
||||||
# get EBUSY) and a simultaneous open could make the app itself fail. A camera
|
|
||||||
# in use is proof you're present, so mark presence and wait it out instead.
|
|
||||||
if _camera_in_use "$DEVICE"; then
|
|
||||||
LAST_MOTION="$now"; touch "$PRESENCE_FLAG"; _start_inhibit
|
|
||||||
sleep "$(_next_interval)"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run the OpenCV motion detector; stderr suppressed to keep the journal clean.
|
|
||||||
python3 "$PYTHON_DETECT" "$DEVICE" 2>/dev/null
|
|
||||||
rc=$?
|
|
||||||
case $rc in
|
|
||||||
# Motion (0) or camera busy/in use (3): user is present. rc=3 means
|
|
||||||
# another app (a video call, howdy) is holding the camera, which is
|
|
||||||
# itself proof you're here, so it keeps the session awake even though we
|
|
||||||
# can't read frames. Both refresh the grace clock and mark presence.
|
|
||||||
0|3) LAST_MOTION="$now"; touch "$PRESENCE_FLAG"; _start_inhibit ;;
|
|
||||||
# No motion: only actually release once we've been still for the whole
|
|
||||||
# grace window. Within it, leave the flag/lock exactly as they were so a
|
|
||||||
# brief pause in movement doesn't flicker presence off.
|
|
||||||
1) if (( now - LAST_MOTION >= GRACE_SECONDS )); then
|
|
||||||
rm -f "$PRESENCE_FLAG"; _stop_inhibit
|
|
||||||
fi ;;
|
|
||||||
# rc=2 = camera unavailable (unplugged/gone) — silently skip, state unchanged
|
|
||||||
esac
|
|
||||||
sleep "$(_next_interval)"
|
sleep "$(_next_interval)"
|
||||||
done
|
done
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Reports the PRESENCE input for the Eww caffeine widget: whether the webcam
|
# Reports the PRESENCE input for the Eww caffeine widget: whether the
|
||||||
# presence-detect daemon currently sees the user (motion on its last tick),
|
# presence-detect daemon currently sees a FIDO key plugged in (on its last
|
||||||
# INDEPENDENT of whether the idle lock is held or who holds it.
|
# tick), INDEPENDENT of whether the idle lock is held or who holds it.
|
||||||
#
|
#
|
||||||
# presence-detect.sh writes PRESENCE_FLAG on every "present" tick and removes it
|
# presence-detect.sh writes PRESENCE_FLAG on every "present" tick and removes
|
||||||
# on every "away" tick, so this is a clean, standalone signal. Note: during a
|
# it on every "away" tick, so this is a clean, standalone signal — it stays
|
||||||
# MANUAL caffeine session the daemon stops reading the camera (to leave it free)
|
# live even during a MANUAL caffeine session, since checking for the key is
|
||||||
# and clears the flag, so presence reads false there. For the MANUAL-inhibit
|
# cheap and doesn't contend with anything. For the MANUAL-inhibit input, see
|
||||||
# input, see caffeine-manual-status.sh.
|
# caffeine-manual-status.sh.
|
||||||
#
|
#
|
||||||
# Prints "true" when presence is detected, else "false".
|
# Prints "true" when presence is detected, else "false".
|
||||||
PRESENCE_FLAG="/tmp/presence-detected"
|
PRESENCE_FLAG="/tmp/presence-detected"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# presence-test.sh — standalone check for FIDO key presence detection.
|
||||||
|
#
|
||||||
|
# Kept separate from enroll-biometrics.sh: there's nothing to enroll or
|
||||||
|
# configure here (fido2-token needs no setup, it just enumerates the key over
|
||||||
|
# USB), so bundling it into the enrollment TUI was more confusing than useful.
|
||||||
|
#
|
||||||
|
# Runs the exact same check presence-detect.sh uses on every poll tick.
|
||||||
|
|
||||||
|
command -v fido2-token &>/dev/null || {
|
||||||
|
printf "\n\033[1;31m fido2-token not found\033[0m (from libfido2).\n"
|
||||||
|
printf " Install it: sudo pacman -S libfido2\n\n"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
printf "\n\033[1;35m Checking for a connected FIDO key...\033[0m\n"
|
||||||
|
printf "\033[35m ─────────────────────────────────────────\033[0m\n\n"
|
||||||
|
|
||||||
|
out=$(fido2-token -L 2>/dev/null)
|
||||||
|
if [[ -n "$out" ]]; then
|
||||||
|
printf " \033[1;32mFIDO key detected:\033[0m\n\n"
|
||||||
|
printf '%s\n' "$out" | sed 's/^/ /'
|
||||||
|
printf "\n Presence detection is working correctly.\n\n"
|
||||||
|
else
|
||||||
|
printf " \033[1;33mNo FIDO key detected.\033[0m\n\n"
|
||||||
|
printf " Make sure your key is plugged in.\n\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Webcam motion presence detector using frame differencing.
|
|
||||||
Exit codes:
|
|
||||||
0 = motion detected (user present)
|
|
||||||
1 = no motion (user likely away)
|
|
||||||
2 = camera unavailable (device node missing / gone) -> caller skips tick
|
|
||||||
3 = camera busy / in use (another app holds it) -> treat as present
|
|
||||||
Usage: presence_detect.py [camera_id]
|
|
||||||
|
|
||||||
Reads the physical camera directly, only for the ~0.5s it takes to grab a few
|
|
||||||
frames, then releases it — so a video call, howdy, etc. can use the camera the
|
|
||||||
rest of the time. When one of those apps IS holding the camera, we can't read
|
|
||||||
it; rather than mistake that for "no motion", we report it as busy/in-use (3),
|
|
||||||
which the daemon treats as presence: a camera in use means the user is here.
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
FRAMES_TO_CHECK = 8
|
|
||||||
DIFFS_NEEDED = 2 # require motion in at least N consecutive-frame diffs
|
|
||||||
PIXEL_DELTA_THRESHOLD = 18 # per-pixel grayscale delta to count as "changed"
|
|
||||||
MOTION_AREA_RATIO = 0.008 # fraction of pixels that must change to call it motion
|
|
||||||
BLUR_KSIZE = (21, 21) # Gaussian blur kernel to suppress sensor noise
|
|
||||||
# Spacing between the frames we diff. Without it the frames are grabbed
|
|
||||||
# back-to-back within a few milliseconds, so slow, small movements barely differ
|
|
||||||
# between adjacent frames. Spacing the grabs out gives slow motion a real
|
|
||||||
# temporal baseline and guarantees each compared frame is a fresh one.
|
|
||||||
INTER_FRAME_DELAY = 0.06 # seconds between grabs (~0.5s total observation window)
|
|
||||||
|
|
||||||
|
|
||||||
def detect(camera_id: int) -> int:
|
|
||||||
# A missing device node means the camera is genuinely gone (unplugged, module
|
|
||||||
# not loaded) -> "unavailable" (2), and the daemon leaves presence untouched.
|
|
||||||
# A node that exists but won't open (below) or won't yield frames (further
|
|
||||||
# down) means something else is streaming it -> "busy/in use" (3) = present.
|
|
||||||
if not os.path.exists(f"/dev/video{camera_id}"):
|
|
||||||
return 2
|
|
||||||
|
|
||||||
cap = cv2.VideoCapture(camera_id)
|
|
||||||
if not cap.isOpened():
|
|
||||||
# Node exists but we can't open it: another app owns the camera.
|
|
||||||
return 3
|
|
||||||
|
|
||||||
motion_diffs = 0
|
|
||||||
frames_read = 0
|
|
||||||
prev_gray = None
|
|
||||||
try:
|
|
||||||
for i in range(FRAMES_TO_CHECK):
|
|
||||||
if i > 0:
|
|
||||||
time.sleep(INTER_FRAME_DELAY)
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
continue
|
|
||||||
frames_read += 1
|
|
||||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
||||||
gray = cv2.GaussianBlur(gray, BLUR_KSIZE, 0)
|
|
||||||
|
|
||||||
if prev_gray is not None:
|
|
||||||
delta = cv2.absdiff(prev_gray, gray)
|
|
||||||
changed = np.count_nonzero(delta > PIXEL_DELTA_THRESHOLD)
|
|
||||||
if changed / delta.size >= MOTION_AREA_RATIO:
|
|
||||||
motion_diffs += 1
|
|
||||||
|
|
||||||
prev_gray = gray
|
|
||||||
finally:
|
|
||||||
cap.release()
|
|
||||||
|
|
||||||
# Opened but produced no frames at all: on V4L2 a second opener can succeed
|
|
||||||
# yet have its reads starved while another app streams the device. Treat that
|
|
||||||
# as busy/in-use (present) rather than "no motion", so an active video call
|
|
||||||
# keeps the session awake even though we never see a usable frame.
|
|
||||||
if frames_read == 0:
|
|
||||||
return 3
|
|
||||||
|
|
||||||
return 0 if motion_diffs >= DIFFS_NEEDED else 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
camera_id = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
|
||||||
sys.exit(detect(camera_id))
|
|
||||||
|
|
@ -71,9 +71,9 @@ spawn-at-startup "dunst"
|
||||||
// swayidle doesn't block waiting for it to exit.
|
// swayidle doesn't block waiting for it to exit.
|
||||||
spawn-at-startup "bash" "-c" "swayidle -w timeout 300 'swaylock -f' timeout 600 'systemctl suspend' before-sleep 'swaylock -f'"
|
spawn-at-startup "bash" "-c" "swayidle -w timeout 300 'swaylock -f' timeout 600 'systemctl suspend' before-sleep 'swaylock -f'"
|
||||||
|
|
||||||
// presence-detect.sh — webcam-based automatic idle inhibitor.
|
// presence-detect.sh — FIDO-key-based automatic idle inhibitor.
|
||||||
// WHY: detects when the user is physically at the desk (via camera) and holds
|
// WHY: detects when a FIDO2/U2F security key is plugged in and holds the idle
|
||||||
// the idle inhibitor active so the screen doesn't lock unnecessarily.
|
// inhibitor active so the screen doesn't lock unnecessarily.
|
||||||
// Complements the manual caffeine.sh toggle (Mod+Shift+C).
|
// Complements the manual caffeine.sh toggle (Mod+Shift+C).
|
||||||
spawn-at-startup "bash" "-c" "~/.config/scripts/presence-detect.sh"
|
spawn-at-startup "bash" "-c" "~/.config/scripts/presence-detect.sh"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Toggle idle inhibit via systemd-inhibit (swayidle respects the logind idle hint).
|
# Toggle idle inhibit via systemd-inhibit (swayidle respects the logind idle hint).
|
||||||
# This lock is shared with presence-detect.sh, which also inhibits idle while
|
# This lock is shared with presence-detect.sh, which also inhibits idle while
|
||||||
# motion is detected — OWNED_FLAG marks lock ownership between the two, so clear
|
# a FIDO key is plugged in — OWNED_FLAG marks lock ownership between the two,
|
||||||
# it on every manual toggle: this action is always a manual takeover of the lock.
|
# so clear it on every manual toggle: this action is always a manual takeover
|
||||||
|
# of the lock.
|
||||||
PID_FILE="/tmp/caffeine-inhibit.pid"
|
PID_FILE="/tmp/caffeine-inhibit.pid"
|
||||||
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../../hyprlua/scripts/presence-test.sh
|
||||||
|
|
@ -153,8 +153,8 @@ HYPRDRIVE_PACKAGES=(
|
||||||
wf-recorder
|
wf-recorder
|
||||||
sound-theme-freedesktop
|
sound-theme-freedesktop
|
||||||
|
|
||||||
python-opencv # webcam presence-detection daemon
|
libfido2 # fido2-token; FIDO key presence-detection daemon
|
||||||
v4l-utils
|
v4l-utils # howdy camera enrollment
|
||||||
)
|
)
|
||||||
sudo pacman -Syu --noconfirm --needed -- "${HYPRDRIVE_PACKAGES[@]}"
|
sudo pacman -Syu --noconfirm --needed -- "${HYPRDRIVE_PACKAGES[@]}"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@
|
||||||
# - EWW and config source paths point to desktopenvs/hyprland/ (not hyprlua/)
|
# - EWW and config source paths point to desktopenvs/hyprland/ (not hyprlua/)
|
||||||
# - hypr-usr/ device-specific .conf files are placed at ~/.config/ root level
|
# - hypr-usr/ device-specific .conf files are placed at ~/.config/ root level
|
||||||
# and sourced by hyprland.conf via 'source' directives
|
# and sourced by hyprland.conf via 'source' directives
|
||||||
# - hyprlua.sh additionally installs python-opencv and v4l-utils for the
|
# - hyprlua.sh additionally installs libfido2 and v4l-utils for the
|
||||||
# webcam-based presence/idle detection daemon
|
# FIDO-key-based presence/idle detection daemon
|
||||||
#
|
#
|
||||||
# High-level install flow:
|
# High-level install flow:
|
||||||
# 1. System update + Flatpak
|
# 1. System update + Flatpak
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
# - Config source paths point to desktopenvs/hyprlua/
|
# - Config source paths point to desktopenvs/hyprlua/
|
||||||
# - Device-specific overrides live in ~/.config/hypr/usr/ (Lua modules
|
# - Device-specific overrides live in ~/.config/hypr/usr/ (Lua modules
|
||||||
# loaded via require("usr.*")), not at ~/.config/ root
|
# loaded via require("usr.*")), not at ~/.config/ root
|
||||||
# - Adds python-opencv + v4l-utils for the webcam presence-detection daemon
|
# - Adds libfido2 (FIDO key presence-detection daemon) + v4l-utils (howdy camera setup)
|
||||||
# - Tablet mode also installs evdev-right-click-emulation from AUR
|
# - Tablet mode also installs evdev-right-click-emulation from AUR
|
||||||
# - gsettings sets prefer-dark globally (relevant for GTK apps under Hyprland)
|
# - gsettings sets prefer-dark globally (relevant for GTK apps under Hyprland)
|
||||||
# - qt6ct is NOT installed (hyprlua dropped it from the package list)
|
# - qt6ct is NOT installed (hyprlua dropped it from the package list)
|
||||||
|
|
@ -155,8 +155,8 @@ HYPRLUA_PACKAGES=(
|
||||||
wf-recorder # screen recorder for wlroots-based compositors
|
wf-recorder # screen recorder for wlroots-based compositors
|
||||||
sound-theme-freedesktop # standard freedesktop sound event samples
|
sound-theme-freedesktop # standard freedesktop sound event samples
|
||||||
|
|
||||||
python-opencv # OpenCV Python bindings; webcam presence-detection daemon
|
libfido2 # fido2-token; FIDO key presence-detection daemon
|
||||||
v4l-utils # Video4Linux2 tools for enumerating and configuring webcams
|
v4l-utils # Video4Linux2 tools for howdy's camera enrollment
|
||||||
)
|
)
|
||||||
sudo pacman -Syu --noconfirm --needed -- "${HYPRLUA_PACKAGES[@]}"
|
sudo pacman -Syu --noconfirm --needed -- "${HYPRLUA_PACKAGES[@]}"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ sudo pacman -Syu --noconfirm --needed \
|
||||||
thunar tumbler thunar-archive-plugin thunar-shares-plugin thunar-volman \
|
thunar tumbler thunar-archive-plugin thunar-shares-plugin thunar-volman \
|
||||||
pcmanfm-qt udisks2 ly kew \
|
pcmanfm-qt udisks2 ly kew \
|
||||||
pavucontrol playerctl wf-recorder sound-theme-freedesktop \
|
pavucontrol playerctl wf-recorder sound-theme-freedesktop \
|
||||||
xinput jq python-opencv v4l-utils \
|
xinput jq libfido2 v4l-utils \
|
||||||
gtk-layer-shell # GTK3 wlr-layer-shell library EWW's gtk-layer-shell-sys
|
gtk-layer-shell # GTK3 wlr-layer-shell library EWW's gtk-layer-shell-sys
|
||||||
# crate links against; without it `cargo build` below
|
# crate links against; without it `cargo build` below
|
||||||
# fails at that build script with a pkg-config error.
|
# fails at that build script with a pkg-config error.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue