a-la-karte/src/krunner/alakarterunner.cpp

190 lines
5.5 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include "alakarterunner.h"
#include <KLocalizedString>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusReply>
#include <QFile>
#include <QIcon>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QStandardPaths>
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)
: KRunner::AbstractRunner(parent, metaData)
{
addSyntax(QStringLiteral("game <query>"), i18n("Search and launch games from A-La-Karte library"));
addSyntax(QStringLiteral("<query>"), i18n("Search games by name"));
}
AlakarteRunner::~AlakarteRunner() = default;
QString AlakarteRunner::libraryPath() const
{
return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation).replace(QStringLiteral("krunner_alakarte"), QStringLiteral("alakarte"))
+ QStringLiteral("/library.json");
}
QList<AlakarteRunner::GameInfo> AlakarteRunner::loadGames() const
{
QList<GameInfo> games;
QFile file(libraryPath());
if (!file.open(QIODevice::ReadOnly)) {
return games;
}
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
if (!doc.isArray()) {
return games;
}
QJsonArray array = doc.array();
for (const QJsonValue &value : array) {
QJsonObject obj = value.toObject();
// Skip hidden games
if (obj[QStringLiteral("hidden")].toBool()) {
continue;
}
GameInfo info;
info.id = obj[QStringLiteral("id")].toString();
info.name = obj[QStringLiteral("name")].toString();
info.developer = obj[QStringLiteral("developer")].toString();
info.platform = obj[QStringLiteral("platform")].toString();
info.launchCommand = obj[QStringLiteral("launchCommand")].toString();
info.coverPath = obj[QStringLiteral("coverUrl")].toString();
if (!info.name.isEmpty() && !info.launchCommand.isEmpty()) {
games.append(info);
}
}
return games;
}
void AlakarteRunner::match(KRunner::RunnerContext &context)
{
const QString query = context.query();
if (query.length() < 2) {
return;
}
QString searchTerm = query;
bool explicitGameSearch = false;
// Check for "game " prefix
if (query.startsWith(QLatin1String("game "), Qt::CaseInsensitive)) {
searchTerm = query.mid(5).trimmed();
explicitGameSearch = true;
}
if (searchTerm.isEmpty()) {
return;
}
QList<GameInfo> games = loadGames();
for (const GameInfo &game : games) {
bool matches = game.name.contains(searchTerm, Qt::CaseInsensitive) || game.developer.contains(searchTerm, Qt::CaseInsensitive);
if (!matches && !explicitGameSearch) {
continue;
}
if (!matches) {
continue;
}
KRunner::QueryMatch match(this);
match.setText(game.name);
QString subtext;
if (!game.developer.isEmpty()) {
subtext = game.developer;
}
if (!game.platform.isEmpty()) {
if (!subtext.isEmpty()) {
subtext += QStringLiteral("");
}
subtext += game.platform;
}
match.setSubtext(subtext);
match.setIconName(QStringLiteral("applications-games"));
match.setId(game.id);
match.setData(game.launchCommand);
// Calculate relevance
qreal relevance = 0.7;
if (game.name.startsWith(searchTerm, Qt::CaseInsensitive)) {
relevance = 0.9;
} else if (game.name.contains(searchTerm, Qt::CaseInsensitive)) {
relevance = 0.8;
}
if (explicitGameSearch) {
relevance += 0.05;
}
match.setRelevance(relevance);
match.setCategoryRelevance(KRunner::QueryMatch::CategoryRelevance::Moderate);
context.addMatch(match);
}
}
void AlakarteRunner::run(const KRunner::RunnerContext &context, const KRunner::QueryMatch &match)
{
Q_UNUSED(context)
QString command = match.data().toString();
if (command.isEmpty()) {
return;
}
// Always try daemon first for all commands (including Steam/Lutris)
if (!match.id().isEmpty()) {
QVariantMap launchSpec = {
{QStringLiteral("command"), command},
{QStringLiteral("gameId"), match.id()},
{QStringLiteral("displayName"), match.text()},
{QStringLiteral("origin"), QStringLiteral("krunner")},
};
if (launchViaDaemon(QDBusConnection::systemBus(), launchSpec) || launchViaDaemon(QDBusConnection::sessionBus(), launchSpec)) {
return;
}
}
}
#include "alakarterunner.moc"