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
@@ -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)};
|
||||
}
|
||||
Reference in new issue
Block a user