hyprland lua support #91
@@ -6,11 +6,123 @@
|
|||||||
#include <qlocalsocket.h>
|
#include <qlocalsocket.h>
|
||||||
#include <qloggingcategory.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qvariant.h>
|
#include <qvariant.h>
|
||||||
|
#include <qmetatype.h>
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(lcHypr, "ZShell.internal.hypr", QtInfoMsg)
|
Q_LOGGING_CATEGORY(lcHypr, "ZShell.internal.hypr", QtInfoMsg)
|
||||||
|
|
||||||
namespace ZShell::internal::hypr {
|
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)
|
HyprExtras::HyprExtras(QObject* parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_requestSocket("")
|
, m_requestSocket("")
|
||||||
@@ -18,37 +130,45 @@ HyprExtras::HyprExtras(QObject* parent)
|
|||||||
, m_socket(nullptr)
|
, m_socket(nullptr)
|
||||||
, m_socketValid(false)
|
, m_socketValid(false)
|
||||||
, m_devices(new HyprDevices(this)) {
|
, m_devices(new HyprDevices(this)) {
|
||||||
|
|
||||||
const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE");
|
const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE");
|
||||||
if (his.isEmpty()) {
|
if (his.isEmpty()) {
|
||||||
qCWarning(lcHypr) << "$HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket.";
|
qCWarning(lcHypr) << "$HYPRLAND_INSTANCE_SIGNATURE is unset.";
|
||||||
return;
|
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()) {
|
if (!QDir(hyprDir).exists()) {
|
||||||
hyprDir = "/tmp/hypr/" + his;
|
hyprDir = "/tmp/hypr/" + his;
|
||||||
|
|
||||||
if (!QDir(hyprDir).exists()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_requestSocket = hyprDir + "/.socket.sock";
|
m_requestSocket = hyprDir + "/.socket.sock";
|
||||||
m_eventSocket = hyprDir + "/.socket2.sock";
|
m_eventSocket = hyprDir + "/.socket2.sock";
|
||||||
|
|
||||||
refreshOptions();
|
refreshOptions();
|
||||||
refreshDevices();
|
refreshDevices();
|
||||||
|
|
||||||
m_socket = new QLocalSocket(this);
|
m_socket = new QLocalSocket(this);
|
||||||
|
|
||||||
QObject::connect(m_socket, &QLocalSocket::errorOccurred, this, &HyprExtras::socketError);
|
connect(m_socket, &QLocalSocket::errorOccurred,
|
||||||
QObject::connect(m_socket, &QLocalSocket::stateChanged, this, &HyprExtras::socketStateChanged);
|
this, &HyprExtras::socketError);
|
||||||
QObject::connect(m_socket, &QLocalSocket::readyRead, this, &HyprExtras::readEvent);
|
|
||||||
|
connect(m_socket, &QLocalSocket::stateChanged,
|
||||||
|
this, &HyprExtras::socketStateChanged);
|
||||||
|
|
||||||
|
connect(m_socket, &QLocalSocket::readyRead,
|
||||||
|
this, &HyprExtras::readEvent);
|
||||||
|
|
||||||
m_socket->connectToServer(m_eventSocket, QLocalSocket::ReadOnly);
|
m_socket->connectToServer(m_eventSocket, QLocalSocket::ReadOnly);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
QVariantHash HyprExtras::options() const {
|
QVariantHash HyprExtras::options() const {
|
||||||
return m_options;
|
return m_options;
|
||||||
}
|
}
|
||||||
@@ -58,148 +178,145 @@ HyprDevices* HyprExtras::devices() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HyprExtras::message(const QString& message) {
|
void HyprExtras::message(const QString& message) {
|
||||||
if (message.isEmpty()) {
|
if (message.isEmpty()) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
makeRequest(message, [](bool success, const QByteArray& res) {
|
makeRequest(message, [](bool success, const QByteArray& res) {
|
||||||
if (!success) {
|
if (!success)
|
||||||
qCWarning(lcHypr) << "message: request error:" << QString::fromUtf8(res);
|
qCWarning(lcHypr) << "message failed:" << res;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void HyprExtras::batchMessage(const QStringList& messages) {
|
void HyprExtras::batchMessage(const QStringList& messages) {
|
||||||
if (messages.isEmpty()) {
|
if (messages.isEmpty()) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
makeRequest("[[BATCH]]" + messages.join(";"), [](bool success, const QByteArray& res) {
|
makeRequest("[[BATCH]]" + messages.join(";"),
|
||||||
if (!success) {
|
[](bool success, const QByteArray& res) {
|
||||||
qCWarning(lcHypr) << "batchMessage: request error:" << QString::fromUtf8(res);
|
if (!success)
|
||||||
}
|
qCWarning(lcHypr) << "batchMessage failed:" << res;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void HyprExtras::applyOptions(const QVariantHash& options) {
|
void HyprExtras::applyOptions(const QVariantHash& options) {
|
||||||
if (options.isEmpty()) {
|
if (options.isEmpty())
|
||||||
return;
|
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;
|
if (luaCalls.isEmpty())
|
||||||
request.reserve(12 + options.size() * 40);
|
return;
|
||||||
request += QLatin1String("[[BATCH]]");
|
|
||||||
for (auto it = options.constBegin(); it != options.constEnd(); ++it) {
|
|
||||||
request += QLatin1String("keyword ") + it.key() + QLatin1Char(' ') + it.value().toString() + QLatin1Char(';');
|
|
||||||
}
|
|
||||||
|
|
||||||
makeRequest(request, [this](bool success, const QByteArray& res) {
|
const QString request =
|
||||||
if (success) {
|
"eval " + luaCalls.join("; ");
|
||||||
|
|
||||||
|
makeRequest(request,
|
||||||
|
[this](bool success, const QByteArray& res) {
|
||||||
|
if (success)
|
||||||
refreshOptions();
|
refreshOptions();
|
||||||
} else {
|
else
|
||||||
qCWarning(lcHypr) << "applyOptions: request error" << QString::fromUtf8(res);
|
qCWarning(lcHypr) << "applyOptions failed:" << res;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void HyprExtras::refreshOptions() {
|
void HyprExtras::refreshOptions() {
|
||||||
if (!m_optionsRefresh.isNull()) {
|
if (!m_optionsRefresh.isNull())
|
||||||
m_optionsRefresh->close();
|
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();
|
m_optionsRefresh.reset();
|
||||||
if (!success) {
|
if (!success) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto options = response.array();
|
const auto arr = response.array();
|
||||||
bool dirty = false;
|
bool dirty = false;
|
||||||
|
|
||||||
for (const auto& o : std::as_const(options)) {
|
for (const auto& o : arr) {
|
||||||
const auto obj = o.toObject();
|
const auto obj = o.toObject();
|
||||||
const auto key = obj.value("value").toString();
|
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) {
|
if (m_options.value(key) != value) {
|
||||||
dirty = true;
|
|
||||||
m_options.insert(key, value);
|
m_options.insert(key, value);
|
||||||
|
dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dirty) {
|
if (dirty)
|
||||||
emit optionsChanged();
|
emit optionsChanged();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void HyprExtras::refreshDevices() {
|
void HyprExtras::refreshDevices() {
|
||||||
if (!m_devicesRefresh.isNull()) {
|
if (!m_devicesRefresh.isNull())
|
||||||
m_devicesRefresh->close();
|
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();
|
m_devicesRefresh.reset();
|
||||||
if (success) {
|
if (success)
|
||||||
m_devices->updateLastIpcObject(response.object());
|
m_devices->updateLastIpcObject(response.object());
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void HyprExtras::socketError(QLocalSocket::LocalSocketError error) const {
|
void HyprExtras::socketError(QLocalSocket::LocalSocketError error) const {
|
||||||
if (!m_socketValid) {
|
qCWarning(lcHypr) << "socket error:" << error;
|
||||||
qCWarning(lcHypr) << "socketError: unable to connect to Hyprland event socket:" << error;
|
|
||||||
} else {
|
|
||||||
qCWarning(lcHypr) << "socketError: Hyprland event socket error:" << error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HyprExtras::socketStateChanged(QLocalSocket::LocalSocketState state) {
|
void HyprExtras::socketStateChanged(QLocalSocket::LocalSocketState state) {
|
||||||
if (state == QLocalSocket::UnconnectedState && m_socketValid) {
|
m_socketValid = (state == QLocalSocket::ConnectedState);
|
||||||
qCWarning(lcHypr) << "socketStateChanged: Hyprland event socket disconnected.";
|
|
||||||
}
|
|
||||||
|
|
||||||
m_socketValid = state == QLocalSocket::ConnectedState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HyprExtras::readEvent() {
|
void HyprExtras::readEvent() {
|
||||||
while (true) {
|
while (true) {
|
||||||
auto rawEvent = m_socket->readLine();
|
auto line = m_socket->readLine();
|
||||||
if (rawEvent.isEmpty()) {
|
if (line.isEmpty()) break;
|
||||||
break;
|
|
||||||
}
|
line.chop(1);
|
||||||
rawEvent.truncate(rawEvent.length() - 1); // Remove trailing \n
|
const auto event = line.left(line.indexOf(">>"));
|
||||||
const auto event = QByteArrayView(rawEvent.data(), rawEvent.indexOf(">>"));
|
|
||||||
handleEvent(QString::fromUtf8(event));
|
handleEvent(QString::fromUtf8(event));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HyprExtras::handleEvent(const QString& event) {
|
void HyprExtras::handleEvent(const QString& event) {
|
||||||
if (event == "configreloaded") {
|
if (event == "configreloaded")
|
||||||
refreshOptions();
|
refreshOptions();
|
||||||
} else if (event == "activelayout") {
|
else if (event == "activelayout")
|
||||||
refreshDevices();
|
refreshDevices();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HyprExtras::SocketPtr HyprExtras::makeRequestJson(
|
HyprExtras::SocketPtr HyprExtras::makeRequestJson(
|
||||||
const QString& request, const std::function<void(bool, QJsonDocument)>& callback) {
|
const QString& request,
|
||||||
return makeRequest("j/" + request, [callback](bool success, const QByteArray& response) {
|
const std::function<void(bool, QJsonDocument)>& callback) {
|
||||||
callback(success, QJsonDocument::fromJson(response));
|
|
||||||
|
return makeRequest("j/" + request,
|
||||||
|
[callback](bool success, const QByteArray& res) {
|
||||||
|
callback(success, QJsonDocument::fromJson(res));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
HyprExtras::SocketPtr HyprExtras::makeRequest(
|
HyprExtras::SocketPtr HyprExtras::makeRequest(
|
||||||
const QString& request, const std::function<void(bool, QByteArray)>& callback) {
|
const QString& request,
|
||||||
if (m_requestSocket.isEmpty()) {
|
const std::function<void(bool, QByteArray)>& callback) {
|
||||||
|
|
||||||
|
if (m_requestSocket.isEmpty())
|
||||||
return SocketPtr();
|
return SocketPtr();
|
||||||
}
|
|
||||||
|
|
||||||
auto socket = SocketPtr::create(this);
|
auto socket = SocketPtr::create(this);
|
||||||
|
|
||||||
QObject::connect(socket.data(), &QLocalSocket::connected, this, [=, this]() {
|
connect(socket.data(), &QLocalSocket::connected, this, [=, this]() {
|
||||||
QObject::connect(socket.data(), &QLocalSocket::readyRead, this, [socket, callback]() {
|
connect(socket.data(), &QLocalSocket::readyRead,
|
||||||
const auto response = socket->readAll();
|
this, [socket, callback]() {
|
||||||
callback(true, std::move(response));
|
callback(true, socket->readAll());
|
||||||
socket->close();
|
socket->close();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -207,14 +324,14 @@ HyprExtras::SocketPtr HyprExtras::makeRequest(
|
|||||||
socket->flush();
|
socket->flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
QObject::connect(socket.data(), &QLocalSocket::errorOccurred, this, [=](QLocalSocket::LocalSocketError err) {
|
connect(socket.data(), &QLocalSocket::errorOccurred,
|
||||||
qCWarning(lcHypr) << "makeRequest: error making request:" << err << "| request:" << request;
|
this, [=](QLocalSocket::LocalSocketError err) {
|
||||||
|
qCWarning(lcHypr) << "request error:" << err << request;
|
||||||
callback(false, {});
|
callback(false, {});
|
||||||
socket->close();
|
socket->close();
|
||||||
});
|
});
|
||||||
|
|
||||||
socket->connectToServer(m_requestSocket);
|
socket->connectToServer(m_requestSocket);
|
||||||
|
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user