#include "hyprextras.hpp" #include #include #include #include namespace caelestia::internal::hypr { HyprExtras::HyprExtras(QObject* parent) : QObject(parent) , m_requestSocket("") , m_eventSocket("") , m_socket(nullptr) , m_socketValid(false) , m_devices(new HyprDevices(this)) { const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); if (his.isEmpty()) { qWarning() << "HyprExtras::HyprExtras: $HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket."; return; } auto hyprDir = QString("%1/hypr/%2").arg(qEnvironmentVariable("XDG_RUNTIME_DIR"), his); if (!QDir(hyprDir).exists()) { hyprDir = "/tmp/hypr/" + his; if (!QDir(hyprDir).exists()) { qWarning() << "HyprExtras::HyprExtras: Hyprland socket directory does not exist. Unable to connect to " "Hyprland socket."; return; } } m_requestSocket = hyprDir + "/.socket.sock"; m_eventSocket = hyprDir + "/.socket2.sock"; refreshOptions(); refreshDevices(); m_socket = new QLocalSocket(this); QObject::connect(m_socket, &QLocalSocket::errorOccurred, this, &HyprExtras::socketError); QObject::connect(m_socket, &QLocalSocket::stateChanged, this, &HyprExtras::socketStateChanged); QObject::connect(m_socket, &QLocalSocket::readyRead, this, &HyprExtras::readEvent); m_socket->connectToServer(m_eventSocket, QLocalSocket::ReadOnly); } QVariantHash HyprExtras::options() const { return m_options; } HyprDevices* HyprExtras::devices() const { return m_devices; } void HyprExtras::message(const QString& message) { if (message.isEmpty()) { return; } makeRequest(message, [](bool success, const QByteArray& res) { if (!success) { qWarning() << "HyprExtras::message: request error:" << QString::fromUtf8(res); } }); } void HyprExtras::batchMessage(const QStringList& messages) { if (messages.isEmpty()) { return; } makeRequest("[[BATCH]]" + messages.join(";"), [](bool success, const QByteArray& res) { if (!success) { qWarning() << "HyprExtras::batchMessage: request error:" << QString::fromUtf8(res); } }); } void HyprExtras::applyOptions(const QVariantHash& options) { if (options.isEmpty()) { return; } QString request = "[[BATCH]]"; for (auto it = options.constBegin(); it != options.constEnd(); ++it) { request += QString("keyword %1 %2;").arg(it.key(), it.value().toString()); } makeRequest(request, [this](bool success, const QByteArray& res) { if (success) { refreshOptions(); } else { qWarning() << "HyprExtras::applyOptions: request error" << QString::fromUtf8(res); } }); } void HyprExtras::refreshOptions() { if (!m_optionsRefresh.isNull()) { m_optionsRefresh->close(); } m_optionsRefresh = makeRequestJson("descriptions", [this](bool success, const QJsonDocument& response) { m_optionsRefresh.reset(); if (!success) { return; } const auto options = response.array(); bool dirty = false; for (const auto& o : std::as_const(options)) { const auto obj = o.toObject(); const auto key = obj.value("value").toString(); const auto value = obj.value("data").toObject().value("current").toVariant(); if (m_options.value(key) != value) { dirty = true; m_options.insert(key, value); } } if (dirty) { emit optionsChanged(); } }); } void HyprExtras::refreshDevices() { if (!m_devicesRefresh.isNull()) { m_devicesRefresh->close(); } m_devicesRefresh = makeRequestJson("devices", [this](bool success, const QJsonDocument& response) { m_devicesRefresh.reset(); if (success) { m_devices->updateLastIpcObject(response.object()); } }); } void HyprExtras::socketError(QLocalSocket::LocalSocketError error) const { if (!m_socketValid) { qWarning() << "HyprExtras::socketError: unable to connect to Hyprland event socket:" << error; } else { qWarning() << "HyprExtras::socketError: Hyprland event socket error:" << error; } } void HyprExtras::socketStateChanged(QLocalSocket::LocalSocketState state) { if (state == QLocalSocket::UnconnectedState && m_socketValid) { qWarning() << "HyprExtras::socketStateChanged: Hyprland event socket disconnected."; } m_socketValid = state == QLocalSocket::ConnectedState; } void HyprExtras::readEvent() { while (true) { auto rawEvent = m_socket->readLine(); if (rawEvent.isEmpty()) { break; } rawEvent.truncate(rawEvent.length() - 1); // Remove trailing \n const auto event = QByteArrayView(rawEvent.data(), rawEvent.indexOf(">>")); handleEvent(QString::fromUtf8(event)); } } void HyprExtras::handleEvent(const QString& event) { if (event == "configreloaded") { refreshOptions(); } else if (event == "activelayout") { refreshDevices(); } } HyprExtras::SocketPtr HyprExtras::makeRequestJson( const QString& request, const std::function& callback) { return makeRequest("j/" + request, [callback](bool success, const QByteArray& response) { callback(success, QJsonDocument::fromJson(response)); }); } HyprExtras::SocketPtr HyprExtras::makeRequest( const QString& request, const std::function& callback) { if (m_requestSocket.isEmpty()) { return SocketPtr(); } auto socket = SocketPtr::create(this); QObject::connect(socket.data(), &QLocalSocket::connected, this, [=, this]() { QObject::connect(socket.data(), &QLocalSocket::readyRead, this, [socket, callback]() { const auto response = socket->readAll(); callback(true, std::move(response)); socket->close(); }); socket->write(request.toUtf8()); socket->flush(); }); QObject::connect(socket.data(), &QLocalSocket::errorOccurred, this, [=](QLocalSocket::LocalSocketError err) { qWarning() << "HyprExtras::makeRequest: error making request:" << err << "| request:" << request; callback(false, {}); socket->close(); }); socket->connectToServer(m_requestSocket); return socket; } } // namespace caelestia::internal::hypr