diff --git a/apps/dde-autostart/src/main.cpp b/apps/dde-autostart/src/main.cpp index 19b9d407..a42db86e 100644 --- a/apps/dde-autostart/src/main.cpp +++ b/apps/dde-autostart/src/main.cpp @@ -77,7 +77,7 @@ void scanAndLaunch() continue; } - if (ApplicationFilter::tryExecCheck(tmp) || ApplicationFilter::showInCheck(tmp) + if (ApplicationFilter::tryExecCheck(tmp, desktopFile.desktopId()) || ApplicationFilter::showInCheck(tmp) || ApplicationFilter::hiddenCheck(tmp)) { qInfo() << "autostart application " << id << " couldn't pass check:" << desktopFile.sourcePath(); continue; diff --git a/misc/CMakeLists.txt b/misc/CMakeLists.txt index dbebc23b..d41b5881 100644 --- a/misc/CMakeLists.txt +++ b/misc/CMakeLists.txt @@ -93,4 +93,5 @@ dtk_add_config_meta_files(APPID ${APPLICATION_SERVICEID} FILES ${CMAKE_CURRENT_LIST_DIR}/dsg/configs/dde-application-manager/org.deepin.dde.am.json ${CMAKE_CURRENT_LIST_DIR}/dsg/configs/dde-application-manager/org.deepin.dde.application-manager.json + ${CMAKE_CURRENT_LIST_DIR}/dsg/configs/dde-application-manager/org.deepin.dde.am.appoverride.json ) diff --git a/misc/dsg/configs/dde-application-manager/org.deepin.dde.am.appoverride.json b/misc/dsg/configs/dde-application-manager/org.deepin.dde.am.appoverride.json new file mode 100644 index 00000000..36fbaf85 --- /dev/null +++ b/misc/dsg/configs/dde-application-manager/org.deepin.dde.am.appoverride.json @@ -0,0 +1,36 @@ +{ + "magic": "dsg.config.meta", + "version": "1.0", + "contents": { + "Exec": { + "value": "", + "serial": 0, + "flags": [], + "name": "Session-specific Exec override", + "name[zh_CN]": "会话感知的 Exec 覆盖", + "description": "Overrides the Exec field. Supports !AM_FULL! placeholder.", + "permissions": "readonly", + "visibility": "public" + }, + "TryExec": { + "value": "", + "serial": 0, + "flags": [], + "name": "Session-specific TryExec override", + "name[zh_CN]": "会话感知的 TryExec 覆盖", + "description": "Overrides the TryExec field. Empty string forces the app to be shown.", + "permissions": "readonly", + "visibility": "public" + }, + "Icon": { + "value": "", + "serial": 0, + "flags": [], + "name": "Session-specific Icon override", + "name[zh_CN]": "会话感知的 Icon 覆盖", + "description": "Overrides the Icon field.", + "permissions": "readwrite", + "visibility": "public" + } + } +} \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d8dde9f0..e2592f10 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,3 +1,14 @@ +find_package(Dtk6 REQUIRED COMPONENTS Core Tools) + +dtk_add_config_to_cpp( + AM_APP_OVERRIDE_CONFIG_SRC + "${CMAKE_SOURCE_DIR}/misc/dsg/configs/dde-application-manager/org.deepin.dde.am.appoverride.json" + OUTPUT_FILE_NAME "am_appoverride_config.hpp" + CLASS_NAME "ApplicationOverrideConfig" +) + +add_custom_target(am_appoverride_config_generate DEPENDS ${AM_APP_OVERRIDE_CONFIG_SRC}) + add_subdirectory(dbus) include(GNUInstallDirs) @@ -9,7 +20,8 @@ set(LIB_NAME dde_am_static) file(GLOB SRCS ${CMAKE_CURRENT_LIST_DIR}/*.cpp ${CMAKE_CURRENT_LIST_DIR}/*.h) list(REMOVE_ITEM SRCS ${UTILS_SRCS}) -add_library(${LIB_NAME} STATIC ${SRCS}) +add_library(${LIB_NAME} STATIC ${SRCS} ${AM_APP_OVERRIDE_CONFIG_SRC}) +add_dependencies(${LIB_NAME} am_appoverride_config_generate) target_include_directories(${LIB_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} diff --git a/src/applicationchecker.cpp b/src/applicationchecker.cpp index faafba5b..d1bc5214 100644 --- a/src/applicationchecker.cpp +++ b/src/applicationchecker.cpp @@ -5,6 +5,7 @@ #include "global.h" #include "constant.h" #include "applicationchecker.h" +#include "sessionoverrideconfig.h" #include #include #include @@ -69,8 +70,26 @@ bool ApplicationFilter::hiddenCheck(const DesktopEntry &entry) noexcept return hidden; } -bool ApplicationFilter::tryExecCheck(const DesktopEntry &entry) noexcept +bool ApplicationFilter::tryExecCheck(const DesktopEntry &entry, QStringView desktopId, const SessionOverrideConfig *sessionConfig) noexcept { + // DConfig session-level TryExec override takes highest priority + if (sessionConfig) { + auto overrideTryExec = sessionConfig->getValue(desktopId.toString(), fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryTryExec)); + if (overrideTryExec) { + auto executable = *overrideTryExec; + if (executable.isEmpty()) { + qCInfo(DDEAMChecker) << "session override TryExec is empty for" << desktopId << ", treated as shown."; + return false; // empty means force-show + } + + if (executable.startsWith(QDir::separator())) { + const QFileInfo info{executable}; + return !(info.exists() && info.isExecutable()); + } + return QStandardPaths::findExecutable(executable).isEmpty(); + } + } + auto tryExecVal = entry.value(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryTryExec)); if (tryExecVal.has_value()) { auto executable = toString(tryExecVal.value()); diff --git a/src/applicationchecker.h b/src/applicationchecker.h index 5d8a4c21..6d608618 100644 --- a/src/applicationchecker.h +++ b/src/applicationchecker.h @@ -5,14 +5,18 @@ #ifndef APPLICATIONCHECKER_H #define APPLICATIONCHECKER_H +#include + #include "desktopentry.h" Q_DECLARE_LOGGING_CATEGORY(DDEAMChecker) +class SessionOverrideConfig; + namespace ApplicationFilter { bool hiddenCheck(const DesktopEntry &entry) noexcept; -bool tryExecCheck(const DesktopEntry &entry) noexcept; +bool tryExecCheck(const DesktopEntry &entry, QStringView desktopId, const SessionOverrideConfig *sessionConfig = nullptr) noexcept; bool showInCheck(const DesktopEntry &entry) noexcept; } // namespace ApplicationFilter diff --git a/src/constant.h b/src/constant.h index 1a36284d..d7e70d31 100644 --- a/src/constant.h +++ b/src/constant.h @@ -63,6 +63,7 @@ constexpr static auto &ApplicationManagerHookDir = u"/deepin/dde-application-man constexpr static auto &ApplicationManagerToolsConfig = u"org.deepin.dde.am"; constexpr static auto &ApplicationManagerConfig = u"org.deepin.dde.application-manager"; +constexpr static auto &ApplicationOverrideConfigResource = u"org.deepin.dde.am.appoverride"; constexpr static auto &AppExtraEnvironments = u"appExtraEnvironments"; constexpr static auto &AppEnvironmentsBlacklist = u"appEnvironmentsBlacklist"; constexpr static auto &SkipEventAppIds = u"skipEventAppIds"; diff --git a/src/dbus/CMakeLists.txt b/src/dbus/CMakeLists.txt index 627453c1..dce45f70 100644 --- a/src/dbus/CMakeLists.txt +++ b/src/dbus/CMakeLists.txt @@ -28,6 +28,8 @@ target_sources(dde_am_dbus PRIVATE ${dde_am_dbus_SOURCE} ) +add_dependencies(dde_am_dbus am_appoverride_config_generate) + target_link_libraries( dde_am_dbus PUBLIC Qt6::Core diff --git a/src/dbus/applicationmanager1service.cpp b/src/dbus/applicationmanager1service.cpp index 9dc5c497..40df6d69 100644 --- a/src/dbus/applicationmanager1service.cpp +++ b/src/dbus/applicationmanager1service.cpp @@ -137,7 +137,7 @@ void forEachAutostartDesktopFile(T &&func) noexcept } } -[[nodiscard]] std::optional parseAutostartDesktopFile(DesktopFile desktopFile) noexcept +[[nodiscard]] std::optional parseAutostartDesktopFile(DesktopFile desktopFile, const SessionOverrideConfig *sessionConfig = nullptr) noexcept { DesktopFileGuard guard{desktopFile}; if (!guard.try_open()) { @@ -152,7 +152,7 @@ void forEachAutostartDesktopFile(T &&func) noexcept return std::nullopt; } - if (ApplicationFilter::tryExecCheck(entry) || ApplicationFilter::showInCheck(entry) || ApplicationFilter::hiddenCheck(entry)) { + if (ApplicationFilter::tryExecCheck(entry, desktopFile.desktopId(), sessionConfig) || ApplicationFilter::showInCheck(entry) || ApplicationFilter::hiddenCheck(entry)) { qInfo() << "autostart application" << desktopFile.desktopId() << "couldn't pass check."; return std::nullopt; } @@ -228,6 +228,25 @@ void ApplicationManager1Service::initService(QDBusConnection &connection) noexce qWarning() << "new CompatibilityManager failed."; } + m_sessionOverrideConfig = std::make_unique(this); + connect(m_sessionOverrideConfig.get(), &SessionOverrideConfig::configChanged, this, [this]() { + qCInfo(DDEAM) << "Session override config changed, rebuilding application list."; + doReloadApplications(); + }); + connect(m_sessionOverrideConfig.get(), &SessionOverrideConfig::overrideChanged, + this, [this](const QString &desktopId, const QString &key) { + auto app = m_applicationList.value(desktopId); + if (!app) + return; + if (key == u"Icon"_s) { + qCInfo(DDEAM) << "Icon override changed for" << desktopId << ", emitting iconsChanged."; + emit app->iconsChanged(); + } else if (key == u"Exec"_s || key == u"TryExec"_s) { + qCInfo(DDEAM) << "Exec/TryExec override changed for" << desktopId << ", emitting execsChanged."; + emit app->execsChanged(); + } + }); + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ApplicationManager1Service::ReloadApplications); // Ensure all directories exist before adding watches @@ -482,7 +501,7 @@ void ApplicationManager1Service::scanInstances() noexcept void ApplicationManager1Service::updateAutostartStatus() noexcept { forEachAutostartDesktopFile([this](DesktopFile desktopFile) -> bool { - auto parsedSource = parseAutostartDesktopFile(std::move(desktopFile)); + auto parsedSource = parseAutostartDesktopFile(std::move(desktopFile), m_sessionOverrideConfig.get()); if (!parsedSource) { return false; } @@ -784,6 +803,8 @@ void ApplicationManager1Service::doReloadApplications() updateAutostartStatus(); + m_sessionOverrideConfig->preload(m_applicationList.keys()); + reloadMimeInfos(); EventReporter::instance().initialize(); diff --git a/src/dbus/applicationmanager1service.h b/src/dbus/applicationmanager1service.h index 91d041c3..5d75fd9c 100644 --- a/src/dbus/applicationmanager1service.h +++ b/src/dbus/applicationmanager1service.h @@ -21,6 +21,7 @@ #include "identifier.h" #include "compatibilitymanager.h" #include "prelaunchsplashhelper.h" +#include "sessionoverrideconfig.h" Q_DECLARE_LOGGING_CATEGORY(DDEAM) @@ -90,6 +91,7 @@ class ApplicationManager1Service final : public QObject, protected QDBusContext [[nodiscard]] const MimeManager1Service &mimeManager() const noexcept { return *m_mimeManager; } [[nodiscard]] const QStringList &systemdPathEnv() const noexcept { return m_systemdPathEnv; } [[nodiscard]] QSharedPointer getCompatibilityManager() const noexcept { return m_compatibilityManager; } + [[nodiscard]] SessionOverrideConfig *getSessionOverrideConfig() const noexcept { return m_sessionOverrideConfig.get(); } [[nodiscard]] PrelaunchSplashHelper *splashHelper() const noexcept { return m_splashHelper.get(); } [[nodiscard]] bool isNewSession() const noexcept { return m_isNewSession; } [[nodiscard]] bool isStartupPhase() const noexcept { return m_startupPhase; } @@ -132,6 +134,7 @@ private Q_SLOTS: bool m_pendingReload{false}; QHash> m_applicationList; QSharedPointer m_compatibilityManager; + std::unique_ptr m_sessionOverrideConfig; std::unique_ptr m_splashHelper; void scanMimeInfos() noexcept; diff --git a/src/dbus/applicationservice.cpp b/src/dbus/applicationservice.cpp index 7a76fda0..e72b0588 100644 --- a/src/dbus/applicationservice.cpp +++ b/src/dbus/applicationservice.cpp @@ -156,6 +156,7 @@ void ApplicationService::appendExtraEnvironments(QVariantMap &runtimeOptions) co void ApplicationService::processCompatibility(const QString &action, QVariantMap &options, QString &execStr) { + const auto originalExec = execStr; auto compatibilityManager = parent()->getCompatibilityManager(); auto getExec = [action, compatibilityManager](const QString &desktopID) -> QString { @@ -193,6 +194,20 @@ void ApplicationService::processCompatibility(const QString &action, QVariantMap addEnv(); qInfo() << "get compatibility : " << m_desktopSource.desktopId() << " Exec : " << execStr; } + + // Session-specific DConfig overrides (higher priority than compatibility file) + auto sessionConfig = parent()->getSessionOverrideConfig(); + if (!sessionConfig) { + return; + } + + const auto &groupKey = fromStaticRaw(DesktopFileEntryKey); + + auto overrideExec = sessionConfig->getValue(m_desktopSource.desktopId(), groupKey, fromStaticRaw(DesktopEntryExec)); + if (overrideExec) { + execStr = SessionOverrideConfig::resolveExecValue(*overrideExec, originalExec); + qInfo() << "session override for" << m_desktopSource.desktopId() << "Exec:" << execStr; + } } ApplicationService::ApplicationService(DesktopFile source, @@ -322,7 +337,7 @@ QSharedPointer ApplicationService::createApplicationService( } } - if (!shouldBeShown(entry)) { + if (!shouldBeShown(entry, app->desktopFileSource().desktopId(), parent->getSessionOverrideConfig())) { qDebug() << "application shouldn't be shown:" << app->desktopFileSource().sourcePath(); return nullptr; } @@ -341,14 +356,14 @@ QSharedPointer ApplicationService::createApplicationService( return app; } -bool ApplicationService::shouldBeShown(const std::unique_ptr &entry) noexcept +bool ApplicationService::shouldBeShown(const std::unique_ptr &entry, QStringView desktopId, const SessionOverrideConfig *sessionConfig) noexcept { if (ApplicationFilter::hiddenCheck(*entry)) { qDebug() << "hidden check failed."; return false; } - if (ApplicationFilter::tryExecCheck(*entry)) { + if (ApplicationFilter::tryExecCheck(*entry, desktopId, sessionConfig)) { qDebug() << "tryExec check failed"; return false; } @@ -796,6 +811,16 @@ QStringMap ApplicationService::icons() const noexcept ret.insert(fromStaticRaw(DesktopFileEntryKey), mainIcon->get().value()); } + auto sessionConfig = parent()->getSessionOverrideConfig(); + if (sessionConfig) { + auto overrideIcon = sessionConfig->getValue(m_desktopSource.desktopId(), + fromStaticRaw(DesktopFileEntryKey), + fromStaticRaw(DesktopEntryIcon)); + if (overrideIcon && !overrideIcon->isEmpty()) { + ret.insert(fromStaticRaw(DesktopFileEntryKey), *overrideIcon); + } + } + return ret; } @@ -857,6 +882,16 @@ QStringMap ApplicationService::execs() const noexcept ret.insert(actionKey, value->get().value()); } + auto sessionConfig = parent()->getSessionOverrideConfig(); + if (sessionConfig) { + auto overrideExec = sessionConfig->getValue(m_desktopSource.desktopId(), + fromStaticRaw(DesktopFileEntryKey), + fromStaticRaw(DesktopEntryExec)); + if (overrideExec && !overrideExec->isEmpty()) { + ret.insert(fromStaticRaw(DesktopFileEntryKey), *overrideExec); + } + } + return ret; } diff --git a/src/dbus/applicationservice.h b/src/dbus/applicationservice.h index 96089d5e..438c3be7 100644 --- a/src/dbus/applicationservice.h +++ b/src/dbus/applicationservice.h @@ -224,7 +224,7 @@ public Q_SLOTS: bool m_propertiesForwarderInitialized{false}; QString m_eventAppId; void updateAfterLaunch(bool isLaunch) noexcept; - static bool shouldBeShown(const std::unique_ptr &entry) noexcept; + static bool shouldBeShown(const std::unique_ptr &entry, QStringView desktopId, const SessionOverrideConfig *sessionConfig = nullptr) noexcept; [[nodiscard]] bool autostartCheck() const noexcept; [[nodiscard]] bool autostartSourceFileExists() const noexcept; [[nodiscard]] bool hasGeneratedAutostartSource() const noexcept; diff --git a/src/sessionoverrideconfig.cpp b/src/sessionoverrideconfig.cpp new file mode 100644 index 00000000..e110a7e2 --- /dev/null +++ b/src/sessionoverrideconfig.cpp @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "sessionoverrideconfig.h" +#include "constant.h" +#include "global.h" +#include "sessiontype.h" + +#include + +using namespace Qt::StringLiterals; + +Q_LOGGING_CATEGORY(logSessionOverride, "dde.am.session.override") + +SessionOverrideConfig::SessionOverrideConfig(QObject *parent) + : QObject(parent) +{ + m_sessionType = currentSessionType(); + switch (m_sessionType) { + case SessionType::Wayland: + m_subpathPrefix = u"/wayland"_s; + break; + case SessionType::X11: + m_subpathPrefix = u"/x11"_s; + break; + case SessionType::Unknown: + qCInfo(logSessionOverride) << "Unknown session type, session overrides disabled."; + break; + } + qCInfo(logSessionOverride) << "Session override config initialized for" << m_subpathPrefix; +} + +SessionOverrideConfig::~SessionOverrideConfig() = default; + +void SessionOverrideConfig::preload(const QStringList &desktopIds) +{ + for (const auto &id : desktopIds) { + ensureLoaded(id); + } +} + +void SessionOverrideConfig::ensureLoaded(const QString &desktopId) const +{ + if (m_loaded.contains(desktopId)) + return; + m_loaded.insert(desktopId); + + configFor(desktopId); +} + +void SessionOverrideConfig::updateOverride(const QString &desktopId) const +{ + auto *config = configFor(desktopId); + if (!config) + return; + + QHash values; + + const auto exec = config->Exec(); + if (!exec.isEmpty()) { + values[u"Exec"_s] = exec; + } + + const auto tryExec = config->TryExec(); + if (!tryExec.isEmpty()) { + values[u"TryExec"_s] = tryExec; + } + + const auto icon = config->Icon(); + if (!icon.isEmpty()) { + values[u"Icon"_s] = icon; + } + + if (!values.isEmpty()) { + m_overrides[desktopId] = values; + } else { + m_overrides.remove(desktopId); + } +} + +ApplicationOverrideConfig *SessionOverrideConfig::configFor(const QString &desktopId) const +{ + if (m_subpathPrefix.isEmpty()) + return nullptr; + + auto it = m_configs.find(desktopId); + if (it != m_configs.end()) + return it->second.get(); + + const auto subpath = u"/"_s % desktopId % m_subpathPrefix; + auto *config = ApplicationOverrideConfig::create(fromStaticRaw(ApplicationServiceID), + subpath, + const_cast(this)); + if (!config) { + qCWarning(logSessionOverride) << "Failed to create DConfig for subpath:" << subpath; + return nullptr; + } + + qCInfo(logSessionOverride) << "Creating DConfig for" << desktopId << "subpath:" << subpath; + + auto *self = const_cast(this); + QObject::connect(config, &ApplicationOverrideConfig::valueChanged, + self, [self, desktopId](const QString &key) { + if (key == u"Exec"_s || key == u"TryExec"_s || key == u"Icon"_s) { + qCInfo(logSessionOverride) << "Override changed for" << desktopId << "key:" << key; + self->updateOverride(desktopId); + emit self->overrideChanged(desktopId, key); + emit self->configChanged(); + } + }); + + QObject::connect(config, &ApplicationOverrideConfig::configInitializeSucceed, + self, [self, desktopId, config](DTK_CORE_NAMESPACE::DConfig *) { + qCInfo(logSessionOverride) << "Override config initialized for" << desktopId; + self->updateOverride(desktopId); + qCInfo(logSessionOverride) << "After init override for" << desktopId + << "Exec:" << config->Exec() + << "Icon:" << config->Icon() + << "TryExec:" << config->TryExec() + << "hasOverride:" << self->hasOverride(desktopId); + }); + + QObject::connect(config, &ApplicationOverrideConfig::configInitializeFailed, + self, [self, desktopId]() { + qCWarning(logSessionOverride) << "Override config initialization failed for" << desktopId; + }); + + m_configs[desktopId] = std::unique_ptr(config); + return config; +} + +std::optional SessionOverrideConfig::getValue(const QString &desktopId, + const QString &group, + const QString &field) const +{ + if (group != fromStaticRaw(DesktopFileEntryKey)) { + return std::nullopt; + } + + ensureLoaded(desktopId); + + auto desktopIt = m_overrides.constFind(desktopId); + if (desktopIt != m_overrides.constEnd()) { + auto fieldIt = desktopIt->constFind(field); + if (fieldIt != desktopIt->constEnd()) { + return fieldIt->toString(); + } + return std::nullopt; + } + + auto *config = configFor(desktopId); + if (!config) + return std::nullopt; + + if (field == u"Exec"_s) { + const auto exec = config->Exec(); + if (exec.isEmpty()) + return std::nullopt; + return exec; + } + if (field == u"TryExec"_s) { + const auto tryExec = config->TryExec(); + if (tryExec.isEmpty()) + return std::nullopt; + return tryExec; + } + if (field == u"Icon"_s) { + const auto icon = config->Icon(); + if (icon.isEmpty()) + return std::nullopt; + return icon; + } + return std::nullopt; +} + +bool SessionOverrideConfig::hasOverride(const QString &desktopId) const +{ + ensureLoaded(desktopId); + return m_overrides.contains(desktopId); +} + +QString SessionOverrideConfig::resolveExecValue(const QString &overrideValue, QStringView originalExec) +{ + QString ret = overrideValue; + if (ret.contains(u"!AM_FULL!"_s)) { + ret.replace(u"!AM_FULL!"_s, originalExec.toString()); + } + return ret; +} \ No newline at end of file diff --git a/src/sessionoverrideconfig.h b/src/sessionoverrideconfig.h new file mode 100644 index 00000000..28a1d5fa --- /dev/null +++ b/src/sessionoverrideconfig.h @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef SESSIONOVERRIDECONFIG_H +#define SESSIONOVERRIDECONFIG_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "am_appoverride_config.hpp" +#include "sessiontype.h" + +class SessionOverrideConfig : public QObject +{ + Q_OBJECT +public: + explicit SessionOverrideConfig(QObject *parent = nullptr); + ~SessionOverrideConfig() override; + + std::optional getValue(const QString &desktopId, const QString &group, const QString &field) const; + bool hasOverride(const QString &desktopId) const; + void preload(const QStringList &desktopIds); + + static QString resolveExecValue(const QString &overrideValue, QStringView originalExec); + +signals: + void configChanged(); + void overrideChanged(const QString &desktopId, const QString &key); + +private: + Q_DISABLE_COPY(SessionOverrideConfig) + void ensureLoaded(const QString &desktopId) const; + void updateOverride(const QString &desktopId) const; + ApplicationOverrideConfig *configFor(const QString &desktopId) const; + + mutable std::map> m_configs; + mutable QHash> m_overrides; + mutable QSet m_loaded; + SessionType m_sessionType; + QString m_subpathPrefix; +}; + +#endif \ No newline at end of file diff --git a/src/sessiontype.cpp b/src/sessiontype.cpp new file mode 100644 index 00000000..3ce26427 --- /dev/null +++ b/src/sessiontype.cpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "sessiontype.h" +#include + +using namespace Qt::StringLiterals; + +SessionType currentSessionType() noexcept +{ + static const auto type = []() { + const auto env = QProcessEnvironment::systemEnvironment(); + if (!env.value(u"WAYLAND_DISPLAY"_s).isEmpty()) { + return SessionType::Wayland; + } + const auto sessionType = env.value(u"XDG_SESSION_TYPE"_s); + if (sessionType.compare(u"wayland"_s, Qt::CaseInsensitive) == 0) { + return SessionType::Wayland; + } + if (sessionType.compare(u"x11"_s, Qt::CaseInsensitive) == 0) { + return SessionType::X11; + } + return SessionType::Unknown; + }(); + return type; +} \ No newline at end of file diff --git a/src/sessiontype.h b/src/sessiontype.h new file mode 100644 index 00000000..29d2003e --- /dev/null +++ b/src/sessiontype.h @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef SESSIONTYPE_H +#define SESSIONTYPE_H + +enum class SessionType +{ + X11, + Wayland, + Unknown +}; + +SessionType currentSessionType() noexcept; + +#endif \ No newline at end of file diff --git a/tests/ut_sessionoverrideconfig.cpp b/tests/ut_sessionoverrideconfig.cpp new file mode 100644 index 00000000..20baaaa1 --- /dev/null +++ b/tests/ut_sessionoverrideconfig.cpp @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "applicationchecker.h" +#include "constant.h" +#include "desktopentry.h" +#include "global.h" +#include "sessionoverrideconfig.h" +#include "sessiontype.h" +#include +#include +#include + +using namespace Qt::StringLiterals; + +namespace { + +void fillConfig(SessionOverrideConfig &config) +{ + config.m_overrides[u"org.example.app"_s] = { + {u"Exec"_s, u"!AM_FULL! --ozone-platform=wayland"_s}, + {u"TryExec"_s, u"/usr/bin/env"_s}, + {u"Icon"_s, u"my-override-icon"_s} + }; + config.m_overrides[u"org.example.hidden"_s] = { + {u"TryExec"_s, u"/nonexistent/binary"_s} + }; + config.m_overrides[u"org.example.forced"_s] = { + {u"TryExec"_s, u""_s} + }; + config.m_loaded = { + u"org.example.app"_s, + u"org.example.hidden"_s, + u"org.example.forced"_s, + u"org.example.missing"_s, + u"org.example.other"_s + }; +} + +bool parseDesktopEntry(DesktopEntry &entry, const QString &content) +{ + QTemporaryFile file; + if (!file.open()) { + return false; + } + QTextStream out(&file); + out << content; + out.flush(); + file.seek(0); + return entry.parse(file) == ParserError::NoError; +} + +} // namespace + +TEST(SessionOverrideConfigTest, GetValueReturnsOverrideForDesktopEntryGroup) +{ + SessionOverrideConfig config; + fillConfig(config); + + const auto exec = config.getValue(u"org.example.app"_s, fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryExec)); + ASSERT_TRUE(exec.has_value()); + EXPECT_EQ(*exec, u"!AM_FULL! --ozone-platform=wayland"_s); + + const auto tryExec = config.getValue(u"org.example.app"_s, fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryTryExec)); + ASSERT_TRUE(tryExec.has_value()); + EXPECT_EQ(*tryExec, u"/usr/bin/env"_s); + + const auto icon = config.getValue(u"org.example.app"_s, fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryIcon)); + ASSERT_TRUE(icon.has_value()); + EXPECT_EQ(*icon, u"my-override-icon"_s); +} + +TEST(SessionOverrideConfigTest, GetValueReturnsNulloptForNonDesktopEntryGroup) +{ + SessionOverrideConfig config; + fillConfig(config); + + const auto actionGroup = QString(fromStaticRaw(DesktopFileActionKey)).append(u"new-window"_s); + EXPECT_FALSE(config.getValue(u"org.example.app"_s, actionGroup, fromStaticRaw(DesktopEntryExec)).has_value()); + EXPECT_FALSE(config.getValue(u"org.example.app"_s, u"Arbitrary Group"_s, u"Exec"_s).has_value()); +} + +TEST(SessionOverrideConfigTest, GetValueReturnsNulloptForMissingFieldOrApp) +{ + SessionOverrideConfig config; + fillConfig(config); + + EXPECT_FALSE(config.getValue(u"org.example.missing"_s, fromStaticRaw(DesktopFileEntryKey), u"Exec"_s).has_value()); + EXPECT_FALSE(config.getValue(u"org.example.app"_s, fromStaticRaw(DesktopFileEntryKey), u"NotExist"_s).has_value()); + EXPECT_FALSE(config.getValue(u"org.example.missing"_s, fromStaticRaw(DesktopFileEntryKey), u"TryExec"_s).has_value()); +} + +TEST(SessionOverrideConfigTest, HasOverrideDetectsPresence) +{ + SessionOverrideConfig config; + fillConfig(config); + + EXPECT_TRUE(config.hasOverride(u"org.example.app"_s)); + EXPECT_TRUE(config.hasOverride(u"org.example.hidden"_s)); + EXPECT_TRUE(config.hasOverride(u"org.example.forced"_s)); + EXPECT_FALSE(config.hasOverride(u"org.example.missing"_s)); +} + +TEST(SessionOverrideConfigTest, ResolveExecValueSubstitutesFullPlaceholder) +{ + EXPECT_EQ(SessionOverrideConfig::resolveExecValue(u"!AM_FULL! --flag"_s, u"/usr/bin/prog %U"_s), + u"/usr/bin/prog %U --flag"_s); + EXPECT_EQ(SessionOverrideConfig::resolveExecValue(u"!AM_FULL!"_s, u"/usr/bin/prog"_s), u"/usr/bin/prog"_s); + EXPECT_EQ(SessionOverrideConfig::resolveExecValue(u"/bin/env !AM_FULL!"_s, u"prog arg"_s), u"/bin/env prog arg"_s); + EXPECT_EQ(SessionOverrideConfig::resolveExecValue(u"a !AM_FULL! b !AM_FULL!"_s, u"x"_s), u"a x b x"_s); +} + +TEST(SessionOverrideConfigTest, ResolveExecValueKeepsValueWithoutPlaceholder) +{ + EXPECT_EQ(SessionOverrideConfig::resolveExecValue(u"/usr/bin/example --x11"_s, u"orig"_s), u"/usr/bin/example --x11"_s); + EXPECT_EQ(SessionOverrideConfig::resolveExecValue(QString{}, u"orig"_s), QString{}); +} + +TEST(SessionOverrideConfigTest, TryExecCheckAppliesOverride) +{ + DesktopEntry entry; + ASSERT_TRUE(parseDesktopEntry(entry, + uR"([Desktop Entry] +Type=Application +Name=Example +Exec=/usr/bin/example +)"_s)); + + SessionOverrideConfig config; + fillConfig(config); + + // override points to an existing executable -> app should be shown + EXPECT_FALSE(ApplicationFilter::tryExecCheck(entry, u"org.example.app"_s, &config)); + + // override points to a missing binary -> app should be hidden + EXPECT_TRUE(ApplicationFilter::tryExecCheck(entry, u"org.example.hidden"_s, &config)); + + // empty override is treated as force-show + EXPECT_FALSE(ApplicationFilter::tryExecCheck(entry, u"org.example.forced"_s, &config)); +} + +TEST(SessionOverrideConfigTest, TryExecCheckWithoutOverrideUsesDesktopEntry) +{ + DesktopEntry entry; + ASSERT_TRUE(parseDesktopEntry(entry, + uR"([Desktop Entry] +Type=Application +Name=Example +TryExec=/usr/bin/env +Exec=/usr/bin/example +)"_s)); + + SessionOverrideConfig config; + fillConfig(config); + + // no override for this app, fall back to desktop entry TryExec (/usr/bin/env exists) -> shown + EXPECT_FALSE(ApplicationFilter::tryExecCheck(entry, u"org.example.other"_s, &config)); +} + +TEST(SessionTypeTest, DetectReturnsKnownValue) +{ + const auto type = currentSessionType(); + EXPECT_TRUE(type == SessionType::X11 || type == SessionType::Wayland || type == SessionType::Unknown); +} \ No newline at end of file