refactor!: rename to face-unlock

This commit is contained in:
Felitendo committed 2026-09-25 09:27:15 +02:00
1 parent 44449f103b
commit 6e6a6e6d03
66 files changed
+1709 -1709

No files matched your search

+1 -1
View File
@@ -48,7 +48,7 @@ AgentSocket::AgentSocket(QObject *parent)
QString AgentSocket::path()
{
return QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + QStringLiteral("/plasma-face-unlock/agent.socket");
return QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + QStringLiteral("/face-unlock/agent.socket");
}
bool AgentSocket::listen()
+1 -1
View File
@@ -2,7 +2,7 @@
//
// Where the daemon tells this session about scans it did not ask for: sudo in
// a terminal, an admin prompt, a test from the menu. The daemon connects to
// $XDG_RUNTIME_DIR/plasma-face-unlock/agent.socket when such a scan starts,
// $XDG_RUNTIME_DIR/face-unlock/agent.socket when such a scan starts,
// so neither side has to keep a connection open (and the daemon can exit when
// it is idle).
//
+2 -2
View File
@@ -74,13 +74,13 @@ void BubbleWindow::create()
// faint box with sharp corners around the bubble. An on-screen
// display only fades, which a clear window does not show.
layer->setScope(QStringLiteral("on-screen-display"));
#ifdef PFU_LAYERSHELL_HAS_SCREEN
#ifdef FU_LAYERSHELL_HAS_SCREEN
layer->setScreen(QGuiApplication::primaryScreen());
#endif
}
m_view->setInitialProperties({{QStringLiteral("bubble"), QVariant::fromValue(m_controller)}});
m_view->loadFromModule(QStringLiteral("PlasmaFaceUnlock"), QStringLiteral("Bubble"));
m_view->loadFromModule(QStringLiteral("FaceUnlock"), QStringLiteral("Bubble"));
if (m_view->status() == QQuickView::Error) {
for (const QQmlError &e : m_view->errors()) {
qWarning("%s", qPrintable(e.toString()));
+2 -2
View File
@@ -38,8 +38,8 @@ DaemonRequest::~DaemonRequest()
QString DaemonRequest::socketPath()
{
const QByteArray env = qgetenv("PFU_SOCKET");
return env.isEmpty() ? QStringLiteral(PFU_SOCKET) : QString::fromLocal8Bit(env);
const QByteArray env = qgetenv("FU_SOCKET");
return env.isEmpty() ? QStringLiteral(FU_SOCKET) : QString::fromLocal8Bit(env);
}
void DaemonRequest::send(const QJsonObject &message)
+6 -6
View File
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// plasma-face-unlock-agent: the part in the user's session.
// face-unlock-agent: the part in the user's session.
//
// (no arguments) stay in the background: unlock the lock screen by face,
// and show the bubble for every scan
@@ -39,7 +39,7 @@ int runEnroll(QGuiApplication &app, const QString &name)
QQmlApplicationEngine engine;
KLocalization::setupLocalizedContext(&engine);
engine.setInitialProperties({{QStringLiteral("controller"), QVariant::fromValue(&controller)}});
engine.loadFromModule(QStringLiteral("PlasmaFaceUnlock"), QStringLiteral("Enroll"));
engine.loadFromModule(QStringLiteral("FaceUnlock"), QStringLiteral("Enroll"));
if (engine.rootObjects().isEmpty()) {
return 2;
}
@@ -76,10 +76,10 @@ void scheduleDemo(BubbleController *bubble)
int main(int argc, char **argv)
{
QGuiApplication app(argc, argv);
app.setApplicationName(QStringLiteral("plasma-face-unlock-agent"));
app.setApplicationVersion(QStringLiteral(PFU_VERSION));
app.setDesktopFileName(QStringLiteral("io.github.loonixtools.plasma-face-unlock-agent"));
KLocalizedString::setApplicationDomain(PFU_NAME);
app.setApplicationName(QStringLiteral("face-unlock-agent"));
app.setApplicationVersion(QStringLiteral(FU_VERSION));
app.setDesktopFileName(QStringLiteral("io.github.loonixtools.face-unlock-agent"));
KLocalizedString::setApplicationDomain(FU_NAME);
QCommandLineParser parser;
parser.setApplicationDescription(i18n("Face unlock for KDE Plasma"));
+1 -1
View File
@@ -6,7 +6,7 @@
import QtQuick
import QtQuick.Window
import PlasmaFaceUnlock
import FaceUnlock
Window {
id: window
+1 -1
View File
@@ -23,7 +23,7 @@ UserConfig::UserConfig(QObject *parent)
QString UserConfig::path()
{
return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/plasma-face-unlock/config");
return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/face-unlock/config");
}
void UserConfig::load()
+1 -1
View File
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// ~/.config/plasma-face-unlock/config: what this user wants from the agent.
// ~/.config/face-unlock/config: what this user wants from the agent.
// Written by the menu, read here, and read again whenever it changes.
#pragma once
+1 -1
View File
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// The system settings, /etc/plasma-face-unlock/config.
// The system settings, /etc/face-unlock/config.
//
// These are the ones that decide how hard it is to get in: which camera, how
// strict the match is, whether a photo is checked for. They belong to root
+1 -1
View File
@@ -2,7 +2,7 @@
//
// The face data.
//
// One file per user under /var/lib/plasma-face-unlock/users, named after the
// One file per user under /var/lib/face-unlock/users, named after the
// numeric user id so a rename cannot hand one person's faces to another. Only
// root can read or write the directory. There are no pictures in it: every
// sample is the 128 numbers the recognizer made of one frame, and the frame
+4 -4
View File
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// plasma-face-unlock-ctl: the shell code's way to the daemon.
// face-unlock-ctl: the shell code's way to the daemon.
//
// bash has no Unix sockets, so this sends one request and prints every
// message that comes back as one line of tab separated key=value pairs:
@@ -68,7 +68,7 @@ void printObject(const QJsonObject &o, const QString &event)
int usage()
{
std::fprintf(stderr,
"usage: plasma-face-unlock-ctl [--socket PATH] COMMAND\n"
"usage: face-unlock-ctl [--socket PATH] COMMAND\n"
" hello | status | cameras | list | watch | unlocked\n"
" verify [USER] [PURPOSE] | test\n"
" remove ID | rename ID NAME | enable ID | disable ID | clear\n");
@@ -81,12 +81,12 @@ int main(int argc, char **argv)
QCoreApplication app(argc, argv);
QStringList args = app.arguments().mid(1);
QString socketPath = QStringLiteral(PFU_SOCKET);
QString socketPath = QStringLiteral(FU_SOCKET);
if (args.size() >= 2 && args.first() == u"--socket") {
socketPath = args.at(1);
args = args.mid(2);
}
if (const QByteArray env = qgetenv("PFU_SOCKET"); !env.isEmpty()) {
if (const QByteArray env = qgetenv("FU_SOCKET"); !env.isEmpty()) {
socketPath = QString::fromLocal8Bit(env);
}
if (args.isEmpty()) {
+1 -1
View File
@@ -25,7 +25,7 @@ AgentLink::AgentLink(uid_t uid, QObject *parent)
: QObject(parent)
, m_uid(uid)
{
const QString dir = QStringLiteral("/run/user/%1/plasma-face-unlock").arg(uid);
const QString dir = QStringLiteral("/run/user/%1/face-unlock").arg(uid);
const QString path = dir + QStringLiteral("/agent.socket");
if (!ownedBy(dir, uid, false) || !ownedBy(path, uid, true)) {
// No agent in that session, or not one of this user's making.
+1 -1
View File
@@ -3,7 +3,7 @@
// Telling a user's session about a scan it did not start itself (sudo, an
// admin prompt, a test from the menu), so the bubble can show it.
//
// The agent listens on /run/user/UID/plasma-face-unlock/agent.socket. That is
// The agent listens on /run/user/UID/face-unlock/agent.socket. That is
// a place the user controls, so nothing is taken for granted: the socket has
// to belong to the user, and so does the process that answers on it, checked
// by the kernel before a single byte is written. What is written is only
+7 -7
View File
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// plasma-face-unlockd: the part that runs as root.
// face-unlockd: the part that runs as root.
//
// It owns the camera and the face data, and it is the only thing that does.
// Everything else (the lock screen agent, sudo, the setup window, the menu)
@@ -66,8 +66,8 @@ int main(int argc, char **argv)
umask(077);
QCoreApplication app(argc, argv);
app.setApplicationName(QStringLiteral("plasma-face-unlockd"));
app.setApplicationVersion(QStringLiteral(PFU_VERSION));
app.setApplicationName(QStringLiteral("face-unlockd"));
app.setApplicationVersion(QStringLiteral(FU_VERSION));
qSetMessagePattern(QStringLiteral("%{if-warning}warning: %{endif}%{if-critical}error: %{endif}%{message}"));
QCommandLineParser parser;
@@ -75,12 +75,12 @@ int main(int argc, char **argv)
parser.addHelpOption();
parser.addVersionOption();
const QCommandLineOption socketOpt(QStringLiteral("socket"), QStringLiteral("Listen here instead of the system socket."), QStringLiteral("path"),
QStringLiteral(PFU_SOCKET));
QStringLiteral(FU_SOCKET));
const QCommandLineOption stateOpt(QStringLiteral("state-dir"), QStringLiteral("Where face data is kept."), QStringLiteral("dir"),
QStringLiteral(PFU_STATEDIR));
const QCommandLineOption configOpt(QStringLiteral("config"), QStringLiteral("The settings file."), QStringLiteral("file"), QStringLiteral(PFU_CONFIG));
QStringLiteral(FU_STATEDIR));
const QCommandLineOption configOpt(QStringLiteral("config"), QStringLiteral("The settings file."), QStringLiteral("file"), QStringLiteral(FU_CONFIG));
const QCommandLineOption modelOpt(QStringLiteral("models"), QStringLiteral("Where the networks are."), QStringLiteral("dir"),
QStringLiteral(PFU_MODELDIR));
QStringLiteral(FU_MODELDIR));
const QCommandLineOption idleOpt(QStringLiteral("idle-exit"), QStringLiteral("Exit after this many idle seconds (0: never)."), QStringLiteral("seconds"));
parser.addOptions({socketOpt, stateOpt, configOpt, modelOpt, idleOpt});
parser.process(app);
+1 -1
View File
@@ -17,7 +17,7 @@
namespace Polkit
{
inline constexpr char ManageAction[] = "io.github.loonixtools.plasma-face-unlock.manage";
inline constexpr char ManageAction[] = "io.github.loonixtools.face-unlock.manage";
// Calls done(true) when the process may go ahead. Interactive: this can take
// as long as it takes somebody to type a password.
+2 -2
View File
@@ -282,7 +282,7 @@ void Server::onRequest(Client *client, const QJsonObject &request)
client->role = cmd;
if (cmd == u"hello") {
reply(client, {{QStringLiteral("event"), QStringLiteral("result")}, {QStringLiteral("ok"), true}, {QStringLiteral("version"), QStringLiteral(PFU_VERSION)}});
reply(client, {{QStringLiteral("event"), QStringLiteral("result")}, {QStringLiteral("ok"), true}, {QStringLiteral("version"), QStringLiteral(FU_VERSION)}});
} else if (cmd == u"status") {
handleStatus(client);
} else if (cmd == u"cameras") {
@@ -344,7 +344,7 @@ void Server::handleStatus(Client *client)
reply(client,
{{QStringLiteral("event"), QStringLiteral("result")},
{QStringLiteral("ok"), true},
{QStringLiteral("version"), QStringLiteral(PFU_VERSION)},
{QStringLiteral("version"), QStringLiteral(FU_VERSION)},
{QStringLiteral("user"), System::nameOf(uid)},
{QStringLiteral("faces"), enabled},
{QStringLiteral("identities"), int(faces.size())},
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env bash
#
# face-unlock: face unlock for KDE Plasma
#
# Look at the screen and it unlocks, the way a phone does. The lock screen,
# sudo in a terminal and the admin password prompts can all take a face
# instead of a password, with a check that it is a face and not a photo of one.
#
# This is the front end: the menu and the commands. The camera, the face data
# and the decision are the daemon's (face-unlockd, running as root),
# the lock screen and the bubble at the top of the screen are the agent's
# (face-unlock-agent, in the session), and sudo and polkit reach the
# daemon through a PAM module. See the man page for how the pieces fit.
#
# Copyright (C) 2026 Felitendo
# SPDX-License-Identifier: GPL-3.0-or-later
set -uo pipefail
FU_SELF="$(readlink -f "${BASH_SOURCE[0]}")"
# Run as root (through sudo from the menu), only what was installed counts.
# An environment that points the libraries somewhere else is ignored then.
if [[ $EUID -eq 0 ]]; then
unset FU_LIBDIR FU_LIBEXECDIR FU_LOCALEDIR FU_PAMDIR FU_CTL FU_AGENT FU_PAM_MODULE FU_SYSCONFIG \
FU_PAM_ETC_DIR FU_PAM_VENDOR_DIRS
fi
FU_LIBDIR="${FU_LIBDIR:-@LIBDIR@}"
for _mod in common config daemon pam system menu; do
# shellcheck source=/dev/null
if ! source "$FU_LIBDIR/$_mod.sh"; then
printf 'face-unlock: cannot load %s/%s.sh\n' "$FU_LIBDIR" "$_mod" >&2
exit 14
fi
done
unset _mod
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
# The daemon is started by its socket. Enabling the socket is the one thing
# that needs root once per machine.
fu_ensure_service() {
fu_status_load && return 0
if ! fu_socket_enabled; then
fu_say " $(fu_msg "Face unlock's service has to be switched on once for this computer. That needs your password.")"
fu_root socket-enable || return 1
sleep 0.3
fi
fu_status_load && return 0
fu_bad "$(fu_reason_text unreachable)"
fu_note "$(fu_msg "Check it with: systemctl status %s" "$FU_UNIT_SOCKET")"
return 1
}
fu_do_setup() {
local name="${1:-}" rc
fu_ensure_service || return 1
if [[ -z ${WAYLAND_DISPLAY:-} && -z ${DISPLAY:-} ]]; then
fu_bad "$(fu_msg "Setting up a face needs the camera picture on screen. Run this inside your Plasma session.")"
return 1
fi
fu_say " $(fu_msg "The setup window is open. Follow it there.")"
if [[ -n $name ]]; then
"$FU_AGENT" --enroll --name "$name" > /dev/null 2>&1
else
"$FU_AGENT" --enroll > /dev/null 2>&1
fi
rc=$?
if (( rc == 0 )); then
fu_ok "$(fu_msg "The face is set up.")"
fu_config_load
if [[ $CFG_ENABLED != yes ]]; then
fu_note "$(fu_msg "Face unlock is still off. Turn it on with [1] or \`%s enable\`." "$FU_NAME")"
fi
return 0
fi
fu_note "$(fu_msg "No face was added.")"
return 1
}
fu_do_enable() {
fu_ensure_service || return 1
fu_faces_load
if (( ${#FU_FACE_IDS[@]} == 0 )); then
fu_say " $(fu_msg "First, set up your face.")"
fu_do_setup || return 1
fi
fu_config_set Enabled yes || { fu_bad "$(fu_msg "Could not save the setting.")"; return 1; }
fu_config_load
if fu_agent_available; then
fu_agent_enable || fu_bad "$(fu_msg "Could not start the lock screen agent.")"
else
fu_note "$(fu_msg "No systemd user session, so the lock screen agent was not started.")"
fi
# What was on the last time face unlock was on.
[[ $CFG_SUDO == yes ]] && ! fu_pam_enabled sudo && fu_root pam-enable sudo
[[ $CFG_POLKIT == yes ]] && ! fu_pam_enabled polkit-1 && fu_root pam-enable polkit-1
fu_ok "$(fu_msg "Face unlock is on. Lock the screen and look at it to try.")"
if [[ $CFG_SUDO != yes && $CFG_POLKIT != yes ]]; then
fu_note "$(fu_msg "It can do sudo and admin prompts too. See Settings.")"
fi
}
fu_do_disable() {
fu_config_set Enabled no || { fu_bad "$(fu_msg "Could not save the setting.")"; return 1; }
fu_agent_available && fu_agent_disable
local service
for service in "${FU_PAM_SERVICES[@]}"; do
if fu_pam_enabled "$service"; then
fu_root pam-disable "$service" || true
fi
done
fu_ok "$(fu_msg "Face unlock is off. Your faces are kept; delete them under Faces.")"
}
fu_do_status() {
fu_head " $FU_PRETTY"
fu_ui_status
printf '\n'
}
fu_do_faces() {
local i state
fu_status_load || { fu_bad "$(fu_reason_text unreachable)"; return 1; }
fu_faces_load
if (( ${#FU_FACE_IDS[@]} == 0 )); then
fu_note "$(fu_msg "No face is set up yet.")"
return 0
fi
for i in "${!FU_FACE_IDS[@]}"; do
if [[ ${FU_FACE_ON[i]} == true ]]; then state="$(fu_msg "on")"; else state="$(fu_msg "off")"; fi
printf ' %s %-24s %-4s %s\n' "${FU_FACE_IDS[i]}" "${FU_FACE_NAMES[i]}" "$state" \
"$(fu_msg "%s samples, %s learned" "${FU_FACE_SAMPLES[i]}" "${FU_FACE_LEARNED[i]}")"
done
}
fu_do_remove() {
local id="${1:-}" line
[[ -n $id ]] || { fu_bad "$(fu_msg "Which face? See \`%s faces\` for the ids." "$FU_NAME")"; return 1; }
line="$(fu_ctl remove "$id" | tail -n1)"
_fu_fields "$line"
if [[ ${FU_F[ok]:-} == true ]]; then
fu_ok "$(fu_msg "Deleted.")"
else
fu_bad "$(fu_reason_text "${FU_F[reason]:-unreachable}")"
return 1
fi
}
fu_do_help() {
cat <<- EOF
$FU_PRETTY $FU_VERSION
$(fu_msg "Usage: face-unlock [command]")
$(fu_msg "Commands:")
enable $(fu_msg "Turn face unlock on")
disable $(fu_msg "Turn it off (faces are kept)")
setup [NAME] $(fu_msg "Add a face")
faces $(fu_msg "List the faces")
remove ID $(fu_msg "Delete a face")
test $(fu_msg "Look at the camera and see what it sees")
status $(fu_msg "Show what is on")
-h, --help $(fu_msg "Show this help")
-V, --version $(fu_msg "Show the version")
$(fu_msg "Without a command an interactive menu is shown.")
EOF
}
# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
main() {
local cmd="${1:-}"
[[ $# -gt 0 ]] && shift
case "$cmd" in
--root) fu_root_verb "$@"; return ;;
esac
# Face data and settings are per user; root has neither a lock screen
# nor a face of its own to set up.
if [[ $EUID -eq 0 && -z ${FU_ALLOW_ROOT:-} ]]; then
fu_bad "$(fu_msg "Run this as your own user, not as root. It asks for your password when it needs to.")"
exit 2
fi
case "$cmd" in
enable) fu_do_enable ;;
disable) fu_do_disable ;;
setup|add|enroll) fu_do_setup "$@" ;;
faces|list) fu_do_faces ;;
remove|delete) fu_do_remove "$@" ;;
test|try) fu_ensure_service && fu_test ;;
status) fu_do_status ;;
-h|--help|help) fu_do_help ;;
-V|--version) printf '%s %s\n' "$FU_NAME" "$FU_VERSION" ;;
'') fu_ui_menu ;;
*)
fu_bad "$(fu_msg "Unknown command: %s" "$cmd")"
printf '\n'
fu_do_help
exit 1
;;
esac
}
main "$@"
+70 -70
View File
@@ -4,42 +4,42 @@
#
# The shell side never touches the camera or the face data. Those belong to
# the daemon, and everything here that needs them asks it through
# plasma-face-unlock-ctl. What this side does write is the user's own settings
# face-unlock-ctl. What this side does write is the user's own settings
# file, and (through sudo, and only when asked) the system settings and the
# PAM files of sudo and polkit.
PFU_VERSION="@VERSION@"
PFU_NAME="plasma-face-unlock"
PFU_PRETTY="Plasma Face Unlock"
FU_VERSION="@VERSION@"
FU_NAME="face-unlock"
FU_PRETTY="Face Unlock"
PFU_LIBDIR="${PFU_LIBDIR:-@LIBDIR@}"
PFU_LIBEXECDIR="${PFU_LIBEXECDIR:-@LIBEXECDIR@}"
PFU_LOCALEDIR="${PFU_LOCALEDIR:-@LOCALEDIR@}"
PFU_PAMDIR="${PFU_PAMDIR:-@PAMDIR@}"
FU_LIBDIR="${FU_LIBDIR:-@LIBDIR@}"
FU_LIBEXECDIR="${FU_LIBEXECDIR:-@LIBEXECDIR@}"
FU_LOCALEDIR="${FU_LOCALEDIR:-@LOCALEDIR@}"
FU_PAMDIR="${FU_PAMDIR:-@PAMDIR@}"
PFU_CTL="${PFU_CTL:-$PFU_LIBEXECDIR/plasma-face-unlock-ctl}"
PFU_AGENT="${PFU_AGENT:-$PFU_LIBEXECDIR/plasma-face-unlock-agent}"
PFU_PAM_MODULE="${PFU_PAM_MODULE:-$PFU_PAMDIR/pam_plasma_face_unlock.so}"
FU_CTL="${FU_CTL:-$FU_LIBEXECDIR/face-unlock-ctl}"
FU_AGENT="${FU_AGENT:-$FU_LIBEXECDIR/face-unlock-agent}"
FU_PAM_MODULE="${FU_PAM_MODULE:-$FU_PAMDIR/pam_face_unlock.so}"
PFU_XDG_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}"
PFU_CONFDIR="${PFU_CONFDIR:-${PFU_XDG_CONFIG}/${PFU_NAME}}"
PFU_CONFIG="${PFU_CONFIG:-${PFU_CONFDIR}/config}"
FU_XDG_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}"
FU_CONFDIR="${FU_CONFDIR:-${FU_XDG_CONFIG}/${FU_NAME}}"
FU_CONFIG="${FU_CONFIG:-${FU_CONFDIR}/config}"
# The system settings. Only root writes them; see system.sh.
PFU_SYSCONFIG="${PFU_SYSCONFIG:-/etc/${PFU_NAME}/config}"
FU_SYSCONFIG="${FU_SYSCONFIG:-/etc/${FU_NAME}/config}"
PFU_UNIT_SOCKET="plasma-face-unlockd.socket"
PFU_UNIT_AGENT="plasma-face-unlock-agent.service"
FU_UNIT_SOCKET="face-unlockd.socket"
FU_UNIT_AGENT="face-unlock-agent.service"
# ---------------------------------------------------------------------------
# Translations
# ---------------------------------------------------------------------------
export TEXTDOMAIN="plasma-face-unlock"
export TEXTDOMAINDIR="${PFU_LOCALEDIR}"
export TEXTDOMAIN="face-unlock"
export TEXTDOMAINDIR="${FU_LOCALEDIR}"
pfu_ui_locale() {
local l="${PFU_UI_LOCALE:-}"
fu_ui_locale() {
local l="${FU_UI_LOCALE:-}"
if [[ -z $l ]]; then
l="${LC_ALL:-}"
@@ -63,53 +63,53 @@ pfu_ui_locale() {
# Every gettext lookup is a fork and the settings screen redraws a screenful of
# labels per keypress, so results are memoized.
declare -A PFU_MSG_CACHE=()
declare -A FU_MSG_CACHE=()
PFU_MSG_RESULT=''
FU_MSG_RESULT=''
# pfu_msg_into <locale> <msgid>
# Plain lookup with the result in PFU_MSG_RESULT and no printf formatting, for
# fu_msg_into <locale> <msgid>
# Plain lookup with the result in FU_MSG_RESULT and no printf formatting, for
# callers that would otherwise pay a fork per label per frame.
pfu_msg_into() {
fu_msg_into() {
local locale="$1" msgid="$2" cachekey
cachekey="${locale}"$'\x1f'"${msgid}"
if [[ -n ${PFU_MSG_CACHE[$cachekey]+set} ]]; then
PFU_MSG_RESULT="${PFU_MSG_CACHE[$cachekey]}"
if [[ -n ${FU_MSG_CACHE[$cachekey]+set} ]]; then
FU_MSG_RESULT="${FU_MSG_CACHE[$cachekey]}"
return 0
fi
PFU_MSG_RESULT="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
[[ -n $PFU_MSG_RESULT ]] || PFU_MSG_RESULT="$msgid"
PFU_MSG_CACHE[$cachekey]="$PFU_MSG_RESULT"
FU_MSG_RESULT="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
[[ -n $FU_MSG_RESULT ]] || FU_MSG_RESULT="$msgid"
FU_MSG_CACHE[$cachekey]="$FU_MSG_RESULT"
return 0
}
# pfu_msg_in <locale> <msgid> [printf args...]
pfu_msg_in() {
# fu_msg_in <locale> <msgid> [printf args...]
fu_msg_in() {
local locale="$1" msgid="$2"
shift 2
pfu_msg_into "$locale" "$msgid"
fu_msg_into "$locale" "$msgid"
# With no arguments the message is plain text, not a format string. Feeding
# it to printf anyway would turn a literal percent sign in a translation
# into an invalid conversion.
if (( $# == 0 )); then
printf '%s' "$PFU_MSG_RESULT"
printf '%s' "$FU_MSG_RESULT"
return
fi
# shellcheck disable=SC2059 # the format string is the translated message
printf -- "$PFU_MSG_RESULT" "$@"
printf -- "$FU_MSG_RESULT" "$@"
}
PFU_LOCALE_CACHED=''
FU_LOCALE_CACHED=''
# pfu_msg <msgid> [printf args...]
pfu_msg() {
[[ -n $PFU_LOCALE_CACHED ]] || PFU_LOCALE_CACHED="$(pfu_ui_locale)"
pfu_msg_in "$PFU_LOCALE_CACHED" "$@"
# fu_msg <msgid> [printf args...]
fu_msg() {
[[ -n $FU_LOCALE_CACHED ]] || FU_LOCALE_CACHED="$(fu_ui_locale)"
fu_msg_in "$FU_LOCALE_CACHED" "$@"
}
# ---------------------------------------------------------------------------
@@ -119,45 +119,45 @@ pfu_msg() {
# Decided once, while stdout is still whatever the process was started with:
# testing -t 1 at the point of use is wrong for anything called through $(...),
# which sees a pipe and would conclude nobody is watching.
PFU_INTERACTIVE=''
[[ -t 1 ]] && PFU_INTERACTIVE=1
FU_INTERACTIVE=''
[[ -t 1 ]] && FU_INTERACTIVE=1
if [[ -n $PFU_INTERACTIVE && -z ${NO_COLOR:-} ]]; then
PFU_C_RESET=$'\033[0m'
PFU_C_BOLD=$'\033[1m'
PFU_C_DIM=$'\033[2m'
PFU_C_BLUE=$'\033[38;2;52;153;255m'
PFU_C_GREEN=$'\033[32m'
PFU_C_YELLOW=$'\033[33m'
PFU_C_RED=$'\033[31m'
if [[ -n $FU_INTERACTIVE && -z ${NO_COLOR:-} ]]; then
FU_C_RESET=$'\033[0m'
FU_C_BOLD=$'\033[1m'
FU_C_DIM=$'\033[2m'
FU_C_BLUE=$'\033[38;2;52;153;255m'
FU_C_GREEN=$'\033[32m'
FU_C_YELLOW=$'\033[33m'
FU_C_RED=$'\033[31m'
else
PFU_C_RESET='' PFU_C_BOLD='' PFU_C_DIM='' PFU_C_BLUE=''
PFU_C_GREEN='' PFU_C_YELLOW='' PFU_C_RED=''
FU_C_RESET='' FU_C_BOLD='' FU_C_DIM='' FU_C_BLUE=''
FU_C_GREEN='' FU_C_YELLOW='' FU_C_RED=''
fi
# What pfu_ok, pfu_bad and pfu_note printed. The menu redraws straight after an
# What fu_ok, fu_bad and fu_note printed. The menu redraws straight after an
# action, which wipes the screen, so it shows these again under the new frame
# rather than holding everything up for a key press.
PFU_UI_NOTICES=()
FU_UI_NOTICES=()
pfu_say() { printf '%s\n' "$*"; }
pfu_head() { printf '\n%s%s%s\n\n' "$PFU_C_BOLD$PFU_C_BLUE" "$*" "$PFU_C_RESET"; }
pfu_ok() { PFU_UI_NOTICES+=("$PFU_C_GREEN✔$PFU_C_RESET $*"); printf '%s✔%s %s\n' "$PFU_C_GREEN" "$PFU_C_RESET" "$*"; }
pfu_bad() { PFU_UI_NOTICES+=("$PFU_C_RED✘$PFU_C_RESET $*"); printf '%s✘%s %s\n' "$PFU_C_RED" "$PFU_C_RESET" "$*" >&2; }
pfu_note() { PFU_UI_NOTICES+=("$PFU_C_DIM•$PFU_C_RESET $*"); printf '%s•%s %s\n' "$PFU_C_DIM" "$PFU_C_RESET" "$*"; }
fu_say() { printf '%s\n' "$*"; }
fu_head() { printf '\n%s%s%s\n\n' "$FU_C_BOLD$FU_C_BLUE" "$*" "$FU_C_RESET"; }
fu_ok() { FU_UI_NOTICES+=("$FU_C_GREEN✔$FU_C_RESET $*"); printf '%s✔%s %s\n' "$FU_C_GREEN" "$FU_C_RESET" "$*"; }
fu_bad() { FU_UI_NOTICES+=("$FU_C_RED✘$FU_C_RESET $*"); printf '%s✘%s %s\n' "$FU_C_RED" "$FU_C_RESET" "$*" >&2; }
fu_note() { FU_UI_NOTICES+=("$FU_C_DIM•$FU_C_RESET $*"); printf '%s•%s %s\n' "$FU_C_DIM" "$FU_C_RESET" "$*"; }
pfu_have() { command -v "$1" > /dev/null 2>&1; }
fu_have() { command -v "$1" > /dev/null 2>&1; }
# Human-readable "x minutes ago" for a unix timestamp. 0 or empty yields the
# translated "never".
#
# Written with [[ ]] and a variable for each number on purpose: xgettext reads
# the < of an (( )) as a redirection and loses every string after it.
pfu_time_ago() {
fu_time_ago() {
local ts="$1" now delta n
if [[ ! $ts =~ ^[0-9]+$ || $ts -eq 0 ]]; then
pfu_msg "never"
fu_msg "never"
printf '\n'
return
fi
@@ -167,22 +167,22 @@ pfu_time_ago() {
[[ $delta -lt 0 ]] && delta=0
if [[ $delta -lt 60 ]]; then
pfu_msg "just now"
fu_msg "just now"
elif [[ $delta -lt 120 ]]; then
pfu_msg "1 minute ago"
fu_msg "1 minute ago"
elif [[ $delta -lt 3600 ]]; then
n=$(( delta / 60 ))
pfu_msg "%d minutes ago" "$n"
fu_msg "%d minutes ago" "$n"
elif [[ $delta -lt 7200 ]]; then
pfu_msg "1 hour ago"
fu_msg "1 hour ago"
elif [[ $delta -lt 86400 ]]; then
n=$(( delta / 3600 ))
pfu_msg "%d hours ago" "$n"
fu_msg "%d hours ago" "$n"
elif [[ $delta -lt 172800 ]]; then
pfu_msg "1 day ago"
fu_msg "1 day ago"
else
n=$(( delta / 86400 ))
pfu_msg "%d days ago" "$n"
fu_msg "%d days ago" "$n"
fi
printf '\n'
}
+34 -34
View File
@@ -2,9 +2,9 @@
#
# Reading and writing the settings files.
#
# Two of them, in the same format: ~/.config/plasma-face-unlock/config for
# Two of them, in the same format: ~/.config/face-unlock/config for
# what this user wants from the lock screen and the bubble, and
# /etc/plasma-face-unlock/config for what the daemon does (which camera, how
# /etc/face-unlock/config for what the daemon does (which camera, how
# strict), which only root writes. Neither is meant to be edited by hand:
# every option is in the menu.
#
@@ -12,25 +12,25 @@
# comments. The daemon and the agent read them with the same rules (see
# src/core/keyvalue.cpp).
declare -A PFU_KV_CACHE=()
declare -A FU_KV_CACHE=()
# _pfu_kv_lookup <file> <Key> [default]
# Result in PFU_KV_VALUE. Assigning rather than printing matters on the
# _fu_kv_lookup <file> <Key> [default]
# Result in FU_KV_VALUE. Assigning rather than printing matters on the
# settings screen, which reads every key on every frame: a command
# substitution there is a fork, and forks are the whole cost of a redraw.
PFU_KV_VALUE=''
FU_KV_VALUE=''
_pfu_kv_lookup() {
_fu_kv_lookup() {
local file="$1" key="$2" default="${3:-}" val='' line content
PFU_KV_VALUE="$default"
FU_KV_VALUE="$default"
[[ -r $file ]] || return 0
if [[ -n ${PFU_KV_CACHE[$file]+set} ]]; then
content="${PFU_KV_CACHE[$file]}"
if [[ -n ${FU_KV_CACHE[$file]+set} ]]; then
content="${FU_KV_CACHE[$file]}"
else
content="$(< "$file")"
PFU_KV_CACHE[$file]="$content"
FU_KV_CACHE[$file]="$content"
fi
while IFS= read -r line; do
@@ -45,19 +45,19 @@ _pfu_kv_lookup() {
val="${val%\"}"
val="${val#\"}"
[[ -n $val ]] && PFU_KV_VALUE="$val"
[[ -n $val ]] && FU_KV_VALUE="$val"
return 0
}
pfu_is_true() {
fu_is_true() {
case "${1,,}" in
yes|y|true|1|on|enabled) return 0 ;;
*) return 1 ;;
esac
}
# pfu_kv_set <file> <Key> <Value> <header line>
pfu_kv_set() {
# fu_kv_set <file> <Key> <Value> <header line>
fu_kv_set() {
local file="$1" key="$2" value="$3" header="$4" tmp
if [[ ! -e $file ]]; then
@@ -65,7 +65,7 @@ pfu_kv_set() {
{
printf '# %s\n' "$header"
printf '#\n'
printf '# Written by `%s`. Nothing here needs editing by hand:\n' "$PFU_NAME"
printf '# Written by `%s`. Nothing here needs editing by hand:\n' "$FU_NAME"
printf '# every option is in the menu.\n'
} > "$file" || return 1
fi
@@ -91,42 +91,42 @@ pfu_kv_set() {
# Replaced, not rewritten in place: the agent watches the directory and
# picks up the new file the moment it lands.
mv -f "$tmp" "$file"
unset 'PFU_KV_CACHE[$file]'
unset 'FU_KV_CACHE[$file]'
}
# ---------------------------------------------------------------------------
# This user's settings
# ---------------------------------------------------------------------------
pfu_config_get() {
_pfu_kv_lookup "$PFU_CONFIG" "$@"
printf '%s\n' "$PFU_KV_VALUE"
fu_config_get() {
_fu_kv_lookup "$FU_CONFIG" "$@"
printf '%s\n' "$FU_KV_VALUE"
}
pfu_config_set() {
pfu_kv_set "$PFU_CONFIG" "$1" "$2" "$PFU_PRETTY"
fu_config_set() {
fu_kv_set "$FU_CONFIG" "$1" "$2" "$FU_PRETTY"
}
# ---------------------------------------------------------------------------
# The system settings (read by anybody, written by root)
# ---------------------------------------------------------------------------
pfu_sys_get() {
_pfu_kv_lookup "$PFU_SYSCONFIG" "$@"
printf '%s\n' "$PFU_KV_VALUE"
fu_sys_get() {
_fu_kv_lookup "$FU_SYSCONFIG" "$@"
printf '%s\n' "$FU_KV_VALUE"
}
# pfu_config_load
# fu_config_load
# Everything the menu shows, resolved once per screen.
pfu_config_load() {
PFU_KV_CACHE=()
fu_config_load() {
FU_KV_CACHE=()
_pfu_kv_lookup "$PFU_CONFIG" Enabled no; CFG_ENABLED=no; pfu_is_true "$PFU_KV_VALUE" && CFG_ENABLED=yes
_pfu_kv_lookup "$PFU_CONFIG" LockScreen yes; CFG_LOCK=no; pfu_is_true "$PFU_KV_VALUE" && CFG_LOCK=yes
_pfu_kv_lookup "$PFU_CONFIG" Sudo no; CFG_SUDO=no; pfu_is_true "$PFU_KV_VALUE" && CFG_SUDO=yes
_pfu_kv_lookup "$PFU_CONFIG" Polkit no; CFG_POLKIT=no; pfu_is_true "$PFU_KV_VALUE" && CFG_POLKIT=yes
_fu_kv_lookup "$FU_CONFIG" Enabled no; CFG_ENABLED=no; fu_is_true "$FU_KV_VALUE" && CFG_ENABLED=yes
_fu_kv_lookup "$FU_CONFIG" LockScreen yes; CFG_LOCK=no; fu_is_true "$FU_KV_VALUE" && CFG_LOCK=yes
_fu_kv_lookup "$FU_CONFIG" Sudo no; CFG_SUDO=no; fu_is_true "$FU_KV_VALUE" && CFG_SUDO=yes
_fu_kv_lookup "$FU_CONFIG" Polkit no; CFG_POLKIT=no; fu_is_true "$FU_KV_VALUE" && CFG_POLKIT=yes
_pfu_kv_lookup "$PFU_SYSCONFIG" Liveness light; CFG_LIVENESS="$PFU_KV_VALUE"
_pfu_kv_lookup "$PFU_SYSCONFIG" Camera auto; CFG_CAMERA="$PFU_KV_VALUE"
_fu_kv_lookup "$FU_SYSCONFIG" Liveness light; CFG_LIVENESS="$FU_KV_VALUE"
_fu_kv_lookup "$FU_SYSCONFIG" Camera auto; CFG_CAMERA="$FU_KV_VALUE"
return 0
}
+103 -103
View File
@@ -2,184 +2,184 @@
#
# Asking the daemon.
#
# Through plasma-face-unlock-ctl, which prints every message as a line of tab
# 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.
# _pfu_fields <line>
# Splits one line of ctl output into the associative array PFU_F.
declare -A PFU_F=()
# _fu_fields <line>
# Splits one line of ctl output into the associative array FU_F.
declare -A FU_F=()
_pfu_fields() {
_fu_fields() {
local line="$1" field
local -a parts
PFU_F=()
FU_F=()
IFS=$'\t' read -r -a parts <<< "$line"
for field in "${parts[@]}"; do
PFU_F["${field%%=*}"]="${field#*=}"
FU_F["${field%%=*}"]="${field#*=}"
done
}
pfu_ctl() {
"$PFU_CTL" "$@"
fu_ctl() {
"$FU_CTL" "$@"
}
# pfu_status_load
# PFU_ST_REACHABLE is yes when the daemon answered at all. Everything else
# fu_status_load
# FU_ST_REACHABLE is yes when the daemon answered at all. Everything else
# is only filled in then.
pfu_status_load() {
fu_status_load() {
local out
PFU_ST_REACHABLE=no
PFU_ST_FACES=0
PFU_ST_LOCKOUT=0
PFU_ST_LAST=0
PFU_ST_PURPOSE=''
PFU_ST_CAMERA=''
PFU_ST_CAMERA_NAME=''
PFU_ST_CAMERA_PRESENT=no
PFU_ST_MODELS=no
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="$(pfu_ctl status 2>/dev/null | tail -n1)"
_pfu_fields "$out"
[[ ${PFU_F[ok]:-} == true ]] || return 1
out="$(fu_ctl status 2>/dev/null | tail -n1)"
_fu_fields "$out"
[[ ${FU_F[ok]:-} == true ]] || return 1
PFU_ST_REACHABLE=yes
PFU_ST_FACES="${PFU_F[faces]:-0}"
PFU_ST_LOCKOUT="${PFU_F[lockout]:-0}"
PFU_ST_LAST="${PFU_F[lastUnlock]:-0}"
PFU_ST_PURPOSE="${PFU_F[lastPurpose]:-}"
PFU_ST_CAMERA="${PFU_F[cameraPath]:-}"
PFU_ST_CAMERA_NAME="${PFU_F[cameraName]:-}"
PFU_ST_CAMERA_PRESENT="${PFU_F[cameraPresent]:-false}"
PFU_ST_MODELS="${PFU_F[models]:-false}"
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
}
# pfu_faces_load
# fu_faces_load
# The caller's faces, into parallel arrays.
pfu_faces_load() {
fu_faces_load() {
local line
PFU_FACE_IDS=() PFU_FACE_NAMES=() PFU_FACE_ON=() PFU_FACE_SAMPLES=() PFU_FACE_LEARNED=() PFU_FACE_CREATED=()
FU_FACE_IDS=() FU_FACE_NAMES=() FU_FACE_ON=() FU_FACE_SAMPLES=() FU_FACE_LEARNED=() FU_FACE_CREATED=()
while IFS= read -r line; do
_pfu_fields "$line"
[[ ${PFU_F[event]:-} == item ]] || continue
PFU_FACE_IDS+=("${PFU_F[id]}")
PFU_FACE_NAMES+=("${PFU_F[name]}")
PFU_FACE_ON+=("${PFU_F[enabled]}")
PFU_FACE_SAMPLES+=("${PFU_F[samples]:-0}")
PFU_FACE_LEARNED+=("${PFU_F[adaptive]:-0}")
PFU_FACE_CREATED+=("${PFU_F[created]:-0}")
done < <(pfu_ctl list 2>/dev/null)
_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)
}
# pfu_cameras_load
pfu_cameras_load() {
# fu_cameras_load
fu_cameras_load() {
local line
PFU_CAM_PATHS=() PFU_CAM_NAMES=() PFU_CAM_IR=()
PFU_CAM_AUTO=''
FU_CAM_PATHS=() FU_CAM_NAMES=() FU_CAM_IR=()
FU_CAM_AUTO=''
while IFS= read -r line; do
_pfu_fields "$line"
case "${PFU_F[event]:-}" in
_fu_fields "$line"
case "${FU_F[event]:-}" in
item)
PFU_CAM_PATHS+=("${PFU_F[path]}")
PFU_CAM_NAMES+=("${PFU_F[name]}")
PFU_CAM_IR+=("${PFU_F[infrared]}")
FU_CAM_PATHS+=("${FU_F[path]}")
FU_CAM_NAMES+=("${FU_F[name]}")
FU_CAM_IR+=("${FU_F[infrared]}")
;;
result)
PFU_CAM_AUTO="${PFU_F[auto]:-}"
FU_CAM_AUTO="${FU_F[auto]:-}"
;;
esac
done < <(pfu_ctl cameras 2>/dev/null)
done < <(fu_ctl cameras 2>/dev/null)
}
# pfu_reason_text <reason>
# fu_reason_text <reason>
# What a daemon answer means, for people.
pfu_reason_text() {
fu_reason_text() {
case "$1" in
mismatch) pfu_msg "The face did not match." ;;
spoof) pfu_msg "That looked like a photo or a screen." ;;
liveness) pfu_msg "The face matched, but did not blink or move." ;;
attention) pfu_msg "The face was not looking at the screen." ;;
quality) pfu_msg "The picture was too dark, too blurry or too far away." ;;
no-face) pfu_msg "No face in view." ;;
lockout) pfu_msg "Face unlock is paused after too many tries. Unlock once with your password." ;;
not-enrolled) pfu_msg "No face is set up yet." ;;
camera) pfu_msg "The camera could not be used." ;;
models) pfu_msg "The recognition models are missing. Reinstall the package." ;;
lid-closed) pfu_msg "The lid is closed." ;;
busy) pfu_msg "The camera is busy with another scan." ;;
unreachable) pfu_msg "The face unlock service is not running." ;;
denied) pfu_msg "Not allowed." ;;
*) pfu_msg "Something went wrong (%s)." "$1" ;;
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." ;;
*) fu_msg "Something went wrong (%s)." "$1" ;;
esac
printf '\n'
}
# pfu_test
# 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.
pfu_test() {
fu_test() {
local line started=0 shown='' score='-' matched=0
pfu_say " $(pfu_msg "Look at the camera. Blink once, or turn your head a little.")"
fu_say " $(fu_msg "Look at the camera. Blink once, or turn your head a little.")"
printf '\n'
while IFS= read -r line; do
_pfu_fields "$line"
case "${PFU_F[event]:-}" in
_fu_fields "$line"
case "${FU_F[event]:-}" in
started)
started=1
;;
face)
printf '\r\033[K %s\n' "$(pfu_msg "Face found.")"
printf '\r\033[K %s\n' "$(fu_msg "Face found.")"
;;
hint)
case "${PFU_F[hint]}" in
blink) printf '\r\033[K %s\n' "$(pfu_msg "Recognised. Now blink once, or turn your head a little.")" ;;
look) printf '\r\033[K %s\n' "$(pfu_msg "Look at the screen.")" ;;
closer) printf '\r\033[K %s\n' "$(pfu_msg "Move closer to the camera.")" ;;
light) printf '\r\033[K %s\n' "$(pfu_msg "It is too dark to see your face.")" ;;
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 ${PFU_F[score]:-} ]] && score="${PFU_F[score]}"
matched="${PFU_F[matched]:-0}"
[[ -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' \
"$(pfu_msg "match")" "$score" "$(pfu_msg "turn")" "${PFU_F[yaw]:-0}" \
"$(pfu_msg "eyes")" "${PFU_F[eyes]:-0}" \
"$(pfu_msg "depth")" "${PFU_F[depth]:-0}" "$(pfu_msg "blink")" "${PFU_F[blink]:-0}" \
"$(pfu_msg "glare")" "${PFU_F[glare]:-0}" "$(pfu_msg "edge")" "${PFU_F[device]:-0}"
[[ -n $PFU_INTERACTIVE ]] && printf '\r\033[K%s%s%s' "$PFU_C_DIM" "$shown" "$PFU_C_RESET"
"$(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 [[ ${PFU_F[ok]} == true ]]; then
if [[ ${FU_F[ok]} == true ]]; then
local how=''
case "${PFU_F[liveness]:-}" in
blink) how="$(pfu_msg "blink")" ;;
depth) how="$(pfu_msg "head turn")" ;;
case "${FU_F[liveness]:-}" in
blink) how="$(fu_msg "blink")" ;;
depth) how="$(fu_msg "head turn")" ;;
esac
pfu_ok "$(pfu_msg "Recognised as %s (match %s) in %s ms." "${PFU_F[name]}" "${PFU_F[score]}" "${PFU_F[ms]}")"
[[ -n $how ]] && pfu_say " $(pfu_msg "Sign of life: %s" "$how")"
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
pfu_bad "$(pfu_reason_text "${PFU_F[reason]}")"
[[ -n ${PFU_F[message]:-} ]] && pfu_say " ${PFU_F[message]}"
if [[ -n ${PFU_F[lockout]:-} ]]; then
pfu_note "$(pfu_msg "That was too many tries. Face unlock is paused until you unlock with your password.")"
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 < <(pfu_ctl test 2>/dev/null)
done < <(fu_ctl test 2>/dev/null)
printf '\n'
pfu_bad "$(pfu_reason_text unreachable)"
fu_bad "$(fu_reason_text unreachable)"
return 1
}
+265 -265
View File
@@ -14,39 +14,39 @@
# character, so the line discipline eats it instead of delivering it, and a
# backspace typed while the interface was between reads simply vanishes.
# Holding non-canonical mode for the whole interface removes the gap.
PFU_TERM_SAVED=''
FU_TERM_SAVED=''
pfu_ui_term_raw() {
pfu_have stty || return 0
fu_ui_term_raw() {
fu_have stty || return 0
[[ -t 0 ]] || return 0
[[ -n $PFU_TERM_SAVED ]] && return 0
[[ -n $FU_TERM_SAVED ]] && return 0
PFU_TERM_SAVED="$(stty -g 2>/dev/null)" || { PFU_TERM_SAVED=''; return 0; }
FU_TERM_SAVED="$(stty -g 2>/dev/null)" || { FU_TERM_SAVED=''; return 0; }
stty -icanon -echo min 1 time 0 2>/dev/null || true
}
pfu_ui_term_restore() {
[[ -n $PFU_TERM_SAVED ]] || return 0
stty "$PFU_TERM_SAVED" 2>/dev/null || true
PFU_TERM_SAVED=''
fu_ui_term_restore() {
[[ -n $FU_TERM_SAVED ]] || return 0
stty "$FU_TERM_SAVED" 2>/dev/null || true
FU_TERM_SAVED=''
}
# Runs an action with the terminal handed back to normal line mode, so anything
# it prints or prompts for (sudo's password prompt, above all) behaves the way
# a program expects.
pfu_ui_cooked() {
pfu_ui_term_restore
fu_ui_cooked() {
fu_ui_term_restore
"$@"
local rc=$?
pfu_ui_term_raw
fu_ui_term_raw
return $rc
}
# pfu_read_key
# fu_read_key
# One keypress, resolved to a symbolic name. Arrow keys arrive as ESC [ A, so
# the tail of the sequence is consumed here rather than being mistaken for
# three separate presses.
pfu_read_key() {
fu_read_key() {
local k rest
IFS= read -rsn1 k || return 1
@@ -72,26 +72,26 @@ pfu_read_key() {
esac
}
# pfu_ui_read_line <initial>
# A minimal line editor built on pfu_read_key, with the result in
# PFU_LINE_RESULT. This exists instead of bash's own `read -r` because mixing
# fu_ui_read_line <initial>
# A minimal line editor built on fu_read_key, with the result in
# FU_LINE_RESULT. This exists instead of bash's own `read -r` because mixing
# line mode into a single-key interface breaks it: after one cooked-mode read
# the following `read -sn1` stops receiving keystrokes entirely.
PFU_LINE_RESULT=''
FU_LINE_RESULT=''
pfu_ui_read_line() {
fu_ui_read_line() {
local buf="${1:-}" key
PFU_LINE_RESULT=''
FU_LINE_RESULT=''
printf '%s' "$buf"
while true; do
key="$(pfu_read_key)" || { printf '\n'; return 1; }
key="$(fu_read_key)" || { printf '\n'; return 1; }
case "$key" in
enter)
printf '\n'
PFU_LINE_RESULT="$buf"
FU_LINE_RESULT="$buf"
return 0
;;
escape)
@@ -118,88 +118,88 @@ pfu_ui_read_line() {
done
}
# _pfu_ui_take_notices
# _fu_ui_take_notices
# The messages the last action left, as lines for under a frame, in
# PFU_UI_NOTICE_TEXT. Each is shown once.
PFU_UI_NOTICE_TEXT=''
# FU_UI_NOTICE_TEXT. Each is shown once.
FU_UI_NOTICE_TEXT=''
_pfu_ui_take_notices() {
_fu_ui_take_notices() {
local n
PFU_UI_NOTICE_TEXT=''
(( ${#PFU_UI_NOTICES[@]} )) || return 0
PFU_UI_NOTICE_TEXT=$'\n'
for n in "${PFU_UI_NOTICES[@]}"; do
PFU_UI_NOTICE_TEXT+=" $n"$'\n'
FU_UI_NOTICE_TEXT=''
(( ${#FU_UI_NOTICES[@]} )) || return 0
FU_UI_NOTICE_TEXT=$'\n'
for n in "${FU_UI_NOTICES[@]}"; do
FU_UI_NOTICE_TEXT+=" $n"$'\n'
done
PFU_UI_NOTICES=()
FU_UI_NOTICES=()
}
# pfu_ui_confirm <question>
pfu_ui_confirm() {
# fu_ui_confirm <question>
fu_ui_confirm() {
local key hint yes
# TRANSLATORS: the letter after "[" is the key for yes. y works too.
hint="$(pfu_msg "[y/N]")"
hint="$(fu_msg "[y/N]")"
yes="${hint:1:1}"
printf '\n %s %s ' "$1" "$hint"
key="$(pfu_read_key)" || return 1
key="$(fu_read_key)" || return 1
printf '%s\n' "$key"
[[ ${key,,} == y || ${key,,} == "${yes,,}" ]]
}
# _pfu_width <text>
# The columns the text takes, into PFU_WIDTH. ${#s} counts characters, but
# _fu_width <text>
# The columns the text takes, into FU_WIDTH. ${#s} counts characters, but
# Chinese, Japanese and Korean ones take two columns each.
PFU_WIDTH=0
FU_WIDTH=0
_pfu_width() {
_fu_width() {
local wide="${1//[^ -〿぀-ヿ㐀-䶿一-鿿가-힯豈-﫿＀-⦆]/}"
PFU_WIDTH=$(( ${#1} + ${#wide} ))
FU_WIDTH=$(( ${#1} + ${#wide} ))
}
# _pfu_row <label> <value>
# _fu_row <label> <value>
# printf's %-28s pads by bytes, so a label containing "ü" comes out one column
# short. The padding is computed here instead.
_pfu_row() {
_fu_row() {
local label="$1" value="$2" pad
_pfu_width "$label"
pad=$(( 30 - PFU_WIDTH ))
_fu_width "$label"
pad=$(( 30 - FU_WIDTH ))
(( pad < 0 )) && pad=0
printf ' %s%*s %s\n' "$label" "$pad" '' "$value"
}
_pfu_onoff() {
_fu_onoff() {
if [[ $1 == yes ]]; then
printf '%s%s%s' "$PFU_C_GREEN" "$(pfu_msg "ON")" "$PFU_C_RESET"
printf '%s%s%s' "$FU_C_GREEN" "$(fu_msg "ON")" "$FU_C_RESET"
else
printf '%s%s%s' "$PFU_C_DIM" "$(pfu_msg "OFF")" "$PFU_C_RESET"
printf '%s%s%s' "$FU_C_DIM" "$(fu_msg "OFF")" "$FU_C_RESET"
fi
}
_pfu_small_onoff() {
_fu_small_onoff() {
if [[ $1 == yes ]]; then
printf '%s%s%s' "$PFU_C_GREEN" "$(pfu_msg "on")" "$PFU_C_RESET"
printf '%s%s%s' "$FU_C_GREEN" "$(fu_msg "on")" "$FU_C_RESET"
else
printf '%s%s%s' "$PFU_C_DIM" "$(pfu_msg "off")" "$PFU_C_RESET"
printf '%s%s%s' "$FU_C_DIM" "$(fu_msg "off")" "$FU_C_RESET"
fi
}
# pfu_value_label <Key> <value>
# fu_value_label <Key> <value>
# How a setting's value reads in the menu.
pfu_value_label() {
fu_value_label() {
case "$1:$2" in
Liveness:heavy) pfu_msg "strict" ;;
Liveness:light) pfu_msg "basic" ;;
Liveness:off) pfu_msg "off" ;;
Strictness:normal) pfu_msg "normal" ;;
Strictness:strict) pfu_msg "strict" ;;
Strictness:relaxed) pfu_msg "relaxed" ;;
BubbleStyle:full) pfu_msg "island with the face" ;;
BubbleStyle:minimal) pfu_msg "small pill with a lock" ;;
AnimationSpeed:normal) pfu_msg "normal" ;;
AnimationSpeed:fast) pfu_msg "fast" ;;
AnimationSpeed:slow) pfu_msg "slow" ;;
ScanSeconds:*) pfu_msg "%s seconds" "$2" ;;
Camera:auto) pfu_msg "automatic" ;;
Liveness:heavy) fu_msg "strict" ;;
Liveness:light) fu_msg "basic" ;;
Liveness:off) fu_msg "off" ;;
Strictness:normal) fu_msg "normal" ;;
Strictness:strict) fu_msg "strict" ;;
Strictness:relaxed) fu_msg "relaxed" ;;
BubbleStyle:full) fu_msg "island with the face" ;;
BubbleStyle:minimal) fu_msg "small pill with a lock" ;;
AnimationSpeed:normal) fu_msg "normal" ;;
AnimationSpeed:fast) fu_msg "fast" ;;
AnimationSpeed:slow) fu_msg "slow" ;;
ScanSeconds:*) fu_msg "%s seconds" "$2" ;;
Camera:auto) fu_msg "automatic" ;;
*) printf '%s' "$2" ;;
esac
}
@@ -208,55 +208,55 @@ pfu_value_label() {
# Status
# ---------------------------------------------------------------------------
# pfu_ui_status
# fu_ui_status
# Shared by the `status` subcommand and the menu header.
pfu_ui_status() {
fu_ui_status() {
local names='' i camera
pfu_config_load
pfu_status_load
fu_config_load
fu_status_load
_pfu_row "$(pfu_msg "Face unlock")" "$(_pfu_onoff "$CFG_ENABLED")"
_fu_row "$(fu_msg "Face unlock")" "$(_fu_onoff "$CFG_ENABLED")"
printf '\n'
if [[ $PFU_ST_REACHABLE != yes ]]; then
_pfu_row "$(pfu_msg "Service")" "${PFU_C_YELLOW}$(pfu_msg "not running")${PFU_C_RESET}"
if [[ $FU_ST_REACHABLE != yes ]]; then
_fu_row "$(fu_msg "Service")" "${FU_C_YELLOW}$(fu_msg "not running")${FU_C_RESET}"
if [[ $CFG_ENABLED == yes ]]; then
printf '\n %s%s%s\n' "$PFU_C_DIM" "$(pfu_msg "Turning face unlock on again starts it.")" "$PFU_C_RESET"
printf '\n %s%s%s\n' "$FU_C_DIM" "$(fu_msg "Turning face unlock on again starts it.")" "$FU_C_RESET"
fi
return 0
fi
pfu_faces_load
for i in "${!PFU_FACE_NAMES[@]}"; do
[[ ${PFU_FACE_ON[i]} == true ]] || continue
names="${names:+$names, }${PFU_FACE_NAMES[i]}"
fu_faces_load
for i in "${!FU_FACE_NAMES[@]}"; do
[[ ${FU_FACE_ON[i]} == true ]] || continue
names="${names:+$names, }${FU_FACE_NAMES[i]}"
done
if (( ${#PFU_FACE_IDS[@]} == 0 )); then
_pfu_row "$(pfu_msg "Faces")" "${PFU_C_YELLOW}$(pfu_msg "none set up yet")${PFU_C_RESET}"
if (( ${#FU_FACE_IDS[@]} == 0 )); then
_fu_row "$(fu_msg "Faces")" "${FU_C_YELLOW}$(fu_msg "none set up yet")${FU_C_RESET}"
else
_pfu_row "$(pfu_msg "Faces")" "${PFU_ST_FACES} ${PFU_C_DIM}(${names:-$(pfu_msg "all turned off")})${PFU_C_RESET}"
_fu_row "$(fu_msg "Faces")" "${FU_ST_FACES} ${FU_C_DIM}(${names:-$(fu_msg "all turned off")})${FU_C_RESET}"
fi
if [[ $PFU_ST_CAMERA_PRESENT == true ]]; then
camera="${PFU_ST_CAMERA_NAME:-$PFU_ST_CAMERA}"
if [[ $FU_ST_CAMERA_PRESENT == true ]]; then
camera="${FU_ST_CAMERA_NAME:-$FU_ST_CAMERA}"
else
camera="${PFU_C_YELLOW}$(pfu_msg "none found")${PFU_C_RESET}"
camera="${FU_C_YELLOW}$(fu_msg "none found")${FU_C_RESET}"
fi
_pfu_row "$(pfu_msg "Camera")" "$camera"
_fu_row "$(fu_msg "Camera")" "$camera"
_pfu_row "$(pfu_msg "Lock screen")" "$(_pfu_small_onoff "$( [[ $CFG_ENABLED == yes && $CFG_LOCK == yes ]] && echo yes || echo no)")"
_pfu_row "$(pfu_msg "sudo")" "$(_pfu_small_onoff "$(pfu_pam_enabled sudo && echo yes || echo no)")"
_pfu_row "$(pfu_msg "Admin prompts")" "$(_pfu_small_onoff "$(pfu_pam_enabled polkit-1 && echo yes || echo no)")"
_pfu_row "$(pfu_msg "Photo check")" "$(pfu_value_label Liveness "$CFG_LIVENESS")"
_fu_row "$(fu_msg "Lock screen")" "$(_fu_small_onoff "$( [[ $CFG_ENABLED == yes && $CFG_LOCK == yes ]] && echo yes || echo no)")"
_fu_row "$(fu_msg "sudo")" "$(_fu_small_onoff "$(fu_pam_enabled sudo && echo yes || echo no)")"
_fu_row "$(fu_msg "Admin prompts")" "$(_fu_small_onoff "$(fu_pam_enabled polkit-1 && echo yes || echo no)")"
_fu_row "$(fu_msg "Photo check")" "$(fu_value_label Liveness "$CFG_LIVENESS")"
if (( PFU_ST_LOCKOUT > 0 )); then
_pfu_row "$(pfu_msg "Paused")" "${PFU_C_YELLOW}$(pfu_msg "for %d more minutes, or until the password is used" "$(( (PFU_ST_LOCKOUT + 59) / 60 ))")${PFU_C_RESET}"
if (( FU_ST_LOCKOUT > 0 )); then
_fu_row "$(fu_msg "Paused")" "${FU_C_YELLOW}$(fu_msg "for %d more minutes, or until the password is used" "$(( (FU_ST_LOCKOUT + 59) / 60 ))")${FU_C_RESET}"
fi
_pfu_row "$(pfu_msg "Last unlock")" "$(pfu_time_ago "$PFU_ST_LAST")"
_fu_row "$(fu_msg "Last unlock")" "$(fu_time_ago "$FU_ST_LAST")"
if [[ $PFU_ST_MODELS != true ]]; then
printf '\n %s%s%s\n' "$PFU_C_RED" "$(pfu_msg "The recognition models are missing. Reinstall the package.")" "$PFU_C_RESET"
if [[ $FU_ST_MODELS != true ]]; then
printf '\n %s%s%s\n' "$FU_C_RED" "$(fu_msg "The recognition models are missing. Reinstall the package.")" "$FU_C_RESET"
fi
}
@@ -270,7 +270,7 @@ pfu_ui_status() {
# type bool, choice (steps through the choices) or camera
# needs a bool setting this one does nothing without; it is dimmed while
# that is off
PFU_SETTINGS=(
FU_SETTINGS=(
"group|Lock screen"
"user|LockScreen|bool|yes|Unlock with your face"
"user|ScanOnWake|bool|yes|Scan when you come back||LockScreen"
@@ -293,85 +293,85 @@ PFU_SETTINGS=(
"user|BubbleForPrompts|bool|yes|Also for sudo and admin prompts||Bubble"
)
# pfu_setting_help <Key> <value>
# fu_setting_help <Key> <value>
# What a setting does, shown under the list for the selected one.
pfu_setting_help() {
fu_setting_help() {
case "$1:$2" in
LockScreen:*) pfu_msg "Unlocks the lock screen when it sees your face. Off: only your password works there." ;;
ScanOnWake:*) pfu_msg "Scans when you press a key or move the mouse on the lock screen, and when the computer wakes up." ;;
ScanOnLock:*) pfu_msg "Scans as soon as the screen locks. Off by default: if you lock it yourself, it would unlock again right away." ;;
sudo:*) pfu_msg "sudo takes your face instead of the password. No match: you type the password as usual." ;;
polkit-1:*) pfu_msg "The password windows of Plasma and apps, for example when you install software. No match: you type the password." ;;
Liveness:heavy) pfu_msg "You have to blink or turn your head a little. This also stops printed photos." ;;
Liveness:off) pfu_msg "No check at all. Only for trying out a camera." ;;
Liveness:*) pfu_msg "Stops photos on a phone, a tablet or glossy paper. You do not have to blink. A matte printed photo can get through." ;;
Strictness:strict) pfu_msg "Fewer wrong matches, but it may not know you with glasses or in bad light." ;;
Strictness:relaxed) pfu_msg "Knows you more easily, but also somebody who looks a lot like you." ;;
Strictness:*) pfu_msg "The default. Right for most people." ;;
Attention:*) pfu_msg "Your eyes have to be open and on the screen. So it does not unlock while you look away or sleep." ;;
Camera:*) pfu_msg "Automatic takes the first normal camera. After a change, set up your face again. An infrared camera needs its light on (linux-enable-ir-emitter)." ;;
ScanSeconds:*) pfu_msg "How long the camera looks for your face before it gives up." ;;
Adapt:*) pfu_msg "After a sure match it keeps how you look now. So a new haircut or glasses need no new setup." ;;
SkipLidClosed:*) pfu_msg "No scan while the laptop is closed, for example at a desk with an external screen." ;;
Bubble:*) pfu_msg "The bubble at the top of the screen shows what the camera is doing." ;;
BubbleStyle:minimal) pfu_msg "A small pill with a lock that opens." ;;
BubbleStyle:*) pfu_msg "An island with a face that looks around, then rings and a tick." ;;
AnimationSpeed:*) pfu_msg "How fast the bubble moves." ;;
BubbleForPrompts:*) pfu_msg "Shows the bubble when sudo or an admin prompt scans your face, too." ;;
LockScreen:*) fu_msg "Unlocks the lock screen when it sees your face. Off: only your password works there." ;;
ScanOnWake:*) fu_msg "Scans when you press a key or move the mouse on the lock screen, and when the computer wakes up." ;;
ScanOnLock:*) fu_msg "Scans as soon as the screen locks. Off by default: if you lock it yourself, it would unlock again right away." ;;
sudo:*) fu_msg "sudo takes your face instead of the password. No match: you type the password as usual." ;;
polkit-1:*) fu_msg "The password windows of Plasma and apps, for example when you install software. No match: you type the password." ;;
Liveness:heavy) fu_msg "You have to blink or turn your head a little. This also stops printed photos." ;;
Liveness:off) fu_msg "No check at all. Only for trying out a camera." ;;
Liveness:*) fu_msg "Stops photos on a phone, a tablet or glossy paper. You do not have to blink. A matte printed photo can get through." ;;
Strictness:strict) fu_msg "Fewer wrong matches, but it may not know you with glasses or in bad light." ;;
Strictness:relaxed) fu_msg "Knows you more easily, but also somebody who looks a lot like you." ;;
Strictness:*) fu_msg "The default. Right for most people." ;;
Attention:*) fu_msg "Your eyes have to be open and on the screen. So it does not unlock while you look away or sleep." ;;
Camera:*) fu_msg "Automatic takes the first normal camera. After a change, set up your face again. An infrared camera needs its light on (linux-enable-ir-emitter)." ;;
ScanSeconds:*) fu_msg "How long the camera looks for your face before it gives up." ;;
Adapt:*) fu_msg "After a sure match it keeps how you look now. So a new haircut or glasses need no new setup." ;;
SkipLidClosed:*) fu_msg "No scan while the laptop is closed, for example at a desk with an external screen." ;;
Bubble:*) fu_msg "The bubble at the top of the screen shows what the camera is doing." ;;
BubbleStyle:minimal) fu_msg "A small pill with a lock that opens." ;;
BubbleStyle:*) fu_msg "An island with a face that looks around, then rings and a tick." ;;
AnimationSpeed:*) fu_msg "How fast the bubble moves." ;;
BubbleForPrompts:*) fu_msg "Shows the bubble when sudo or an admin prompt scans your face, too." ;;
esac
}
# _pfu_wrap <width> <text>
# Word wrap, into PFU_WRAP_LINES.
PFU_WRAP_LINES=()
# _fu_wrap <width> <text>
# Word wrap, into FU_WRAP_LINES.
FU_WRAP_LINES=()
_pfu_wrap() {
_fu_wrap() {
local width="$1" word line='' used=0 c i
local -a words
PFU_WRAP_LINES=()
FU_WRAP_LINES=()
read -r -a words <<< "$2"
for word in "${words[@]}"; do
_pfu_width "$word"
if (( used > 0 && used + 1 + PFU_WIDTH > width )); then
PFU_WRAP_LINES+=("$line")
_fu_width "$word"
if (( used > 0 && used + 1 + FU_WIDTH > width )); then
FU_WRAP_LINES+=("$line")
line='' used=0
fi
(( used > 0 )) && line+=' ' used=$(( used + 1 ))
if (( used + PFU_WIDTH <= width )); then
line+="$word" used=$(( used + PFU_WIDTH ))
if (( used + FU_WIDTH <= width )); then
line+="$word" used=$(( used + FU_WIDTH ))
continue
fi
# Longer than a line: Chinese and Japanese have no spaces, so break
# between any two characters.
for (( i = 0; i < ${#word}; i++ )); do
c="${word:i:1}"
_pfu_width "$c"
if (( used + PFU_WIDTH > width )); then
PFU_WRAP_LINES+=("$line")
_fu_width "$c"
if (( used + FU_WIDTH > width )); then
FU_WRAP_LINES+=("$line")
line='' used=0
fi
line+="$c" used=$(( used + PFU_WIDTH ))
line+="$c" used=$(( used + FU_WIDTH ))
done
done
[[ -n $line ]] && PFU_WRAP_LINES+=("$line")
[[ -n $line ]] && FU_WRAP_LINES+=("$line")
return 0
}
# _pfu_setting_value <scope> <Key> <default>
# The current value, in PFU_SETTING_VALUE.
PFU_SETTING_VALUE=''
# _fu_setting_value <scope> <Key> <default>
# The current value, in FU_SETTING_VALUE.
FU_SETTING_VALUE=''
_pfu_setting_value() {
_fu_setting_value() {
case "$1" in
user) _pfu_kv_lookup "$PFU_CONFIG" "$2" "$3"; PFU_SETTING_VALUE="$PFU_KV_VALUE" ;;
sys) _pfu_kv_lookup "$PFU_SYSCONFIG" "$2" "$3"; PFU_SETTING_VALUE="$PFU_KV_VALUE" ;;
pam) if pfu_pam_enabled "$2"; then PFU_SETTING_VALUE=yes; else PFU_SETTING_VALUE=no; fi ;;
user) _fu_kv_lookup "$FU_CONFIG" "$2" "$3"; FU_SETTING_VALUE="$FU_KV_VALUE" ;;
sys) _fu_kv_lookup "$FU_SYSCONFIG" "$2" "$3"; FU_SETTING_VALUE="$FU_KV_VALUE" ;;
pam) if fu_pam_enabled "$2"; then FU_SETTING_VALUE=yes; else FU_SETTING_VALUE=no; fi ;;
esac
}
# _pfu_step <current> <step> <choice>...
# _fu_step <current> <step> <choice>...
# The choice <step> (1 or -1) away from the current one, round at the ends.
_pfu_step() {
_fu_step() {
local current="$1" step="$2" i
shift 2
local -a all=("$@")
@@ -384,100 +384,100 @@ _pfu_step() {
printf '%s\n' "${all[0]}"
}
# _pfu_next_choice <current> <a,b,c> <step>
_pfu_next_choice() {
# _fu_next_choice <current> <a,b,c> <step>
_fu_next_choice() {
local -a choices
IFS=',' read -r -a choices <<< "$2"
_pfu_step "$1" "$3" "${choices[@]}"
_fu_step "$1" "$3" "${choices[@]}"
}
# _pfu_next_camera <current> <step>
_pfu_next_camera() {
pfu_cameras_load
_pfu_step "$1" "$2" auto "${PFU_CAM_PATHS[@]}"
# _fu_next_camera <current> <step>
_fu_next_camera() {
fu_cameras_load
_fu_step "$1" "$2" auto "${FU_CAM_PATHS[@]}"
}
_pfu_camera_label() {
_fu_camera_label() {
local value="$1" i
if [[ $value == auto ]]; then
for i in "${!PFU_CAM_PATHS[@]}"; do
if [[ ${PFU_CAM_PATHS[i]} == "$PFU_CAM_AUTO" ]]; then
pfu_msg "automatic (%s)" "${PFU_CAM_NAMES[i]}"
for i in "${!FU_CAM_PATHS[@]}"; do
if [[ ${FU_CAM_PATHS[i]} == "$FU_CAM_AUTO" ]]; then
fu_msg "automatic (%s)" "${FU_CAM_NAMES[i]}"
return
fi
done
pfu_msg "automatic"
fu_msg "automatic"
return
fi
for i in "${!PFU_CAM_PATHS[@]}"; do
if [[ ${PFU_CAM_PATHS[i]} == "$value" ]]; then
printf '%s' "${PFU_CAM_NAMES[i]}"
[[ ${PFU_CAM_IR[i]} == true ]] && printf ' %s' "$(pfu_msg "(infrared)")"
for i in "${!FU_CAM_PATHS[@]}"; do
if [[ ${FU_CAM_PATHS[i]} == "$value" ]]; then
printf '%s' "${FU_CAM_NAMES[i]}"
[[ ${FU_CAM_IR[i]} == true ]] && printf ' %s' "$(fu_msg "(infrared)")"
return
fi
done
printf '%s %s' "$value" "$(pfu_msg "(not connected)")"
printf '%s %s' "$value" "$(fu_msg "(not connected)")"
}
# _pfu_setting_change <scope> <Key> <type> <current> <choices> <step>
_pfu_setting_change() {
# _fu_setting_change <scope> <Key> <type> <current> <choices> <step>
_fu_setting_change() {
local scope="$1" key="$2" type="$3" current="$4" choices="$5" step="$6" next
case "$type" in
bool) if pfu_is_true "$current"; then next=no; else next=yes; fi ;;
choice) next="$(_pfu_next_choice "$current" "$choices" "$step")" ;;
camera) next="$(_pfu_next_camera "$current" "$step")" ;;
bool) if fu_is_true "$current"; then next=no; else next=yes; fi ;;
choice) next="$(_fu_next_choice "$current" "$choices" "$step")" ;;
camera) next="$(_fu_next_camera "$current" "$step")" ;;
esac
case "$scope" in
user)
pfu_config_set "$key" "$next" || pfu_bad "$(pfu_msg "Could not save the setting.")"
fu_config_set "$key" "$next" || fu_bad "$(fu_msg "Could not save the setting.")"
;;
sys)
printf '\n'
pfu_ui_cooked pfu_root set "$key" "$next" || pfu_bad "$(pfu_msg "Could not save the setting.")"
PFU_KV_CACHE=()
fu_ui_cooked fu_root set "$key" "$next" || fu_bad "$(fu_msg "Could not save the setting.")"
FU_KV_CACHE=()
;;
pam)
printf '\n'
if [[ $next == yes ]]; then
pfu_ui_cooked pfu_root pam-enable "$key" || pfu_bad "$(pfu_msg "Could not save the setting.")"
fu_ui_cooked fu_root pam-enable "$key" || fu_bad "$(fu_msg "Could not save the setting.")"
else
pfu_ui_cooked pfu_root pam-disable "$key" || pfu_bad "$(pfu_msg "Could not save the setting.")"
fu_ui_cooked fu_root pam-disable "$key" || fu_bad "$(fu_msg "Could not save the setting.")"
fi
# Remembered, so that turning face unlock off and on again brings
# it back.
case "$key" in
sudo) pfu_config_set Sudo "$next" ;;
polkit-1) pfu_config_set Polkit "$next" ;;
sudo) fu_config_set Sudo "$next" ;;
polkit-1) fu_config_set Polkit "$next" ;;
esac
;;
esac
}
# pfu_ui_settings
# fu_ui_settings
# A cursor list in groups, with what the selected setting does under it. The
# frame is assembled in memory and written once, and everything constant is
# resolved before the loop.
pfu_ui_settings() {
fu_ui_settings() {
local -a scopes=() keys=() types=() defaults=() labels=() widths=() choices=() needs=() values=() rows=()
local spec scope key type default label choice need locale i j frame row pad dirty=1 cursor=0 shown
local width=0 wrap cols
locale="$(pfu_ui_locale)"
for spec in "${PFU_SETTINGS[@]}"; do
locale="$(fu_ui_locale)"
for spec in "${FU_SETTINGS[@]}"; do
IFS='|' read -r scope key type default label choice need <<< "$spec"
# A heading has its label where the key would be.
[[ $scope == group ]] && label="$key" key=''
scopes+=("$scope"); keys+=("$key"); types+=("$type"); defaults+=("$default")
choices+=("$choice"); needs+=("$need")
pfu_msg_into "$locale" "$label"
labels+=("$PFU_MSG_RESULT")
_pfu_width "$PFU_MSG_RESULT"
widths+=("$PFU_WIDTH")
fu_msg_into "$locale" "$label"
labels+=("$FU_MSG_RESULT")
_fu_width "$FU_MSG_RESULT"
widths+=("$FU_WIDTH")
if [[ $scope != group ]]; then
rows+=($(( ${#keys[@]} - 1 )))
(( PFU_WIDTH > width )) && width=$PFU_WIDTH
(( FU_WIDTH > width )) && width=$FU_WIDTH
fi
done
local count=${#rows[@]}
@@ -491,77 +491,77 @@ pfu_ui_settings() {
rule="${rule// /─}"
local title hint shared l_on l_off
pfu_msg_into "$locale" "Settings"; title="$PFU_MSG_RESULT"
pfu_msg_into "$locale" "↑↓ select ←→ or Space: change q: back"; hint="$PFU_MSG_RESULT"
pfu_msg_into "$locale" "for all users, asks for your password"; shared="$PFU_MSG_RESULT"
pfu_msg_into "$locale" "ON"; l_on="$PFU_MSG_RESULT"
pfu_msg_into "$locale" "OFF"; l_off="$PFU_MSG_RESULT"
fu_msg_into "$locale" "Settings"; title="$FU_MSG_RESULT"
fu_msg_into "$locale" "↑↓ select ←→ or Space: change q: back"; hint="$FU_MSG_RESULT"
fu_msg_into "$locale" "for all users, asks for your password"; shared="$FU_MSG_RESULT"
fu_msg_into "$locale" "ON"; l_on="$FU_MSG_RESULT"
fu_msg_into "$locale" "OFF"; l_off="$FU_MSG_RESULT"
local clearseq
clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J'
pfu_cameras_load
fu_cameras_load
while true; do
if (( dirty )); then
PFU_KV_CACHE=()
FU_KV_CACHE=()
declare -A current=()
for i in "${rows[@]}"; do
_pfu_setting_value "${scopes[i]}" "${keys[i]}" "${defaults[i]}"
values[i]="$PFU_SETTING_VALUE"
current[${keys[i]}]="$PFU_SETTING_VALUE"
_fu_setting_value "${scopes[i]}" "${keys[i]}" "${defaults[i]}"
values[i]="$FU_SETTING_VALUE"
current[${keys[i]}]="$FU_SETTING_VALUE"
done
dirty=0
fi
frame="$clearseq"$'\n'"${PFU_C_BOLD}${PFU_C_BLUE} ${title}${PFU_C_RESET}"$'\n'
frame="$clearseq"$'\n'"${FU_C_BOLD}${FU_C_BLUE} ${title}${FU_C_RESET}"$'\n'
local selected=${rows[cursor]} marker before after dim
for i in "${!keys[@]}"; do
if [[ ${scopes[i]} == group ]]; then
frame+=$'\n'" ${PFU_C_BOLD}${labels[i]}${PFU_C_RESET}"
frame+=$'\n'" ${FU_C_BOLD}${labels[i]}${FU_C_RESET}"
# The next row says whose settings these are.
[[ ${scopes[i + 1]} != user ]] && frame+=" ${PFU_C_DIM}${shared}${PFU_C_RESET}"
[[ ${scopes[i + 1]} != user ]] && frame+=" ${FU_C_DIM}${shared}${FU_C_RESET}"
frame+=$'\n'
continue
fi
case "${types[i]}" in
bool)
if pfu_is_true "${values[i]}"; then
shown="${PFU_C_GREEN}${l_on}${PFU_C_RESET}"
if fu_is_true "${values[i]}"; then
shown="${FU_C_GREEN}${l_on}${FU_C_RESET}"
else
shown="${PFU_C_DIM}${l_off}${PFU_C_RESET}"
shown="${FU_C_DIM}${l_off}${FU_C_RESET}"
fi
;;
camera) shown="$(_pfu_camera_label "${values[i]}")" ;;
*) shown="$(pfu_value_label "${keys[i]}" "${values[i]}")" ;;
camera) shown="$(_fu_camera_label "${values[i]}")" ;;
*) shown="$(fu_value_label "${keys[i]}" "${values[i]}")" ;;
esac
# Arrows on the selected choice: left and right step through it.
before=' ' after=''
if (( i == selected )) && [[ ${types[i]} != bool ]]; then
before="${PFU_C_BLUE}◂${PFU_C_RESET} " after=" ${PFU_C_BLUE}▸${PFU_C_RESET}"
before="${FU_C_BLUE}◂${FU_C_RESET} " after=" ${FU_C_BLUE}▸${FU_C_RESET}"
fi
dim=''
[[ -n ${needs[i]} ]] && ! pfu_is_true "${current[${needs[i]}]}" && dim="$PFU_C_DIM"
[[ -n ${needs[i]} ]] && ! fu_is_true "${current[${needs[i]}]}" && dim="$FU_C_DIM"
pad=$(( width + 2 - widths[i] ))
if (( i == selected )); then marker="${PFU_C_BLUE}▸${PFU_C_RESET} "; else marker=' '; fi
printf -v row ' %s%s%s%s%*s%s%s%s' "$marker" "$dim" "${labels[i]}" "$PFU_C_RESET" "$pad" '' "$before" "$dim$shown$PFU_C_RESET" "$after"
if (( i == selected )); then marker="${FU_C_BLUE}▸${FU_C_RESET} "; else marker=' '; fi
printf -v row ' %s%s%s%s%*s%s%s%s' "$marker" "$dim" "${labels[i]}" "$FU_C_RESET" "$pad" '' "$before" "$dim$shown$FU_C_RESET" "$after"
frame+="$row"$'\n'
done
# What the selected setting does, in a box of fixed height so the
# screen does not jump while moving through the list.
_pfu_wrap "$wrap" "$(pfu_setting_help "${keys[selected]}" "${values[selected]}")"
frame+=$'\n'" ${PFU_C_DIM}${rule}${PFU_C_RESET}"$'\n'
_fu_wrap "$wrap" "$(fu_setting_help "${keys[selected]}" "${values[selected]}")"
frame+=$'\n'" ${FU_C_DIM}${rule}${FU_C_RESET}"$'\n'
for j in 0 1 2; do
frame+=" ${PFU_WRAP_LINES[j]:-}"$'\n'
frame+=" ${FU_WRAP_LINES[j]:-}"$'\n'
done
frame+=$'\n'" ${PFU_C_DIM}${hint}${PFU_C_RESET}"$'\n'
_pfu_ui_take_notices
frame+="$PFU_UI_NOTICE_TEXT"
frame+=$'\n'" ${FU_C_DIM}${hint}${FU_C_RESET}"$'\n'
_fu_ui_take_notices
frame+="$FU_UI_NOTICE_TEXT"
printf '%s' "$frame"
key="$(pfu_read_key)" || return 0
key="$(fu_read_key)" || return 0
case "$key" in
up|k) cursor=$(( (cursor - 1 + count) % count )) ;;
@@ -569,7 +569,7 @@ pfu_ui_settings() {
space|enter|right|l|left|h)
local step=1
[[ $key == left || $key == h ]] && step=-1
_pfu_setting_change "${scopes[selected]}" "${keys[selected]}" "${types[selected]}" "${values[selected]}" "${choices[selected]}" "$step"
_fu_setting_change "${scopes[selected]}" "${keys[selected]}" "${types[selected]}" "${values[selected]}" "${choices[selected]}" "$step"
dirty=1
;;
q|Q|escape) return 0 ;;
@@ -582,72 +582,72 @@ pfu_ui_settings() {
# Faces
# ---------------------------------------------------------------------------
pfu_ui_faces() {
fu_ui_faces() {
local key frame row pad i cursor=0 count locale shown
locale="$(pfu_ui_locale)"
locale="$(fu_ui_locale)"
local title hint empty l_on l_off
pfu_msg_into "$locale" "Faces"; title="$PFU_MSG_RESULT"
fu_msg_into "$locale" "Faces"; title="$FU_MSG_RESULT"
# TRANSLATORS: keep the letters, they are the keys.
pfu_msg_into "$locale" "Up/Down: select, Space: on/off, r: rename, d: delete, a: add, q: back"; hint="$PFU_MSG_RESULT"
pfu_msg_into "$locale" "No face is set up yet. Press a to add one."; empty="$PFU_MSG_RESULT"
pfu_msg_into "$locale" "on"; l_on="$PFU_MSG_RESULT"
pfu_msg_into "$locale" "off"; l_off="$PFU_MSG_RESULT"
fu_msg_into "$locale" "Up/Down: select, Space: on/off, r: rename, d: delete, a: add, q: back"; hint="$FU_MSG_RESULT"
fu_msg_into "$locale" "No face is set up yet. Press a to add one."; empty="$FU_MSG_RESULT"
fu_msg_into "$locale" "on"; l_on="$FU_MSG_RESULT"
fu_msg_into "$locale" "off"; l_off="$FU_MSG_RESULT"
local clearseq
clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J'
while true; do
pfu_faces_load
count=${#PFU_FACE_IDS[@]}
fu_faces_load
count=${#FU_FACE_IDS[@]}
(( cursor >= count )) && cursor=$(( count > 0 ? count - 1 : 0 ))
frame="$clearseq"$'\n'"${PFU_C_BOLD}${PFU_C_BLUE} ${title}${PFU_C_RESET}"$'\n\n'
frame="$clearseq"$'\n'"${FU_C_BOLD}${FU_C_BLUE} ${title}${FU_C_RESET}"$'\n\n'
if (( count == 0 )); then
frame+=" ${PFU_C_DIM}${empty}${PFU_C_RESET}"$'\n'
frame+=" ${FU_C_DIM}${empty}${FU_C_RESET}"$'\n'
fi
local marker selected="${PFU_C_BLUE}▸${PFU_C_RESET} " samples
for i in "${!PFU_FACE_IDS[@]}"; do
if [[ ${PFU_FACE_ON[i]} == true ]]; then shown="${PFU_C_GREEN}${l_on}${PFU_C_RESET}"; else shown="${PFU_C_DIM}${l_off}${PFU_C_RESET}"; fi
samples="$(pfu_msg "%s samples, %s learned" "${PFU_FACE_SAMPLES[i]}" "${PFU_FACE_LEARNED[i]}")"
_pfu_width "${PFU_FACE_NAMES[i]}"
pad=$(( 30 - PFU_WIDTH ))
local marker selected="${FU_C_BLUE}▸${FU_C_RESET} " samples
for i in "${!FU_FACE_IDS[@]}"; do
if [[ ${FU_FACE_ON[i]} == true ]]; then shown="${FU_C_GREEN}${l_on}${FU_C_RESET}"; else shown="${FU_C_DIM}${l_off}${FU_C_RESET}"; fi
samples="$(fu_msg "%s samples, %s learned" "${FU_FACE_SAMPLES[i]}" "${FU_FACE_LEARNED[i]}")"
_fu_width "${FU_FACE_NAMES[i]}"
pad=$(( 30 - FU_WIDTH ))
(( pad < 0 )) && pad=0
if (( i == cursor )); then marker="$selected"; else marker=' '; fi
printf -v row ' %s%s%*s %s %s%s%s' "$marker" "${PFU_FACE_NAMES[i]}" "$pad" '' "$shown" "$PFU_C_DIM" "$samples" "$PFU_C_RESET"
printf -v row ' %s%s%*s %s %s%s%s' "$marker" "${FU_FACE_NAMES[i]}" "$pad" '' "$shown" "$FU_C_DIM" "$samples" "$FU_C_RESET"
frame+="$row"$'\n'
done
frame+=$'\n'" ${PFU_C_DIM}${hint}${PFU_C_RESET}"$'\n'
_pfu_ui_take_notices
frame+="$PFU_UI_NOTICE_TEXT"
frame+=$'\n'" ${FU_C_DIM}${hint}${FU_C_RESET}"$'\n'
_fu_ui_take_notices
frame+="$FU_UI_NOTICE_TEXT"
printf '%s' "$frame"
key="$(pfu_read_key)" || return 0
key="$(fu_read_key)" || return 0
case "$key" in
up|k) (( count )) && cursor=$(( (cursor - 1 + count) % count )) ;;
down|j) (( count )) && cursor=$(( (cursor + 1) % count )) ;;
space|enter)
(( count )) || continue
if [[ ${PFU_FACE_ON[cursor]} == true ]]; then
pfu_ctl disable "${PFU_FACE_IDS[cursor]}" > /dev/null
if [[ ${FU_FACE_ON[cursor]} == true ]]; then
fu_ctl disable "${FU_FACE_IDS[cursor]}" > /dev/null
else
pfu_ctl enable "${PFU_FACE_IDS[cursor]}" > /dev/null
fu_ctl enable "${FU_FACE_IDS[cursor]}" > /dev/null
fi
;;
r|R)
(( count )) || continue
printf '\n %s ' "$(pfu_msg "New name:")"
if pfu_ui_read_line "${PFU_FACE_NAMES[cursor]}" && [[ -n $PFU_LINE_RESULT ]]; then
pfu_ctl rename "${PFU_FACE_IDS[cursor]}" "$PFU_LINE_RESULT" > /dev/null
printf '\n %s ' "$(fu_msg "New name:")"
if fu_ui_read_line "${FU_FACE_NAMES[cursor]}" && [[ -n $FU_LINE_RESULT ]]; then
fu_ctl rename "${FU_FACE_IDS[cursor]}" "$FU_LINE_RESULT" > /dev/null
fi
;;
d|D)
(( count )) || continue
if pfu_ui_confirm "$(pfu_msg "Delete \"%s\"?" "${PFU_FACE_NAMES[cursor]}")"; then
pfu_ctl remove "${PFU_FACE_IDS[cursor]}" > /dev/null
if fu_ui_confirm "$(fu_msg "Delete \"%s\"?" "${FU_FACE_NAMES[cursor]}")"; then
fu_ctl remove "${FU_FACE_IDS[cursor]}" > /dev/null
fi
;;
a|A) pfu_ui_cooked pfu_do_setup ;;
a|A) fu_ui_cooked fu_do_setup ;;
q|Q|escape) return 0 ;;
*) ;;
esac
@@ -658,11 +658,11 @@ pfu_ui_faces() {
# The menu
# ---------------------------------------------------------------------------
pfu_ui_menu() {
fu_ui_menu() {
local choice keep=0
pfu_ui_term_raw
trap 'pfu_ui_term_restore' EXIT INT TERM
fu_ui_term_raw
trap 'fu_ui_term_restore' EXIT INT TERM
while true; do
# After a test the menu goes under what the camera saw instead.
@@ -671,21 +671,21 @@ pfu_ui_menu() {
else
clear 2>/dev/null || true
fi
pfu_head " $PFU_PRETTY"
pfu_ui_status
fu_head " $FU_PRETTY"
fu_ui_status
printf '\n'
printf ' [1] %s\n' "$(pfu_msg "Turn face unlock on or off")"
printf ' [2] %s\n' "$(pfu_msg "Add a face")"
printf ' [3] %s\n' "$(pfu_msg "Faces")"
printf ' [4] %s\n' "$(pfu_msg "Settings")"
printf ' [5] %s\n' "$(pfu_msg "Try it")"
printf ' [q] %s\n' "$(pfu_msg "Quit")"
_pfu_ui_take_notices
printf '%s' "$PFU_UI_NOTICE_TEXT"
printf ' [1] %s\n' "$(fu_msg "Turn face unlock on or off")"
printf ' [2] %s\n' "$(fu_msg "Add a face")"
printf ' [3] %s\n' "$(fu_msg "Faces")"
printf ' [4] %s\n' "$(fu_msg "Settings")"
printf ' [5] %s\n' "$(fu_msg "Try it")"
printf ' [q] %s\n' "$(fu_msg "Quit")"
_fu_ui_take_notices
printf '%s' "$FU_UI_NOTICE_TEXT"
printf '\n > '
choice="$(pfu_read_key)" || {
printf '\n'; pfu_ui_term_restore; trap - EXIT INT TERM; return 0
choice="$(fu_read_key)" || {
printf '\n'; fu_ui_term_restore; trap - EXIT INT TERM; return 0
}
case "$choice" in
enter|space|up|down|left|right|escape) choice='' ;;
@@ -694,24 +694,24 @@ pfu_ui_menu() {
case "$choice" in
1)
pfu_config_load
fu_config_load
if [[ $CFG_ENABLED == yes ]]; then
pfu_ui_cooked pfu_do_disable
fu_ui_cooked fu_do_disable
else
pfu_ui_cooked pfu_do_enable
fu_ui_cooked fu_do_enable
fi
;;
2) pfu_ui_cooked pfu_do_setup ;;
3) pfu_ui_faces ;;
4) pfu_ui_settings ;;
2) fu_ui_cooked fu_do_setup ;;
3) fu_ui_faces ;;
4) fu_ui_settings ;;
5)
printf '\n'
pfu_ui_cooked pfu_test
fu_ui_cooked fu_test
# Already on screen, with the rest of the test.
PFU_UI_NOTICES=()
FU_UI_NOTICES=()
keep=1
;;
q|Q) pfu_ui_term_restore; trap - EXIT INT TERM; return 0 ;;
q|Q) fu_ui_term_restore; trap - EXIT INT TERM; return 0 ;;
# Anything else (Enter, arrow keys, stray characters) just
# redraws. Escape is deliberately not a quit key, so a mistyped
# arrow key cannot close the menu.
+49 -49
View File
@@ -10,7 +10,7 @@
#
# The line that goes in:
#
# -auth sufficient /usr/lib/security/pam_plasma_face_unlock.so
# -auth sufficient /usr/lib/security/pam_face_unlock.so
#
# "sufficient": a match lets the person in, anything else falls through to the
# lines below as if this one were not there. The dash makes PAM skip it
@@ -26,24 +26,24 @@
# an update to that file still counts.
# Undoing takes out exactly those lines, or that file.
PFU_PAM_MARK="# plasma-face-unlock: the face first, the password if that does not work"
PFU_PAM_WRAPPER_MARK="# Written by plasma-face-unlock."
PFU_PAM_SERVICES=(sudo polkit-1)
FU_PAM_MARK="# face-unlock: the face first, the password if that does not work"
FU_PAM_WRAPPER_MARK="# Written by face-unlock."
FU_PAM_SERVICES=(sudo polkit-1)
# Overridable for the tests only; the root side never takes them from the
# environment (see the top of plasma-face-unlock).
PFU_PAM_ETC_DIR="${PFU_PAM_ETC_DIR:-/etc/pam.d}"
read -r -a PFU_PAM_VENDOR_DIRS <<< "${PFU_PAM_VENDOR_DIRS:-/usr/lib/pam.d /usr/share/pam.d /lib/pam.d}"
# environment (see the top of face-unlock).
FU_PAM_ETC_DIR="${FU_PAM_ETC_DIR:-/etc/pam.d}"
read -r -a FU_PAM_VENDOR_DIRS <<< "${FU_PAM_VENDOR_DIRS:-/usr/lib/pam.d /usr/share/pam.d /lib/pam.d}"
pfu_pam_etc() {
printf '%s/%s\n' "$PFU_PAM_ETC_DIR" "$1"
fu_pam_etc() {
printf '%s/%s\n' "$FU_PAM_ETC_DIR" "$1"
}
# pfu_pam_vendor <service>
# fu_pam_vendor <service>
# The distribution's own copy, when it keeps one outside /etc.
pfu_pam_vendor() {
fu_pam_vendor() {
local dir
for dir in "${PFU_PAM_VENDOR_DIRS[@]}"; do
for dir in "${FU_PAM_VENDOR_DIRS[@]}"; do
if [[ -f $dir/$1 ]]; then
printf '%s\n' "$dir/$1"
return 0
@@ -52,24 +52,24 @@ pfu_pam_vendor() {
return 1
}
pfu_pam_line() {
printf -- '-auth sufficient %s\n' "$PFU_PAM_MODULE"
fu_pam_line() {
printf -- '-auth sufficient %s\n' "$FU_PAM_MODULE"
}
# pfu_pam_enabled <service>
pfu_pam_enabled() {
# fu_pam_enabled <service>
fu_pam_enabled() {
local file
file="$(pfu_pam_etc "$1")"
[[ -r $file ]] && grep -q 'pam_plasma_face_unlock\.so' "$file"
file="$(fu_pam_etc "$1")"
[[ -r $file ]] && grep -q 'pam_face_unlock\.so' "$file"
}
# pfu_pam_insert <file>
# fu_pam_insert <file>
# Prints the file with the line added before its first auth line. Debian and
# Ubuntu have none in sudo's file, only "@include common-auth", and that
# counts as one: after it the password has already been asked for. A file
# with neither (which would be odd for either service) gets it at the end.
pfu_pam_insert() {
awk -v mark="$PFU_PAM_MARK" -v line="$(pfu_pam_line)" '
fu_pam_insert() {
awk -v mark="$FU_PAM_MARK" -v line="$(fu_pam_line)" '
!done && ($0 ~ /^[[:space:]]*-?auth[[:space:]]/ || $0 ~ /^[[:space:]]*@include[[:space:]]+common-auth([[:space:]]|$)/) {
print mark
print line
@@ -85,35 +85,35 @@ pfu_pam_insert() {
' "$1"
}
# pfu_pam_remove_lines <file>
# fu_pam_remove_lines <file>
# Prints the file without our comment and our line.
pfu_pam_remove_lines() {
awk -v mark="$PFU_PAM_MARK" '
fu_pam_remove_lines() {
awk -v mark="$FU_PAM_MARK" '
$0 == mark { next }
/pam_plasma_face_unlock\.so/ { next }
/pam_face_unlock\.so/ { next }
{ print }
' "$1"
}
pfu_pam_wrapper() {
fu_pam_wrapper() {
local vendor="$1"
printf '#%%PAM-1.0\n'
printf '%s The face first, then everything\n' "$PFU_PAM_WRAPPER_MARK"
printf '%s The face first, then everything\n' "$FU_PAM_WRAPPER_MARK"
printf '# %s does for this service. Removed again by\n' "$vendor"
printf '# "plasma-face-unlock disable" or from its settings.\n\n'
pfu_pam_line
printf '# "face-unlock disable" or from its settings.\n\n'
fu_pam_line
printf 'auth include %s\n' "$vendor"
printf 'account include %s\n' "$vendor"
printf 'password include %s\n' "$vendor"
printf 'session include %s\n' "$vendor"
}
# _pfu_pam_replace <file> <content>
# _fu_pam_replace <file> <content>
# Writes the new file next to the old one and swaps it in, with the old one's
# owner and mode. A half-written PAM file is a locked-out machine.
_pfu_pam_replace() {
_fu_pam_replace() {
local file="$1" content="$2" tmp
tmp="$(mktemp "${file}.pfu.XXXXXX")" || return 1
tmp="$(mktemp "${file}.fu.XXXXXX")" || return 1
printf '%s\n' "$content" > "$tmp" || { rm -f "$tmp"; return 1; }
if [[ -e $file ]]; then
chown --reference="$file" "$tmp" 2>/dev/null
@@ -124,40 +124,40 @@ _pfu_pam_replace() {
mv -f "$tmp" "$file"
}
# pfu_pam_enable <service> (root)
pfu_pam_enable() {
# fu_pam_enable <service> (root)
fu_pam_enable() {
local service="$1" file vendor content
file="$(pfu_pam_etc "$service")"
file="$(fu_pam_etc "$service")"
[[ -e $PFU_PAM_MODULE ]] || { pfu_bad "$(pfu_msg "The PAM module is not installed at %s." "$PFU_PAM_MODULE")"; return 1; }
pfu_pam_enabled "$service" && return 0
[[ -e $FU_PAM_MODULE ]] || { fu_bad "$(fu_msg "The PAM module is not installed at %s." "$FU_PAM_MODULE")"; return 1; }
fu_pam_enabled "$service" && return 0
if [[ -f $file ]]; then
content="$(pfu_pam_insert "$file")" || return 1
elif vendor="$(pfu_pam_vendor "$service")"; then
content="$(pfu_pam_wrapper "$vendor")"
content="$(fu_pam_insert "$file")" || return 1
elif vendor="$(fu_pam_vendor "$service")"; then
content="$(fu_pam_wrapper "$vendor")"
else
pfu_bad "$(pfu_msg "There is no PAM configuration for %s on this system." "$service")"
fu_bad "$(fu_msg "There is no PAM configuration for %s on this system." "$service")"
return 1
fi
_pfu_pam_replace "$file" "$content"
_fu_pam_replace "$file" "$content"
}
# pfu_pam_disable <service> (root)
pfu_pam_disable() {
# fu_pam_disable <service> (root)
fu_pam_disable() {
local service="$1" file content
file="$(pfu_pam_etc "$service")"
file="$(fu_pam_etc "$service")"
pfu_pam_enabled "$service" || return 0
fu_pam_enabled "$service" || return 0
if head -n 3 "$file" | grep -qF "$PFU_PAM_WRAPPER_MARK"; then
if head -n 3 "$file" | grep -qF "$FU_PAM_WRAPPER_MARK"; then
# Ours from the first line to the last. Without it the distribution's
# own copy is in charge again.
rm -f -- "$file"
return
fi
content="$(pfu_pam_remove_lines "$file")" || return 1
_pfu_pam_replace "$file" "$content"
content="$(fu_pam_remove_lines "$file")" || return 1
_fu_pam_replace "$file" "$content"
}
+34 -34
View File
@@ -8,17 +8,17 @@
# 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 <Key> <Value> one line of /etc/plasma-face-unlock/config
# --root set <Key> <Value> one line of /etc/face-unlock/config
# --root pam-enable <service> sudo or polkit-1, see pam.sh
# --root pam-disable <service>
# --root socket-enable start the daemon's socket, and at boot
# --root socket-disable
PFU_SELF="${PFU_SELF:-$(readlink -f "${BASH_SOURCE[1]:-$0}")}"
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 PFU_SYS_VALID=(
declare -A FU_SYS_VALID=(
[Camera]='^(auto|/dev/video[0-9]+)$'
[Liveness]='^(off|light|heavy)$'
[Strictness]='^(relaxed|normal|strict)$'
@@ -30,63 +30,63 @@ declare -A PFU_SYS_VALID=(
[LockoutMinutes]='^[1-9][0-9]{0,3}$'
)
# pfu_root <verb> [args...]
pfu_root() {
# fu_root <verb> [args...]
fu_root() {
local candidate
if [[ $EUID -eq 0 ]]; then
pfu_root_verb "$@"
fu_root_verb "$@"
return
fi
for candidate in sudo run0 doas; do
if pfu_have "$candidate"; then
"$candidate" "$PFU_SELF" --root "$@"
if fu_have "$candidate"; then
"$candidate" "$FU_SELF" --root "$@"
return
fi
done
pfu_bad "$(pfu_msg "This needs root, and neither sudo, run0 nor doas is installed.")"
fu_bad "$(fu_msg "This needs root, and neither sudo, run0 nor doas is installed.")"
return 1
}
pfu_root_verb() {
fu_root_verb() {
local verb="${1:-}"
[[ $# -gt 0 ]] && shift
if [[ $EUID -ne 0 ]]; then
pfu_bad "$(pfu_msg "This command has to run as root.")"
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 ${PFU_SYS_VALID[$key]+set} || ! $value =~ ${PFU_SYS_VALID[$key]} ]]; then
pfu_bad "$(pfu_msg "Not a valid setting: %s=%s" "$key" "$value")"
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
pfu_kv_set "$PFU_SYSCONFIG" "$key" "$value" "$PFU_PRETTY (system settings)" || return 1
chmod 0644 "$PFU_SYSCONFIG"
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
for s in "${PFU_PAM_SERVICES[@]}"; do
for s in "${FU_PAM_SERVICES[@]}"; do
[[ $s == "$service" ]] && known=1
done
(( known )) || { pfu_bad "$(pfu_msg "Not a service this can be used for: %s" "$service")"; return 1; }
(( known )) || { fu_bad "$(fu_msg "Not a service this can be used for: %s" "$service")"; return 1; }
if [[ $verb == pam-enable ]]; then
pfu_pam_enable "$service"
fu_pam_enable "$service"
else
pfu_pam_disable "$service"
fu_pam_disable "$service"
fi
;;
socket-enable)
systemctl enable --now "$PFU_UNIT_SOCKET" > /dev/null 2>&1
systemctl enable --now "$FU_UNIT_SOCKET" > /dev/null 2>&1
;;
socket-disable)
systemctl disable --now "$PFU_UNIT_SOCKET" > /dev/null 2>&1
systemctl disable --now "$FU_UNIT_SOCKET" > /dev/null 2>&1
;;
*)
pfu_bad "$(pfu_msg "Unknown command: %s" "$verb")"
fu_bad "$(fu_msg "Unknown command: %s" "$verb")"
return 1
;;
esac
@@ -96,27 +96,27 @@ pfu_root_verb() {
# The two services
# ---------------------------------------------------------------------------
pfu_socket_enabled() {
systemctl is-enabled --quiet "$PFU_UNIT_SOCKET" 2>/dev/null
fu_socket_enabled() {
systemctl is-enabled --quiet "$FU_UNIT_SOCKET" 2>/dev/null
}
pfu_agent_available() {
pfu_have systemctl && [[ -n ${XDG_RUNTIME_DIR:-} ]]
fu_agent_available() {
fu_have systemctl && [[ -n ${XDG_RUNTIME_DIR:-} ]]
}
pfu_agent_enabled() {
systemctl --user is-enabled --quiet "$PFU_UNIT_AGENT" 2>/dev/null
fu_agent_enabled() {
systemctl --user is-enabled --quiet "$FU_UNIT_AGENT" 2>/dev/null
}
pfu_agent_running() {
systemctl --user is-active --quiet "$PFU_UNIT_AGENT" 2>/dev/null
fu_agent_running() {
systemctl --user is-active --quiet "$FU_UNIT_AGENT" 2>/dev/null
}
pfu_agent_enable() {
fu_agent_enable() {
systemctl --user daemon-reload > /dev/null 2>&1 || true
systemctl --user enable --now "$PFU_UNIT_AGENT" > /dev/null 2>&1
systemctl --user enable --now "$FU_UNIT_AGENT" > /dev/null 2>&1
}
pfu_agent_disable() {
systemctl --user disable --now "$PFU_UNIT_AGENT" > /dev/null 2>&1 || true
fu_agent_disable() {
systemctl --user disable --now "$FU_UNIT_AGENT" > /dev/null 2>&1 || true
}
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// pam_plasma_face_unlock: face unlock for sudo and for admin prompts.
// pam_face_unlock: face unlock for sudo and for admin prompts.
//
// All the vision is in the daemon. This asks it to scan for the user and
// turns the answer into a PAM result, and it is meant to sit first in a stack
@@ -49,7 +49,7 @@
#include <systemd/sd-login.h>
#endif
#define DOMAIN PFU_NAME
#define DOMAIN FU_NAME
// Marks a message for translation. say() translates it.
#define _(s) (s)
@@ -62,7 +62,7 @@ struct options {
static void parse_options(struct options *o, int argc, const char **argv)
{
o->socket = PFU_SOCKET;
o->socket = FU_SOCKET;
o->purpose = NULL;
o->timeout = 25;
o->debug = false;
@@ -229,7 +229,7 @@ __attribute__((visibility("default"))) PAM_EXTERN int pam_sm_authenticate(pam_ha
{
struct options o;
parse_options(&o, argc, argv);
bindtextdomain(DOMAIN, PFU_LOCALEDIR);
bindtextdomain(DOMAIN, FU_LOCALEDIR);
const char *user = NULL;
if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS || !nonempty(user)) {
-229
View File
@@ -1,229 +0,0 @@
#!/usr/bin/env bash
#
# plasma-face-unlock: face unlock for KDE Plasma
#
# Look at the screen and it unlocks, the way a phone does. The lock screen,
# sudo in a terminal and the admin password prompts can all take a face
# instead of a password, with a check that it is a face and not a photo of one.
#
# This is the front end: the menu and the commands. The camera, the face data
# and the decision are the daemon's (plasma-face-unlockd, running as root),
# the lock screen and the bubble at the top of the screen are the agent's
# (plasma-face-unlock-agent, in the session), and sudo and polkit reach the
# daemon through a PAM module. See the man page for how the pieces fit.
#
# Copyright (C) 2026 Felitendo
# SPDX-License-Identifier: GPL-3.0-or-later
set -uo pipefail
PFU_SELF="$(readlink -f "${BASH_SOURCE[0]}")"
# Run as root (through sudo from the menu), only what was installed counts.
# An environment that points the libraries somewhere else is ignored then.
if [[ $EUID -eq 0 ]]; then
unset PFU_LIBDIR PFU_LIBEXECDIR PFU_LOCALEDIR PFU_PAMDIR PFU_CTL PFU_AGENT PFU_PAM_MODULE PFU_SYSCONFIG \
PFU_PAM_ETC_DIR PFU_PAM_VENDOR_DIRS
fi
PFU_LIBDIR="${PFU_LIBDIR:-@LIBDIR@}"
for _mod in common config daemon pam system menu; do
# shellcheck source=/dev/null
if ! source "$PFU_LIBDIR/$_mod.sh"; then
printf 'plasma-face-unlock: cannot load %s/%s.sh\n' "$PFU_LIBDIR" "$_mod" >&2
exit 14
fi
done
unset _mod
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
# The daemon is started by its socket. Enabling the socket is the one thing
# that needs root once per machine.
pfu_ensure_service() {
pfu_status_load && return 0
if ! pfu_socket_enabled; then
pfu_say " $(pfu_msg "Face unlock's service has to be switched on once for this computer. That needs your password.")"
pfu_root socket-enable || return 1
sleep 0.3
fi
pfu_status_load && return 0
pfu_bad "$(pfu_reason_text unreachable)"
pfu_note "$(pfu_msg "Check it with: systemctl status %s" "$PFU_UNIT_SOCKET")"
return 1
}
pfu_do_setup() {
local name="${1:-}" rc
pfu_ensure_service || return 1
if [[ -z ${WAYLAND_DISPLAY:-} && -z ${DISPLAY:-} ]]; then
pfu_bad "$(pfu_msg "Setting up a face needs the camera picture on screen. Run this inside your Plasma session.")"
return 1
fi
pfu_say " $(pfu_msg "The setup window is open. Follow it there.")"
if [[ -n $name ]]; then
"$PFU_AGENT" --enroll --name "$name" > /dev/null 2>&1
else
"$PFU_AGENT" --enroll > /dev/null 2>&1
fi
rc=$?
if (( rc == 0 )); then
pfu_ok "$(pfu_msg "The face is set up.")"
pfu_config_load
if [[ $CFG_ENABLED != yes ]]; then
pfu_note "$(pfu_msg "Face unlock is still off. Turn it on with [1] or \`%s enable\`." "$PFU_NAME")"
fi
return 0
fi
pfu_note "$(pfu_msg "No face was added.")"
return 1
}
pfu_do_enable() {
pfu_ensure_service || return 1
pfu_faces_load
if (( ${#PFU_FACE_IDS[@]} == 0 )); then
pfu_say " $(pfu_msg "First, set up your face.")"
pfu_do_setup || return 1
fi
pfu_config_set Enabled yes || { pfu_bad "$(pfu_msg "Could not save the setting.")"; return 1; }
pfu_config_load
if pfu_agent_available; then
pfu_agent_enable || pfu_bad "$(pfu_msg "Could not start the lock screen agent.")"
else
pfu_note "$(pfu_msg "No systemd user session, so the lock screen agent was not started.")"
fi
# What was on the last time face unlock was on.
[[ $CFG_SUDO == yes ]] && ! pfu_pam_enabled sudo && pfu_root pam-enable sudo
[[ $CFG_POLKIT == yes ]] && ! pfu_pam_enabled polkit-1 && pfu_root pam-enable polkit-1
pfu_ok "$(pfu_msg "Face unlock is on. Lock the screen and look at it to try.")"
if [[ $CFG_SUDO != yes && $CFG_POLKIT != yes ]]; then
pfu_note "$(pfu_msg "It can do sudo and admin prompts too. See Settings.")"
fi
}
pfu_do_disable() {
pfu_config_set Enabled no || { pfu_bad "$(pfu_msg "Could not save the setting.")"; return 1; }
pfu_agent_available && pfu_agent_disable
local service
for service in "${PFU_PAM_SERVICES[@]}"; do
if pfu_pam_enabled "$service"; then
pfu_root pam-disable "$service" || true
fi
done
pfu_ok "$(pfu_msg "Face unlock is off. Your faces are kept; delete them under Faces.")"
}
pfu_do_status() {
pfu_head " $PFU_PRETTY"
pfu_ui_status
printf '\n'
}
pfu_do_faces() {
local i state
pfu_status_load || { pfu_bad "$(pfu_reason_text unreachable)"; return 1; }
pfu_faces_load
if (( ${#PFU_FACE_IDS[@]} == 0 )); then
pfu_note "$(pfu_msg "No face is set up yet.")"
return 0
fi
for i in "${!PFU_FACE_IDS[@]}"; do
if [[ ${PFU_FACE_ON[i]} == true ]]; then state="$(pfu_msg "on")"; else state="$(pfu_msg "off")"; fi
printf ' %s %-24s %-4s %s\n' "${PFU_FACE_IDS[i]}" "${PFU_FACE_NAMES[i]}" "$state" \
"$(pfu_msg "%s samples, %s learned" "${PFU_FACE_SAMPLES[i]}" "${PFU_FACE_LEARNED[i]}")"
done
}
pfu_do_remove() {
local id="${1:-}" line
[[ -n $id ]] || { pfu_bad "$(pfu_msg "Which face? See \`%s faces\` for the ids." "$PFU_NAME")"; return 1; }
line="$(pfu_ctl remove "$id" | tail -n1)"
_pfu_fields "$line"
if [[ ${PFU_F[ok]:-} == true ]]; then
pfu_ok "$(pfu_msg "Deleted.")"
else
pfu_bad "$(pfu_reason_text "${PFU_F[reason]:-unreachable}")"
return 1
fi
}
pfu_do_help() {
cat <<- EOF
$PFU_PRETTY $PFU_VERSION
$(pfu_msg "Usage: plasma-face-unlock [command]")
$(pfu_msg "Commands:")
enable $(pfu_msg "Turn face unlock on")
disable $(pfu_msg "Turn it off (faces are kept)")
setup [NAME] $(pfu_msg "Add a face")
faces $(pfu_msg "List the faces")
remove ID $(pfu_msg "Delete a face")
test $(pfu_msg "Look at the camera and see what it sees")
status $(pfu_msg "Show what is on")
-h, --help $(pfu_msg "Show this help")
-V, --version $(pfu_msg "Show the version")
$(pfu_msg "Without a command an interactive menu is shown.")
EOF
}
# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
main() {
local cmd="${1:-}"
[[ $# -gt 0 ]] && shift
case "$cmd" in
--root) pfu_root_verb "$@"; return ;;
esac
# Face data and settings are per user; root has neither a lock screen
# nor a face of its own to set up.
if [[ $EUID -eq 0 && -z ${PFU_ALLOW_ROOT:-} ]]; then
pfu_bad "$(pfu_msg "Run this as your own user, not as root. It asks for your password when it needs to.")"
exit 2
fi
case "$cmd" in
enable) pfu_do_enable ;;
disable) pfu_do_disable ;;
setup|add|enroll) pfu_do_setup "$@" ;;
faces|list) pfu_do_faces ;;
remove|delete) pfu_do_remove "$@" ;;
test|try) pfu_ensure_service && pfu_test ;;
status) pfu_do_status ;;
-h|--help|help) pfu_do_help ;;
-V|--version) printf '%s %s\n' "$PFU_NAME" "$PFU_VERSION" ;;
'') pfu_ui_menu ;;
*)
pfu_bad "$(pfu_msg "Unknown command: %s" "$cmd")"
printf '\n'
pfu_do_help
exit 1
;;
esac
}
main "$@"
+8 -8
View File
@@ -2,11 +2,11 @@
// rest of the installation is.
#pragma once
#define PFU_VERSION "@PFU_VERSION@"
#define PFU_NAME "plasma-face-unlock"
#define PFU_LIBEXECDIR "@PFU_LIBEXECDIR@"
#define PFU_MODELDIR "@PFU_MODELDIR@"
#define PFU_LOCALEDIR "@PFU_LOCALEDIR@"
#define PFU_SOCKET "@PFU_SOCKET@"
#define PFU_STATEDIR "@PFU_STATEDIR@"
#define PFU_CONFIG "@PFU_CONFIG@"
#define FU_VERSION "@FU_VERSION@"
#define FU_NAME "face-unlock"
#define FU_LIBEXECDIR "@FU_LIBEXECDIR@"
#define FU_MODELDIR "@FU_MODELDIR@"
#define FU_LOCALEDIR "@FU_LOCALEDIR@"
#define FU_SOCKET "@FU_SOCKET@"
#define FU_STATEDIR "@FU_STATEDIR@"
#define FU_CONFIG "@FU_CONFIG@"