Desparately try to re-use the tried-and-true IPC Python script for communications with the driver

Creates a wrapper script around the existing IPC library that makes for simpler invocations
This commit is contained in:
wheaney 2025-08-26 11:16:22 -07:00
parent 0d8fb02388
commit 843f7907e7
13 changed files with 310 additions and 78 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ __pycache__
gschemas.compiled
out/
*.po~
kwin/src/xrdriveripc/xrdriveripc.py

View File

@ -59,6 +59,10 @@ rm -rf $XR_DRIVER_TMP_DIR
cp $XR_DRIVER_BINARY $PACKAGE_DIR/xrDriver.tar.gz
cp $XR_DRIVER_DIR/bin/xr_driver_setup $PACKAGE_DIR/bin
# alternative to symlinking, since the Docker build can't resolve to the parent directory
# this file is in .gitignore so it doesn't get duplicated
cp ui/modules/PyXRLinuxDriverIPC/xrdriveripc.py $KWIN_DIR/src/xrdriveripc/xrdriveripc.py
pushd $KWIN_DIR
docker-build/init.sh
docker-build/run-build.sh $BUILD_ARCH

View File

@ -77,6 +77,7 @@ if [ -n "\$QT_PLUGIN_PATH" ]; then
else
export QT_PLUGIN_PATH="$QT_PLUGIN_DIR"
fi
export QT_DEBUG_PLUGINS=1
EOF
fi

View File

@ -30,6 +30,9 @@ RUN pacman -Sy --noconfirm --needed \
kwindowsystem \
kwin \
&& pacman -Scc --noconfirm
RUN pacman -Sy --noconfirm --needed \
python \
&& pacman -Scc --noconfirm
WORKDIR /source

View File

@ -31,6 +31,9 @@ RUN pacman -Sy --noconfirm --needed \
kwindowsystem \
kwin \
&& pacman -Scc --noconfirm
RUN pacman -Sy --noconfirm --needed \
python \
&& pacman -Scc --noconfirm
WORKDIR /source

View File

