diff --git a/desktopenvs/hyprlua/hypr-usr/autostart.lua b/desktopenvs/hyprlua/hypr-usr/autostart.lua index d4c948c..c26115c 100644 --- a/desktopenvs/hyprlua/hypr-usr/autostart.lua +++ b/desktopenvs/hyprlua/hypr-usr/autostart.lua @@ -20,4 +20,5 @@ hl.on("hyprland.start", function() hl.exec_cmd("blueman-applet") hl.exec_cmd("blueman-tray") hl.exec_cmd("hypridle") + hl.exec_cmd("bash ~/Dotfiles/desktopenvs/hyprlua/scripts/presence-detect.sh") end) diff --git a/desktopenvs/hyprlua/hypr-usr/binds.lua b/desktopenvs/hyprlua/hypr-usr/binds.lua index e5fb31b..1b52193 100644 --- a/desktopenvs/hyprlua/hypr-usr/binds.lua +++ b/desktopenvs/hyprlua/hypr-usr/binds.lua @@ -260,6 +260,7 @@ hl.bind(mainMod .. " + CTRL + Z", hl.dsp.exec_cmd("chamel clear-and-deactivate") hl.bind(mainMod .. " + CTRL + C", hl.dsp.exec_cmd("dunstctl close-all")) hl.bind(mainMod .. " + CTRL + G", hl.dsp.exec_cmd("~/.config/scripts/onscreenkb.sh")) hl.bind(mainMod .. " + SHIFT + C", hl.dsp.exec_cmd("~/.config/scripts/caffeine.sh")) +hl.bind(mainMod .. " + SHIFT + B", hl.dsp.exec_cmd("[tag +centered-S] kitty bash ~/.config/scripts/enroll-biometrics.sh")) hl.bind(mainMod .. " + SHIFT + X", hl.dsp.exec_cmd("~/.config/scripts/hyprland-toggle-touchpad.sh")) hl.bind(mainMod .. " + CTRL + E", hl.dsp.exec_cmd("~/.config/scripts/screenrotationwcw.sh")) diff --git a/desktopenvs/hyprlua/hypr/hypridle.conf b/desktopenvs/hyprlua/hypr/hypridle.conf index dcc57d1..fbf07ba 100644 --- a/desktopenvs/hyprlua/hypr/hypridle.conf +++ b/desktopenvs/hyprlua/hypr/hypridle.conf @@ -1,22 +1,19 @@ -#source ~/.config/idle.conf general { - lock_cmd = pidof hyprlock || hyprlock # avoid starting multiple hyprlock instances. - before_sleep_cmd = loginctl lock-session # lock before suspend. - after_sleep_cmd = systemctl restart fprintd.service ;; hyprctl dispatch dpms on # to avoid having to press a key twice to turn on the display. - + lock_cmd = pidof hyprlock || hyprlock + before_sleep_cmd = loginctl lock-session + # fprintd restart ensures fingerprint sensor is ready after resume + after_sleep_cmd = systemctl restart fprintd.service ; hyprctl dispatch dpms on + ignore_dbus_inhibit = false # respect systemd-inhibit locks (presence-detect, caffeine) } + +# Presence detection resets the idle timer every 2 minutes while you're visible, +# so these timeouts only run when you've actually stepped away. listener { - timeout = 120 - on-timeout = loginctl lock-session # lock screen when timeout has passed + timeout = 180 # 3 min — lock screen + on-timeout = loginctl lock-session } listener { - timeout = 600 #10min - on-timeout = systemctl suspend-then-hibernate # suspend pc + timeout = 600 # 10 min — suspend + on-timeout = systemctl suspend-then-hibernate } - - -#listener { -# timeout = 18000 #5h -# on-timeout = /usr/bin/reboot #reboot -#} diff --git a/desktopenvs/hyprlua/scripts/caffeine.sh b/desktopenvs/hyprlua/scripts/caffeine.sh index b6d40e6..b21cc27 100755 --- a/desktopenvs/hyprlua/scripts/caffeine.sh +++ b/desktopenvs/hyprlua/scripts/caffeine.sh @@ -1,17 +1,17 @@ #!/bin/bash +# Toggle idle inhibit via systemd-inhibit (hypridle respects the logind idle hint). +PID_FILE="/tmp/caffeine-inhibit.pid" -statecon=$( pidof hypridle | grep "[1234567890]" ) -#echo $statecon -if [ "$statecon" != "" ]; then - - notify-send -t 1000 "caffeine mode on" - killall hypridle - +if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then + kill "$(cat "$PID_FILE")" + rm -f "$PID_FILE" + notify-send -t 2000 "Caffeine" "Idle inhibit OFF" else - - notify-send -t 1000 "caffeine mode off" - hypridle & - + systemd-inhibit --what=idle:sleep \ + --who="caffeine" \ + --why="Caffeine mode active" \ + --mode=block \ + sleep infinity & + echo $! > "$PID_FILE" + notify-send -t 2000 "Caffeine" "Idle inhibited" fi - - diff --git a/desktopenvs/hyprlua/scripts/enroll-biometrics.sh b/desktopenvs/hyprlua/scripts/enroll-biometrics.sh new file mode 100755 index 0000000..5aa2b73 --- /dev/null +++ b/desktopenvs/hyprlua/scripts/enroll-biometrics.sh @@ -0,0 +1,251 @@ +#!/bin/bash +# enroll-biometrics.sh — TUI for face biometric setup. +# +# Two subsystems: +# 1. Presence detection — configure/test the webcam used by presence-detect.sh +# 2. Howdy face auth — enroll/manage/test face models for PAM authentication + +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" + +# ── Dialog theme (Cyberqueer) ───────────────────────────────────────────────── +TMP_D=$(mktemp -d) +trap 'rm -rf "$TMP_D"' EXIT INT TERM + +export DIALOGRC="$TMP_D/dialogrc" +cat > "$DIALOGRC" << 'RCEOF' +use_shadow = ON +use_colors = ON +screen_color = (BLACK,BLACK,ON) +shadow_color = (BLACK,BLACK,ON) +title_color = (MAGENTA,BLACK,ON) +border_color = (MAGENTA,BLACK,ON) +button_active_color = (BLACK,MAGENTA,ON) +button_inactive_color = (WHITE,BLACK,OFF) +button_key_active_color = (BLACK,CYAN,ON) +button_key_inactive_color = (CYAN,BLACK,ON) +button_label_active_color = (BLACK,MAGENTA,ON) +button_label_inactive_color = (WHITE,BLACK,OFF) +inputbox_color = (WHITE,BLACK,OFF) +inputbox_border_color = (MAGENTA,BLACK,ON) +menubox_color = (WHITE,BLACK,OFF) +menubox_border_color = (MAGENTA,BLACK,ON) +item_color = (WHITE,BLACK,OFF) +item_selected_color = (BLACK,MAGENTA,ON) +tag_color = (CYAN,BLACK,ON) +tag_selected_color = (BLACK,CYAN,ON) +check_color = (WHITE,BLACK,OFF) +check_selected_color = (BLACK,MAGENTA,ON) +uarrow_color = (MAGENTA,BLACK,ON) +darrow_color = (MAGENTA,BLACK,ON) +RCEOF + +# ── Helpers ─────────────────────────────────────────────────────────────────── +die() { clear; printf "\nError: %s\n\n" "$1" >&2; exit 1; } + +msg() { + dialog --backtitle "$BACKTITLE" --title " $1 " --msgbox "\n$2\n" "$3" "$4" +} + +require_dialog() { + command -v dialog &>/dev/null && return + sudo pacman -S --noconfirm dialog || die "dialog not found" +} + +# ── Camera helpers ───────────────────────────────────────────────────────────── +list_cameras() { + for dev in /dev/video*; do + [[ -c "$dev" ]] || continue + local id="${dev#/dev/video}" + local name + name=$(v4l2-ctl --device="$dev" --info 2>/dev/null \ + | awk -F': ' '/Card type/{print $2}' | head -1) + [[ -z "$name" ]] && name="Camera $id" + printf '%s\n%s\n' "$id" "$name" + 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" +} + +# ── Presence detection ──────────────────────────────────────────────────────── +presence_configure_camera() { + local -a cam_items + mapfile -t cam_items < <(list_cameras) + + if [[ ${#cam_items[@]} -eq 0 ]]; then + msg "No Cameras Found" \ + "No video devices found at /dev/video*.\n\nMake sure your webcam is connected." \ + 10 55 + return + fi + + local current; current=$(get_camera_id) + local choice + choice=$(dialog --backtitle "$BACKTITLE" \ + --title " Select Camera " \ + --menu "\nCurrent: /dev/video${current}\n\nSelect camera to use for presence detection:" \ + 16 62 6 \ + "${cam_items[@]}" \ + 3>&1 1>&2 2>&3) || return + + set_camera_id "$choice" + msg "Camera Set" \ + "Presence detection will use /dev/video${choice}.\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 " Look at the camera and stay still.\n\n" + + python3 "$PYTHON_DETECT" "$cam" 2>/dev/null + local rc=$? + + case $rc in + 0) msg "Test Result" "Face detected!\n\nPresence detection is working correctly." 8 52 ;; + 1) msg "Test Result" \ + "No face detected.\n\nMake sure you are in front of the camera\nand there is 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_installed() { command -v howdy &>/dev/null; } + +howdy_require() { + howdy_installed && return 0 + dialog --backtitle "$BACKTITLE" --title " Howdy Not Found " \ + --yesno "\nhowdy is not installed.\n\nInstall it now via yay?" 8 48 || return 1 + clear + printf "\nInstalling howdy...\n\n" + if command -v yay &>/dev/null; then + yay -S --noconfirm --needed howdy + else + sudo pacman -S --noconfirm --needed howdy + fi + howdy_installed || { msg "Install Failed" "howdy installation failed.\nInstall it manually: yay -S howdy" 8 52; return 1; } +} + +# ── Howdy operations ────────────────────────────────────────────────────────── +howdy_add() { + howdy_require || return + + local name + name=$(dialog --backtitle "$BACKTITLE" \ + --title " Add Face Model " \ + --inputbox "\nEnter a label for this face model:" 8 52 "$USER" \ + 3>&1 1>&2 2>&3) || return + [[ -z "$name" ]] && return + + clear + printf "\n\033[1;35m Enrolling face model '%s'...\033[0m\n" "$name" + printf "\033[35m ─────────────────────────────────────────\033[0m\n\n" + printf " Look at the camera and hold still.\n\n" + sudo howdy add -n "$name" + local rc=$? + + if [[ $rc -eq 0 ]]; then + msg "Enrolled" \ + "Face model '$name' added.\n\nYou may need to relogin for PAM changes to take effect." \ + 9 58 + else + msg "Failed" "Enrollment failed (code $rc).\n\nCheck camera and lighting." 8 52 + fi +} + +howdy_list() { + howdy_require || return + local out; out=$(sudo howdy list 2>&1) + msg "Enrolled Face Models" "$out" 20 64 +} + +howdy_remove() { + howdy_require || return + + local models; models=$(sudo howdy list 2>&1) + if [[ -z "$models" || "$models" == *"no models"* ]]; then + msg "No Models" "No face models are currently enrolled." 7 44 + return + fi + + local id + id=$(dialog --backtitle "$BACKTITLE" \ + --title " Remove Face Model " \ + --inputbox "\nCurrent models:\n\n${models}\n\nEnter model ID to remove:" 20 64 \ + 3>&1 1>&2 2>&3) || return + [[ -z "$id" || ! "$id" =~ ^[0-9]+$ ]] && { + msg "Invalid ID" "Please enter a numeric model ID." 6 36; return + } + + dialog --backtitle "$BACKTITLE" --title " Confirm " \ + --yesno "\nRemove face model ID $id?" 7 40 || return + + sudo howdy remove -I "$id" + msg "Done" "Face model $id removed." 6 38 +} + +howdy_test() { + howdy_require || return + clear + printf "\n\033[1;35m Testing howdy authentication...\033[0m\n" + printf "\033[35m ─────────────────────────────────────────\033[0m\n\n" + printf " Look at the camera.\n\n" + sudo howdy test + local rc=$? + if [[ $rc -eq 0 ]]; then + msg "Result" "Authentication successful." 6 38 + else + msg "Result" "Authentication failed (code $rc).\n\nCheck enrolled models and camera setup." 9 52 + fi +} + +# ── Main menu ───────────────────────────────────────────────────────────────── +main_menu() { + local choice + choice=$(dialog --backtitle "$BACKTITLE" \ + --title " Biometric Enrollment " \ + --menu "\nSelect an option:" 20 66 8 \ + "1" "Presence detection — configure camera" \ + "2" "Presence detection — test detection" \ + "3" "Howdy face auth — add face model" \ + "4" "Howdy face auth — list enrolled models" \ + "5" "Howdy face auth — remove face model" \ + "6" "Howdy face auth — test authentication" \ + 3>&1 1>&2 2>&3) || { clear; exit 0; } + + case "$choice" in + 1) presence_configure_camera ;; + 2) presence_test_camera ;; + 3) howdy_add ;; + 4) howdy_list ;; + 5) howdy_remove ;; + 6) howdy_test ;; + esac + main_menu +} + +require_dialog +main_menu diff --git a/desktopenvs/hyprlua/scripts/presence-detect.sh b/desktopenvs/hyprlua/scripts/presence-detect.sh new file mode 100755 index 0000000..387a4f4 --- /dev/null +++ b/desktopenvs/hyprlua/scripts/presence-detect.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Webcam presence detection daemon. +# Checks for a face every 2 minutes; holds a systemd-inhibit idle lock while +# the user is detected so hypridle never fires during an active session. +# +# Camera selection: set PRESENCE_DETECT_CAMERA env var or write +# CAMERA= to ~/.config/presence-detect.conf +# +# Exit codes from python helper: 0=face, 1=no face, 2=camera error + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PYTHON_DETECT="$SCRIPT_DIR/python/presence_detect.py" +INHIBIT_PID_FILE="/tmp/presence-inhibit.pid" +PRESENCE_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/presence-detect.conf" +INTERVAL=120 # seconds between checks + +_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 +} + +_inhibit_running() { + [[ -f "$INHIBIT_PID_FILE" ]] && kill -0 "$(cat "$INHIBIT_PID_FILE")" 2>/dev/null +} + +_start_inhibit() { + _inhibit_running && return + systemd-inhibit --what=idle --who="presence-detect" \ + --why="User presence detected" --mode=block \ + sleep infinity & + echo $! > "$INHIBIT_PID_FILE" + logger -t presence-detect "Presence detected — idle inhibited" +} + +_stop_inhibit() { + _inhibit_running || return + kill "$(cat "$INHIBIT_PID_FILE")" 2>/dev/null + rm -f "$INHIBIT_PID_FILE" + logger -t presence-detect "No presence — idle inhibit released" +} + +_cleanup() { + _stop_inhibit + exit 0 +} +trap _cleanup SIGTERM SIGINT SIGHUP + +while true; do + CAMERA="$(_camera_id)" + python3 "$PYTHON_DETECT" "$CAMERA" 2>/dev/null + rc=$? + case $rc in + 0) _start_inhibit ;; + 1) _stop_inhibit ;; + # rc=2 = camera unavailable — leave current inhibit state unchanged + esac + sleep "$INTERVAL" +done diff --git a/desktopenvs/hyprlua/scripts/python/presence_detect.py b/desktopenvs/hyprlua/scripts/python/presence_detect.py new file mode 100755 index 0000000..9cb06d5 --- /dev/null +++ b/desktopenvs/hyprlua/scripts/python/presence_detect.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +""" +Webcam face presence detector using OpenCV haar cascades. +Exit codes: 0 = face detected, 1 = no face, 2 = camera error +Usage: presence_detect.py [camera_id] +""" +import sys +import cv2 + +FRAMES_TO_CHECK = 6 +FACES_NEEDED = 2 # require face in at least N frames to count as present +SCALE_FACTOR = 1.1 +MIN_NEIGHBORS = 4 + + +def detect(camera_id: int) -> int: + cap = cv2.VideoCapture(camera_id) + if not cap.isOpened(): + return 2 + + cascade = cv2.CascadeClassifier( + cv2.data.haarcascades + "haarcascade_frontalface_default.xml" + ) + detected = 0 + try: + for _ in range(FRAMES_TO_CHECK): + ok, frame = cap.read() + if not ok: + continue + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + faces = cascade.detectMultiScale(gray, SCALE_FACTOR, MIN_NEIGHBORS) + if len(faces) > 0: + detected += 1 + finally: + cap.release() + + return 0 if detected >= FACES_NEEDED else 1 + + +if __name__ == "__main__": + camera_id = int(sys.argv[1]) if len(sys.argv) > 1 else 0 + sys.exit(detect(camera_id)) diff --git a/desktopenvs/niri/niri/modules/autostart.kdl b/desktopenvs/niri/niri/modules/autostart.kdl index f89ff45..9e82ad4 100644 --- a/desktopenvs/niri/niri/modules/autostart.kdl +++ b/desktopenvs/niri/niri/modules/autostart.kdl @@ -9,4 +9,5 @@ spawn-at-startup "bash" "-c" "gammastep -O 4500" spawn-at-startup "nm-applet" spawn-at-startup "dunst" spawn-at-startup "bash" "-c" "swayidle -w timeout 300 'swaylock -f' timeout 600 'systemctl suspend' before-sleep 'swaylock -f'" +spawn-at-startup "bash" "-c" "~/.config/scripts/presence-detect.sh" spawn-at-startup "blueman-applet" diff --git a/desktopenvs/niri/niri/modules/binds.kdl b/desktopenvs/niri/niri/modules/binds.kdl index b497589..e871560 100644 --- a/desktopenvs/niri/niri/modules/binds.kdl +++ b/desktopenvs/niri/niri/modules/binds.kdl @@ -204,6 +204,7 @@ binds { Mod+Ctrl+C { spawn "dunstctl" "close-all"; } Mod+Ctrl+G { spawn "bash" "-c" "~/.config/scripts/onscreenkb.sh"; } Mod+Shift+C { spawn "bash" "-c" "~/.config/scripts/caffeine.sh"; } + Mod+Shift+B { spawn "bash" "-c" "kitty -e ~/.config/scripts/enroll-biometrics.sh"; } Mod+Shift+X { spawn "bash" "-c" "~/.config/scripts/niri-toggle-touchpad.sh"; } Mod+Ctrl+E { spawn "bash" "-c" "~/.config/scripts/screenrotationwcw.sh"; } diff --git a/desktopenvs/niri/scripts/caffeine.sh b/desktopenvs/niri/scripts/caffeine.sh index 6fb2c12..db3b021 100755 --- a/desktopenvs/niri/scripts/caffeine.sh +++ b/desktopenvs/niri/scripts/caffeine.sh @@ -1,14 +1,17 @@ #!/bin/bash +# Toggle idle inhibit via systemd-inhibit (swayidle respects the logind idle hint). +PID_FILE="/tmp/caffeine-inhibit.pid" -statecon=$(pidof swayidle | grep "[1234567890]") - -if [ "$statecon" != "" ]; then - notify-send -t 1000 "Caffeine mode ON — idle inhibited" - killall swayidle +if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then + kill "$(cat "$PID_FILE")" + rm -f "$PID_FILE" + notify-send -t 2000 "Caffeine" "Idle inhibit OFF" else - notify-send -t 1000 "Caffeine mode OFF — idle active" - swayidle -w \ - timeout 300 'swaylock -f' \ - timeout 600 'systemctl suspend' \ - before-sleep 'swaylock -f' & + systemd-inhibit --what=idle:sleep \ + --who="caffeine" \ + --why="Caffeine mode active" \ + --mode=block \ + sleep infinity & + echo $! > "$PID_FILE" + notify-send -t 2000 "Caffeine" "Idle inhibited" fi diff --git a/desktopenvs/niri/scripts/enroll-biometrics.sh b/desktopenvs/niri/scripts/enroll-biometrics.sh new file mode 120000 index 0000000..0c6ed96 --- /dev/null +++ b/desktopenvs/niri/scripts/enroll-biometrics.sh @@ -0,0 +1 @@ +../../hyprlua/scripts/enroll-biometrics.sh \ No newline at end of file diff --git a/desktopenvs/niri/scripts/presence-detect.sh b/desktopenvs/niri/scripts/presence-detect.sh new file mode 120000 index 0000000..472cbb6 --- /dev/null +++ b/desktopenvs/niri/scripts/presence-detect.sh @@ -0,0 +1 @@ +../../hyprlua/scripts/presence-detect.sh \ No newline at end of file diff --git a/desktopenvs/niri/scripts/python/python b/desktopenvs/niri/scripts/python similarity index 100% rename from desktopenvs/niri/scripts/python/python rename to desktopenvs/niri/scripts/python diff --git a/setup/modules/Desktop-Environments/hyprlua.sh b/setup/modules/Desktop-Environments/hyprlua.sh index b68e3a5..8deb82b 100755 --- a/setup/modules/Desktop-Environments/hyprlua.sh +++ b/setup/modules/Desktop-Environments/hyprlua.sh @@ -26,7 +26,8 @@ sudo pacman -Syu --noconfirm --needed \ cool-retro-term qalculate-gtk iwd dbus \ thunar tumbler thunar-archive-plugin thunar-shares-plugin thunar-volman \ hyprpicker pcmanfm-qt udisks2 ly kew \ - hyprpolkitagent pavucontrol playerctl wf-recorder sound-theme-freedesktop + hyprpolkitagent pavucontrol playerctl wf-recorder sound-theme-freedesktop \ + python-opencv v4l-utils # 3. Enable essential services log "Enabling essential services..." diff --git a/setup/modules/Desktop-Environments/niri.sh b/setup/modules/Desktop-Environments/niri.sh index f7a9d46..b3c2278 100755 --- a/setup/modules/Desktop-Environments/niri.sh +++ b/setup/modules/Desktop-Environments/niri.sh @@ -27,7 +27,7 @@ sudo pacman -Syu --noconfirm --needed \ thunar tumbler thunar-archive-plugin thunar-shares-plugin thunar-volman \ pcmanfm-qt udisks2 ly kew \ pavucontrol playerctl wf-recorder sound-theme-freedesktop \ - xinput jq + xinput jq python-opencv v4l-utils # 3. Enable essential services log "Enabling essential services..."