io/ipchandler: add signal listener support
Some checks failed
Build / Nix (push) Has been cancelled
Build / Nix-1 (push) Has been cancelled
Build / Nix-2 (push) Has been cancelled
Build / Nix-3 (push) Has been cancelled
Build / Nix-4 (push) Has been cancelled
Build / Nix-5 (push) Has been cancelled
Build / Nix-6 (push) Has been cancelled
Build / Nix-7 (push) Has been cancelled
Build / Nix-8 (push) Has been cancelled
Build / Nix-9 (push) Has been cancelled
Build / Nix-10 (push) Has been cancelled
Build / Nix-11 (push) Has been cancelled
Build / Nix-12 (push) Has been cancelled
Build / Nix-13 (push) Has been cancelled
Build / Nix-14 (push) Has been cancelled
Build / Nix-15 (push) Has been cancelled
Build / Nix-16 (push) Has been cancelled
Build / Nix-17 (push) Has been cancelled
Build / Nix-18 (push) Has been cancelled
Build / Nix-19 (push) Has been cancelled
Build / Nix-20 (push) Has been cancelled
Build / Nix-21 (push) Has been cancelled
Build / Nix-22 (push) Has been cancelled
Build / Nix-23 (push) Has been cancelled
Build / Nix-24 (push) Has been cancelled
Build / Nix-25 (push) Has been cancelled
Build / Nix-26 (push) Has been cancelled
Build / Nix-27 (push) Has been cancelled
Build / Nix-28 (push) Has been cancelled
Build / Nix-29 (push) Has been cancelled
Build / Nix-30 (push) Has been cancelled
Build / Nix-31 (push) Has been cancelled
Build / Nix-32 (push) Has been cancelled
Build / Nix-33 (push) Has been cancelled
Build / Archlinux (push) Has been cancelled
Lint / Lint (push) Has been cancelled

This commit is contained in:
outfoxxed 2026-01-15 23:04:10 -08:00
parent 783b97152a
commit d03c59768c
No known key found for this signature in database
GPG key ID: 4C88A185FB89301E
13 changed files with 405 additions and 20 deletions

View file

