forked from quickshell/quickshell
		
	core/desktopentry: add limited desktop entry api
This commit is contained in:
		
							parent
							
								
									ce5ddbf8ba
								
							
						
					
					
						commit
						f655875547
					
				
					 4 changed files with 520 additions and 0 deletions
				
			
		| 
						 | 
					@ -28,6 +28,7 @@ qt_add_library(quickshell-core STATIC
 | 
				
			||||||
	boundcomponent.cpp
 | 
						boundcomponent.cpp
 | 
				
			||||||
	model.cpp
 | 
						model.cpp
 | 
				
			||||||
	elapsedtimer.cpp
 | 
						elapsedtimer.cpp
 | 
				
			||||||
 | 
						desktopentry.cpp
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
 | 
					set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										367
									
								
								src/core/desktopentry.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										367
									
								
								src/core/desktopentry.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,367 @@
 | 
				
			||||||
 | 
					#include "desktopentry.hpp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <qcontainerfwd.h>
 | 
				
			||||||
 | 
					#include <qdebug.h>
 | 
				
			||||||
 | 
					#include <qdir.h>
 | 
				
			||||||
 | 
					#include <qfileinfo.h>
 | 
				
			||||||
 | 
					#include <qfilesystemwatcher.h>
 | 
				
			||||||
 | 
					#include <qhash.h>
 | 
				
			||||||
 | 
					#include <qlist.h>
 | 
				
			||||||
 | 
					#include <qlocale.h>
 | 
				
			||||||
 | 
					#include <qlogging.h>
 | 
				
			||||||
 | 
					#include <qloggingcategory.h>
 | 
				
			||||||
 | 
					#include <qnamespace.h>
 | 
				
			||||||
 | 
					#include <qobject.h>
 | 
				
			||||||
 | 
					#include <qpair.h>
 | 
				
			||||||
 | 
					#include <qprocess.h>
 | 
				
			||||||
 | 
					#include <qstringview.h>
 | 
				
			||||||
 | 
					#include <qtenvironmentvariables.h>
 | 
				
			||||||
 | 
					#include <ranges>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "model.hpp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Locale {
 | 
				
			||||||
 | 
						explicit Locale() = default;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						explicit Locale(const QString& string) {
 | 
				
			||||||
 | 
							auto territoryIdx = string.indexOf('_');
 | 
				
			||||||
 | 
							auto codesetIdx = string.indexOf('.');
 | 
				
			||||||
 | 
							auto modifierIdx = string.indexOf('@');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							auto parseEnd = string.length();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (modifierIdx != -1) {
 | 
				
			||||||
 | 
								this->modifier = string.sliced(modifierIdx + 1, parseEnd - modifierIdx - 1);
 | 
				
			||||||
 | 
								parseEnd = modifierIdx;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (codesetIdx != -1) {
 | 
				
			||||||
 | 
								parseEnd = codesetIdx;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (territoryIdx != -1) {
 | 
				
			||||||
 | 
								this->territory = string.sliced(territoryIdx + 1, parseEnd - territoryIdx - 1);
 | 
				
			||||||
 | 
								parseEnd = territoryIdx;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this->language = string.sliced(0, parseEnd);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[[nodiscard]] bool isValid() const { return !this->language.isEmpty(); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[[nodiscard]] int matchScore(const Locale& other) const {
 | 
				
			||||||
 | 
							if (this->language != other.language) return 0;
 | 
				
			||||||
 | 
							auto territoryMatches = !this->territory.isEmpty() && this->territory == other.territory;
 | 
				
			||||||
 | 
							auto modifierMatches = !this->modifier.isEmpty() && this->modifier == other.modifier;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							auto score = 1;
 | 
				
			||||||
 | 
							if (territoryMatches) score += 2;
 | 
				
			||||||
 | 
							if (modifierMatches) score += 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return score;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						static const Locale& system() {
 | 
				
			||||||
 | 
							static Locale* locale = nullptr; // NOLINT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (locale == nullptr) {
 | 
				
			||||||
 | 
								auto lstr = qEnvironmentVariable("LC_MESSAGES");
 | 
				
			||||||
 | 
								if (lstr.isEmpty()) lstr = qEnvironmentVariable("LANG");
 | 
				
			||||||
 | 
								locale = new Locale(lstr);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return *locale;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						QString language;
 | 
				
			||||||
 | 
						QString territory;
 | 
				
			||||||
 | 
						QString modifier;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					QDebug operator<<(QDebug debug, const Locale& locale) {
 | 
				
			||||||
 | 
						auto saver = QDebugStateSaver(debug);
 | 
				
			||||||
 | 
						debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
 | 
				
			||||||
 | 
						                << ", modifier" << locale.modifier << ')';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return debug;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void DesktopEntry::parseEntry(const QString& text) {
 | 
				
			||||||
 | 
						const auto& system = Locale::system();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						auto groupName = QString();
 | 
				
			||||||
 | 
						auto entries = QHash<QString, QPair<Locale, QString>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						auto finishCategory = [&]() {
 | 
				
			||||||
 | 
							if (groupName == "Desktop Entry") {
 | 
				
			||||||
 | 
								if (entries["Type"].second != "Application") return;
 | 
				
			||||||
 | 
								if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for (const auto& [key, pair]: entries.asKeyValueRange()) {
 | 
				
			||||||
 | 
									auto& [_, value] = pair;
 | 
				
			||||||
 | 
									this->mEntries.insert(key, value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if (key == "Name") this->mName = value;
 | 
				
			||||||
 | 
									else if (key == "GenericName") this->mGenericName = value;
 | 
				
			||||||
 | 
									else if (key == "NoDisplay") this->mNoDisplay = value == "true";
 | 
				
			||||||
 | 
									else if (key == "Comment") this->mComment = value;
 | 
				
			||||||
 | 
									else if (key == "Icon") this->mIcon = value;
 | 
				
			||||||
 | 
									else if (key == "Exec") this->mExecString = value;
 | 
				
			||||||
 | 
									else if (key == "Path") this->mWorkingDirectory = value;
 | 
				
			||||||
 | 
									else if (key == "Terminal") this->mTerminal = value == "true";
 | 
				
			||||||
 | 
									else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts);
 | 
				
			||||||
 | 
									else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else if (groupName.startsWith("Desktop Action ")) {
 | 
				
			||||||
 | 
								auto actionName = groupName.sliced(16);
 | 
				
			||||||
 | 
								auto* action = new DesktopAction(actionName, this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for (const auto& [key, pair]: entries.asKeyValueRange()) {
 | 
				
			||||||
 | 
									const auto& [_, value] = pair;
 | 
				
			||||||
 | 
									action->mEntries.insert(key, value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if (key == "Name") action->mName = value;
 | 
				
			||||||
 | 
									else if (key == "Icon") action->mIcon = value;
 | 
				
			||||||
 | 
									else if (key == "Exec") action->mExecString = value;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								this->mActions.insert(actionName, action);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							entries.clear();
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (auto& line: text.split(u'\n', Qt::SkipEmptyParts)) {
 | 
				
			||||||
 | 
							if (line.startsWith(u'#')) continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (line.startsWith(u'[') && line.endsWith(u']')) {
 | 
				
			||||||
 | 
								finishCategory();
 | 
				
			||||||
 | 
								groupName = line.sliced(1, line.length() - 2);
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							auto splitIdx = line.indexOf(u'=');
 | 
				
			||||||
 | 
							if (splitIdx == -1) {
 | 
				
			||||||
 | 
								qCDebug(logDesktopEntry) << "Encountered invalid line in desktop entry (no =)" << line;
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							auto key = line.sliced(0, splitIdx);
 | 
				
			||||||
 | 
							const auto& value = line.sliced(splitIdx + 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							auto localeIdx = key.indexOf('[');
 | 
				
			||||||
 | 
							Locale locale;
 | 
				
			||||||
 | 
							if (localeIdx != -1 && localeIdx != key.length() - 1) {
 | 
				
			||||||
 | 
								locale = Locale(key.sliced(localeIdx + 1, key.length() - localeIdx - 2));
 | 
				
			||||||
 | 
								key = key.sliced(0, localeIdx);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (entries.contains(key)) {
 | 
				
			||||||
 | 
								const auto& old = entries.value(key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (system.matchScore(locale) > system.matchScore(old.first)) {
 | 
				
			||||||
 | 
									entries.insert(key, qMakePair(locale, value));
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								entries.insert(key, qMakePair(locale, value));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						finishCategory();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void DesktopEntry::execute() const {
 | 
				
			||||||
 | 
						DesktopEntry::doExec(this->mExecString, this->mWorkingDirectory);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); }
 | 
				
			||||||
 | 
					bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
 | 
				
			||||||
 | 
						QVector<QString> arguments;
 | 
				
			||||||
 | 
						QString currentArgument;
 | 
				
			||||||
 | 
						auto parsingString = false;
 | 
				
			||||||
 | 
						auto escape = 0;
 | 
				
			||||||
 | 
						auto percent = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (auto c: execString) {
 | 
				
			||||||
 | 
							if (escape == 0 && c == u'\\') {
 | 
				
			||||||
 | 
								escape = 1;
 | 
				
			||||||
 | 
							} else if (parsingString) {
 | 
				
			||||||
 | 
								if (c == '\\') {
 | 
				
			||||||
 | 
									escape++;
 | 
				
			||||||
 | 
									if (escape == 4) {
 | 
				
			||||||
 | 
										currentArgument += '\\';
 | 
				
			||||||
 | 
										escape = 0;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else if (escape != 0) {
 | 
				
			||||||
 | 
									if (escape != 2) {
 | 
				
			||||||
 | 
										// Technically this is an illegal state, but the spec has a terrible double escape
 | 
				
			||||||
 | 
										// rule in strings for no discernable reason. Assuming someone might understandably
 | 
				
			||||||
 | 
										// misunderstand it, treat it as a normal escape and log it.
 | 
				
			||||||
 | 
										qCWarning(logDesktopEntry).noquote()
 | 
				
			||||||
 | 
										    << "Illegal escape sequence in desktop entry exec string:" << execString;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									currentArgument += c;
 | 
				
			||||||
 | 
									escape = 0;
 | 
				
			||||||
 | 
								} else if (c == u'"') {
 | 
				
			||||||
 | 
									parsingString = false;
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									currentArgument += c;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else if (escape != 0) {
 | 
				
			||||||
 | 
								currentArgument += c;
 | 
				
			||||||
 | 
								escape = 0;
 | 
				
			||||||
 | 
							} else if (percent) {
 | 
				
			||||||
 | 
								if (c == '%') {
 | 
				
			||||||
 | 
									currentArgument += '%';
 | 
				
			||||||
 | 
								} // else discard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								percent = false;
 | 
				
			||||||
 | 
							} else if (c == '%') {
 | 
				
			||||||
 | 
								percent = true;
 | 
				
			||||||
 | 
							} else if (c == u'"') {
 | 
				
			||||||
 | 
								parsingString = true;
 | 
				
			||||||
 | 
							} else if (c == u' ') {
 | 
				
			||||||
 | 
								if (!currentArgument.isEmpty()) {
 | 
				
			||||||
 | 
									arguments.push_back(currentArgument);
 | 
				
			||||||
 | 
									currentArgument.clear();
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								currentArgument += c;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!currentArgument.isEmpty()) {
 | 
				
			||||||
 | 
							arguments.push_back(currentArgument);
 | 
				
			||||||
 | 
							currentArgument.clear();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return arguments;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void DesktopEntry::doExec(const QString& execString, const QString& workingDirectory) {
 | 
				
			||||||
 | 
						auto args = DesktopEntry::parseExecString(execString);
 | 
				
			||||||
 | 
						if (args.isEmpty()) {
 | 
				
			||||||
 | 
							qCWarning(logDesktopEntry) << "Tried to exec string" << execString << "which parsed as empty.";
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						auto process = QProcess();
 | 
				
			||||||
 | 
						process.setProgram(args.at(0));
 | 
				
			||||||
 | 
						process.setArguments(args.sliced(1));
 | 
				
			||||||
 | 
						if (!workingDirectory.isEmpty()) process.setWorkingDirectory(workingDirectory);
 | 
				
			||||||
 | 
						process.startDetached();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void DesktopAction::execute() const {
 | 
				
			||||||
 | 
						DesktopEntry::doExec(this->mExecString, this->entry->mWorkingDirectory);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DesktopEntryManager::DesktopEntryManager() {
 | 
				
			||||||
 | 
						this->scanDesktopEntries();
 | 
				
			||||||
 | 
						this->populateApplications();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void DesktopEntryManager::scanDesktopEntries() {
 | 
				
			||||||
 | 
						QList<QString> dataPaths;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						qCDebug(logDesktopEntry) << "Creating desktop entry scanners";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (auto& path: std::ranges::reverse_view(dataPaths)) {
 | 
				
			||||||
 | 
							auto p = QDir(path).filePath("applications");
 | 
				
			||||||
 | 
							auto file = QFileInfo(p);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!file.isDir()) {
 | 
				
			||||||
 | 
								qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory";
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							qCDebug(logDesktopEntry) << "Scanning path" << p;
 | 
				
			||||||
 | 
							this->scanPath(p);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void DesktopEntryManager::populateApplications() {
 | 
				
			||||||
 | 
						for (auto& entry: this->desktopEntries.values()) {
 | 
				
			||||||
 | 
							if (!entry->noDisplay()) this->mApplications.insertObject(entry);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
 | 
				
			||||||
 | 
						auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (auto& entry: entries) {
 | 
				
			||||||
 | 
							if (entry.isDir()) this->scanPath(entry.path(), prefix + dir.dirName() + "-");
 | 
				
			||||||
 | 
							else if (entry.isFile()) {
 | 
				
			||||||
 | 
								auto path = entry.filePath();
 | 
				
			||||||
 | 
								if (!path.endsWith(".desktop")) {
 | 
				
			||||||
 | 
									qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
 | 
				
			||||||
 | 
									continue;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								auto* file = new QFile(path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (!file->open(QFile::ReadOnly)) {
 | 
				
			||||||
 | 
									qCDebug(logDesktopEntry) << "Could not open file" << path;
 | 
				
			||||||
 | 
									continue;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								auto text = QString::fromUtf8(file->readAll());
 | 
				
			||||||
 | 
								auto* dentry = new DesktopEntry(id, this);
 | 
				
			||||||
 | 
								dentry->parseEntry(text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (!dentry->isValid()) {
 | 
				
			||||||
 | 
									qCDebug(logDesktopEntry) << "Skipping desktop entry" << path;
 | 
				
			||||||
 | 
									delete dentry;
 | 
				
			||||||
 | 
									continue;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								qCDebug(logDesktopEntry) << "Found desktop entry" << id << "at" << path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (desktopEntries.contains(id)) {
 | 
				
			||||||
 | 
									qCDebug(logDesktopEntry) << "Replacing old entry for" << id;
 | 
				
			||||||
 | 
									delete desktopEntries.value(id);
 | 
				
			||||||
 | 
									desktopEntries.remove(id);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								desktopEntries.insert(id, dentry);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DesktopEntryManager* DesktopEntryManager::instance() {
 | 
				
			||||||
 | 
						static auto* instance = new DesktopEntryManager(); // NOLINT
 | 
				
			||||||
 | 
						return instance;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DesktopEntry* DesktopEntryManager::byId(const QString& id) {
 | 
				
			||||||
 | 
						return this->desktopEntries.value(id);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DesktopEntry* DesktopEntries::byId(const QString& id) {
 | 
				
			||||||
 | 
						return DesktopEntryManager::instance()->byId(id);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ObjectModel<DesktopEntry>* DesktopEntries::applications() {
 | 
				
			||||||
 | 
						return DesktopEntryManager::instance()->applications();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										151
									
								
								src/core/desktopentry.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/core/desktopentry.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,151 @@
 | 
				
			||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <utility>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <qcontainerfwd.h>
 | 
				
			||||||
 | 
					#include <qdir.h>
 | 
				
			||||||
 | 
					#include <qhash.h>
 | 
				
			||||||
 | 
					#include <qobject.h>
 | 
				
			||||||
 | 
					#include <qqmlintegration.h>
 | 
				
			||||||
 | 
					#include <qtmetamacros.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "model.hpp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DesktopAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A desktop entry. See [DesktopEntries](../desktopentries) for details.
 | 
				
			||||||
 | 
					class DesktopEntry: public QObject {
 | 
				
			||||||
 | 
						Q_OBJECT;
 | 
				
			||||||
 | 
						Q_PROPERTY(QString id MEMBER mId CONSTANT);
 | 
				
			||||||
 | 
						/// Name of the specific application, such as "Firefox".
 | 
				
			||||||
 | 
						Q_PROPERTY(QString name MEMBER mName CONSTANT);
 | 
				
			||||||
 | 
						/// Short description of the application, such as "Web Browser". May be empty.
 | 
				
			||||||
 | 
						Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT);
 | 
				
			||||||
 | 
						/// If true, this application should not be displayed in menus and launchers.
 | 
				
			||||||
 | 
						Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT);
 | 
				
			||||||
 | 
						/// Long description of the application, such as "View websites on the internet". May be empty.
 | 
				
			||||||
 | 
						Q_PROPERTY(QString comment MEMBER mComment CONSTANT);
 | 
				
			||||||
 | 
						/// Name of the icon associated with this application. May be empty.
 | 
				
			||||||
 | 
						Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
 | 
				
			||||||
 | 
						/// The raw `Exec` string from the desktop entry. You probably want `execute()`.
 | 
				
			||||||
 | 
						Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
 | 
				
			||||||
 | 
						/// The working directory to execute from.
 | 
				
			||||||
 | 
						Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT);
 | 
				
			||||||
 | 
						/// If the application should run in a terminal.
 | 
				
			||||||
 | 
						Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT);
 | 
				
			||||||
 | 
						Q_PROPERTY(QVector<QString> categories MEMBER mCategories CONSTANT);
 | 
				
			||||||
 | 
						Q_PROPERTY(QVector<QString> keywords MEMBER mKeywords CONSTANT);
 | 
				
			||||||
 | 
						Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT);
 | 
				
			||||||
 | 
						QML_ELEMENT;
 | 
				
			||||||
 | 
						QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public:
 | 
				
			||||||
 | 
						explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						void parseEntry(const QString& text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// Run the application. Currently ignores `runInTerminal` and field codes.
 | 
				
			||||||
 | 
						Q_INVOKABLE void execute() const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[[nodiscard]] bool isValid() const;
 | 
				
			||||||
 | 
						[[nodiscard]] bool noDisplay() const;
 | 
				
			||||||
 | 
						[[nodiscard]] QVector<DesktopAction*> actions() const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// currently ignores all field codes.
 | 
				
			||||||
 | 
						static QVector<QString> parseExecString(const QString& execString);
 | 
				
			||||||
 | 
						static void doExec(const QString& execString, const QString& workingDirectory);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private:
 | 
				
			||||||
 | 
						QHash<QString, QString> mEntries;
 | 
				
			||||||
 | 
						QHash<QString, DesktopAction*> mActions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						QString mId;
 | 
				
			||||||
 | 
						QString mName;
 | 
				
			||||||
 | 
						QString mGenericName;
 | 
				
			||||||
 | 
						bool mNoDisplay = false;
 | 
				
			||||||
 | 
						QString mComment;
 | 
				
			||||||
 | 
						QString mIcon;
 | 
				
			||||||
 | 
						QString mExecString;
 | 
				
			||||||
 | 
						QString mWorkingDirectory;
 | 
				
			||||||
 | 
						bool mTerminal = false;
 | 
				
			||||||
 | 
						QVector<QString> mCategories;
 | 
				
			||||||
 | 
						QVector<QString> mKeywords;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						friend class DesktopAction;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// An action of a [DesktopEntry](../desktopentry).
 | 
				
			||||||
 | 
					class DesktopAction: public QObject {
 | 
				
			||||||
 | 
						Q_OBJECT;
 | 
				
			||||||
 | 
						Q_PROPERTY(QString id MEMBER mId CONSTANT);
 | 
				
			||||||
 | 
						Q_PROPERTY(QString name MEMBER mName CONSTANT);
 | 
				
			||||||
 | 
						Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
 | 
				
			||||||
 | 
						/// The raw `Exec` string from the desktop entry. You probably want `execute()`.
 | 
				
			||||||
 | 
						Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
 | 
				
			||||||
 | 
						QML_ELEMENT;
 | 
				
			||||||
 | 
						QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public:
 | 
				
			||||||
 | 
						explicit DesktopAction(QString id, DesktopEntry* entry)
 | 
				
			||||||
 | 
						    : QObject(entry)
 | 
				
			||||||
 | 
						    , entry(entry)
 | 
				
			||||||
 | 
						    , mId(std::move(id)) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// Run the application. Currently ignores `runInTerminal` and field codes.
 | 
				
			||||||
 | 
						Q_INVOKABLE void execute() const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private:
 | 
				
			||||||
 | 
						DesktopEntry* entry;
 | 
				
			||||||
 | 
						QString mId;
 | 
				
			||||||
 | 
						QString mName;
 | 
				
			||||||
 | 
						QString mIcon;
 | 
				
			||||||
 | 
						QString mExecString;
 | 
				
			||||||
 | 
						QHash<QString, QString> mEntries;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						friend class DesktopEntry;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DesktopEntryManager: public QObject {
 | 
				
			||||||
 | 
						Q_OBJECT;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public:
 | 
				
			||||||
 | 
						void scanDesktopEntries();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[[nodiscard]] DesktopEntry* byId(const QString& id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[[nodiscard]] ObjectModel<DesktopEntry>* applications();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						static DesktopEntryManager* instance();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private:
 | 
				
			||||||
 | 
						explicit DesktopEntryManager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						void populateApplications();
 | 
				
			||||||
 | 
						void scanPath(const QDir& dir, const QString& prefix = QString());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						QHash<QString, DesktopEntry*> desktopEntries;
 | 
				
			||||||
 | 
						ObjectModel<DesktopEntry> mApplications {this};
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					///! Desktop entry index.
 | 
				
			||||||
 | 
					/// Index of desktop entries according to the [desktop entry specification].
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Primarily useful for looking up icons and metadata from an id, as there is
 | 
				
			||||||
 | 
					/// currently no mechanism for usage based sorting of entries and other launcher niceties.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// [desktop entry specification]: https://specifications.freedesktop.org/desktop-entry-spec/latest/
 | 
				
			||||||
 | 
					class DesktopEntries: public QObject {
 | 
				
			||||||
 | 
						Q_OBJECT;
 | 
				
			||||||
 | 
						/// All desktop entries of type Application that are not Hidden or NoDisplay.
 | 
				
			||||||
 | 
						Q_PROPERTY(ObjectModel<DesktopEntry>* applications READ applications CONSTANT);
 | 
				
			||||||
 | 
						QML_ELEMENT;
 | 
				
			||||||
 | 
						QML_SINGLETON;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public:
 | 
				
			||||||
 | 
						explicit DesktopEntries();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// Look up a desktop entry by name. Includes NoDisplay entries. May return null.
 | 
				
			||||||
 | 
						Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[[nodiscard]] static ObjectModel<DesktopEntry>* applications();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -20,5 +20,6 @@ headers = [
 | 
				
			||||||
	"boundcomponent.hpp",
 | 
						"boundcomponent.hpp",
 | 
				
			||||||
	"model.hpp",
 | 
						"model.hpp",
 | 
				
			||||||
	"elapsedtimer.hpp",
 | 
						"elapsedtimer.hpp",
 | 
				
			||||||
 | 
						"desktopentry.hpp",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
-----
 | 
					-----
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue