Skip to content

Commit dd426f2

Browse files
committed
core/desktopentry: off-thread desktop-entry parsing and general cleanups
1 parent 9afd2f0 commit dd426f2

File tree

4 files changed

+199
-151
lines changed

4 files changed

+199
-151
lines changed

src/core/desktopentry.cpp

Lines changed: 160 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -85,64 +85,71 @@ struct Locale {
8585
QDebug operator<<(QDebug debug, const Locale& locale) {
8686
auto saver = QDebugStateSaver(debug);
8787
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
88-
<< ", modifier" << locale.modifier << ')';
88+
<< ", modifier=" << locale.modifier << ')';
8989

9090
return debug;
9191
}
9292

93-
void DesktopEntry::parseEntry(const QString& text) {
93+
ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& text) {
94+
ParsedDesktopEntryData data;
95+
data.id = id;
9496
const auto& system = Locale::system();
9597

9698
auto groupName = QString();
9799
auto entries = QHash<QString, QPair<Locale, QString>>();
98100

99-
auto finishCategory = [this, &groupName, &entries]() {
101+
auto finishCategory = [&data, &groupName, &entries]() {
100102
if (groupName == "Desktop Entry") {
101-
if (entries["Type"].second != "Application") return;
102-
if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
103+
const auto typeIt = entries.constFind("Type");
104+
if (typeIt == entries.cend() || typeIt->second != "Application") return;
105+
106+
if (const auto hiddenIt = entries.constFind("Hidden");
107+
hiddenIt != entries.cend() && hiddenIt->second == "true")
108+
return;
103109

104110
for (const auto& [key, pair]: entries.asKeyValueRange()) {
105111
auto& [_, value] = pair;
106-
this->mEntries.insert(key, value);
107-
108-
if (key == "Name") this->mName = value;
109-
else if (key == "GenericName") this->mGenericName = value;
110-
else if (key == "StartupWMClass") this->mStartupClass = value;
111-
else if (key == "NoDisplay") this->mNoDisplay = value == "true";
112-
else if (key == "Comment") this->mComment = value;
113-
else if (key == "Icon") this->mIcon = value;
112+
data.entries.insert(key, value);
113+
114+
if (key == "Name") data.name = value;
115+
else if (key == "GenericName") data.genericName = value;
116+
else if (key == "StartupWMClass") data.startupClass = value;
117+
else if (key == "NoDisplay") data.noDisplay = value == "true";
118+
else if (key == "Comment") data.comment = value;
119+
else if (key == "Icon") data.icon = value;
114120
else if (key == "Exec") {
115-
this->mExecString = value;
116-
this->mCommand = DesktopEntry::parseExecString(value);
117-
} else if (key == "Path") this->mWorkingDirectory = value;
118-
else if (key == "Terminal") this->mTerminal = value == "true";
119-
else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts);
120-
else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts);
121+
data.execString = value;
122+
data.command = DesktopEntry::parseExecString(value);
123+
} else if (key == "Path") data.workingDirectory = value;
124+
else if (key == "Terminal") data.terminal = value == "true";
125+
else if (key == "Categories") data.categories = value.split(u';', Qt::SkipEmptyParts);
126+
else if (key == "Keywords") data.keywords = value.split(u';', Qt::SkipEmptyParts);
121127
}
122128
} else if (groupName.startsWith("Desktop Action ")) {
123129
auto actionName = groupName.sliced(16);
124-
auto* action = new DesktopAction(actionName, this);
130+
auto action = DesktopActionData();
131+
action.id = actionName;
125132

126133
for (const auto& [key, pair]: entries.asKeyValueRange()) {
127134
const auto& [_, value] = pair;
128-
action->mEntries.insert(key, value);
135+
action.entries.insert(key, value);
129136

130-
if (key == "Name") action->mName = value;
131-
else if (key == "Icon") action->mIcon = value;
137+
if (key == "Name") action.name = value;
138+
else if (key == "Icon") action.icon = value;
132139
else if (key == "Exec") {
133-
action->mExecString = value;
134-
action->mCommand = DesktopEntry::parseExecString(value);
140+
action.execString = value;
141+
action.command = DesktopEntry::parseExecString(value);
135142
}
136143
}
137144

138-
this->mActions.insert(actionName, action);
145+
data.actions.insert(actionName, action);
139146
}
140147

141148
entries.clear();
142149
};
143150

