diff --git a/.gitignore b/.gitignore index 51c61dc..ae97f4c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__ gschemas.compiled out/ *.po~ +kwin/src/xrdriveripc/xrdriveripc.py diff --git a/bin/package_kwin b/bin/package_kwin index f176aa7..b30f076 100755 --- a/bin/package_kwin +++ b/bin/package_kwin @@ -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 diff --git a/kwin/bin/setup b/kwin/bin/setup index 270e559..23a532d 100755 --- a/kwin/bin/setup +++ b/kwin/bin/setup @@ -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 diff --git a/kwin/docker-build/Dockerfile b/kwin/docker-build/Dockerfile index 89d8677..51def55 100644 --- a/kwin/docker-build/Dockerfile +++ b/kwin/docker-build/Dockerfile @@ -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 diff --git a/kwin/docker-build/Dockerfile.steamos b/kwin/docker-build/Dockerfile.steamos index 23f32e6..3670dbc 100644 --- a/kwin/docker-build/Dockerfile.steamos +++ b/kwin/docker-build/Dockerfile.steamos @@ -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 diff --git a/kwin/src/CMakeLists.txt b/kwin/src/CMakeLists.txt index e6ad0c2..619ee0b 100644 --- a/kwin/src/CMakeLists.txt +++ b/kwin/src/CMakeLists.txt @@ -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) + +install(DIRECTORY qml DESTINATION ${KDE_INSTALL_DATADIR}/kwin/effects/breezy_desktop) \ No newline at end of file diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 3ef6549..8f50393 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -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 #include @@ -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::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::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); diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h index c9e438a..77db59a 100644 --- a/kwin/src/breezydesktopeffect.h +++ b/kwin/src/breezydesktopeffect.h @@ -68,8 +68,6 @@ namespace KWin void deactivate(); void enableDriver(); void toggle(); - void recenter(); - void toggleZoomOnFocus(); void addVirtualDisplay(QSize size); void updateImuRotation(); void updateCursorImage(); diff --git a/kwin/src/kcm/CMakeLists.txt b/kwin/src/kcm/CMakeLists.txt index 5a0913b..7152d1d 100644 --- a/kwin/src/kcm/CMakeLists.txt +++ b/kwin/src/kcm/CMakeLists.txt @@ -1,7 +1,3 @@ -# SPDX-FileCopyrightText: 2022 Vlad Zahorodnii -# -# 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 ) diff --git a/kwin/src/xrdriveripc/CMakeLists.txt b/kwin/src/xrdriveripc/CMakeLists.txt new file mode 100644 index 0000000..81887b6 --- /dev/null +++ b/kwin/src/xrdriveripc/CMakeLists.txt @@ -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 + $<$:/EHsc> + $<$>:-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) \ No newline at end of file diff --git a/kwin/src/xrdriveripc/xrdriveripc.cpp b/kwin/src/xrdriveripc/xrdriveripc.cpp new file mode 100644 index 0000000..8d7b93b --- /dev/null +++ b/kwin/src/xrdriveripc/xrdriveripc.cpp @@ -0,0 +1,134 @@ +// New implementation using QProcess to call python +#include "xrdriveripc.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 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(v)) obj.insert(QString::fromStdString(k), std::get(v)); + else if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), std::get(v)); + else if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), std::get(v)); + else if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), QString::fromStdString(std::get(v))); + } + QByteArray payload = QJsonDocument(obj).toJson(QJsonDocument::Compact); + QByteArray out = invokePython(QStringLiteral("write_config"), payload, {}); + return !out.isEmpty(); +} + +bool XRDriverIPCBridge::writeControlFlags(const std::map &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; +} diff --git a/kwin/src/xrdriveripc/xrdriveripc.h b/kwin/src/xrdriveripc/xrdriveripc.h new file mode 100644 index 0000000..72ae974 --- /dev/null +++ b/kwin/src/xrdriveripc/xrdriveripc.h @@ -0,0 +1,51 @@ +// C++ bridge now invoking xrdriveripc via external python process +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +// 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; +using XRDict = std::map; + +class XR_DRIVER_IPC_EXPORT XRDriverIPCBridge { +public: + static XRDriverIPCBridge &instance(); + + std::optional retrieveConfig(); + std::optional retrieveDriverState(); + bool writeConfig(const XRDict &configUpdate); + bool writeControlFlags(const std::map &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 +}; diff --git a/kwin/src/xrdriveripc/xrdriveripc_runner.py b/kwin/src/xrdriveripc/xrdriveripc_runner.py new file mode 100644 index 0000000..748bc70 --- /dev/null +++ b/kwin/src/xrdriveripc/xrdriveripc_runner.py @@ -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())