feat: add an own lock screen with the bubble and your wallpaper
This commit is contained in:
1 parent
c693ca216c
commit
c228291520
44 files changed
+3891
-1033
No files matched your search
@@ -26,7 +26,7 @@ AgentSocket::AgentSocket(QObject *parent)
|
||||
continue;
|
||||
}
|
||||
auto buffer = std::make_shared<QByteArray>();
|
||||
connect(socket, &QLocalSocket::readyRead, this, [this, socket, buffer] {
|
||||
connect(socket, &QLocalSocket::readyRead, this, [this, socket, buffer, cred] {
|
||||
*buffer += socket->readAll();
|
||||
if (buffer->size() > 64 * 1024) {
|
||||
socket->abort();
|
||||
@@ -36,8 +36,12 @@ AgentSocket::AgentSocket(QObject *parent)
|
||||
while ((nl = buffer->indexOf('\n')) >= 0) {
|
||||
const QJsonObject o = QJsonDocument::fromJson(buffer->left(nl)).object();
|
||||
buffer->remove(0, nl + 1);
|
||||
if (o.value(u"event").toString() == u"scan") {
|
||||
const QString what = o.value(u"event").toString();
|
||||
if (what == u"scan") {
|
||||
Q_EMIT scanEvent(o);
|
||||
} else if (what == u"lock" && cred.uid == ::getuid()) {
|
||||
m_waiting.append(socket);
|
||||
Q_EMIT lockRequested();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -58,6 +62,37 @@ bool AgentSocket::running()
|
||||
return probe.waitForConnected(500);
|
||||
}
|
||||
|
||||
bool AgentSocket::requestLock()
|
||||
{
|
||||
QLocalSocket socket;
|
||||
socket.connectToServer(path());
|
||||
if (!socket.waitForConnected(500)) {
|
||||
return false;
|
||||
}
|
||||
socket.write(QJsonDocument(QJsonObject{{QStringLiteral("event"), QStringLiteral("lock")}}).toJson(QJsonDocument::Compact) + '\n');
|
||||
if (!socket.waitForBytesWritten(500)) {
|
||||
return false;
|
||||
}
|
||||
// The answer comes once the compositor has the lock, so that an idle
|
||||
// daemon that locks before sleep does not suspend an unlocked screen.
|
||||
QByteArray answer;
|
||||
while (!answer.contains('\n') && socket.waitForReadyRead(5000)) {
|
||||
answer += socket.readAll();
|
||||
}
|
||||
return QJsonDocument::fromJson(answer.left(answer.indexOf('\n'))).object().value(u"event").toString() == u"locked";
|
||||
}
|
||||
|
||||
void AgentSocket::confirmLock()
|
||||
{
|
||||
for (const QPointer<QLocalSocket> &socket : std::as_const(m_waiting)) {
|
||||
if (socket) {
|
||||
socket->write(QJsonDocument(QJsonObject{{QStringLiteral("event"), QStringLiteral("locked")}}).toJson(QJsonDocument::Compact) + '\n');
|
||||
socket->flush();
|
||||
}
|
||||
}
|
||||
m_waiting.clear();
|
||||
}
|
||||
|
||||
bool AgentSocket::listen()
|
||||
{
|
||||
const QString p = path();
|
||||
|
||||
+11
-1
@@ -7,13 +7,16 @@
|
||||
// it is idle).
|
||||
//
|
||||
// Only root and this user may talk here, and all they can do is make the
|
||||
// bubble move.
|
||||
// bubble move, or (this user) ask for face-unlock's own lock screen.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QLocalServer>
|
||||
#include <QLocalSocket>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
|
||||
class AgentSocket : public QObject
|
||||
{
|
||||
@@ -26,11 +29,18 @@ public:
|
||||
// session starts the agent from its own config, and that must not make
|
||||
// two of them.
|
||||
static bool running();
|
||||
// Ask the running agent to lock the screen, and wait until it is. False
|
||||
// when there is no agent or the lock did not come.
|
||||
static bool requestLock();
|
||||
// Tell everybody waiting in requestLock() that the screen is locked.
|
||||
void confirmLock();
|
||||
static QString path();
|
||||
|
||||
Q_SIGNALS:
|
||||
void scanEvent(const QJsonObject &event);
|
||||
void lockRequested();
|
||||
|
||||
private:
|
||||
QLocalServer m_server;
|
||||
QList<QPointer<QLocalSocket>> m_waiting;
|
||||
};
|
||||
@@ -175,6 +175,9 @@ InputWatcher::~InputWatcher() = default;
|
||||
void InputWatcher::watch(int calmMs)
|
||||
{
|
||||
stop();
|
||||
m_watching = true;
|
||||
m_calmMs = calmMs;
|
||||
m_calm.start();
|
||||
auto *wayland = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>();
|
||||
if (!m_notifier->isActive() || !wayland || !wayland->seat()) {
|
||||
if (!m_mutter && QDBusConnection::sessionBus().interface()->isServiceRegistered(MutterService)) {
|
||||
@@ -205,8 +208,18 @@ void InputWatcher::watch(int calmMs)
|
||||
});
|
||||
}
|
||||
|
||||
void InputWatcher::noteInput()
|
||||
{
|
||||
if (m_watching && m_calm.elapsed() >= m_calmMs) {
|
||||
m_watching = false;
|
||||
QMetaObject::invokeMethod(this, &InputWatcher::input, Qt::QueuedConnection);
|
||||
}
|
||||
m_calm.restart();
|
||||
}
|
||||
|
||||
void InputWatcher::stop()
|
||||
{
|
||||
m_watching = false;
|
||||
m_notification.reset();
|
||||
if (m_mutter) {
|
||||
m_mutter->stop();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QObject>
|
||||
|
||||
#include <memory>
|
||||
@@ -29,6 +30,9 @@ public:
|
||||
// calmMs. 0: the next input. Replaces what was watched before.
|
||||
void watch(int calmMs);
|
||||
void stop();
|
||||
// A key or the pointer on face-unlock's own lock screen, which sees them
|
||||
// itself. Counts like input the compositor reports.
|
||||
void noteInput();
|
||||
|
||||
Q_SIGNALS:
|
||||
void input();
|
||||
@@ -37,4 +41,7 @@ private:
|
||||
std::unique_ptr<IdleNotifier> m_notifier;
|
||||
std::unique_ptr<IdleNotification> m_notification;
|
||||
MutterIdle *m_mutter = nullptr;
|
||||
bool m_watching = false;
|
||||
int m_calmMs = 0;
|
||||
QElapsedTimer m_calm;
|
||||
};
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "bubblecontroller.h"
|
||||
#include "daemonclient.h"
|
||||
#include "lockscreencontroller.h"
|
||||
#include "userconfig.h"
|
||||
|
||||
#include <QDBusConnection>
|
||||
@@ -23,11 +24,18 @@ constexpr int CalmBeforeRetryMs = 2000;
|
||||
constexpr int ScanAfterWakeMs = 1000;
|
||||
} // namespace
|
||||
|
||||
LockController::LockController(BubbleController *bubble, UserConfig *config, QObject *parent)
|
||||
LockController::LockController(BubbleController *bubble, UserConfig *config, SessionLock *lock, LockScreenController *screen, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_bubble(bubble)
|
||||
, m_config(config)
|
||||
, m_screen(screen)
|
||||
{
|
||||
if (lock) {
|
||||
m_lock.setOwnLock(lock);
|
||||
}
|
||||
if (screen) {
|
||||
connect(screen, &LockScreenController::input, &m_input, &InputWatcher::noteInput);
|
||||
}
|
||||
m_armTimer.setSingleShot(true);
|
||||
connect(&m_armTimer, &QTimer::timeout, this, [this] {
|
||||
arm(0);
|
||||
@@ -68,7 +76,11 @@ void LockController::onLockedChanged(bool locked)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_config->lockScreen()) {
|
||||
const bool face = m_config->enabled() && m_config->lockScreen();
|
||||
if (m_screen) {
|
||||
m_screen->setFaceUnlock(face);
|
||||
}
|
||||
if (!face) {
|
||||
return;
|
||||
}
|
||||
m_locked = true;
|
||||
|
||||
@@ -26,13 +26,16 @@
|
||||
|
||||
class BubbleController;
|
||||
class DaemonRequest;
|
||||
class LockScreenController;
|
||||
class SessionLock;
|
||||
class UserConfig;
|
||||
|
||||
class LockController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
LockController(BubbleController *bubble, UserConfig *config, QObject *parent = nullptr);
|
||||
// lock and screen are face-unlock's own lock screen, where there is one.
|
||||
LockController(BubbleController *bubble, UserConfig *config, SessionLock *lock, LockScreenController *screen, QObject *parent = nullptr);
|
||||
|
||||
bool locked() const
|
||||
{
|
||||
@@ -53,6 +56,7 @@ private:
|
||||
|
||||
BubbleController *m_bubble;
|
||||
UserConfig *m_config;
|
||||
LockScreenController *m_screen;
|
||||
bool m_locked = false;
|
||||
bool m_stopped = false;
|
||||
QElapsedTimer m_lockedFor;
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "lockscreencontroller.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFile>
|
||||
#include <QPointer>
|
||||
|
||||
#include <security/pam_appl.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <pwd.h>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct Conversation {
|
||||
QByteArray password;
|
||||
};
|
||||
|
||||
// Answers every hidden question with the password and every other one with
|
||||
// nothing, as a lock screen that only asks for the password has to.
|
||||
int converse(int count, const pam_message **messages, pam_response **responses, void *data)
|
||||
{
|
||||
auto *conversation = static_cast<Conversation *>(data);
|
||||
auto *answers = static_cast<pam_response *>(calloc(size_t(count), sizeof(pam_response)));
|
||||
if (!answers) {
|
||||
return PAM_BUF_ERR;
|
||||
}
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (messages[i]->msg_style == PAM_PROMPT_ECHO_OFF) {
|
||||
answers[i].resp = strdup(conversation->password.constData());
|
||||
} else if (messages[i]->msg_style == PAM_PROMPT_ECHO_ON) {
|
||||
answers[i].resp = strdup("");
|
||||
}
|
||||
}
|
||||
*responses = answers;
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
QByteArray serviceName()
|
||||
{
|
||||
static const char *const services[] = {"face-unlock-lock", "password-auth", "common-auth", "login"};
|
||||
static const char *const dirs[] = {"/etc/pam.d/", "/usr/lib/pam.d/", "/usr/share/pam.d/"};
|
||||
for (const char *service : services) {
|
||||
for (const char *dir : dirs) {
|
||||
if (QFile::exists(QString::fromLatin1(dir) + QString::fromLatin1(service))) {
|
||||
return service;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "login";
|
||||
}
|
||||
|
||||
bool checkPassword(const QByteArray &user, const QByteArray &password)
|
||||
{
|
||||
Conversation conversation{password};
|
||||
const pam_conv conv{converse, &conversation};
|
||||
pam_handle_t *pamh = nullptr;
|
||||
// For development only, like the daemon's --socket: a directory of PAM
|
||||
// files of its own, so a test never counts as a wrong password for the
|
||||
// real account.
|
||||
const QByteArray confdir = qgetenv("FU_PAM_CONFDIR");
|
||||
int rc = confdir.isEmpty() ? pam_start(serviceName().constData(), user.constData(), &conv, &pamh)
|
||||
: pam_start_confdir("face-unlock-lock", user.constData(), &conv, confdir.constData(), &pamh);
|
||||
if (rc == PAM_SUCCESS) {
|
||||
rc = pam_authenticate(pamh, 0);
|
||||
if (rc == PAM_SUCCESS) {
|
||||
pam_setcred(pamh, PAM_REFRESH_CRED);
|
||||
}
|
||||
}
|
||||
pam_end(pamh, rc);
|
||||
std::memset(conversation.password.data(), 0, size_t(conversation.password.size()));
|
||||
return rc == PAM_SUCCESS;
|
||||
}
|
||||
|
||||
passwd *me()
|
||||
{
|
||||
return getpwuid(getuid());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
LockScreenController::LockScreenController(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
LockScreenController::~LockScreenController()
|
||||
{
|
||||
clearPassword();
|
||||
}
|
||||
|
||||
void LockScreenController::setPassword(const QString &password)
|
||||
{
|
||||
if (m_password == password) {
|
||||
return;
|
||||
}
|
||||
m_password = password;
|
||||
Q_EMIT passwordChanged();
|
||||
if (!m_message.isEmpty()) {
|
||||
m_message.clear();
|
||||
Q_EMIT messageChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void LockScreenController::setFaceUnlock(bool on)
|
||||
{
|
||||
if (m_faceUnlock != on) {
|
||||
m_faceUnlock = on;
|
||||
Q_EMIT faceUnlockChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void LockScreenController::setBlur(bool blur)
|
||||
{
|
||||
if (m_blur != blur) {
|
||||
m_blur = blur;
|
||||
Q_EMIT blurChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString LockScreenController::userName() const
|
||||
{
|
||||
const passwd *pw = me();
|
||||
if (!pw) {
|
||||
return {};
|
||||
}
|
||||
// The full name from the comment field, else the login name.
|
||||
const QString full = QString::fromLocal8Bit(pw->pw_gecos).section(u',', 0, 0).trimmed();
|
||||
return full.isEmpty() ? QString::fromLocal8Bit(pw->pw_name) : full;
|
||||
}
|
||||
|
||||
void LockScreenController::submit()
|
||||
{
|
||||
if (m_busy || m_password.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const passwd *pw = me();
|
||||
if (!pw) {
|
||||
return;
|
||||
}
|
||||
m_busy = true;
|
||||
Q_EMIT busyChanged();
|
||||
|
||||
const QByteArray user(pw->pw_name);
|
||||
QByteArray password = m_password.toUtf8();
|
||||
QPointer<LockScreenController> self(this);
|
||||
std::thread([self, user, password]() mutable {
|
||||
const bool ok = checkPassword(user, password);
|
||||
std::memset(password.data(), 0, size_t(password.size()));
|
||||
QMetaObject::invokeMethod(qApp, [self, ok] {
|
||||
if (self) {
|
||||
self->finish(ok);
|
||||
}
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void LockScreenController::finish(bool ok)
|
||||
{
|
||||
m_busy = false;
|
||||
Q_EMIT busyChanged();
|
||||
clearPassword();
|
||||
Q_EMIT passwordChanged();
|
||||
if (ok) {
|
||||
Q_EMIT authenticated();
|
||||
return;
|
||||
}
|
||||
m_message = i18n("Wrong password");
|
||||
Q_EMIT messageChanged();
|
||||
Q_EMIT rejected();
|
||||
}
|
||||
|
||||
void LockScreenController::activity()
|
||||
{
|
||||
Q_EMIT input();
|
||||
}
|
||||
|
||||
void LockScreenController::clearPassword()
|
||||
{
|
||||
// What is still in memory of it, as far as a QString lets that happen.
|
||||
m_password.fill(u' ');
|
||||
m_password.clear();
|
||||
}
|
||||
|
||||
void LockScreenController::reset()
|
||||
{
|
||||
clearPassword();
|
||||
Q_EMIT passwordChanged();
|
||||
if (!m_message.isEmpty()) {
|
||||
m_message.clear();
|
||||
Q_EMIT messageChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// The own lock screen's side of things (see SessionLock): the password,
|
||||
// whether it is being checked, what to say under it.
|
||||
//
|
||||
// The password is checked with PAM in a thread of its own, the way swaylock
|
||||
// and hyprlock do it. The service is face-unlock-lock when an administrator
|
||||
// has written one, else the distribution's plain password stack:
|
||||
// password-auth (Fedora), common-auth (Debian, Ubuntu) or login. Not the face
|
||||
// module: the face has its own way in here, the agent.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class LockScreenController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged)
|
||||
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
|
||||
// After a wrong password, until the next key.
|
||||
Q_PROPERTY(QString message READ message NOTIFY messageChanged)
|
||||
// Whether the face is looked for, which changes what the screen says.
|
||||
Q_PROPERTY(bool faceUnlock READ faceUnlock NOTIFY faceUnlockChanged)
|
||||
Q_PROPERTY(QString userName READ userName CONSTANT)
|
||||
// Whether to blur the picture behind it.
|
||||
Q_PROPERTY(bool blur READ blur NOTIFY blurChanged)
|
||||
public:
|
||||
explicit LockScreenController(QObject *parent = nullptr);
|
||||
~LockScreenController() override;
|
||||
|
||||
QString password() const
|
||||
{
|
||||
return m_password;
|
||||
}
|
||||
void setPassword(const QString &password);
|
||||
bool busy() const
|
||||
{
|
||||
return m_busy;
|
||||
}
|
||||
QString message() const
|
||||
{
|
||||
return m_message;
|
||||
}
|
||||
bool faceUnlock() const
|
||||
{
|
||||
return m_faceUnlock;
|
||||
}
|
||||
void setFaceUnlock(bool on);
|
||||
QString userName() const;
|
||||
bool blur() const
|
||||
{
|
||||
return m_blur;
|
||||
}
|
||||
void setBlur(bool blur);
|
||||
|
||||
// Check the password typed so far.
|
||||
Q_INVOKABLE void submit();
|
||||
// A key or the pointer on the lock screen.
|
||||
Q_INVOKABLE void activity();
|
||||
|
||||
// A fresh lock: no password, nothing said.
|
||||
void reset();
|
||||
|
||||
Q_SIGNALS:
|
||||
void passwordChanged();
|
||||
void busyChanged();
|
||||
void messageChanged();
|
||||
void faceUnlockChanged();
|
||||
void blurChanged();
|
||||
void authenticated();
|
||||
// A wrong password, for the field to shake.
|
||||
void rejected();
|
||||
void input();
|
||||
|
||||
private:
|
||||
void finish(bool ok);
|
||||
void clearPassword();
|
||||
|
||||
QString m_password;
|
||||
QString m_message;
|
||||
bool m_busy = false;
|
||||
bool m_faceUnlock = true;
|
||||
bool m_blur = false;
|
||||
};
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "lockwatcher.h"
|
||||
|
||||
#include "lockers.h"
|
||||
#include "sessionlock.h"
|
||||
#include "wayland.h"
|
||||
|
||||
#include <QDBusConnection>
|
||||
@@ -162,14 +163,20 @@ void LockWatcher::pollLockers()
|
||||
update();
|
||||
}
|
||||
|
||||
void LockWatcher::setOwnLock(SessionLock *lock)
|
||||
{
|
||||
m_own = lock;
|
||||
connect(lock, &SessionLock::lockedChanged, this, &LockWatcher::update);
|
||||
}
|
||||
|
||||
bool LockWatcher::canUnlock() const
|
||||
{
|
||||
return !m_ownLockers || m_lockerRunning;
|
||||
return (m_own && m_own->isLocked()) || !m_ownLockers || m_lockerRunning;
|
||||
}
|
||||
|
||||
void LockWatcher::update()
|
||||
{
|
||||
const bool locked = m_screenSaver || m_lockedHint || m_lockerRunning;
|
||||
const bool locked = m_screenSaver || m_lockedHint || m_lockerRunning || (m_own && m_own->isLocked());
|
||||
if (locked != m_locked) {
|
||||
m_locked = locked;
|
||||
Q_EMIT lockedChanged(locked);
|
||||
@@ -178,6 +185,10 @@ void LockWatcher::update()
|
||||
|
||||
void LockWatcher::unlock()
|
||||
{
|
||||
if (m_own && m_own->isLocked()) {
|
||||
m_own->unlock();
|
||||
return;
|
||||
}
|
||||
// Looked up again rather than taken from the last poll: a second is
|
||||
// long enough for a pid to belong to something else.
|
||||
Lockers::signal(SIGUSR1);
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
// (ext-session-lock).
|
||||
//
|
||||
// hyprlock and swaylock do not listen to logind. They unlock on SIGUSR1.
|
||||
// Any other lock screen of that kind only opens itself: it gets the face
|
||||
// through the PAM module in its own stack (see src/lib/pam.sh), when Enter is
|
||||
// pressed.
|
||||
// face-unlock's own lock screen (SessionLock) is simply told to. Any other
|
||||
// lock screen of that kind only opens itself: it gets the face through the
|
||||
// PAM module in its own stack (see src/lib/pam.sh), when Enter is pressed.
|
||||
//
|
||||
// None of this lowers the bar: any program running as this user can already
|
||||
// ask logind to unlock the session, or send its own lock screen a signal. The
|
||||
@@ -28,6 +28,8 @@
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
class SessionLock;
|
||||
|
||||
class LockWatcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -42,6 +44,7 @@ public:
|
||||
// logind unlocks (Plasma, GNOME), only hyprlock and swaylock elsewhere.
|
||||
bool canUnlock() const;
|
||||
void unlock();
|
||||
void setOwnLock(SessionLock *lock);
|
||||
|
||||
Q_SIGNALS:
|
||||
void lockedChanged(bool locked);
|
||||
@@ -63,6 +66,7 @@ private:
|
||||
bool m_locked = false;
|
||||
// The compositor's lock screen is a program of its own.
|
||||
bool m_ownLockers = false;
|
||||
SessionLock *m_own = nullptr;
|
||||
QString m_session;
|
||||
QTimer m_poll;
|
||||
};
|
||||
+155
-2
@@ -5,6 +5,9 @@
|
||||
// (no arguments) stay in the background: unlock the lock screen by face,
|
||||
// and show the bubble for every scan
|
||||
// --enroll open the window that sets up a face
|
||||
// --lock lock the screen: with face-unlock's own lock screen
|
||||
// where the lock screen is a program of its own (Hyprland,
|
||||
// Niri), else through logind with the desktop's
|
||||
// --demo play the bubble's animations once, for trying out a
|
||||
// style (--bubble-style minimal) and for screenshots
|
||||
|
||||
@@ -14,6 +17,8 @@
|
||||
#include "bubblewindow.h"
|
||||
#include "enrollcontroller.h"
|
||||
#include "lockcontroller.h"
|
||||
#include "lockscreencontroller.h"
|
||||
#include "sessionlock.h"
|
||||
#include "userconfig.h"
|
||||
#include "wayland.h"
|
||||
|
||||
@@ -23,11 +28,17 @@
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <QCommandLineParser>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#include <QFile>
|
||||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlEngine>
|
||||
#include <QTimer>
|
||||
|
||||
#include <cstring>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace
|
||||
@@ -55,6 +66,92 @@ int runEnroll(QGuiApplication &app, const QString &name)
|
||||
return controller.state() == u"done" ? 0 : code;
|
||||
}
|
||||
|
||||
// Where --lock tells the command that ran it that the screen is locked, when
|
||||
// it forked to stay behind as the lock screen. -1: it did not.
|
||||
int readyFd = -1;
|
||||
|
||||
void signalReady(bool ok)
|
||||
{
|
||||
if (readyFd >= 0) {
|
||||
if (ok) {
|
||||
[[maybe_unused]] const ssize_t n = write(readyFd, "1", 1);
|
||||
}
|
||||
close(readyFd);
|
||||
readyFd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Whether an agent listens on its socket. Asked before Qt is up, to decide
|
||||
// whether to fork.
|
||||
bool agentListening()
|
||||
{
|
||||
const char *runtime = getenv("XDG_RUNTIME_DIR");
|
||||
if (!runtime) {
|
||||
return false;
|
||||
}
|
||||
sockaddr_un addr{};
|
||||
addr.sun_family = AF_UNIX;
|
||||
const int len = snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/face-unlock/agent.socket", runtime);
|
||||
if (len <= 0 || size_t(len) >= sizeof(addr.sun_path)) {
|
||||
return false;
|
||||
}
|
||||
const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
const bool ok = fd >= 0 && connect(fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) == 0;
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// With no agent running, --lock stays behind as the lock screen until it is
|
||||
// unlocked. The command that asked for it returns as soon as the screen is
|
||||
// locked, the way swaylock -f does: an idle daemon locking before sleep
|
||||
// waits for that, and not a moment longer.
|
||||
void forkForLock(int argc, char **argv)
|
||||
{
|
||||
bool lock = false;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
lock = lock || std::strcmp(argv[i], "--lock") == 0;
|
||||
}
|
||||
int fds[2];
|
||||
if (!lock || agentListening() || pipe(fds) != 0) {
|
||||
return;
|
||||
}
|
||||
const pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
close(fds[0]);
|
||||
close(fds[1]);
|
||||
return;
|
||||
}
|
||||
if (pid > 0) {
|
||||
close(fds[1]);
|
||||
char c = 0;
|
||||
const bool locked = read(fds[0], &c, 1) == 1;
|
||||
_exit(locked ? 0 : 1);
|
||||
}
|
||||
close(fds[0]);
|
||||
setsid();
|
||||
readyFd = fds[1];
|
||||
}
|
||||
|
||||
// Plasma and GNOME lock with their own lock screen when logind asks them to,
|
||||
// as `loginctl lock-session` does.
|
||||
int lockThroughLogind()
|
||||
{
|
||||
const QString id = qEnvironmentVariable("XDG_SESSION_ID");
|
||||
QDBusMessage call = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.login1"),
|
||||
QStringLiteral("/org/freedesktop/login1"),
|
||||
QStringLiteral("org.freedesktop.login1.Manager"),
|
||||
QStringLiteral("LockSession"));
|
||||
call << (id.isEmpty() ? QStringLiteral("auto") : id);
|
||||
const QDBusMessage reply = QDBusConnection::systemBus().call(call);
|
||||
if (reply.type() == QDBusMessage::ErrorMessage) {
|
||||
qWarning("logind would not lock: %s", qPrintable(reply.errorMessage()));
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The whole life of a bubble, twice: a face that is recognised after a blink,
|
||||
// then one that is not.
|
||||
void scheduleDemo(BubbleController *bubble)
|
||||
@@ -77,6 +174,7 @@ void scheduleDemo(BubbleController *bubble)
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
forkForLock(argc, argv);
|
||||
QGuiApplication app(argc, argv);
|
||||
app.setApplicationName(QStringLiteral("face-unlock-agent"));
|
||||
app.setApplicationVersion(QStringLiteral(FU_VERSION));
|
||||
@@ -89,10 +187,11 @@ int main(int argc, char **argv)
|
||||
parser.addVersionOption();
|
||||
const QCommandLineOption enrollOpt(QStringLiteral("enroll"), i18n("Set up a face."));
|
||||
const QCommandLineOption nameOpt(QStringLiteral("name"), i18n("What to call the new face."), QStringLiteral("name"));
|
||||
const QCommandLineOption lockOpt(QStringLiteral("lock"), i18n("Lock the screen."));
|
||||
const QCommandLineOption demoOpt(QStringLiteral("demo"), i18n("Play the bubble's animations once."));
|
||||
// Not "--style": QGuiApplication takes that one for itself.
|
||||
const QCommandLineOption styleOpt(QStringLiteral("bubble-style"), i18n("Bubble style for the demo: full or minimal."), QStringLiteral("style"));
|
||||
parser.addOptions({enrollOpt, nameOpt, demoOpt, styleOpt});
|
||||
parser.addOptions({enrollOpt, nameOpt, lockOpt, demoOpt, styleOpt});
|
||||
parser.process(app);
|
||||
|
||||
if (parser.isSet(enrollOpt)) {
|
||||
@@ -103,6 +202,13 @@ int main(int argc, char **argv)
|
||||
return runEnroll(app, name);
|
||||
}
|
||||
|
||||
const bool lockNow = parser.isSet(lockOpt);
|
||||
if (lockNow && !SessionLock::available()) {
|
||||
const int rc = lockThroughLogind();
|
||||
signalReady(rc == 0);
|
||||
return rc;
|
||||
}
|
||||
|
||||
app.setQuitOnLastWindowClosed(false);
|
||||
QQmlEngine engine;
|
||||
KLocalization::setupLocalizedContext(&engine);
|
||||
@@ -131,6 +237,9 @@ int main(int argc, char **argv)
|
||||
|
||||
AgentSocket socket;
|
||||
if (AgentSocket::running()) {
|
||||
if (lockNow) {
|
||||
return AgentSocket::requestLock() ? 0 : 1;
|
||||
}
|
||||
qInfo("an agent is already running in this session");
|
||||
return 0;
|
||||
}
|
||||
@@ -141,6 +250,50 @@ int main(int argc, char **argv)
|
||||
service = std::make_unique<BubbleService>(&bubble);
|
||||
}
|
||||
|
||||
LockController lock(&bubble, &config);
|
||||
LockScreenController screen;
|
||||
std::unique_ptr<SessionLock> sessionLock;
|
||||
if (SessionLock::available()) {
|
||||
sessionLock = std::make_unique<SessionLock>(&engine, &bubble, &screen, &config);
|
||||
SessionLock *lock = sessionLock.get();
|
||||
QObject::connect(&socket, &AgentSocket::lockRequested, lock, [lock, &socket] {
|
||||
lock->lock();
|
||||
if (lock->isConfirmed()) {
|
||||
socket.confirmLock();
|
||||
}
|
||||
});
|
||||
QObject::connect(lock, &SessionLock::confirmed, &socket, &AgentSocket::confirmLock);
|
||||
}
|
||||
LockController lock(&bubble, &config, sessionLock.get(), &screen);
|
||||
|
||||
if (lockNow) {
|
||||
// No agent was running, so this one is only here for the lock. It
|
||||
// goes once the lock is gone and the bubble has finished.
|
||||
QObject::connect(sessionLock.get(), &SessionLock::lockedChanged, &app, [&bubble](bool locked) {
|
||||
if (locked) {
|
||||
return;
|
||||
}
|
||||
if (!bubble.shown()) {
|
||||
QCoreApplication::quit();
|
||||
}
|
||||
QObject::connect(&bubble, &BubbleController::shownChanged, qApp, [&bubble] {
|
||||
if (!bubble.shown()) {
|
||||
QCoreApplication::quit();
|
||||
}
|
||||
});
|
||||
QTimer::singleShot(5000, qApp, &QCoreApplication::quit);
|
||||
});
|
||||
QObject::connect(sessionLock.get(), &SessionLock::confirmed, &app, [] {
|
||||
signalReady(true);
|
||||
});
|
||||
sessionLock->lock();
|
||||
if (!sessionLock->isLocked()) {
|
||||
signalReady(false);
|
||||
return 1;
|
||||
}
|
||||
} else if (sessionLock && QFile::exists(SessionLock::markerPath())) {
|
||||
// The last agent went away with the screen locked, and the compositor
|
||||
// kept it locked. Take the lock back, so it can be opened again.
|
||||
sessionLock->lock();
|
||||
}
|
||||
return app.exec();
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// face-unlock's own lock screen, one per screen (see SessionLock): the time,
|
||||
// the password, and on the main screen the bubble at the top, the same one
|
||||
// that shows over the desktop. Behind it the desktop's picture, or the one
|
||||
// the user set (see Wallpaper).
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var lock
|
||||
required property var bubble
|
||||
required property bool primary
|
||||
required property url wallpaper
|
||||
|
||||
property date now: new Date()
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.now = new Date()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0; color: "#11161D" }
|
||||
GradientStop { position: 0.6; color: "#000000" }
|
||||
}
|
||||
}
|
||||
|
||||
// Loaded aside, so a big picture does not hold up the lock, and faded in.
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
opacity: picture.status === Image.Ready ? 1 : 0
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
Image {
|
||||
id: picture
|
||||
// Past the edges when blurred: the blur would pull in the nothing
|
||||
// beyond them and darken the rim.
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.lock.blur ? -64 : 0
|
||||
source: root.wallpaper
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize: Qt.size(root.width, root.height)
|
||||
asynchronous: true
|
||||
cache: false
|
||||
layer.enabled: root.lock.blur
|
||||
layer.effect: MultiEffect {
|
||||
blurEnabled: true
|
||||
blur: 1
|
||||
blurMax: 64
|
||||
}
|
||||
}
|
||||
// Dimmed a little, so the time and the password read on any picture.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#000000"
|
||||
opacity: 0.35
|
||||
}
|
||||
}
|
||||
|
||||
// Any touch counts as somebody being back, and a click anywhere puts the
|
||||
// keys into the password.
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onPositionChanged: root.lock.activity()
|
||||
onPressed: {
|
||||
root.lock.activity()
|
||||
field.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
// Below the open bubble, which hangs from the top edge.
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: Math.max(Theme.topGap + Theme.openHeight + 24, Math.round(parent.height * 0.2))
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Theme.textPrimary
|
||||
font.pixelSize: Math.max(56, Math.min(120, Math.round(root.height * 0.11)))
|
||||
font.weight: Font.Light
|
||||
text: Qt.formatTime(root.now, Qt.locale().timeFormat(Locale.ShortFormat))
|
||||
}
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Theme.textDetail
|
||||
font.pixelSize: 18
|
||||
font.weight: Font.Medium
|
||||
text: Qt.formatDate(root.now, Qt.locale().dateFormat(Locale.LongFormat))
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: Math.round(parent.height * 0.62)
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Theme.textPrimary
|
||||
font.pixelSize: 17
|
||||
font.weight: Font.DemiBold
|
||||
text: root.lock.userName
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 280
|
||||
height: 44
|
||||
radius: height / 2
|
||||
color: Theme.surfaceRaised
|
||||
opacity: root.lock.busy ? 0.6 : 1
|
||||
border.width: field.activeFocus ? 1 : 0
|
||||
border.color: Qt.rgba(1, 1, 1, 0.18)
|
||||
|
||||
property real shake: 0
|
||||
transform: Translate { x: pill.shake }
|
||||
SequentialAnimation {
|
||||
id: shakeAnimation
|
||||
NumberAnimation { target: pill; property: "shake"; to: -10; duration: 55; easing.type: Easing.OutQuad }
|
||||
NumberAnimation { target: pill; property: "shake"; to: 9; duration: 70 }
|
||||
NumberAnimation { target: pill; property: "shake"; to: -6; duration: 65 }
|
||||
NumberAnimation { target: pill; property: "shake"; to: 4; duration: 60 }
|
||||
NumberAnimation { target: pill; property: "shake"; to: 0; duration: 55; easing.type: Easing.OutQuad }
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: field
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 20
|
||||
anchors.rightMargin: 20
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
horizontalAlignment: TextInput.AlignHCenter
|
||||
echoMode: TextInput.Password
|
||||
passwordCharacter: "●"
|
||||
color: Theme.textPrimary
|
||||
selectionColor: Theme.accent
|
||||
font.pixelSize: 15
|
||||
clip: true
|
||||
focus: true
|
||||
// The dots show how far it is; a blinking bar in the middle
|
||||
// of the empty field only cuts the word in it in two.
|
||||
cursorDelegate: Item {}
|
||||
readOnly: root.lock.busy
|
||||
text: root.lock.password
|
||||
onTextEdited: root.lock.password = text
|
||||
onAccepted: root.lock.submit()
|
||||
Keys.onPressed: event => {
|
||||
root.lock.activity()
|
||||
event.accepted = false
|
||||
}
|
||||
Component.onCompleted: forceActiveFocus()
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
color: Theme.textSecondary
|
||||
font.pixelSize: 15
|
||||
text: i18n("Password")
|
||||
visible: field.text.length === 0
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 360
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
color: root.lock.message.length > 0 ? Theme.failure : Theme.textDetail
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.Medium
|
||||
text: root.lock.message.length > 0 ? root.lock.message
|
||||
: root.lock.faceUnlock ? i18n("Look at the camera or type your password")
|
||||
: i18n("Type your password")
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.lock
|
||||
function onRejected() {
|
||||
shakeAnimation.restart()
|
||||
}
|
||||
}
|
||||
|
||||
// The bubble, where it shows over the desktop too.
|
||||
Bubble {
|
||||
visible: root.primary
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: 0
|
||||
bubble: root.bubble
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "sessionlock.h"
|
||||
|
||||
#include "bubblecontroller.h"
|
||||
#include "lockscreencontroller.h"
|
||||
#include "userconfig.h"
|
||||
#include "wallpaper.h"
|
||||
#include "wayland.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QQuickView>
|
||||
#include <QScreen>
|
||||
#include <QStandardPaths>
|
||||
#include <QUrl>
|
||||
#include <QtWaylandClient/private/qwaylanddisplay_p.h>
|
||||
#include <QtWaylandClient/private/qwaylandintegration_p.h>
|
||||
#include <QtWaylandClient/private/qwaylandscreen_p.h>
|
||||
#include <QtWaylandClient/private/qwaylandshellintegration_p.h>
|
||||
#include <QtWaylandClient/private/qwaylandshellsurface_p.h>
|
||||
#include <QtWaylandClient/private/qwaylandsurface_p.h>
|
||||
#include <QtWaylandClient/private/qwaylandwindow_p.h>
|
||||
|
||||
#include "qwayland-ext-session-lock-v1.h"
|
||||
|
||||
// The manager, and through it the role every lock window gets.
|
||||
class LockIntegration : public QtWaylandClient::QWaylandShellIntegrationTemplate<LockIntegration>, public QtWayland::ext_session_lock_manager_v1
|
||||
{
|
||||
public:
|
||||
LockIntegration()
|
||||
: QWaylandShellIntegrationTemplate<LockIntegration>(1)
|
||||
{
|
||||
}
|
||||
~LockIntegration() override
|
||||
{
|
||||
if (object()) {
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
QtWaylandClient::QWaylandShellSurface *createShellSurface(QtWaylandClient::QWaylandWindow *window) override;
|
||||
|
||||
// The lock the surfaces belong to.
|
||||
::ext_session_lock_v1 *current = nullptr;
|
||||
};
|
||||
|
||||
// One screen's lock surface. Like LayerShellQt's layer surface: the size
|
||||
// comes from the compositor, and nothing is drawn before it has sent one.
|
||||
class LockSurface : public QtWaylandClient::QWaylandShellSurface, public QtWayland::ext_session_lock_surface_v1
|
||||
{
|
||||
public:
|
||||
LockSurface(::ext_session_lock_v1 *lock, QtWaylandClient::QWaylandWindow *window)
|
||||
: QWaylandShellSurface(window)
|
||||
{
|
||||
init(ext_session_lock_v1_get_lock_surface(lock, window->waylandSurface()->object(), window->waylandScreen()->output()));
|
||||
}
|
||||
~LockSurface() override
|
||||
{
|
||||
destroy();
|
||||
}
|
||||
bool isExposed() const override
|
||||
{
|
||||
return m_configured;
|
||||
}
|
||||
// Qt commits a new role at once, as xdg-shell wants. A lock surface must
|
||||
// not be committed before it has acknowledged its first size.
|
||||
bool commitSurfaceRole() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
void applyConfigure() override
|
||||
{
|
||||
window()->resizeFromApplyConfigure(m_size);
|
||||
}
|
||||
|
||||
protected:
|
||||
void ext_session_lock_surface_v1_configure(uint32_t serial, uint32_t width, uint32_t height) override
|
||||
{
|
||||
ack_configure(serial);
|
||||
m_size = QSize(int(width), int(height));
|
||||
if (!m_configured) {
|
||||
m_configured = true;
|
||||
applyConfigure();
|
||||
window()->updateExposure();
|
||||
} else {
|
||||
window()->applyConfigureWhenPossible();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
QSize m_size;
|
||||
bool m_configured = false;
|
||||
};
|
||||
|
||||
QtWaylandClient::QWaylandShellSurface *LockIntegration::createShellSurface(QtWaylandClient::QWaylandWindow *window)
|
||||
{
|
||||
return new LockSurface(current, window);
|
||||
}
|
||||
|
||||
class Lock : public QtWayland::ext_session_lock_v1
|
||||
{
|
||||
public:
|
||||
Lock(::ext_session_lock_v1 *object, SessionLock *owner)
|
||||
: QtWayland::ext_session_lock_v1(object)
|
||||
, m_owner(owner)
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
// Queued: not from inside the Wayland event, which the answer to it might
|
||||
// destroy.
|
||||
void ext_session_lock_v1_locked() override
|
||||
{
|
||||
QMetaObject::invokeMethod(m_owner, &SessionLock::onLocked, Qt::QueuedConnection);
|
||||
}
|
||||
void ext_session_lock_v1_finished() override
|
||||
{
|
||||
QMetaObject::invokeMethod(m_owner, &SessionLock::onFinished, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
private:
|
||||
SessionLock *m_owner;
|
||||
};
|
||||
|
||||
SessionLock::SessionLock(QQmlEngine *engine, BubbleController *bubble, LockScreenController *screen, UserConfig *config, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_engine(engine)
|
||||
, m_bubble(bubble)
|
||||
, m_screen(screen)
|
||||
, m_config(config)
|
||||
{
|
||||
connect(qGuiApp, &QGuiApplication::screenAdded, this, [this](QScreen *s) {
|
||||
if (m_lock) {
|
||||
addScreen(s);
|
||||
}
|
||||
});
|
||||
connect(qGuiApp, &QGuiApplication::screenRemoved, this, &SessionLock::removeScreen);
|
||||
connect(m_screen, &LockScreenController::authenticated, this, &SessionLock::unlock);
|
||||
}
|
||||
|
||||
SessionLock::~SessionLock()
|
||||
{
|
||||
// Leaving with the lock held keeps the session locked, which is the
|
||||
// point. The windows go, the lock stays.
|
||||
qDeleteAll(m_views);
|
||||
delete m_lock;
|
||||
delete m_integration;
|
||||
}
|
||||
|
||||
bool SessionLock::available()
|
||||
{
|
||||
return Wayland::hasGlobal("ext_session_lock_manager_v1");
|
||||
}
|
||||
|
||||
QString SessionLock::markerPath()
|
||||
{
|
||||
return QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + QStringLiteral("/face-unlock/locked");
|
||||
}
|
||||
|
||||
void SessionLock::lock()
|
||||
{
|
||||
if (m_lock) {
|
||||
return;
|
||||
}
|
||||
auto *qpa = QtWaylandClient::QWaylandIntegration::instance();
|
||||
if (!m_integration && qpa) {
|
||||
m_integration = new LockIntegration;
|
||||
if (!m_integration->initialize(qpa->display())) {
|
||||
delete m_integration;
|
||||
m_integration = nullptr;
|
||||
}
|
||||
}
|
||||
if (!m_integration) {
|
||||
qWarning("the compositor has no ext_session_lock_manager_v1, so this cannot lock the screen");
|
||||
return;
|
||||
}
|
||||
|
||||
m_confirmed = false;
|
||||
m_unlockWanted = false;
|
||||
m_lock = new Lock(m_integration->lock(), this);
|
||||
m_integration->current = m_lock->object();
|
||||
m_screen->reset();
|
||||
m_pictures = Wallpaper::forLock(m_config->lockWallpaper());
|
||||
m_screen->setBlur(m_config->lockBlur());
|
||||
for (QScreen *s : QGuiApplication::screens()) {
|
||||
addScreen(s);
|
||||
}
|
||||
|
||||
QDir().mkpath(QFileInfo(markerPath()).absolutePath());
|
||||
QFile marker(markerPath());
|
||||
if (!marker.open(QIODevice::WriteOnly)) {
|
||||
qWarning("cannot write %s", qPrintable(markerPath()));
|
||||
}
|
||||
Q_EMIT lockedChanged(true);
|
||||
}
|
||||
|
||||
void SessionLock::addScreen(QScreen *screen)
|
||||
{
|
||||
if (m_views.contains(screen) || !dynamic_cast<QtWaylandClient::QWaylandScreen *>(screen->handle())) {
|
||||
return;
|
||||
}
|
||||
auto *view = new QQuickView(m_engine, nullptr);
|
||||
view->setColor(Qt::black);
|
||||
view->setScreen(screen);
|
||||
view->setResizeMode(QQuickView::SizeRootObjectToView);
|
||||
view->resize(screen->size());
|
||||
view->setInitialProperties({{QStringLiteral("lock"), QVariant::fromValue(m_screen)},
|
||||
{QStringLiteral("bubble"), QVariant::fromValue(m_bubble)},
|
||||
{QStringLiteral("wallpaper"), Wallpaper::pick(m_pictures, screen->name())},
|
||||
{QStringLiteral("primary"), screen == QGuiApplication::primaryScreen()}});
|
||||
view->loadFromModule(QStringLiteral("FaceUnlock"), QStringLiteral("LockScreen"));
|
||||
for (const QQmlError &e : view->errors()) {
|
||||
qWarning("%s", qPrintable(e.toString()));
|
||||
}
|
||||
view->create();
|
||||
if (auto *wayland = dynamic_cast<QtWaylandClient::QWaylandWindow *>(view->handle())) {
|
||||
wayland->setShellIntegration(m_integration);
|
||||
}
|
||||
view->show();
|
||||
view->requestActivate();
|
||||
m_views.insert(screen, view);
|
||||
}
|
||||
|
||||
void SessionLock::removeScreen(QScreen *screen)
|
||||
{
|
||||
if (auto view = m_views.take(screen)) {
|
||||
delete view;
|
||||
}
|
||||
}
|
||||
|
||||
void SessionLock::onLocked()
|
||||
{
|
||||
if (!m_lock) {
|
||||
return;
|
||||
}
|
||||
m_confirmed = true;
|
||||
Q_EMIT confirmed();
|
||||
if (m_unlockWanted) {
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void SessionLock::onFinished()
|
||||
{
|
||||
if (!m_lock) {
|
||||
return;
|
||||
}
|
||||
// Refused (another lock screen is up) or ended by the compositor.
|
||||
if (m_confirmed) {
|
||||
m_lock->unlock_and_destroy();
|
||||
} else {
|
||||
qWarning("the compositor did not let this lock the screen; is another lock screen running?");
|
||||
m_lock->destroy();
|
||||
}
|
||||
release();
|
||||
}
|
||||
|
||||
void SessionLock::unlock()
|
||||
{
|
||||
if (!m_lock) {
|
||||
return;
|
||||
}
|
||||
// Unlocking before the compositor has confirmed the lock is a protocol
|
||||
// error. It follows as soon as it has.
|
||||
if (!m_confirmed) {
|
||||
m_unlockWanted = true;
|
||||
return;
|
||||
}
|
||||
m_lock->unlock_and_destroy();
|
||||
release();
|
||||
}
|
||||
|
||||
void SessionLock::release()
|
||||
{
|
||||
qDeleteAll(m_views);
|
||||
m_views.clear();
|
||||
delete m_lock;
|
||||
m_lock = nullptr;
|
||||
m_confirmed = false;
|
||||
m_integration->current = nullptr;
|
||||
QFile::remove(markerPath());
|
||||
// Out to the compositor now: an agent started only for this lock quits
|
||||
// straight after.
|
||||
Wayland::sync();
|
||||
m_screen->reset();
|
||||
Q_EMIT lockedChanged(false);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// face-unlock's own lock screen, for whoever wants it on a compositor whose
|
||||
// lock screen is a program of its own (ext-session-lock: Hyprland, Niri,
|
||||
// Sway and others): `face-unlock lock`.
|
||||
//
|
||||
// Any other lock screen there covers the bubble, and only opens itself (it
|
||||
// gets the face through PAM, see src/lib/pam.sh). This one is the lock
|
||||
// screen, so the bubble shows on it and a face opens it straight away. The
|
||||
// picture behind it is the user's choice (LockWallpaper).
|
||||
//
|
||||
// Qt knows no ext-session-lock, so each screen's window gets the lock surface
|
||||
// role through Qt's shell integration, the way LayerShellQt gives windows the
|
||||
// layer-shell role. If this program goes away while locked, the compositor
|
||||
// keeps the session locked: that is the protocol's promise.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QUrl>
|
||||
|
||||
class BubbleController;
|
||||
class LockScreenController;
|
||||
class UserConfig;
|
||||
class QQmlEngine;
|
||||
class QQuickView;
|
||||
class QScreen;
|
||||
class LockIntegration;
|
||||
class Lock;
|
||||
|
||||
class SessionLock : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
SessionLock(QQmlEngine *engine, BubbleController *bubble, LockScreenController *screen, UserConfig *config, QObject *parent = nullptr);
|
||||
~SessionLock() override;
|
||||
|
||||
// Whether the compositor offers ext-session-lock.
|
||||
static bool available();
|
||||
|
||||
// From the lock request until the lock is gone again.
|
||||
bool isLocked() const
|
||||
{
|
||||
return m_lock != nullptr;
|
||||
}
|
||||
// The compositor has confirmed the lock: nothing unlocked is on screen.
|
||||
bool isConfirmed() const
|
||||
{
|
||||
return m_confirmed;
|
||||
}
|
||||
|
||||
// A file that says the screen is locked, so that an agent started again
|
||||
// after a crash locks again (see main.cpp).
|
||||
static QString markerPath();
|
||||
|
||||
public Q_SLOTS:
|
||||
void lock();
|
||||
void unlock();
|
||||
|
||||
Q_SIGNALS:
|
||||
void lockedChanged(bool locked);
|
||||
void confirmed();
|
||||
|
||||
private:
|
||||
friend class Lock;
|
||||
void onLocked();
|
||||
void onFinished();
|
||||
void addScreen(QScreen *screen);
|
||||
void removeScreen(QScreen *screen);
|
||||
void release();
|
||||
|
||||
QQmlEngine *m_engine;
|
||||
BubbleController *m_bubble;
|
||||
LockScreenController *m_screen;
|
||||
UserConfig *m_config;
|
||||
LockIntegration *m_integration = nullptr;
|
||||
Lock *m_lock = nullptr;
|
||||
bool m_confirmed = false;
|
||||
bool m_unlockWanted = false;
|
||||
QHash<QScreen *, QPointer<QQuickView>> m_views;
|
||||
// The pictures behind it, by output (see Wallpaper).
|
||||
QHash<QString, QUrl> m_pictures;
|
||||
};
|
||||
@@ -29,6 +29,9 @@ QString UserConfig::path()
|
||||
void UserConfig::load()
|
||||
{
|
||||
const KeyValueFile kv = KeyValueFile::load(path());
|
||||
m_enabled = kv.boolean(QStringLiteral("Enabled"), false);
|
||||
m_lockWallpaper = kv.value(QStringLiteral("LockWallpaper"));
|
||||
m_lockBlur = kv.boolean(QStringLiteral("LockBlur"), false);
|
||||
m_lockScreen = kv.boolean(QStringLiteral("LockScreen"), true);
|
||||
m_scanOnWake = kv.boolean(QStringLiteral("ScanOnWake"), true);
|
||||
m_scanOnLock = kv.boolean(QStringLiteral("ScanOnLock"), false);
|
||||
|
||||
@@ -17,6 +17,22 @@ class UserConfig : public QObject
|
||||
public:
|
||||
explicit UserConfig(QObject *parent = nullptr);
|
||||
|
||||
// Face unlock turned on at all. The agent's service only runs when it is,
|
||||
// but the own lock screen (--lock) runs either way.
|
||||
bool enabled() const
|
||||
{
|
||||
return m_enabled;
|
||||
}
|
||||
// The picture behind the own lock screen (see Wallpaper): empty for the
|
||||
// desktop's, "none", a file, or a folder to take one from at random.
|
||||
QString lockWallpaper() const
|
||||
{
|
||||
return m_lockWallpaper;
|
||||
}
|
||||
bool lockBlur() const
|
||||
{
|
||||
return m_lockBlur;
|
||||
}
|
||||
// Unlock the lock screen by face.
|
||||
bool lockScreen() const
|
||||
{
|
||||
@@ -72,6 +88,9 @@ private:
|
||||
void load();
|
||||
|
||||
QFileSystemWatcher m_watcher;
|
||||
bool m_enabled = false;
|
||||
QString m_lockWallpaper;
|
||||
bool m_lockBlur = false;
|
||||
bool m_lockScreen = true;
|
||||
bool m_scanOnWake = true;
|
||||
bool m_scanOnLock = false;
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "wallpaper.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QProcess>
|
||||
#include <QRandomGenerator>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
// A picture, or one at random from a folder.
|
||||
QUrl picture(const QString &setting)
|
||||
{
|
||||
QString path = setting;
|
||||
if (path.startsWith(u"~/")) {
|
||||
path = QDir::homePath() + path.mid(1);
|
||||
}
|
||||
const QFileInfo info(path);
|
||||
if (setting.isEmpty() || !info.exists()) {
|
||||
return {};
|
||||
}
|
||||
if (info.isFile()) {
|
||||
return QUrl::fromLocalFile(info.absoluteFilePath());
|
||||
}
|
||||
const QStringList pictures = QDir(path).entryList({QStringLiteral("*.jpg"), QStringLiteral("*.jpeg"), QStringLiteral("*.png"), QStringLiteral("*.webp")},
|
||||
QDir::Files | QDir::Readable);
|
||||
if (pictures.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return QUrl::fromLocalFile(QDir(path).absoluteFilePath(pictures.at(QRandomGenerator::global()->bounded(int(pictures.size())))));
|
||||
}
|
||||
|
||||
// What a command prints, or nothing when it is not there or hangs.
|
||||
QString run(const QString &program, const QStringList &args)
|
||||
{
|
||||
QProcess process;
|
||||
process.start(program, args);
|
||||
if (!process.waitForFinished(1000) || process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
|
||||
process.kill();
|
||||
process.waitForFinished(100);
|
||||
return {};
|
||||
}
|
||||
return QString::fromLocal8Bit(process.readAllStandardOutput());
|
||||
}
|
||||
|
||||
// The pictures the running wallpaper programs of this user show.
|
||||
QHash<QString, QString> desktop()
|
||||
{
|
||||
QHash<QString, QString> found;
|
||||
const uint me = getuid();
|
||||
const QStringList pids = QDir(QStringLiteral("/proc")).entryList({QStringLiteral("[0-9]*")}, QDir::Dirs);
|
||||
for (const QString &pid : pids) {
|
||||
const QString dir = QStringLiteral("/proc/") + pid;
|
||||
if (QFileInfo(dir).ownerId() != me) {
|
||||
continue;
|
||||
}
|
||||
QFile comm(dir + QStringLiteral("/comm"));
|
||||
if (!comm.open(QIODevice::ReadOnly)) {
|
||||
continue;
|
||||
}
|
||||
const QByteArray name = comm.readAll().trimmed();
|
||||
if (name == "swaybg") {
|
||||
QFile cmdline(dir + QStringLiteral("/cmdline"));
|
||||
if (cmdline.open(QIODevice::ReadOnly)) {
|
||||
QStringList args;
|
||||
for (const QByteArray &arg : cmdline.readAll().split('\0')) {
|
||||
args << QString::fromLocal8Bit(arg);
|
||||
}
|
||||
// Relative to where it was started.
|
||||
const QDir cwd(QFileInfo(dir + QStringLiteral("/cwd")).symLinkTarget());
|
||||
QHash<QString, QString> shown = Wallpaper::fromSwaybg(args.mid(1));
|
||||
for (QString &path : shown) {
|
||||
path = cwd.absoluteFilePath(path);
|
||||
}
|
||||
found.insert(shown);
|
||||
}
|
||||
} else if (name == "awww-daemon" || name == "swww-daemon") {
|
||||
found.insert(Wallpaper::fromAwww(run(QString::fromLatin1(name.left(4)), {QStringLiteral("query")})));
|
||||
} else if (name == "hyprpaper") {
|
||||
found.insert(Wallpaper::fromList(run(QStringLiteral("hyprctl"), {QStringLiteral("hyprpaper"), QStringLiteral("listactive")})));
|
||||
} else if (name == "wpaperd") {
|
||||
found.insert(Wallpaper::fromList(run(QStringLiteral("wpaperctl"), {QStringLiteral("all-wallpapers")})));
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
QHash<QString, QString> Wallpaper::fromSwaybg(const QStringList &args)
|
||||
{
|
||||
QHash<QString, QString> found;
|
||||
QString output;
|
||||
for (int i = 0; i < args.size(); ++i) {
|
||||
const QString &arg = args.at(i);
|
||||
const bool hasNext = i + 1 < args.size();
|
||||
if ((arg == u"-o" || arg == u"--output") && hasNext) {
|
||||
output = args.at(++i);
|
||||
} else if (arg.startsWith(u"--output=")) {
|
||||
output = arg.mid(9);
|
||||
} else if ((arg == u"-i" || arg == u"--image") && hasNext) {
|
||||
found.insert(output == u"*" ? QString() : output, args.at(++i));
|
||||
} else if (arg.startsWith(u"--image=")) {
|
||||
found.insert(output == u"*" ? QString() : output, arg.mid(8));
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
QHash<QString, QString> Wallpaper::fromAwww(const QString &text)
|
||||
{
|
||||
// "eDP-1: 1920x1080, scale: 1, currently displaying: image: /a/b.jpg",
|
||||
// with a namespace in front on awww. A plain colour is no picture.
|
||||
static const QRegularExpression line(QStringLiteral("([^\\s:]+): \\d+x\\d+,.*currently displaying: image: (.+)$"), QRegularExpression::MultilineOption);
|
||||
QHash<QString, QString> found;
|
||||
auto it = line.globalMatch(text);
|
||||
while (it.hasNext()) {
|
||||
const auto match = it.next();
|
||||
found.insert(match.captured(1), match.captured(2).trimmed());
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
QHash<QString, QString> Wallpaper::fromList(const QString &text)
|
||||
{
|
||||
// "eDP-1: /a/b.jpg", on older hyprpaper "eDP-1 = /a/b.jpg".
|
||||
static const QRegularExpression line(QStringLiteral("^([^\\s:=]+)(?:: | = )(/.+)$"), QRegularExpression::MultilineOption);
|
||||
QHash<QString, QString> found;
|
||||
auto it = line.globalMatch(text);
|
||||
while (it.hasNext()) {
|
||||
const auto match = it.next();
|
||||
found.insert(match.captured(1), match.captured(2).trimmed());
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
QHash<QString, QUrl> Wallpaper::forLock(const QString &setting)
|
||||
{
|
||||
QHash<QString, QUrl> pictures;
|
||||
if (setting.compare(u"none", Qt::CaseInsensitive) == 0) {
|
||||
return pictures;
|
||||
}
|
||||
if (!setting.isEmpty()) {
|
||||
const QUrl url = picture(setting);
|
||||
if (!url.isEmpty()) {
|
||||
pictures.insert(QString(), url);
|
||||
}
|
||||
return pictures;
|
||||
}
|
||||
const QHash<QString, QString> shown = desktop();
|
||||
for (auto it = shown.cbegin(); it != shown.cend(); ++it) {
|
||||
const QUrl url = picture(it.value());
|
||||
if (!url.isEmpty()) {
|
||||
pictures.insert(it.key(), url);
|
||||
}
|
||||
}
|
||||
return pictures;
|
||||
}
|
||||
|
||||
QUrl Wallpaper::pick(const QHash<QString, QUrl> &pictures, const QString &output)
|
||||
{
|
||||
if (pictures.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
if (pictures.contains(output)) {
|
||||
return pictures.value(output);
|
||||
}
|
||||
if (pictures.contains(QString())) {
|
||||
return pictures.value(QString());
|
||||
}
|
||||
// Named differently than here: some picture of the desktop is still
|
||||
// better than none.
|
||||
QStringList outputs = pictures.keys();
|
||||
outputs.sort();
|
||||
return pictures.value(outputs.first());
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// The picture behind face-unlock's own lock screen. By default the one on
|
||||
// the desktop. Hyprland, niri and the like leave the wallpaper to a program
|
||||
// of its own, so it comes from whichever of the common ones runs: swaybg,
|
||||
// awww (once swww), hyprpaper or wpaperd, each for every output.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
|
||||
namespace Wallpaper
|
||||
{
|
||||
// The pictures for a lock by output name, "" for every other output.
|
||||
// setting is LockWallpaper: empty for the desktop's, "none", a picture, or a
|
||||
// folder to take one from at random.
|
||||
QHash<QString, QUrl> forLock(const QString &setting);
|
||||
// The one for this output.
|
||||
QUrl pick(const QHash<QString, QUrl> &pictures, const QString &output);
|
||||
|
||||
// What the programs tell, by output name. Apart for the tests.
|
||||
QHash<QString, QString> fromSwaybg(const QStringList &args);
|
||||
// `awww query` and `swww query`.
|
||||
QHash<QString, QString> fromAwww(const QString &text);
|
||||
// `hyprctl hyprpaper listactive` and `wpaperctl all-wallpapers`.
|
||||
QHash<QString, QString> fromList(const QString &text);
|
||||
}
|
||||
+21
-6
@@ -21,6 +21,12 @@ void onGlobalRemove(void *, wl_registry *, uint32_t)
|
||||
|
||||
const wl_registry_listener listener = {onGlobal, onGlobalRemove};
|
||||
|
||||
wl_display *display()
|
||||
{
|
||||
auto *app = qGuiApp ? qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>() : nullptr;
|
||||
return app ? app->display() : nullptr;
|
||||
}
|
||||
|
||||
const QSet<QByteArray> &globals()
|
||||
{
|
||||
static QSet<QByteArray> names;
|
||||
@@ -30,17 +36,16 @@ const QSet<QByteArray> &globals()
|
||||
}
|
||||
asked = true;
|
||||
|
||||
auto *app = qGuiApp ? qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>() : nullptr;
|
||||
wl_display *display = app ? app->display() : nullptr;
|
||||
if (!display) {
|
||||
wl_display *d = display();
|
||||
if (!d) {
|
||||
return names;
|
||||
}
|
||||
wl_event_queue *queue = wl_display_create_queue(display);
|
||||
auto *wrapped = static_cast<wl_display *>(wl_proxy_create_wrapper(display));
|
||||
wl_event_queue *queue = wl_display_create_queue(d);
|
||||
auto *wrapped = static_cast<wl_display *>(wl_proxy_create_wrapper(d));
|
||||
wl_proxy_set_queue(reinterpret_cast<wl_proxy *>(wrapped), queue);
|
||||
wl_registry *registry = wl_display_get_registry(wrapped);
|
||||
wl_registry_add_listener(registry, &listener, &names);
|
||||
wl_display_roundtrip_queue(display, queue);
|
||||
wl_display_roundtrip_queue(d, queue);
|
||||
wl_registry_destroy(registry);
|
||||
wl_proxy_wrapper_destroy(wrapped);
|
||||
wl_event_queue_destroy(queue);
|
||||
@@ -52,3 +57,13 @@ bool Wayland::hasGlobal(const char *interface)
|
||||
{
|
||||
return globals().contains(QByteArray(interface));
|
||||
}
|
||||
|
||||
void Wayland::sync()
|
||||
{
|
||||
// On a queue of its own, like above: Qt reads the default one.
|
||||
if (wl_display *d = display()) {
|
||||
wl_event_queue *queue = wl_display_create_queue(d);
|
||||
wl_display_roundtrip_queue(d, queue);
|
||||
wl_event_queue_destroy(queue);
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,7 @@ namespace Wayland
|
||||
// "zwlr_layer_shell_v1". Asked once, on an event queue of its own, so Qt's
|
||||
// own handling of the connection is not disturbed.
|
||||
bool hasGlobal(const char *interface);
|
||||
|
||||
// Wait until the compositor has handled everything sent so far.
|
||||
void sync();
|
||||
} // namespace Wayland
|
||||
@@ -197,6 +197,7 @@ fu_do_help() {
|
||||
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")
|
||||
lock $(fu_msg "Lock the screen")
|
||||
status $(fu_msg "Show what is on")
|
||||
-h, --help $(fu_msg "Show this help")
|
||||
-V, --version $(fu_msg "Show the version")
|
||||
@@ -236,6 +237,7 @@ main() {
|
||||
remove|delete) fu_do_remove "$@" ;;
|
||||
test|try) fu_ensure_service && fu_test ;;
|
||||
status) fu_do_status ;;
|
||||
lock) exec "$FU_AGENT" --lock ;;
|
||||
|
||||
-h|--help|help) fu_do_help ;;
|
||||
-V|--version) printf '%s %s\n' "$FU_NAME" "$FU_VERSION" ;;
|
||||
|
||||
+3
-1
@@ -31,7 +31,9 @@ FU_SYSCONFIG="${FU_SYSCONFIG:-/etc/${FU_NAME}/config}"
|
||||
FU_UNIT_SOCKET="face-unlockd.socket"
|
||||
FU_UNIT_AGENT="face-unlock-agent.service"
|
||||
|
||||
# The desktop this runs in: plasma, gnome, hyprland, niri or other.
|
||||
# The desktop this runs in: plasma, gnome, hyprland, niri or other. All but
|
||||
# the first two have a lock screen that is a program of its own, and there
|
||||
# face-unlock's own one (`face-unlock lock`) is on offer.
|
||||
case ":${XDG_CURRENT_DESKTOP:-}:" in
|
||||
*:KDE:*) FU_DESKTOP=plasma ;;
|
||||
*:GNOME:*) FU_DESKTOP=gnome ;;
|
||||
|
||||
+39
-3
@@ -277,16 +277,20 @@ fu_ui_status() {
|
||||
# scope user (this user's file), sys (the system file, through sudo),
|
||||
# pam (the service of that name, through sudo), or group for a
|
||||
# heading, with only the label after it
|
||||
# type bool, choice (steps through the choices) or camera
|
||||
# type bool, choice (steps through the choices), camera, or path (typed
|
||||
# in; empty for the desktop's picture)
|
||||
# needs a bool setting this one does nothing without; it is dimmed while
|
||||
# that is off
|
||||
# only lockers: only where the lock screen is a program of its own with a
|
||||
# PAM file (Hyprland, Niri), see fu_pam_lockers_here
|
||||
# PAM file (Hyprland, Niri), see fu_pam_lockers_here; ownlock: only
|
||||
# where face-unlock's own lock screen can be used
|
||||
FU_SETTINGS=(
|
||||
"group|Lock screen"
|
||||
"user|LockScreen|bool|yes|Unlock with your face"
|
||||
"user|ScanOnWake|bool|yes|Scan when you come back||LockScreen"
|
||||
"user|ScanOnLock|bool|no|Scan right after locking||LockScreen"
|
||||
"user|LockWallpaper|path||Wallpaper|||ownlock"
|
||||
"user|LockBlur|bool|no|Blur the wallpaper|||ownlock"
|
||||
"group|Password prompts"
|
||||
"pam|sudo|bool|no|sudo in a terminal"
|
||||
"pam|polkit-1|bool|no|Admin prompts"
|
||||
@@ -315,7 +319,7 @@ fu_setting_help() {
|
||||
return
|
||||
;;
|
||||
hyprland:LockScreen|niri:LockScreen)
|
||||
fu_msg "Scans when you come back to hyprlock or swaylock and opens them. Any other lock screen scans when you press Enter on the empty password field."
|
||||
fu_msg "Scans when you come back to hyprlock, swaylock or face-unlock's own lock screen (\`face-unlock lock\`, with the bubble), and opens them. Other lock screens scan on Enter."
|
||||
return
|
||||
;;
|
||||
esac
|
||||
@@ -324,6 +328,8 @@ fu_setting_help() {
|
||||
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." ;;
|
||||
LockWallpaper:*) fu_msg "The picture behind face-unlock's own lock screen (\`face-unlock lock\`). Empty: the one on your desktop. Or a file, a folder to take one from at random, or \`none\`. Enter to type it." ;;
|
||||
LockBlur:*) fu_msg "Blurs the picture behind face-unlock's own lock screen." ;;
|
||||
lockscreens:*) fu_msg "The lock screen takes your face in its password check. Press Enter on the empty field to scan. Found here: %s." "$(fu_pam_lockers_list)" ;;
|
||||
polkit-1:*) fu_msg "The password windows of your desktop 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." ;;
|
||||
@@ -451,6 +457,24 @@ _fu_camera_label() {
|
||||
printf '%s %s' "$value" "$(fu_msg "(not connected)")"
|
||||
}
|
||||
|
||||
# _fu_path_label <path>
|
||||
# A path as it fits in the list: with ~ for home, and cut from the left.
|
||||
# Empty is the desktop's picture.
|
||||
_fu_path_label() {
|
||||
local path="$1"
|
||||
if [[ -z $path ]]; then
|
||||
fu_msg "same as desktop"
|
||||
return
|
||||
fi
|
||||
if [[ ${path,,} == none ]]; then
|
||||
fu_msg "none"
|
||||
return
|
||||
fi
|
||||
[[ $path == "$HOME"/* ]] && path="~${path#"$HOME"}"
|
||||
(( ${#path} > 36 )) && path="…${path: -35}"
|
||||
printf '%s' "$path"
|
||||
}
|
||||
|
||||
# _fu_setting_change <scope> <Key> <type> <current> <choices> <step>
|
||||
_fu_setting_change() {
|
||||
local scope="$1" key="$2" type="$3" current="$4" options="$5" step="$6" next
|
||||
@@ -459,6 +483,16 @@ _fu_setting_change() {
|
||||
bool) if fu_is_true "$current"; then next=no; else next=yes; fi ;;
|
||||
choice) next="$(_fu_next_choice "$current" "$options" "$step")" ;;
|
||||
camera) next="$(_fu_next_camera "$current" "$step")" ;;
|
||||
path)
|
||||
printf '\n %s ' "$(fu_msg "Picture or folder:")"
|
||||
fu_ui_read_line "$current" || return 0
|
||||
next="$FU_LINE_RESULT"
|
||||
# shellcheck disable=SC2088 # typed by the user, expanded here
|
||||
if [[ -n $next && ${next,,} != none && ! -e ${next/#\~/$HOME} ]]; then
|
||||
fu_bad "$(fu_msg "Not found: %s" "$next")"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$scope" in
|
||||
@@ -501,6 +535,7 @@ fu_ui_settings() {
|
||||
for spec in "${FU_SETTINGS[@]}"; do
|
||||
IFS='|' read -r scope key type default label choice need only <<< "$spec"
|
||||
[[ $only == lockers ]] && ! fu_pam_lockers_here && continue
|
||||
[[ $only == ownlock && ( $FU_DESKTOP == plasma || $FU_DESKTOP == gnome ) ]] && continue
|
||||
# A heading has its label where the key would be.
|
||||
[[ $scope == group ]] && label="$key" key=''
|
||||
scopes+=("$scope"); keys+=("$key"); types+=("$type"); defaults+=("$default")
|
||||
@@ -568,6 +603,7 @@ fu_ui_settings() {
|
||||
fi
|
||||
;;
|
||||
camera) shown="$(_fu_camera_label "${values[i]}")" ;;
|
||||
path) shown="$(_fu_path_label "${values[i]}")" ;;
|
||||
*) shown="$(fu_value_label "${keys[i]}" "${values[i]}")" ;;
|
||||
esac
|
||||
# Arrows on the selected choice: left and right step through it.
|
||||
|
||||
Reference in new issue
Block a user