@ -1,3 +1,4 @@
add_subdirectory(xrdriveripc)
add_subdirectory(kcm)
kcoreaddons_add_plugin(breezy_desktop INSTALL_NAMESPACE "kwin/effects/plugins/")
@ -38,6 +39,7 @@ target_compile_definitions(breezy_desktop PRIVATE
KWIN_VERSION_ENCODED=${KWIN_VERSION_ENCODED}
)
target_include_directories(breezy_desktop PRIVATE /usr/include/kwin)
target_include_directories(breezy_desktop PRIVATE xrdriveripc)
target_link_libraries(breezy_desktop
Qt6::Core
Qt6::Gui
@ -51,6 +53,9 @@ target_link_libraries(breezy_desktop
KF6::WindowSystem
KWin::kwin
xr_driver_ipc
)
install(DIRECTORY qml DESTINATION ${KDE_INSTALL_DATADIR}/kwin/effects/breezy_desktop)

View File

@ -1,11 +1,13 @@
#include "core/rendertarget.h"
#include "core/renderviewport.h"
#include "kcm/shortcuts.h"
#include "breezydesktopeffect.h"
#include "breezydesktopconfig.h"
#include "effect/effect.h"
#include "effect/effecthandler.h"
#include "opengl/glutils.h"
#include "core/rendertarget.h"
#include "core/renderviewport.h"
#include "xrdriveripc.h"
#include <kwin/main.h>
#include <core/outputbackend.h>
@ -80,11 +82,17 @@ BreezyDesktopEffect::BreezyDesktopEffect()
);
setupGlobalShortcut(
BreezyShortcuts::RECENTER,
[this]() { this->recenter(); }
[this]() {
XRDriverIPCBridge::instance().writeControlFlags({
{"recenter_screen", true}
});
}
);
setupGlobalShortcut(
BreezyShortcuts::TOGGLE_ZOOM_ON_FOCUS,
[this]() { this->toggleZoomOnFocus(); }
[this]() {
this->setZoomOnFocusEnabled(!m_zoomOnFocusEnabled);
}
);
connect(effects, &EffectsHandler::cursorShapeChanged, this, &BreezyDesktopEffect::updateCursorImage);
@ -209,60 +217,11 @@ void BreezyDesktopEffect::deactivate()
void BreezyDesktopEffect::enableDriver()
{
qCCritical(KWIN_XR) << "\t\t\tBreezy - enableDriver";
QByteArray homeEnv = qgetenv("HOME");
QString program = QString::fromUtf8(homeEnv) + QStringLiteral("/.local/bin/xr_driver_cli");
// Helper lambda to start the second call
auto setBreezyDesktopMode = [this, program]() {
QProcess *proc2 = new QProcess(this);
proc2->setProgram(program);
proc2->setArguments({QStringLiteral("-bd")}); // change the mode to Breezy Desktop
proc2->setProcessChannelMode(QProcess::MergedChannels);
connect(proc2, &QProcess::readyReadStandardOutput, this, [proc2]() {
const QByteArray out = proc2->readAllStandardOutput();
if (!out.isEmpty()) {
qCInfo(KWIN_XR) << "xr_driver_cli -bd:" << out;
}
});
connect(proc2, &QProcess::errorOccurred, this, [proc2](QProcess::ProcessError err) {
qCCritical(KWIN_XR) << "xr_driver_cli -bd error" << err << proc2->errorString();
});
connect(proc2, QOverload<int,QProcess::ExitStatus>::of(&QProcess::finished),
this, [this, proc2](int code, QProcess::ExitStatus status) {
qCInfo(KWIN_XR) << "xr_driver_cli -bd exited" << code << "status" << status;
proc2->deleteLater();
});
proc2->start();
};
QProcess *proc1 = new QProcess(this);
proc1->setProgram(program);
proc1->setArguments({QStringLiteral("-e")}); // enable flag
proc1->setProcessChannelMode(QProcess::MergedChannels);
connect(proc1, &QProcess::readyReadStandardOutput, this, [proc1]() {
const QByteArray out = proc1->readAllStandardOutput();
if (!out.isEmpty()) {
qCInfo(KWIN_XR) << "xr_driver_cli -e:" << out;
}
XRDriverIPCBridge::instance().writeConfig({
{"disabled", false},
{"output_mode", "external_only"},
{"external_mode", "breezy_desktop"}
});
connect(proc1, &QProcess::errorOccurred, this, [proc1](QProcess::ProcessError err) {
qCCritical(KWIN_XR) << "xr_driver_cli -e error" << err << proc1->errorString();
});
connect(proc1, QOverload<int,QProcess::ExitStatus>::of(&QProcess::finished),
this, [proc1, setBreezyDesktopMode](int code, QProcess::ExitStatus status) {
qCInfo(KWIN_XR) << "xr_driver_cli -e exited" << code << "status" << status;
proc1->deleteLater();
if (status == QProcess::NormalExit && code == 0) {
setBreezyDesktopMode();
} else {
qCCritical(KWIN_XR) << "First call failed; not starting second.";
}
});
proc1->start();
}
void BreezyDesktopEffect::realDeactivate()
@ -271,20 +230,6 @@ void BreezyDesktopEffect::realDeactivate()
setRunning(false);
}
void BreezyDesktopEffect::recenter()
{
QFile controlFile(QStringLiteral("/dev/shm/xr_driver_control"));
if (controlFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
controlFile.write("recenter_screen=true\n");
controlFile.close();
}
}
void BreezyDesktopEffect::toggleZoomOnFocus()
{
setZoomOnFocusEnabled(!m_zoomOnFocusEnabled);
}
void BreezyDesktopEffect::addVirtualDisplay(QSize size)
{
// QSize size(2560, 1440);

View File

@ -68,8 +68,6 @@ namespace KWin
void deactivate();
void enableDriver();
void toggle();
void recenter();
void toggleZoomOnFocus();
void addVirtualDisplay(QSize size);
void updateImuRotation();
void updateCursorImage();

View File

@ -1,7 +1,3 @@
# SPDX-FileCopyrightText: 2022 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
#
# SPDX-License-Identifier: BSD-3-Clause
set(breezy_desktop_config_SOURCES breezydesktopeffectkcm.cpp)
ki18n_wrap_ui(breezy_desktop_config_SOURCES breezydesktopeffectkcm.ui)
qt_add_dbus_interface(breezy_desktop_config_SOURCES ${KWIN_EFFECTS_INTERFACE} kwineffects_interface)
@ -17,4 +13,6 @@ target_link_libraries(breezy_desktop_config
KF6::I18n
KF6::KCMUtils
KF6::XmlGui
xr_driver_ipc
)

View File

@ -0,0 +1,26 @@
add_library(xr_driver_ipc STATIC
xrdriveripc.cpp
)
# Ensure position independent code so the static archive can link into the KWin effect plugin (a shared module)
set_target_properties(xr_driver_ipc PROPERTIES POSITION_INDEPENDENT_CODE ON)
# Generate an export header so symbols can be visible outside the shared lib
include(GenerateExportHeader)
generate_export_header(xr_driver_ipc EXPORT_FILE_NAME xr_driver_ipc_export.h)
target_include_directories(xr_driver_ipc
PUBLIC
${CMAKE_CURRENT_BINARY_DIR} # for generated export header
)
target_compile_options(xr_driver_ipc PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/EHsc>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-fexceptions>
)
target_link_libraries(xr_driver_ipc
PRIVATE Qt6::Core
)
install(FILES xrdriveripc.py xrdriveripc_runner.py DESTINATION ${KDE_INSTALL_DATADIR}/kwin/effects/breezy_desktop)

View File

@ -0,0 +1,134 @@
// New implementation using QProcess to call python
#include "xrdriveripc.h"
#include <iostream>
#include <cmath>
#include <QFileInfo>
#include <QProcess>
#include <QProcessEnvironment>
#include <QStandardPaths>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
XRDriverIPCBridge &XRDriverIPCBridge::instance() {
static XRDriverIPCBridge inst;
if (!inst.m_initialized) {
QString installedFile = QStandardPaths::locate(
QStandardPaths::GenericDataLocation,
QStringLiteral("kwin/effects/breezy_desktop/xrdriveripc.py"),
QStandardPaths::LocateFile);
if (installedFile.isEmpty()) {
throw std::runtime_error("Cannot locate kwin/effects/breezy_desktop/xrdriveripc.py");
}
inst.m_pythonDir = QFileInfo(installedFile).path();
inst.m_initialized = true;
}
return inst;
}
std::string XRDriverIPCBridge::configHome() const {
QString configHome = QString::fromUtf8(qgetenv("XDG_CONFIG_HOME"));
if (configHome.isEmpty()) {
QString homeDir = QString::fromUtf8(qgetenv("HOME"));
configHome = homeDir + QStringLiteral("/.config");
}
return configHome.toStdString();
}
QByteArray XRDriverIPCBridge::invokePython(const QString &method,
const QByteArray &payloadJson,
const QString &singleArg) const {
QProcess proc;
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("BREEZY_METHOD"), method);
env.insert(QStringLiteral("BREEZY_CONFIG_HOME"), QString::fromStdString(configHome()));
if (!singleArg.isEmpty()) env.insert(QStringLiteral("BREEZY_ARG"), singleArg);
if (!payloadJson.isEmpty()) env.insert(QStringLiteral("BREEZY_PAYLOAD"), QString::fromUtf8(payloadJson));
proc.setProcessEnvironment(env);
// Expect xrdriveripc_runner.py to reside in the same directory as xrdriveripc.py (m_pythonDir)
QString wrapperPath = m_pythonDir + QStringLiteral("/xrdriveripc_runner.py");
proc.start(QStringLiteral("python3"), QStringList() << wrapperPath);
if (!proc.waitForStarted(5000)) {
std::cerr << "Failed to start python process" << std::endl;
return {};
}
proc.closeWriteChannel();
if (!proc.waitForFinished(15000)) {
proc.kill();
std::cerr << "Python process timeout" << std::endl;
return {};
}
if (proc.exitStatus() != QProcess::NormalExit || proc.exitCode() != 0) {
std::cerr << "Python process failed (" << proc.exitCode() << "):\n"
<< proc.readAllStandardError().toStdString() << std::endl;
return {};
}
return proc.readAllStandardOutput().trimmed();
}
static XRDict jsonToXRDict(const QJsonObject &obj) {
XRDict out;
for (auto it = obj.begin(); it != obj.end(); ++it) {
const QString &k = it.key();
const QJsonValue &v = it.value();
if (v.isBool()) out[k.toStdString()] = v.toBool();
else if (v.isDouble() && std::floor(v.toDouble()) == v.toDouble())
out[k.toStdString()] = (int)v.toDouble();
else if (v.isDouble()) out[k.toStdString()] = v.toDouble();
else if (v.isString()) out[k.toStdString()] = v.toString().toStdString();
else out[k.toStdString()] = std::monostate{};
}
return out;
}
std::optional<XRDict> XRDriverIPCBridge::retrieveConfig() {
QByteArray out = invokePython(QStringLiteral("retrieve_config"), {}, QStringLiteral("1"));
if (out.isEmpty()) return std::nullopt;
QJsonParseError err; auto doc = QJsonDocument::fromJson(out, &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) return std::nullopt;
return jsonToXRDict(doc.object());
}
std::optional<XRDict> XRDriverIPCBridge::retrieveDriverState() {
QByteArray out = invokePython(QStringLiteral("retrieve_driver_state"), {}, {});
if (out.isEmpty()) return std::nullopt;
QJsonParseError err; auto doc = QJsonDocument::fromJson(out, &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) return std::nullopt;
return jsonToXRDict(doc.object());
}
bool XRDriverIPCBridge::writeConfig(const XRDict &configUpdate) {
QJsonObject obj;
for (const auto &kv : configUpdate) {
const std::string &k = kv.first; const XRValue &v = kv.second;
if (std::holds_alternative<bool>(v)) obj.insert(QString::fromStdString(k), std::get<bool>(v));
else if (std::holds_alternative<int>(v)) obj.insert(QString::fromStdString(k), std::get<int>(v));
else if (std::holds_alternative<double>(v)) obj.insert(QString::fromStdString(k), std::get<double>(v));
else if (std::holds_alternative<std::string>(v)) obj.insert(QString::fromStdString(k), QString::fromStdString(std::get<std::string>(v)));
}
QByteArray payload = QJsonDocument(obj).toJson(QJsonDocument::Compact);
QByteArray out = invokePython(QStringLiteral("write_config"), payload, {});
return !out.isEmpty();
}
bool XRDriverIPCBridge::writeControlFlags(const std::map<std::string, bool> &flags) {
QJsonObject obj; for (const auto &kv : flags) obj.insert(QString::fromStdString(kv.first), kv.second);
QByteArray payload = QJsonDocument(obj).toJson(QJsonDocument::Compact);
QByteArray out = invokePython(QStringLiteral("write_control_flags"), payload, {});
return !out.isEmpty();
}
bool XRDriverIPCBridge::requestToken(const std::string &email) {
QByteArray out = invokePython(QStringLiteral("request_token"), {}, QString::fromStdString(email));
if (out.isEmpty()) return false;
auto value = QJsonValue(QString::fromStdString(out.toStdString()));
return value.isBool() ? value.toBool() : false;
}
bool XRDriverIPCBridge::verifyToken(const std::string &token) {
QByteArray out = invokePython(QStringLiteral("verify_token"), {}, QString::fromStdString(token));
if (out.isEmpty()) return false;
auto value = QJsonValue(QString::fromStdString(out.toStdString()));
return value.isBool() ? value.toBool() : false;
}

View File

@ -0,0 +1,51 @@
// C++ bridge now invoking xrdriveripc via external python process
#pragma once
#include <string>
#include <map>
#include <variant>
#include <vector>
#include <optional>
#include <QString>
#include <QByteArray>
// Export header generated by CMake (GenerateExportHeader)
#ifdef __has_include
# if __has_include("xr_driver_ipc_export.h")
# include "xr_driver_ipc_export.h"
# endif
#endif
#ifndef XR_DRIVER_IPC_EXPORT
# define XR_DRIVER_IPC_EXPORT __attribute__((visibility("default")))
#endif
// Simple variant type for config/state key values we care about
using XRValue = std::variant<std::monostate, bool, int, double, std::string>;
using XRDict = std::map<std::string, XRValue>;
class XR_DRIVER_IPC_EXPORT XRDriverIPCBridge {
public:
static XRDriverIPCBridge &instance();
std::optional<XRDict> retrieveConfig();
std::optional<XRDict> retrieveDriverState();
bool writeConfig(const XRDict &configUpdate);
bool writeControlFlags(const std::map<std::string, bool> &flags);
bool requestToken(const std::string &email);
bool verifyToken(const std::string &token);
private:
XRDriverIPCBridge() = default;
~XRDriverIPCBridge() = default;
XRDriverIPCBridge(const XRDriverIPCBridge&) = delete;
XRDriverIPCBridge& operator=(const XRDriverIPCBridge&) = delete;
std::string configHome() const;
QByteArray invokePython(const QString &method,
const QByteArray &payloadJson,
const QString &singleArg) const;
bool m_initialized = false;
QString m_pythonDir; // directory containing xrdriveripc.py
};

View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Wrapper script invoked by xrdriveripc.cpp via QProcess.
It reads environment variables to determine which XRDriverIPC method to call
and prints the JSON-serialized result to stdout, mirroring the prior inline
python one-liner implementation.
"""
from __future__ import annotations
import json
import os
import sys
import traceback
def main() -> int:
# Ensure the current directory (where xrdriveripc.py lives) is in sys.path
script_dir = os.path.dirname(os.path.abspath(__file__))
if script_dir not in sys.path:
sys.path.insert(0, script_dir)
try:
import xrdriveripc # type: ignore
except Exception as e: # pragma: no cover - import failure path
print("Failed to import xrdriveripc: %s" % e, file=sys.stderr)
return 2
method = os.environ.get("BREEZY_METHOD")
if not method:
print("BREEZY_METHOD not set", file=sys.stderr)
return 2
config_home = os.environ.get("BREEZY_CONFIG_HOME")
inst = xrdriveripc.XRDriverIPC(config_home=config_home)
arg = os.environ.get("BREEZY_ARG")
payload_raw = os.environ.get("BREEZY_PAYLOAD")
# Dispatch replicating previous inline logic
try:
if method == "retrieve_config":
res = getattr(inst, method)(int(arg) if arg else 1)
elif method in ("write_config", "write_control_flags") and payload_raw:
res = getattr(inst, method)(json.loads(payload_raw))
elif method in ("request_token", "verify_token") and arg:
res = getattr(inst, method)(arg)
else:
res = getattr(inst, method)()
except Exception: # pragma: no cover - runtime failure path
traceback.print_exc()
return 3
try:
print(json.dumps(res))
except Exception: # pragma: no cover
traceback.print_exc()
return 3
return 0
if __name__ == "__main__": # pragma: no cover
sys.exit(main())