Merge branch 'main' into feat/fullscreen-reveal-bar
C++ / fmt (pull_request) Successful in 5s
JS/TS / fmt (pull_request) Successful in 18s
JS/TS / lint (pull_request) Successful in 18s
Python / fmt (pull_request) Successful in 34s
Python / lint (pull_request) Successful in 32s
Python / test (pull_request) Successful in 1m1s
Python / typecheck (pull_request) Successful in 1m21s
Rust / fmt (pull_request) Successful in 33s
C++ / build (pull_request) Successful in 2m17s
Rust / build (pull_request) Successful in 1m43s
Python / buildcheck (pull_request) Successful in 2m26s
Rust / clippy (pull_request) Successful in 1m39s
C++ / clang-tidy (pull_request) Successful in 7m58s

This commit is contained in:
2026-08-16 12:50:52 +02:00
15 changed files with 140 additions and 91 deletions
+4 -4
View File
@@ -126,7 +126,7 @@ MouseArea {
CustomRect { CustomRect {
id: item id: item
readonly property bool active: modelData === root.active readonly property bool active: modelData === root?.active
required property int index required property int index
required property MenuItem modelData required property MenuItem modelData
@@ -163,19 +163,19 @@ MouseArea {
MaterialIcon { MaterialIcon {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
text: item.modelData.icon text: item.modelData?.icon ?? ""
} }
CustomText { CustomText {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true Layout.fillWidth: true
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
text: item.modelData.text text: item.modelData?.text ?? ""
} }
Loader { Loader {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
active: item.modelData.trailingIcon.length > 0 active: item.modelData?.trailingIcon.length > 0
asynchronous: true asynchronous: true
visible: active visible: active
-2
View File
@@ -318,8 +318,6 @@ Item {
root.visibilities.sidebar = false; root.visibilities.sidebar = false;
root.panels.popouts.hasCurrent = false; root.panels.popouts.hasCurrent = false;
root.visibilities.launcher = false; root.visibilities.launcher = false;
} else {
Config.save();
} }
} }
-1
View File
@@ -73,7 +73,6 @@ Singleton {
Config.dock.pinnedApps = pinnedApps; Config.dock.pinnedApps = pinnedApps;
root.unpinnedOrder = visibleUnpinned.concat(root.unpinnedOrder.map(normalizeId).filter(id => !pinnedApps.includes(id) && !visibleUnpinned.includes(id))); root.unpinnedOrder = visibleUnpinned.concat(root.unpinnedOrder.map(normalizeId).filter(id => !pinnedApps.includes(id) && !visibleUnpinned.includes(id)));
Config.saveNoToast();
} }
function isPinned(appId) { function isPinned(appId) {
+8 -41
View File
@@ -11,8 +11,8 @@ Singleton {
id: root id: root
property int availableUpdates: 0 property int availableUpdates: 0
property string cmd: ""
property bool commandReady property bool commandReady
property bool hasHelper
property bool loaded property bool loaded
property double now: Date.now() property double now: Date.now()
property var updates: ({}) property var updates: ({})
@@ -38,18 +38,12 @@ Singleton {
} }
function performPackageUpdate(pkg: string): void { function performPackageUpdate(pkg: string): void {
if (root.cmd === "pacman") pkgUpdateProc.command = ["pkexec", "pacman", "--noconfirm", "-Sy", pkg];
pkgUpdateProc.command = ["pkexec", root.cmd, "--noconfirm", "-Sy", pkg];
else
pkgUpdateProc.command = [root.cmd, "--noconfirm", "--sudo", "pkexec", "-Sy", pkg];
pkgUpdateProc.running = true; pkgUpdateProc.running = true;
} }
function performSystemUpdate(): void { function performSystemUpdate(): void {
if (root.cmd === "pacman") sysUpdateProc.command = ["pkexec", "pacman", "--noconfirm", "-Syu"];
sysUpdateProc.command = ["pkexec", root.cmd, "--noconfirm", "-Syu"];
else
sysUpdateProc.command = [root.cmd, "--noconfirm", "--sudo", "pkexec", "-Syu"];
sysUpdateProc.running = true; sysUpdateProc.running = true;
} }
@@ -87,7 +81,7 @@ Singleton {
Process { Process {
id: cmdDetect id: cmdDetect
command: ["sh", "-c", "command -v checkupdates || command -v yay || command -v paru"] command: ["sh", "-c", "command -v checkupdates"]
running: true running: true
stdout: StdioCollector { stdout: StdioCollector {
@@ -96,47 +90,20 @@ Singleton {
let helper; let helper;
if (cmd.length > 0) { if (cmd.length > 0) {
helper = cmd.split("/").pop(); helper = true;
} else { } else {
helper = "pacman"; helper = false;
}
if (helper === "checkupdates") {
updatesProc.command = [helper];
} else {
updatesProc.command = [helper, "-Qu"];
} }
root.hasHelper = helper;
root.commandReady = true; root.commandReady = true;
} }
} }
} }
Process {
id: updateCmdDetect
command: ["sh", "-c", "command -v yay || command -v paru"]
running: true
stdout: StdioCollector {
onStreamFinished: {
const cmd = this.text.trim();
let helper;
if (cmd.length > 0) {
helper = cmd.split("/").pop();
} else {
helper = "pacman";
}
root.cmd = helper;
}
}
}
Process { Process {
id: updatesProc id: updatesProc
command: [] command: root.hasHelper ? ["checkupdates"] : []
running: false running: false
stdout: StdioCollector { stdout: StdioCollector {
-2
View File
@@ -78,8 +78,6 @@ CustomClippingRect {
required property var modelData required property var modelData
readonly property list<string> sections: modelData.update.split(" ") readonly property list<string> sections: modelData.update.split(" ")
// anchors.left: parent.left
// anchors.right: parent.right
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
implicitHeight: root.itemHeight implicitHeight: root.itemHeight
implicitWidth: 600 implicitWidth: 600
@@ -82,7 +82,6 @@ Searcher {
list.visibilities.launcher = false; list.visibilities.launcher = false;
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--scheme", variant]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--scheme", variant]);
Config.colors.schemeType = variant; Config.colors.schemeType = variant;
Config.save();
} }
} }
} }
+1 -4
View File
@@ -70,10 +70,7 @@ PageBase {
} }
] ]
onSelected: item => { onSelected: item => Config.screenshot.mode = item.value
Config.screenshot.mode = item.value;
Config.save();
}
} }
SectionHeader { SectionHeader {
-2
View File
@@ -104,7 +104,6 @@ PageBase {
onApplySettings: (start, end) => { onApplySettings: (start, end) => {
Config.general.color.scheduleDarkStart = start; Config.general.color.scheduleDarkStart = start;
Config.general.color.scheduleDarkEnd = end; Config.general.color.scheduleDarkEnd = end;
Config.save();
ModeScheduler.checkStartup(); ModeScheduler.checkStartup();
PopupManager.requestClose(); PopupManager.requestClose();
} }
@@ -146,7 +145,6 @@ PageBase {
onApplySettings: (start, end) => { onApplySettings: (start, end) => {
Config.general.color.scheduleHyprsunsetStart = start; Config.general.color.scheduleHyprsunsetStart = start;
Config.general.color.scheduleHyprsunsetEnd = end; Config.general.color.scheduleHyprsunsetEnd = end;
Config.save();
Hyprsunset.checkStartup(); Hyprsunset.checkStartup();
PopupManager.requestClose(); PopupManager.requestClose();
} }
+29 -19
View File
@@ -95,15 +95,31 @@ QString Config::filePath() const {
void Config::loadSync() { void Config::loadSync() {
m_loading = true; m_loading = true;
QFile f(filePath()); QFile f(filePath());
QJsonObject before;
bool existed = false;
if (f.open(QIODevice::ReadOnly)) { if (f.open(QIODevice::ReadOnly)) {
const auto doc = QJsonDocument::fromJson(f.readAll()); const auto doc = QJsonDocument::fromJson(f.readAll());
if (doc.isObject()) loadFromJson(QJsonValue(doc.object())); if (doc.isObject()) {
before = doc.object();
existed = true;
} else {
qInfo() << "Config: existing config at" << filePath()
<< "is empty or not a valid JSON object - using defaults";
}
} else { } else {
qInfo() << "Config: no existing config at" << filePath() qInfo() << "Config: no existing config at" << filePath()
<< "- using defaults"; << "- using defaults";
} }
loadFromJson(QJsonValue(before));
m_loading = false; m_loading = false;
const auto after = toJson().toObject();
if (!existed || after != before) saveNow();
} }
void Config::updateWatch() { void Config::updateWatch() {
@@ -148,27 +164,21 @@ void Config::loadAsync() {
if (f.open(QIODevice::ReadOnly)) { if (f.open(QIODevice::ReadOnly)) {
const auto doc = QJsonDocument::fromJson(f.readAll()); const auto doc = QJsonDocument::fromJson(f.readAll());
const bool valid = doc.isObject();
const QJsonObject before = valid ? doc.object() : QJsonObject();
if (doc.isObject()) { QMetaObject::invokeMethod(
QMetaObject::invokeMethod( this,
this, [this, valid, before]() {
[this, doc]() { loadFromJson(QJsonValue(before));
loadFromJson(QJsonValue(doc.object())); m_loading = false;
m_loading = false;
if (m_reloadPending) m_reloadTimer.start(); const auto after = toJson().toObject();
}, if (!valid || after != before) saveNow();
Qt::QueuedConnection);
} else {
QMetaObject::invokeMethod(
this,
[this]() {
m_loading = false;
if (m_reloadPending) m_reloadTimer.start(); if (m_reloadPending) m_reloadTimer.start();
}, },
Qt::QueuedConnection); Qt::QueuedConnection);
}
} else { } else {
qInfo() << "Config: failed to reload from" << filePath() qInfo() << "Config: failed to reload from" << filePath()
<< "- using in-memory values"; << "- using in-memory values";
+14 -5
View File
@@ -79,13 +79,14 @@ void ConfigList::loadFromJson(const QJsonValue& json) {
if (!json.isArray()) { if (!json.isArray()) {
qCWarning( qCWarning(
lcConfig, lcConfig,
"Option '%s' must be a list, ignoring", "Option '%s' must be a list, resetting to default",
qUtf8Printable(propertyPath())); qUtf8Printable(propertyPath()));
m_rejectedJson = json;
resetToDefaults();
materializeDefaults();
return; return;
} }
m_rejectedJson = QJsonValue::Undefined;
populate(json.toArray()); populate(json.toArray());
m_loaded = true; m_loaded = true;
} }
@@ -93,12 +94,11 @@ void ConfigList::loadFromJson(const QJsonValue& json) {
QJsonValue ConfigList::toJson() const { QJsonValue ConfigList::toJson() const {
if (m_loaded) return elementsToJson(); if (m_loaded) return elementsToJson();
return m_rejectedJson; return QJsonValue::Undefined;
} }
void ConfigList::clearLoadedKeys() { void ConfigList::clearLoadedKeys() {
m_loaded = false; m_loaded = false;
m_rejectedJson = QJsonValue::Undefined;
} }
QStringList ConfigList::unknownKeys() const { QStringList ConfigList::unknownKeys() const {
@@ -113,6 +113,15 @@ QStringList ConfigList::unknownKeys() const {
return keys; return keys;
} }
void ConfigList::materializeDefaults() {
for (auto* const item : m_items)
item->materializeDefaults();
if (m_global) return;
m_loaded = true;
}
void ConfigList::resyncFromGlobal() { void ConfigList::resyncFromGlobal() {
syncValuesFromGlobal(); syncValuesFromGlobal();
} }
+3 -5
View File
@@ -33,6 +33,7 @@ class ConfigList : public ConfigNode {
[[nodiscard]] QJsonValue toJson() const override; [[nodiscard]] QJsonValue toJson() const override;
void clearLoadedKeys() override; void clearLoadedKeys() override;
[[nodiscard]] QStringList unknownKeys() const override; [[nodiscard]] QStringList unknownKeys() const override;
void materializeDefaults() override;
void resyncFromGlobal() override; void resyncFromGlobal() override;
signals: signals:
@@ -71,7 +72,6 @@ class ConfigList : public ConfigNode {
QJsonArray m_defaults; QJsonArray m_defaults;
QList<ConfigObject*> m_items; QList<ConfigObject*> m_items;
bool m_loaded = false; bool m_loaded = false;
QJsonValue m_rejectedJson = QJsonValue::Undefined;
}; };
} // namespace ZShell::config } // namespace ZShell::config
@@ -103,7 +103,6 @@ class ConfigList : public ConfigNode {
} \ } \
}; };
#define CONFIG_LIST(Type, name, ...) \ #define CONFIG_LIST(Type, name, ...) \
Q_PROPERTY(ZShell::config::Type* name READ name CONSTANT) \ Q_PROPERTY(ZShell::config::Type* name READ name CONSTANT) \
\ \
@@ -115,6 +114,8 @@ class ConfigList : public ConfigNode {
private: \ private: \
Type* m_##name = new Type(this __VA_OPT__(, __VA_ARGS__)); Type* m_##name = new Type(this __VA_OPT__(, __VA_ARGS__));
#define LIST_ENTRY(id, enabled) \
vmap({{"id", QString::fromUtf8(#id)}, {"enabled", enabled}})
namespace ZShell::config { namespace ZShell::config {
@@ -136,6 +137,3 @@ class ListEntry : public ConfigObject {
CONFIG_LIST_TYPE(ListEntry, EntryList) CONFIG_LIST_TYPE(ListEntry, EntryList)
} // namespace ZShell::config } // namespace ZShell::config
#define LIST_ENTRY(id, enabled) \
vmap({{"id", QString::fromUtf8(#id)}, {"enabled", enabled}})
+2
View File
@@ -26,6 +26,8 @@ class ConfigNode : public QObject {
[[nodiscard]] virtual QStringList unknownKeys() const = 0; [[nodiscard]] virtual QStringList unknownKeys() const = 0;
[[nodiscard]] virtual QList<ConfigNode*> childNodes() const; [[nodiscard]] virtual QList<ConfigNode*> childNodes() const;
virtual void materializeDefaults() = 0;
void syncFromGlobal(ConfigNode* global); void syncFromGlobal(ConfigNode* global);
virtual void resyncFromGlobal() = 0; virtual void resyncFromGlobal() = 0;
+78 -4
View File
@@ -7,6 +7,51 @@
namespace ZShell::config { namespace ZShell::config {
namespace {
bool isStringArray(const QJsonArray& arr) {
for (const auto& v : arr) {
if (!v.isString()) return false;
}
return true;
}
bool jsonValueMatchesType(const QJsonValue& val, QMetaType type) {
switch (val.type()) {
case QJsonValue::Bool:
return type.id() == QMetaType::Bool;
case QJsonValue::Double:
switch (type.id()) {
case QMetaType::Int:
case QMetaType::UInt:
case QMetaType::LongLong:
case QMetaType::ULongLong:
case QMetaType::Double:
case QMetaType::Float:
return true;
default:
return false;
}
case QJsonValue::String:
return type.id() == QMetaType::QString;
case QJsonValue::Array:
if (type.id() == QMetaType::QStringList)
return isStringArray(val.toArray());
return type.id() == QMetaType::QVariantList;
case QJsonValue::Object:
case QJsonValue::Null:
case QJsonValue::Undefined:
default:
return false;
}
}
} // namespace
ConfigObject::ConfigObject(QObject* parent) : ConfigNode(parent) {} ConfigObject::ConfigObject(QObject* parent) : ConfigNode(parent) {}
void ConfigObject::loadFromJson(const QJsonValue& json) { void ConfigObject::loadFromJson(const QJsonValue& json) {
@@ -14,6 +59,7 @@ void ConfigObject::loadFromJson(const QJsonValue& json) {
const auto* meta = metaObject(); const auto* meta = metaObject();
QSet<QString> known; QSet<QString> known;
QSet<QString> invalid;
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) { for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i); auto prop = meta->property(i);
@@ -34,6 +80,15 @@ void ConfigObject::loadFromJson(const QJsonValue& json) {
if (!prop.isWritable()) continue; if (!prop.isWritable()) continue;
if (!jsonValueMatchesType(jsonVal, prop.metaType())) {
qWarning() << "Config: type mismatch for" << key << "in"
<< meta->className() << "- expected"
<< prop.metaType().name() << "got value" << jsonVal
<< "- resetting to default";
invalid.insert(key);
continue;
}
if (prop.metaType().id() == QMetaType::QStringList) { if (prop.metaType().id() == QMetaType::QStringList) {
QStringList list; QStringList list;
const auto jsonArr = jsonVal.toArray(); const auto jsonArr = jsonVal.toArray();
@@ -56,10 +111,12 @@ void ConfigObject::loadFromJson(const QJsonValue& json) {
m_extras = {}; m_extras = {};
for (auto it = obj.begin(); it != obj.end(); ++it) { for (auto it = obj.begin(); it != obj.end(); ++it) {
if (!known.contains(it.key())) { if (!known.contains(it.key()) || invalid.contains(it.key())) {
m_extras.insert(it.key(), it.value()); m_extras.insert(it.key(), it.value());
} }
} }
materializeDefaults();
} }
QJsonValue ConfigObject::toJson() const { QJsonValue ConfigObject::toJson() const {
@@ -104,9 +161,6 @@ QJsonValue ConfigObject::toJson() const {
obj.insert(key, QJsonValue::fromVariant(value)); obj.insert(key, QJsonValue::fromVariant(value));
} }
for (auto it = m_extras.begin(); it != m_extras.end(); ++it)
obj.insert(it.key(), it.value());
if (obj.isEmpty()) return QJsonValue::Undefined; if (obj.isEmpty()) return QJsonValue::Undefined;
return obj; return obj;
@@ -159,6 +213,26 @@ QList<ConfigNode*> ConfigObject::childNodes() const {
return nodes; return nodes;
} }
void ConfigObject::materializeDefaults() {
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name());
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
node->materializeDefaults();
continue;
}
if (!prop.isWritable()) continue;
if (m_global) continue;
m_loadedKeys.insert(key);
}
}
void ConfigObject::syncValuesFromGlobal() { void ConfigObject::syncValuesFromGlobal() {
const auto* meta = metaObject(); const auto* meta = metaObject();
+1
View File
@@ -62,6 +62,7 @@ class ConfigObject : public ConfigNode {
void clearLoadedKeys() override; void clearLoadedKeys() override;
[[nodiscard]] QStringList unknownKeys() const override; [[nodiscard]] QStringList unknownKeys() const override;
[[nodiscard]] QList<ConfigNode*> childNodes() const override; [[nodiscard]] QList<ConfigNode*> childNodes() const override;
void materializeDefaults() override;
void resyncFromGlobal() override; void resyncFromGlobal() override;
[[nodiscard]] virtual QStringList identityKeys() const; [[nodiscard]] virtual QStringList identityKeys() const;
-1
View File
@@ -113,7 +113,6 @@ Singleton {
function setMode(mode: string): void { function setMode(mode: string): void {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]);
Config.general.color.mode = mode; Config.general.color.mode = mode;
Config.save();
} }
function swapRG(c: color): color { function swapRG(c: color): color {