From e6ea6b6efcf724c96ba20ab165e9d4fdcad93f42 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 14 Jul 2025 11:09:34 -0400 Subject: [PATCH 1/7] Add rescan option for DesktopEntryManager --- src/core/desktopentry.cpp | 44 ++++++++++++++++++++++++++++++++++++++- src/core/desktopentry.hpp | 9 ++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 4673881..159fcd0 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -305,6 +305,38 @@ void DesktopEntryManager::scanDesktopEntries() { } } +void DesktopEntryManager::rescan() { + // Store old entries for cleanup + auto oldEntries = this->desktopEntries; + + // Clear the maps + this->desktopEntries.clear(); + this->lowercaseDesktopEntries.clear(); + + // Scan for new entries + this->scanDesktopEntries(); + + // Collect applications for diff update + QVector newApplications; + for (auto& entry: this->desktopEntries.values()) { + if (!entry->noDisplay()) { + newApplications.append(entry); + } + } + + // Update the model using diff + this->mApplications.diffUpdate(newApplications); + + // Clean up old entries + for (auto* e: oldEntries) { + if (!this->desktopEntries.contains(e->mId)) { + e->deleteLater(); + } + } + + emit applicationsChanged(); +} + void DesktopEntryManager::populateApplications() { for (auto& entry: this->desktopEntries.values()) { if (!entry->noDisplay()) this->mApplications.insertObject(entry); @@ -386,7 +418,15 @@ DesktopEntry* DesktopEntryManager::byId(const QString& id) { ObjectModel* DesktopEntryManager::applications() { return &this->mApplications; } -DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); } +DesktopEntries::DesktopEntries() { + auto* mgr = DesktopEntryManager::instance(); + QObject::connect( + mgr, + &DesktopEntryManager::applicationsChanged, + this, + &DesktopEntries::applicationsChanged + ); +} DesktopEntry* DesktopEntries::byId(const QString& id) { return DesktopEntryManager::instance()->byId(id); @@ -395,3 +435,5 @@ DesktopEntry* DesktopEntries::byId(const QString& id) { ObjectModel* DesktopEntries::applications() { return DesktopEntryManager::instance()->applications(); } + +void DesktopEntries::rescan() { DesktopEntryManager::instance()->rescan(); } \ No newline at end of file diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index ee8f511..b70bf9f 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -149,6 +149,7 @@ class DesktopEntryManager: public QObject { public: void scanDesktopEntries(); + void rescan(); [[nodiscard]] DesktopEntry* byId(const QString& id); @@ -156,6 +157,9 @@ class DesktopEntryManager: public QObject { static DesktopEntryManager* instance(); +signals: + void applicationsChanged(); + private: explicit DesktopEntryManager(); @@ -189,4 +193,9 @@ class DesktopEntries: public QObject { Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id); [[nodiscard]] static ObjectModel* applications(); + + Q_INVOKABLE static void rescan(); + +signals: + void applicationsChanged(); }; From ab1e455090fc45eb94d40946ab9929ea4a888ec6 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 14 Jul 2025 21:37:40 -0400 Subject: [PATCH 2/7] More comprehensive DesktopEntry monitoring with QFileSystemWatcher --- src/core/CMakeLists.txt | 2 + src/core/desktopentry.cpp | 169 +++++++++++++++++++--------- src/core/desktopentry.hpp | 12 +- src/core/desktopentrymonitor.cpp | 187 +++++++++++++++++++++++++++++++ src/core/desktopentrymonitor.hpp | 38 +++++++ src/core/desktoputils.cpp | 34 ++++++ src/core/desktoputils.hpp | 7 ++ 7 files changed, 391 insertions(+), 58 deletions(-) create mode 100644 src/core/desktopentrymonitor.cpp create mode 100644 src/core/desktopentrymonitor.hpp create mode 100644 src/core/desktoputils.cpp create mode 100644 src/core/desktoputils.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index eca7270..f394203 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -23,6 +23,8 @@ qt_add_library(quickshell-core STATIC model.cpp elapsedtimer.cpp desktopentry.cpp + desktopentrymonitor.cpp + desktoputils.cpp objectrepeater.cpp platformmenu.cpp qsmenu.cpp diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 159fcd0..14b57a8 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -16,6 +16,8 @@ #include #include "../io/processcore.hpp" +#include "desktopentrymonitor.hpp" +#include "desktoputils.hpp" #include "logcat.hpp" #include "model.hpp" #include "qmlglobal.hpp" @@ -268,73 +270,36 @@ void DesktopAction::execute() const { } DesktopEntryManager::DesktopEntryManager() { + // Create file watcher for desktop entries + this->monitor = new DesktopEntryMonitor(this); + connect( + this->monitor, + &DesktopEntryMonitor::desktopEntriesChanged, + this, + &DesktopEntryManager::handleFileChanges + ); + + // Initial scan this->scanDesktopEntries(); this->populateApplications(); } void DesktopEntryManager::scanDesktopEntries() { - QList dataPaths; - - if (qEnvironmentVariableIsSet("XDG_DATA_HOME")) { - dataPaths.push_back(qEnvironmentVariable("XDG_DATA_HOME")); - } else if (qEnvironmentVariableIsSet("HOME")) { - dataPaths.push_back(qEnvironmentVariable("HOME") + "/.local/share"); - } - - if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) { - auto var = qEnvironmentVariable("XDG_DATA_DIRS"); - dataPaths += var.split(u':', Qt::SkipEmptyParts); - } else { - dataPaths.push_back("/usr/local/share"); - dataPaths.push_back("/usr/share"); - } + auto desktopPaths = DesktopUtils::getDesktopDirectories(); qCDebug(logDesktopEntry) << "Creating desktop entry scanners"; - for (auto& path: std::ranges::reverse_view(dataPaths)) { - auto p = QDir(path).filePath("applications"); - auto file = QFileInfo(p); + for (auto& path: std::ranges::reverse_view(desktopPaths)) { + auto file = QFileInfo(path); if (!file.isDir()) { - qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory"; + qCDebug(logDesktopEntry) << "Not scanning path" << path << "as it is not a directory"; continue; } - qCDebug(logDesktopEntry) << "Scanning path" << p; - this->scanPath(p); - } -} - -void DesktopEntryManager::rescan() { - // Store old entries for cleanup - auto oldEntries = this->desktopEntries; - - // Clear the maps - this->desktopEntries.clear(); - this->lowercaseDesktopEntries.clear(); - - // Scan for new entries - this->scanDesktopEntries(); - - // Collect applications for diff update - QVector newApplications; - for (auto& entry: this->desktopEntries.values()) { - if (!entry->noDisplay()) { - newApplications.append(entry); - } - } - - // Update the model using diff - this->mApplications.diffUpdate(newApplications); - - // Clean up old entries - for (auto* e: oldEntries) { - if (!this->desktopEntries.contains(e->mId)) { - e->deleteLater(); - } + qCDebug(logDesktopEntry) << "Scanning path" << path; + this->scanPath(path); } - - emit applicationsChanged(); } void DesktopEntryManager::populateApplications() { @@ -418,6 +383,102 @@ DesktopEntry* DesktopEntryManager::byId(const QString& id) { ObjectModel* DesktopEntryManager::applications() { return &this->mApplications; } +void DesktopEntryManager::handleFileChanges( + const QHash& changes +) { + + qCDebug(logDesktopEntry) << "Handling file changes:" << changes.size() << "changes"; + + bool needsUpdate = false; + + for (auto it = changes.begin(); it != changes.end(); ++it) { + const QString& path = it.key(); + DesktopEntryMonitor::ChangeEvent event = it.value(); + + switch (event) { + case DesktopEntryMonitor::ChangeEvent::Added: + case DesktopEntryMonitor::ChangeEvent::Modified: { + // Parse the desktop file + QFile file(path); + if (file.open(QFile::ReadOnly)) { + QString id = this->extractIdFromPath(path); + auto* entry = new DesktopEntry(id, this); + entry->parseEntry(QString::fromUtf8(file.readAll())); + + if (entry->isValid()) { + // Remove old entry if exists + if (this->desktopEntries.contains(id)) { + auto* oldEntry = this->desktopEntries.value(id); + this->desktopEntries.remove(id); + this->lowercaseDesktopEntries.remove(id.toLower()); + oldEntry->deleteLater(); + } + + this->desktopEntries.insert(id, entry); + this->lowercaseDesktopEntries.insert(id.toLower(), entry); + needsUpdate = true; + + qCDebug(logDesktopEntry) << "Updated desktop entry:" << id; + } else { + delete entry; + } + } + break; + } + + case DesktopEntryMonitor::ChangeEvent::Removed: { + QString id = this->extractIdFromPath(path); + if (this->desktopEntries.contains(id)) { + auto* entry = this->desktopEntries.take(id); + this->lowercaseDesktopEntries.remove(id.toLower()); + entry->deleteLater(); + needsUpdate = true; + + qCDebug(logDesktopEntry) << "Removed desktop entry:" << id; + } + break; + } + } + } + + if (needsUpdate) { + this->updateApplicationModel(); + emit applicationsChanged(); + } +} + +QString DesktopEntryManager::extractIdFromPath(const QString& path) { + // Extract ID from path following XDG spec + // e.g., /usr/share/applications/firefox.desktop -> firefox + // e.g., /usr/share/applications/kde4/kate.desktop -> kde4-kate + + QFileInfo info(path); + QString id = info.completeBaseName(); + + // Find the applications directory in the path + int appIdx = path.lastIndexOf("/applications/"); + if (appIdx != -1) { + QString relativePath = path.mid(appIdx + 14); // Skip "/applications/" + QFileInfo relInfo(relativePath); + + // Replace directory separators with dashes + QString dirPath = relInfo.path(); + if (dirPath != ".") { + id = dirPath.replace('/', '-') + '-' + relInfo.completeBaseName(); + } + } + + return id; +} + +void DesktopEntryManager::updateApplicationModel() { + QVector newApplications; + for (auto& entry: this->desktopEntries.values()) { + if (!entry->noDisplay()) newApplications.append(entry); + } + this->mApplications.diffUpdate(newApplications); +} + DesktopEntries::DesktopEntries() { auto* mgr = DesktopEntryManager::instance(); QObject::connect( @@ -435,5 +496,3 @@ DesktopEntry* DesktopEntries::byId(const QString& id) { ObjectModel* DesktopEntries::applications() { return DesktopEntryManager::instance()->applications(); } - -void DesktopEntries::rescan() { DesktopEntryManager::instance()->rescan(); } \ No newline at end of file diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index b70bf9f..ebfec91 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -13,6 +13,7 @@ #include "model.hpp" class DesktopAction; +class DesktopEntryMonitor; /// A desktop entry. See @@DesktopEntries for details. class DesktopEntry: public QObject { @@ -144,12 +145,13 @@ class DesktopAction: public QObject { friend class DesktopEntry; }; +#include "desktopentrymonitor.hpp" + class DesktopEntryManager: public QObject { Q_OBJECT; public: void scanDesktopEntries(); - void rescan(); [[nodiscard]] DesktopEntry* byId(const QString& id); @@ -160,15 +162,21 @@ class DesktopEntryManager: public QObject { signals: void applicationsChanged(); +private slots: + void handleFileChanges(const QHash& changes); + private: explicit DesktopEntryManager(); void populateApplications(); void scanPath(const QDir& dir, const QString& prefix = QString()); + QString extractIdFromPath(const QString& path); + void updateApplicationModel(); QHash desktopEntries; QHash lowercaseDesktopEntries; ObjectModel mApplications {this}; + DesktopEntryMonitor* monitor = nullptr; }; ///! Desktop entry index. @@ -194,8 +202,6 @@ class DesktopEntries: public QObject { [[nodiscard]] static ObjectModel* applications(); - Q_INVOKABLE static void rescan(); - signals: void applicationsChanged(); }; diff --git a/src/core/desktopentrymonitor.cpp b/src/core/desktopentrymonitor.cpp new file mode 100644 index 0000000..e502813 --- /dev/null +++ b/src/core/desktopentrymonitor.cpp @@ -0,0 +1,187 @@ +#include "desktopentrymonitor.hpp" + +#include +#include +#include +#include +#include +#include + +#include "desktoputils.hpp" +#include "logcat.hpp" + +namespace { +QS_LOGGING_CATEGORY(logDesktopMonitor, "quickshell.desktopmonitor", QtWarningMsg); +} + +DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) { + this->watcher = new QFileSystemWatcher(this); + this->debounceTimer = new QTimer(this); + this->debounceTimer->setSingleShot(true); + this->debounceTimer->setInterval(500); // 500ms debounce + + // Initialize XDG desktop paths + this->initializeDesktopPaths(); + + // Setup connections + connect( + this->watcher, + &QFileSystemWatcher::directoryChanged, + this, + &DesktopEntryMonitor::onDirectoryChanged + ); + connect( + this->watcher, + &QFileSystemWatcher::fileChanged, + this, + &DesktopEntryMonitor::onFileChanged + ); + connect(this->debounceTimer, &QTimer::timeout, this, &DesktopEntryMonitor::processChanges); + + // Start monitoring + this->startMonitoring(); +} + +void DesktopEntryMonitor::initializeDesktopPaths() { + this->desktopPaths = DesktopUtils::getDesktopDirectories(); +} + +void DesktopEntryMonitor::startMonitoring() { + for (const QString& path: this->desktopPaths) { + if (QDir(path).exists()) { + qCDebug(logDesktopMonitor) << "Monitoring desktop entry path:" << path; + this->addDirectoryRecursive(path); + } + } +} + +void DesktopEntryMonitor::addDirectoryRecursive(const QString& dirPath) { + QDir dir(dirPath); + if (!dir.exists()) return; + + // Add root directory to watcher + if (this->watcher->addPath(dirPath)) { + qCDebug(logDesktopMonitor) << "Added directory to watcher:" << dirPath; + } + + QDirIterator it( + dirPath, + QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot, + QDirIterator::Subdirectories + ); + + while (it.hasNext()) { + QString path = it.next(); + QFileInfo info(path); + + if (info.isDir()) { + if (this->watcher->addPath(path)) { + qCDebug(logDesktopMonitor) << "Added directory to watcher:" << path; + } + } else if (path.endsWith(".desktop")) { + if (this->watcher->addPath(path)) { + this->fileTimestamps[path] = info.lastModified(); + qCDebug(logDesktopMonitor) << "Monitoring file:" << path; + } + } + } +} + +void DesktopEntryMonitor::onDirectoryChanged(const QString& path) { + qCDebug(logDesktopMonitor) << "Directory changed:" << path; + QDir dir(path); + QList currentFiles = dir.entryList({"*.desktop"}, QDir::Files); + + // Check for new files + for (const QString& file: currentFiles) { + QString fullPath = dir.absoluteFilePath(file); + if (!this->watcher->files().contains(fullPath)) { + if (this->watcher->addPath(fullPath)) { + this->fileTimestamps[fullPath] = QFileInfo(fullPath).lastModified(); + this->queueChange(ChangeEvent::Added, fullPath); + qCDebug(logDesktopMonitor) << "New desktop file detected:" << fullPath; + } + } + } + + // Check for deleted files + QList watchedFiles = this->watcher->files(); + for (const QString& watchedFile: watchedFiles) { + if (QFileInfo(watchedFile).dir().absolutePath() == path && !QFile::exists(watchedFile)) { + this->watcher->removePath(watchedFile); + this->fileTimestamps.remove(watchedFile); + this->queueChange(ChangeEvent::Removed, watchedFile); + qCDebug(logDesktopMonitor) << "Desktop file removed:" << watchedFile; + } + } + + // Check for new subdirectories + QList subdirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString& subdir: subdirs) { + QString subdirPath = dir.absoluteFilePath(subdir); + if (!this->watcher->directories().contains(subdirPath)) { + // Add the new subdirectory and all its contents iteratively + if (this->watcher->addPath(subdirPath)) { + qCDebug(logDesktopMonitor) << "Added new directory to watcher:" << subdirPath; + } + + QDirIterator it( + subdirPath, + QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot, + QDirIterator::Subdirectories + ); + + while (it.hasNext()) { + QString path = it.next(); + QFileInfo info(path); + + if (info.isDir()) { + if (this->watcher->addPath(path)) { + qCDebug(logDesktopMonitor) << "Added directory to watcher:" << path; + } + } else if (path.endsWith(".desktop")) { + if (this->watcher->addPath(path)) { + this->fileTimestamps[path] = info.lastModified(); + qCDebug(logDesktopMonitor) << "Monitoring file:" << path; + } + } + } + } + } +} + +void DesktopEntryMonitor::onFileChanged(const QString& path) { + if (!path.endsWith(".desktop")) return; + + QFileInfo info(path); + if (info.exists()) { + QDateTime currentModified = info.lastModified(); + if (this->fileTimestamps.value(path) != currentModified) { + this->fileTimestamps[path] = currentModified; + this->queueChange(ChangeEvent::Modified, path); + qCDebug(logDesktopMonitor) << "Desktop file modified:" << path; + } + } else { + // File was deleted + this->watcher->removePath(path); + this->fileTimestamps.remove(path); + this->queueChange(ChangeEvent::Removed, path); + qCDebug(logDesktopMonitor) << "Desktop file removed:" << path; + } +} + +void DesktopEntryMonitor::queueChange(ChangeEvent event, const QString& path) { + this->pendingChanges.insert(path, event); + this->debounceTimer->start(); +} + +void DesktopEntryMonitor::processChanges() { + if (this->pendingChanges.isEmpty()) return; + + qCDebug(logDesktopMonitor) << "Processing" << this->pendingChanges.size() << "pending changes"; + + QHash changes = this->pendingChanges; + this->pendingChanges.clear(); + + emit desktopEntriesChanged(changes); +} \ No newline at end of file diff --git a/src/core/desktopentrymonitor.hpp b/src/core/desktopentrymonitor.hpp new file mode 100644 index 0000000..a28b6dd --- /dev/null +++ b/src/core/desktopentrymonitor.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class DesktopEntryMonitor: public QObject { + Q_OBJECT + +public: + enum class ChangeEvent { Added, Modified, Removed }; + + explicit DesktopEntryMonitor(QObject* parent = nullptr); + ~DesktopEntryMonitor() = default; + +signals: + void desktopEntriesChanged(const QHash& changes); + +private slots: + void onDirectoryChanged(const QString& path); + void onFileChanged(const QString& path); + void processChanges(); + +private: + void initializeDesktopPaths(); + void startMonitoring(); + void addDirectoryRecursive(const QString& dirPath); + void queueChange(ChangeEvent event, const QString& path); + + QFileSystemWatcher* watcher; + QStringList desktopPaths; + QHash fileTimestamps; + QTimer* debounceTimer; + QHash pendingChanges; +}; \ No newline at end of file diff --git a/src/core/desktoputils.cpp b/src/core/desktoputils.cpp new file mode 100644 index 0000000..700c45f --- /dev/null +++ b/src/core/desktoputils.cpp @@ -0,0 +1,34 @@ +#include "desktoputils.hpp" + +#include +#include + +QList DesktopUtils::getDesktopDirectories() { + QList dataPaths; + + // XDG_DATA_HOME + QString dataHome = qEnvironmentVariable("XDG_DATA_HOME"); + if (dataHome.isEmpty()) { + if (qEnvironmentVariableIsSet("HOME")) { + dataHome = qEnvironmentVariable("HOME") + "/.local/share"; + } + } + if (!dataHome.isEmpty()) { + dataPaths.append(dataHome + "/applications"); + } + + // XDG_DATA_DIRS + QString dataDirs = qEnvironmentVariable("XDG_DATA_DIRS"); + if (dataDirs.isEmpty()) { + dataDirs = "/usr/local/share:/usr/share"; + } + + for (const QString& dir: dataDirs.split(':', Qt::SkipEmptyParts)) { + if (!dir.isEmpty()) { + // Reduce the amount of files we scan and watch since we only care about .desktop files + dataPaths.append(dir + "/applications"); + } + } + + return dataPaths; +} \ No newline at end of file diff --git a/src/core/desktoputils.hpp b/src/core/desktoputils.hpp new file mode 100644 index 0000000..4df3d60 --- /dev/null +++ b/src/core/desktoputils.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace DesktopUtils { +QList getDesktopDirectories(); +} \ No newline at end of file From 537d415cc62d6f7f7379d21943ac15f22ecd2980 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 14 Jul 2025 21:50:54 -0400 Subject: [PATCH 3/7] Cleanup and optimizations De-dupe code paths to scanAndWatch function, Optimize lookups by using a QSet, remove paths from watcher if deleted --- src/core/desktopentrymonitor.cpp | 101 ++++++++++++++++--------------- src/core/desktopentrymonitor.hpp | 5 +- 2 files changed, 55 insertions(+), 51 deletions(-) diff --git a/src/core/desktopentrymonitor.cpp b/src/core/desktopentrymonitor.cpp index e502813..4adafb8 100644 --- a/src/core/desktopentrymonitor.cpp +++ b/src/core/desktopentrymonitor.cpp @@ -11,7 +11,7 @@ #include "logcat.hpp" namespace { -QS_LOGGING_CATEGORY(logDesktopMonitor, "quickshell.desktopmonitor", QtWarningMsg); +QS_LOGGING_CATEGORY(logDesktopMonitor, "quickshell.desktopentrymonitor", QtWarningMsg); } DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) { @@ -50,54 +50,76 @@ void DesktopEntryMonitor::startMonitoring() { for (const QString& path: this->desktopPaths) { if (QDir(path).exists()) { qCDebug(logDesktopMonitor) << "Monitoring desktop entry path:" << path; - this->addDirectoryRecursive(path); + this->addDirectoryHierarchy(path); } } } -void DesktopEntryMonitor::addDirectoryRecursive(const QString& dirPath) { +void DesktopEntryMonitor::addDirectoryHierarchy(const QString& dirPath) { + this->scanAndWatch(dirPath); +} + +void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) { QDir dir(dirPath); if (!dir.exists()) return; - // Add root directory to watcher + // Add directory to watcher if (this->watcher->addPath(dirPath)) { qCDebug(logDesktopMonitor) << "Added directory to watcher:" << dirPath; } - QDirIterator it( - dirPath, - QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot, - QDirIterator::Subdirectories - ); - - while (it.hasNext()) { - QString path = it.next(); - QFileInfo info(path); - - if (info.isDir()) { + // Add .desktop files + for (const auto& entry: dir.entryInfoList({"*.desktop"}, QDir::Files)) { + auto path = entry.absoluteFilePath(); + if (!this->watchedFiles.contains(path)) { if (this->watcher->addPath(path)) { - qCDebug(logDesktopMonitor) << "Added directory to watcher:" << path; - } - } else if (path.endsWith(".desktop")) { - if (this->watcher->addPath(path)) { - this->fileTimestamps[path] = info.lastModified(); + this->fileTimestamps[path] = entry.lastModified(); + this->watchedFiles.insert(path); qCDebug(logDesktopMonitor) << "Monitoring file:" << path; } } } + + // Recurse into subdirs + for (const auto& sub: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { + this->scanAndWatch(dir.absoluteFilePath(sub)); + } } void DesktopEntryMonitor::onDirectoryChanged(const QString& path) { qCDebug(logDesktopMonitor) << "Directory changed:" << path; QDir dir(path); + + // Check if directory still exists - handle removal/unmounting + if (!dir.exists()) { + qCDebug(logDesktopMonitor) << "Directory no longer exists, cleaning up:" << path; + this->watcher->removePath(path); + + // Remove all watched files from this directory and its subdirectories + for (auto it = this->watchedFiles.begin(); it != this->watchedFiles.end();) { + const QString& watchedFile = *it; + if (watchedFile.startsWith(path + "/") || watchedFile == path) { + this->watcher->removePath(watchedFile); + this->fileTimestamps.remove(watchedFile); + this->queueChange(ChangeEvent::Removed, watchedFile); + qCDebug(logDesktopMonitor) << "Removed file due to directory deletion:" << watchedFile; + it = this->watchedFiles.erase(it); + } else { + ++it; + } + } + return; + } + QList currentFiles = dir.entryList({"*.desktop"}, QDir::Files); // Check for new files for (const QString& file: currentFiles) { QString fullPath = dir.absoluteFilePath(file); - if (!this->watcher->files().contains(fullPath)) { + if (!this->watchedFiles.contains(fullPath)) { if (this->watcher->addPath(fullPath)) { this->fileTimestamps[fullPath] = QFileInfo(fullPath).lastModified(); + this->watchedFiles.insert(fullPath); this->queueChange(ChangeEvent::Added, fullPath); qCDebug(logDesktopMonitor) << "New desktop file detected:" << fullPath; } @@ -105,13 +127,16 @@ void DesktopEntryMonitor::onDirectoryChanged(const QString& path) { } // Check for deleted files - QList watchedFiles = this->watcher->files(); - for (const QString& watchedFile: watchedFiles) { + for (auto it = this->watchedFiles.begin(); it != this->watchedFiles.end();) { + const QString& watchedFile = *it; if (QFileInfo(watchedFile).dir().absolutePath() == path && !QFile::exists(watchedFile)) { this->watcher->removePath(watchedFile); this->fileTimestamps.remove(watchedFile); this->queueChange(ChangeEvent::Removed, watchedFile); qCDebug(logDesktopMonitor) << "Desktop file removed:" << watchedFile; + it = this->watchedFiles.erase(it); + } else { + ++it; } } @@ -120,38 +145,13 @@ void DesktopEntryMonitor::onDirectoryChanged(const QString& path) { for (const QString& subdir: subdirs) { QString subdirPath = dir.absoluteFilePath(subdir); if (!this->watcher->directories().contains(subdirPath)) { - // Add the new subdirectory and all its contents iteratively - if (this->watcher->addPath(subdirPath)) { - qCDebug(logDesktopMonitor) << "Added new directory to watcher:" << subdirPath; - } - - QDirIterator it( - subdirPath, - QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot, - QDirIterator::Subdirectories - ); - - while (it.hasNext()) { - QString path = it.next(); - QFileInfo info(path); - - if (info.isDir()) { - if (this->watcher->addPath(path)) { - qCDebug(logDesktopMonitor) << "Added directory to watcher:" << path; - } - } else if (path.endsWith(".desktop")) { - if (this->watcher->addPath(path)) { - this->fileTimestamps[path] = info.lastModified(); - qCDebug(logDesktopMonitor) << "Monitoring file:" << path; - } - } - } + this->scanAndWatch(subdirPath); } } } void DesktopEntryMonitor::onFileChanged(const QString& path) { - if (!path.endsWith(".desktop")) return; + if (QFileInfo(path).suffix().toLower() != "desktop") return; QFileInfo info(path); if (info.exists()) { @@ -165,6 +165,7 @@ void DesktopEntryMonitor::onFileChanged(const QString& path) { // File was deleted this->watcher->removePath(path); this->fileTimestamps.remove(path); + this->watchedFiles.remove(path); this->queueChange(ChangeEvent::Removed, path); qCDebug(logDesktopMonitor) << "Desktop file removed:" << path; } diff --git a/src/core/desktopentrymonitor.hpp b/src/core/desktopentrymonitor.hpp index a28b6dd..d79e252 100644 --- a/src/core/desktopentrymonitor.hpp +++ b/src/core/desktopentrymonitor.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -27,12 +28,14 @@ private slots: private: void initializeDesktopPaths(); void startMonitoring(); - void addDirectoryRecursive(const QString& dirPath); + void addDirectoryHierarchy(const QString& dirPath); + void scanAndWatch(const QString& dirPath); void queueChange(ChangeEvent event, const QString& path); QFileSystemWatcher* watcher; QStringList desktopPaths; QHash fileTimestamps; + QSet watchedFiles; QTimer* debounceTimer; QHash pendingChanges; }; \ No newline at end of file From d6ab231a517242ff85d88a97d1534cacdd1cab59 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 14 Jul 2025 22:03:27 -0400 Subject: [PATCH 4/7] Use extractIdFromPath helper in scanPath --- src/core/desktopentry.cpp | 6 +++--- src/core/desktopentry.hpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 14b57a8..616839c 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -308,11 +308,11 @@ void DesktopEntryManager::populateApplications() { } } -void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { +void DesktopEntryManager::scanPath(const QDir& dir) { auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); for (auto& entry: entries) { - if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-"); + if (entry.isDir()) this->scanPath(entry.absoluteFilePath()); else if (entry.isFile()) { auto path = entry.filePath(); if (!path.endsWith(".desktop")) { @@ -326,7 +326,7 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { continue; } - auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8); + auto id = this->extractIdFromPath(entry.absoluteFilePath()); auto lowerId = id.toLower(); auto text = QString::fromUtf8(file.readAll()); diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index ebfec91..0475cae 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -169,7 +169,7 @@ private slots: explicit DesktopEntryManager(); void populateApplications(); - void scanPath(const QDir& dir, const QString& prefix = QString()); + void scanPath(const QDir& dir); QString extractIdFromPath(const QString& path); void updateApplicationModel(); From 0bbce67fd055852706f2865ef6ea3315757d7c9f Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 14 Jul 2025 22:49:07 -0400 Subject: [PATCH 5/7] Only watch the desktop folders - Create less fs watchers - Just re-scan when a directory containing .desktop files changes --- src/core/desktopentry.cpp | 72 +++++++------------------ src/core/desktopentrymonitor.cpp | 93 ++++---------------------------- src/core/desktopentrymonitor.hpp | 5 -- 3 files changed, 28 insertions(+), 142 deletions(-) diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 616839c..5307610 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -384,67 +384,33 @@ DesktopEntry* DesktopEntryManager::byId(const QString& id) { ObjectModel* DesktopEntryManager::applications() { return &this->mApplications; } void DesktopEntryManager::handleFileChanges( - const QHash& changes + const QHash& ) { + qCDebug(logDesktopEntry) << "Directory change detected, performing full rescan"; - qCDebug(logDesktopEntry) << "Handling file changes:" << changes.size() << "changes"; - - bool needsUpdate = false; - - for (auto it = changes.begin(); it != changes.end(); ++it) { - const QString& path = it.key(); - DesktopEntryMonitor::ChangeEvent event = it.value(); - - switch (event) { - case DesktopEntryMonitor::ChangeEvent::Added: - case DesktopEntryMonitor::ChangeEvent::Modified: { - // Parse the desktop file - QFile file(path); - if (file.open(QFile::ReadOnly)) { - QString id = this->extractIdFromPath(path); - auto* entry = new DesktopEntry(id, this); - entry->parseEntry(QString::fromUtf8(file.readAll())); - - if (entry->isValid()) { - // Remove old entry if exists - if (this->desktopEntries.contains(id)) { - auto* oldEntry = this->desktopEntries.value(id); - this->desktopEntries.remove(id); - this->lowercaseDesktopEntries.remove(id.toLower()); - oldEntry->deleteLater(); - } - - this->desktopEntries.insert(id, entry); - this->lowercaseDesktopEntries.insert(id.toLower(), entry); - needsUpdate = true; - - qCDebug(logDesktopEntry) << "Updated desktop entry:" << id; - } else { - delete entry; - } - } - break; - } + auto oldEntries = this->desktopEntries; - case DesktopEntryMonitor::ChangeEvent::Removed: { - QString id = this->extractIdFromPath(path); - if (this->desktopEntries.contains(id)) { - auto* entry = this->desktopEntries.take(id); - this->lowercaseDesktopEntries.remove(id.toLower()); - entry->deleteLater(); - needsUpdate = true; + this->desktopEntries.clear(); + this->lowercaseDesktopEntries.clear(); - qCDebug(logDesktopEntry) << "Removed desktop entry:" << id; - } - break; - } + this->scanDesktopEntries(); + + QVector newApplications; + for (auto& entry: this->desktopEntries.values()) { + if (!entry->noDisplay()) { + newApplications.append(entry); } } - if (needsUpdate) { - this->updateApplicationModel(); - emit applicationsChanged(); + this->mApplications.diffUpdate(newApplications); + + for (auto* e: oldEntries) { + if (!this->desktopEntries.contains(e->mId)) { + e->deleteLater(); + } } + + emit applicationsChanged(); } QString DesktopEntryManager::extractIdFromPath(const QString& path) { diff --git a/src/core/desktopentrymonitor.cpp b/src/core/desktopentrymonitor.cpp index 4adafb8..daf1be1 100644 --- a/src/core/desktopentrymonitor.cpp +++ b/src/core/desktopentrymonitor.cpp @@ -30,12 +30,6 @@ DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) { this, &DesktopEntryMonitor::onDirectoryChanged ); - connect( - this->watcher, - &QFileSystemWatcher::fileChanged, - this, - &DesktopEntryMonitor::onFileChanged - ); connect(this->debounceTimer, &QTimer::timeout, this, &DesktopEntryMonitor::processChanges); // Start monitoring @@ -68,18 +62,6 @@ void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) { qCDebug(logDesktopMonitor) << "Added directory to watcher:" << dirPath; } - // Add .desktop files - for (const auto& entry: dir.entryInfoList({"*.desktop"}, QDir::Files)) { - auto path = entry.absoluteFilePath(); - if (!this->watchedFiles.contains(path)) { - if (this->watcher->addPath(path)) { - this->fileTimestamps[path] = entry.lastModified(); - this->watchedFiles.insert(path); - qCDebug(logDesktopMonitor) << "Monitoring file:" << path; - } - } - } - // Recurse into subdirs for (const auto& sub: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { this->scanAndWatch(dir.absoluteFilePath(sub)); @@ -94,52 +76,11 @@ void DesktopEntryMonitor::onDirectoryChanged(const QString& path) { if (!dir.exists()) { qCDebug(logDesktopMonitor) << "Directory no longer exists, cleaning up:" << path; this->watcher->removePath(path); - - // Remove all watched files from this directory and its subdirectories - for (auto it = this->watchedFiles.begin(); it != this->watchedFiles.end();) { - const QString& watchedFile = *it; - if (watchedFile.startsWith(path + "/") || watchedFile == path) { - this->watcher->removePath(watchedFile); - this->fileTimestamps.remove(watchedFile); - this->queueChange(ChangeEvent::Removed, watchedFile); - qCDebug(logDesktopMonitor) << "Removed file due to directory deletion:" << watchedFile; - it = this->watchedFiles.erase(it); - } else { - ++it; - } - } + // Directory removal will be handled by full rescan + this->queueChange(ChangeEvent::Modified, path); // Trigger full rescan return; } - QList currentFiles = dir.entryList({"*.desktop"}, QDir::Files); - - // Check for new files - for (const QString& file: currentFiles) { - QString fullPath = dir.absoluteFilePath(file); - if (!this->watchedFiles.contains(fullPath)) { - if (this->watcher->addPath(fullPath)) { - this->fileTimestamps[fullPath] = QFileInfo(fullPath).lastModified(); - this->watchedFiles.insert(fullPath); - this->queueChange(ChangeEvent::Added, fullPath); - qCDebug(logDesktopMonitor) << "New desktop file detected:" << fullPath; - } - } - } - - // Check for deleted files - for (auto it = this->watchedFiles.begin(); it != this->watchedFiles.end();) { - const QString& watchedFile = *it; - if (QFileInfo(watchedFile).dir().absolutePath() == path && !QFile::exists(watchedFile)) { - this->watcher->removePath(watchedFile); - this->fileTimestamps.remove(watchedFile); - this->queueChange(ChangeEvent::Removed, watchedFile); - qCDebug(logDesktopMonitor) << "Desktop file removed:" << watchedFile; - it = this->watchedFiles.erase(it); - } else { - ++it; - } - } - // Check for new subdirectories QList subdirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& subdir: subdirs) { @@ -148,27 +89,9 @@ void DesktopEntryMonitor::onDirectoryChanged(const QString& path) { this->scanAndWatch(subdirPath); } } -} - -void DesktopEntryMonitor::onFileChanged(const QString& path) { - if (QFileInfo(path).suffix().toLower() != "desktop") return; - QFileInfo info(path); - if (info.exists()) { - QDateTime currentModified = info.lastModified(); - if (this->fileTimestamps.value(path) != currentModified) { - this->fileTimestamps[path] = currentModified; - this->queueChange(ChangeEvent::Modified, path); - qCDebug(logDesktopMonitor) << "Desktop file modified:" << path; - } - } else { - // File was deleted - this->watcher->removePath(path); - this->fileTimestamps.remove(path); - this->watchedFiles.remove(path); - this->queueChange(ChangeEvent::Removed, path); - qCDebug(logDesktopMonitor) << "Desktop file removed:" << path; - } + // Queue a change to trigger full rescan of all desktop paths + this->queueChange(ChangeEvent::Modified, path); } void DesktopEntryMonitor::queueChange(ChangeEvent event, const QString& path) { @@ -179,10 +102,12 @@ void DesktopEntryMonitor::queueChange(ChangeEvent event, const QString& path) { void DesktopEntryMonitor::processChanges() { if (this->pendingChanges.isEmpty()) return; - qCDebug(logDesktopMonitor) << "Processing" << this->pendingChanges.size() << "pending changes"; + qCDebug(logDesktopMonitor) << "Processing directory changes, triggering full rescan"; - QHash changes = this->pendingChanges; + // Clear pending changes since we're doing a full rescan this->pendingChanges.clear(); - emit desktopEntriesChanged(changes); + // Emit with empty hash to signal full rescan needed + QHash emptyChanges; + emit desktopEntriesChanged(emptyChanges); } \ No newline at end of file diff --git a/src/core/desktopentrymonitor.hpp b/src/core/desktopentrymonitor.hpp index d79e252..dec85ab 100644 --- a/src/core/desktopentrymonitor.hpp +++ b/src/core/desktopentrymonitor.hpp @@ -1,10 +1,8 @@ #pragma once -#include #include #include #include -#include #include #include @@ -22,7 +20,6 @@ class DesktopEntryMonitor: public QObject { private slots: void onDirectoryChanged(const QString& path); - void onFileChanged(const QString& path); void processChanges(); private: @@ -34,8 +31,6 @@ private slots: QFileSystemWatcher* watcher; QStringList desktopPaths; - QHash fileTimestamps; - QSet watchedFiles; QTimer* debounceTimer; QHash pendingChanges; }; \ No newline at end of file From daa7f934dfc8c5c74cb3554a3558adc7a4609954 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 14 Jul 2025 23:32:10 -0400 Subject: [PATCH 6/7] consistency cleanups --- src/core/desktopentrymonitor.cpp | 23 ++++++++++++----------- src/core/desktopentrymonitor.hpp | 1 - 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/desktopentrymonitor.cpp b/src/core/desktopentrymonitor.cpp index daf1be1..1398b86 100644 --- a/src/core/desktopentrymonitor.cpp +++ b/src/core/desktopentrymonitor.cpp @@ -18,19 +18,24 @@ DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) { this->watcher = new QFileSystemWatcher(this); this->debounceTimer = new QTimer(this); this->debounceTimer->setSingleShot(true); - this->debounceTimer->setInterval(500); // 500ms debounce + this->debounceTimer->setInterval(50); // Initialize XDG desktop paths this->initializeDesktopPaths(); // Setup connections - connect( + QObject::connect( this->watcher, &QFileSystemWatcher::directoryChanged, this, &DesktopEntryMonitor::onDirectoryChanged ); - connect(this->debounceTimer, &QTimer::timeout, this, &DesktopEntryMonitor::processChanges); + QObject::connect( + this->debounceTimer, + &QTimer::timeout, + this, + &DesktopEntryMonitor::processChanges + ); // Start monitoring this->startMonitoring(); @@ -44,17 +49,13 @@ void DesktopEntryMonitor::startMonitoring() { for (const QString& path: this->desktopPaths) { if (QDir(path).exists()) { qCDebug(logDesktopMonitor) << "Monitoring desktop entry path:" << path; - this->addDirectoryHierarchy(path); + this->scanAndWatch(path); } } } -void DesktopEntryMonitor::addDirectoryHierarchy(const QString& dirPath) { - this->scanAndWatch(dirPath); -} - void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) { - QDir dir(dirPath); + auto dir = QDir(dirPath); if (!dir.exists()) return; // Add directory to watcher @@ -70,7 +71,7 @@ void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) { void DesktopEntryMonitor::onDirectoryChanged(const QString& path) { qCDebug(logDesktopMonitor) << "Directory changed:" << path; - QDir dir(path); + auto dir = QDir(path); // Check if directory still exists - handle removal/unmounting if (!dir.exists()) { @@ -82,7 +83,7 @@ void DesktopEntryMonitor::onDirectoryChanged(const QString& path) { } // Check for new subdirectories - QList subdirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + auto subdirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& subdir: subdirs) { QString subdirPath = dir.absoluteFilePath(subdir); if (!this->watcher->directories().contains(subdirPath)) { diff --git a/src/core/desktopentrymonitor.hpp b/src/core/desktopentrymonitor.hpp index dec85ab..d7f4621 100644 --- a/src/core/desktopentrymonitor.hpp +++ b/src/core/desktopentrymonitor.hpp @@ -25,7 +25,6 @@ private slots: private: void initializeDesktopPaths(); void startMonitoring(); - void addDirectoryHierarchy(const QString& dirPath); void scanAndWatch(const QString& dirPath); void queueChange(ChangeEvent event, const QString& path); From b9cbcd27c5107e6b8c0bd027a52ff88f318a9384 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 14 Jul 2025 23:40:02 -0400 Subject: [PATCH 7/7] use auto everywhere --- src/core/desktopentry.cpp | 12 ++++++------ src/core/desktopentrymonitor.cpp | 4 ++-- src/core/desktoputils.cpp | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 5307610..1b4ebcd 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -418,17 +418,17 @@ QString DesktopEntryManager::extractIdFromPath(const QString& path) { // e.g., /usr/share/applications/firefox.desktop -> firefox // e.g., /usr/share/applications/kde4/kate.desktop -> kde4-kate - QFileInfo info(path); - QString id = info.completeBaseName(); + auto info = QFileInfo(path); + auto id = info.completeBaseName(); // Find the applications directory in the path - int appIdx = path.lastIndexOf("/applications/"); + auto appIdx = path.lastIndexOf("/applications/"); if (appIdx != -1) { - QString relativePath = path.mid(appIdx + 14); // Skip "/applications/" - QFileInfo relInfo(relativePath); + auto relativePath = path.mid(appIdx + 14); // Skip "/applications/" + auto relInfo = QFileInfo(relativePath); // Replace directory separators with dashes - QString dirPath = relInfo.path(); + auto dirPath = relInfo.path(); if (dirPath != ".") { id = dirPath.replace('/', '-') + '-' + relInfo.completeBaseName(); } diff --git a/src/core/desktopentrymonitor.cpp b/src/core/desktopentrymonitor.cpp index 1398b86..0a04706 100644 --- a/src/core/desktopentrymonitor.cpp +++ b/src/core/desktopentrymonitor.cpp @@ -85,7 +85,7 @@ void DesktopEntryMonitor::onDirectoryChanged(const QString& path) { // Check for new subdirectories auto subdirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& subdir: subdirs) { - QString subdirPath = dir.absoluteFilePath(subdir); + auto subdirPath = dir.absoluteFilePath(subdir); if (!this->watcher->directories().contains(subdirPath)) { this->scanAndWatch(subdirPath); } @@ -109,6 +109,6 @@ void DesktopEntryMonitor::processChanges() { this->pendingChanges.clear(); // Emit with empty hash to signal full rescan needed - QHash emptyChanges; + auto emptyChanges = QHash {}; emit desktopEntriesChanged(emptyChanges); } \ No newline at end of file diff --git a/src/core/desktoputils.cpp b/src/core/desktoputils.cpp index 700c45f..9029580 100644 --- a/src/core/desktoputils.cpp +++ b/src/core/desktoputils.cpp @@ -4,10 +4,10 @@ #include QList DesktopUtils::getDesktopDirectories() { - QList dataPaths; + auto dataPaths = QList {}; // XDG_DATA_HOME - QString dataHome = qEnvironmentVariable("XDG_DATA_HOME"); + auto dataHome = qEnvironmentVariable("XDG_DATA_HOME"); if (dataHome.isEmpty()) { if (qEnvironmentVariableIsSet("HOME")) { dataHome = qEnvironmentVariable("HOME") + "/.local/share"; @@ -18,7 +18,7 @@ QList DesktopUtils::getDesktopDirectories() { } // XDG_DATA_DIRS - QString dataDirs = qEnvironmentVariable("XDG_DATA_DIRS"); + auto dataDirs = qEnvironmentVariable("XDG_DATA_DIRS"); if (dataDirs.isEmpty()) { dataDirs = "/usr/local/share:/usr/share"; }