144-
for (auto& line: text.split(u'\n', Qt::SkipEmptyParts)) {
145-
if (line.startsWith(u'#')) continue;
151+
for (auto& line: text.split(u'\n')) {
152+
if (line.isEmpty() || line.startsWith(u'#')) continue;
146153

147154
if (line.startsWith(u'[') && line.endsWith(u']')) {
148155
finishCategory();
@@ -160,7 +167,7 @@ void DesktopEntry::parseEntry(const QString& text) {
160167
const auto& value = line.sliced(splitIdx + 1);
161168

162169
auto localeIdx = key.indexOf('[');
163-
Locale locale;
170+
auto locale = Locale();
164171
if (localeIdx != -1 && localeIdx != key.length() - 1) {
165172
locale = Locale(key.sliced(localeIdx + 1, key.length() - localeIdx - 2));
166173
key = key.sliced(0, localeIdx);
@@ -181,6 +188,38 @@ void DesktopEntry::parseEntry(const QString& text) {
181188
}
182189

183190
finishCategory();
191+
return data;
192+
}
193+
194+
void DesktopEntry::applyParsedData(const ParsedDesktopEntryData& data) {
195+
this->mEntries = data.entries;
196+
this->mName = data.name;
197+
this->mGenericName = data.genericName;
198+
this->mStartupClass = data.startupClass;
199+
this->mNoDisplay = data.noDisplay;
200+
this->mComment = data.comment;
201+
this->mIcon = data.icon;
202+
this->mExecString = data.execString;
203+
this->mCommand = data.command;
204+
this->mWorkingDirectory = data.workingDirectory;
205+
this->mTerminal = data.terminal;
206+
this->mCategories = data.categories;
207+
this->mKeywords = data.keywords;
208+
209+
for (auto* action: this->mActions.values()) {
210+
action->deleteLater();
211+
}
212+
this->mActions.clear();
213+
214+
for (const auto& [actionName, actionData]: data.actions.asKeyValueRange()) {
215+
auto* action = new DesktopAction(actionData.id, this);
216+
action->mEntries = actionData.entries;
217+
action->mName = actionData.name;
218+
action->mIcon = actionData.icon;
219+
action->mExecString = actionData.execString;
220+
action->mCommand = actionData.command;
221+
this->mActions.insert(actionName, action);
222+
}
184223
}
185224

186225
void DesktopEntry::execute() const {
@@ -193,8 +232,8 @@ bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
193232
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
194233

195234
QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
196-
QVector<QString> arguments;
197-
QString currentArgument;
235+
auto arguments = QVector<QString>();
236+
auto currentArgument = QString();
198237
auto parsingString = false;
199238
auto escape = 0;
200239
auto percent = false;
@@ -257,7 +296,7 @@ QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
257296
}
258297

259298
void DesktopEntry::doExec(const QList<QString>& execString, const QString& workingDirectory) {
260-
qs::io::process::ProcessContext ctx;
299+
auto ctx = qs::io::process::ProcessContext();
261300
ctx.setCommand(execString);
262301
ctx.setWorkingDirectory(workingDirectory);
263302
QuickshellGlobal::execDetached(ctx);
@@ -272,10 +311,11 @@ DesktopEntryScanner::DesktopEntryScanner(DesktopEntryManager* manager): manager(
272311
}
273312

274313
void DesktopEntryScanner::run() {
275-
auto desktopPaths = manager->monitor->getDesktopDirectories();
314+
auto desktopPaths = DesktopEntryManager::desktopPaths();
276315
auto scanResults = DesktopEntryScanResults();
277316

278-
for (auto& path: std::ranges::reverse_view(desktopPaths)) {
317+
for (int i = desktopPaths.size() - 1; i >= 0; --i) {
318+
const auto& path = desktopPaths.at(i);
279319
auto file = QFileInfo(path);
280320
if (!file.isDir()) continue;
281321

@@ -284,9 +324,9 @@ void DesktopEntryScanner::run() {
284324

285325
QMetaObject::invokeMethod(
286326
this->manager,
287-
"onScanCompleted",
327+
&DesktopEntryManager::onScanCompleted,
288328
Qt::QueuedConnection,
289-
Q_ARG(DesktopEntryScanResults, scanResults)
329+
scanResults
290330
);
291331
}
292332

@@ -309,19 +349,19 @@ void DesktopEntryScanner::scanDirectory(const QDir& dir, DesktopEntryScanResults
309349
continue;
310350
}
311351

312-
auto id = manager->extractIdFromPath(entry.absoluteFilePath());
352+
auto id = DesktopEntryManager::extractIdFromPath(entry.absoluteFilePath());
313353
auto content = QString::fromUtf8(file.readAll());
314354

315-
ParsedDesktopEntry parsed;
316-
parsed.id = id;
317-
parsed.content = std::move(content);
318-
319-
entries.append(std::move(parsed));
355+
auto data = DesktopEntry::parseText(id, content);
356+
entries.append(std::move(data));
320357
}
321358
}
322359
}
323360

324361
DesktopEntryManager::DesktopEntryManager() {
362+
qRegisterMetaType<ParsedDesktopEntryData>("ParsedDesktopEntryData");
363+
qRegisterMetaType<DesktopEntryScanResults>("DesktopEntryScanResults");
364+
325365
this->monitor = new DesktopEntryMonitor(this);
326366
connect(
327367
this->monitor,
@@ -334,26 +374,16 @@ DesktopEntryManager::DesktopEntryManager() {
334374
}
335375

336376
void DesktopEntryManager::scanDesktopEntries() {
337-
auto desktopPaths = this->monitor->getDesktopDirectories();
338-
auto scanResults = DesktopEntryScanResults();
339-
340-
qCDebug(logDesktopEntry) << "Creating desktop entry scanners";
341-
342-
auto scanner = DesktopEntryScanner(this);
343-
for (auto& path: std::ranges::reverse_view(desktopPaths)) {
344-
auto file = QFileInfo(path);
377+
qCDebug(logDesktopEntry) << "Starting desktop entry scan";
345378

346-
if (!file.isDir()) {
347-
qCDebug(logDesktopEntry) << "Not scanning path" << path << "as it is not a directory";
348-
continue;
349-
}
350-
351-
qCDebug(logDesktopEntry) << "Scanning path" << path;
352-
scanner.scanDirectory(QDir(path), scanResults);
379+
if (this->scanInProgress) {
380+
qCDebug(logDesktopEntry) << "Scan already in progress, skipping";
381+
return;
353382
}
354383

355-
this->onScanCompleted(scanResults);
356-
this->scanInProgress = false;
384+
this->scanInProgress = true;
385+
auto* scanner = new DesktopEntryScanner(this);
386+
QThreadPool::globalInstance()->start(scanner);
357387
}
358388

359389
DesktopEntryManager* DesktopEntryManager::instance() {
@@ -406,21 +436,57 @@ void DesktopEntryManager::handleFileChanges() {
406436
}
407437

408438
QString DesktopEntryManager::extractIdFromPath(const QString& path) {
409-
auto info = QFileInfo(path);
410-
auto id = info.completeBaseName();
439+
const auto fi = QFileInfo(path);
440+
const auto canon = fi.canonicalFilePath();
441+
const auto actual = canon.isEmpty() ? fi.absoluteFilePath() : canon;
442+
for (const auto& rootPath: DesktopEntryManager::desktopPaths()) {
443+
auto root = QDir(rootPath);
444+
const auto rel = root.relativeFilePath(actual);
445+
if (!rel.startsWith("../")) {
446+
auto relInfo = QFileInfo(rel);
447+
auto id = QString();
448+
if (relInfo.path() != "." && !relInfo.path().isEmpty()) {
449+
id = relInfo.path();
450+
id.replace('/', '-');
451+
id += '-' + relInfo.completeBaseName();
452+
} else {
453+
id = relInfo.completeBaseName();
454+
}
455+
return id;
456+
}
457+
}
458+
return fi.completeBaseName();
459+
}
411460

412-
auto appIdx = path.lastIndexOf("/applications/");
413-
if (appIdx != -1) {
414-
auto relativePath = path.mid(appIdx + 14);
415-
auto relInfo = QFileInfo(relativePath);
461+
const QStringList& DesktopEntryManager::desktopPaths() {
462+
static const auto paths = []() {
463+
auto dataPaths = QStringList();
416464

417-
auto dirPath = relInfo.path();
418-
if (dirPath != ".") {
419-
id = dirPath.replace('/', '-') + '-' + relInfo.completeBaseName();
465+
auto dataHome = qEnvironmentVariable("XDG_DATA_HOME");
466+
if (dataHome.isEmpty()) {
467+
if (qEnvironmentVariableIsSet("HOME")) {
468+
dataHome = qEnvironmentVariable("HOME") + "/.local/share";
469+
}
470+
}
471+
if (!dataHome.isEmpty()) {
472+
dataPaths.append(dataHome + "/applications");
473+
}
474+
475+
auto dataDirs = qEnvironmentVariable("XDG_DATA_DIRS");
476+
if (dataDirs.isEmpty()) {
477+
dataDirs = "/usr/local/share:/usr/share";
420478
}
421-
}
422479

423-
return id;
480+
for (const auto& dir: dataDirs.split(':', Qt::SkipEmptyParts)) {
481+
if (!dir.isEmpty()) {
482+
dataPaths.append(dir + "/applications");
483+
}
484+
}
485+
486+
return dataPaths;
487+
}();
488+
489+
return paths;
424490
}
425491

426492
void DesktopEntryManager::onScanCompleted(const DesktopEntryScanResults& scanResults) {
@@ -430,30 +496,41 @@ void DesktopEntryManager::onScanCompleted(const DesktopEntryScanResults& scanRes
430496
auto newEntries = QHash<QString, DesktopEntry*>();
431497
auto newLowercaseEntries = QHash<QString, DesktopEntry*>();
432498

433-
for (const auto& parsed: scanResults) {
434-
auto* dentry = new DesktopEntry(parsed.id, this);
435-
dentry->parseEntry(parsed.content);
499+
for (const auto& data: scanResults) {
500+
DesktopEntry* dentry = nullptr;
501+
502+
if (auto it = oldEntries.find(data.id); it != oldEntries.end()) {
503+
dentry = it.value();
504+
oldEntries.erase(it);
505+
dentry->applyParsedData(data);
506+
} else {
507+
dentry = new DesktopEntry(data.id, this);
508+
dentry->applyParsedData(data);
509+
}
436510

437511
if (!dentry->isValid()) {
438-
qCDebug(logDesktopEntry) << "Skipping desktop entry" << parsed.id;
439-
delete dentry;
512+
qCDebug(logDesktopEntry) << "Skipping desktop entry" << data.id;
513+
if (!oldEntries.contains(data.id)) {
514+
dentry->deleteLater();
515+
}
440516
continue;
441517
}
442518

443-
qCDebug(logDesktopEntry) << "Found desktop entry" << parsed.id;
519+
qCDebug(logDesktopEntry) << "Found desktop entry" << data.id;
444520

445-
auto lowerId = parsed.id.toLower();
521+
auto lowerId = data.id.toLower();
446522

447-
auto conflictingId = newEntries.contains(parsed.id);
523+
auto conflictingId = newEntries.contains(data.id);
448524

449525
if (conflictingId) {
450-
qCDebug(logDesktopEntry) << "Replacing old entry for" << parsed.id;
451-
delete newEntries.value(parsed.id);
452-
newEntries.remove(parsed.id);
526+
qCDebug(logDesktopEntry) << "Replacing old entry for" << data.id;
527+
if (auto* victim = newEntries.take(data.id)) {
528+
victim->deleteLater();
529+
}
453530
newLowercaseEntries.remove(lowerId);
454531
}
455532

456-
newEntries.insert(parsed.id, dentry);
533+
newEntries.insert(data.id, dentry);
457534

458535
if (newLowercaseEntries.contains(lowerId)) {
459536
qCInfo(logDesktopEntry).nospace()
@@ -480,11 +557,8 @@ void DesktopEntryManager::onScanCompleted(const DesktopEntryScanResults& scanRes
480557
this->mApplications.diffUpdate(newApplications);
481558

482559
for (auto* e: oldEntries) {
483-
if (!this->desktopEntries.contains(e->mId)) {
484-
e->deleteLater();
485-
}
560+
e->deleteLater();
486561
}
487-
oldEntries.clear();
488562

489563
emit applicationsChanged();
490564
}

0 commit comments

Comments
 (0)