@ -24,7 +24,7 @@ qt_add_qml_module(quickshell-io
qs_add_module_deps_light(quickshell-io Quickshell)
install_qml_module(quickshell-io)
target_link_libraries(quickshell-io PRIVATE Qt::Quick)
target_link_libraries(quickshell-io PRIVATE Qt::Quick quickshell-ipc)
target_link_libraries(quickshell PRIVATE quickshell-ioplugin)
qs_module_pch(quickshell-io)

View file

@ -190,6 +190,14 @@ QString WirePropertyDefinition::toString() const {
return "property " % this->name % ": " % this->type;
}
QString WireSignalDefinition::toString() const {
if (this->rettype.isEmpty()) {
return "signal " % this->name % "()";
} else {
return "signal " % this->name % "(" % this->retname % ": " % this->rettype % ')';
}
}
QString WireTargetDefinition::toString() const {
QString accum = "target " % this->name;
@ -201,6 +209,10 @@ QString WireTargetDefinition::toString() const {
accum += "\n " % prop.toString();
}
for (const auto& sig: this->signalFunctions) {
accum += "\n " % sig.toString();
}
return accum;
}

View file

@ -146,14 +146,31 @@ struct WirePropertyDefinition {
DEFINE_SIMPLE_DATASTREAM_OPS(WirePropertyDefinition, data.name, data.type);
struct WireTargetDefinition {
struct WireSignalDefinition {
QString name;
QVector<WireFunctionDefinition> functions;
QVector<WirePropertyDefinition> properties;
QString retname;
QString rettype;
[[nodiscard]] QString toString() const;
};
DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions, data.properties);
DEFINE_SIMPLE_DATASTREAM_OPS(WireSignalDefinition, data.name, data.retname, data.rettype);
struct WireTargetDefinition {
QString name;
QVector<WireFunctionDefinition> functions;
QVector<WirePropertyDefinition> properties;
QVector<WireSignalDefinition> signalFunctions;
[[nodiscard]] QString toString() const;
};
DEFINE_SIMPLE_DATASTREAM_OPS(
WireTargetDefinition,
data.name,
data.functions,
data.properties,
data.signalFunctions
);
} // namespace qs::io::ipc

View file

@ -1,9 +1,11 @@
#include "ipccomm.hpp"
#include <utility>
#include <variant>
#include <qcontainerfwd.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtextstream.h>
#include <qtypes.h>
@ -18,10 +20,6 @@ using namespace qs::ipc;
namespace qs::io::ipc::comm {
struct NoCurrentGeneration: std::monostate {};
struct TargetNotFound: std::monostate {};
struct EntryNotFound: std::monostate {};
using QueryResponse = std::variant<
std::monostate,
NoCurrentGeneration,
@ -313,4 +311,106 @@ int getProperty(IpcClient* client, const QString& target, const QString& propert
return -1;
}
int listenToSignal(IpcClient* client, const QString& target, const QString& signal, bool once) {
if (target.isEmpty()) {
qCCritical(logBare) << "Target required to listen for signals.";
return -1;
} else if (signal.isEmpty()) {
qCCritical(logBare) << "Signal required to listen.";
return -1;
}
client->sendMessage(IpcCommand(SignalListenCommand {.target = target, .signal = signal}));
while (true) {
SignalListenResponse slot;
if (!client->waitForResponse(slot)) return -1;
if (std::holds_alternative<SignalResponse>(slot)) {
auto& result = std::get<SignalResponse>(slot);
QTextStream(stdout) << result.response << Qt::endl;
if (once) return 0;
else continue;
} else if (std::holds_alternative<TargetNotFound>(slot)) {
qCCritical(logBare) << "Target not found.";
} else if (std::holds_alternative<EntryNotFound>(slot)) {
qCCritical(logBare) << "Signal not found.";
} else if (std::holds_alternative<NoCurrentGeneration>(slot)) {
qCCritical(logBare) << "Not ready to accept queries yet.";
} else {
qCCritical(logIpc) << "Received invalid IPC response from" << client;
}
break;
}
return -1;
}
void SignalListenCommand::exec(qs::ipc::IpcServerConnection* conn) {
auto resp = conn->responseStream<SignalListenResponse>();
if (auto* generation = EngineGeneration::currentGeneration()) {
auto* registry = IpcHandlerRegistry::forGeneration(generation);
auto* handler = registry->findHandler(this->target);
if (!handler) {
resp << TargetNotFound();
return;
}
auto* signal = handler->findSignal(this->signal);
if (!signal) {
resp << EntryNotFound();
return;
}
new RemoteSignalListener(conn, *this);
} else {
conn->respond(SignalListenResponse(NoCurrentGeneration()));
}
}
RemoteSignalListener::RemoteSignalListener(
qs::ipc::IpcServerConnection* conn,
SignalListenCommand command
)
: conn(conn)
, command(std::move(command)) {
conn->setParent(this);
QObject::connect(
IpcSignalRemoteListener::instance(),
&IpcSignalRemoteListener::triggered,
this,
&RemoteSignalListener::onSignal
);
QObject::connect(
conn,
&qs::ipc::IpcServerConnection::destroyed,
this,
&RemoteSignalListener::onConnDestroyed
);
qCDebug(logIpc) << "Remote listener created for" << this->command.target << this->command.signal
<< ":" << this;
}
RemoteSignalListener::~RemoteSignalListener() {
qCDebug(logIpc) << "Destroying remote listener" << this;
}
void RemoteSignalListener::onSignal(
const QString& target,
const QString& signal,
const QString& value
) {
if (target != this->command.target || signal != this->command.signal) return;
qCDebug(logIpc) << "Remote signal" << signal << "triggered on" << target << "with value" << value;
this->conn->respond(SignalListenResponse(SignalResponse {.response = value}));
}
void RemoteSignalListener::onConnDestroyed() { this->deleteLater(); }
} // namespace qs::io::ipc::comm

View file

@ -2,6 +2,8 @@
#include <qcontainerfwd.h>
#include <qflags.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../ipc/ipc.hpp"
@ -48,4 +50,52 @@ DEFINE_SIMPLE_DATASTREAM_OPS(StringPropReadCommand, data.target, data.property);
int getProperty(qs::ipc::IpcClient* client, const QString& target, const QString& property);
struct SignalListenCommand {
QString target;
QString signal;
void exec(qs::ipc::IpcServerConnection* conn);
};
DEFINE_SIMPLE_DATASTREAM_OPS(SignalListenCommand, data.target, data.signal);
int listenToSignal(
qs::ipc::IpcClient* client,
const QString& target,
const QString& signal,
bool once
);
struct NoCurrentGeneration: std::monostate {};
struct TargetNotFound: std::monostate {};
struct EntryNotFound: std::monostate {};
struct SignalResponse {
QString response;
};
DEFINE_SIMPLE_DATASTREAM_OPS(SignalResponse, data.response);
using SignalListenResponse = std::
variant<std::monostate, NoCurrentGeneration, TargetNotFound, EntryNotFound, SignalResponse>;
class RemoteSignalListener: public QObject {
Q_OBJECT;
public:
explicit RemoteSignalListener(qs::ipc::IpcServerConnection* conn, SignalListenCommand command);
~RemoteSignalListener() override;
Q_DISABLE_COPY_MOVE(RemoteSignalListener);
private slots:
void onSignal(const QString& target, const QString& signal, const QString& value);
void onConnDestroyed();
private:
qs::ipc::IpcServerConnection* conn;
SignalListenCommand command;
};
} // namespace qs::io::ipc::comm

