diff --git a/Plugins/ZShell/Internal/hyprextras.cpp b/Plugins/ZShell/Internal/hyprextras.cpp index a869ae2..b5f9845 100644 --- a/Plugins/ZShell/Internal/hyprextras.cpp +++ b/Plugins/ZShell/Internal/hyprextras.cpp @@ -6,11 +6,123 @@ #include #include #include +#include Q_LOGGING_CATEGORY(lcHypr, "ZShell.internal.hypr", QtInfoMsg) namespace ZShell::internal::hypr { +static QString luaEscapeString(const QString& s) { + QString out; + out.reserve(s.size() + 2); + + out += '"'; + for (QChar c : s) { + switch (c.unicode()) { + case '\\': out += "\\\\"; break; + case '"': out += "\\\""; break; + case '\n': out += "\\n"; break; + case '\r': out += "\\r"; break; + case '\t': out += "\\t"; break; + default: out += c; break; + } + } + out += '"'; + + return out; +} + +static QString luaValue(const QVariant& v); + +static QString luaArray(const QVariantList& list) { + QStringList parts; + parts.reserve(list.size()); + + for (const auto& item : list) { + parts << luaValue(item); + } + + return "{ " + parts.join(", ") + " }"; +} + +static QString luaMap(const QVariantMap& map) { + QStringList parts; + parts.reserve(map.size()); + + for (auto it = map.cbegin(); it != map.cend(); ++it) { + parts << it.key() + " = " + luaValue(it.value()); + } + + return "{ " + parts.join(", ") + " }"; +} + +static QString luaValue(const QVariant& v) { + if (!v.isValid() || v.isNull()) + return "nil"; + + switch (v.metaType().id()) { + case QMetaType::Bool: + return v.toBool() ? "true" : "false"; + + case QMetaType::Int: + case QMetaType::UInt: + case QMetaType::LongLong: + case QMetaType::ULongLong: + case QMetaType::Double: + return v.toString(); + + case QMetaType::QString: + return luaEscapeString(v.toString()); + + case QMetaType::QStringList: + return luaArray(v.toStringList().toList()); + + case QMetaType::QVariantList: + return luaArray(v.toList()); + + case QMetaType::QVariantMap: + return luaMap(v.toMap()); + + case QMetaType::QVariantHash: { + QVariantMap map; + for (auto it = v.toHash().begin(); it != v.toHash().end(); ++it) + map.insert(it.key(), it.value()); + return luaMap(map); + } + + default: + return luaEscapeString(v.toString()); + } +} + +static QString normalizeKey(QString key) { + key = key.trimmed(); + key.replace(':', '.'); + return key; +} + +static QString buildHlConfig(const QString& key, const QVariant& value) { + const QStringList parts = normalizeKey(key).split('.', Qt::SkipEmptyParts); + if (parts.isEmpty()) + return {}; + + QString out = "hl.config({ "; + + for (int i = 0; i < parts.size(); ++i) { + out += parts[i] + " = "; + if (i != parts.size() - 1) + out += "{ "; + } + + out += luaValue(value); + + for (int i = 0; i < parts.size() - 1; ++i) + out += " }"; + + out += " })"; + return out; +} + HyprExtras::HyprExtras(QObject* parent) : QObject(parent) , m_requestSocket("") @@ -18,37 +130,45 @@ HyprExtras::HyprExtras(QObject* parent) , m_socket(nullptr) , m_socketValid(false) , m_devices(new HyprDevices(this)) { + const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); if (his.isEmpty()) { - qCWarning(lcHypr) << "$HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket."; + qCWarning(lcHypr) << "$HYPRLAND_INSTANCE_SIGNATURE is unset."; return; } - auto hyprDir = QString("%1/hypr/%2").arg(qEnvironmentVariable("XDG_RUNTIME_DIR"), his); + auto hyprDir = QString("%1/hypr/%2") + .arg(qEnvironmentVariable("XDG_RUNTIME_DIR"), his); + if (!QDir(hyprDir).exists()) { hyprDir = "/tmp/hypr/" + his; - if (!QDir(hyprDir).exists()) { - qCWarning(lcHypr) << "Hyprland socket directory does not exist. Unable to connect to Hyprland socket."; + qCWarning(lcHypr) << "Hyprland socket directory not found."; return; } } m_requestSocket = hyprDir + "/.socket.sock"; - m_eventSocket = hyprDir + "/.socket2.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); + connect(m_socket, &QLocalSocket::errorOccurred, + this, &HyprExtras::socketError); + + connect(m_socket, &QLocalSocket::stateChanged, + this, &HyprExtras::socketStateChanged); + + connect(m_socket, &QLocalSocket::readyRead, + this, &HyprExtras::readEvent); m_socket->connectToServer(m_eventSocket, QLocalSocket::ReadOnly); } + QVariantHash HyprExtras::options() const { return m_options; } @@ -58,148 +178,145 @@ HyprDevices* HyprExtras::devices() const { } void HyprExtras::message(const QString& message) { - if (message.isEmpty()) { - return; - } + if (message.isEmpty()) return; makeRequest(message, [](bool success, const QByteArray& res) { - if (!success) { - qCWarning(lcHypr) << "message: request error:" << QString::fromUtf8(res); - } + if (!success) + qCWarning(lcHypr) << "message failed:" << res; }); } void HyprExtras::batchMessage(const QStringList& messages) { - if (messages.isEmpty()) { - return; - } + if (messages.isEmpty()) return; - makeRequest("[[BATCH]]" + messages.join(";"), [](bool success, const QByteArray& res) { - if (!success) { - qCWarning(lcHypr) << "batchMessage: request error:" << QString::fromUtf8(res); - } + makeRequest("[[BATCH]]" + messages.join(";"), + [](bool success, const QByteArray& res) { + if (!success) + qCWarning(lcHypr) << "batchMessage failed:" << res; }); } void HyprExtras::applyOptions(const QVariantHash& options) { - if (options.isEmpty()) { + if (options.isEmpty()) return; + + QStringList luaCalls; + luaCalls.reserve(options.size()); + + for (auto it = options.begin(); it != options.end(); ++it) { + const auto call = buildHlConfig(it.key(), it.value()); + if (!call.isEmpty()) + luaCalls << call; } - QString request; - request.reserve(12 + options.size() * 40); - request += QLatin1String("[[BATCH]]"); - for (auto it = options.constBegin(); it != options.constEnd(); ++it) { - request += QLatin1String("keyword ") + it.key() + QLatin1Char(' ') + it.value().toString() + QLatin1Char(';'); - } + if (luaCalls.isEmpty()) + return; - makeRequest(request, [this](bool success, const QByteArray& res) { - if (success) { + const QString request = + "eval " + luaCalls.join("; "); + + makeRequest(request, + [this](bool success, const QByteArray& res) { + if (success) refreshOptions(); - } else { - qCWarning(lcHypr) << "applyOptions: request error" << QString::fromUtf8(res); - } + else + qCWarning(lcHypr) << "applyOptions failed:" << res; }); } void HyprExtras::refreshOptions() { - if (!m_optionsRefresh.isNull()) { + if (!m_optionsRefresh.isNull()) m_optionsRefresh->close(); - } - m_optionsRefresh = makeRequestJson("descriptions", [this](bool success, const QJsonDocument& response) { + m_optionsRefresh = makeRequestJson("descriptions", + [this](bool success, const QJsonDocument& response) { m_optionsRefresh.reset(); - if (!success) { - return; - } + if (!success) return; - const auto options = response.array(); + const auto arr = response.array(); bool dirty = false; - for (const auto& o : std::as_const(options)) { + for (const auto& o : arr) { const auto obj = o.toObject(); const auto key = obj.value("value").toString(); - const auto value = obj.value("data").toObject().value("current").toVariant(); + const auto value = + obj.value("data").toObject() + .value("current") + .toVariant(); + if (m_options.value(key) != value) { - dirty = true; m_options.insert(key, value); + dirty = true; } } - if (dirty) { + if (dirty) emit optionsChanged(); - } }); } void HyprExtras::refreshDevices() { - if (!m_devicesRefresh.isNull()) { + if (!m_devicesRefresh.isNull()) m_devicesRefresh->close(); - } - m_devicesRefresh = makeRequestJson("devices", [this](bool success, const QJsonDocument& response) { + m_devicesRefresh = makeRequestJson("devices", + [this](bool success, const QJsonDocument& response) { m_devicesRefresh.reset(); - if (success) { + if (success) m_devices->updateLastIpcObject(response.object()); - } }); } void HyprExtras::socketError(QLocalSocket::LocalSocketError error) const { - if (!m_socketValid) { - qCWarning(lcHypr) << "socketError: unable to connect to Hyprland event socket:" << error; - } else { - qCWarning(lcHypr) << "socketError: Hyprland event socket error:" << error; - } + qCWarning(lcHypr) << "socket error:" << error; } void HyprExtras::socketStateChanged(QLocalSocket::LocalSocketState state) { - if (state == QLocalSocket::UnconnectedState && m_socketValid) { - qCWarning(lcHypr) << "socketStateChanged: Hyprland event socket disconnected."; - } - - m_socketValid = state == QLocalSocket::ConnectedState; + 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(">>")); + auto line = m_socket->readLine(); + if (line.isEmpty()) break; + + line.chop(1); + const auto event = line.left(line.indexOf(">>")); + handleEvent(QString::fromUtf8(event)); } } void HyprExtras::handleEvent(const QString& event) { - if (event == "configreloaded") { + if (event == "configreloaded") refreshOptions(); - } else if (event == "activelayout") { + 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)); + const QString& request, + const std::function& callback) { + + return makeRequest("j/" + request, + [callback](bool success, const QByteArray& res) { + callback(success, QJsonDocument::fromJson(res)); }); } HyprExtras::SocketPtr HyprExtras::makeRequest( - const QString& request, const std::function& callback) { - if (m_requestSocket.isEmpty()) { + 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)); + connect(socket.data(), &QLocalSocket::connected, this, [=, this]() { + connect(socket.data(), &QLocalSocket::readyRead, + this, [socket, callback]() { + callback(true, socket->readAll()); socket->close(); }); @@ -207,14 +324,14 @@ HyprExtras::SocketPtr HyprExtras::makeRequest( socket->flush(); }); - QObject::connect(socket.data(), &QLocalSocket::errorOccurred, this, [=](QLocalSocket::LocalSocketError err) { - qCWarning(lcHypr) << "makeRequest: error making request:" << err << "| request:" << request; + connect(socket.data(), &QLocalSocket::errorOccurred, + this, [=](QLocalSocket::LocalSocketError err) { + qCWarning(lcHypr) << "request error:" << err << request; callback(false, {}); socket->close(); }); socket->connectToServer(m_requestSocket); - return socket; }