feat: ask which lock screen to use on hyprland and niri
This commit is contained in:
1 parent
5699158bb1
commit
1da82f21fc
34 files changed
+5319
-1609
No files matched your search
+2
-1
@@ -1,7 +1,8 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// The lock screens that are programs of their own and take signals from
|
||||
// outside: hyprlock, swaylock and gtklock open on SIGUSR1.
|
||||
// outside: hyprlock, swaylock and gtklock open on SIGUSR1, hyprlock reads
|
||||
// its labels again on SIGUSR2.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -0,0 +1,647 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "lockpreview.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QHash>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QPointF>
|
||||
#include <QStandardPaths>
|
||||
#include <QTime>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include <algorithm>
|
||||
#include <pwd.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
QString readFile(const QString &path)
|
||||
{
|
||||
QFile f(path);
|
||||
return f.open(QIODevice::ReadOnly | QIODevice::Text) ? QString::fromUtf8(f.readAll()) : QString();
|
||||
}
|
||||
|
||||
QString expandHome(const QString &path)
|
||||
{
|
||||
return path == u"~" ? QDir::homePath() : path.startsWith(u"~/") ? QDir::homePath() + path.mid(1) : path;
|
||||
}
|
||||
|
||||
QString configHome()
|
||||
{
|
||||
return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation);
|
||||
}
|
||||
|
||||
// The first file of name under the user's and then the system's config
|
||||
// directories, as hyprlock and swaylock look for theirs.
|
||||
QString findConfig(const QString &name)
|
||||
{
|
||||
QStringList dirs{configHome()};
|
||||
dirs += QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation);
|
||||
for (const QString &dir : std::as_const(dirs)) {
|
||||
if (QFileInfo::exists(dir + u'/' + name)) {
|
||||
return dir + u'/' + name;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool truthy(const QString &value)
|
||||
{
|
||||
return value == u"1" || value.compare(u"true", Qt::CaseInsensitive) == 0 || value.compare(u"yes", Qt::CaseInsensitive) == 0
|
||||
|| value.compare(u"on", Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// hyprlang, as far as a lock screen's config needs it
|
||||
|
||||
struct Section {
|
||||
QString name;
|
||||
QHash<QString, QString> values;
|
||||
};
|
||||
|
||||
// Sections, $variables, source = (with ~ and wildcards, relative to the
|
||||
// file), and # comments, where ## stands for a #.
|
||||
class Hyprlang
|
||||
{
|
||||
public:
|
||||
void parse(const QString &path, int depth = 0);
|
||||
|
||||
QList<Section> sections;
|
||||
|
||||
private:
|
||||
QString expand(const QString &value) const;
|
||||
|
||||
QHash<QString, QString> m_vars;
|
||||
QList<qsizetype> m_open;
|
||||
};
|
||||
|
||||
QString stripComment(const QString &line)
|
||||
{
|
||||
QString out;
|
||||
for (qsizetype i = 0; i < line.size(); ++i) {
|
||||
if (line.at(i) == u'#') {
|
||||
if (i + 1 < line.size() && line.at(i + 1) == u'#') {
|
||||
out += u'#';
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
out += line.at(i);
|
||||
}
|
||||
return out.trimmed();
|
||||
}
|
||||
|
||||
void Hyprlang::parse(const QString &path, int depth)
|
||||
{
|
||||
if (depth > 8) {
|
||||
return;
|
||||
}
|
||||
const QString dir = QFileInfo(path).absolutePath();
|
||||
const QStringList lines = readFile(path).split(u'\n');
|
||||
for (const QString &raw : lines) {
|
||||
const QString line = stripComment(raw);
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (line == u"}") {
|
||||
if (!m_open.isEmpty()) {
|
||||
m_open.removeLast();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (line.endsWith(u'{')) {
|
||||
sections.append({line.chopped(1).trimmed(), {}});
|
||||
m_open.append(sections.size() - 1);
|
||||
continue;
|
||||
}
|
||||
const qsizetype eq = line.indexOf(u'=');
|
||||
if (eq < 0) {
|
||||
continue;
|
||||
}
|
||||
const QString key = line.left(eq).trimmed();
|
||||
const QString value = expand(line.mid(eq + 1).trimmed());
|
||||
if (key.startsWith(u'$')) {
|
||||
m_vars.insert(key.mid(1), value);
|
||||
} else if (key == u"source" && m_open.isEmpty()) {
|
||||
const QFileInfo pattern(QDir(dir).absoluteFilePath(expandHome(value)));
|
||||
const QStringList matches = QDir(pattern.absolutePath()).entryList({pattern.fileName()}, QDir::Files, QDir::Name);
|
||||
for (const QString &match : matches) {
|
||||
parse(pattern.absolutePath() + u'/' + match, depth + 1);
|
||||
}
|
||||
} else if (!m_open.isEmpty()) {
|
||||
sections[m_open.last()].values.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString Hyprlang::expand(const QString &value) const
|
||||
{
|
||||
if (!value.contains(u'$') || m_vars.isEmpty()) {
|
||||
return value;
|
||||
}
|
||||
// Longest first, so $font does not eat the start of $fontsize.
|
||||
QStringList names = m_vars.keys();
|
||||
std::sort(names.begin(), names.end(), [](const QString &a, const QString &b) {
|
||||
return a.size() > b.size();
|
||||
});
|
||||
QString out = value;
|
||||
for (const QString &name : std::as_const(names)) {
|
||||
out.replace(u'$' + name, m_vars.value(name));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Values
|
||||
|
||||
// "rgba(a, b) rgba(c) 45deg": the colours and the angle.
|
||||
QVariantMap gradient(const QString &value)
|
||||
{
|
||||
QVariantList colors;
|
||||
double angle = 0;
|
||||
QString token;
|
||||
int depth = 0;
|
||||
const auto flush = [&] {
|
||||
const QString t = token.trimmed();
|
||||
token.clear();
|
||||
if (t.endsWith(u"deg")) {
|
||||
angle = t.chopped(3).toDouble();
|
||||
} else if (const QColor c = LockPreview::color(t); c.isValid()) {
|
||||
colors.append(c);
|
||||
}
|
||||
};
|
||||
for (const QChar ch : value) {
|
||||
if (ch == u'(') {
|
||||
++depth;
|
||||
} else if (ch == u')') {
|
||||
--depth;
|
||||
}
|
||||
if (ch.isSpace() && depth == 0) {
|
||||
flush();
|
||||
} else {
|
||||
token += ch;
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return {{QStringLiteral("colors"), colors}, {QStringLiteral("angle"), angle}};
|
||||
}
|
||||
|
||||
// "x, y", each in pixels or percent of the screen.
|
||||
QPointF layout(const QString &value, const QSizeF &screen)
|
||||
{
|
||||
const QStringList parts = value.split(u',');
|
||||
const auto one = [](const QString &part, qreal whole) {
|
||||
const QString s = part.trimmed();
|
||||
return s.endsWith(u'%') ? s.chopped(1).toDouble() / 100.0 * whole : s.toDouble();
|
||||
};
|
||||
return {one(parts.value(0), screen.width()), one(parts.value(1), screen.height())};
|
||||
}
|
||||
|
||||
// A Pango font description ("Noto Sans Light 12") as family, weight and
|
||||
// slant.
|
||||
void font(const QString &description, QVariantMap &into)
|
||||
{
|
||||
static const QHash<QString, int> weights{
|
||||
{QStringLiteral("thin"), 100}, {QStringLiteral("ultra-light"), 200}, {QStringLiteral("extra-light"), 200},
|
||||
{QStringLiteral("ultralight"), 200}, {QStringLiteral("extralight"), 200}, {QStringLiteral("light"), 300},
|
||||
{QStringLiteral("semi-light"), 350}, {QStringLiteral("book"), 380}, {QStringLiteral("regular"), 400},
|
||||
{QStringLiteral("normal"), 400}, {QStringLiteral("medium"), 500}, {QStringLiteral("semi-bold"), 600},
|
||||
{QStringLiteral("semibold"), 600}, {QStringLiteral("demi-bold"), 600}, {QStringLiteral("demibold"), 600},
|
||||
{QStringLiteral("bold"), 700}, {QStringLiteral("ultra-bold"), 800}, {QStringLiteral("extra-bold"), 800},
|
||||
{QStringLiteral("extrabold"), 800}, {QStringLiteral("heavy"), 900}, {QStringLiteral("black"), 900},
|
||||
};
|
||||
QStringList words = description.section(u',', 0, 0).split(u' ', Qt::SkipEmptyParts);
|
||||
int weight = 400;
|
||||
bool italic = false;
|
||||
while (words.size() > 1) {
|
||||
const QString word = words.last().toLower();
|
||||
bool number = false;
|
||||
word.toDouble(&number);
|
||||
if (number) {
|
||||
words.removeLast();
|
||||
} else if (word == u"italic" || word == u"oblique") {
|
||||
italic = true;
|
||||
words.removeLast();
|
||||
} else if (weights.contains(word)) {
|
||||
weight = weights.value(word);
|
||||
words.removeLast();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
into.insert(QStringLiteral("family"), words.isEmpty() ? QStringLiteral("Sans") : words.join(u' '));
|
||||
into.insert(QStringLiteral("weight"), weight);
|
||||
into.insert(QStringLiteral("italic"), italic);
|
||||
}
|
||||
|
||||
// Pango markup as Qt's StyledText: span becomes font, b and i, the rest of
|
||||
// Pango's tags go and their text stays.
|
||||
QString styled(const QString &markup)
|
||||
{
|
||||
static const QRegularExpression tag(QStringLiteral("<(/?)([a-zA-Z]+)([^>]*)>"));
|
||||
static const QRegularExpression attribute(QStringLiteral("([a-z_]+)\\s*=\\s*[\"']([^\"']*)[\"']"));
|
||||
QString out;
|
||||
QStringList closers;
|
||||
qsizetype last = 0;
|
||||
auto it = tag.globalMatch(markup);
|
||||
while (it.hasNext()) {
|
||||
const auto m = it.next();
|
||||
out += markup.mid(last, m.capturedStart() - last);
|
||||
last = m.capturedEnd();
|
||||
const bool closing = !m.captured(1).isEmpty();
|
||||
const QString name = m.captured(2).toLower();
|
||||
if (name == u"span") {
|
||||
if (closing) {
|
||||
out += closers.isEmpty() ? QString() : closers.takeLast();
|
||||
continue;
|
||||
}
|
||||
QString open;
|
||||
QString close;
|
||||
QString fontAttrs;
|
||||
auto attrs = attribute.globalMatch(m.captured(3));
|
||||
while (attrs.hasNext()) {
|
||||
const auto a = attrs.next();
|
||||
const QString key = a.captured(1);
|
||||
QString value = a.captured(2);
|
||||
if (key == u"foreground" || key == u"fgcolor" || key == u"color") {
|
||||
// Pango's #RRGGBBAA is Qt's #AARRGGBB.
|
||||
if (value.startsWith(u'#') && value.size() == 9) {
|
||||
value = u'#' + value.mid(7, 2) + value.mid(1, 6);
|
||||
}
|
||||
fontAttrs += QStringLiteral(" color=\"%1\"").arg(value);
|
||||
} else if (key == u"font_family" || key == u"face") {
|
||||
fontAttrs += QStringLiteral(" face=\"%1\"").arg(value);
|
||||
} else if ((key == u"weight" || key == u"font_weight") && (value.contains(u"bold") || value.contains(u"heavy") || value.toInt() >= 600)) {
|
||||
open += QStringLiteral("<b>");
|
||||
close.prepend(QStringLiteral("</b>"));
|
||||
} else if ((key == u"style" || key == u"font_style") && value == u"italic") {
|
||||
open += QStringLiteral("<i>");
|
||||
close.prepend(QStringLiteral("</i>"));
|
||||
}
|
||||
}
|
||||
if (!fontAttrs.isEmpty()) {
|
||||
open.prepend(QStringLiteral("<font%1>").arg(fontAttrs));
|
||||
close += QStringLiteral("</font>");
|
||||
}
|
||||
out += open;
|
||||
closers.append(close);
|
||||
} else if (name == u"b" || name == u"i" || name == u"u") {
|
||||
out += m.captured(0);
|
||||
}
|
||||
}
|
||||
out += markup.mid(last);
|
||||
out.replace(u'\n', QStringLiteral("<br>"));
|
||||
return out;
|
||||
}
|
||||
|
||||
// What a command prints, or nothing when it takes too long.
|
||||
QString run(const QString &command, QElapsedTimer &budget)
|
||||
{
|
||||
// A config full of slow commands (weather, music) must not keep the
|
||||
// window from opening.
|
||||
if (budget.elapsed() > 2000) {
|
||||
return {};
|
||||
}
|
||||
QProcess process;
|
||||
process.start(QStringLiteral("/bin/sh"), {QStringLiteral("-c"), command});
|
||||
if (!process.waitForFinished(700)) {
|
||||
process.kill();
|
||||
process.waitForFinished(100);
|
||||
return {};
|
||||
}
|
||||
return QString::fromUtf8(process.readAllStandardOutput());
|
||||
}
|
||||
|
||||
// A label's text as hyprlock would show it right after locking.
|
||||
QString labelText(QString text, bool trim, QElapsedTimer &budget)
|
||||
{
|
||||
const passwd *pw = getpwuid(getuid());
|
||||
text.replace(QStringLiteral("$DESC"), pw ? QString::fromLocal8Bit(pw->pw_gecos) : QString());
|
||||
text.replace(QStringLiteral("$USER"), pw ? QString::fromLocal8Bit(pw->pw_name) : QString());
|
||||
text.replace(QStringLiteral("<br/>"), QStringLiteral("\n"));
|
||||
const QTime now = QTime::currentTime();
|
||||
text.replace(QStringLiteral("$TIME12"), now.toString(QStringLiteral("hh:mm AP")));
|
||||
text.replace(QStringLiteral("$TIME"), now.toString(QStringLiteral("HH:mm")));
|
||||
static const QRegularExpression layoutVar(QStringLiteral("\\$LAYOUT(\\[([^\\]]*)\\])?"));
|
||||
auto layouts = layoutVar.globalMatch(text);
|
||||
while (layouts.hasNext()) {
|
||||
const auto m = layouts.next();
|
||||
const QString first = m.captured(2).section(u',', 0, 0).trimmed();
|
||||
text.replace(m.captured(0), first.isEmpty() ? QStringLiteral("en") : first);
|
||||
}
|
||||
// What only a lock screen that runs knows: nothing has failed yet.
|
||||
static const QRegularExpression runtime(QStringLiteral("\\$(ATTEMPTS(\\[[^\\]]*\\])?|PAMFAIL|FPRINTFAIL|FPRINTPROMPT|FAIL)"));
|
||||
text.remove(runtime);
|
||||
text.replace(QStringLiteral("$PAMPROMPT"), QStringLiteral("Password:"));
|
||||
|
||||
if (text.startsWith(u"cmd[") && text.contains(u']')) {
|
||||
const QString command = text.mid(text.indexOf(u']') + 1);
|
||||
// face-unlock's own line, as it reads during a scan.
|
||||
text = command.contains(u"face-unlock/lock-text") ? i18n("Looking for your face…").toHtmlEscaped() : run(command, budget);
|
||||
}
|
||||
return trim ? text.trimmed() : text;
|
||||
}
|
||||
|
||||
void place(const Section &s, const QString &position, const QSizeF &screen, QVariantMap &into)
|
||||
{
|
||||
const QPointF pos = layout(s.values.value(QStringLiteral("position"), position), screen);
|
||||
into.insert(QStringLiteral("x"), pos.x());
|
||||
into.insert(QStringLiteral("y"), pos.y());
|
||||
into.insert(QStringLiteral("halign"), s.values.value(QStringLiteral("halign"), QStringLiteral("center")));
|
||||
into.insert(QStringLiteral("valign"), s.values.value(QStringLiteral("valign"), QStringLiteral("center")));
|
||||
into.insert(QStringLiteral("rotate"), s.values.value(QStringLiteral("rotate"), QStringLiteral("0")).toDouble());
|
||||
}
|
||||
|
||||
QColor colorOr(const QString &value, const QColor &fallback)
|
||||
{
|
||||
const QColor c = LockPreview::color(value);
|
||||
return c.isValid() ? c : fallback;
|
||||
}
|
||||
|
||||
// swaylock's colours: RRGGBB or RRGGBBAA, without a #.
|
||||
QColor hexColor(const QString &value, const QColor &fallback)
|
||||
{
|
||||
QString v = value.trimmed();
|
||||
if (v.startsWith(u'#')) {
|
||||
v = v.mid(1);
|
||||
}
|
||||
bool ok = false;
|
||||
const uint n = v.toUInt(&ok, 16);
|
||||
if (!ok || (v.size() != 6 && v.size() != 8)) {
|
||||
return fallback;
|
||||
}
|
||||
return v.size() == 6 ? QColor((n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff) : QColor((n >> 24) & 0xff, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff);
|
||||
}
|
||||
|
||||
QVariantMap wallpaperOnly(const QUrl &wallpaper)
|
||||
{
|
||||
return {{QStringLiteral("type"), QStringLiteral("background")},
|
||||
{QStringLiteral("color"), QColor(0x11, 0x11, 0x11)},
|
||||
{QStringLiteral("source"), wallpaper},
|
||||
{QStringLiteral("fill"), QStringLiteral("fill")},
|
||||
{QStringLiteral("blur"), 0.0},
|
||||
{QStringLiteral("dim"), 0.0}};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
QColor LockPreview::color(const QString &value)
|
||||
{
|
||||
const QString v = value.trimmed();
|
||||
static const QRegularExpression call(QStringLiteral("^(rgba?)\\((.*)\\)$"));
|
||||
const auto m = call.match(v);
|
||||
if (m.hasMatch()) {
|
||||
const QString inner = m.captured(2).trimmed();
|
||||
const QStringList parts = inner.split(u',');
|
||||
if (parts.size() >= 3) {
|
||||
QColor c(parts.at(0).trimmed().toInt(), parts.at(1).trimmed().toInt(), parts.at(2).trimmed().toInt());
|
||||
if (parts.size() >= 4) {
|
||||
c.setAlphaF(std::clamp(parts.at(3).trimmed().toDouble(), 0.0, 1.0));
|
||||
}
|
||||
return c;
|
||||
}
|
||||
return hexColor(inner, {});
|
||||
}
|
||||
bool ok = false;
|
||||
const qulonglong n = v.startsWith(u"0x", Qt::CaseInsensitive) ? v.mid(2).toULongLong(&ok, 16) : v.toULongLong(&ok, 10);
|
||||
if (!ok) {
|
||||
return {};
|
||||
}
|
||||
return QColor(int((n >> 16) & 0xff), int((n >> 8) & 0xff), int(n & 0xff), int((n >> 24) & 0xff));
|
||||
}
|
||||
|
||||
QString LockPreview::locker()
|
||||
{
|
||||
static const QStringList names{QStringLiteral("hyprlock"), QStringLiteral("swaylock"), QStringLiteral("gtklock"), QStringLiteral("waylock")};
|
||||
// Where a lock screen is started from: the idle daemons first, then the
|
||||
// keys. The first one named there, outside comments.
|
||||
static const QStringList places{QStringLiteral("hypr/hypridle.conf"), QStringLiteral("swayidle/config"), QStringLiteral("niri/config.kdl"),
|
||||
QStringLiteral("hypr/hyprland.lua"), QStringLiteral("hypr/hyprland.conf")};
|
||||
for (const QString &place : places) {
|
||||
for (const QString &line : readFile(configHome() + u'/' + place).split(u'\n')) {
|
||||
const QString code = line.trimmed();
|
||||
if (code.startsWith(u'#') || code.startsWith(u"//") || code.startsWith(u"--")) {
|
||||
continue;
|
||||
}
|
||||
qsizetype first = -1;
|
||||
QString found;
|
||||
for (const QString &name : names) {
|
||||
const qsizetype at = code.indexOf(name);
|
||||
if (at >= 0 && (first < 0 || at < first)) {
|
||||
first = at;
|
||||
found = name;
|
||||
}
|
||||
}
|
||||
if (!found.isEmpty()) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Started some other way (a script of its own): one that is set up.
|
||||
if (!QStandardPaths::findExecutable(QStringLiteral("hyprlock")).isEmpty() && !findConfig(QStringLiteral("hypr/hyprlock.conf")).isEmpty()) {
|
||||
return QStringLiteral("hyprlock");
|
||||
}
|
||||
for (const QString &name : names) {
|
||||
if (!QStandardPaths::findExecutable(name).isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QVariantList LockPreview::hyprlock(const QString &configPath, const QSizeF &screen, const QString &output, const QUrl &wallpaper)
|
||||
{
|
||||
Hyprlang config;
|
||||
config.parse(configPath);
|
||||
|
||||
bool trim = true;
|
||||
for (const Section &s : std::as_const(config.sections)) {
|
||||
if (s.name == u"general" && s.values.contains(QStringLiteral("text_trim"))) {
|
||||
trim = truthy(s.values.value(QStringLiteral("text_trim")));
|
||||
}
|
||||
}
|
||||
|
||||
QElapsedTimer budget;
|
||||
budget.start();
|
||||
QList<QPair<int, QVariantMap>> placed;
|
||||
bool ourLine = false;
|
||||
bool background = false;
|
||||
for (const Section &s : std::as_const(config.sections)) {
|
||||
const auto value = [&s](const char *key, const char *fallback) {
|
||||
return s.values.value(QString::fromLatin1(key), QString::fromLatin1(fallback));
|
||||
};
|
||||
const QString monitor = value("monitor", "");
|
||||
if (!monitor.isEmpty() && monitor != output && !monitor.startsWith(u"desc:")) {
|
||||
continue;
|
||||
}
|
||||
QVariantMap w;
|
||||
int z = value("zindex", "0").toInt();
|
||||
if (s.name == u"background") {
|
||||
background = true;
|
||||
z = value("zindex", "-1").toInt();
|
||||
const QString path = value("path", "");
|
||||
const int passes = value("blur_passes", "0").toInt();
|
||||
w = wallpaperOnly(path == u"screenshot" ? wallpaper : path.isEmpty() ? QUrl() : QUrl::fromLocalFile(expandHome(path)));
|
||||
w.insert(QStringLiteral("color"), colorOr(value("color", ""), QColor(0x11, 0x11, 0x11)));
|
||||
w.insert(QStringLiteral("blur"), passes > 0 ? std::min(1.0, passes * value("blur_size", "8").toDouble() / 24.0) : 0.0);
|
||||
// hyprlock's blur also darkens, by its brightness.
|
||||
w.insert(QStringLiteral("dim"), passes > 0 ? std::clamp(1.0 - value("brightness", "0.8172").toDouble(), 0.0, 1.0) : 0.0);
|
||||
} else if (s.name == u"label") {
|
||||
const QString text = value("text", "Sample Text");
|
||||
ourLine = ourLine || text.contains(u"face-unlock/lock-text");
|
||||
w.insert(QStringLiteral("type"), QStringLiteral("label"));
|
||||
w.insert(QStringLiteral("text"), styled(labelText(text, trim, budget)));
|
||||
font(value("font_family", "Sans"), w);
|
||||
w.insert(QStringLiteral("size"), value("font_size", "16").toDouble());
|
||||
w.insert(QStringLiteral("color"), colorOr(value("color", ""), Qt::white));
|
||||
w.insert(QStringLiteral("align"), value("text_align", "center"));
|
||||
w.insert(QStringLiteral("shadow"), value("shadow_passes", "0").toInt() > 0);
|
||||
w.insert(QStringLiteral("shadowSize"), value("shadow_size", "3").toDouble());
|
||||
w.insert(QStringLiteral("shadowColor"), colorOr(value("shadow_color", ""), Qt::black));
|
||||
place(s, QStringLiteral("0,0"), screen, w);
|
||||
} else if (s.name == u"input-field") {
|
||||
const QPointF size = layout(value("size", "400,90"), screen);
|
||||
w.insert(QStringLiteral("type"), QStringLiteral("input"));
|
||||
w.insert(QStringLiteral("width"), size.x());
|
||||
w.insert(QStringLiteral("height"), size.y());
|
||||
w.insert(QStringLiteral("thickness"), value("outline_thickness", "4").toDouble());
|
||||
w.insert(QStringLiteral("outer"), gradient(value("outer_color", "0xFF111111")));
|
||||
w.insert(QStringLiteral("inner"), colorOr(value("inner_color", ""), QColor(0xdd, 0xdd, 0xdd)));
|
||||
w.insert(QStringLiteral("fontColor"), colorOr(value("font_color", ""), Qt::black));
|
||||
font(value("font_family", "Sans"), w);
|
||||
w.insert(QStringLiteral("placeholder"), styled(labelText(value("placeholder_text", "<i>Input Password</i>"), trim, budget)));
|
||||
w.insert(QStringLiteral("rounding"), value("rounding", "-1").toDouble());
|
||||
place(s, QStringLiteral("0,0"), screen, w);
|
||||
} else if (s.name == u"shape") {
|
||||
const QPointF size = layout(value("size", "100,100"), screen);
|
||||
w.insert(QStringLiteral("type"), QStringLiteral("shape"));
|
||||
w.insert(QStringLiteral("width"), size.x());
|
||||
w.insert(QStringLiteral("height"), size.y());
|
||||
w.insert(QStringLiteral("color"), colorOr(value("color", ""), QColor(0x11, 0x11, 0x11)));
|
||||
w.insert(QStringLiteral("rounding"), value("rounding", "0").toDouble());
|
||||
w.insert(QStringLiteral("borderSize"), value("border_size", "0").toDouble());
|
||||
w.insert(QStringLiteral("border"), gradient(value("border_color", "0xFF00CFE6")));
|
||||
place(s, QStringLiteral("0,0"), screen, w);
|
||||
} else if (s.name == u"image") {
|
||||
const QString path = value("path", "");
|
||||
if (path.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
w.insert(QStringLiteral("type"), QStringLiteral("image"));
|
||||
w.insert(QStringLiteral("source"), QUrl::fromLocalFile(expandHome(path)));
|
||||
w.insert(QStringLiteral("size"), value("size", "150").toDouble());
|
||||
w.insert(QStringLiteral("rounding"), value("rounding", "-1").toDouble());
|
||||
w.insert(QStringLiteral("borderSize"), value("border_size", "4").toDouble());
|
||||
w.insert(QStringLiteral("border"), gradient(value("border_color", "0xFFDDDDDD")));
|
||||
place(s, QStringLiteral("0,0"), screen, w);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
placed.append({z, w});
|
||||
}
|
||||
|
||||
if (!background) {
|
||||
QVariantMap w = wallpaperOnly({});
|
||||
placed.append({-1, w});
|
||||
}
|
||||
// face-unlock's line, as the menu puts it in (fu_hyprlock_text_on),
|
||||
// for those who have not picked it yet.
|
||||
if (!ourLine) {
|
||||
QVariantMap w{{QStringLiteral("type"), QStringLiteral("label")},
|
||||
{QStringLiteral("text"), i18n("Looking for your face…").toHtmlEscaped()},
|
||||
{QStringLiteral("size"), 20.0},
|
||||
{QStringLiteral("color"), QColor(255, 255, 255, 242)},
|
||||
{QStringLiteral("align"), QStringLiteral("center")},
|
||||
{QStringLiteral("shadow"), true},
|
||||
{QStringLiteral("shadowSize"), 3.0},
|
||||
{QStringLiteral("shadowColor"), QColor(Qt::black)},
|
||||
{QStringLiteral("x"), 0.0},
|
||||
{QStringLiteral("y"), -48.0},
|
||||
{QStringLiteral("halign"), QStringLiteral("center")},
|
||||
{QStringLiteral("valign"), QStringLiteral("top")},
|
||||
{QStringLiteral("rotate"), 0.0}};
|
||||
font(QStringLiteral("Sans"), w);
|
||||
placed.append({10, w});
|
||||
}
|
||||
|
||||
std::stable_sort(placed.begin(), placed.end(), [](const auto &a, const auto &b) {
|
||||
return a.first < b.first;
|
||||
});
|
||||
QVariantList widgets;
|
||||
for (const auto &p : std::as_const(placed)) {
|
||||
widgets.append(p.second);
|
||||
}
|
||||
return widgets;
|
||||
}
|
||||
|
||||
QVariantList LockPreview::swaylock(const QString &configPath, const QUrl &wallpaper)
|
||||
{
|
||||
// One option per line, as on the command line without the dashes.
|
||||
QHash<QString, QString> o;
|
||||
for (const QString &raw : readFile(configPath).split(u'\n')) {
|
||||
const QString line = raw.trimmed();
|
||||
if (line.isEmpty() || line.startsWith(u'#')) {
|
||||
continue;
|
||||
}
|
||||
o.insert(line.section(u'=', 0, 0).trimmed(), line.contains(u'=') ? line.section(u'=', 1).trimmed() : QString());
|
||||
}
|
||||
|
||||
// An image may name its output first: [[<output>]:]<path>.
|
||||
QString image = o.value(QStringLiteral("image"));
|
||||
if (image.contains(u':') && !image.startsWith(u'/') && !image.startsWith(u'~')) {
|
||||
image = image.section(u':', 1);
|
||||
}
|
||||
QVariantMap bg = wallpaperOnly(o.contains(QStringLiteral("screenshots")) ? wallpaper
|
||||
: image.isEmpty() ? QUrl()
|
||||
: QUrl::fromLocalFile(expandHome(image)));
|
||||
bg.insert(QStringLiteral("color"), hexColor(o.value(QStringLiteral("color")), Qt::white));
|
||||
bg.insert(QStringLiteral("fill"), o.value(QStringLiteral("scaling"), QStringLiteral("fill")));
|
||||
// swaylock-effects: effect-blur=<radius>x<times>.
|
||||
const QString blur = o.value(QStringLiteral("effect-blur"));
|
||||
if (!blur.isEmpty()) {
|
||||
bg.insert(QStringLiteral("blur"), std::min(1.0, blur.section(u'x', 0, 0).toDouble() * blur.section(u'x', 1, 1).toDouble() / 24.0));
|
||||
}
|
||||
QVariantList widgets{bg};
|
||||
|
||||
// The ring shows only while typing, unless it is asked to stay.
|
||||
const bool clock = o.contains(QStringLiteral("clock"));
|
||||
if (o.contains(QStringLiteral("indicator")) || o.contains(QStringLiteral("indicator-idle-visible")) || clock) {
|
||||
const QDateTime now = QDateTime::currentDateTime();
|
||||
widgets.append(QVariantMap{
|
||||
{QStringLiteral("type"), QStringLiteral("ring")},
|
||||
{QStringLiteral("radius"), o.value(QStringLiteral("indicator-radius"), QStringLiteral("50")).toDouble()},
|
||||
{QStringLiteral("thickness"), o.value(QStringLiteral("indicator-thickness"), QStringLiteral("10")).toDouble()},
|
||||
{QStringLiteral("inside"), hexColor(o.value(QStringLiteral("inside-color")), QColor(0, 0, 0, 0xc0))},
|
||||
{QStringLiteral("ring"), hexColor(o.value(QStringLiteral("ring-color")), QColor(0x33, 0x7d, 0x00))},
|
||||
{QStringLiteral("textColor"), hexColor(o.value(QStringLiteral("text-color")), QColor(0xe5, 0xa4, 0x45))},
|
||||
{QStringLiteral("text"), clock ? now.toString(QStringLiteral("HH:mm")) + u'\n' + now.toString(QStringLiteral("yyyy-MM-dd")) : QString()},
|
||||
});
|
||||
}
|
||||
return widgets;
|
||||
}
|
||||
|
||||
QVariantList LockPreview::widgets(const QString &locker, const QSizeF &screen, const QString &output, const QUrl &wallpaper)
|
||||
{
|
||||
if (locker == u"hyprlock") {
|
||||
const QString path = findConfig(QStringLiteral("hypr/hyprlock.conf"));
|
||||
if (!path.isEmpty()) {
|
||||
return hyprlock(path, screen, output, wallpaper);
|
||||
}
|
||||
} else if (locker == u"swaylock") {
|
||||
QString path = findConfig(QStringLiteral("swaylock/config"));
|
||||
if (path.isEmpty() && QFileInfo::exists(QDir::homePath() + QStringLiteral("/.swaylock/config"))) {
|
||||
path = QDir::homePath() + QStringLiteral("/.swaylock/config");
|
||||
}
|
||||
return swaylock(path, wallpaper);
|
||||
}
|
||||
// Something this does not know how to draw: the desktop's picture.
|
||||
return {wallpaperOnly(wallpaper)};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// The user's own lock screen, rebuilt for its picture in the window that asks
|
||||
// which lock screen to use (LockChoice.qml): what hyprlock draws from its
|
||||
// config, with face-unlock's line at the top, or swaylock's background and
|
||||
// ring. A screenshot would take locking the screen. LockPreview.qml draws
|
||||
// what this returns.
|
||||
//
|
||||
// hyprlock places a widget from the corner or middle its halign and valign
|
||||
// name, with y upwards, and takes font sizes as points. Colours and sizes
|
||||
// here are hyprlang's: rgba(r, g, b, a), rgba(RRGGBBAA), 0xAARRGGBB, and
|
||||
// "x, y" in pixels or percent of the screen.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QSizeF>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QVariantList>
|
||||
|
||||
namespace LockPreview
|
||||
{
|
||||
// The lock screen the user starts: the one their idle daemon or key runs,
|
||||
// else one that is installed. hyprlock, swaylock, another name, or empty.
|
||||
QString locker();
|
||||
|
||||
// That lock screen's widgets, bottom first, as maps for LockPreview.qml.
|
||||
// output names the screen, for widgets meant for one monitor; wallpaper is
|
||||
// the desktop's picture, for a background that is a screenshot.
|
||||
QVariantList widgets(const QString &locker, const QSizeF &screen, const QString &output, const QUrl &wallpaper);
|
||||
|
||||
// The pieces, apart for the tests.
|
||||
QVariantList hyprlock(const QString &configPath, const QSizeF &screen, const QString &output, const QUrl &wallpaper);
|
||||
QVariantList swaylock(const QString &configPath, const QUrl &wallpaper);
|
||||
QColor color(const QString &value);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "locktext.h"
|
||||
|
||||
#include "bubblecontroller.h"
|
||||
#include "lockers.h"
|
||||
#include "userconfig.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QSaveFile>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
|
||||
#include <csignal>
|
||||
|
||||
LockText::LockText(BubbleController *bubble, UserConfig *config, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_bubble(bubble)
|
||||
, m_config(config)
|
||||
{
|
||||
connect(m_bubble, &BubbleController::phaseChanged, this, &LockText::update);
|
||||
connect(m_bubble, &BubbleController::messageChanged, this, &LockText::update);
|
||||
connect(m_config, &UserConfig::changed, this, &LockText::update);
|
||||
// Nothing left over from an agent before this one.
|
||||
write({});
|
||||
}
|
||||
|
||||
LockText::~LockText()
|
||||
{
|
||||
QFile::remove(path());
|
||||
}
|
||||
|
||||
QString LockText::path()
|
||||
{
|
||||
return QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + QStringLiteral("/face-unlock/lock-text");
|
||||
}
|
||||
|
||||
QString LockText::text(const QString &phase, const QString &message)
|
||||
{
|
||||
if (phase == u"scanning") {
|
||||
return message.isEmpty() ? i18n("Looking for your face…") : message;
|
||||
}
|
||||
if (phase == u"success") {
|
||||
return i18n("Face recognized");
|
||||
}
|
||||
// failure and lockout say why; hidden says nothing.
|
||||
return phase == u"hidden" ? QString() : message;
|
||||
}
|
||||
|
||||
void LockText::update()
|
||||
{
|
||||
const bool on = m_config->lockScreenStyle() == u"yours";
|
||||
const QString now = on ? text(m_bubble->phase(), m_bubble->message()) : QString();
|
||||
if (now != m_text) {
|
||||
write(now);
|
||||
// Left alone by those who did not pick it: their hyprlock has no
|
||||
// label to read it.
|
||||
m_tries = 0;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void LockText::refresh()
|
||||
{
|
||||
// A hyprlock that is only starting reads the file once it is up, but
|
||||
// may have read it just before this changed: asked again then.
|
||||
if (Lockers::signal(SIGUSR2, "hyprlock") > 0 && ++m_tries < 20) {
|
||||
QTimer::singleShot(250, this, &LockText::refresh);
|
||||
}
|
||||
}
|
||||
|
||||
void LockText::write(const QString &text)
|
||||
{
|
||||
m_text = text;
|
||||
QDir().mkpath(QFileInfo(path()).absolutePath());
|
||||
QSaveFile file(path());
|
||||
// hyprlock reads labels as Pango markup.
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
file.write(text.toHtmlEscaped().toUtf8());
|
||||
file.commit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// The bubble as a line of text, for a lock screen that covers it: hyprlock.
|
||||
// When the user keeps their own lock screen (LockScreenStyle=yours), the
|
||||
// menu puts a label into hyprlock's config that shows the file at path()
|
||||
// (see src/lib/system.sh). This writes what the bubble shows into it, and
|
||||
// SIGUSR2 makes hyprlock read it again.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class BubbleController;
|
||||
class UserConfig;
|
||||
|
||||
class LockText : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
LockText(BubbleController *bubble, UserConfig *config, QObject *parent = nullptr);
|
||||
~LockText() override;
|
||||
|
||||
static QString path();
|
||||
// The line for a phase and message of the bubble.
|
||||
static QString text(const QString &phase, const QString &message);
|
||||
|
||||
private:
|
||||
void update();
|
||||
void refresh();
|
||||
void write(const QString &text);
|
||||
|
||||
BubbleController *m_bubble;
|
||||
UserConfig *m_config;
|
||||
QString m_text;
|
||||
int m_tries = 0;
|
||||
};
|
||||
+70
-1
@@ -10,6 +10,9 @@
|
||||
// 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
|
||||
// --choose-lock-screen
|
||||
// open the window that asks which lock screen to use
|
||||
// where it is a program of its own, and print the answer
|
||||
|
||||
#include "agentsocket.h"
|
||||
#include "bubblecontroller.h"
|
||||
@@ -17,9 +20,12 @@
|
||||
#include "bubblewindow.h"
|
||||
#include "enrollcontroller.h"
|
||||
#include "lockcontroller.h"
|
||||
#include "lockpreview.h"
|
||||
#include "lockscreencontroller.h"
|
||||
#include "locktext.h"
|
||||
#include "sessionlock.h"
|
||||
#include "userconfig.h"
|
||||
#include "wallpaper.h"
|
||||
#include "wayland.h"
|
||||
|
||||
#include "buildconfig.h"
|
||||
@@ -34,8 +40,11 @@
|
||||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlEngine>
|
||||
#include <QScreen>
|
||||
#include <QTimer>
|
||||
#include <QWindow>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
@@ -66,6 +75,56 @@ int runEnroll(QGuiApplication &app, const QString &name)
|
||||
return controller.state() == u"done" ? 0 : code;
|
||||
}
|
||||
|
||||
// The window of --choose-lock-screen. Prints "own" or "yours" for the menu,
|
||||
// which carries it out.
|
||||
int runChooseLockScreen(QGuiApplication &app)
|
||||
{
|
||||
app.setQuitOnLastWindowClosed(true);
|
||||
UserConfig config;
|
||||
|
||||
// Both choices show this screen as it would look: face-unlock's lock
|
||||
// screen as it is, with a scan going on, and the user's own rebuilt.
|
||||
const QScreen *screen = QGuiApplication::primaryScreen();
|
||||
const QString output = screen ? screen->name() : QString();
|
||||
const QSize size = screen ? screen->size() : QSize(1920, 1080);
|
||||
const QUrl desktop = Wallpaper::pick(Wallpaper::forLock({}), output);
|
||||
const QUrl ownWallpaper = config.lockWallpaper().isEmpty() ? desktop : Wallpaper::pick(Wallpaper::forLock(config.lockWallpaper()), output);
|
||||
const QString locker = LockPreview::locker();
|
||||
|
||||
LockScreenController lock;
|
||||
lock.setBlur(config.lockBlur());
|
||||
BubbleController bubble(&config);
|
||||
bubble.scanStarted();
|
||||
// A scan that goes on: the bubble closes by itself after a while.
|
||||
QTimer rescan;
|
||||
QObject::connect(&rescan, &QTimer::timeout, &bubble, &BubbleController::scanStarted);
|
||||
rescan.start(20000);
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
KLocalization::setupLocalizedContext(&engine);
|
||||
engine.setInitialProperties({{QStringLiteral("current"), config.lockScreenStyle()},
|
||||
{QStringLiteral("locker"), locker},
|
||||
{QStringLiteral("widgets"), LockPreview::widgets(locker, size, output, desktop)},
|
||||
{QStringLiteral("screenSize"), size},
|
||||
{QStringLiteral("ownWallpaper"), ownWallpaper},
|
||||
{QStringLiteral("lock"), QVariant::fromValue(&lock)},
|
||||
{QStringLiteral("bubble"), QVariant::fromValue(&bubble)}});
|
||||
engine.loadFromModule(QStringLiteral("FaceUnlock"), QStringLiteral("LockChoice"));
|
||||
auto *window = engine.rootObjects().isEmpty() ? nullptr : qobject_cast<QWindow *>(engine.rootObjects().constFirst());
|
||||
if (!window) {
|
||||
return 2;
|
||||
}
|
||||
// Shown only now, at the size its texts need (see LockChoice.qml).
|
||||
window->show();
|
||||
app.exec();
|
||||
const QString answer = window->property("answer").toString();
|
||||
if (answer.isEmpty()) {
|
||||
return 1;
|
||||
}
|
||||
std::printf("%s\n", qPrintable(answer));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -191,9 +250,14 @@ int main(int argc, char **argv)
|
||||
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, lockOpt, demoOpt, styleOpt});
|
||||
const QCommandLineOption chooseOpt(QStringLiteral("choose-lock-screen"), i18n("Ask which lock screen to use, and print the answer."));
|
||||
parser.addOptions({enrollOpt, nameOpt, lockOpt, demoOpt, styleOpt, chooseOpt});
|
||||
parser.process(app);
|
||||
|
||||
if (parser.isSet(chooseOpt)) {
|
||||
return runChooseLockScreen(app);
|
||||
}
|
||||
|
||||
if (parser.isSet(enrollOpt)) {
|
||||
QString name = parser.value(nameOpt);
|
||||
if (name.isEmpty()) {
|
||||
@@ -264,6 +328,11 @@ int main(int argc, char **argv)
|
||||
QObject::connect(lock, &SessionLock::confirmed, &socket, &AgentSocket::confirmLock);
|
||||
}
|
||||
LockController lock(&bubble, &config, sessionLock.get(), &screen);
|
||||
// hyprlock covers the bubble: it can show it as a line of text instead.
|
||||
std::unique_ptr<LockText> text;
|
||||
if (sessionLock && !lockNow) {
|
||||
text = std::make_unique<LockText>(&bubble, &config);
|
||||
}
|
||||
|
||||
if (lockNow) {
|
||||
// No agent was running, so this one is only here for the lock. It
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Where the lock screen is a program of the user's (Hyprland, Niri): lock
|
||||
// with face-unlock's own, with the bubble, or keep that program, which then
|
||||
// shows a line of text (hyprlock). Each choice shows the user's screen as it
|
||||
// would look: face-unlock's lock screen live, and their own rebuilt from its
|
||||
// config (see LockPreview). The pick goes back to the menu, which carries
|
||||
// it out.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Window
|
||||
import FaceUnlock
|
||||
|
||||
Window {
|
||||
id: window
|
||||
|
||||
// "own", "yours", or empty when there is none yet.
|
||||
required property string current
|
||||
// The user's lock screen (see LockPreview::locker) and what it shows.
|
||||
required property string locker
|
||||
required property var widgets
|
||||
// The screen the pictures show, and the parts of face-unlock's own.
|
||||
required property size screenSize
|
||||
required property url ownWallpaper
|
||||
required property var lock
|
||||
required property var bubble
|
||||
// What was picked, empty when the window was closed.
|
||||
property string answer: ""
|
||||
property string selected: current
|
||||
|
||||
function pick() {
|
||||
if (selected.length > 0) {
|
||||
answer = selected
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
// One fixed size, so tiling compositors float it (see Enroll). The
|
||||
// height follows the texts, whose length depends on the language. It
|
||||
// is taken from them rather than from the Row and Column, which only lay
|
||||
// out once the window shows, and the agent shows it after that.
|
||||
readonly property int fitHeight: Math.ceil(36 + heading.height + 10 + intro.height + 28 + Math.max(own.needed, yours.needed) + 94)
|
||||
width: 800
|
||||
height: fitHeight
|
||||
minimumWidth: 800
|
||||
maximumWidth: 800
|
||||
minimumHeight: fitHeight
|
||||
maximumHeight: fitHeight
|
||||
visible: false
|
||||
color: Theme.panel
|
||||
title: i18n("Choose your lock screen")
|
||||
|
||||
Component {
|
||||
id: ownScene
|
||||
LockScreen {
|
||||
lock: window.lock
|
||||
bubble: window.bubble
|
||||
primary: true
|
||||
wallpaper: window.ownWallpaper
|
||||
interactive: false
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: yoursScene
|
||||
LockPreview {
|
||||
widgets: window.widgets
|
||||
}
|
||||
}
|
||||
|
||||
// One of the two: the screen, a radio button with the name, what it means.
|
||||
component Choice: Rectangle {
|
||||
id: card
|
||||
|
||||
required property string style
|
||||
required property Component scene
|
||||
required property string name
|
||||
required property string about
|
||||
readonly property bool chosen: window.selected === style
|
||||
// The height its text needs; both take the larger one.
|
||||
readonly property real needed: body.y + body.height + 20
|
||||
|
||||
width: 364
|
||||
radius: 18
|
||||
color: area.containsMouse && !chosen ? Qt.lighter(Theme.surface, 1.25) : Theme.surface
|
||||
border.width: 2
|
||||
border.color: chosen ? Theme.accent : "transparent"
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
// The whole screen, made small: filling the picture, cut at the
|
||||
// sides or at the top and bottom.
|
||||
Item {
|
||||
id: picture
|
||||
x: 14
|
||||
y: 14
|
||||
width: parent.width - 28
|
||||
height: Math.round(width * 10 / 16)
|
||||
clip: true
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: mask
|
||||
}
|
||||
|
||||
Loader {
|
||||
sourceComponent: card.scene
|
||||
width: window.screenSize.width
|
||||
height: window.screenSize.height
|
||||
transformOrigin: Item.TopLeft
|
||||
scale: Math.max(picture.width / width, picture.height / height)
|
||||
x: (picture.width - width * scale) / 2
|
||||
y: (picture.height - height * scale) / 2
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: mask
|
||||
width: picture.width
|
||||
height: picture.height
|
||||
radius: 10
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
// A radio button: a ring, filled when picked.
|
||||
Rectangle {
|
||||
id: radio
|
||||
x: 18
|
||||
y: title.y + 3
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
color: "transparent"
|
||||
border.width: 2
|
||||
border.color: card.chosen ? Theme.accent : Theme.textSecondary
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 8
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Theme.accent
|
||||
visible: card.chosen
|
||||
}
|
||||
}
|
||||
Text {
|
||||
id: title
|
||||
anchors.top: picture.bottom
|
||||
anchors.topMargin: 16
|
||||
x: radio.x + radio.width + 10
|
||||
width: parent.width - x - 18
|
||||
wrapMode: Text.WordWrap
|
||||
color: Theme.textPrimary
|
||||
font.pixelSize: 16
|
||||
font.weight: Font.DemiBold
|
||||
text: card.name
|
||||
}
|
||||
Text {
|
||||
id: body
|
||||
anchors.top: title.bottom
|
||||
anchors.topMargin: 8
|
||||
x: 18
|
||||
width: parent.width - 36
|
||||
wrapMode: Text.WordWrap
|
||||
color: Theme.textDetail
|
||||
font.pixelSize: 13
|
||||
lineHeight: 1.15
|
||||
text: card.about
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: area
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: window.selected = card.style
|
||||
onDoubleClicked: {
|
||||
window.selected = card.style
|
||||
window.pick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: head
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 36
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: parent.width - 96
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
id: heading
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: Theme.textPrimary
|
||||
font.pixelSize: 26
|
||||
font.weight: Font.Bold
|
||||
text: i18n("Your lock screen")
|
||||
}
|
||||
Text {
|
||||
id: intro
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
color: Theme.textSecondary
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.Medium
|
||||
lineHeight: 1.15
|
||||
text: i18n("Your desktop leaves the lock screen to a program of your choice. Pick how face unlock shows up there. You can change it later under Settings.")
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: cards
|
||||
anchors.top: head.bottom
|
||||
anchors.topMargin: 28
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 24
|
||||
focus: true
|
||||
|
||||
Keys.onLeftPressed: window.selected = "own"
|
||||
Keys.onRightPressed: window.selected = "yours"
|
||||
Keys.onReturnPressed: window.pick()
|
||||
Keys.onEnterPressed: window.pick()
|
||||
Keys.onEscapePressed: window.close()
|
||||
|
||||
Choice {
|
||||
id: own
|
||||
height: Math.max(own.needed, yours.needed)
|
||||
style: "own"
|
||||
scene: ownScene
|
||||
name: i18n("face-unlock's lock screen")
|
||||
about: i18n("The bubble at the top, the time and your wallpaper. Your shortcut and idle lock then run face-unlock lock.")
|
||||
}
|
||||
Choice {
|
||||
id: yours
|
||||
height: own.height
|
||||
style: "yours"
|
||||
scene: yoursScene
|
||||
name: i18n("Keep your lock screen")
|
||||
about: window.locker === "hyprlock"
|
||||
? i18n("It stays as it is. hyprlock shows a line of text at the top instead of the bubble. Enter on the empty password field scans.")
|
||||
: i18n("It stays as it is, without the bubble. Enter on the empty password field scans.")
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 32
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 12
|
||||
|
||||
PillButton {
|
||||
primary: false
|
||||
text: i18n("Cancel")
|
||||
onClicked: window.close()
|
||||
}
|
||||
PillButton {
|
||||
enabled: window.selected.length > 0
|
||||
text: i18n("Continue")
|
||||
onClicked: window.pick()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// The user's own lock screen, drawn from what LockPreview (C++) read out of
|
||||
// its config, at the size of the screen. Whoever shows it scales it down.
|
||||
// Placement as in hyprlock: from the side or middle halign and valign name,
|
||||
// x to the right and y upwards. One Repeater per kind of widget, stacked in
|
||||
// the order hyprlock draws them.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var widgets
|
||||
|
||||
readonly property var numbered: widgets.map((w, i) => Object.assign({order: i}, w))
|
||||
function only(type) {
|
||||
return numbered.filter(w => w.type === type)
|
||||
}
|
||||
function alignX(halign, size, offset) {
|
||||
return halign === "center" ? (width - size) / 2 + offset
|
||||
: halign === "right" ? width - size + offset
|
||||
: offset
|
||||
}
|
||||
function alignY(valign, size, offset) {
|
||||
const up = valign === "center" ? (height - size) / 2 + offset
|
||||
: valign === "top" ? height - size + offset
|
||||
: offset
|
||||
return height - up - size
|
||||
}
|
||||
|
||||
clip: true
|
||||
|
||||
Repeater {
|
||||
model: root.only("background")
|
||||
|
||||
Item {
|
||||
id: back
|
||||
required property var modelData
|
||||
readonly property var w: modelData
|
||||
z: w.order
|
||||
width: root.width
|
||||
height: root.height
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: back.w.color
|
||||
}
|
||||
Image {
|
||||
// Past the edges when blurred: the blur would pull in the
|
||||
// nothing beyond them and darken the rim.
|
||||
anchors.fill: parent
|
||||
anchors.margins: back.w.blur > 0 ? -Math.round(64 * back.w.blur) : 0
|
||||
visible: back.w.fill !== "solid_color"
|
||||
source: back.w.source
|
||||
asynchronous: true
|
||||
sourceSize: Qt.size(root.width, root.height)
|
||||
fillMode: back.w.fill === "fit" ? Image.PreserveAspectFit
|
||||
: back.w.fill === "stretch" ? Image.Stretch
|
||||
: back.w.fill === "center" ? Image.Pad
|
||||
: back.w.fill === "tile" ? Image.Tile
|
||||
: Image.PreserveAspectCrop
|
||||
layer.enabled: back.w.blur > 0
|
||||
layer.effect: MultiEffect {
|
||||
blurEnabled: true
|
||||
blur: 1
|
||||
blurMax: Math.round(64 * back.w.blur)
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: back.w.dim
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.only("label")
|
||||
|
||||
Text {
|
||||
id: line
|
||||
required property var modelData
|
||||
readonly property var w: modelData
|
||||
z: w.order
|
||||
x: root.alignX(w.halign, width, w.x)
|
||||
y: root.alignY(w.valign, height, w.y)
|
||||
rotation: w.rotate
|
||||
text: w.text
|
||||
textFormat: Text.StyledText
|
||||
color: w.color
|
||||
font.family: w.family
|
||||
font.weight: w.weight
|
||||
font.italic: w.italic
|
||||
font.pointSize: w.size
|
||||
horizontalAlignment: w.align === "left" ? Text.AlignLeft : w.align === "right" ? Text.AlignRight : Text.AlignHCenter
|
||||
layer.enabled: w.shadow
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowColor: line.w.shadowColor
|
||||
shadowBlur: Math.min(1, line.w.shadowSize / 8)
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.only("input")
|
||||
|
||||
// The outline is the outer colour around the inner one. A gradient
|
||||
// outline needs an opaque inside, which leaves it showing only at
|
||||
// the edge.
|
||||
Rectangle {
|
||||
id: field
|
||||
required property var modelData
|
||||
readonly property var w: modelData
|
||||
readonly property var outer: w.outer.colors
|
||||
readonly property bool blend: outer.length > 1 && w.inner.a >= 0.99
|
||||
z: w.order
|
||||
x: root.alignX(w.halign, width, w.x)
|
||||
y: root.alignY(w.valign, height, w.y)
|
||||
rotation: w.rotate
|
||||
width: w.width
|
||||
height: w.height
|
||||
radius: w.rounding < 0 ? height / 2 : Math.min(w.rounding, height / 2)
|
||||
color: blend ? "transparent" : w.inner
|
||||
border.width: blend ? 0 : w.thickness
|
||||
border.color: outer.length > 0 ? outer[0] : "transparent"
|
||||
gradient: blend ? outline : null
|
||||
|
||||
Gradient {
|
||||
id: outline
|
||||
orientation: Math.abs(Math.cos(field.w.outer.angle * Math.PI / 180)) >= Math.abs(Math.sin(field.w.outer.angle * Math.PI / 180))
|
||||
? Gradient.Horizontal : Gradient.Vertical
|
||||
GradientStop { position: 0; color: field.outer.length > 0 ? field.outer[0] : "transparent" }
|
||||
GradientStop { position: 1; color: field.outer.length > 0 ? field.outer[field.outer.length - 1] : "transparent" }
|
||||
}
|
||||
Rectangle {
|
||||
visible: field.blend
|
||||
anchors.fill: parent
|
||||
anchors.margins: field.w.thickness
|
||||
radius: Math.max(0, field.radius - field.w.thickness)
|
||||
color: field.w.inner
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: field.w.placeholder
|
||||
textFormat: Text.StyledText
|
||||
color: field.w.fontColor
|
||||
font.family: field.w.family
|
||||
font.weight: field.w.weight
|
||||
font.italic: field.w.italic
|
||||
font.pointSize: Math.max(1, field.height / 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.only("shape")
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
readonly property var w: modelData
|
||||
z: w.order
|
||||
x: root.alignX(w.halign, width, w.x)
|
||||
y: root.alignY(w.valign, height, w.y)
|
||||
rotation: w.rotate
|
||||
width: w.width
|
||||
height: w.height
|
||||
radius: w.rounding < 0 ? Math.min(width, height) / 2 : Math.min(w.rounding, Math.min(width, height) / 2)
|
||||
color: w.color
|
||||
border.width: w.borderSize
|
||||
border.color: w.border.colors.length > 0 ? w.border.colors[0] : "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.only("image")
|
||||
|
||||
Item {
|
||||
id: frame
|
||||
required property var modelData
|
||||
readonly property var w: modelData
|
||||
readonly property real round: w.rounding < 0 ? width / 2 : Math.min(w.rounding, width / 2)
|
||||
z: w.order
|
||||
x: root.alignX(w.halign, width, w.x)
|
||||
y: root.alignY(w.valign, height, w.y)
|
||||
rotation: w.rotate
|
||||
width: w.size + 2 * w.borderSize
|
||||
height: width
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: frame.round
|
||||
color: frame.w.border.colors.length > 0 ? frame.w.border.colors[0] : "transparent"
|
||||
}
|
||||
Image {
|
||||
id: photo
|
||||
anchors.fill: parent
|
||||
anchors.margins: frame.w.borderSize
|
||||
source: frame.w.source
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: photoMask
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: photoMask
|
||||
width: photo.width
|
||||
height: photo.height
|
||||
radius: Math.max(0, frame.round - frame.w.borderSize)
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.only("ring")
|
||||
|
||||
// swaylock's indicator, in the middle.
|
||||
Rectangle {
|
||||
id: circle
|
||||
required property var modelData
|
||||
readonly property var w: modelData
|
||||
z: w.order
|
||||
anchors.centerIn: parent
|
||||
width: 2 * w.radius + 2 * w.thickness
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: w.inside
|
||||
border.width: w.thickness
|
||||
border.color: w.ring
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: circle.w.text
|
||||
color: circle.w.textColor
|
||||
font.pixelSize: circle.w.radius / 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ Item {
|
||||
required property var bubble
|
||||
required property bool primary
|
||||
required property url wallpaper
|
||||
// Off for the picture in the window that asks which lock screen to use:
|
||||
// nothing to type into, no keys taken.
|
||||
property bool interactive: true
|
||||
|
||||
property date now: new Date()
|
||||
Timer {
|
||||
@@ -71,6 +74,7 @@ Item {
|
||||
// keys into the password.
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.interactive
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onPositionChanged: root.lock.activity()
|
||||
@@ -150,7 +154,8 @@ Item {
|
||||
selectionColor: Theme.accent
|
||||
font.pixelSize: 15
|
||||
clip: true
|
||||
focus: true
|
||||
focus: root.interactive
|
||||
enabled: root.interactive
|
||||
// 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 {}
|
||||
@@ -162,7 +167,11 @@ Item {
|
||||
root.lock.activity()
|
||||
event.accepted = false
|
||||
}
|
||||
Component.onCompleted: forceActiveFocus()
|
||||
Component.onCompleted: {
|
||||
if (root.interactive) {
|
||||
forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -32,6 +32,7 @@ void UserConfig::load()
|
||||
m_enabled = kv.boolean(QStringLiteral("Enabled"), false);
|
||||
m_lockWallpaper = kv.value(QStringLiteral("LockWallpaper"));
|
||||
m_lockBlur = kv.boolean(QStringLiteral("LockBlur"), false);
|
||||
m_lockScreenStyle = kv.value(QStringLiteral("LockScreenStyle"));
|
||||
m_lockScreen = kv.boolean(QStringLiteral("LockScreen"), true);
|
||||
m_scanOnWake = kv.boolean(QStringLiteral("ScanOnWake"), true);
|
||||
m_scanOnLock = kv.boolean(QStringLiteral("ScanOnLock"), false);
|
||||
|
||||
@@ -29,6 +29,14 @@ public:
|
||||
{
|
||||
return m_lockWallpaper;
|
||||
}
|
||||
// Where the lock screen is a program of the user's (Hyprland, Niri):
|
||||
// "own" to lock with face-unlock's, "yours" to keep that program, which
|
||||
// then shows the bubble as a line of text (see LockText). Empty until
|
||||
// the user picked one.
|
||||
QString lockScreenStyle() const
|
||||
{
|
||||
return m_lockScreenStyle;
|
||||
}
|
||||
bool lockBlur() const
|
||||
{
|
||||
return m_lockBlur;
|
||||
@@ -90,6 +98,7 @@ private:
|
||||
QFileSystemWatcher m_watcher;
|
||||
bool m_enabled = false;
|
||||
QString m_lockWallpaper;
|
||||
QString m_lockScreenStyle;
|
||||
bool m_lockBlur = false;
|
||||
bool m_lockScreen = true;
|
||||
bool m_scanOnWake = true;
|
||||
|
||||
+7
-1
@@ -125,7 +125,13 @@ fu_do_enable() {
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
if fu_pam_lockers_here && fu_pam_lockers_enabled; then
|
||||
# Where the lock screen is a program of its own: face-unlock's, with the
|
||||
# bubble, or that program with a line of text. Asked once.
|
||||
if fu_own_lock_here && [[ -n ${WAYLAND_DISPLAY:-} && -z $(fu_config_get LockScreenStyle) ]]; then
|
||||
fu_say " $(fu_msg "Pick your lock screen in the window.")"
|
||||
fu_lock_choose || fu_note "$(fu_msg "No lock screen picked. You can do it later under Settings.")"
|
||||
fi
|
||||
if fu_pam_lockers_here && fu_pam_lockers_enabled && [[ $(fu_config_get LockScreenStyle) != own ]]; then
|
||||
fu_note "$(fu_msg "The lock screen (%s) takes your face too. If it does not scan when you come back, press Enter on the empty password field." "$(fu_pam_lockers_list)")"
|
||||
fi
|
||||
|
||||
|
||||
@@ -156,6 +156,8 @@ 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" "$*"; }
|
||||
# A line of config to copy, under a note.
|
||||
fu_code() { FU_UI_NOTICES+=(" $*"); printf ' %s\n' "$*"; }
|
||||
|
||||
fu_have() { command -v "$1" > /dev/null 2>&1; }
|
||||
|
||||
|
||||
+28
-7
@@ -200,6 +200,8 @@ fu_value_label() {
|
||||
AnimationSpeed:slow) fu_msg "slow" ;;
|
||||
ScanSeconds:*) fu_msg "%s seconds" "$2" ;;
|
||||
Camera:auto) fu_msg "automatic" ;;
|
||||
LockScreenStyle:own) fu_msg "face-unlock's" ;;
|
||||
LockScreenStyle:*) fu_msg "yours" ;;
|
||||
*) printf '%s' "$2" ;;
|
||||
esac
|
||||
}
|
||||
@@ -277,10 +279,11 @@ 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), 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
|
||||
# type bool, choice (steps through the choices), camera, path (typed
|
||||
# in; empty for the desktop's picture), or lockstyle (the window
|
||||
# with the two lock screens, see fu_lock_choose)
|
||||
# needs a bool setting this one does nothing without, or Key=value; it is
|
||||
# dimmed while that is not so
|
||||
# only lockers: only where the lock screen is a program of its own with a
|
||||
# PAM file (Hyprland, Niri), see fu_pam_lockers_here; ownlock: only
|
||||
# where face-unlock's own lock screen can be used
|
||||
@@ -289,8 +292,9 @@ FU_SETTINGS=(
|
||||
"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"
|
||||
"user|LockScreenStyle|lockstyle|yours|Which lock screen|||ownlock"
|
||||
"user|LockWallpaper|path||Wallpaper||LockScreenStyle=own|ownlock"
|
||||
"user|LockBlur|bool|no|Blur the wallpaper||LockScreenStyle=own|ownlock"
|
||||
"group|Password prompts"
|
||||
"pam|sudo|bool|no|sudo in a terminal"
|
||||
"pam|polkit-1|bool|no|Admin prompts"
|
||||
@@ -328,6 +332,7 @@ 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." ;;
|
||||
LockScreenStyle:*) fu_msg "face-unlock's lock screen shows the bubble, and you lock with \`face-unlock lock\`. Or keep yours: hyprlock then shows a line of text at the top. Enter opens the choice." ;;
|
||||
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)" ;;
|
||||
@@ -483,6 +488,18 @@ _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")" ;;
|
||||
lockstyle)
|
||||
# The window with the pictures where there is a screen for it,
|
||||
# else the other one of the two.
|
||||
if [[ -n ${WAYLAND_DISPLAY:-} ]]; then
|
||||
fu_ui_cooked fu_lock_choose
|
||||
elif [[ $current == own ]]; then
|
||||
fu_lock_style_set yours
|
||||
else
|
||||
fu_lock_style_set own
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
path)
|
||||
printf '\n %s ' "$(fu_msg "Picture or folder:")"
|
||||
fu_ui_read_line "$current" || return 0
|
||||
@@ -612,7 +629,11 @@ fu_ui_settings() {
|
||||
before="${FU_C_BLUE}◂${FU_C_RESET} " after=" ${FU_C_BLUE}▸${FU_C_RESET}"
|
||||
fi
|
||||
dim=''
|
||||
[[ -n ${needs[i]} ]] && ! fu_is_true "${current[${needs[i]}]}" && dim="$FU_C_DIM"
|
||||
case "${needs[i]}" in
|
||||
'') ;;
|
||||
*=*) [[ ${current[${needs[i]%%=*}]} == "${needs[i]#*=}" ]] || dim="$FU_C_DIM" ;;
|
||||
*) fu_is_true "${current[${needs[i]}]}" || dim="$FU_C_DIM" ;;
|
||||
esac
|
||||
pad=$(( width + 2 - widths[i] ))
|
||||
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"
|
||||
|
||||
@@ -193,6 +193,129 @@ fu_gnome_extension_disable() {
|
||||
gsettings set org.gnome.shell enabled-extensions "$list" > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The lock screen where it is a program of its own (Hyprland, Niri)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Either face-unlock's (`face-unlock lock`, with the bubble), or the user's
|
||||
# own program. hyprlock then shows what the bubble would as a line of text: a
|
||||
# label in its config reads a file the agent writes (see
|
||||
# src/agent/locktext.h). Asked once in a window, and under Settings.
|
||||
|
||||
FU_HYPRLOCK_SNIPPET="$FU_XDG_CONFIG/$FU_NAME/hyprlock.conf"
|
||||
FU_HYPRLOCK_MARK="# $FU_NAME: what face unlock is doing, at the top"
|
||||
|
||||
fu_own_lock_here() {
|
||||
[[ $FU_DESKTOP != plasma && $FU_DESKTOP != gnome ]]
|
||||
}
|
||||
|
||||
# fu_hyprlock_text_on
|
||||
# 0 done, 1 could not write, 2 no hyprlock, 3 hyprlock has no config here.
|
||||
fu_hyprlock_text_on() {
|
||||
local conf="$FU_XDG_CONFIG/hypr/hyprlock.conf"
|
||||
fu_have hyprlock || return 2
|
||||
[[ -f $conf ]] || return 3
|
||||
mkdir -p "${FU_HYPRLOCK_SNIPPET%/*}" || return 1
|
||||
cat > "$FU_HYPRLOCK_SNIPPET" <<- EOF || return 1
|
||||
# Written by $FU_NAME: what face unlock is doing, as a line at the top of
|
||||
# hyprlock. Picking $FU_NAME's lock screen in its menu takes it out again.
|
||||
label {
|
||||
monitor =
|
||||
text = cmd[update:0:1] cat "\$XDG_RUNTIME_DIR/$FU_NAME/lock-text" 2>/dev/null
|
||||
color = rgba(255, 255, 255, 0.95)
|
||||
font_size = 20
|
||||
shadow_passes = 2
|
||||
shadow_size = 3
|
||||
halign = center
|
||||
valign = top
|
||||
position = 0, -48
|
||||
zindex = 10
|
||||
}
|
||||
EOF
|
||||
grep -qxF "$FU_HYPRLOCK_MARK" "$conf" && return 0
|
||||
# With ~ where it can, like a hand-written config. hyprlock expands it.
|
||||
local snippet="$FU_HYPRLOCK_SNIPPET"
|
||||
# shellcheck disable=SC2088 # for hyprlock, not for the shell
|
||||
[[ $snippet == "$HOME"/* ]] && snippet="~/${snippet#"$HOME"/}"
|
||||
printf '\n%s\nsource = %s\n' "$FU_HYPRLOCK_MARK" "$snippet" >> "$conf" || return 1
|
||||
}
|
||||
|
||||
# fu_hyprlock_text_off
|
||||
# The mark, the source line under it, and the empty lines before it. Written
|
||||
# back in place, so a hyprlock.conf that is a link to a dotfile stays one.
|
||||
fu_hyprlock_text_off() {
|
||||
local conf="$FU_XDG_CONFIG/hypr/hyprlock.conf" content
|
||||
rm -f "$FU_HYPRLOCK_SNIPPET"
|
||||
[[ -f $conf ]] && grep -qxF "$FU_HYPRLOCK_MARK" "$conf" || return 0
|
||||
content="$(awk -v mark="$FU_HYPRLOCK_MARK" '
|
||||
$0 == mark { held = ""; skip = 1; next }
|
||||
skip && /^[[:space:]]*source[[:space:]]*=/ { skip = 0; next }
|
||||
{ skip = 0 }
|
||||
/^[[:space:]]*$/ { held = held $0 "\n"; next }
|
||||
{ printf "%s", held; held = ""; print }
|
||||
' "$conf")" || return 1
|
||||
printf '%s\n' "$content" > "$conf"
|
||||
}
|
||||
|
||||
# fu_own_lock_hint
|
||||
# Where face-unlock's lock screen has to be started from: the user's key and
|
||||
# idle lock, which are theirs to change.
|
||||
fu_own_lock_hint() {
|
||||
# shellcheck disable=SC2088 # shown to the user, not a path to open
|
||||
case "$FU_DESKTOP" in
|
||||
hyprland)
|
||||
if [[ -f $FU_XDG_CONFIG/hypr/hyprland.lua ]]; then
|
||||
fu_note "$(fu_msg "To lock with it, add this to %s, and set %s in %s:" "~/.config/hypr/hyprland.lua" "lock_cmd = $FU_NAME lock" "hypridle.conf")"
|
||||
fu_code "hl.bind(\"SUPER + L\", hl.dsp.exec_cmd(\"$FU_NAME lock\"))"
|
||||
else
|
||||
fu_note "$(fu_msg "To lock with it, add this to %s, and set %s in %s:" "~/.config/hypr/hyprland.conf" "lock_cmd = $FU_NAME lock" "hypridle.conf")"
|
||||
fu_code "bind = SUPER, L, exec, $FU_NAME lock"
|
||||
fi
|
||||
;;
|
||||
niri)
|
||||
fu_note "$(fu_msg "To lock with it, add this to the binds in %s, and use %s in swayidle:" "~/.config/niri/config.kdl" "$FU_NAME lock")"
|
||||
fu_code "Mod+Alt+L { spawn \"$FU_NAME\" \"lock\"; }"
|
||||
;;
|
||||
*)
|
||||
fu_note "$(fu_msg "To lock with it, run %s where your lock screen is started now." "$FU_NAME lock")"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# fu_lock_style_set <own|yours>
|
||||
fu_lock_style_set() {
|
||||
# shellcheck disable=SC2088 # shown to the user, not a path to open
|
||||
local conf="~/.config/hypr/hyprlock.conf"
|
||||
case "$1" in
|
||||
own)
|
||||
fu_config_set LockScreenStyle own || { fu_bad "$(fu_msg "Could not save the setting.")"; return 1; }
|
||||
fu_hyprlock_text_off
|
||||
fu_ok "$(fu_msg "You lock with face-unlock's lock screen now.")"
|
||||
fu_own_lock_hint
|
||||
;;
|
||||
yours)
|
||||
fu_config_set LockScreenStyle yours || { fu_bad "$(fu_msg "Could not save the setting.")"; return 1; }
|
||||
fu_hyprlock_text_on
|
||||
case $? in
|
||||
0) fu_ok "$(fu_msg "hyprlock shows what face unlock is doing at the top, from the next time it locks.")" ;;
|
||||
1) fu_bad "$(fu_msg "Could not add the line to %s." "$conf")" ;;
|
||||
2) fu_ok "$(fu_msg "Your lock screen stays as it is. Press Enter on the empty password field to scan.")" ;;
|
||||
3) fu_note "$(fu_msg "hyprlock has no %s, so it cannot show the text." "$conf")" ;;
|
||||
esac
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# fu_lock_choose
|
||||
# Asks in a window which lock screen to use, and carries it out. 1 when the
|
||||
# window was closed or cannot open.
|
||||
fu_lock_choose() {
|
||||
local choice
|
||||
[[ -n ${WAYLAND_DISPLAY:-} ]] || return 1
|
||||
choice="$("$FU_AGENT" --choose-lock-screen 2> /dev/null)" || return 1
|
||||
fu_lock_style_set "$choice"
|
||||
}
|
||||
|
||||
fu_agent_disable() {
|
||||
systemctl --user disable --now "$FU_UNIT_AGENT" > /dev/null 2>&1 || true
|
||||
}
|
||||
Reference in new issue
Block a user