View file

@ -1,5 +1,7 @@
#include "ipchandler.hpp"
#include <cstddef>
#include <memory>
#include <utility>
#include <qcontainerfwd.h>
#include <qdebug.h>
@ -139,6 +141,75 @@ WirePropertyDefinition IpcProperty::wireDef() const {
return wire;
}
WireSignalDefinition IpcSignal::wireDef() const {
WireSignalDefinition wire;
wire.name = this->signal.name();
if (this->targetSlot != IpcSignalListener::SLOT_VOID) {
wire.retname = this->signal.parameterNames().value(0);
if (this->targetSlot == IpcSignalListener::SLOT_STRING) wire.rettype = "string";
else if (this->targetSlot == IpcSignalListener::SLOT_INT) wire.rettype = "int";
else if (this->targetSlot == IpcSignalListener::SLOT_BOOL) wire.rettype = "bool";
else if (this->targetSlot == IpcSignalListener::SLOT_REAL) wire.rettype = "real";
else if (this->targetSlot == IpcSignalListener::SLOT_COLOR) wire.rettype = "color";
}
return wire;
}
// NOLINTBEGIN (cppcoreguidelines-interfaces-global-init)
// clang-format off
const int IpcSignalListener::SLOT_VOID = IpcSignalListener::staticMetaObject.indexOfSlot("invokeVoid()");
const int IpcSignalListener::SLOT_STRING = IpcSignalListener::staticMetaObject.indexOfSlot("invokeString(QString)");
const int IpcSignalListener::SLOT_INT = IpcSignalListener::staticMetaObject.indexOfSlot("invokeInt(int)");
const int IpcSignalListener::SLOT_BOOL = IpcSignalListener::staticMetaObject.indexOfSlot("invokeBool(bool)");
const int IpcSignalListener::SLOT_REAL = IpcSignalListener::staticMetaObject.indexOfSlot("invokeReal(double)");
const int IpcSignalListener::SLOT_COLOR = IpcSignalListener::staticMetaObject.indexOfSlot("invokeColor(QColor)");
// clang-format on
// NOLINTEND
bool IpcSignal::resolve(QString& error) {
if (this->signal.parameterCount() > 1) {
error = "Due to technical limitations, IPC signals can have at most one argument.";
return false;
}
auto slot = IpcSignalListener::SLOT_VOID;
if (this->signal.parameterCount() == 1) {
auto paramType = this->signal.parameterType(0);
if (paramType == QMetaType::QString) slot = IpcSignalListener::SLOT_STRING;
else if (paramType == QMetaType::Int) slot = IpcSignalListener::SLOT_INT;
else if (paramType == QMetaType::Bool) slot = IpcSignalListener::SLOT_BOOL;
else if (paramType == QMetaType::Double) slot = IpcSignalListener::SLOT_REAL;
else if (paramType == QMetaType::QColor) slot = IpcSignalListener::SLOT_COLOR;
else {
error = QString("Type of argument (%2: %3) cannot be used across IPC.")
.arg(this->signal.parameterNames().value(0))
.arg(QMetaType(paramType).name());
return false;
}
}
this->targetSlot = slot;
return true;
}
void IpcSignal::connectListener(IpcHandler* handler) {
if (this->targetSlot == -1) {
qFatal() << "Tried to connect unresolved IPC signal";
}
this->listener = std::make_shared<IpcSignalListener>(this->signal.name());
QMetaObject::connect(handler, this->signal.methodIndex(), this->listener.get(), this->targetSlot);
QObject::connect(
this->listener.get(),
&IpcSignalListener::triggered,
handler,
&IpcHandler::onSignalTriggered
);
}
IpcCallStorage::IpcCallStorage(const IpcFunction& function): returnSlot(function.returnType) {
for (const auto& arg: function.argumentTypes) {
this->argumentSlots.emplace_back(arg);
@ -172,16 +243,28 @@ void IpcHandler::onPostReload() {
// which should handle inheritance on the qml side.
for (auto i = smeta.methodCount(); i != meta->methodCount(); i++) {
const auto& method = meta->method(i);
if (method.methodType() != QMetaMethod::Slot) continue;
if (method.methodType() == QMetaMethod::Slot) {
auto ipcFunc = IpcFunction(method);
QString error;
auto ipcFunc = IpcFunction(method);
QString error;
if (!ipcFunc.resolve(error)) {
qmlWarning(this).nospace().noquote()
<< "Error parsing function \"" << method.name() << "\": " << error;
} else {
this->functionMap.insert(method.name(), ipcFunc);
}
} else if (method.methodType() == QMetaMethod::Signal) {
qmlDebug(this) << "Signal detected: " << method.name();
auto ipcSig = IpcSignal(method);
QString error;
if (!ipcFunc.resolve(error)) {
qmlWarning(this).nospace().noquote()
<< "Error parsing function \"" << method.name() << "\": " << error;
} else {
this->functionMap.insert(method.name(), ipcFunc);
if (!ipcSig.resolve(error)) {
qmlWarning(this).nospace().noquote()
<< "Error parsing signal \"" << method.name() << "\": " << error;
} else {
ipcSig.connectListener(this);
this->signalMap.emplace(method.name(), std::move(ipcSig));
}
}
}
@ -222,6 +305,11 @@ IpcHandlerRegistry* IpcHandlerRegistry::forGeneration(EngineGeneration* generati
return dynamic_cast<IpcHandlerRegistry*>(ext);
}
void IpcHandler::onSignalTriggered(const QString& signal, const QString& value) const {
emit IpcSignalRemoteListener::instance()
-> triggered(this->registeredState.target, signal, value);
}
void IpcHandler::updateRegistration(bool destroying) {
if (!this->complete) return;
@ -324,6 +412,10 @@ WireTargetDefinition IpcHandler::wireDef() const {
wire.properties += prop.wireDef();
}
for (const auto& sig: this->signalMap.values()) {
wire.signalFunctions += sig.wireDef();
}
return wire;
}
@ -368,6 +460,13 @@ IpcProperty* IpcHandler::findProperty(const QString& name) {
else return &*itr;
}
IpcSignal* IpcHandler::findSignal(const QString& name) {
auto itr = this->signalMap.find(name);
if (itr == this->signalMap.end()) return nullptr;
else return &*itr;
}
IpcHandler* IpcHandlerRegistry::findHandler(const QString& target) {
return this->handlers.value(target);
}
@ -382,4 +481,9 @@ QVector<WireTargetDefinition> IpcHandlerRegistry::wireTargets() const {
return wire;
}
IpcSignalRemoteListener* IpcSignalRemoteListener::instance() {
static auto* instance = new IpcSignalRemoteListener();
return instance;
}
} // namespace qs::io::ipc

View file

@ -1,8 +1,10 @@
#pragma once
#include <cstddef>
#include <memory>
#include <vector>
#include <qcolor.h>
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qhash.h>
@ -67,6 +69,54 @@ public:
const IpcType* type = nullptr;
};
class IpcSignalListener: public QObject {
Q_OBJECT;
public:
IpcSignalListener(QString signal): signal(std::move(signal)) {}
static const int SLOT_VOID;
static const int SLOT_STRING;
static const int SLOT_INT;
static const int SLOT_BOOL;
static const int SLOT_REAL;
static const int SLOT_COLOR;
signals:
void triggered(const QString& signal, const QString& value);
private slots:
void invokeVoid() { this->triggered(this->signal, "void"); }
void invokeString(const QString& value) { this->triggered(this->signal, value); }
void invokeInt(int value) { this->triggered(this->signal, QString::number(value)); }
void invokeBool(bool value) { this->triggered(this->signal, value ? "true" : "false"); }
void invokeReal(double value) { this->triggered(this->signal, QString::number(value)); }
void invokeColor(QColor value) { this->triggered(this->signal, value.name(QColor::HexArgb)); }
private:
QString signal;
};
class IpcHandler;
class IpcSignal {
public:
explicit IpcSignal(QMetaMethod signal): signal(signal) {}
bool resolve(QString& error);
[[nodiscard]] WireSignalDefinition wireDef() const;
QMetaMethod signal;
int targetSlot = -1;
void connectListener(IpcHandler* handler);
private:
void connectListener(QObject* handler, IpcSignalListener* listener) const;
std::shared_ptr<IpcSignalListener> listener;
};
class IpcHandlerRegistry;
///! Handler for IPC message calls.
@ -100,6 +150,11 @@ class IpcHandlerRegistry;
/// - `real` will be converted to a string and returned.
/// - `color` will be converted to a hex string in the form `#AARRGGBB` and returned.
///
/// #### Signals
/// IPC handler signals can be observed remotely using `qs ipc wait` (one call)
/// and `qs ipc listen` (many calls). IPC signals may have zero or one argument, where
/// the argument is one of the types listed above, or no arguments for void.
///
/// #### Example
/// The following example creates ipc functions to control and retrieve the appearance
/// of a Rectangle.
@ -119,10 +174,18 @@ class IpcHandlerRegistry;
///
/// function setColor(color: color): void { rect.color = color; }
/// function getColor(): color { return rect.color; }
///
/// function setAngle(angle: real): void { rect.rotation = angle; }
/// function getAngle(): real { return rect.rotation; }
/// function setRadius(radius: int): void { rect.radius = radius; }
///
/// function setRadius(radius: int): void {
/// rect.radius = radius;
/// this.radiusChanged(radius);
/// }
///
/// function getRadius(): int { return rect.radius; }
///
/// signal radiusChanged(newRadius: int);
/// }
/// }
/// ```
@ -136,6 +199,7 @@ class IpcHandlerRegistry;
/// function getAngle(): real
/// function setRadius(radius: int): void
/// function getRadius(): int
/// signal radiusChanged(newRadius: int)
/// ```
///
/// and then invoked using `qs ipc call`.
@ -179,14 +243,15 @@ public:
QString listMembers(qsizetype indent);
[[nodiscard]] IpcFunction* findFunction(const QString& name);
[[nodiscard]] IpcProperty* findProperty(const QString& name);
[[nodiscard]] IpcSignal* findSignal(const QString& name);
[[nodiscard]] WireTargetDefinition wireDef() const;
signals:
void enabledChanged();
void targetChanged();
private slots:
//void handleIpcPropertyChange();
public slots:
void onSignalTriggered(const QString& signal, const QString& value) const;
private:
void updateRegistration(bool destroying = false);
@ -204,6 +269,7 @@ private:
QHash<QString, IpcFunction> functionMap;
QHash<QString, IpcProperty> propertyMap;
QHash<QString, IpcSignal> signalMap;
friend class IpcHandlerRegistry;
};
@ -227,4 +293,14 @@ private:
QHash<QString, QVector<IpcHandler*>> knownHandlers;
};
class IpcSignalRemoteListener: public QObject {
Q_OBJECT;
public:
static IpcSignalRemoteListener* instance();
signals:
void triggered(const QString& target, const QString& signal, const QString& value);
};
} // namespace qs::io::ipc

