# shellcheck shell=bash # # Asking the daemon. # # Through face-unlock-ctl, which prints every message as a line of tab # separated key=value pairs (see src/ctl/main.cpp). What comes back is parsed # into plain variables and arrays here, so the rest of the shell code never # sees the wire format. # _fu_fields # Splits one line of ctl output into the associative array FU_F. declare -A FU_F=() _fu_fields() { local line="$1" field local -a parts FU_F=() IFS=$'\t' read -r -a parts <<< "$line" for field in "${parts[@]}"; do FU_F["${field%%=*}"]="${field#*=}" done } fu_ctl() { "$FU_CTL" "$@" } # fu_status_load # FU_ST_REACHABLE is yes when the daemon answered at all. Everything else # is only filled in then. fu_status_load() { local out FU_ST_REACHABLE=no FU_ST_FACES=0 FU_ST_LOCKOUT=0 FU_ST_LAST=0 FU_ST_PURPOSE='' FU_ST_CAMERA='' FU_ST_CAMERA_NAME='' FU_ST_CAMERA_PRESENT=no FU_ST_MODELS=no out="$(fu_ctl status 2>/dev/null | tail -n1)" _fu_fields "$out" [[ ${FU_F[ok]:-} == true ]] || return 1 FU_ST_REACHABLE=yes FU_ST_FACES="${FU_F[faces]:-0}" FU_ST_LOCKOUT="${FU_F[lockout]:-0}" FU_ST_LAST="${FU_F[lastUnlock]:-0}" FU_ST_PURPOSE="${FU_F[lastPurpose]:-}" FU_ST_CAMERA="${FU_F[cameraPath]:-}" FU_ST_CAMERA_NAME="${FU_F[cameraName]:-}" FU_ST_CAMERA_PRESENT="${FU_F[cameraPresent]:-false}" FU_ST_MODELS="${FU_F[models]:-false}" return 0 } # fu_faces_load # The caller's faces, into parallel arrays. fu_faces_load() { local line FU_FACE_IDS=() FU_FACE_NAMES=() FU_FACE_ON=() FU_FACE_SAMPLES=() FU_FACE_LEARNED=() FU_FACE_CREATED=() while IFS= read -r line; do _fu_fields "$line" [[ ${FU_F[event]:-} == item ]] || continue FU_FACE_IDS+=("${FU_F[id]}") FU_FACE_NAMES+=("${FU_F[name]}") FU_FACE_ON+=("${FU_F[enabled]}") FU_FACE_SAMPLES+=("${FU_F[samples]:-0}") FU_FACE_LEARNED+=("${FU_F[adaptive]:-0}") FU_FACE_CREATED+=("${FU_F[created]:-0}") done < <(fu_ctl list 2>/dev/null) } # fu_cameras_load fu_cameras_load() { local line FU_CAM_PATHS=() FU_CAM_NAMES=() FU_CAM_IR=() FU_CAM_AUTO='' while IFS= read -r line; do _fu_fields "$line" case "${FU_F[event]:-}" in item) FU_CAM_PATHS+=("${FU_F[path]}") FU_CAM_NAMES+=("${FU_F[name]}") FU_CAM_IR+=("${FU_F[infrared]}") ;; result) FU_CAM_AUTO="${FU_F[auto]:-}" ;; esac done < <(fu_ctl cameras 2>/dev/null) } # fu_reason_text # What a daemon answer means, for people. fu_reason_text() { case "$1" in mismatch) fu_msg "The face did not match." ;; spoof) fu_msg "That looked like a photo or a screen." ;; liveness) fu_msg "The face matched, but did not blink or move." ;; attention) fu_msg "The face was not looking at the screen." ;; quality) fu_msg "The picture was too dark, too blurry or too far away." ;; no-face) fu_msg "No face in view." ;; lockout) fu_msg "Face unlock is paused after too many tries. Unlock once with your password." ;; not-enrolled) fu_msg "No face is set up yet." ;; camera) fu_msg "The camera could not be used." ;; models) fu_msg "The recognition models are missing. Reinstall the package." ;; lid-closed) fu_msg "The lid is closed." ;; busy) fu_msg "The camera is busy with another scan." ;; unreachable) fu_msg "The face unlock service is not running." ;; denied) fu_msg "Not allowed." # Hyprland, Niri and the like bring no polkit agent, so no password window shows. if [[ $FU_DESKTOP != plasma && $FU_DESKTOP != gnome ]]; then printf ' ' fu_msg "No password window? Start a polkit agent, for example hyprpolkitagent." fi ;; *) fu_msg "Something went wrong (%s)." "$1" ;; esac printf '\n' } # fu_test # One scan, with everything the checks see on one line that keeps updating. # The bubble shows it too, if the agent is running. fu_test() { local line started=0 shown='' score='-' matched=0 fu_say " $(fu_msg "Look at the camera. Blink once, or turn your head a little.")" printf '\n' while IFS= read -r line; do _fu_fields "$line" case "${FU_F[event]:-}" in started) started=1 ;; face) printf '\r\033[K %s\n' "$(fu_msg "Face found.")" ;; hint) case "${FU_F[hint]}" in blink) printf '\r\033[K %s\n' "$(fu_msg "Recognised. Now blink once, or turn your head a little.")" ;; look) printf '\r\033[K %s\n' "$(fu_msg "Look at the screen.")" ;; closer) printf '\r\033[K %s\n' "$(fu_msg "Move closer to the camera.")" ;; light) printf '\r\033[K %s\n' "$(fu_msg "It is too dark to see your face.")" ;; esac ;; frame) [[ -n ${FU_F[score]:-} ]] && score="${FU_F[score]}" matched="${FU_F[matched]:-0}" # match, turn of the head, eyes, and how close each photo check is # to firing, as numbers for anybody tuning it. printf -v shown ' %s %-6s %s %5s° %s %-5s %s %-4s %s %-4s %s %-4s %s %-4s' \ "$(fu_msg "match")" "$score" "$(fu_msg "turn")" "${FU_F[yaw]:-0}" \ "$(fu_msg "eyes")" "${FU_F[eyes]:-0}" \ "$(fu_msg "depth")" "${FU_F[depth]:-0}" "$(fu_msg "blink")" "${FU_F[blink]:-0}" \ "$(fu_msg "glare")" "${FU_F[glare]:-0}" "$(fu_msg "edge")" "${FU_F[device]:-0}" [[ -n $FU_INTERACTIVE ]] && printf '\r\033[K%s%s%s' "$FU_C_DIM" "$shown" "$FU_C_RESET" ;; result) printf '\r\033[K' if [[ ${FU_F[ok]} == true ]]; then local how='' case "${FU_F[liveness]:-}" in blink) how="$(fu_msg "blink")" ;; depth) how="$(fu_msg "head turn")" ;; esac fu_ok "$(fu_msg "Recognised as %s (match %s) in %s ms." "${FU_F[name]}" "${FU_F[score]}" "${FU_F[ms]}")" [[ -n $how ]] && fu_say " $(fu_msg "Sign of life: %s" "$how")" else fu_bad "$(fu_reason_text "${FU_F[reason]}")" [[ -n ${FU_F[message]:-} ]] && fu_say " ${FU_F[message]}" if [[ -n ${FU_F[lockout]:-} ]]; then fu_note "$(fu_msg "That was too many tries. Face unlock is paused until you unlock with your password.")" fi fi (( started )) || true return 0 ;; esac done < <(fu_ctl test 2>/dev/null) printf '\n' fu_bad "$(fu_reason_text unreachable)" return 1 }