Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ install: build
sudo cp -v ./build/prefix/usr/lib64/qt6/plugins/kf6/krunner/bazaarrunner.so /usr/lib64/qt6/plugins/kf6/krunner/bazaarrunner.so
sudo cp -v ./build/prefix/usr/share/locale/es/LC_MESSAGES/plasma_runner_bazaarrunner.mo /usr/share/locale/es/LC_MESSAGES/plasma_runner_bazaarrunner.mo

# This should work on Aurora/Bazzite
smoke: build
#!/usr/bin/env bash
set -euo pipefail

./build/bin/bazaar-dbus-tool --search "KolourPaint"
./build/bin/bazaar-dbus-tool --activate org.kde.kolourpaint


gdb:
gdb -ex "run" -ex "bt" --args ./build/bin/bazaar-dbus-tool -s spotify
Expand Down
14 changes: 8 additions & 6 deletions src/bazaar-dbus-tool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ using namespace Qt::Literals::StringLiterals;

int main(int argc, char *argv[])
{
// override Fedora's weird logging rules for Qt apps
qputenv("QT_FORCE_STDERR_LOGGING", "1");

QCoreApplication app(argc, argv);
QCoreApplication::setApplicationName("bazaar-dbus-tool"_L1);
QCoreApplication::setApplicationVersion("1.0"_L1);
Expand Down Expand Up @@ -39,23 +42,23 @@ int main(int argc, char *argv[])

if (!client.isConnected()) {
std::cerr << "Error: Could not connect to Bazaar D-Bus service" << std::endl;
std::cerr << "Last error: " << client.lastError().toStdString() << std::endl;
std::cerr << "Make sure Bazaar is running and the search provider is enabled." << std::endl;
return 1;
}

std::cout << "Successfully connected to Bazaar D-Bus service" << std::endl;
std::cout << "Successfully connected to Bazaar D-Bus service " << client.serviceName()->toStdString() << std::endl;

if (parser.isSet(searchOption)) {
QString query = parser.value(searchOption);
std::cout << "Searching for: " << query.toStdString() << std::endl;

QList<AppSuggestion> results = client.search(query);
const auto result = client.search(query);
const QList<AppSuggestion> &results = result.apps;

if (results.isEmpty()) {
std::cout << "No results found for query: " << query.toStdString() << std::endl;
if (!client.lastError().isEmpty()) {
std::cerr << "Error: " << client.lastError().toStdString() << std::endl;
if (!result.error.isEmpty()) {
std::cerr << "Error: " << result.error.toStdString() << std::endl;
}
} else {
std::cout << "Found " << results.size() << " results:" << std::endl;
Expand Down Expand Up @@ -83,7 +86,6 @@ int main(int argc, char *argv[])
std::cout << "Successfully activated application: " << appId.toStdString() << std::endl;
} else {
std::cerr << "Failed to activate application: " << appId.toStdString() << std::endl;
std::cerr << "Error: " << client.lastError().toStdString() << std::endl;
}
} else {
std::cout << "Usage examples:" << std::endl;
Expand Down
158 changes: 89 additions & 69 deletions src/bazaarclient.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#include "bazaarclient.h"

#include <QDBusConnection>
#include <QDBusReply>
#include <QDBusConnectionInterface>
#include <QDBusMessage>
#include <QDBusArgument>
#include <QDBusVariant>
Expand All @@ -13,9 +13,14 @@ using namespace Qt::Literals::StringLiterals;
// TODO: make this configurable in a KCM
static constexpr int kMaxNumResults = 6;

QString kDBusServiceName = QStringLiteral("io.github.kolunmi.Bazaar");
QString kDBusServicePath = QStringLiteral("/io/github/kolunmi/Bazaar/SearchProvider");
QString kDBusServiceInterface = QStringLiteral("org.gnome.Shell.SearchProvider2");
static const QStringList kDBusServiceNames = {
"io.github.kolunmi.Bazaar.SearchProvider"_L1,
"io.github.kolunmi.Bazaar"_L1,
};

static const QString kDBusServicePath = "/io/github/kolunmi/Bazaar/SearchProvider"_L1;
static const QString kDBusServiceInterface = "org.gnome.Shell.SearchProvider2"_L1;
static const QString kNoProviderError = "No Bazaar search provider found on the session bus"_L1;

struct ResultMetas {
QList<QVariantMap> metas;
Expand Down Expand Up @@ -64,62 +69,87 @@ const QDBusArgument& operator>>(const QDBusArgument& arg, ResultMetas& metas)
return arg;
}

namespace {

// Pick the first candidate bus name that is registered or activatable
std::optional<QString> resolveServiceName()
{
QDBusConnectionInterface *bus = QDBusConnection::sessionBus().interface();
if (!bus) {
qWarning() << "BazaarClient: no session bus available";
return std::nullopt;
}

const QStringList registered = bus->registeredServiceNames().value();
const QStringList activatable = bus->activatableServiceNames().value();

for (const QString &candidate : kDBusServiceNames) {
if (registered.contains(candidate) || activatable.contains(candidate)) {
return candidate;
}
}

return std::nullopt;
}

QDBusMessage callProvider(const QString &serviceName, const QString &method, const QVariantList &args)
{
QDBusMessage call = QDBusMessage::createMethodCall(serviceName, kDBusServicePath, kDBusServiceInterface, method);
call.setArguments(args);
return QDBusConnection::sessionBus().call(call);
}

} // namespace

BazaarClient::BazaarClient() {
m_bazaarInterface = std::make_unique<QDBusInterface>(
kDBusServiceName,
kDBusServicePath,
kDBusServiceInterface,
QDBusConnection::sessionBus()
);

if (!m_bazaarInterface->isValid()) {
m_lastError = m_bazaarInterface->lastError().message();
qWarning() << "BazaarClient: failed to connect to Bazaar D-Bus service " << kDBusServiceName << ": " << m_lastError;
qWarning() << "BazaarClient: Make sure Bazaar is running and the search provider is enabled.";
m_serviceName = resolveServiceName();

if (!m_serviceName) {
qWarning() << "BazaarClient: " << kNoProviderError;
qWarning() << "BazaarClient: Make sure Bazaar is installed and the search provider is enabled.";
} else {
qDebug() << "BazaarClient: successfully connected to Bazaar D-Bus service " << kDBusServiceName;
m_lastError.clear();
qDebug() << "BazaarClient: using Bazaar D-Bus service" << *m_serviceName;
}
}

bool BazaarClient::isConnected() const {
return m_bazaarInterface && m_bazaarInterface->isValid();
return m_serviceName.has_value();
}

QString BazaarClient::lastError() const {
return m_lastError;
std::optional<QString> BazaarClient::serviceName() const {
return m_serviceName;
}

QList<AppSuggestion> BazaarClient::search(const QString &term, const std::function<bool()> &isContextValid) {
QList<AppSuggestion> results;
if (!isConnected()) {
m_lastError = "Not connected to Bazaar D-Bus interface"_L1;
qDebug() << "BazaarClient::search:" << m_lastError;
return results;
SearchResult BazaarClient::search(const QString &term, const std::function<bool()> &isContextValid) {
SearchResult result;

if (!m_serviceName) {
result.error = kNoProviderError;
qDebug() << "BazaarClient::search:" << result.error;
return result;
}

if (term.length() < 2) {
m_lastError = "Search term too short (minimum 2 characters)"_L1;
return results;
result.error = "Search term too short (minimum 2 characters)"_L1;
return result;
}

// Split search term into individual terms
QStringList terms = term.split(QLatin1Char(' '), Qt::SkipEmptyParts);

if (isContextValid && !isContextValid()) {
return results;
return result;
}

// Get initial result set
QStringList resultIds = getInitialResultSet(terms);
QStringList resultIds = getInitialResultSet(terms, result.error);
if (resultIds.isEmpty()) {
qDebug() << "BazaarClient::search: No results returned from Bazaar for query:" << term;
return results;
return result;
}

if (isContextValid && !isContextValid()) {
return results;
return result;
}

qDebug() << "BazaarClient::search: Bazaar returned" << resultIds.size() << "result IDs (will be truncated to " << kMaxNumResults << ")";
Expand All @@ -128,7 +158,7 @@ QList<AppSuggestion> BazaarClient::search(const QString &term, const std::functi
resultIds = resultIds.mid(0, kMaxNumResults);
}

QList<QVariantMap> metas = getResultMetas(resultIds);
QList<QVariantMap> metas = getResultMetas(resultIds, result.error);

// Extract metadata for each result
for (int i = 0; i < resultIds.size() && i < metas.size(); ++i) {
Expand Down Expand Up @@ -164,75 +194,65 @@ QList<AppSuggestion> BazaarClient::search(const QString &term, const std::functi
continue;
}

results.append(suggestion);
result.apps.append(suggestion);
}

return results;
return result;
}

bool BazaarClient::activateResult(const QString &appId, const QStringList &searchTerms) {
if (!isConnected()) {
m_lastError = "Not connected to Bazaar D-Bus interface"_L1;
qWarning() << "BazaarClient::activateResult:" << m_lastError;
if (!m_serviceName) {
qWarning() << "BazaarClient::activateResult:" << kNoProviderError;
return false;
}

qDebug() << "BazaarClient::activateResult: Activating app ID:" << appId;

uint timestamp = static_cast<uint>(QDateTime::currentSecsSinceEpoch());

QDBusReply<void> reply = m_bazaarInterface->call(
QStringLiteral("ActivateResult"),
appId,
searchTerms,
timestamp
);
QDBusMessage reply = callProvider(*m_serviceName, "ActivateResult"_L1, {appId, searchTerms, timestamp});

if (!reply.isValid()) {
m_lastError = reply.error().message();
qWarning() << "BazaarClient::activateResult: Failed to activate result:" << m_lastError;
if (reply.type() == QDBusMessage::ErrorMessage) {
qWarning() << "BazaarClient::activateResult: Failed to activate result:" << reply.errorMessage();
return false;
}

return true;
}

QStringList BazaarClient::getInitialResultSet(const QStringList &terms) {
QDBusReply<QStringList> reply = m_bazaarInterface->call(QStringLiteral("GetInitialResultSet"), terms);
QStringList BazaarClient::getInitialResultSet(const QStringList &terms, QString &error) {
QDBusMessage reply = callProvider(*m_serviceName, "GetInitialResultSet"_L1, {terms});

if (!reply.isValid()) {
m_lastError = reply.error().message();
qWarning() << "BazaarClient::getInitialResultSet: Failed to get search results:" << m_lastError;
if (reply.type() == QDBusMessage::ErrorMessage) {
error = reply.errorMessage();
qWarning() << "BazaarClient::getInitialResultSet: Failed to get search results:" << error;
return QStringList();
}

return reply.value();
if (reply.arguments().isEmpty()) {
error = "No arguments in GetInitialResultSet reply"_L1;
qWarning() << "BazaarClient::getInitialResultSet:" << error;
return QStringList();
}

return reply.arguments().at(0).toStringList();
}

QList<QVariantMap> BazaarClient::getResultMetas(const QStringList &resultIds) {
QList<QVariantMap> BazaarClient::getResultMetas(const QStringList &resultIds, QString &error) {
ResultMetas metas;

QDBusMessage metaCall = QDBusMessage::createMethodCall(
kDBusServiceName,
kDBusServicePath,
kDBusServiceInterface,
QStringLiteral("GetResultMetas")
);

metaCall << resultIds;

QDBusMessage metaReply = QDBusConnection::sessionBus().call(metaCall);
QDBusMessage metaReply = callProvider(*m_serviceName, "GetResultMetas"_L1, {resultIds});

if (metaReply.type() == QDBusMessage::ErrorMessage) {
m_lastError = metaReply.errorMessage();
qWarning() << "BazaarClient::getResultMetas: Failed to get result metadata:" << m_lastError;
error = metaReply.errorMessage();
qWarning() << "BazaarClient::getResultMetas: Failed to get result metadata:" << error;
return {};
}


if (metaReply.arguments().isEmpty()) {
m_lastError = "No arguments in GetResultMetas reply"_L1;
qWarning() << "BazaarClient::getResultMetas:" << m_lastError;
error = "No arguments in GetResultMetas reply"_L1;
qWarning() << "BazaarClient::getResultMetas:" << error;
return {};
}

Expand Down
21 changes: 12 additions & 9 deletions src/bazaarclient.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
#include <QString>
#include <QStringList>
#include <QVariantMap>
#include <QDBusInterface>
#include <functional>
#include <memory>
#include <optional>

struct AppSuggestion {
QString id;
Expand All @@ -14,20 +13,24 @@ struct AppSuggestion {
QString iconName;
};

struct SearchResult {
QList<AppSuggestion> apps;
QString error;
};

class BazaarClient {
public:
BazaarClient();

bool isConnected() const;
QString lastError() const;
std::optional<QString> serviceName() const;

QList<AppSuggestion> search(const QString &term, const std::function<bool()> &isContextValid = nullptr);
bool activateResult(const QString &appId, const QStringList &searchTerms);
[[nodiscard]] SearchResult search(const QString &term, const std::function<bool()> &isContextValid = nullptr);
[[nodiscard]] bool activateResult(const QString &appId, const QStringList &searchTerms);

private:
std::unique_ptr<QDBusInterface> m_bazaarInterface;
QString m_lastError;
std::optional<QString> m_serviceName;

QStringList getInitialResultSet(const QStringList &terms);
QList<QVariantMap> getResultMetas(const QStringList &resultIds);
QStringList getInitialResultSet(const QStringList &terms, QString &error);
QList<QVariantMap> getResultMetas(const QStringList &resultIds, QString &error);
};
Loading