feat: using xkbregistry instead of manually parsing evdev.xml

This commit is contained in:
praharshaAdhikari 2026-07-03 02:41:35 +05:45 committed by Nick Bolton
parent 22a4b135f0
commit 7a90a30ec8
10 changed files with 81 additions and 247 deletions

View file

@ -1,3 +1,4 @@
# SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
# SPDX-FileCopyrightText: (C) 2024 - 2026 Chris Rizzitello <sithlord48@gmail.com>
# 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 \

View file

@ -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

View file

@ -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)
message(STATUS "xkbcommon version: ${LIBXKBCOMMON_VERSION}")
include_directories(${LIBXKBCOMMON_INCLUDE_DIRS})
message(STATUS "xkbcommon version: ${LIBXKBCOMMON_VERSION}")
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()

View file

@ -1,3 +1,4 @@
# SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
# SPDX-FileCopyrightText: (C) 2024 - 2026 Chris Rizzitello <sithlord48@gmail.com>
# 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()

View file

@ -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 <platform/OSXAutoTypes.h>
#endif
#include <filesystem>
AppUtilUnix::AppUtilUnix(const IEventQueue *)
{
// do nothing
@ -45,17 +44,7 @@ std::vector<std::string> AppUtilUnix::getKeyboardLayoutList()
std::vector<std::string> layoutLangCodes;
#if WINAPI_XWINDOWS
// Check /usr/local first used on bsd and some systems
std::vector<std::string> 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);

View file

@ -23,5 +23,4 @@ public:
void startNode() override;
std::vector<std::string> getKeyboardLayoutList() override;
std::string getCurrentLanguageCode() override;
std::string m_evdev;
};

View file

