Compare commits

..

No commits in common. "c7956eed8b68077dbba409571da605f3687b10ce" and "ded8ad83da95016f384b19072e9ffbf956bdd0d5" have entirely different histories.

27 changed files with 128 additions and 3398 deletions

View file

@ -10,7 +10,6 @@ add_executable(alakarte
gamepadmanager.cpp gamepadmanager.cpp
inputmanager.cpp inputmanager.cpp
mediamanager.cpp mediamanager.cpp
runnermanagerclient.cpp
screenshotmodel.cpp screenshotmodel.cpp
platformimporter.cpp platformimporter.cpp
steamimporter.cpp steamimporter.cpp
@ -140,5 +139,3 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
install(TARGETS alakarte ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) install(TARGETS alakarte ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
add_subdirectory(gamecenter) add_subdirectory(gamecenter)
add_subdirectory(runner)

View file

@ -27,7 +27,6 @@ App::App(QObject *parent)
: QObject(parent) : QObject(parent)
, m_gameModel(new GameModel(this)) , m_gameModel(new GameModel(this))
, m_launcher(new GameLauncher(this)) , m_launcher(new GameLauncher(this))
, m_runnerManager(new RunnerManagerClient(this))
, m_steamGridDB(new SteamGridDB(this)) , m_steamGridDB(new SteamGridDB(this))
, m_mediaManager(new MediaManager(this)) , m_mediaManager(new MediaManager(this))
, m_config(new Config(this)) , m_config(new Config(this))
@ -170,11 +169,6 @@ GameLauncher *App::launcher() const
return m_launcher; return m_launcher;
} }
RunnerManagerClient *App::runnerManager() const
{
return m_runnerManager;
}
SteamGridDB *App::steamGridDB() const SteamGridDB *App::steamGridDB() const
{ {
return m_steamGridDB; return m_steamGridDB;

View file

@ -12,7 +12,6 @@
#include "gamelauncher.h" #include "gamelauncher.h"
#include "gamemodel.h" #include "gamemodel.h"
#include "mediamanager.h" #include "mediamanager.h"
#include "runnermanagerclient.h"
#include "steamgriddb.h" #include "steamgriddb.h"
class App : public QObject class App : public QObject
@ -23,7 +22,6 @@ class App : public QObject
Q_PROPERTY(GameModel *gameModel READ gameModel CONSTANT) Q_PROPERTY(GameModel *gameModel READ gameModel CONSTANT)
Q_PROPERTY(GameLauncher *launcher READ launcher CONSTANT) Q_PROPERTY(GameLauncher *launcher READ launcher CONSTANT)
Q_PROPERTY(RunnerManagerClient *runnerManager READ runnerManager CONSTANT)
Q_PROPERTY(SteamGridDB *steamGridDB READ steamGridDB CONSTANT) Q_PROPERTY(SteamGridDB *steamGridDB READ steamGridDB CONSTANT)
Q_PROPERTY(MediaManager *mediaManager READ mediaManager CONSTANT) Q_PROPERTY(MediaManager *mediaManager READ mediaManager CONSTANT)
Q_PROPERTY(Config *config READ config CONSTANT) Q_PROPERTY(Config *config READ config CONSTANT)
@ -36,7 +34,6 @@ public:
GameModel *gameModel() const; GameModel *gameModel() const;
GameLauncher *launcher() const; GameLauncher *launcher() const;
RunnerManagerClient *runnerManager() const;
SteamGridDB *steamGridDB() const; SteamGridDB *steamGridDB() const;
MediaManager *mediaManager() const; MediaManager *mediaManager() const;
Config *config() const; Config *config() const;
@ -77,7 +74,6 @@ private:
GameModel *m_gameModel; GameModel *m_gameModel;
GameLauncher *m_launcher; GameLauncher *m_launcher;
RunnerManagerClient *m_runnerManager;
SteamGridDB *m_steamGridDB; SteamGridDB *m_steamGridDB;
MediaManager *m_mediaManager; MediaManager *m_mediaManager;
Config *m_config; Config *m_config;

View file

@ -159,19 +159,6 @@ void Game::setLaunchRunner(const QString &runner)
} }
} }
QString Game::launchRunnerId() const
{
return m_launchRunnerId;
}
void Game::setLaunchRunnerId(const QString &runnerId)
{
if (m_launchRunnerId != runnerId) {
m_launchRunnerId = runnerId;
Q_EMIT launchRunnerIdChanged();
}
}
QString Game::launchRunnerPath() const QString Game::launchRunnerPath() const
{ {
return m_launchRunnerPath; return m_launchRunnerPath;
@ -336,8 +323,7 @@ QJsonObject Game::toJson() const
obj[QStringLiteral("hidden")] = m_hidden; obj[QStringLiteral("hidden")] = m_hidden;
obj[QStringLiteral("installed")] = m_installed; obj[QStringLiteral("installed")] = m_installed;
const bool hasLaunchConfig = const bool hasLaunchConfig = !m_launchEnv.isEmpty() || !m_launchRunner.isEmpty() || !m_launchRunnerPath.isEmpty() || !m_launchPrefixPath.isEmpty();
!m_launchEnv.isEmpty() || !m_launchRunner.isEmpty() || !m_launchRunnerId.isEmpty() || !m_launchRunnerPath.isEmpty() || !m_launchPrefixPath.isEmpty();
if (hasLaunchConfig) { if (hasLaunchConfig) {
QJsonObject launchObj; QJsonObject launchObj;
@ -352,9 +338,6 @@ QJsonObject Game::toJson() const
if (!m_launchRunner.isEmpty()) { if (!m_launchRunner.isEmpty()) {
launchObj.insert(QStringLiteral("runner"), m_launchRunner); launchObj.insert(QStringLiteral("runner"), m_launchRunner);
} }
if (!m_launchRunnerId.isEmpty()) {
launchObj.insert(QStringLiteral("runnerId"), m_launchRunnerId);
}
if (!m_launchRunnerPath.isEmpty()) { if (!m_launchRunnerPath.isEmpty()) {
launchObj.insert(QStringLiteral("runnerPath"), m_launchRunnerPath); launchObj.insert(QStringLiteral("runnerPath"), m_launchRunnerPath);
} }
@ -408,7 +391,6 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
QVariantMap env; QVariantMap env;
QString runner; QString runner;
QString runnerId;
QString runnerPath; QString runnerPath;
QString prefixPath; QString prefixPath;
const QJsonValue launchValue = json.value(QStringLiteral("launch")); const QJsonValue launchValue = json.value(QStringLiteral("launch"));
@ -420,7 +402,6 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
} }
runner = launchObj.value(QStringLiteral("runner")).toString(); runner = launchObj.value(QStringLiteral("runner")).toString();
runnerId = launchObj.value(QStringLiteral("runnerId")).toString();
runnerPath = launchObj.value(QStringLiteral("runnerPath")).toString(); runnerPath = launchObj.value(QStringLiteral("runnerPath")).toString();
prefixPath = launchObj.value(QStringLiteral("prefixPath")).toString(); prefixPath = launchObj.value(QStringLiteral("prefixPath")).toString();
} }
@ -433,9 +414,6 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
if (runner.isEmpty()) { if (runner.isEmpty()) {
runner = json.value(QStringLiteral("launchRunner")).toString(); runner = json.value(QStringLiteral("launchRunner")).toString();
} }
if (runnerId.isEmpty()) {
runnerId = json.value(QStringLiteral("launchRunnerId")).toString();
}
if (runnerPath.isEmpty()) { if (runnerPath.isEmpty()) {
runnerPath = json.value(QStringLiteral("launchRunnerPath")).toString(); runnerPath = json.value(QStringLiteral("launchRunnerPath")).toString();
} }
@ -450,9 +428,6 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
if (!runner.isEmpty()) { if (!runner.isEmpty()) {
game->setLaunchRunner(runner); game->setLaunchRunner(runner);
} }
if (!runnerId.isEmpty()) {
game->setLaunchRunnerId(runnerId);
}
if (!runnerPath.isEmpty()) { if (!runnerPath.isEmpty()) {
game->setLaunchRunnerPath(runnerPath); game->setLaunchRunnerPath(runnerPath);
} }

View file

@ -28,7 +28,6 @@ class Game : public QObject
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged) Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged)
Q_PROPERTY(QVariantMap launchEnv READ launchEnv WRITE setLaunchEnv NOTIFY launchEnvChanged) Q_PROPERTY(QVariantMap launchEnv READ launchEnv WRITE setLaunchEnv NOTIFY launchEnvChanged)
Q_PROPERTY(QString launchRunner READ launchRunner WRITE setLaunchRunner NOTIFY launchRunnerChanged) Q_PROPERTY(QString launchRunner READ launchRunner WRITE setLaunchRunner NOTIFY launchRunnerChanged)
Q_PROPERTY(QString launchRunnerId READ launchRunnerId WRITE setLaunchRunnerId NOTIFY launchRunnerIdChanged)
Q_PROPERTY(QString launchRunnerPath READ launchRunnerPath WRITE setLaunchRunnerPath NOTIFY launchRunnerPathChanged) Q_PROPERTY(QString launchRunnerPath READ launchRunnerPath WRITE setLaunchRunnerPath NOTIFY launchRunnerPathChanged)
Q_PROPERTY(QString launchPrefixPath READ launchPrefixPath WRITE setLaunchPrefixPath NOTIFY launchPrefixPathChanged) Q_PROPERTY(QString launchPrefixPath READ launchPrefixPath WRITE setLaunchPrefixPath NOTIFY launchPrefixPathChanged)
Q_PROPERTY(QString platform READ platform WRITE setPlatform NOTIFY platformChanged) Q_PROPERTY(QString platform READ platform WRITE setPlatform NOTIFY platformChanged)
@ -77,9 +76,6 @@ public:
QString launchRunner() const; QString launchRunner() const;
void setLaunchRunner(const QString &runner); void setLaunchRunner(const QString &runner);
QString launchRunnerId() const;
void setLaunchRunnerId(const QString &runnerId);
QString launchRunnerPath() const; QString launchRunnerPath() const;
void setLaunchRunnerPath(const QString &path); void setLaunchRunnerPath(const QString &path);
@ -131,7 +127,6 @@ Q_SIGNALS:
void workingDirectoryChanged(); void workingDirectoryChanged();
void launchEnvChanged(); void launchEnvChanged();
void launchRunnerChanged(); void launchRunnerChanged();
void launchRunnerIdChanged();
void launchRunnerPathChanged(); void launchRunnerPathChanged();
void launchPrefixPathChanged(); void launchPrefixPathChanged();
void platformChanged(); void platformChanged();
@ -156,7 +151,6 @@ private:
QString m_workingDirectory; QString m_workingDirectory;
QVariantMap m_launchEnv; QVariantMap m_launchEnv;
QString m_launchRunner; QString m_launchRunner;
QString m_launchRunnerId;
QString m_launchRunnerPath; QString m_launchRunnerPath;
QString m_launchPrefixPath; QString m_launchPrefixPath;
QString m_platform; QString m_platform;

View file

@ -24,9 +24,6 @@ install(TARGETS alakarte_gamecenter ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus") file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd") file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus-system")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd-system")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus-policy")
configure_file(dbus/org.kde.GameCenter1.service.in configure_file(dbus/org.kde.GameCenter1.service.in
"${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service" "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
@ -38,21 +35,6 @@ configure_file(systemd/org.kde.GameCenter1.service.in
@ONLY @ONLY
) )
configure_file(dbus/org.kde.GameCenter1.system.service.in
"${CMAKE_CURRENT_BINARY_DIR}/dbus-system/org.kde.GameCenter1.service"
@ONLY
)
configure_file(systemd/org.kde.GameCenter1.system.service.in
"${CMAKE_CURRENT_BINARY_DIR}/systemd-system/org.kde.GameCenter1.service"
@ONLY
)
configure_file(dbus/org.kde.GameCenter1.conf.in
"${CMAKE_CURRENT_BINARY_DIR}/dbus-policy/org.kde.GameCenter1.conf"
@ONLY
)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service" install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR} DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}
) )
@ -60,15 +42,3 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.GameCenter1.service" install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.GameCenter1.service"
DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR} DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}
) )
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus-system/org.kde.GameCenter1.service"
DESTINATION ${KDE_INSTALL_DBUSSYSTEMSERVICEDIR}
)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd-system/org.kde.GameCenter1.service"
DESTINATION ${KDE_INSTALL_SYSTEMDUNITDIR}
)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus-policy/org.kde.GameCenter1.conf"
DESTINATION ${KDE_INSTALL_SYSCONFDIR}/dbus-1/system.d
)

View file

@ -1,13 +0,0 @@
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.kde.GameCenter1"/>
</policy>
<policy context="default">
<allow send_destination="org.kde.GameCenter1"/>
<allow receive_sender="org.kde.GameCenter1"/>
</policy>
</busconfig>