View file

@ -61,6 +61,7 @@ IpcServerConnection::IpcServerConnection(QLocalSocket* socket, IpcServer* server
void IpcServerConnection::onDisconnected() {
qCInfo(logIpc) << "IPC connection disconnected" << this;
delete this;
}
void IpcServerConnection::onReadyRead() {
@ -84,6 +85,11 @@ void IpcServerConnection::onReadyRead() {
);
if (!this->stream.commitTransaction()) return;
// async connections reparent
if (dynamic_cast<IpcServer*>(this->parent()) != nullptr) {
delete this;
}
}
IpcClient::IpcClient(const QString& path) {

View file

@ -16,6 +16,7 @@ using IpcCommand = std::variant<
IpcKillCommand,
qs::io::ipc::comm::QueryMetadataCommand,
qs::io::ipc::comm::StringCallCommand,
qs::io::ipc::comm::SignalListenCommand,
qs::io::ipc::comm::StringPropReadCommand>;
} // namespace qs::ipc

View file

@ -411,6 +411,10 @@ int ipcCommand(CommandState& cmd) {
return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.name);
} else if (*cmd.ipc.getprop) {
return qs::io::ipc::comm::getProperty(&client, *cmd.ipc.target, *cmd.ipc.name);
} else if (*cmd.ipc.wait) {
return qs::io::ipc::comm::listenToSignal(&client, *cmd.ipc.target, *cmd.ipc.name, true);
} else if (*cmd.ipc.listen) {
return qs::io::ipc::comm::listenToSignal(&client, *cmd.ipc.target, *cmd.ipc.name, false);
} else {
QVector<QString> arguments;
for (auto& arg: cmd.ipc.arguments) {

View file

@ -74,6 +74,8 @@ struct CommandState {
CLI::App* show = nullptr;
CLI::App* call = nullptr;
CLI::App* getprop = nullptr;
CLI::App* wait = nullptr;
CLI::App* listen = nullptr;
bool showOld = false;
QStringOption target;
QStringOption name;

View file

@ -1,6 +1,7 @@
#include <cstddef>
#include <limits>
#include <memory>
#include <utility>
#include <CLI/App.hpp>
#include <CLI/CLI.hpp> // NOLINT: Need to include this for impls of some CLI11 classes
@ -226,6 +227,16 @@ int parseCommand(int argc, char** argv, CommandState& state) {
->allow_extra_args();
}
auto signalCmd = [&](std::string cmd, std::string desc) {
auto* scmd = sub->add_subcommand(std::move(cmd), std::move(desc));
scmd->add_option("target", state.ipc.target, "The target to listen on.");
scmd->add_option("signal", state.ipc.name, "The signal to listen for.");
return scmd;
};
state.ipc.wait = signalCmd("wait", "Wait for one IpcHandler signal.");
state.ipc.listen = signalCmd("listen", "Listen for IpcHandler signals.");
{
auto* prop =
sub->add_subcommand("prop", "Manipulate IpcHandler properties.")->require_subcommand();