diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index 137b2002d..b69c08b31 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -1,3 +1,4 @@ +# SPDX-FileCopyrightText: (C) 2026 Deskflow Developers # SPDX-FileCopyrightText: (C) 2024 - 2026 Chris Rizzitello # SPDX-License-Identifier: MIT @@ -35,8 +36,8 @@ runs: if [ ${{inputs.like}} == "debian" ]; then apt update -qqq > /dev/null apt install -qqq cmake build-essential ninja-build \ - xorg-dev libx11-dev libxtst-dev libssl-dev \ - libxkbfile-dev qt6-base-dev qt6-tools-dev libxkbcommon-dev \ + xorg-dev libx11-dev libxtst-dev libssl-dev libxkbfile-dev \ + qt6-base-dev qt6-tools-dev libxkbregistry-dev libxkbcommon-dev \ libei-dev libportal-dev help2man -y >/dev/null elif [ ${{inputs.like}} == "fedora" ]; then dnf install -y cmake make ninja-build gcc-c++ rpm-build openssl-devel \ @@ -47,7 +48,7 @@ runs: zypper install -y --force-resolution \ cmake make ninja gcc-c++ rpm-build libopenssl-devel \ libXtst-devel libxkbfile-devel qt6-base-devel qt6-tools-devel \ - qt6-linguist-devel libxkbcommon-devel libxkbcommon-x11-devel \ + qt6-linguist-devel libxkbcommon-devel libxkbcommon-x11-devel libxkbregistry-devel \ libei-devel libportal-devel help2man libXinerama-devel libXrandr-devel elif [ ${{ inputs.like }} == "arch" ]; then pacman -Syu --noconfirm base-devel cmake ninja \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 1eed72198..acbe3207c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -173,7 +173,7 @@ add_definitions(-DQT_NO_KEYWORDS) #Find and setup Qt find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS Core Widgets Network) if(UNIX AND NOT APPLE) - find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS DBus Xml) + find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS DBus) endif() # Define the location of Qt deployment tool diff --git a/cmake/Libraries.cmake b/cmake/Libraries.cmake index a8edc0643..04d98d44d 100644 --- a/cmake/Libraries.cmake +++ b/cmake/Libraries.cmake @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: (C) 2024 - 2025 Deskflow Developers +# SPDX-FileCopyrightText: (C) 2024 - 2026 Deskflow Developers # SPDX-FileCopyrightText: (C) 2024 Symless Ltd # SPDX-License-Identifier: MIT @@ -97,9 +97,12 @@ macro(configure_unix_libs) find_package(PkgConfig) if(PKG_CONFIG_FOUND) pkg_check_modules(LIBXKBCOMMON REQUIRED xkbcommon) - include_directories(${LIBXKBCOMMON_INCLUDE_DIRS}) - message(STATUS "xkbcommon version: ${LIBXKBCOMMON_VERSION}") + include_directories(${LIBXKBCOMMON_INCLUDE_DIRS}) + + pkg_check_modules(LIBXKBREGISTRY REQUIRED xkbregistry) + message(STATUS "xkbregistry version: ${LIBXKBREGISTRY_VERSION}") + include_directories(${LIBXKBREGISTRY_INCLUDE_DIRS}) else() message(WARNING "pkg-config not found, skipping wayland libraries") endif() diff --git a/src/lib/deskflow/CMakeLists.txt b/src/lib/deskflow/CMakeLists.txt index 676957903..63f7101e8 100644 --- a/src/lib/deskflow/CMakeLists.txt +++ b/src/lib/deskflow/CMakeLists.txt @@ -1,3 +1,4 @@ +# SPDX-FileCopyrightText: (C) 2026 Deskflow Developers # SPDX-FileCopyrightText: (C) 2024 - 2026 Chris Rizzitello # SPDX-FileCopyrightText: (C) 2012 - 2025 Symless Ltd # SPDX-FileCopyrightText: (C) 2009 - 2012 Nick Bolton @@ -95,8 +96,8 @@ target_link_libraries(${lib_name} Qt6::Network PRIVATE platform - ) +) if(UNIX AND NOT APPLE) - target_link_libraries(${lib_name} PRIVATE server Qt6::Xml) + target_link_libraries(${lib_name} PRIVATE server ${LIBXKBREGISTRY_LINK_LIBRARIES}) endif() diff --git a/src/lib/deskflow/unix/AppUtilUnix.cpp b/src/lib/deskflow/unix/AppUtilUnix.cpp index 3f3990a80..80bef0d67 100644 --- a/src/lib/deskflow/unix/AppUtilUnix.cpp +++ b/src/lib/deskflow/unix/AppUtilUnix.cpp @@ -1,5 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2026 Deskflow Developers * SPDX-FileCopyrightText: (C) 2012 - 2016 Synergy App Ltd * SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception @@ -18,8 +19,6 @@ #include #endif -#include - AppUtilUnix::AppUtilUnix(const IEventQueue *) { // do nothing @@ -45,17 +44,7 @@ std::vector AppUtilUnix::getKeyboardLayoutList() std::vector layoutLangCodes; #if WINAPI_XWINDOWS - // Check /usr/local first used on bsd and some systems - std::vector evdev_candidate = { - "/usr/share/X11/xkb/rules/evdev.xml", // Linux - "/usr/local/share/X11/xkb/rules/evdev.xml", // FreeBSD, DragonFlyBSD - "/usr/X11R7/lib/X11/xkb/rules/evdev.xml", // NetBSD - "/usr/X11R6/share/X11/xkb/rules/evdev.xml", // OpenBSD - }; - - for (auto it = evdev_candidate.begin(); it != evdev_candidate.end() && !std::filesystem::exists(m_evdev = *it); it++) - ; - layoutLangCodes = X11LayoutsParser::getX11LanguageList(m_evdev); + layoutLangCodes = X11LayoutsParser::getX11LanguageList(); #elif defined(Q_OS_MAC) CFStringRef keys[] = {kTISPropertyInputSourceCategory}; @@ -151,7 +140,7 @@ std::string AppUtilUnix::getCurrentLanguageCode() XFree(kbdDescr); XCloseDisplay(display); - result = X11LayoutsParser::convertLayoutToISO(m_evdev, result); + result = X11LayoutsParser::convertLayoutToISO(result); #elif defined(Q_OS_MAC) AutoTISInputSourceRef source(nullptr, CFRelease); diff --git a/src/lib/deskflow/unix/AppUtilUnix.h b/src/lib/deskflow/unix/AppUtilUnix.h index 5521a6d17..b1d9674a7 100644 --- a/src/lib/deskflow/unix/AppUtilUnix.h +++ b/src/lib/deskflow/unix/AppUtilUnix.h @@ -23,5 +23,4 @@ public: void startNode() override; std::vector getKeyboardLayoutList() override; std::string getCurrentLanguageCode() override; - std::string m_evdev; }; diff --git a/src/lib/deskflow/unix/X11LayoutsParser.cpp b/src/lib/deskflow/unix/X11LayoutsParser.cpp index 482b8a708..237fd78a0 100644 --- a/src/lib/deskflow/unix/X11LayoutsParser.cpp +++ b/src/lib/deskflow/unix/X11LayoutsParser.cpp @@ -1,5 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2026 Deskflow Developers * SPDX-FileCopyrightText: (C) 2012 - 2016 Synergy App Ltd * SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception @@ -9,13 +10,13 @@ #include #include -#include -#include +#include "base/Log.h" #include "DeskflowXkbKeyboard.h" #include "ISO639Table.h" #include "X11LayoutsParser.h" -#include "base/Log.h" + +#include namespace { @@ -31,63 +32,52 @@ void splitLine(std::vector &parts, const std::string &line, char de } // namespace -bool X11LayoutsParser::readXMLConfigItemElem(const QDomNode &node, std::vector &langList) -{ - auto configItemElem = node.firstChildElement("configItem"); - if (configItemElem.isNull()) { - LOG_WARN("failed to read \"configItem\" in xml file"); - return false; - } - - langList.emplace_back(); - if (auto nameElem = configItemElem.firstChildElement("name"); !nameElem.isNull()) - langList.back().name = nameElem.toElement().text().toStdString(); - - if (auto languageListElem = configItemElem.elementsByTagName("languageList"); !languageListElem.isEmpty()) { - for (int i = 0; i < languageListElem.count(); i++) { - const auto isoElem = languageListElem.at(i).namedItem("iso639Id").toElement(); - langList.back().layoutBaseISO639_2.emplace_back(isoElem.text().toStdString()); - } - } - - return true; -} - -std::vector X11LayoutsParser::getAllLanguageData(const std::string &pathToEvdevFile) +std::vector X11LayoutsParser::getAllLanguageData() { std::vector allCodes; - QFile inFile(QString::fromStdString(pathToEvdevFile)); - if (!inFile.open(QIODevice::ReadOnly)) { - LOG_WARN("unable to open %s", pathToEvdevFile.c_str()); + rxkb_context *ctx = rxkb_context_new(RXKB_CONTEXT_NO_FLAGS); + + if (!ctx) { + LOG_WARN("failed to create xkb registry context"); return allCodes; } - QDomDocument xmlDoc; - xmlDoc.setContent(inFile.readAll()); - - const auto xkbConfigElem = xmlDoc.firstChildElement("xkbConfigRegistry"); - if (xkbConfigElem.isNull()) { - LOG_WARN("failed to read xkbConfigRegistry in %s", pathToEvdevFile.c_str()); + if (!rxkb_context_parse_default_ruleset(ctx)) { + LOG_WARN("failed to parse xkb registry ruleset"); + rxkb_context_unref(ctx); return allCodes; } - auto layoutListElem = xkbConfigElem.firstChildElement("layoutList"); - if (layoutListElem.isNull()) { - LOG_WARN("failed to read layoutList in %s", pathToEvdevFile.c_str()); - return allCodes; + for (rxkb_layout *layout = rxkb_layout_first((ctx)); layout; layout = rxkb_layout_next(layout)) { + const char *name = rxkb_layout_get_name(layout); + const char *variant = rxkb_layout_get_variant(layout); + + std::vector isoCodes; + for (rxkb_iso639_code *isoCode = rxkb_layout_get_iso639_first(layout); isoCode; + isoCode = rxkb_iso639_code_next(isoCode)) { + if (const char *code = rxkb_iso639_code_get_code(isoCode)) { + isoCodes.emplace_back(code); + } + } + + if (!variant) { + allCodes.emplace_back(); + allCodes.back().name = name ? name : ""; + allCodes.back().layoutBaseISO639_2 = std::move(isoCodes); + } else { + auto requiredName = name ? name : std::string{}; + auto iterator = std::ranges::find_if(allCodes, [&](const Lang &l) { return l.name == requiredName; }); + if (iterator == allCodes.end()) + continue; + + iterator->variants.emplace_back(); + iterator->variants.back().name = variant; + iterator->variants.back().layoutBaseISO639_2 = std::move(isoCodes); + } } - const auto layouts = layoutListElem.elementsByTagName("layout"); - for (int i = 0; i < layouts.count(); i++) { - auto item = layouts.at(i); - if (!readXMLConfigItemElem(item, allCodes)) - continue; - - auto variantListElem = item.namedItem("variantList").childNodes(); - for (int j = 0; j < variantListElem.count(); j++) - readXMLConfigItemElem(variantListElem.at(j), allCodes.back().variants); - } + rxkb_context_unref(ctx); return allCodes; } @@ -101,13 +91,13 @@ void X11LayoutsParser::appendVectorUniq(const std::vector &source, } void X11LayoutsParser::convertLayoutToISO639_2( - const std::string &pathToEvdevFile, bool needToReloadEvdev, const std::vector &layoutNames, - const std::vector &layoutVariantNames, std::vector &iso639_2Codes + const std::vector &layoutNames, const std::vector &layoutVariantNames, + std::vector &iso639_2Codes ) { static std::vector allLang; - if (allLang.empty() || needToReloadEvdev) { - allLang = getAllLanguageData(pathToEvdevFile); + if (allLang.empty()) { + allLang = getAllLanguageData(); } for (size_t i = 0; i < layoutNames.size(); i++) { const auto &layoutName = layoutNames[i]; @@ -154,7 +144,7 @@ void X11LayoutsParser::convertLayoutToISO639_2( } } -std::vector X11LayoutsParser::getX11LanguageList(const std::string &pathToEvdevFile) +std::vector X11LayoutsParser::getX11LanguageList() { std::vector layoutNames; std::vector layoutVariantNames; @@ -165,13 +155,11 @@ std::vector X11LayoutsParser::getX11LanguageList(const std::string std::vector iso639_2Codes; iso639_2Codes.reserve(layoutNames.size()); - convertLayoutToISO639_2(pathToEvdevFile, true, layoutNames, layoutVariantNames, iso639_2Codes); + convertLayoutToISO639_2(layoutNames, layoutVariantNames, iso639_2Codes); return convertISO639_2ToISO639_1(iso639_2Codes); } -std::string X11LayoutsParser::convertLayoutToISO( - const std::string &pathToEvdevFile, const std::string &layoutLangCode, bool needToReloadFiles -) +std::string X11LayoutsParser::convertLayoutToISO(const std::string &layoutLangCode) { if (layoutLangCode.empty()) { LOG_VERBOSE("skip converting empty layout lang code"); @@ -179,7 +167,7 @@ std::string X11LayoutsParser::convertLayoutToISO( } std::vector iso639_2Codes; - convertLayoutToISO639_2(pathToEvdevFile, needToReloadFiles, {layoutLangCode}, {""}, iso639_2Codes); + convertLayoutToISO639_2({layoutLangCode}, {""}, iso639_2Codes); if (iso639_2Codes.empty()) { LOG_WARN("failed to convert layout lang code: \"%s\"", layoutLangCode.c_str()); return ""; diff --git a/src/lib/deskflow/unix/X11LayoutsParser.h b/src/lib/deskflow/unix/X11LayoutsParser.h index 373e9218d..9d2830fbf 100644 --- a/src/lib/deskflow/unix/X11LayoutsParser.h +++ b/src/lib/deskflow/unix/X11LayoutsParser.h @@ -1,5 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2026 Deskflow Developers * SPDX-FileCopyrightText: (C) 2012 - 2016 Synergy App Ltd * SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception @@ -11,15 +12,11 @@ #include #include -class QDomNode; - class X11LayoutsParser { public: - static std::vector getX11LanguageList(const std::string &pathToEvdevFile); - static std::string convertLayoutToISO( - const std::string &pathToEvdevFile, const std::string &layoutLangCode, bool needToReloadFiles = false - ); + static std::vector getX11LanguageList(); + static std::string convertLayoutToISO(const std::string &layoutLangCode); private: struct Lang @@ -29,15 +26,13 @@ private: std::vector variants; }; - static bool readXMLConfigItemElem(const QDomNode &node, std::vector &langList); - - static std::vector getAllLanguageData(const std::string &pathToEvdevFile); + static std::vector getAllLanguageData(); static void appendVectorUniq(const std::vector &source, std::vector &dst); static void convertLayoutToISO639_2( - const std::string &pathToEvdevFile, bool needToReloadEvdev, const std::vector &layoutNames, - const std::vector &layoutVariantNames, std::vector &iso639_2Codes + const std::vector &layoutNames, const std::vector &layoutVariantNames, + std::vector &iso639_2Codes ); static std::vector convertISO639_2ToISO639_1(const std::vector &iso639_2Codes); diff --git a/src/unittests/deskflow/X11LayoutParserTests.cpp b/src/unittests/deskflow/X11LayoutParserTests.cpp index 88008c612..5b1a8d25e 100644 --- a/src/unittests/deskflow/X11LayoutParserTests.cpp +++ b/src/unittests/deskflow/X11LayoutParserTests.cpp @@ -1,5 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2026 Deskflow Developers * SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello * SPDX-FileCopyrightText: (C) 2014 - 2016 Synergy App Ltd * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception @@ -9,77 +10,17 @@ #include "deskflow/unix/X11LayoutsParser.h" -void X11LayoutParserTests::initTestCase() -{ - QDir dir; - QVERIFY(dir.mkpath(kTestDir)); - - QFile correctEvdevFile(kTestCorrectFile); - QVERIFY(correctEvdevFile.open(QIODevice::WriteOnly)); - correctEvdevFile.write(kCorrectEvContents.toUtf8()); - correctEvdevFile.close(); - - QVERIFY(correctEvdevFile.open(QIODevice::ReadOnly)); - QCOMPARE(correctEvdevFile.readAll(), kCorrectEvContents); - correctEvdevFile.close(); - - QFile futureEvdevFile(kTestFutureFile); - QVERIFY(futureEvdevFile.open(QIODevice::WriteOnly)); - futureEvdevFile.write(kFutureEvContents.toUtf8()); - futureEvdevFile.close(); - - QVERIFY(futureEvdevFile.open(QIODevice::ReadOnly)); - QCOMPARE(futureEvdevFile.readAll(), kFutureEvContents); - futureEvdevFile.close(); - - QFile badEvdevFile1(kTestBadFile1); - QVERIFY(badEvdevFile1.open(QIODevice::WriteOnly)); - badEvdevFile1.write(kBadEv1Contents.toUtf8()); - badEvdevFile1.close(); - - QVERIFY(badEvdevFile1.open(QIODevice::ReadOnly)); - QCOMPARE(badEvdevFile1.readAll(), kBadEv1Contents); - badEvdevFile1.close(); - - QFile badEvdevFile2(kTestBadFile2); - QVERIFY(badEvdevFile2.open(QIODevice::WriteOnly)); - badEvdevFile2.write(kBadEv2Contents.toUtf8()); - badEvdevFile2.close(); - - QVERIFY(badEvdevFile2.open(QIODevice::ReadOnly)); - QCOMPARE(badEvdevFile2.readAll(), kBadEv2Contents); - badEvdevFile2.close(); - - QFile badEvdevFile3(kTestBadFile3); - QVERIFY(badEvdevFile3.open(QIODevice::WriteOnly)); - badEvdevFile3.write(kBadEv3Contents.toUtf8()); - badEvdevFile3.close(); - - QVERIFY(badEvdevFile3.open(QIODevice::ReadOnly)); - QCOMPARE(badEvdevFile3.readAll(), kBadEv3Contents); - badEvdevFile3.close(); -} - -void X11LayoutParserTests::xmlParse() -{ - const QString badPath = QStringLiteral("%1/%2").arg(kTestDir, "notafile"); - QVERIFY(X11LayoutsParser::getX11LanguageList(badPath.toStdString()).empty()); - - // we would like to check the method - // X11LayoutsParser::getAllLanguageData(kTestCorrectFile.toStdString()); - // however it's logic is flawed making as the return list size is based on number of languges host has installed - // this is correct on a real system but makes it hard to test the method - - QVERIFY(X11LayoutsParser::getX11LanguageList(kTestBadFile1.toStdString()).empty()); - QVERIFY(X11LayoutsParser::getX11LanguageList(kTestBadFile2.toStdString()).empty()); - QVERIFY(X11LayoutsParser::getX11LanguageList(kTestBadFile3.toStdString()).empty()); -} - void X11LayoutParserTests::convertLayouts() { - QCOMPARE(X11LayoutsParser::convertLayoutToISO(kTestCorrectFile.toStdString(), "us", true), "en"); - QCOMPARE(X11LayoutsParser::convertLayoutToISO(kTestBadFile1.toStdString(), "us", true), ""); - QCOMPARE(X11LayoutsParser::convertLayoutToISO(kTestFutureFile.toStdString(), "us", true), ""); + // An empty layout name yields an empty result. + QCOMPARE(X11LayoutsParser::convertLayoutToISO(""), ""); + + // An unknown layout name is not present in the registry. + QCOMPARE(X11LayoutsParser::convertLayoutToISO("notARealLayout"), ""); + + // A well-known layout resolves to its ISO 639-1 code. "us" is always + // present in the xkeyboard-config data and maps to "eng" -> "en". + QCOMPARE(X11LayoutsParser::convertLayoutToISO("us"), "en"); } QTEST_MAIN(X11LayoutParserTests) diff --git a/src/unittests/deskflow/X11LayoutParserTests.h b/src/unittests/deskflow/X11LayoutParserTests.h index 0211d38ba..04e08c6b7 100644 --- a/src/unittests/deskflow/X11LayoutParserTests.h +++ b/src/unittests/deskflow/X11LayoutParserTests.h @@ -1,5 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2026 Deskflow Developers * SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception */ @@ -13,92 +14,8 @@ class X11LayoutParserTests : public QObject Q_OBJECT private Q_SLOTS: // Test are run in order top to bottom - void initTestCase(); - void xmlParse(); void convertLayouts(); private: Log m_log; - const QString kTestDir = "tmp/test"; - const QString kTestCorrectFile = "tmp/test/correctEvdev.xml"; - const QString kTestFutureFile = "tmp/test/evdevFromFuture.xml"; - const QString kTestBadFile1 = "tmp/test/evdevBad1.xml"; - const QString kTestBadFile2 = "tmp/test/evdevBad2.xml"; - const QString kTestBadFile3 = "tmp/test/evdevBad3.xml"; - - const QString kCorrectEvContents = QStringLiteral( - "\n" - "\n" - " \n" - " \n" - " \n" - " us\n" - " \n" - " en\n" - " English (US)\n" - " \n" - " eng\n" - " \n" - " \n" - " \n" - " \n" - " \n" - " eng\n" - " eng\n" - " Cherokee\n" - " \n" - " eng\n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " ru\n" - " \n" - " ru\n" - " Russian\n" - " \n" - " rus\n" - " \n" - " \n" - " \n" - " \n" - "\n" - ); - - const QString kFutureEvContents = QStringLiteral( - "\n" - "\n" - " \n" - " \n" - " \n" - " futureLangName\n" - " \n" - " fln\n" - " \n" - " \n" - " \n" - " \n" - "\n" - ); - - const QString kBadEv1Contents = QStringLiteral("\n"); - - const QString kBadEv2Contents = QStringLiteral( - "\n" - "\n" - "" - ); - - const QString kBadEv3Contents = QStringLiteral( - "\n" - "\n" - " \n" - " \n" - " \n" - " \n" - "\n" - ); };