View file

@ -1,4 +0,0 @@
[D-BUS Service]
Name=org.kde.GameCenter1
Exec=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-gamecenter --system
SystemdService=org.kde.GameCenter1.service

View file

@ -7,13 +7,9 @@
#include <QDBusConnection> #include <QDBusConnection>
#include <QDBusConnectionInterface> #include <QDBusConnectionInterface>
#include <QDBusError> #include <QDBusError>
#include <QDBusInterface>
#include <QDBusMessage>
#include <QDBusObjectPath> #include <QDBusObjectPath>
#include <QDBusReply> #include <QDBusReply>
#include <QDBusVariant> #include <QDBusVariant>
#include <QDBusVirtualObject>
#include <QDir>
#include <QProcess> #include <QProcess>
#include <QProcessEnvironment> #include <QProcessEnvironment>
#include <QStandardPaths> #include <QStandardPaths>
@ -23,14 +19,8 @@
#include <QRegularExpression> #include <QRegularExpression>
#include <QSet>
#include <utility> #include <utility>
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1");
static const QString kRunnerInterface = QStringLiteral("org.kde.ALaKarte.Runner1");
static QString ensureScopeUnitName(const QString &unitName) static QString ensureScopeUnitName(const QString &unitName)
{ {
if (unitName.endsWith(QLatin1String(".scope"))) { if (unitName.endsWith(QLatin1String(".scope"))) {
@ -59,10 +49,6 @@ static QVariant normalizeForDbus(QVariant v)
return QString(v.toChar()); return QString(v.toChar());
} }
if (v.userType() == QMetaType::QString || v.userType() == QMetaType::QStringList) {
return v;
}
if (v.canConvert<QDBusArgument>()) { if (v.canConvert<QDBusArgument>()) {
const QDBusArgument arg = v.value<QDBusArgument>(); const QDBusArgument arg = v.value<QDBusArgument>();
const QMap<QString, QString> asStringMap = qdbus_cast<QMap<QString, QString>>(arg); const QMap<QString, QString> asStringMap = qdbus_cast<QMap<QString, QString>>(arg);
@ -91,7 +77,7 @@ static QVariant normalizeForDbus(QVariant v)
} }
return m; return m;
} }
if (v.userType() == QMetaType::QVariantList) { if (v.canConvert<QVariantList>()) {
QVariantList list = v.toList(); QVariantList list = v.toList();
for (QVariant &item : list) { for (QVariant &item : list) {
item = normalizeForDbus(item); item = normalizeForDbus(item);
@ -110,57 +96,6 @@ static QVariantMap normalizeVariantMapForDbus(const QVariantMap &map)
return out; return out;
} }
static bool tryResolveWithRunnerManager(const QVariantMap &spec, QVariantMap &out)
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected() || !bus.interface()) {
return false;
}
if (!bus.interface()->isServiceRegistered(kRunnerService)) {
bus.interface()->startService(kRunnerService);
}
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
if (!iface.isValid()) {
if (bus.interface()->startService(kRunnerService).isValid()) {
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
if (retryIface.isValid()) {
retryIface.setTimeout(2000);
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
if (retryReply.isValid()) {
out = normalizeVariantMapForDbus(retryReply.value());
return true;
}
}
}
return false;
}
iface.setTimeout(2000);
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
if (!reply.isValid()) {
if (reply.error().type() == QDBusError::ServiceUnknown) {
bus.interface()->startService(kRunnerService);
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
if (!retryIface.isValid()) {
return false;
}
retryIface.setTimeout(2000);
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
if (!retryReply.isValid()) {
return false;
}
out = normalizeVariantMapForDbus(retryReply.value());
return true;
}
return false;
}
out = normalizeVariantMapForDbus(reply.value());
return true;
}
static QString mapStringValue(const QVariantMap &map, const QStringList &keys) static QString mapStringValue(const QVariantMap &map, const QStringList &keys)
{ {
for (const QString &k : keys) { for (const QString &k : keys) {
@ -236,445 +171,6 @@ private:
} // namespace } // namespace
static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1");
static const QString kGameCenterPath = QStringLiteral("/org/kde/GameCenter1");
static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
namespace
{
static QString gameCenterIntrospectionXml()
{
return QStringLiteral(
"<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-Bus Object Introspection 1.0//EN\" "
"\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">"
"<node>"
" <interface name=\"org.freedesktop.DBus.Introspectable\">"
" <method name=\"Introspect\">"
" <arg name=\"xml\" type=\"s\" direction=\"out\"/>"
" </method>"
" </interface>"
" <interface name=\"org.kde.GameCenter1\">"
" <method name=\"Ping\">"
" <arg name=\"out\" type=\"s\" direction=\"out\"/>"
" </method>"
" <method name=\"Version\">"
" <arg name=\"major\" type=\"u\" direction=\"out\"/>"
" <arg name=\"minor\" type=\"u\" direction=\"out\"/>"
" </method>"
" <method name=\"GetCapabilities\">"
" <arg name=\"caps\" type=\"a{sv}\" direction=\"out\"/>"
" </method>"
" <method name=\"ListSessions\">"
" <arg name=\"sessions\" type=\"av\" direction=\"out\"/>"
" </method>"
" <method name=\"GetSession\">"
" <arg name=\"sessionId\" type=\"s\" direction=\"in\"/>"
" <arg name=\"session\" type=\"a{sv}\" direction=\"out\"/>"
" </method>"
" <method name=\"Launch\">"
" <arg name=\"launchSpec\" type=\"a{sv}\" direction=\"in\"/>"
" <arg name=\"sessionId\" type=\"s\" direction=\"out\"/>"
" </method>"
" <method name=\"Stop\">"
" <arg name=\"sessionId\" type=\"s\" direction=\"in\"/>"
" </method>"
" <method name=\"StopByGameId\">"
" <arg name=\"gameId\" type=\"s\" direction=\"in\"/>"
" </method>"
" <method name=\"SetPolicy\">"
" <arg name=\"policy\" type=\"a{sv}\" direction=\"in\"/>"
" </method>"
" <method name=\"GetPolicy\">"
" <arg name=\"policy\" type=\"a{sv}\" direction=\"out\"/>"
" </method>"
" <signal name=\"SessionAdded\">"
" <arg name=\"session\" type=\"a{sv}\"/>"
" </signal>"
" <signal name=\"SessionChanged\">"
" <arg name=\"session\" type=\"a{sv}\"/>"
" </signal>"
" <signal name=\"SessionRemoved\">"
" <arg name=\"sessionId\" type=\"s\"/>"
" <arg name=\"finalState\" type=\"a{sv}\"/>"
" </signal>"
" <signal name=\"LaunchFailed\">"
" <arg name=\"error\" type=\"a{sv}\"/>"
" </signal>"
" </interface>"
"</node>");
}
} // namespace
class GameCenterSystemProxyPrivate;
class GameCenterProxyVirtualObject : public QDBusVirtualObject
{
Q_OBJECT
public:
explicit GameCenterProxyVirtualObject(GameCenterSystemProxyPrivate *proxy, QObject *parent = nullptr)
: QDBusVirtualObject(parent)
, m_proxy(proxy)
{
}
QString introspect(const QString &path) const override;
bool handleMessage(const QDBusMessage &message, const QDBusConnection &connection) override;
private:
GameCenterSystemProxyPrivate *m_proxy = nullptr;
};
class BackendSignalForwarder : public QObject
{
Q_OBJECT
public:
explicit BackendSignalForwarder(uint uid, GameCenterSystemProxyPrivate *proxy, QObject *parent = nullptr)
: QObject(parent)
, m_uid(uid)
, m_proxy(proxy)
{
}
public Q_SLOTS:
void onSessionAdded(const QVariantMap &session);
void onSessionChanged(const QVariantMap &session);
void onSessionRemoved(const QString &sessionId, const QVariantMap &finalState);
void onLaunchFailed(const QVariantMap &error);
private:
uint m_uid = 0;
GameCenterSystemProxyPrivate *m_proxy = nullptr;
};
struct BackendBusEntry {
BackendBusEntry(const QDBusConnection &bus, BackendSignalForwarder *forwarder)
: bus(bus)
, forwarder(forwarder)
{
}
QDBusConnection bus;
BackendSignalForwarder *forwarder = nullptr;
};
class GameCenterSystemProxyPrivate : public QObject
{
Q_OBJECT
public:
explicit GameCenterSystemProxyPrivate(QObject *parent = nullptr)
: QObject(parent)
{
}
bool init();
bool handleSystemMessage(const QDBusMessage &message, const QDBusConnection &connection);
void forwardSignalToUid(uint uid, const QString &member, const QList<QVariant> &args);
private Q_SLOTS:
void handleServiceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner);
private:
bool uidForSystemSender(const QString &senderUniqueName, uint &uid) const;
void rememberClient(const QString &senderUniqueName, uint uid);
void forgetClient(const QString &senderUniqueName);
BackendBusEntry *ensureBackend(uint uid);
QDBusConnection m_systemBus = QDBusConnection::systemBus();
std::unique_ptr<GameCenterProxyVirtualObject> m_virtualObject;
QHash<uint, BackendBusEntry> m_backends;
QHash<QString, uint> m_clientUid;
QHash<uint, QSet<QString>> m_uidClients;
};
QString GameCenterProxyVirtualObject::introspect(const QString &path) const
{
if (path == kGameCenterPath) {
return gameCenterIntrospectionXml();
}
return {};
}
bool GameCenterProxyVirtualObject::handleMessage(const QDBusMessage &message, const QDBusConnection &connection)
{
if (!m_proxy) {
return false;
}
return m_proxy->handleSystemMessage(message, connection);
}
void BackendSignalForwarder::onSessionAdded(const QVariantMap &session)
{
if (m_proxy) {
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionAdded"), {session});
}
}
void BackendSignalForwarder::onSessionChanged(const QVariantMap &session)
{
if (m_proxy) {
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionChanged"), {session});
}
}
void BackendSignalForwarder::onSessionRemoved(const QString &sessionId, const QVariantMap &finalState)
{
if (m_proxy) {
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionRemoved"), {sessionId, finalState});
}
}
void BackendSignalForwarder::onLaunchFailed(const QVariantMap &error)
{
if (m_proxy) {
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("LaunchFailed"), {error});
}
}
bool GameCenterSystemProxyPrivate::init()
{
if (!m_systemBus.isConnected()) {
return false;
}
if (!m_systemBus.registerService(kGameCenterService)) {
return false;
}
m_virtualObject = std::make_unique<GameCenterProxyVirtualObject>(this);
if (!m_systemBus.registerVirtualObject(kGameCenterPath, m_virtualObject.get(), QDBusConnection::SingleNode)) {
return false;
}
if (m_systemBus.interface()) {
connect(m_systemBus.interface(), &QDBusConnectionInterface::serviceOwnerChanged, this, &GameCenterSystemProxyPrivate::handleServiceOwnerChanged);
}
return true;
}
bool GameCenterSystemProxyPrivate::uidForSystemSender(const QString &senderUniqueName, uint &uid) const
{
if (!m_systemBus.interface()) {
return false;
}
const QDBusReply<uint> uidReply = m_systemBus.interface()->serviceUid(senderUniqueName);
if (!uidReply.isValid()) {
return false;
}
uid = uidReply.value();
return true;
}
void GameCenterSystemProxyPrivate::rememberClient(const QString &senderUniqueName, uint uid)
{
const auto it = m_clientUid.constFind(senderUniqueName);
if (it != m_clientUid.constEnd()) {
if (it.value() == uid) {
return;
}
forgetClient(senderUniqueName);
}
m_clientUid.insert(senderUniqueName, uid);
m_uidClients[uid].insert(senderUniqueName);
}
void GameCenterSystemProxyPrivate::forgetClient(const QString &senderUniqueName)
{
const auto it = m_clientUid.find(senderUniqueName);
if (it == m_clientUid.end()) {
return;
}
const uint uid = it.value();
m_clientUid.erase(it);
auto clientsIt = m_uidClients.find(uid);
if (clientsIt != m_uidClients.end()) {
clientsIt.value().remove(senderUniqueName);
if (clientsIt.value().isEmpty()) {
m_uidClients.erase(clientsIt);
}
}
}
void GameCenterSystemProxyPrivate::handleServiceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner)
{
Q_UNUSED(oldOwner)
if (!newOwner.isEmpty()) {
return;
}
if (!name.startsWith(QLatin1Char(':'))) {
return;
}
forgetClient(name);
}
BackendBusEntry *GameCenterSystemProxyPrivate::ensureBackend(uint uid)
{
const auto it = m_backends.find(uid);
if (it != m_backends.end()) {
return &it.value();
}
const QString address = QStringLiteral("unix:path=/run/user/%1/bus").arg(uid);
const QString connectionName = QStringLiteral("gamecenter-user-%1").arg(uid);
QDBusConnection backend = QDBusConnection::connectToBus(address, connectionName);
if (!backend.isConnected()) {
return nullptr;
}
auto *forwarder = new BackendSignalForwarder(uid, this, this);
BackendBusEntry entry(backend, forwarder);
backend.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("SessionAdded"), forwarder, SLOT(onSessionAdded(QVariantMap)));
backend
.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("SessionChanged"), forwarder, SLOT(onSessionChanged(QVariantMap)));
backend.connect(kGameCenterService,
kGameCenterPath,
kGameCenterInterface,
QStringLiteral("SessionRemoved"),
forwarder,
SLOT(onSessionRemoved(QString, QVariantMap)));
backend.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("LaunchFailed"), forwarder, SLOT(onLaunchFailed(QVariantMap)));
auto inserted = m_backends.insert(uid, entry);
return &inserted.value();
}
void GameCenterSystemProxyPrivate::forwardSignalToUid(uint uid, const QString &member, const QList<QVariant> &args)
{
if (!m_systemBus.isConnected()) {
return;
}
const auto clientsIt = m_uidClients.constFind(uid);
if (clientsIt == m_uidClients.constEnd()) {
return;
}
const QSet<QString> clients = clientsIt.value();
for (const QString &client : clients) {
if (client.isEmpty()) {
continue;
}
QDBusMessage signal = QDBusMessage::createTargetedSignal(client, kGameCenterPath, kGameCenterInterface, member);
signal.setArguments(args);
m_systemBus.send(signal);
}
}
bool GameCenterSystemProxyPrivate::handleSystemMessage(const QDBusMessage &message, const QDBusConnection &connection)
{
if (message.type() != QDBusMessage::MethodCallMessage) {
return false;
}
if (message.interface() == QLatin1String("org.freedesktop.DBus.Introspectable") && message.member() == QLatin1String("Introspect")) {
if (!message.isReplyRequired()) {
return true;
}
connection.send(message.createReply(gameCenterIntrospectionXml()));
return true;
}
const QString sender = message.service();
if (sender.isEmpty()) {
if (message.isReplyRequired()) {
connection.send(message.createErrorReply(QDBusError::AccessDenied, QStringLiteral("missing sender")));
}
return true;
}
uint uid = 0;
if (!uidForSystemSender(sender, uid)) {
if (message.isReplyRequired()) {
connection.send(message.createErrorReply(QDBusError::AccessDenied, QStringLiteral("failed to determine caller uid")));
}
return true;
}
rememberClient(sender, uid);
BackendBusEntry *backend = ensureBackend(uid);
if (!backend) {
if (message.isReplyRequired()) {
connection.send(message.createErrorReply(QDBusError::ServiceUnknown, QStringLiteral("user bus backend unavailable")));
}
return true;
}
QDBusMessage forward = QDBusMessage::createMethodCall(kGameCenterService, message.path(), message.interface(), message.member());
forward.setArguments(message.arguments());
forward.setAutoStartService(true);
const QDBusMessage backendReply = backend->bus.call(forward, QDBus::Block, -1);
if (!message.isReplyRequired()) {
return true;
}
if (backendReply.type() == QDBusMessage::ErrorMessage) {
connection.send(message.createErrorReply(backendReply.errorName(), backendReply.errorMessage()));
return true;
}
if (backendReply.type() != QDBusMessage::ReplyMessage) {
connection.send(message.createErrorReply(QDBusError::Failed, QStringLiteral("backend did not reply")));
return true;
}
QDBusMessage out = message.createReply(backendReply.arguments());
if (message.interface() == kGameCenterInterface && message.member() == QLatin1String("GetCapabilities")) {
if (!out.arguments().isEmpty()) {
QVariant capsV = unwrapDbusVariant(out.arguments().first());
QVariantMap caps;
if (capsV.canConvert<QDBusArgument>()) {
const QDBusArgument arg = capsV.value<QDBusArgument>();
caps = qdbus_cast<QVariantMap>(arg);
} else if (capsV.canConvert<QVariantMap>()) {
caps = capsV.toMap();
}
if (!caps.isEmpty()) {
caps.insert(QStringLiteral("supportsSystemBus"), true);
out.setArguments({caps});
}
}
}
connection.send(out);
return true;
}
GameCenterSystemProxy::GameCenterSystemProxy(QObject *parent)
: QObject(parent)
, d(std::make_unique<GameCenterSystemProxyPrivate>())
{
}
GameCenterSystemProxy::~GameCenterSystemProxy() = default;
bool GameCenterSystemProxy::init()
{
return d->init();
}
void GameCenterDaemon::watchSystemdUnit(const QString &sessionId, const QString &unitName, const QDBusObjectPath &unitPath) void GameCenterDaemon::watchSystemdUnit(const QString &sessionId, const QString &unitName, const QDBusObjectPath &unitPath)
{ {
if (unitName.isEmpty()) { if (unitName.isEmpty()) {
@ -1049,69 +545,6 @@ QString GameCenterDaemon::launchDirect(const QVariantMap &launchSpec)
envOverrides = extractVariantMap(launchSpec.value(QStringLiteral("envOverrides"))); envOverrides = extractVariantMap(launchSpec.value(QStringLiteral("envOverrides")));
} }
const QString runnerId = mapStringValue(launchSpec, {QStringLiteral("runnerId")});
const QString runner = mapStringValue(launchSpec, {QStringLiteral("runner")});
const QString runnerPath = mapStringValue(launchSpec, {QStringLiteral("runnerPath")});
const QString prefixPath = mapStringValue(launchSpec, {QStringLiteral("prefixPath")});
const QString requestedProgram = mapStringValue(launchSpec, {QStringLiteral("requestedProgram")});
QStringList requestedArgs;
if (launchSpec.contains(QStringLiteral("requestedArgs"))) {
requestedArgs = variantToStringList(launchSpec.value(QStringLiteral("requestedArgs")));
}
QVariantMap requestedEnvOverrides;
if (launchSpec.contains(QStringLiteral("requestedEnvOverrides"))) {
requestedEnvOverrides = extractVariantMap(launchSpec.value(QStringLiteral("requestedEnvOverrides")));
}
const bool wantsRunnerResolution = !runnerId.isEmpty() || runner == QLatin1String("wine") || runner == QLatin1String("proton");
if (wantsRunnerResolution) {
const QString baseProgram = requestedProgram.isEmpty() ? program : requestedProgram;
const QStringList baseArgs = requestedArgs.isEmpty() ? args : requestedArgs;
const QVariantMap baseEnv = requestedEnvOverrides.isEmpty() ? envOverrides : requestedEnvOverrides;
const QVariantMap resolveSpec = {
{QStringLiteral("runnerId"), runnerId},
{QStringLiteral("runner"), runner},
{QStringLiteral("runnerPath"), runnerPath},
{QStringLiteral("gameId"), gameId},
{QStringLiteral("prefixPath"), prefixPath},
{QStringLiteral("program"), baseProgram},
{QStringLiteral("args"), baseArgs},
{QStringLiteral("envOverrides"), baseEnv},
};
QVariantMap resolved;
if (tryResolveWithRunnerManager(resolveSpec, resolved)) {
if (!resolved.value(QStringLiteral("ok")).toBool()) {
QVariantMap ctx = launchSpec;
ctx.insert(QStringLiteral("runnerResolution"), resolved);
failLaunch(resolved.value(QStringLiteral("error")).toString(), ctx);
return {};
}
program = mapStringValue(resolved, {QStringLiteral("finalProgram")});
if (resolved.contains(QStringLiteral("finalArgs"))) {
args = variantToStringList(resolved.value(QStringLiteral("finalArgs")));
}
if (resolved.contains(QStringLiteral("effectiveEnv"))) {
envOverrides = extractVariantMap(resolved.value(QStringLiteral("effectiveEnv")));
}
const QString resolvedPrefixPath = mapStringValue(resolved, {QStringLiteral("resolvedPrefixPath")});
if (!resolvedPrefixPath.isEmpty()) {
QDir().mkpath(resolvedPrefixPath);
}
} else if (!runnerId.isEmpty()) {
QVariantMap ctx = launchSpec;
ctx.insert(QStringLiteral("runnerResolution"),
QVariantMap{{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("runner service unavailable")}});
failLaunch(QStringLiteral("runner service is not available"), ctx);
return {};
}
}
const QString sessionId = QUuid::createUuid().toString(QUuid::WithoutBraces); const QString sessionId = QUuid::createUuid().toString(QUuid::WithoutBraces);
const QString unitName = ensureScopeUnitName(QStringLiteral("alakarte-game-%1").arg(sessionId)); const QString unitName = ensureScopeUnitName(QStringLiteral("alakarte-game-%1").arg(sessionId));

View file

@ -3,8 +3,6 @@
#pragma once #pragma once
#include <memory>
#include <QDBusObjectPath> #include <QDBusObjectPath>
#include <QDateTime> #include <QDateTime>
#include <QHash> #include <QHash>
@ -96,19 +94,3 @@ private:
QHash<QString, QString> m_unitNameToSessionId; QHash<QString, QString> m_unitNameToSessionId;
QHash<QString, QObject *> m_unitPathWatchers; QHash<QString, QObject *> m_unitPathWatchers;
}; };
class GameCenterSystemProxyPrivate;
class GameCenterSystemProxy : public QObject
{
Q_OBJECT
public:
explicit GameCenterSystemProxy(QObject *parent = nullptr);
~GameCenterSystemProxy() override;
bool init();
private:
std::unique_ptr<GameCenterSystemProxyPrivate> d;
};

