#include "DesktopEntry.hpp" #include #include #include #include #include #include #include #include #include namespace { QString unquote(QString value) { value = value.trimmed(); if (value.size() >= 2) { const QChar first = value.front(); const QChar last = value.back(); if ((first == QLatin1Char('"') && last == QLatin1Char('"')) || (first == QLatin1Char('\'') && last == QLatin1Char('\''))) { value = value.mid(1, value.size() - 2); } } return value; } bool parseBool(const QString &value) { const QString lowered = value.trimmed().toLower(); return lowered == QLatin1String("true") || lowered == QLatin1String("1"); } struct LocalizedValue { QString fallback; QMap locales; }; void storeLocalized(LocalizedValue *target, const QString &key, const QString &prefix, const QString &value) { if (key == prefix) { target->fallback = value; return; } const QString start = prefix + QLatin1Char('['); if (!key.startsWith(start) || !key.endsWith(QLatin1Char(']'))) return; const QString locale = key.mid(start.size(), key.size() - start.size() - 1); if (!locale.isEmpty()) target->locales.insert(locale, value); } QString resolveLocalized(const LocalizedValue &value, const QLocale &locale) { const QString name = locale.name(); // e.g. en_US const QString language = name.section(QLatin1Char('_'), 0, 0); auto match = [&value](const QString &tag) -> QString { if (tag.isEmpty()) return {}; return value.locales.value(tag); }; if (QString found = match(name); !found.isEmpty()) return found; if (QString found = match(language); !found.isEmpty()) return found; const int underscore = name.indexOf(QLatin1Char('_')); if (underscore > 0) { if (QString found = match(name.left(underscore)); !found.isEmpty()) return found; } return value.fallback; } QString findTerminalEmulator() { const QStringList candidates = { QStringLiteral("foot"), QStringLiteral("x-terminal-emulator"), QStringLiteral("kitty"), QStringLiteral("alacritty"), QStringLiteral("kgx"), QStringLiteral("gnome-terminal"), QStringLiteral("konsole"), QStringLiteral("xterm"), }; for (const QString &name : candidates) { const QString path = QStandardPaths::findExecutable(name); if (!path.isEmpty()) return path; } return {}; } bool executableAvailable(const QString &command) { if (command.isEmpty()) return false; const QFileInfo info(command); if (info.isAbsolute()) return info.isExecutable(); return !QStandardPaths::findExecutable(command).isEmpty(); } } // namespace QStringList splitDesktopList(const QString &value) { const QStringList raw = value.split(QLatin1Char(';'), Qt::SkipEmptyParts); QStringList cleaned; cleaned.reserve(raw.size()); for (const QString &item : raw) { const QString trimmed = item.trimmed(); if (!trimmed.isEmpty()) cleaned.append(trimmed); } return cleaned; } QStringList currentDesktopIdentifiers() { const QByteArray raw = qgetenv("XDG_CURRENT_DESKTOP"); if (raw.isEmpty()) return {QStringLiteral("NebulaOS")}; QStringList ids; for (const QByteArray &part : raw.split(':')) { const QString id = QString::fromLocal8Bit(part).trimmed(); if (!id.isEmpty()) ids.append(id); } if (ids.isEmpty()) ids.append(QStringLiteral("NebulaOS")); return ids; } QStringList applicationDirectories() { QStringList dirs; QString dataHome = QString::fromLocal8Bit(qgetenv("XDG_DATA_HOME")); if (dataHome.isEmpty()) dataHome = QDir::homePath() + QStringLiteral("/.local/share"); dirs.append(QDir::cleanPath(dataHome + QStringLiteral("/applications"))); QString dataDirs = QString::fromLocal8Bit(qgetenv("XDG_DATA_DIRS")); if (dataDirs.isEmpty()) dataDirs = QStringLiteral("/usr/local/share:/usr/share"); for (const QString &root : dataDirs.split(QLatin1Char(':'), Qt::SkipEmptyParts)) { const QString trimmed = root.trimmed(); if (trimmed.isEmpty()) continue; dirs.append(QDir::cleanPath(trimmed + QStringLiteral("/applications"))); } dirs.removeDuplicates(); return dirs; } QString resolveIconName(const QString &icon) { if (icon.isEmpty()) return {}; const QFileInfo info(icon); if (info.isAbsolute()) return icon; QString name = icon; if (name.endsWith(QLatin1String(".png"), Qt::CaseInsensitive) || name.endsWith(QLatin1String(".svg"), Qt::CaseInsensitive) || name.endsWith(QLatin1String(".xpm"), Qt::CaseInsensitive)) { name = QFileInfo(name).completeBaseName(); } return name; } QStringList expandExec(const QString &exec, const DesktopEntry &entry) { QString expanded; expanded.reserve(exec.size()); for (int i = 0; i < exec.size(); ++i) { const QChar ch = exec.at(i); if (ch != QLatin1Char('%') || i + 1 >= exec.size()) { expanded.append(ch); continue; } const QChar code = exec.at(++i); auto trimTrailingSpace = [&expanded]() { if (expanded.endsWith(QLatin1Char(' '))) expanded.chop(1); }; switch (code.toLatin1()) { case '%': expanded.append(QLatin1Char('%')); break; case 'f': case 'F': case 'u': case 'U': case 'd': case 'D': case 'n': case 'N': case 'v': case 'm': // File/URL/deprecated codes are omitted when launching without arguments. trimTrailingSpace(); break; case 'c': expanded.append(QLatin1Char('"')); expanded.append(entry.name); expanded.append(QLatin1Char('"')); break; case 'k': expanded.append(QLatin1Char('"')); expanded.append(entry.filePath); expanded.append(QLatin1Char('"')); break; case 'i': if (!entry.icon.isEmpty()) { expanded.append(QStringLiteral("--icon \"")); expanded.append(entry.icon); expanded.append(QLatin1Char('"')); } else { trimTrailingSpace(); } break; default: trimTrailingSpace(); break; } } const QStringList parts = QProcess::splitCommand(expanded.trimmed()); return parts; } bool DesktopEntry::isLauncherVisible() const { if (hidden || noDisplay) return false; if (exec.trimmed().isEmpty()) return false; if (!tryExec.trimmed().isEmpty() && !executableAvailable(tryExec.trimmed())) return false; return true; } bool DesktopEntry::launch() const { QStringList args = expandExec(exec, *this); if (args.isEmpty()) { qWarning("nebula: cannot launch %s: empty Exec", qPrintable(desktopId)); return false; } QString program = args.takeFirst(); if (program.isEmpty()) { qWarning("nebula: cannot launch %s: missing program", qPrintable(desktopId)); return false; } if (terminal) { const QString term = findTerminalEmulator(); if (term.isEmpty()) { qWarning("nebula: cannot launch %s: Terminal=true but no terminal emulator was found", qPrintable(desktopId)); return false; } QStringList wrapped; wrapped.append(QStringLiteral("-e")); wrapped.append(program); wrapped.append(args); args = std::move(wrapped); program = term; } const QString cwd = workingDirectory.isEmpty() ? QDir::homePath() : workingDirectory; qint64 pid = 0; if (!QProcess::startDetached(program, args, cwd, &pid)) { qWarning("nebula: failed to launch %s (%s)", qPrintable(desktopId), qPrintable(program)); return false; } qInfo("nebula: launched %s (%s) pid %lld", qPrintable(desktopId), qPrintable(program), static_cast(pid)); return true; } DesktopEntry parseDesktopEntryFile(const QString &filePath, const QString &desktopId) { DesktopEntry entry; entry.filePath = filePath; entry.desktopId = desktopId; QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return entry; LocalizedValue names; LocalizedValue genericNames; LocalizedValue comments; QString type = QStringLiteral("Application"); QStringList onlyShowIn; QStringList notShowIn; bool inDesktopEntry = false; while (!file.atEnd()) { QString line = QString::fromUtf8(file.readLine()); if (!line.isEmpty() && line.back() == QLatin1Char('\n')) line.chop(1); if (!line.isEmpty() && line.back() == QLatin1Char('\r')) line.chop(1); const QString trimmed = line.trimmed(); if (trimmed.isEmpty() || trimmed.startsWith(QLatin1Char('#'))) continue; if (trimmed.startsWith(QLatin1Char('[')) && trimmed.endsWith(QLatin1Char(']'))) { inDesktopEntry = (trimmed == QLatin1String("[Desktop Entry]")); continue; } if (!inDesktopEntry) continue; const int equals = trimmed.indexOf(QLatin1Char('=')); if (equals <= 0) continue; const QString key = trimmed.left(equals).trimmed(); const QString value = unquote(trimmed.mid(equals + 1)); if (key == QLatin1String("Type")) type = value; else if (key == QLatin1String("Exec")) entry.exec = value; else if (key == QLatin1String("TryExec")) entry.tryExec = value; else if (key == QLatin1String("Icon")) entry.icon = value; else if (key == QLatin1String("Path")) entry.workingDirectory = value; else if (key == QLatin1String("Categories")) entry.categories = splitDesktopList(value); else if (key == QLatin1String("Terminal")) entry.terminal = parseBool(value); else if (key == QLatin1String("Hidden")) entry.hidden = parseBool(value); else if (key == QLatin1String("NoDisplay")) entry.noDisplay = parseBool(value); else if (key == QLatin1String("OnlyShowIn")) onlyShowIn = splitDesktopList(value); else if (key == QLatin1String("NotShowIn")) notShowIn = splitDesktopList(value); else { storeLocalized(&names, key, QStringLiteral("Name"), value); storeLocalized(&genericNames, key, QStringLiteral("GenericName"), value); storeLocalized(&comments, key, QStringLiteral("Comment"), value); } } const QLocale locale = QLocale::system(); entry.name = resolveLocalized(names, locale); entry.genericName = resolveLocalized(genericNames, locale); entry.comment = resolveLocalized(comments, locale); entry.icon = resolveIconName(entry.icon); if (entry.desktopId.isEmpty()) entry.desktopId = QFileInfo(filePath).fileName(); if (type != QLatin1String("Application")) { entry.hidden = true; return entry; } const QStringList desktops = currentDesktopIdentifiers(); if (!onlyShowIn.isEmpty()) { bool matched = false; for (const QString &desktop : desktops) { if (onlyShowIn.contains(desktop)) { matched = true; break; } } if (!matched) entry.noDisplay = true; } for (const QString &desktop : desktops) { if (notShowIn.contains(desktop)) { entry.noDisplay = true; break; } } return entry; }