# shellcheck shell=bash # # The few things that need root, and how the menu gets them done. # # The menu runs as the user. When it needs root it runs this same program # again through sudo (or run0, or doas) with --root and one verb, the way # cachy-auto-update does, so the password is typed into the terminal the user # is already looking at. The verbs are all there is: there is no way to hand # the root side a command of one's own. # # --root set one line of /etc/face-unlock/config # --root pam-enable sudo, polkit-1, a lock screen, or lockscreens # --root pam-disable for all lock screens installed; see pam.sh # --root socket-enable start the daemon's socket, and at boot # --root socket-disable # --root migrate move over from plasma-face-unlock, see migrate.sh # --root polkit-agent-install install a polkit agent, see fu_polkit_agent_install FU_SELF="${FU_SELF:-$(readlink -f "${BASH_SOURCE[1]:-$0}")}" # What each system setting may be set to. Anything else is refused on the root # side, whatever the menu sent. declare -A FU_SYS_VALID=( [Camera]='^(auto|/dev/video[0-9]+)$' [Liveness]='^(off|light|heavy)$' [Strictness]='^(relaxed|normal|strict)$' [Attention]='^(yes|no)$' [ScanSeconds]='^([2-9]|1[0-5])$' [Adapt]='^(yes|no)$' [SkipLidClosed]='^(yes|no)$' [MaxFailures]='^([1-9]|1[0-9]|20)$' [LockoutMinutes]='^[1-9][0-9]{0,3}$' ) # fu_root [args...] fu_root() { local candidate if [[ $EUID -eq 0 ]]; then fu_root_verb "$@" return fi for candidate in sudo run0 doas; do if fu_have "$candidate"; then "$candidate" "$FU_SELF" --root "$@" return fi done fu_bad "$(fu_msg "This needs root, and neither sudo, run0 nor doas is installed.")" return 1 } fu_root_verb() { local verb="${1:-}" [[ $# -gt 0 ]] && shift if [[ $EUID -ne 0 ]]; then fu_bad "$(fu_msg "This command has to run as root.")" return 2 fi case "$verb" in set) local key="${1:-}" value="${2:-}" if [[ -z ${FU_SYS_VALID[$key]+set} || ! $value =~ ${FU_SYS_VALID[$key]} ]]; then fu_bad "$(fu_msg "Not a valid setting: %s=%s" "$key" "$value")" return 1 fi fu_kv_set "$FU_SYSCONFIG" "$key" "$value" "$FU_PRETTY (system settings)" || return 1 chmod 0644 "$FU_SYSCONFIG" ;; pam-enable|pam-disable) local service="${1:-}" known=0 s rc=0 local -a services=("$service") for s in "${FU_PAM_SERVICES[@]}" "${FU_PAM_LOCKERS[@]}"; do [[ $s == "$service" ]] && known=1 done if [[ $service == lockscreens ]]; then known=1 mapfile -t services < <(fu_pam_lockers) fi (( known )) || { fu_bad "$(fu_msg "Not a service this can be used for: %s" "$service")"; return 1; } for s in "${services[@]}"; do if [[ $verb == pam-enable ]]; then fu_pam_enable "$s" || rc=1 else fu_pam_disable "$s" || rc=1 fi done return $rc ;; socket-enable) systemctl enable --now "$FU_UNIT_SOCKET" > /dev/null 2>&1 ;; socket-disable) systemctl disable --now "$FU_UNIT_SOCKET" > /dev/null 2>&1 ;; migrate) fu_migrate_system ;; polkit-agent-install) fu_polkit_agent_install ;; *) fu_bad "$(fu_msg "Unknown command: %s" "$verb")" return 1 ;; esac } # --------------------------------------------------------------------------- # The two services # --------------------------------------------------------------------------- fu_socket_enabled() { systemctl is-enabled --quiet "$FU_UNIT_SOCKET" 2>/dev/null } fu_agent_available() { fu_have systemctl && [[ -n ${XDG_RUNTIME_DIR:-} ]] } fu_agent_enabled() { systemctl --user is-enabled --quiet "$FU_UNIT_AGENT" 2>/dev/null } fu_agent_running() { systemctl --user is-active --quiet "$FU_UNIT_AGENT" 2>/dev/null } fu_agent_enable() { systemctl --user daemon-reload > /dev/null 2>&1 || true systemctl --user enable --now "$FU_UNIT_AGENT" > /dev/null 2>&1 } # fu_agent_autostarts # Whether the agent's service starts with the session. It is wanted by # graphical-session.target, which Hyprland only reaches when it runs under # uwsm. Plasma, GNOME and niri-session always do. fu_agent_autostarts() { [[ $FU_DESKTOP != hyprland ]] || systemctl --user is-active --quiet graphical-session.target } # fu_agent_hint [unit] # What to add to Hyprland's config so it starts the lock screen agent, or the # polkit agent's unit, in the Lua it has since 0.56 or in the older format. fu_agent_hint() { local unit="${1:-$FU_UNIT_AGENT}" # shellcheck disable=SC2088 # shown to the user, not a path to open local conf="~/.config/hypr/hyprland.conf" # shellcheck disable=SC2088 [[ -f $FU_XDG_CONFIG/hypr/hyprland.lua ]] && conf="~/.config/hypr/hyprland.lua" if [[ $unit == "$FU_UNIT_AGENT" ]]; then fu_note "$(fu_msg "Hyprland does not start the lock screen agent by itself. Add this to %s:" "$conf")" else fu_note "$(fu_msg "Hyprland does not start the polkit agent by itself. Add this to %s:" "$conf")" fi if [[ $conf == *.lua ]]; then fu_code "hl.on(\"hyprland.start\", function ()" fu_code " hl.exec_cmd(\"systemctl --user start $unit\")" fu_code "end)" else fu_code "exec-once = systemctl --user start $unit" fi } # --------------------------------------------------------------------------- # The polkit agent # --------------------------------------------------------------------------- # polkit asks for the password in a window of the session's polkit agent: to # add a face, and in the admin prompts. Plasma and GNOME bring one, Hyprland # and Niri do not. Without one no window opens, and polkit just says no. FU_POLKIT_AGENT_MISSING='' # fu_polkit_agent_missing # Whether no polkit agent runs in this session. polkit has no call to ask # that, so this tries to register an agent of its own: polkit refuses it when # there is one. When it is let in, busctl ends at once, and polkit drops it # again. Any other answer counts as not missing, so nobody is warned wrongly. # Asked once per run. fu_polkit_agent_missing() { local session if [[ -z $FU_POLKIT_AGENT_MISSING ]]; then FU_POLKIT_AGENT_MISSING=no if [[ $FU_DESKTOP != plasma && $FU_DESKTOP != gnome && -n ${WAYLAND_DISPLAY:-} ]] && fu_have busctl; then session="${XDG_SESSION_ID:-$(loginctl show-user "$UID" -p Display --value 2> /dev/null)}" if [[ -n $session ]] && busctl call --system org.freedesktop.PolicyKit1 \ /org/freedesktop/PolicyKit1/Authority org.freedesktop.PolicyKit1.Authority \ RegisterAuthenticationAgent '(sa{sv})ss' unix-session 1 session-id s "$session" \ C /io/github/loonixtools/FaceUnlock/Probe > /dev/null 2>&1; then FU_POLKIT_AGENT_MISSING=yes fi fi fi [[ $FU_POLKIT_AGENT_MISSING == yes ]] } # fu_polkit_agent_install # Root side. hyprpolkitagent where the distribution has it, else KDE's agent, # which runs anywhere and which niri suggests. The package manager shows what # it installs and asks. fu_polkit_agent_install() { if fu_have pacman; then pacman -Si hyprpolkitagent > /dev/null 2>&1 && { pacman -S --needed hyprpolkitagent; return; } pacman -S --needed polkit-kde-agent elif fu_have dnf; then dnf -q info hyprpolkitagent > /dev/null 2>&1 && { dnf install hyprpolkitagent; return; } dnf install polkit-kde elif fu_have apt-get; then apt-cache show hyprpolkitagent > /dev/null 2>&1 && { apt-get install hyprpolkitagent; return; } apt-get install polkit-kde-agent-1 else fu_bad "$(fu_msg "No package manager found to install a polkit agent with.")" return 1 fi } # _fu_polkit_agent_unit # The user service of the polkit agent that is installed, if any. _fu_polkit_agent_unit() { local unit for unit in hyprpolkitagent.service plasma-polkit-agent.service; do if systemctl --user list-unit-files --quiet "$unit" > /dev/null 2>&1; then printf '%s\n' "$unit" return 0 fi done return 1 } # fu_polkit_agent_fix # Installs a polkit agent if there is none, and starts it now and with the # session. fu_polkit_agent_fix() { local unit if ! unit="$(_fu_polkit_agent_unit)"; then fu_say " $(fu_msg "Installing a polkit agent. That needs your password.")" fu_root polkit-agent-install || { fu_bad "$(fu_msg "Could not install a polkit agent.")"; return 1; } systemctl --user daemon-reload > /dev/null 2>&1 || true unit="$(_fu_polkit_agent_unit)" || { fu_bad "$(fu_msg "Could not install a polkit agent.")"; return 1; } fi if [[ $unit == plasma-polkit-agent.service ]]; then # KDE's has no [Install] section, and outside Plasma nothing makes it # wait for the desktop: it would start before there is one. local dropin="$FU_XDG_CONFIG/systemd/user/$unit.d" mkdir -p "$dropin" && printf '# Written by %s.\n[Unit]\nAfter=graphical-session.target\n' "$FU_NAME" > "$dropin/$FU_NAME.conf" systemctl --user daemon-reload > /dev/null 2>&1 || true systemctl --user add-wants graphical-session.target "$unit" > /dev/null 2>&1 systemctl --user start "$unit" > /dev/null 2>&1 else systemctl --user enable --now "$unit" > /dev/null 2>&1 fi if ! systemctl --user is-active --quiet "$unit"; then fu_bad "$(fu_msg "The polkit agent did not start. See: systemctl --user status %s" "$unit")" return 1 fi FU_POLKIT_AGENT_MISSING=no fu_ok "$(fu_msg "The polkit agent is running. Password windows open now.")" fu_agent_autostarts || fu_agent_hint "$unit" } # --------------------------------------------------------------------------- # The bubble's GNOME Shell extension # --------------------------------------------------------------------------- # GNOME lets no program draw above its windows; its extensions may. The # package brings one, and turning face unlock on switches it on. FU_GNOME_EXTENSION="face-unlock@loonixtools.github.io" # fu_gnome_extension_enable # gnome-extensions only knows the extensions GNOME found when the session # started. One that came with the package later goes straight into the # setting, and GNOME loads it at the next login: returns 2 then. fu_gnome_extension_enable() { local list new fu_have gsettings || return 1 if gnome-extensions info "$FU_GNOME_EXTENSION" > /dev/null 2>&1; then gnome-extensions enable "$FU_GNOME_EXTENSION" > /dev/null 2>&1 && return 0 fi list="$(gsettings get org.gnome.shell enabled-extensions 2>/dev/null)" || return 1 case "$list" in *"'$FU_GNOME_EXTENSION'"*) return 2 ;; '@as []'|'[]') new="['$FU_GNOME_EXTENSION']" ;; *) new="${list%]}, '$FU_GNOME_EXTENSION']" ;; esac gsettings set org.gnome.shell enabled-extensions "$new" || return 1 return 2 } fu_gnome_extension_disable() { local list fu_have gsettings || return 0 gnome-extensions disable "$FU_GNOME_EXTENSION" > /dev/null 2>&1 && return 0 list="$(gsettings get org.gnome.shell enabled-extensions 2>/dev/null)" || return 0 list="${list//, \'$FU_GNOME_EXTENSION\'/}" list="${list//\'$FU_GNOME_EXTENSION\', /}" list="${list//\'$FU_GNOME_EXTENSION\'/}" [[ $list == '[]' ]] && list='@as []' gsettings set org.gnome.shell enabled-extensions "$list" > /dev/null 2>&1 || true } # --------------------------------------------------------------------------- # The lock screen where it is a program of its own (Hyprland, Niri) # --------------------------------------------------------------------------- # Either face-unlock's (`face-unlock lock`, with the bubble), or the user's # own program. hyprlock then shows what the bubble would as a line of text: a # label in its config reads a file the agent writes (see # src/agent/locktext.h). Asked once in a window, and under Settings. FU_HYPRLOCK_SNIPPET="$FU_XDG_CONFIG/$FU_NAME/hyprlock.conf" FU_HYPRLOCK_MARK="# $FU_NAME: what face unlock is doing, at the top" fu_own_lock_here() { [[ $FU_DESKTOP != plasma && $FU_DESKTOP != gnome ]] } # fu_hyprlock_text_on # 0 done, 1 could not write, 2 no hyprlock, 3 hyprlock has no config here. fu_hyprlock_text_on() { local conf="$FU_XDG_CONFIG/hypr/hyprlock.conf" fu_have hyprlock || return 2 [[ -f $conf ]] || return 3 mkdir -p "${FU_HYPRLOCK_SNIPPET%/*}" || return 1 cat > "$FU_HYPRLOCK_SNIPPET" <<- EOF || return 1 # Written by $FU_NAME: what face unlock is doing, as a line at the top of # hyprlock. Picking $FU_NAME's lock screen in its menu takes it out again. label { monitor = text = cmd[update:0:1] cat "\$XDG_RUNTIME_DIR/$FU_NAME/lock-text" 2>/dev/null color = rgba(255, 255, 255, 0.95) font_size = 20 shadow_passes = 2 shadow_size = 3 halign = center valign = top position = 0, -48 zindex = 10 } EOF grep -qxF "$FU_HYPRLOCK_MARK" "$conf" && return 0 # With ~ where it can, like a hand-written config. hyprlock expands it. local snippet="$FU_HYPRLOCK_SNIPPET" # shellcheck disable=SC2088 # for hyprlock, not for the shell [[ $snippet == "$HOME"/* ]] && snippet="~/${snippet#"$HOME"/}" printf '\n%s\nsource = %s\n' "$FU_HYPRLOCK_MARK" "$snippet" >> "$conf" || return 1 } # fu_hyprlock_text_off # The mark, the source line under it, and the empty lines before it. Written # back in place, so a hyprlock.conf that is a link to a dotfile stays one. fu_hyprlock_text_off() { local conf="$FU_XDG_CONFIG/hypr/hyprlock.conf" content rm -f "$FU_HYPRLOCK_SNIPPET" [[ -f $conf ]] && grep -qxF "$FU_HYPRLOCK_MARK" "$conf" || return 0 content="$(awk -v mark="$FU_HYPRLOCK_MARK" ' $0 == mark { held = ""; skip = 1; next } skip && /^[[:space:]]*source[[:space:]]*=/ { skip = 0; next } { skip = 0 } /^[[:space:]]*$/ { held = held $0 "\n"; next } { printf "%s", held; held = ""; print } ' "$conf")" || return 1 printf '%s\n' "$content" > "$conf" } # fu_own_lock_hint # Where face-unlock's lock screen has to be started from: the user's key and # idle lock, which are theirs to change. fu_own_lock_hint() { # shellcheck disable=SC2088 # shown to the user, not a path to open case "$FU_DESKTOP" in hyprland) if [[ -f $FU_XDG_CONFIG/hypr/hyprland.lua ]]; then fu_note "$(fu_msg "To lock with it, add this to %s, and set %s in %s:" "~/.config/hypr/hyprland.lua" "lock_cmd = $FU_NAME lock" "hypridle.conf")" fu_code "hl.bind(\"SUPER + L\", hl.dsp.exec_cmd(\"$FU_NAME lock\"))" else fu_note "$(fu_msg "To lock with it, add this to %s, and set %s in %s:" "~/.config/hypr/hyprland.conf" "lock_cmd = $FU_NAME lock" "hypridle.conf")" fu_code "bind = SUPER, L, exec, $FU_NAME lock" fi ;; niri) fu_note "$(fu_msg "To lock with it, add this to the binds in %s, and use %s in swayidle:" "~/.config/niri/config.kdl" "$FU_NAME lock")" fu_code "Mod+Alt+L { spawn \"$FU_NAME\" \"lock\"; }" ;; *) fu_note "$(fu_msg "To lock with it, run %s where your lock screen is started now." "$FU_NAME lock")" ;; esac } # fu_lock_style_set fu_lock_style_set() { # shellcheck disable=SC2088 # shown to the user, not a path to open local conf="~/.config/hypr/hyprlock.conf" case "$1" in own) fu_config_set LockScreenStyle own || { fu_bad "$(fu_msg "Could not save the setting.")"; return 1; } fu_hyprlock_text_off fu_ok "$(fu_msg "You lock with face-unlock's lock screen now.")" fu_own_lock_hint ;; yours) fu_config_set LockScreenStyle yours || { fu_bad "$(fu_msg "Could not save the setting.")"; return 1; } fu_hyprlock_text_on case $? in 0) fu_ok "$(fu_msg "hyprlock shows what face unlock is doing at the top, from the next time it locks.")" ;; 1) fu_bad "$(fu_msg "Could not add the line to %s." "$conf")" ;; 2) fu_ok "$(fu_msg "Your lock screen stays as it is. Press Enter on the empty password field to scan.")" ;; 3) fu_note "$(fu_msg "hyprlock has no %s, so it cannot show the text." "$conf")" ;; esac ;; *) return 1 ;; esac } # fu_lock_choose # Asks in a window which lock screen to use, and carries it out. 1 when the # window was closed or cannot open. fu_lock_choose() { local choice [[ -n ${WAYLAND_DISPLAY:-} ]] || return 1 choice="$("$FU_AGENT" --choose-lock-screen 2> /dev/null)" || return 1 fu_lock_style_set "$choice" } fu_agent_disable() { systemctl --user disable --now "$FU_UNIT_AGENT" > /dev/null 2>&1 || true }