View file

@ -1,8 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors // SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include <QCommandLineOption>
#include <QCommandLineParser>
#include <QCoreApplication> #include <QCoreApplication>
#include "gamecenterdaemon.h" #include "gamecenterdaemon.h"
@ -11,21 +9,6 @@ int main(int argc, char *argv[])
{ {
QCoreApplication app(argc, argv); QCoreApplication app(argc, argv);
QCommandLineParser parser;
parser.setApplicationDescription(QStringLiteral("A-La-Karte Game Center"));
parser.addHelpOption();
parser.addOption(QCommandLineOption(QStringLiteral("system"), QStringLiteral("Run as system bus proxy")));
parser.process(app);
if (parser.isSet(QStringLiteral("system"))) {
GameCenterSystemProxy proxy;
if (!proxy.init()) {
return 1;
}
return app.exec();
}
GameCenterDaemon daemon; GameCenterDaemon daemon;
if (!daemon.init()) { if (!daemon.init()) {
return 1; return 1;

View file

@ -1,8 +0,0 @@
[Unit]
Description=A-La-Karte Game Center
[Service]
ExecStart=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-gamecenter --system
Type=dbus
BusName=org.kde.GameCenter1
Restart=no

View file

@ -7,8 +7,6 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QDBusArgument> #include <QDBusArgument>
#include <QDBusConnection> #include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusError>
#include <QDBusInterface> #include <QDBusInterface>
#include <QDBusReply> #include <QDBusReply>
#include <QDBusVariant> #include <QDBusVariant>
@ -21,88 +19,16 @@
#include <QTimer> #include <QTimer>
#include <QUrl> #include <QUrl>
static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1"); static QStringList steamCandidateRoots()
static const QString kGameCenterPath = QStringLiteral("/org/kde/GameCenter1");
static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1");
static const QString kRunnerInterface = QStringLiteral("org.kde.ALaKarte.Runner1");
static bool pingDaemon(QDBusConnection bus)
{ {
if (!bus.isConnected()) { const QString home = QDir::homePath();
return false; return {
} home + QStringLiteral("/.steam/root"),
home + QStringLiteral("/.steam/steam"),
QDBusInterface iface(kGameCenterService, kGameCenterPath, kGameCenterInterface, bus); home + QStringLiteral("/.local/share/Steam"),
if (!iface.isValid()) { home + QStringLiteral("/.var/app/com.valvesoftware.Steam/data/Steam"),
return false; home + QStringLiteral("/.var/app/com.valvesoftware.Steam/.local/share/Steam"),
} };
iface.setTimeout(2000);
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
return reply.isValid() && reply.value() == QLatin1String("ok");
}
static void disconnectDaemonSignals(QDBusConnection bus, GameLauncher *launcher)
{
if (!bus.isConnected()) {
return;
}
bus.disconnect(kGameCenterService,
kGameCenterPath,
kGameCenterInterface,
QStringLiteral("SessionAdded"),
launcher,
SLOT(onDaemonSessionAdded(QVariantMap)));
bus.disconnect(kGameCenterService,
kGameCenterPath,
kGameCenterInterface,
QStringLiteral("SessionChanged"),
launcher,
SLOT(onDaemonSessionChanged(QVariantMap)));
bus.disconnect(kGameCenterService,
kGameCenterPath,
kGameCenterInterface,
QStringLiteral("SessionRemoved"),
launcher,
SLOT(onDaemonSessionRemoved(QString, QVariantMap)));
bus.disconnect(kGameCenterService,
kGameCenterPath,
kGameCenterInterface,
QStringLiteral("LaunchFailed"),
launcher,
SLOT(onDaemonLaunchFailed(QVariantMap)));
}
static void connectDaemonSignals(QDBusConnection bus, GameLauncher *launcher)
{
if (!bus.isConnected()) {
return;
}
bus.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("SessionAdded"), launcher, SLOT(onDaemonSessionAdded(QVariantMap)));
bus.connect(kGameCenterService,
kGameCenterPath,
kGameCenterInterface,
QStringLiteral("SessionChanged"),
launcher,
SLOT(onDaemonSessionChanged(QVariantMap)));
bus.connect(kGameCenterService,
kGameCenterPath,
kGameCenterInterface,
QStringLiteral("SessionRemoved"),
launcher,
SLOT(onDaemonSessionRemoved(QString, QVariantMap)));
bus.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("LaunchFailed"), launcher, SLOT(onDaemonLaunchFailed(QVariantMap)));
} }
static QVariant unwrapDbusVariant(QVariant v) static QVariant unwrapDbusVariant(QVariant v)
@ -122,59 +48,6 @@ static QVariantMap unwrapVariantMap(const QVariantMap &map)
return out; return out;
} }
static bool tryResolveWithRunnerManager(const QVariantMap &spec, QVariantMap &out)
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected() || !bus.interface()) {
return false;
}
if (!bus.interface()->isServiceRegistered(kRunnerService)) {
bus.interface()->startService(kRunnerService);
}
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
if (!iface.isValid()) {
if (bus.interface()->startService(kRunnerService).isValid()) {
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
if (retryIface.isValid()) {
retryIface.setTimeout(2000);
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
if (retryReply.isValid()) {
out = unwrapVariantMap(retryReply.value());
return true;
}
}
}
return false;
}
iface.setTimeout(2000);
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
if (!reply.isValid()) {
if (reply.error().type() == QDBusError::ServiceUnknown) {
bus.interface()->startService(kRunnerService);
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
if (!retryIface.isValid()) {
return false;
}
retryIface.setTimeout(2000);
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
if (!retryReply.isValid()) {
return false;
}
out = unwrapVariantMap(retryReply.value());
return true;
}
return false;
}
out = unwrapVariantMap(reply.value());
return true;
}
static QStringList steamCandidateRoots();
static QString findSteamClientInstallPathFromProton(const QString &protonExe) static QString findSteamClientInstallPathFromProton(const QString &protonExe)
{ {
if (protonExe.isEmpty()) { if (protonExe.isEmpty()) {
@ -257,23 +130,40 @@ static QString discoverDefaultProtonExecutable()
return cached; return cached;
} }
static QStringList steamCandidateRoots()
{
const QString home = QDir::homePath();
return {
home + QStringLiteral("/.steam/root"),
home + QStringLiteral("/.steam/steam"),
home + QStringLiteral("/.local/share/Steam"),
home + QStringLiteral("/.var/app/com.valvesoftware.Steam/data/Steam"),
home + QStringLiteral("/.var/app/com.valvesoftware.Steam/.local/share/Steam"),
};
}
GameLauncher::GameLauncher(QObject *parent) GameLauncher::GameLauncher(QObject *parent)
: QObject(parent) : QObject(parent)
, m_daemonAvailable(false)
, m_usingSystemBus(false)
{ {
QDBusConnection bus = QDBusConnection::sessionBus();
if (bus.isConnected()) {
bus.connect(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("SessionAdded"),
this,
SLOT(onDaemonSessionAdded(QVariantMap)));
bus.connect(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("SessionChanged"),
this,
SLOT(onDaemonSessionChanged(QVariantMap)));
bus.connect(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("SessionRemoved"),
this,
SLOT(onDaemonSessionRemoved(QString, QVariantMap)));
bus.connect(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("LaunchFailed"),
this,
SLOT(onDaemonLaunchFailed(QVariantMap)));
}
if (auto *app = qobject_cast<App *>(parent)) { if (auto *app = qobject_cast<App *>(parent)) {
connect(app->gameModel(), &GameModel::countChanged, this, [this]() { connect(app->gameModel(), &GameModel::countChanged, this, [this]() {
QTimer::singleShot(0, this, [this]() { QTimer::singleShot(0, this, [this]() {
@ -332,14 +222,12 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
} }
const QString runner = game->launchRunner().trimmed(); const QString runner = game->launchRunner().trimmed();
const QString runnerId = game->launchRunnerId().trimmed();
const QString runnerPath = game->launchRunnerPath().trimmed(); const QString runnerPath = game->launchRunnerPath().trimmed();
const QString prefixPath = game->launchPrefixPath().trimmed(); const QString prefixPath = game->launchPrefixPath().trimmed();
const QVariantMap launchEnv = game->launchEnv(); const QVariantMap launchEnv = game->launchEnv();
const bool hasLaunchOverrides = !runner.isEmpty() || !runnerId.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !launchEnv.isEmpty(); const bool hasLaunchOverrides = !runner.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !launchEnv.isEmpty();
info.insert(QStringLiteral("runner"), runner); info.insert(QStringLiteral("runner"), runner);
info.insert(QStringLiteral("runnerId"), runnerId);
info.insert(QStringLiteral("runnerPath"), runnerPath); info.insert(QStringLiteral("runnerPath"), runnerPath);
info.insert(QStringLiteral("prefixPath"), prefixPath); info.insert(QStringLiteral("prefixPath"), prefixPath);
info.insert(QStringLiteral("workingDirectory"), game->workingDirectory()); info.insert(QStringLiteral("workingDirectory"), game->workingDirectory());
@ -399,47 +287,6 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
return info; return info;
} }
if (!runnerId.isEmpty() || runner == QLatin1String("wine") || runner == QLatin1String("proton")) {
QVariantMap runnerSpec = {
{QStringLiteral("runnerId"), runnerId},
{QStringLiteral("runner"), runner},
{QStringLiteral("runnerPath"), runnerPath},
{QStringLiteral("gameId"), game->id()},
{QStringLiteral("prefixPath"), prefixPath},
{QStringLiteral("program"), program},
{QStringLiteral("args"), parts},
{QStringLiteral("envOverrides"), envOverrides},
};
QVariantMap resolved;
if (tryResolveWithRunnerManager(runnerSpec, resolved)) {
if (!resolved.value(QStringLiteral("ok")).toBool()) {
info.insert(QStringLiteral("error"), resolved.value(QStringLiteral("error")).toString());
return info;
}
info.insert(QStringLiteral("finalProgram"), resolved.value(QStringLiteral("finalProgram")).toString());
info.insert(QStringLiteral("finalArgs"), resolved.value(QStringLiteral("finalArgs")).toStringList());
info.insert(QStringLiteral("effectiveEnv"), resolved.value(QStringLiteral("effectiveEnv")).toMap());
const QString resolvedPrefixPath = resolved.value(QStringLiteral("resolvedPrefixPath")).toString();
if (!resolvedPrefixPath.isEmpty()) {
info.insert(QStringLiteral("resolvedPrefixPath"), resolvedPrefixPath);
}
const QString resolvedSteamInstallPath = resolved.value(QStringLiteral("resolvedSteamInstallPath")).toString();
if (!resolvedSteamInstallPath.isEmpty()) {
info.insert(QStringLiteral("resolvedSteamInstallPath"), resolvedSteamInstallPath);
}
info.insert(QStringLiteral("ok"), true);
return info;
} else if (!runnerId.isEmpty()) {
info.insert(QStringLiteral("error"), tr("Runner service is not available"));
return info;
}
}
if (runner == QLatin1String("custom")) { if (runner == QLatin1String("custom")) {
if (runnerPath.isEmpty()) { if (runnerPath.isEmpty()) {
info.insert(QStringLiteral("error"), tr("Custom runner is enabled but no runner path is configured")); info.insert(QStringLiteral("error"), tr("Custom runner is enabled but no runner path is configured"));
@ -507,6 +354,7 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
} }
void GameLauncher::launchGame(Game *game) void GameLauncher::launchGame(Game *game)
{ {
if (!game) { if (!game) {
return; return;
@ -528,12 +376,7 @@ void GameLauncher::launchGame(Game *game)
const QString launchType = info.value(QStringLiteral("launchType")).toString(); const QString launchType = info.value(QStringLiteral("launchType")).toString();
const QString provider = info.value(QStringLiteral("provider")).toString(); const QString provider = info.value(QStringLiteral("provider")).toString();
const QString runner = info.value(QStringLiteral("runner")).toString(); if (info.value(QStringLiteral("runner")).toString() == QLatin1String("proton")) {
const QString runnerId = info.value(QStringLiteral("runnerId")).toString();
const QString runnerPath = info.value(QStringLiteral("runnerPath")).toString();
const QString prefixPath = info.value(QStringLiteral("prefixPath")).toString();
const QVariantMap envOverrides = info.value(QStringLiteral("envOverrides")).toMap();
if (runner == QLatin1String("proton") || runner == QLatin1String("wine")) {
const QString resolvedPrefixPath = info.value(QStringLiteral("resolvedPrefixPath")).toString(); const QString resolvedPrefixPath = info.value(QStringLiteral("resolvedPrefixPath")).toString();
if (!resolvedPrefixPath.isEmpty()) { if (!resolvedPrefixPath.isEmpty()) {
QDir().mkpath(resolvedPrefixPath); QDir().mkpath(resolvedPrefixPath);
@ -547,10 +390,10 @@ void GameLauncher::launchGame(Game *game)
// Always try daemon first — for all launch types // Always try daemon first — for all launch types
{ {
QDBusInterface iface(kGameCenterService, QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
kGameCenterPath, QStringLiteral("/org/kde/GameCenter1"),
kGameCenterInterface, QStringLiteral("org.kde.GameCenter1"),
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus()); QDBusConnection::sessionBus());
QVariantMap launchSpec = { QVariantMap launchSpec = {
{QStringLiteral("command"), launchCommand}, {QStringLiteral("command"), launchCommand},
{QStringLiteral("gameId"), game->id()}, {QStringLiteral("gameId"), game->id()},
@ -558,26 +401,6 @@ void GameLauncher::launchGame(Game *game)
{QStringLiteral("provider"), provider}, {QStringLiteral("provider"), provider},
{QStringLiteral("origin"), QStringLiteral("ui")}, {QStringLiteral("origin"), QStringLiteral("ui")},
}; };
if (!runnerId.isEmpty()) {
launchSpec.insert(QStringLiteral("runnerId"), runnerId);
}
if (!runner.isEmpty()) {
launchSpec.insert(QStringLiteral("runner"), runner);
}
if (!runnerPath.isEmpty()) {
launchSpec.insert(QStringLiteral("runnerPath"), runnerPath);
}
if (!prefixPath.isEmpty()) {
launchSpec.insert(QStringLiteral("prefixPath"), prefixPath);
}
launchSpec.insert(QStringLiteral("requestedProgram"), info.value(QStringLiteral("program")).toString());
launchSpec.insert(QStringLiteral("requestedArgs"), info.value(QStringLiteral("args")).toStringList());
if (!envOverrides.isEmpty()) {
launchSpec.insert(QStringLiteral("requestedEnvOverrides"), envOverrides);
}
if (!finalProgram.isEmpty()) { if (!finalProgram.isEmpty()) {
launchSpec.insert(QStringLiteral("program"), finalProgram); launchSpec.insert(QStringLiteral("program"), finalProgram);
launchSpec.insert(QStringLiteral("args"), finalArgs); launchSpec.insert(QStringLiteral("args"), finalArgs);
@ -604,8 +427,6 @@ void GameLauncher::launchGame(Game *game)
return; return;
} }
const QString launchError = reply.isValid() ? QString() : reply.error().message();
// Daemon call failed — fallback for URL-type commands only // Daemon call failed — fallback for URL-type commands only
if (launchType == QLatin1String("url")) { if (launchType == QLatin1String("url")) {
const QString url = info.value(QStringLiteral("url")).toString(); const QString url = info.value(QStringLiteral("url")).toString();
@ -618,12 +439,8 @@ void GameLauncher::launchGame(Game *game)
} }
// No fallback for non-URL commands — emit error // No fallback for non-URL commands — emit error
if (!launchError.isEmpty()) {
Q_EMIT gameError(game, tr("Game Center launch failed: %1").arg(launchError));
} else {
Q_EMIT gameError(game, tr("Game Center daemon is not available")); Q_EMIT gameError(game, tr("Game Center daemon is not available"));
} }
}
} }
void GameLauncher::stopGame(Game *game) void GameLauncher::stopGame(Game *game)
@ -632,10 +449,10 @@ void GameLauncher::stopGame(Game *game)
return; return;
} }
QDBusInterface iface(kGameCenterService, QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
kGameCenterPath, QStringLiteral("/org/kde/GameCenter1"),
kGameCenterInterface, QStringLiteral("org.kde.GameCenter1"),
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus()); QDBusConnection::sessionBus());
iface.call(QStringLiteral("StopByGameId"), game->id()); iface.call(QStringLiteral("StopByGameId"), game->id());
} }
@ -766,10 +583,10 @@ void GameLauncher::onDaemonLaunchFailed(const QVariantMap &error)
void GameLauncher::syncDaemonSessions() void GameLauncher::syncDaemonSessions()
{ {
QDBusInterface iface(kGameCenterService, QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
kGameCenterPath, QStringLiteral("/org/kde/GameCenter1"),
kGameCenterInterface, QStringLiteral("org.kde.GameCenter1"),
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus()); QDBusConnection::sessionBus());
const QDBusReply<QVariantList> reply = iface.call(QStringLiteral("ListSessions")); const QDBusReply<QVariantList> reply = iface.call(QStringLiteral("ListSessions"));
if (!reply.isValid()) { if (!reply.isValid()) {
@ -806,28 +623,13 @@ void GameLauncher::applyRunningStateToLibrary()
void GameLauncher::checkDaemonAvailability() void GameLauncher::checkDaemonAvailability()
{ {
const bool systemAvailable = pingDaemon(QDBusConnection::systemBus()); QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
const bool sessionAvailable = systemAvailable ? false : pingDaemon(QDBusConnection::sessionBus()); QStringLiteral("/org/kde/GameCenter1"),
const bool available = systemAvailable || sessionAvailable; QStringLiteral("org.kde.GameCenter1"),
const bool useSystemBus = systemAvailable; QDBusConnection::sessionBus());
disconnectDaemonSignals(QDBusConnection::systemBus(), this);
disconnectDaemonSignals(QDBusConnection::sessionBus(), this);
if (available) {
connectDaemonSignals(useSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus(), this);
}
const bool busChanged = m_usingSystemBus != useSystemBus;
if (busChanged) {
m_daemonGameToSession.clear();
m_daemonSessionToGame.clear();
Q_EMIT runningGamesChanged();
applyRunningStateToLibrary();
}
m_usingSystemBus = useSystemBus;
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
const bool available = reply.isValid() && reply.value() == QLatin1String("ok");
if (available != m_daemonAvailable) { if (available != m_daemonAvailable) {
m_daemonAvailable = available; m_daemonAvailable = available;
Q_EMIT daemonAvailableChanged(); Q_EMIT daemonAvailableChanged();

View file

@ -48,7 +48,6 @@ private:
QHash<QString, QString> m_daemonGameToSession; QHash<QString, QString> m_daemonGameToSession;
QHash<QString, QString> m_daemonSessionToGame; QHash<QString, QString> m_daemonSessionToGame;
bool m_daemonAvailable = false; bool m_daemonAvailable = false;
bool m_usingSystemBus = false;
void checkDaemonAvailability(); void checkDaemonAvailability();
void syncDaemonSessions(); void syncDaemonSessions();

View file

@ -17,26 +17,6 @@
K_PLUGIN_CLASS_WITH_JSON(AlakarteRunner, "plasma-runner-alakarte.json") K_PLUGIN_CLASS_WITH_JSON(AlakarteRunner, "plasma-runner-alakarte.json")
static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1");
static const QString kGameCenterPath = QStringLiteral("/org/kde/GameCenter1");
static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
static bool launchViaDaemon(QDBusConnection bus, const QVariantMap &launchSpec)
{
if (!bus.isConnected()) {
return false;
}
QDBusInterface iface(kGameCenterService, kGameCenterPath, kGameCenterInterface, bus);
if (!iface.isValid()) {
return false;
}
iface.setTimeout(2000);
const QDBusReply<QString> reply = iface.call(QStringLiteral("Launch"), launchSpec);
return reply.isValid() && !reply.value().isEmpty();
}
AlakarteRunner::AlakarteRunner(QObject *parent, const KPluginMetaData &metaData) AlakarteRunner::AlakarteRunner(QObject *parent, const KPluginMetaData &metaData)
: KRunner::AbstractRunner(parent, metaData) : KRunner::AbstractRunner(parent, metaData)
{ {
@ -174,6 +154,10 @@ void AlakarteRunner::run(const KRunner::RunnerContext &context, const KRunner::Q
// Always try daemon first for all commands (including Steam/Lutris) // Always try daemon first for all commands (including Steam/Lutris)
if (!match.id().isEmpty()) { if (!match.id().isEmpty()) {
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QDBusConnection::sessionBus());
QVariantMap launchSpec = { QVariantMap launchSpec = {
{QStringLiteral("command"), command}, {QStringLiteral("command"), command},
{QStringLiteral("gameId"), match.id()}, {QStringLiteral("gameId"), match.id()},
@ -181,7 +165,8 @@ void AlakarteRunner::run(const KRunner::RunnerContext &context, const KRunner::Q
{QStringLiteral("origin"), QStringLiteral("krunner")}, {QStringLiteral("origin"), QStringLiteral("krunner")},
}; };
if (launchViaDaemon(QDBusConnection::systemBus(), launchSpec) || launchViaDaemon(QDBusConnection::sessionBus(), launchSpec)) { const QDBusReply<QString> reply = iface.call(QStringLiteral("Launch"), launchSpec);
if (reply.isValid() && !reply.value().isEmpty()) {
return; return;
} }
} }

View file

@ -97,18 +97,6 @@ Kirigami.OverlaySheet {
wrapMode: Text.WrapAnywhere wrapMode: Text.WrapAnywhere
} }
QQC2.Label {
Kirigami.FormData.label: i18n("Runner ID:")
text: launchInfo && launchInfo.runnerId ? launchInfo.runnerId : i18n("None")
wrapMode: Text.WrapAnywhere
}
QQC2.Label {
Kirigami.FormData.label: i18n("Runner path:")
text: launchInfo && launchInfo.runnerPath ? launchInfo.runnerPath : i18n("Not specified")
wrapMode: Text.WrapAnywhere
}
QQC2.Label { QQC2.Label {
Kirigami.FormData.label: i18n("Program:") Kirigami.FormData.label: i18n("Program:")
text: launchInfo && launchInfo.finalProgram ? launchInfo.finalProgram : "" text: launchInfo && launchInfo.finalProgram ? launchInfo.finalProgram : ""
@ -132,12 +120,6 @@ Kirigami.OverlaySheet {
text: launchInfo && launchInfo.resolvedPrefixPath ? launchInfo.resolvedPrefixPath : (launchInfo && launchInfo.prefixPath ? launchInfo.prefixPath : "") text: launchInfo && launchInfo.resolvedPrefixPath ? launchInfo.resolvedPrefixPath : (launchInfo && launchInfo.prefixPath ? launchInfo.prefixPath : "")
wrapMode: Text.WrapAnywhere wrapMode: Text.WrapAnywhere
} }
QQC2.Label {
Kirigami.FormData.label: i18n("Steam install path:")
text: launchInfo && launchInfo.resolvedSteamInstallPath ? launchInfo.resolvedSteamInstallPath : i18n("Not applicable")
wrapMode: Text.WrapAnywhere
}
} }
} }
} }

View file

@ -19,10 +19,6 @@ Kirigami.Dialog {
id: envModel id: envModel
} }
ListModel {
id: runnerChoicesModel
}
signal gameCreated(var game) signal gameCreated(var game)
signal gameUpdated(var game) signal gameUpdated(var game)
@ -39,35 +35,14 @@ Kirigami.Dialog {
Kirigami.Action { Kirigami.Action {
text: isEditing ? i18n("Apply") : i18n("Add") text: isEditing ? i18n("Apply") : i18n("Add")
icon.name: isEditing ? "dialog-ok-apply" : "list-add" icon.name: isEditing ? "dialog-ok-apply" : "list-add"
enabled: nameField.text.trim() !== "" && executableField.text.trim() !== "" && (dialog.currentRunnerChoiceKind() !== "custom" || runnerPathField.text.trim() !== "") enabled: nameField.text.trim() !== "" && executableField.text.trim() !== "" && (runnerCombo.currentIndex !== 3 || runnerPathField.text.trim() !== "")
onTriggered: { onTriggered: {
let choice = dialog.currentRunnerChoice() let runnerValue = dialog.runnerFromIndex(runnerCombo.currentIndex)
let runnerKind = choice && choice.kind ? String(choice.kind) : "native"
let runnerType = choice && choice.runnerType ? String(choice.runnerType) : (choice && choice.runner ? String(choice.runner) : "")
let runnerValue = choice && choice.runner ? String(choice.runner) : ""
let runnerIdValue = choice && choice.runnerId ? String(choice.runnerId) : ""
let runnerPathValue = runnerPathField.text.trim() let runnerPathValue = runnerPathField.text.trim()
let prefixPathValue = prefixPathField.text.trim() let prefixPathValue = prefixPathField.text.trim()
if (runnerValue === "") {
if (runnerKind === "native") {
runnerValue = ""
runnerIdValue = ""
runnerPathValue = "" runnerPathValue = ""
prefixPathValue = "" prefixPathValue = ""
} else if (runnerKind === "inventory") {
runnerValue = ""
runnerPathValue = ""
} else if (runnerKind === "wine" || runnerKind === "proton") {
runnerIdValue = ""
} else if (runnerKind === "custom") {
runnerIdValue = ""
prefixPathValue = ""
}
if (runnerType !== "wine" && runnerType !== "proton") {
prefixPathValue = ""
} }
if (isEditing) { if (isEditing) {
game.name = nameField.text.trim() game.name = nameField.text.trim()
@ -76,7 +51,6 @@ Kirigami.Dialog {
game.workingDirectory = workingDirField.text.trim() game.workingDirectory = workingDirField.text.trim()
game.launchEnv = dialog.envModelToMap() game.launchEnv = dialog.envModelToMap()
game.launchRunner = runnerValue game.launchRunner = runnerValue
game.launchRunnerId = runnerIdValue
game.launchRunnerPath = runnerPathValue game.launchRunnerPath = runnerPathValue
game.launchPrefixPath = prefixPathValue game.launchPrefixPath = prefixPathValue
if (selectedCoverPath !== "") { if (selectedCoverPath !== "") {
@ -91,7 +65,6 @@ Kirigami.Dialog {
newGame.workingDirectory = workingDirField.text.trim() newGame.workingDirectory = workingDirField.text.trim()
newGame.launchEnv = dialog.envModelToMap() newGame.launchEnv = dialog.envModelToMap()
newGame.launchRunner = runnerValue newGame.launchRunner = runnerValue
newGame.launchRunnerId = runnerIdValue
newGame.launchRunnerPath = runnerPathValue newGame.launchRunnerPath = runnerPathValue
newGame.launchPrefixPath = prefixPathValue newGame.launchPrefixPath = prefixPathValue
if (selectedCoverPath !== "") { if (selectedCoverPath !== "") {
@ -113,24 +86,7 @@ Kirigami.Dialog {
property string selectedCoverPath: "" property string selectedCoverPath: ""
readonly property bool anyConfirmOpen: !!(deletePrefixConfirmDialog && deletePrefixConfirmDialog.opened) readonly property bool anyMenuOpen: runnerCombo && runnerCombo.popup && runnerCombo.popup.visible
readonly property bool anyMenuOpen: !!(runnerCombo && runnerCombo.popup && runnerCombo.popup.visible)
function currentConfirmDialog() {
if (deletePrefixConfirmDialog && deletePrefixConfirmDialog.opened) return deletePrefixConfirmDialog
return null
}
function closeCurrentConfirmDialog() {
let d = currentConfirmDialog()
if (!d) return
if (typeof d.reject === "function") {
d.reject()
} else {
d.close()
}
}
function closeCurrentMenu() { function closeCurrentMenu() {
if (runnerCombo && runnerCombo.popup && runnerCombo.popup.visible) { if (runnerCombo && runnerCombo.popup && runnerCombo.popup.visible) {
@ -225,84 +181,16 @@ Kirigami.Dialog {
return 0 return 0
} }
function currentRunnerChoice() {
if (!runnerChoicesModel || runnerChoicesModel.count <= 0) return null
let idx = runnerCombo ? runnerCombo.currentIndex : 0
if (idx < 0 || idx >= runnerChoicesModel.count) return runnerChoicesModel.get(0)
return runnerChoicesModel.get(idx)
}
function currentRunnerChoiceKind() {
let c = dialog.currentRunnerChoice()
if (!c || !c.kind) return "native"
return String(c.kind)
}
function currentRunnerChoiceRunnerType() {
let c = dialog.currentRunnerChoice()
if (!c) return ""
if (c.runnerType) return String(c.runnerType)
if (c.runner) return String(c.runner)
return ""
}
function rebuildRunnerChoices() {
runnerChoicesModel.clear()
runnerChoicesModel.append({ kind: "native", label: i18n("Native"), runner: "", runnerId: "", runnerType: "" })
runnerChoicesModel.append({ kind: "wine", label: i18n("Wine (system)"), runner: "wine", runnerId: "", runnerType: "wine" })
runnerChoicesModel.append({ kind: "proton", label: i18n("Proton (auto)"), runner: "proton", runnerId: "", runnerType: "proton" })
let runners = (App.runnerManager && App.runnerManager.runners) ? App.runnerManager.runners : []
for (let i = 0; i < runners.length; i++) {
let r = runners[i]
if (!r || !r.id) continue
let name = r.name ? String(r.name) : String(r.id)
let source = r.source ? String(r.source) : ""
let type = r.type ? String(r.type) : ""
let label = name
if (type !== "" || source !== "") {
label = name + " (" + type + (source !== "" ? (" · " + source) : "") + ")"
}
runnerChoicesModel.append({ kind: "inventory", label: label, runner: "", runnerId: String(r.id), runnerType: type })
}
runnerChoicesModel.append({ kind: "custom", label: i18n("Custom runner path"), runner: "custom", runnerId: "", runnerType: "custom" })
}
function indexForRunnerId(runnerId) {
let id = (runnerId || "").trim()
if (id === "") return 0
for (let i = 0; i < runnerChoicesModel.count; i++) {
let row = runnerChoicesModel.get(i)
if (row.kind === "inventory" && String(row.runnerId || "") === id) return i
}
return 0
}
function indexForRunner(runner) {
let r = (runner || "").trim().toLowerCase()
if (r === "wine") return 1
if (r === "proton") return 2
if (r === "custom") return Math.max(0, runnerChoicesModel.count - 1)
return 0
}
function loadFields() { function loadFields() {
selectedCoverPath = "" selectedCoverPath = ""
envModel.clear() envModel.clear()
dialog.rebuildRunnerChoices()
if (isEditing && game) { if (isEditing && game) {
nameField.text = game.name || "" nameField.text = game.name || ""
developerField.text = game.developer || "" developerField.text = game.developer || ""
executableField.text = game.launchCommand || "" executableField.text = game.launchCommand || ""
workingDirField.text = game.workingDirectory || "" workingDirField.text = game.workingDirectory || ""
let idx = dialog.indexForRunnerId(game.launchRunnerId) runnerCombo.currentIndex = dialog.runnerToIndex(game.launchRunner)
if (idx === 0) {
idx = dialog.indexForRunner(game.launchRunner)
}
runnerCombo.currentIndex = idx
runnerPathField.text = game.launchRunnerPath || "" runnerPathField.text = game.launchRunnerPath || ""
prefixPathField.text = game.launchPrefixPath || "" prefixPathField.text = game.launchPrefixPath || ""
@ -325,66 +213,6 @@ Kirigami.Dialog {
} }
} }
Connections {
target: App.runnerManager
function onRunnersChanged() {
if (!dialog.opened) return
let desiredRunnerId = ""
if (isEditing && game) {
desiredRunnerId = String(game.launchRunnerId || "").trim()
}
let current = dialog.currentRunnerChoice()
let currentRunnerId = current && current.runnerId ? String(current.runnerId) : ""
let currentRunner = current && current.runner ? String(current.runner) : ""
dialog.rebuildRunnerChoices()
if (desiredRunnerId !== "") {
runnerCombo.currentIndex = dialog.indexForRunnerId(desiredRunnerId)
} else if (currentRunnerId !== "") {
runnerCombo.currentIndex = dialog.indexForRunnerId(currentRunnerId)
} else {
runnerCombo.currentIndex = dialog.indexForRunner(currentRunner)
}
}
function onPrefixEnsured(gameId, prefixPath) {
if (!dialog.opened) return
if (!isEditing || !game) return
if (String(gameId) !== String(game.id)) return
prefixPathField.text = String(prefixPath || "")
}
function onPrefixDeleted(gameId, prefixPath) {
if (!dialog.opened) return
if (!isEditing || !game) return
if (String(gameId) !== String(game.id)) return
prefixPathField.text = ""
}
}
function effectivePrefixPath() {
let p = prefixPathField.text.trim()
if (p !== "") return p
if (!isEditing || !game) return ""
return StandardPaths.writableLocation(StandardPaths.AppDataLocation) + "/prefixes/" + game.id
}
function toFileUrl(path) {
let p = String(path || "").trim()
if (p === "") return ""
if (p.startsWith("~/")) {
p = StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + p.substring(2)
}
if (p.startsWith("file:")) return p
if (p.startsWith("/")) {
return "file:///" + encodeURI(p.substring(1))
}
return "file:///" + encodeURI(p)
}
Connections { Connections {
target: GamepadManager target: GamepadManager
function onNavigateUp() { function onNavigateUp() {
@ -497,11 +325,26 @@ Kirigami.Dialog {
title: i18n("Compatibility") title: i18n("Compatibility")
} }
FormCard.FormComboBoxDelegate { Item {
id: runnerCombo Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
text: i18n("Runner") text: i18n("Runner")
model: runnerChoicesModel Layout.alignment: Qt.AlignVCenter
textRole: "label" }
QQC2.ComboBox {
id: runnerCombo
Layout.fillWidth: true
model: [i18n("Native"), i18n("Wine"), i18n("Proton"), i18n("Custom")]
}
}
} }
FormCard.FormDelegateSeparator {} FormCard.FormDelegateSeparator {}
@ -510,8 +353,8 @@ Kirigami.Dialog {
id: runnerPathField id: runnerPathField
label: i18n("Runner Path") label: i18n("Runner Path")
text: "" text: ""
placeholderText: dialog.currentRunnerChoiceKind() === "proton" ? i18n("Auto-detect Proton") : (dialog.currentRunnerChoiceKind() === "wine" ? i18n("Use system Wine") : i18n("Required for Custom")) placeholderText: runnerCombo.currentIndex === 2 ? i18n("Auto-detect Proton") : (runnerCombo.currentIndex === 1 ? i18n("Use system Wine") : i18n("Required for Custom"))
enabled: dialog.currentRunnerChoiceKind() === "custom" || dialog.currentRunnerChoiceKind() === "wine" || dialog.currentRunnerChoiceKind() === "proton" enabled: runnerCombo.currentIndex !== 0
} }
FormCard.FormDelegateSeparator {} FormCard.FormDelegateSeparator {}
@ -519,7 +362,7 @@ Kirigami.Dialog {
FormCard.FormButtonDelegate { FormCard.FormButtonDelegate {
text: i18n("Browse Runner...") text: i18n("Browse Runner...")
icon.name: "document-open" icon.name: "document-open"
enabled: dialog.currentRunnerChoiceKind() === "custom" || dialog.currentRunnerChoiceKind() === "wine" || dialog.currentRunnerChoiceKind() === "proton" enabled: runnerCombo.currentIndex !== 0
onClicked: runnerFileDialog.open() onClicked: runnerFileDialog.open()
} }
@ -530,7 +373,7 @@ Kirigami.Dialog {
label: i18n("Prefix Path") label: i18n("Prefix Path")
text: "" text: ""
placeholderText: i18n("Default (AppDataLocation/prefixes/<gameId>)") placeholderText: i18n("Default (AppDataLocation/prefixes/<gameId>)")
enabled: dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton" enabled: runnerCombo.currentIndex === 1 || runnerCombo.currentIndex === 2
} }
FormCard.FormDelegateSeparator {} FormCard.FormDelegateSeparator {}
@ -538,52 +381,9 @@ Kirigami.Dialog {
FormCard.FormButtonDelegate { FormCard.FormButtonDelegate {
text: i18n("Browse Prefix...") text: i18n("Browse Prefix...")
icon.name: "document-open-folder" icon.name: "document-open-folder"
enabled: dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton" enabled: runnerCombo.currentIndex === 1 || runnerCombo.currentIndex === 2
onClicked: prefixFolderDialog.open() onClicked: prefixFolderDialog.open()
} }
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Open Prefix Folder")
icon.name: "folder-open"
enabled: isEditing && (dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton")
onClicked: {
let p = dialog.effectivePrefixPath()
if (p === "") return
Qt.openUrlExternally(dialog.toFileUrl(p))
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Delete Prefix")
icon.name: "edit-delete"
enabled: isEditing
&& (dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton")
&& (prefixPathField.text.trim() === "")
onClicked: deletePrefixConfirmDialog.open()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Create Prefix")
icon.name: "folder-new"
enabled: isEditing
&& (dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton")
onClicked: App.runnerManager.ensurePrefix(game.id, dialog.currentRunnerChoiceRunnerType(), prefixPathField.text.trim())
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Reset Prefix to Default")
icon.name: "edit-clear"
enabled: dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton"
onClicked: prefixPathField.text = ""
}
} }
FormCard.FormCard { FormCard.FormCard {
@ -635,9 +435,13 @@ Kirigami.Dialog {
onClicked: envModel.clear() onClicked: envModel.clear()
} }
ColumnLayout { Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing Layout.margins: Kirigami.Units.largeSpacing
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
spacing: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
Repeater { Repeater {
@ -672,6 +476,7 @@ Kirigami.Dialog {
} }
} }
} }
}
FormCard.FormCard { FormCard.FormCard {
Layout.fillWidth: true Layout.fillWidth: true
@ -825,35 +630,9 @@ Kirigami.Dialog {
} }
onOpened: { onOpened: {
App.runnerManager.refreshRunners()
loadFields() loadFields()
nameField.forceActiveFocus() nameField.forceActiveFocus()
} }
onGameChanged: loadFields() onGameChanged: loadFields()
Kirigami.PromptDialog {
id: deletePrefixConfirmDialog
title: i18n("Delete Prefix")
subtitle: (isEditing && game)
? i18n("Delete the prefix for '%1'? This will remove the entire prefix directory.", game.name)
: i18n("Delete this prefix? This will remove the entire prefix directory.")
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
onAccepted: {
if (!isEditing || !game) return
App.runnerManager.deletePrefix(game.id, prefixPathField.text.trim())
}
onOpened: {
Qt.callLater(function() {
if (typeof deletePrefixConfirmDialog.standardButton === "function") {
let noButton = deletePrefixConfirmDialog.standardButton(Kirigami.Dialog.No)
if (noButton) {
noButton.forceActiveFocus()
return
}
}
deletePrefixConfirmDialog.forceActiveFocus()
})
}
}
} }

View file

@ -102,7 +102,6 @@ Kirigami.ApplicationWindow {
function currentConfirmDialog() { function currentConfirmDialog() {
if (removeGameConfirmDialog.opened) return removeGameConfirmDialog if (removeGameConfirmDialog.opened) return removeGameConfirmDialog
if (gameEditDialog.visible && gameEditDialog.anyConfirmOpen) return gameEditDialog.currentConfirmDialog()
let layerContent = root.settingsLayerContentItem() let layerContent = root.settingsLayerContentItem()
if (settingsLayerOpen && layerContent && layerContent.anyConfirmOpen) return layerContent.currentConfirmDialog() if (settingsLayerOpen && layerContent && layerContent.anyConfirmOpen) return layerContent.currentConfirmDialog()
if (settingsSheet.opened && settingsContent.anyConfirmOpen) return settingsContent.currentConfirmDialog() if (settingsSheet.opened && settingsContent.anyConfirmOpen) return settingsContent.currentConfirmDialog()
@ -178,10 +177,6 @@ Kirigami.ApplicationWindow {
return true return true
} }
if (gameEditDialog.visible) { if (gameEditDialog.visible) {
if (gameEditDialog.anyConfirmOpen) {
gameEditDialog.closeCurrentConfirmDialog()
return true
}
if (gameEditDialog.anyMenuOpen) { if (gameEditDialog.anyMenuOpen) {
gameEditDialog.closeCurrentMenu() gameEditDialog.closeCurrentMenu()
return true return true
@ -2046,17 +2041,6 @@ Kirigami.ApplicationWindow {
} }
} }
Connections {
target: App.runnerManager
function onLastErrorChanged() {
if (!App.runnerManager) return
let msg = String(App.runnerManager.lastError || "")
if (msg.trim() === "") return
showPassiveNotification(i18n("Runner error: %1", msg), "long")
}
}
Component.onCompleted: { Component.onCompleted: {
if (!root.isMobile) { if (!root.isMobile) {
const savedW = App.config.windowWidth const savedW = App.config.windowWidth

View file

@ -13,10 +13,6 @@ ColumnLayout {
id: settingsPage id: settingsPage
spacing: Kirigami.Units.mediumSpacing spacing: Kirigami.Units.mediumSpacing
property string runnerInstallUrl: ""
property string runnerInstallSha256: ""
property string runnerInstallName: ""
function focusFirstControl() { function focusFirstControl() {
showPlatformBadgesDelegate.forceActiveFocus() showPlatformBadgesDelegate.forceActiveFocus()
} }
@ -550,165 +546,6 @@ FormCard.FormHeader {
} }
} }
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
title: i18n("Runners")
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormTextFieldDelegate {
label: i18n("Archive URL or local path")
text: settingsPage.runnerInstallUrl
placeholderText: i18n("https://... or /home/.../file.tar.gz")
enabled: !App.runnerManager.busy
onTextChanged: settingsPage.runnerInstallUrl = text
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextFieldDelegate {
label: i18n("SHA256/SHA512 (optional)")
text: settingsPage.runnerInstallSha256
placeholderText: i18n("Leave empty to skip verification")
enabled: !App.runnerManager.busy
onTextChanged: settingsPage.runnerInstallSha256 = text
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextFieldDelegate {
label: i18n("Name (optional)")
text: settingsPage.runnerInstallName
placeholderText: i18n("Proton-GE")
enabled: !App.runnerManager.busy
onTextChanged: settingsPage.runnerInstallName = text
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Install from URL")
description: App.runnerManager.busy ? App.runnerManager.status : i18n("Download and install runner")
icon.name: "download"
enabled: settingsPage.runnerInstallUrl.trim() !== "" && !App.runnerManager.busy
onClicked: App.runnerManager.installRunnerFromUrl(settingsPage.runnerInstallUrl, settingsPage.runnerInstallSha256, settingsPage.runnerInstallName)
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Cancel install")
icon.name: "dialog-cancel"
enabled: App.runnerManager.busy
onClicked: App.runnerManager.cancelCurrentInstall()
}
FormCard.FormDelegateSeparator {}
Item {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
spacing: Kirigami.Units.smallSpacing
QQC2.Label {
Layout.fillWidth: true
text: App.runnerManager.status
wrapMode: Text.WordWrap
visible: App.runnerManager.status !== ""
}
QQC2.Label {
Layout.fillWidth: true
text: App.runnerManager.lastError
wrapMode: Text.WordWrap
visible: App.runnerManager.lastError !== ""
color: Kirigami.Theme.negativeTextColor
}
QQC2.ProgressBar {
Layout.fillWidth: true
from: 0
to: App.runnerManager.totalBytes > 0 ? App.runnerManager.totalBytes : 1
value: App.runnerManager.receivedBytes
indeterminate: App.runnerManager.busy && App.runnerManager.totalBytes <= 0
visible: App.runnerManager.busy
}
}
}
}
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
title: i18n("Installed Runners")
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormButtonDelegate {
text: i18n("Refresh")
icon.name: "view-refresh"
enabled: !App.runnerManager.busy
onClicked: App.runnerManager.refreshRunners()
}
FormCard.FormDelegateSeparator {}
Repeater {
model: App.runnerManager.runners
ColumnLayout {
Layout.fillWidth: true
readonly property var runner: modelData
readonly property string runnerId: (runner && runner.id) ? runner.id : ""
readonly property string runnerName: (runner && runner.name) ? runner.name : runnerId
readonly property string runnerType: (runner && runner.type) ? runner.type : ""
readonly property string runnerSource: (runner && runner.source) ? runner.source : ""
readonly property string runnerPath: (runner && runner.path) ? runner.path : ""
FormCard.FormTextDelegate {
text: runnerName
description: (runnerType !== "" ? (runnerType + " · ") : "") + (runnerSource !== "" ? runnerSource : "")
textItem.wrapMode: Text.WordWrap
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextDelegate {
text: runnerPath
textItem.wrapMode: Text.WrapAnywhere
textItem.font: Kirigami.Theme.smallFont
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Uninstall")
icon.name: "edit-delete"
enabled: runnerSource === "installed" && runnerId !== "" && !App.runnerManager.busy
onClicked: App.runnerManager.uninstallRunner(runnerId)
}
FormCard.FormDelegateSeparator {}
}
}
FormCard.FormTextDelegate {
visible: App.runnerManager.runners.length === 0
text: i18n("No runners found")
textItem.font: Kirigami.Theme.smallFont
}
}
FormCard.FormHeader { FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true Layout.fillWidth: true

View file

@ -1,41 +0,0 @@
# SPDX-License-Identifier: BSD-3-Clause
# SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
add_executable(alakarte_runnerd
main.cpp
runnermanagerdaemon.cpp
runnermanagerdaemon.h
)
target_link_libraries(alakarte_runnerd PRIVATE
Qt6::Core
Qt6::DBus
Qt6::Network
)
set_target_properties(alakarte_runnerd PROPERTIES
OUTPUT_NAME "alakarte-runnerd"
)
install(TARGETS alakarte_runnerd ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd")
configure_file(dbus/org.kde.ALaKarte.Runner1.service.in
"${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.ALaKarte.Runner1.service"
@ONLY
)
configure_file(systemd/org.kde.ALaKarte.Runner1.service.in
"${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.ALaKarte.Runner1.service"
@ONLY
)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.ALaKarte.Runner1.service"
DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}
)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.ALaKarte.Runner1.service"
DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}
)

View file

@ -1,4 +0,0 @@
[D-BUS Service]
Name=org.kde.ALaKarte.Runner1
Exec=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-runnerd
SystemdService=org.kde.ALaKarte.Runner1.service

View file

@ -1,20 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include <QCoreApplication>
#include <QDBusConnection>
#include "runnermanagerdaemon.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QCoreApplication::setApplicationName(QStringLiteral("alakarte"));
RunnerManagerDaemon daemon;
if (!daemon.init()) {
return 1;
}
return app.exec();
}

File diff suppressed because it is too large Load diff

View file

@ -1,84 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#pragma once
#include <QHash>
#include <QObject>
#include <QPointer>
#include <QSet>
#include <QVariantMap>
class QNetworkReply;
class QProcess;
class RunnerManagerDaemon : public QObject
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.kde.ALaKarte.Runner1")
public:
explicit RunnerManagerDaemon(QObject *parent = nullptr);
bool init();
public Q_SLOTS:
QString Ping() const;
void Version(uint &major, uint &minor) const;
QVariantList ListRunners();
bool Rescan();
bool RegisterRunner(const QVariantMap &runner);
bool UnregisterRunner(const QString &runnerId);
QVariantMap InstallRunnerFromArchive(const QVariantMap &spec);
QVariantMap InstallRunnerFromUrl(const QVariantMap &spec);
QVariantMap UninstallRunner(const QString &runnerId);
bool CancelInstall(const QString &installId);
QVariantMap EnsurePrefix(const QVariantMap &spec);
QVariantMap DeletePrefix(const QVariantMap &spec);
QVariantMap ResolveLaunch(const QVariantMap &spec) const;
Q_SIGNALS:
void InstallStarted(const QString &installId, const QVariantMap &spec);
void InstallProgress(const QString &installId, qint64 receivedBytes, qint64 totalBytes);
void InstallFinished(const QString &installId, const QVariantMap &result);
private:
struct RunnerInfo {
QString id;
QString type;
QString name;
QString path;
QString source;
QString installDir;
QVariantMap toVariantMap() const;
static RunnerInfo fromVariantMap(const QVariantMap &map);
bool isValid() const;
};
QString registryPath() const;
void loadRegistry();
bool saveRegistry() const;
QList<RunnerInfo> discoverRunners() const;
QString defaultProtonExecutable() const;
static QStringList steamCandidateRoots();
static QString findSteamClientInstallPathFromProton(const QString &protonExe);
RunnerInfo runnerById(const QString &runnerId) const;
QList<RunnerInfo> m_registered;
QList<RunnerInfo> m_discovered;
QSet<QString> m_activeInstalls;
QSet<QString> m_cancelRequested;
QHash<QString, QPointer<QNetworkReply>> m_installReplies;
QHash<QString, QPointer<QProcess>> m_installProcesses;
};

View file

@ -1,8 +0,0 @@
[Unit]
Description=A-La-Karte Runner Manager
[Service]
ExecStart=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-runnerd
Type=dbus
BusName=org.kde.ALaKarte.Runner1
Restart=no

View file

@ -1,604 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include "runnermanagerclient.h"
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusInterface>
#include <QDBusMetaType>
#include <QDBusPendingCall>
#include <QDBusPendingCallWatcher>
#include <QDBusReply>
#include <QDir>
#include <QFileInfo>
#include <QProcess>
#include <QSharedPointer>
#include <QTimer>
#include <QUrl>
#include <QUuid>
#include <functional>
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1");
static const QString kRunnerInterface = QStringLiteral("org.kde.ALaKarte.Runner1");
static QString stableIdForUrl(const QString &type, const QString &url)
{
const QByteArray digest = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Sha1).toHex();
return type + QStringLiteral(":url:") + QString::fromLatin1(digest);
}
static QString stableIdKeyForInputUrl(QString urlString)
{
urlString = urlString.trimmed();
if (urlString.startsWith(QLatin1String("~/"))) {
urlString = QDir::homePath() + QLatin1Char('/') + urlString.mid(2);
}
QUrl url(urlString);
if (url.isValid() && url.scheme().toLower() == QLatin1String("file")) {
QFileInfo fi(url.toLocalFile());
const QString canonical = fi.canonicalFilePath();
const QString abs = fi.absoluteFilePath();
const QString path = canonical.isEmpty() ? abs : canonical;
return QUrl::fromLocalFile(path).toString(QUrl::FullyEncoded);
}
if (url.scheme().isEmpty()) {
QFileInfo fi(urlString);
if (fi.isAbsolute() || urlString.startsWith(QLatin1String("./")) || urlString.startsWith(QLatin1String("../"))) {
const QString canonical = fi.exists() ? fi.canonicalFilePath() : QString();
const QString abs = fi.absoluteFilePath();
const QString path = canonical.isEmpty() ? abs : canonical;
return QUrl::fromLocalFile(path).toString(QUrl::FullyEncoded);
}
}
if (url.isValid() && !url.scheme().isEmpty()) {
return url.toString(QUrl::FullyEncoded);
}
return urlString;
}
RunnerManagerClient::RunnerManagerClient(QObject *parent)
: QObject(parent)
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (bus.isConnected()) {
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("InstallStarted"), this, SLOT(onInstallStarted(QString, QVariantMap)));
bus.connect(kRunnerService,
kRunnerPath,
kRunnerInterface,
QStringLiteral("InstallProgress"),
this,
SLOT(onInstallProgress(QString, qlonglong, qlonglong)));
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("InstallFinished"), this, SLOT(onInstallFinished(QString, QVariantMap)));
}
if (QCoreApplication::instance()) {
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
shutdownSpawnedRunnerDaemon();
});
}
refreshRunners();
}
RunnerManagerClient::~RunnerManagerClient()
{
shutdownSpawnedRunnerDaemon();
}
void RunnerManagerClient::shutdownSpawnedRunnerDaemon()
{
if (!m_runnerdProcess) {
return;
}
if (m_runnerdProcess->state() == QProcess::NotRunning) {
return;
}
m_runnerdProcess->terminate();
if (!m_runnerdProcess->waitForFinished(1000)) {
m_runnerdProcess->kill();
m_runnerdProcess->waitForFinished(1000);
}
}
void RunnerManagerClient::ensureRunnerDaemon()
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected() || !bus.interface()) {
return;
}
if (bus.interface()->isServiceRegistered(kRunnerService)) {
return;
}
bool activatable = false;
{
const QDBusReply<QStringList> names = bus.interface()->activatableServiceNames();
if (names.isValid()) {
activatable = names.value().contains(kRunnerService);
}
}
const QDBusReply<void> reply = bus.interface()->startService(kRunnerService);
if (reply.isValid()) {
return;
}
if (bus.interface()->isServiceRegistered(kRunnerService)) {
return;
}
if (activatable) {
setLastError(reply.error().message());
return;
}
#if defined(QT_NO_DEBUG)
setLastError(QStringLiteral("Runner service is not available (missing DBus activation for org.kde.ALaKarte.Runner1)"));
return;
#endif
if (m_runnerdSpawnAttempted) {
return;
}
m_runnerdSpawnAttempted = true;
QString program = QCoreApplication::applicationDirPath() + QLatin1String("/alakarte-runnerd");
if (!QFileInfo::exists(program)) {
program = QStringLiteral("alakarte-runnerd");
}
if (!m_runnerdProcess) {
m_runnerdProcess = new QProcess(this);
m_runnerdProcess->setProcessChannelMode(QProcess::MergedChannels);
connect(m_runnerdProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) {
setLastError(QStringLiteral("Failed to start runner service"));
});
connect(m_runnerdProcess, &QProcess::finished, this, [this](int, QProcess::ExitStatus) {
m_runnerdProcess->deleteLater();
m_runnerdProcess = nullptr;
});
}
m_runnerdProcess->setProgram(program);
m_runnerdProcess->setArguments({});
m_runnerdProcess->start();
}
bool RunnerManagerClient::busy() const
{
return m_busy;
}
QString RunnerManagerClient::installId() const
{
return m_installId;
}
qint64 RunnerManagerClient::receivedBytes() const
{
return m_receivedBytes;
}
qint64 RunnerManagerClient::totalBytes() const
{
return m_totalBytes;
}
QString RunnerManagerClient::status() const
{
return m_status;
}
QString RunnerManagerClient::lastError() const
{
return m_lastError;
}
QVariantList RunnerManagerClient::runners() const
{
return m_runners;
}
static QString normalizeHex(QString s)
{
s = s.trimmed().toLower();
const int colon = s.indexOf(QLatin1Char(':'));
if (colon >= 0) {
const QString prefix = s.left(colon);
if (prefix == QLatin1String("sha256") || prefix == QLatin1String("sha512")) {
s = s.mid(colon + 1);
}
}
QString out;
out.reserve(s.size());
for (const QChar &c : s) {
if ((c >= QLatin1Char('0') && c <= QLatin1Char('9')) || (c >= QLatin1Char('a') && c <= QLatin1Char('f'))) {
out.push_back(c);
}
}
return out;
}
void RunnerManagerClient::installRunnerFromUrl(const QString &url, const QString &sha256, const QString &name, const QString &type, bool overwrite)
{
const QString trimmedUrl = url.trimmed();
if (trimmedUrl.isEmpty() || m_busy) {
return;
}
const QString effectiveType = type.trimmed().isEmpty() ? QStringLiteral("proton") : type.trimmed();
setLastError(QString());
setProgress(0, 0);
setStatus(QStringLiteral("Starting..."));
const QString newInstallId = QUuid::createUuid().toString(QUuid::WithoutBraces);
setInstallId(newInstallId);
setBusy(true);
QVariantMap spec;
spec.insert(QStringLiteral("installId"), newInstallId);
spec.insert(QStringLiteral("url"), trimmedUrl);
spec.insert(QStringLiteral("type"), effectiveType);
spec.insert(QStringLiteral("name"), name.trimmed());
spec.insert(QStringLiteral("overwrite"), overwrite);
const QString expected = normalizeHex(sha256);
if (!expected.isEmpty()) {
if (expected.size() == 128) {
spec.insert(QStringLiteral("sha512"), expected);
} else {
spec.insert(QStringLiteral("sha256"), expected);
}
}
const QString stableId = stableIdForUrl(effectiveType, stableIdKeyForInputUrl(trimmedUrl));
spec.insert(QStringLiteral("id"), stableId);
ensureRunnerDaemon();
auto attempts = QSharedPointer<int>::create(0);
auto callPtr = QSharedPointer<std::function<void()>>::create();
*callPtr = [this, spec, attempts, callPtr]() {
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("InstallRunnerFromUrl"), spec);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, spec, attempts, callPtr](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;
w->deleteLater();
if (reply.isError()) {
const QDBusError::ErrorType t = reply.error().type();
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
(*attempts)++;
ensureRunnerDaemon();
QTimer::singleShot(500, this, [callPtr]() {
(*callPtr)();
});
return;
}
setStatus(QStringLiteral("Failed to call InstallRunnerFromUrl"));
setLastError(reply.error().message());
setBusy(false);
return;
}
QVariantMap result = reply.value();
const bool ok = result.value(QStringLiteral("ok")).toBool();
if (!ok) {
const QString err = result.value(QStringLiteral("error")).toString();
setLastError(err);
setStatus(err.isEmpty() ? QStringLiteral("Failed") : err);
setBusy(false);
return;
}
setStatus(QStringLiteral("Started"));
});
};
(*callPtr)();
}
void RunnerManagerClient::deletePrefix(const QString &gameId, const QString &prefixPath)
{
const QString id = gameId.trimmed();
const QString p = prefixPath.trimmed();
if (id.isEmpty()) {
return;
}
ensureRunnerDaemon();
QVariantMap spec;
spec.insert(QStringLiteral("gameId"), id);
if (!p.isEmpty()) {
spec.insert(QStringLiteral("prefixPath"), p);
}
auto attempts = QSharedPointer<int>::create(0);
auto callPtr = QSharedPointer<std::function<void()>>::create();
*callPtr = [this, spec, id, attempts, callPtr]() {
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("DeletePrefix"), spec);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;
w->deleteLater();
if (reply.isError()) {
const QDBusError::ErrorType t = reply.error().type();
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
(*attempts)++;
ensureRunnerDaemon();
QTimer::singleShot(500, this, [callPtr]() {
(*callPtr)();
});
return;
}
setLastError(reply.error().message());
return;
}
const QVariantMap result = reply.value();
if (!result.value(QStringLiteral("ok")).toBool()) {
setLastError(result.value(QStringLiteral("error")).toString());
return;
}
const QString removed = result.value(QStringLiteral("prefixPath")).toString();
Q_EMIT prefixDeleted(id, removed);
});
};
(*callPtr)();
}
void RunnerManagerClient::cancelCurrentInstall()
{
if (!m_busy || m_installId.isEmpty()) {
return;
}
setStatus(QStringLiteral("Cancelling..."));
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
iface.asyncCall(QStringLiteral("CancelInstall"), m_installId);
}
void RunnerManagerClient::onInstallStarted(const QString &installId, const QVariantMap &)
{
if (installId.isEmpty() || installId != m_installId) {
return;
}
setStatus(QStringLiteral("Downloading..."));
}
void RunnerManagerClient::onInstallProgress(const QString &installId, qlonglong received, qlonglong total)
{
if (installId.isEmpty() || installId != m_installId) {
return;
}
setProgress(static_cast<qint64>(received), static_cast<qint64>(total));
}
void RunnerManagerClient::onInstallFinished(const QString &installId, const QVariantMap &result)
{
if (installId.isEmpty() || installId != m_installId) {
return;
}
const bool ok = result.value(QStringLiteral("ok")).toBool();
if (!ok) {
const QString err = result.value(QStringLiteral("error")).toString();
setLastError(err);
setStatus(err.isEmpty() ? QStringLiteral("Failed") : err);
} else {
setStatus(QStringLiteral("Installed"));
}
setBusy(false);
refreshRunners();
}
void RunnerManagerClient::refreshRunners()
{
ensureRunnerDaemon();
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("ListRunners"));
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantList> reply = *w;
w->deleteLater();
if (reply.isError()) {
if (!m_refreshRetryPending && (reply.error().type() == QDBusError::ServiceUnknown || reply.error().type() == QDBusError::NoReply)) {
m_refreshRetryPending = true;
ensureRunnerDaemon();
QTimer::singleShot(500, this, [this]() {
m_refreshRetryPending = false;
refreshRunners();
});
}
return;
}
const QVariantList raw = reply.value();
QVariantList list;
list.reserve(raw.size());
for (const QVariant &v : raw) {
if (v.metaType() == QMetaType::fromType<QVariantMap>()) {
list.push_back(v.toMap());
continue;
}
if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
const QDBusArgument arg = v.value<QDBusArgument>();
const QVariantMap map = qdbus_cast<QVariantMap>(arg);
list.push_back(map);
continue;
}
if (v.canConvert<QVariantMap>()) {
list.push_back(v.toMap());
continue;
}
list.push_back(QVariantMap{});
}
if (m_runners == list) {
return;
}
m_runners = list;
Q_EMIT runnersChanged();
});
}
void RunnerManagerClient::uninstallRunner(const QString &runnerId)
{
const QString id = runnerId.trimmed();
if (id.isEmpty()) {
return;
}
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("UninstallRunner"), id);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;
w->deleteLater();
if (reply.isError()) {
setLastError(reply.error().message());
return;
}
const QVariantMap result = reply.value();
if (!result.value(QStringLiteral("ok")).toBool()) {
setLastError(result.value(QStringLiteral("error")).toString());
}
refreshRunners();
});
}
void RunnerManagerClient::ensurePrefix(const QString &gameId, const QString &runner, const QString &prefixPath)
{
const QString id = gameId.trimmed();
const QString r = runner.trimmed();
const QString p = prefixPath.trimmed();
if (id.isEmpty()) {
return;
}
ensureRunnerDaemon();
QVariantMap spec;
spec.insert(QStringLiteral("gameId"), id);
if (!r.isEmpty()) {
spec.insert(QStringLiteral("runner"), r);
}
if (!p.isEmpty()) {
spec.insert(QStringLiteral("prefixPath"), p);
}
auto attempts = QSharedPointer<int>::create(0);
auto callPtr = QSharedPointer<std::function<void()>>::create();
*callPtr = [this, spec, id, attempts, callPtr]() {
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("EnsurePrefix"), spec);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;
w->deleteLater();
if (reply.isError()) {
const QDBusError::ErrorType t = reply.error().type();
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
(*attempts)++;
ensureRunnerDaemon();
QTimer::singleShot(500, this, [callPtr]() {
(*callPtr)();
});
return;
}
setLastError(reply.error().message());
return;
}
const QVariantMap result = reply.value();
if (!result.value(QStringLiteral("ok")).toBool()) {
setLastError(result.value(QStringLiteral("error")).toString());
return;
}
const QString ensured = result.value(QStringLiteral("prefixPath")).toString();
if (!ensured.isEmpty()) {
Q_EMIT prefixEnsured(id, ensured);
}
});
};
(*callPtr)();
}
void RunnerManagerClient::setBusy(bool busy)
{
if (m_busy == busy) {
return;
}
m_busy = busy;
Q_EMIT busyChanged();
}
void RunnerManagerClient::setInstallId(const QString &installId)
{
if (m_installId == installId) {
return;
}
m_installId = installId;
Q_EMIT installIdChanged();
}
void RunnerManagerClient::setProgress(qint64 received, qint64 total)
{
if (m_receivedBytes == received && m_totalBytes == total) {
return;
}
m_receivedBytes = received;
m_totalBytes = total;
Q_EMIT progressChanged();
}
void RunnerManagerClient::setStatus(const QString &status)
{
if (m_status == status) {
return;
}
m_status = status;
Q_EMIT statusChanged();
}
void RunnerManagerClient::setLastError(const QString &error)
{
if (m_lastError == error) {
return;
}
m_lastError = error;
Q_EMIT lastErrorChanged();
}

View file

@ -1,85 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#pragma once
#include <QObject>
#include <QVariantList>
#include <QVariantMap>
class RunnerManagerClient : public QObject
{
Q_OBJECT
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
Q_PROPERTY(QString installId READ installId NOTIFY installIdChanged)
Q_PROPERTY(qint64 receivedBytes READ receivedBytes NOTIFY progressChanged)
Q_PROPERTY(qint64 totalBytes READ totalBytes NOTIFY progressChanged)
Q_PROPERTY(QString status READ status NOTIFY statusChanged)
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
Q_PROPERTY(QVariantList runners READ runners NOTIFY runnersChanged)
public:
explicit RunnerManagerClient(QObject *parent = nullptr);
~RunnerManagerClient() override;
bool busy() const;
QString installId() const;
qint64 receivedBytes() const;
qint64 totalBytes() const;
QString status() const;
QString lastError() const;
QVariantList runners() const;
Q_INVOKABLE void installRunnerFromUrl(const QString &url,
const QString &sha256 = QString(),
const QString &name = QString(),
const QString &type = QStringLiteral("proton"),
bool overwrite = true);
Q_INVOKABLE void cancelCurrentInstall();
Q_INVOKABLE void refreshRunners();
Q_INVOKABLE void uninstallRunner(const QString &runnerId);
Q_INVOKABLE void ensurePrefix(const QString &gameId, const QString &runner, const QString &prefixPath = QString());
Q_INVOKABLE void deletePrefix(const QString &gameId, const QString &prefixPath = QString());
Q_SIGNALS:
void busyChanged();
void installIdChanged();
void progressChanged();
void statusChanged();
void lastErrorChanged();
void runnersChanged();
void prefixEnsured(const QString &gameId, const QString &prefixPath);
void prefixDeleted(const QString &gameId, const QString &prefixPath);
private Q_SLOTS:
void onInstallStarted(const QString &installId, const QVariantMap &spec);
void onInstallProgress(const QString &installId, qlonglong received, qlonglong total);
void onInstallFinished(const QString &installId, const QVariantMap &result);
private:
void ensureRunnerDaemon();
void shutdownSpawnedRunnerDaemon();
void setBusy(bool busy);
void setInstallId(const QString &installId);
void setProgress(qint64 received, qint64 total);
void setStatus(const QString &status);
void setLastError(const QString &error);
bool m_busy = false;
QString m_installId;
qint64 m_receivedBytes = 0;
qint64 m_totalBytes = 0;
QString m_status;
QString m_lastError;
QVariantList m_runners;
bool m_refreshRetryPending = false;
class QProcess *m_runnerdProcess = nullptr;
bool m_runnerdSpawnAttempted = false;
};