@ -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 <algorithm>
#include <sstream>
#include <QDomDocument>
#include <QFile>
#include "base/Log.h"
#include "DeskflowXkbKeyboard.h"
#include "ISO639Table.h"
#include "X11LayoutsParser.h"
#include "base/Log.h"
#include <xkbcommon/xkbregistry.h>
namespace {
@ -31,63 +32,52 @@ void splitLine(std::vector<std::string> &parts, const std::string &line, char de
} // namespace
bool X11LayoutsParser::readXMLConfigItemElem(const QDomNode &node, std::vector<Lang> &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::Lang> X11LayoutsParser::getAllLanguageData(const std::string &pathToEvdevFile)
std::vector<X11LayoutsParser::Lang> X11LayoutsParser::getAllLanguageData()
{
std::vector<Lang> 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<std::string> 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);
}
}
const auto layouts = layoutListElem.elementsByTagName("layout");
for (int i = 0; i < layouts.count(); i++) {
auto item = layouts.at(i);
if (!readXMLConfigItemElem(item, allCodes))
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;
auto variantListElem = item.namedItem("variantList").childNodes();
for (int j = 0; j < variantListElem.count(); j++)
readXMLConfigItemElem(variantListElem.at(j), allCodes.back().variants);
iterator->variants.emplace_back();
iterator->variants.back().name = variant;
iterator->variants.back().layoutBaseISO639_2 = std::move(isoCodes);
}
}
rxkb_context_unref(ctx);
return allCodes;
}
@ -101,13 +91,13 @@ void X11LayoutsParser::appendVectorUniq(const std::vector<std::string> &source,
}
void X11LayoutsParser::convertLayoutToISO639_2(
const std::string &pathToEvdevFile, bool needToReloadEvdev, const std::vector<std::string> &layoutNames,
const std::vector<std::string> &layoutVariantNames, std::vector<std::string> &iso639_2Codes
const std::vector<std::string> &layoutNames, const std::vector<std::string> &layoutVariantNames,
std::vector<std::string> &iso639_2Codes
)
{
static std::vector<X11LayoutsParser::Lang> 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<std::string> X11LayoutsParser::getX11LanguageList(const std::string &pathToEvdevFile)
std::vector<std::string> X11LayoutsParser::getX11LanguageList()
{
std::vector<std::string> layoutNames;
std::vector<std::string> layoutVariantNames;
@ -165,13 +155,11 @@ std::vector<std::string> X11LayoutsParser::getX11LanguageList(const std::string
std::vector<std::string> 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<std::string> 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 "";

View file

@ -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 <string>
#include <vector>
class QDomNode;
class X11LayoutsParser
{
public:
static std::vector<std::string> getX11LanguageList(const std::string &pathToEvdevFile);
static std::string convertLayoutToISO(
const std::string &pathToEvdevFile, const std::string &layoutLangCode, bool needToReloadFiles = false
);
static std::vector<std::string> getX11LanguageList();
static std::string convertLayoutToISO(const std::string &layoutLangCode);
private:
struct Lang
@ -29,15 +26,13 @@ private:
std::vector<Lang> variants;
};
static bool readXMLConfigItemElem(const QDomNode &node, std::vector<Lang> &langList);
static std::vector<Lang> getAllLanguageData(const std::string &pathToEvdevFile);
static std::vector<Lang> getAllLanguageData();
static void appendVectorUniq(const std::vector<std::string> &source, std::vector<std::string> &dst);
static void convertLayoutToISO639_2(
const std::string &pathToEvdevFile, bool needToReloadEvdev, const std::vector<std::string> &layoutNames,
const std::vector<std::string> &layoutVariantNames, std::vector<std::string> &iso639_2Codes
const std::vector<std::string> &layoutNames, const std::vector<std::string> &layoutVariantNames,
std::vector<std::string> &iso639_2Codes
);
static std::vector<std::string> convertISO639_2ToISO639_1(const std::vector<std::string> &iso639_2Codes);

View file

@ -1,5 +1,6 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
* SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello <sithlord48@gmail.com>
* 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)

View file

@ -1,5 +1,6 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
* SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello <sithlord48@gmail.com>
* 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(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<xkbConfigRegistry version=\"1.1\">\n"
" <layoutList>\n"
" <layout>\n"
" <configItem>\n"
" <name>us</name>\n"
" <!-- Keyboard indicator for English layouts -->\n"
" <shortDescription>en</shortDescription>\n"
" <description>English (US)</description>\n"
" <languageList>\n"
" <iso639Id>eng</iso639Id>\n"
" </languageList>\n"
" </configItem>\n"
" <variantList>\n"
" <variant>\n"
" <configItem>\n"
" <name>eng</name>\n"
" <shortDescription>eng</shortDescription>\n"
" <description>Cherokee</description>\n"
" <languageList>\n"
" <iso639Id>eng</iso639Id>\n"
" </languageList>\n"
" </configItem>\n"
" </variant>\n"
" </variantList>\n"
" </layout>\n"
" <layout>\n"
" <configItem>\n"
" <name>ru</name>\n"
" <!-- Keyboard indicator for Russian layouts -->\n"
" <shortDescription>ru</shortDescription>\n"
" <description>Russian</description>\n"
" <languageList>\n"
" <iso639Id>rus</iso639Id>\n"
" </languageList>\n"
" </configItem>\n"
" </layout>\n"
" </layoutList>\n"
"</xkbConfigRegistry>\n"
);
const QString kFutureEvContents = QStringLiteral(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<xkbConfigRegistry version=\"1.1\">\n"
" <layoutList>\n"
" <layout>\n"
" <configItem>\n"
" <name>futureLangName</name>\n"
" <languageList>\n"
" <iso639Id>fln</iso639Id>\n"
" </languageList>\n"
" </configItem>\n"
" </layout>\n"
" </layoutList>\n"
"</xkbConfigRegistry>\n"
);
const QString kBadEv1Contents = QStringLiteral("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
const QString kBadEv2Contents = QStringLiteral(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<xkbConfigRegistry version=\"1.1\">\n"
"</xkbConfigRegistry>"
);
const QString kBadEv3Contents = QStringLiteral(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<xkbConfigRegistry version=\"1.1\">\n"
" <layoutList>\n"
" <layout>\n"
" </layout>\n"
" </layoutList>\n"
"</xkbConfigRegistry>\n"
);
};