Compare commits

..

36 Commits

Author SHA1 Message Date
nirenjan 6ec133488b doc: Add new headers 2026-03-31 21:53:57 -07:00
nirenjan 667e8e2a7b feat: Update symbol visibility 2026-03-30 22:35:52 -07:00
nirenjan 357ea96676 fix: Fix syntax issue with logical operators 2026-03-30 11:52:41 -07:00
nirenjan 6a36fc7764 fix: Use logical operators with boolean arguments
SonarQube flagged the use of bitwise operators with boolean variables.
This is changed to use the correct logical operators instead of their
bitwise equivalents.
2026-03-30 11:47:12 -07:00
nirenjan 283b476c5e fix: Handle issues found by SonarQube 2026-03-30 10:52:47 -07:00
nirenjan 75e6f253c9 feat: Add virtual keyboard/mouse library support
Previously, x52d had conditional compilation flags linking to libevdev
on Linux. However, I wanted to change this so that we use an abstraction
layer that will link to the appropriate backend (evdev on Linux only for
now). The idea is that we get rid of all conditional compilation blocks
and deal with the backend through the vkm library.

This new library handles the mouse scrolling, clicking, as well as
adding the ability to include keyboard events so that we can support the
profiles feature in a future commit.
2026-03-30 10:01:50 -07:00
nirenjan 230951a232
Merge pull request #65 from nirenjan/dependabot/github_actions/codecov/codecov-action-6
build(deps): bump codecov/codecov-action from 5 to 6
2026-03-27 01:59:47 -07:00
dependabot[bot] c99f775b70
build(deps): bump codecov/codecov-action from 5 to 6
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 08:17:04 +00:00
nirenjan 8914184613 build: Migrate CI to use distro containers
Previously, the build workflow was restricted to running only on Ubuntu
22.04, Ubuntu 24.04 and macOS, which are the only available native
runners on Github Actions. However, the Ubuntu runner does allow us to
run the build inside a container. Therefore, this commit adds the
ability to pull a prebuilt container with all the necessary dependencies
and build libx52 inside of that container. This commit also adds support
scripts to build the containers and run the CI build against those
prebuilt containers locally for testing, without having to rely
exclusively on Github Actions.

This change also adds support for testing libx52 against Alpine Linux,
in order to verify the portability, given that Alpine uses musl instead
of glibc. The limitation is that we need to mount the `/dev/bus/usb`
device tree inside the container, otherwise libusb inside the Alpine
image fails with LIBUSB_ERROR_OTHER. This is not a concern on the other
distributions, but due to limitations in the Github actions environment,
there is no `/dev/bus/usb` tree to export. For this reason, Alpine is
not a part of the CI build, but is available for testing locally.

Also, because a default bare container would need several minutes of
package installation just to get to a point where we could run
build-and-test.sh, this includes a prebuild workflow which generates the
container images and pushes them to ghcr.io, and the build workflow
pulls from there. There is also logic to ensure that we only keep the
latest image, since there is no value in retaining older images.
2026-03-26 12:23:38 -07:00
nirenjan fdafda1d34
Merge pull request #64 from nirenjan/dependabot/github_actions/actions/deploy-pages-5
build(deps): bump actions/deploy-pages from 4 to 5
2026-03-26 05:56:34 -07:00
dependabot[bot] 9501813c36
build(deps): bump actions/deploy-pages from 4 to 5
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4 to 5.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/deploy-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-26 08:18:04 +00:00
nirenjan aa555f5e66 doc: Update INSTALL.md with Meson instructions
This change removes the old autotools based documentation and switches
to using Meson exclusively. In addition, the build action will build
using meson compile and meson test instead of ninja/ninja test.
2026-03-22 19:13:00 -07:00
nirenjan 842e7e53ed fix: Scope the list of man pages installed
The previous commit to address #63 added a number of extra unwanted man
pages. This commit addresses that and scopes down the actual installed
man pages to only x52cli and x52bugreport.

GH-Issue: #63
GH-Issue-URL: https://github.com/nirenjan/libx52/issues/63
2026-03-22 15:59:35 -07:00
nirenjan dfbe3e6d21 fix: Enable install for libx52dcomm
The change to build using Meson broke the install target, causing x52ctl
and x52d to fail with a missing libx52dcomm library. This was fixed by
setting `install: true` in the library call.

In addition, several features were used that were leftover from my
earlier attempts to migrate to Meson, but targeted older Meson versions.
Some of these features were deprecated in newer Meson versions, and
therefore, cause warnings to show up during meson setup.

GH-Issue: #63
GH-Issue-URL: https://github.com/nirenjan/libx52/issues/63
2026-03-22 15:47:13 -07:00
nirenjan 732bc21b65 fix: Address compiler warnings
When enabling --warnlevel=3 during Meson setup, the build threw up
several warnings, mostly related to either unused parameters, or
sometimes an integer type mismatch. This commit addresses all of those
changes and ensures that the build does not contain any unnecessary
warnings.
2026-03-19 00:10:18 -07:00
nirenjan 3c1abd57d5 fix(pinelog): Cleanup unused parameter warnings
With a recent enough version, the compiler reports a number of unused
parameter warnings when Meson is configured with `--warnlevel=3`. This
commit addresses those warnings.
2026-03-18 22:25:26 -07:00
nirenjan b0b457d14e doc: Update Doxyfile.in to 1.9.8 syntax
Doxygen in Ubuntu 24.04 has removed several features, and introduced a
few new ones. This commit updates the Doxyfile.in template to use the
current syntax.

CI: [skip ci]
2026-03-18 00:54:55 -07:00
nirenjan ae077dbed8 fix: Add library version for libx52dcomm
This change adds the library version for libx52dcomm, so that it can be
versioned if necessary.
2026-03-17 16:43:54 -07:00
nirenjan d29be6213f doc: Add badges for SonarQube and CodeCov
CI: [skip ci] [skip doxy]
2026-03-16 23:22:23 -07:00
nirenjan 273ed22f8e refactor(x52ctl): Break out interactive mode
SonarQube keeps complaining about issues with cognitive complexity and
bad practices, this commit addresses those commits.
2026-03-16 23:07:39 -07:00
nirenjan cdb00739ca fix(x52ctl): Remove unnecessary goto statements
The `goto cleanup` statements are not really needed, and can be safely
replaced with `break` statements in x52ctl.
2026-03-16 22:50:11 -07:00
nirenjan 08a6b0a736 build: Add coverage support with codecov.io
This change adds code coverage to the Meson builds, and reports them to
codecov.io, to help keep the code coverage high.
2026-03-16 22:39:12 -07:00
nirenjan 45d561e0d8 test: Fix SMP test in libx52util
The SMP test was incorrectly calculating the codepoints, resulting in a
test of the codepoints inside the BMP, instead of the individual SMPs.
2026-03-16 22:23:00 -07:00
nirenjan b626a9367f fix(pinelog): Mark benchmark tests as such in Meson
Prior to this change, the pinelog benchmark suite was running as a
regular test, however, this is not ideal since it can result in timing
issues and giving false data to the runners.

This change explicitly marks them as benchmarks, so they can run using
`meson test --benchmark`
2026-03-16 15:56:38 -07:00
nirenjan a1098bc134 feat: Add benchmarking for libx52util-bmp-test 2026-03-16 11:16:41 -07:00
nirenjan 569902be76 fix: Add support for Automake tests
In order to continue to support Automake builds, we need to update the
source to comply with C90 standards, as well as ignoring the warnings in
the test binary.
2026-03-16 10:45:02 -07:00
nirenjan 0cb137bbe0 feat: Handle the entire BMP in libx52util
Prior to this change, the libx52util_convert_utf8_string function had a
limited set of characters that it would convert to the MFD character
map, these characters were derived from the x52_char_map.cfg file.
However, this is a tiny subset of the actual supported characters in the
Basic Multilingual Plane (BMP), since many characters in the BMP can be
normalized to a different character (or character sequence) that has a
corresponding glyph on the X52 MFD.

One example of this is the half-width Katakana characters which are
mapped in the display, however the corresponding full-width characters
were not explicitly mapped. With this commit, the generator script now
automatically detects that the half-width characters can be normalized
to the corresponding full width forms, and maps the full width forms
back to the correct characters on the MFD.

A second benefit of this change is that the MFD can now show characters
that would otherwise never be seen, for example, the 3/4 symbol or 5/8
symbol have no corresponding glyph in the MFD, but they can be
translated to the sequence `3` `/` `4`, giving us much more flexibility
on the characters that can actually be displayed.

Finally, with this change, the function also maps missing or unsupported
characters to the box character (0xDB in the display), making it clearer
that there was something there that could not be displayed. Earlier, it
would have simply skipped that character.
2026-03-16 10:18:16 -07:00
nirenjan 899ea57bf7 doc: Update ChangeLog.md to list Meson bugs
Because several bugs were found in the Meson build infrastructure since
the release of v0.3.3, I've had to keep the existing Autotools
infrastructure running for a bit longer while I address the bugs. As of
this commit, Autotools is still the preferred way to build libx52,
though it is technically deprecated and will be replaced with Meson as
the source of truth.

CI: [skip ci] [skip doxy]
2026-03-13 09:54:23 -07:00
nirenjan e1e020a4f5 fix: Handle test dependency on x52ctl
Prior to this change, running meson test without running meson compile
first would cause the daemon communication tests to fail since it
wouldn't find the x52ctl binary. While I could rewrite the test runner
to directly talk to the daemon, it's faster to just ensure the
dependencies are setup correctly.
2026-03-13 09:27:51 -07:00
nirenjan 7cbf091dc7 fix: Ensure man pages and docs are installed via Meson 2026-03-12 16:19:49 -07:00
nirenjan 9d180531b9 fix: Treat NLS=auto as enabled
Prior to this change, the check was for an explicit -Dnls=enabled,
however, if the option was never set, it defaulted to disabled. With
this change, unless explicitly disabled, the Meson build system will
automatically build with NLS support.
2026-03-12 15:53:30 -07:00
nirenjan 74229b391d fix: Update pkgconfig to use correct details 2026-03-12 15:35:11 -07:00
nirenjan 5f8177f16b fix: Use correct paths for local,sysconf,localstate dirs 2026-03-12 15:17:00 -07:00
nirenjan c5ec15231f feat: Add meson support for configuring udev rules dir 2026-03-12 14:45:08 -07:00
nirenjan 33bbafe970 fix: Fix systemd service installation in Meson
The Meson build change broke the systemd service file installation. This
commit fixes that, while retaining Autotools support.
2026-03-12 13:21:59 -07:00
nirenjan e9a806a6a2 fix: Update meson.build to handle localization
The previous version of the Meson build files did not handle the po
directory correctly, and lost a lot of information. As part of the
migration away from Autotools, this is one more item that needs to be
checked off.

CI: [ci skip] [doxy skip]
2026-03-12 12:48:23 -07:00
90 changed files with 5438 additions and 1124 deletions

View File

@ -2,10 +2,20 @@
# Run the build and tests
set -e
meson setup -Dprefix=/usr -Dsysconfdir=/etc -Dlocalstatedir=/var -Dnls=enabled build
cd build
ninja
ninja test
BUILDDIR="${1:-build}"
rm -rf "$BUILDDIR"
# Handle the meson dist failure in CI
if [[ "$GITHUB_ACTIONS" == "true" ]]
then
git config --global --add safe.directory '*'
fi
meson setup -Dprefix=/usr -Dsysconfdir=/etc -Dlocalstatedir=/var -Dnls=enabled "$BUILDDIR"
cd "$BUILDDIR"
meson compile
meson test
meson dist
# Print bugreport output

View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Generate the list of distros as a build matrix"""
import pathlib
import json
import sys
class BuildMatrix:
"""Generate a build matrix for Github actions"""
COMPILER = 'gcc'
OS = 'ubuntu-latest'
EXPERIMENTAL = False
def __init__(self, image_prefix):
self.matrix = []
self.image_prefix = image_prefix
self.get_distros()
self.add_extra_builds()
self.generate_output()
def build_matrix_obj(self, distro, experimental, os=None, compiler=None, image=None):
"""Build the matrix object for the given distro"""
matrix_obj = {
'distro': distro,
'experimental': experimental,
'os': os or self.OS,
'compiler': compiler or self.COMPILER,
}
if image is None:
image = f"{self.image_prefix}/ci-build-{distro}:latest"
matrix_obj['image'] = image
return matrix_obj
def get_distros(self):
"""Get the list of distros from the Dockerfiles"""
for dockerfile in pathlib.Path('.').glob('docker/Dockerfile.*'):
distro = dockerfile.suffix[1:]
with open(dockerfile, encoding='utf-8') as dfd:
experimental = 'experimental="true"' in dfd.read()
self.matrix.append(self.build_matrix_obj(distro, experimental))
def add_extra_builds(self):
"""Add manual canary builds that don't have a corresponding dockerfile"""
canary_build = self.build_matrix_obj('ubuntu22', False, compiler='clang')
self.matrix.append(canary_build)
macos_build = self.build_matrix_obj('macos', True, os='macos-latest',
compiler='clang', image='')
self.matrix.append(macos_build)
def generate_output(self):
"""Generate the output for github actions"""
matrix_data = json.dumps(self.matrix)
print(f"matrix={matrix_data}")
if __name__ == '__main__':
BuildMatrix(sys.argv[1])

View File

@ -0,0 +1,46 @@
#!/bin/bash
# Get the list of changed Dockerfiles
# Usage: get-changed-dockerfiles.sh <before-SHA> <head-SHA>
set -euxo pipefail
mapfile -t ALL_DOCKERFILES < <(git ls-files 'docker/Dockerfile.*')
if [[ -n "${BUILD_DOCKER_MANUAL_ENV:-}" ]]
then
if [[ "${BUILD_DOCKER_MANUAL_ENV}" == "ALL" ]]
then
DOCKERFILES=("${ALL_DOCKERFILES[@]}")
else
BUILD_LIST=($BUILD_DOCKER_MANUAL_ENV)
DOCKERFILES=()
for item in "${BUILD_LIST[@]}"
do
if [[ -f "docker/Dockerfile.${item}" ]]
then
DOCKERFILES+=("$item")
fi
done
fi
else
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR HEAD^..HEAD)
mapfile -t DOCKERFILES < <(echo "$CHANGED_FILES" | grep 'docker/Dockerfile')
mapfile -t SCRIPT_CHANGES < <(echo "$CHANGED_FILES" | grep 'docker/' | grep '\.sh$')
for file in "${SCRIPT_CHANGES[@]}"
do
for dockerfile in "${ALL_DOCKERFILES[@]}"
do
if grep -q "$(basename "$file")" "$dockerfile"
then
DOCKERFILES+=("$dockerfile")
fi
done
done
fi
echo -n "matrix="
echo "${DOCKERFILES[@]}" | \
tr ' ' '\n' | sort -u | sed 's,docker/Dockerfile\.,,' | \
jq -Rsc 'split("\n") | map(select(length > 0)) | unique'

View File

@ -10,34 +10,46 @@ on:
- '!gh-pages'
paths-ignore:
- 'kernel_module/**'
- 'docker/**'
pull_request:
branches: [ master ]
jobs:
build:
if: "!(contains(github.event.head_commit.message, '[ci skip]') || contains(github.event.head_commit.message, '[skip ci]'))"
name: ${{ join(matrix.*, '/') }}
runs-on: ${{ matrix.os }}
continue-on-error: ${{ startsWith(matrix.os, 'macos-') }}
env:
CC: ${{ matrix.cc }}
list-distros:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v6
- id: set-matrix
run: .github/scripts/generate_build_matrix.py ghcr.io/${{ github.repository }} >> $GITHUB_OUTPUT
build:
needs: list-distros
if: "!(contains(github.event.head_commit.message, '[ci skip]') || contains(github.event.head_commit.message, '[skip ci]'))"
strategy:
fail-fast: false
matrix:
os: ['ubuntu-22.04', 'ubuntu-24.04', 'macos-latest']
cc: ['gcc', 'clang']
include: ${{ fromJson(needs.list-distros.outputs.matrix) }}
name: ${{ format('{0}/{1}{2}', matrix.distro, matrix.compiler, matrix.experimental == true && ' (experimental)' || '') }}
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
container:
image: ${{ matrix.image }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install dependencies (Ubuntu)
run: ./.github/scripts/install-dependencies-ubuntu.sh
if: ${{ startsWith(matrix.os, 'ubuntu-') }}
- name: Install dependencies (MacOS)
run: ./.github/scripts/install-dependencies-macos.sh
if: ${{ startsWith(matrix.os, 'macos-') }}
- name: Build and Test
env:
CC: ${{ matrix.compiler }}
run: ./.github/scripts/build-and-test.sh

44
.github/workflows/coverage.yml vendored 100644
View File

@ -0,0 +1,44 @@
name: Code Coverage
permissions:
contents: read
on:
push:
branches:
- '*'
- '!gh-pages'
pull_request:
branches:
- 'master'
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install dependencies
run: |
./.github/scripts/install-dependencies-ubuntu.sh
sudo apt-get install -y gcovr
- name: Configure and Build
run: |
meson setup build -Db_coverage=true --buildtype=debug
meson compile -C build
- name: Run Tests
run: meson test -C build
- name: Generate Coverage Report
run: |
# This generates the XML report for the upload step
ninja -C build coverage-xml
- name: Upload Report to Codecov
uses: codecov/codecov-action@v6
with:
files: buildd/meson-logs/coverage.xml
fail_ci_if_error: true

66
.github/workflows/docker.yml vendored 100644
View File

@ -0,0 +1,66 @@
name: Build Docker CI Images
on:
workflow_dispatch:
inputs:
distros:
description: "List distros to build (space separated)"
type: string
default: "ALL"
push:
paths:
- "docker/Dockerfile.*"
- "docker/*.sh"
jobs:
detect-changes:
runs-on: ubuntu-latest
env:
BUILD_DOCKER_MANUAL_ENV: ${{ inputs.distros || '' }}
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0 # Needed for detecting changes
- name: Set build matrix
id: set-matrix
run: .github/scripts/get-changed-dockerfiles.sh >> $GITHUB_OUTPUT
build-and-push:
needs: detect-changes
if: ${{ needs.detect-changes.outputs.matrix != '[]' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false # Don't cancel other distros if this one fails
matrix:
distro: ${{ fromJson(needs.detect-changes.outputs.matrix) }}
permissions:
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v7
with:
context: docker
file: ./docker/Dockerfile.${{ matrix.distro }}
push: true
tags: ghcr.io/${{ github.repository }}/ci-build-${{ matrix.distro }}:latest
- name: Cleanup old builds
uses: actions/delete-package-versions@v5
with:
package-name: libx52/ci-build-${{ matrix.distro }}
package-type: container
delete-only-untagged-versions: true

View File

@ -38,4 +38,4 @@ jobs:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5

View File

@ -6,8 +6,23 @@ The format is based upon [Keep a Changelog].
## [Unreleased]
### Changed
- Migrated CI builds to run in multiple distro containers.
### Fixed
- Addressed meson build bugs found in v0.3.3
## [0.3.3] - 2026-03-12
**Note:** While this release does introduce Meson support and deprecate the
Autotools framework, there are several bugs in the Meson build files, causing a
number of missing builds, notably the systemd service file, the documentation
and man pages. In addition, the translation files were not handled at all, and
translation services were disabled by default, compared to the Autotools
framework where it was enabled by default. These bugs will be addressed in the
next release.
### Added
- Updated build infrastructure to use Meson instead of Autotools.
- Added a [privacy policy](PRIVACY.md) to comply with GDPR/CCPA regulations.

File diff suppressed because it is too large Load Diff

View File

@ -5,22 +5,22 @@ Build has been tested on the following operating systems (x86-64 only):
* Ubuntu 22.04 LTS
* Ubuntu 24.04 LTS
* macOS Big Sur 11
* macOS Monterey 12
* Fedora latest (Fedora 42, as of this commit)
* Archlinux
* Alpine Linux (Experimental)
* Ubuntu 26.04 LTS (Experimental as of this commit)
* macOS (latest tag on Github, ARM)
# Prerequisites
## Required Packages
* automake
* autoconf
* autopoint
* meson
* ninja
* gettext
* hidapi + headers
* inih
* libtool
* libusb-1.0 + headers
* libevdev + headers (on Linux)
* pkg-config
* python3 (3.6 or greater)
* git (not required for builds, but necessary to clone the repository)
@ -29,15 +29,16 @@ Build has been tested on the following operating systems (x86-64 only):
| Platform | Install instructions |
| -------- | -------------------- |
| Ubuntu | `sudo apt-get install automake autoconf gettext autopoint libhidapi-dev libevdev-dev libtool libusb-1.0-0-dev libinih-dev pkg-config python3 git` |
| MacOS + Homebrew | `brew install automake autoconf gettext hidapi libtool libusb pkg-config python3 git` |
| Arch Linux | `pacman -S base-devel libusb hidapi libevdev libinih python git` |
| Fedora | `sudo dnf install autoconf automake gettext-devel findutils libtool hidapi-devel libusb-devel libevdev-devel inih-devel pkg-config python3 git` |
| Ubuntu | `sudo apt-get install meson gettext libhidapi-dev libevdev-dev libusb-1.0-0-dev libinih-dev pkg-config python3 git` |
| MacOS + Homebrew | `brew install meson gettext hidapi libtool libusb pkg-config python3 git` |
| Arch Linux | `pacman -S base-devel meson libusb hidapi libevdev libinih python git` |
| Fedora | `sudo dnf install meson gettext-devel findutils hidapi-devel libusb-devel libevdev-devel inih-devel pkg-config python3 git` |
## Optional Packages
* doxygen - to generate HTML documentation and man pages
* libcmocka (1.1 or greater) + headers - to run unit tests
* libevdev + headers (on Linux) - to add virtual keyboard/mouse support
# Installation Instructions
@ -49,37 +50,36 @@ git clone https://github.com/nirenjan/libx52.git
2. Run autogen.sh
```
cd ./libx52
./autogen.sh
meson setup build -Dprefix=/usr
```
3. Run the following commands:
```
./configure --prefix=/usr --localstatedir=/var --sysconfdir=/etc
make && sudo make install
meson compile -C build && meson install -C build
```
You may want to remove or edit the `--prefix=/usr` option, most users prefer
non-distro binaries in `/usr/local` (default without `--prefix`) or `/opt`.
You may want to remove or edit the `-Dprefix=/usr` option, most users prefer
non-distro binaries in `/usr/local` (default without `-Dprefix`) or `/opt`.
## Configuration options
### udev
The configuration system should automatically detect the udev rules directory,
but you can override it by using the following argument to `configure`:
but you can override it by using the following argument to `meson setup`:
```
--with-udevrulesdir=/path/to/udev/rules.d
-Dudev-rules-dir=/path/to/udev/rules.d
```
### Input group
The udev rules that are installed provide read/write access to members of the
input devices group. This defaults to `plugdev`, but can be modified using
the following argument to `configure`:
the following argument to `meson setup`:
```
--with-input-group=group
-Dinput-group=group
```
### Systemd support
@ -89,13 +89,13 @@ itself to run in the background. Typical deployments with systemd will have it
run in the foreground, and disable timestamps in the logs, since those are
inserted automatically by journald.
Systemd support is enabled by default, but can be disabled with the
`--disable-systemd` argument to `configure`
Systemd support is enabled by default, which disables timestamps in the program
logs, but you can re-enable timestamps by passing `-Dsystemd-logs=disabled`
argument to `meson setup`
It is also possible to configure the directory in which the service file is
installed with the following option. This is ignored if you have specified
`--disable-systemd`.
installed with the following option. This is ignored if systemd is not found.
```
--with-systemdsystemunitdir=/path/to/systemd/system
-Dsystemd-unit-dir=/path/to/systemd/system
```

View File

@ -4,6 +4,8 @@ Saitek X52Pro joystick driver for Linux
![Build/Test](https://github.com/nirenjan/libx52/workflows/Build/Test/badge.svg)
![Kernel Module](https://github.com/nirenjan/libx52/workflows/Kernel%20Module/badge.svg)
![CodeQL](https://github.com/nirenjan/libx52/workflows/CodeQL/badge.svg)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=nirenjan_libx52&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=nirenjan_libx52)
[![codecov](https://codecov.io/gh/nirenjan/libx52/graph/badge.svg?token=BH1DQQ5N3Y)](https://codecov.io/gh/nirenjan/libx52)
This project adds a new driver for the Saitek/MadCatz X52 Pro flight
control system. The X52 pro is a HOTAS (hand on throttle and stick)

View File

@ -65,7 +65,7 @@ devinfo_cleanup:
libx52io_exit(ctx);
}
int main(int argc, char **argv)
int main(void)
{
const struct libusb_version *libusb;

View File

@ -26,22 +26,26 @@ int libx52_init(libx52_device **dev)
int libx52_connect(libx52_device *dev)
{
(void)dev;
function_called();
return mock();
}
int libx52_update(libx52_device *dev)
{
(void)dev;
return LIBX52_SUCCESS;
}
void libx52_exit(libx52_device *dev)
{
(void)dev;
return;
}
const char *libx52_strerror(libx52_error_code rc)
{
(void)rc;
function_called();
return "";
}
@ -172,7 +176,7 @@ const struct CMUnitTest tests[] = {
#include "test_x52_cli_tests.c"
};
int main(int argc, char **argv)
int main(void)
{
cmocka_set_message_output(CM_OUTPUT_TAP);
cmocka_run_group_tests(tests, NULL, NULL);

View File

@ -8,7 +8,7 @@
#ifndef TEST_LIST
// Setup the test case function
#define TEST_CASE(tc) static void tc(void **state)
#define TEST_CASE(tc) static void tc(void **state __attribute__((unused)))
#define TEST_DEF(x) x
// Function header, this calls the corresponding libx52 function, and expects
// a certain number of calls to that function

View File

@ -19,6 +19,11 @@ LT_INIT
PKG_PROG_PKG_CONFIG
PKG_INSTALLDIR
AX_COMPILER_FLAGS
VISIBILITY_CFLAGS=
if test "x$GCC" = xyes; then
VISIBILITY_CFLAGS="-fvisibility=hidden"
fi
AC_SUBST([VISIBILITY_CFLAGS])
AC_CANONICAL_HOST
AX_GCC_FUNC_ATTRIBUTE([constructor])
AX_GCC_FUNC_ATTRIBUTE([destructor])

View File

@ -61,8 +61,11 @@ libx52dcomm_la_CFLAGS = \
-DLOCALEDIR=\"$(localedir)\" \
-DLOGDIR=\"$(localstatedir)/log\" \
-DRUNDIR=\"$(localstatedir)/run\" \
$(VISIBILITY_CFLAGS) \
$(WARN_CFLAGS)
libx52dcomm_la_LDFLAGS = $(WARN_LDFLAGS)
libx52dcomm_la_LDFLAGS = \
-export-symbols-regex '^x52d_(dial_command|dial_notify|format_command|send_command|recv_notification)$' \
$(WARN_LDFLAGS)
x52include_HEADERS += daemon/x52dcomm.h

View File

@ -1,4 +1,6 @@
# x52d
libx52dcomm_version = '1.0.0'
libx52dcomm_sources = [
'x52d_comm_client.c',
'x52d_comm_internal.c'
@ -8,6 +10,9 @@ install_headers('x52dcomm.h', subdir: meson.project_name())
lib_libx52dcomm = library('x52dcomm', libx52dcomm_sources,
dependencies: [dep_intl],
version: libx52dcomm_version,
c_args: sym_hidden_cargs,
install: true,
include_directories: includes)
x52d_sources = [
@ -22,28 +27,24 @@ x52d_sources = [
'x52d_notify.c',
'x52d_led.c',
'x52d_command.c',
'x52d_io.c',
'x52d_mouse_handler.c',
]
dep_threads = dependency('threads')
x52d_linkwith = [lib_libx52, lib_libx52dcomm]
# Comm sources are compiled into x52d (same as Autotools); libx52dcomm is only for x52ctl.
x52d_linkwith = [lib_libx52, lib_vkm, lib_libx52io]
x52d_deps = [dep_pinelog, dep_inih, dep_threads, dep_intl]
x52d_cflags = []
if dep_evdev.found()
x52d_sources += 'x52d_io.c'
x52d_sources += 'x52d_mouse_evdev.c'
x52d_cflags += '-DHAVE_EVDEV'
x52d_linkwith += lib_libx52io
x52d_deps += dep_evdev
endif
exe_x52d = executable('x52d', x52d_sources,
exe_x52d = executable('x52d', x52d_sources + libx52dcomm_sources,
install: true,
include_directories: includes,
c_args: x52d_cflags,
c_args: sym_hidden_cargs + x52d_cflags,
dependencies: x52d_deps,
link_with: x52d_linkwith)
executable('x52ctl', 'x52ctl.c',
exe_x52ctl = executable('x52ctl', 'x52ctl.c',
install: true,
dependencies: [dep_intl],
include_directories: includes,
@ -53,7 +54,7 @@ install_data('x52d.conf',
install_dir: join_paths(get_option('sysconfdir'), 'x52d'))
test('daemon-communication', files('test_daemon_comm.py')[0],
depends: exe_x52d, protocol: 'tap')
depends: [exe_x52d, exe_x52ctl], protocol: 'tap')
x52d_mouse_test_sources = ['x52d_mouse_test.c', 'x52d_mouse.c']
x52d_mouse_test = executable('x52d-mouse-test', x52d_mouse_test_sources,
@ -62,3 +63,24 @@ x52d_mouse_test = executable('x52d-mouse-test', x52d_mouse_test_sources,
test('x52d-mouse-test', x52d_mouse_test, protocol: 'tap')
# Install service file
if dep_systemd.found()
systemd_system_unit_dir = get_option('systemd-unit-dir')
if systemd_system_unit_dir == ''
systemd_system_unit_dir = dep_systemd.get_variable(
pkgconfig: 'systemd_system_unit_dir',
default_value: '/lib/systemd/system')
endif
sed = find_program('sed')
bindir_path = get_option('prefix') / get_option('bindir')
sed_script = 's|%bindir%|' + bindir_path + '|g'
systemd_service_file = configure_file(
input: 'x52d.service.in',
output: 'x52d.service',
command: [sed, sed_script, '@INPUT@'],
capture: true,
install: true,
install_dir: systemd_system_unit_dir
)
endif

View File

@ -89,15 +89,53 @@ static int send_command(int sock_fd, int argc, char **argv)
return 0;
}
static void interactive_mode(int sock_fd)
{
bool keep_running = true;
char buffer[1024];
fputs("> ", stdout);
while (keep_running && fgets(buffer, sizeof(buffer), stdin) != NULL) {
int sargc;
char *sargv[512] = { 0 };
int pos;
if (strcasecmp(buffer, "quit\n") == 0) {
keep_running = false;
} else {
/* Break the buffer into argc/argv */
sargc = 0;
pos = 0;
while (buffer[pos]) {
if (isspace(buffer[pos])) {
buffer[pos] = '\0';
pos++;
} else {
sargv[sargc] = &buffer[pos];
sargc++;
for (; buffer[pos] && !isspace(buffer[pos]); pos++);
}
}
if (send_command(sock_fd, sargc, sargv)) {
keep_running = false;
}
}
if (keep_running) {
fputs("\n> ", stdout);
}
}
}
int main(int argc, char **argv)
{
bool interactive = false;
char *socket_path = NULL;
const char *socket_path = NULL;
int opt;
int sock_fd;
int rc = EXIT_SUCCESS;
char buffer[1024];
/*
* Parse command line arguments
@ -142,47 +180,13 @@ int main(int argc, char **argv)
_("Running in interactive mode, ignoring extra arguments\n"));
}
fputs("> ", stdout);
while (fgets(buffer, sizeof(buffer), stdin) != NULL) {
int sargc;
char *sargv[512] = { 0 };
int pos;
if (strcasecmp(buffer, "quit\n") == 0) {
break;
}
/* Break the buffer into argc/argv */
sargc = 0;
pos = 0;
while (buffer[pos]) {
if (isspace(buffer[pos])) {
buffer[pos] = '\0';
pos++;
} else {
sargv[sargc] = &buffer[pos];
sargc++;
for (; buffer[pos] && !isspace(buffer[pos]); pos++);
}
}
if (send_command(sock_fd, sargc, sargv)) {
rc = EXIT_FAILURE;
goto cleanup;
}
fputs("\n> ", stdout);
}
interactive_mode(sock_fd);
} else {
if (send_command(sock_fd, argc - optind, &argv[optind])) {
rc = EXIT_FAILURE;
goto cleanup;
}
}
cleanup:
close(sock_fd);
return rc;
}

View File

@ -169,6 +169,7 @@ static pthread_t clock_thr;
static void * x52_clock_thr(void *param)
{
int rc;
(void)param;
PINELOG_INFO(_("Starting X52 clock manager thread"));
for (;;) {

View File

@ -409,6 +409,7 @@ int x52d_command_loop(int sock_fd)
static void * x52d_command_thread(void *param)
{
(void)param;
for (;;) {
if (x52d_command_loop(command_sock_fd) < 0) {
PINELOG_FATAL(_("Error %d during command loop: %s"),

View File

@ -29,6 +29,7 @@ static volatile bool device_update_needed;
static void *x52_dev_thr(void *param)
{
int rc;
(void)param;
#define DEV_ACQ_DELAY 5 // seconds
#define DEV_UPD_DELAY 50000 // microseconds

View File

@ -36,6 +36,7 @@ static void *x52_io_thr(void *param)
int rc;
libx52io_report report;
libx52io_report prev_report;
(void)param;
#define IO_READ_TIMEOUT 50 /* milliseconds */
#define IO_ACQ_TIMEOUT 5 /* seconds */

View File

@ -13,6 +13,7 @@
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <inttypes.h>
#include <unistd.h>
#include <errno.h>
@ -38,12 +39,14 @@ static void termination_handler(int signum)
static volatile bool flag_reload;
static void reload_handler(int signum)
{
(void)signum;
flag_reload = true;
}
static volatile bool flag_save_cfg;
static void save_config_handler(int signum)
{
(void)signum;
flag_save_cfg = true;
}
@ -111,14 +114,16 @@ static void start_daemon(bool foreground, const char *pid_file)
pid_fd = fopen(pid_file, "r");
if (pid_fd != NULL) {
int rc;
intmax_t tmp_pid;
/* File exists, read the PID and check if it exists */
rc = fscanf(pid_fd, "%u", &pid);
rc = fscanf(pid_fd, "%" SCNdMAX, &tmp_pid);
fclose(pid_fd);
if (rc != 1) {
perror("fscanf");
} else {
pid = (pid_t)tmp_pid;
rc = kill(pid, 0);
if (rc == 0 || (rc < 0 && errno == EPERM)) {
PINELOG_FATAL(_("Daemon is already running as PID %u"), pid);
@ -324,10 +329,8 @@ int main(int argc, char **argv)
goto cleanup;
}
x52d_notify_init(notify_sock);
#if defined(HAVE_EVDEV)
x52d_io_init();
x52d_mouse_evdev_init();
#endif
x52d_mouse_handler_init();
// Re-enable signals
rc = pthread_sigmask(SIG_UNBLOCK, &sigblockset, NULL);
@ -366,10 +369,8 @@ cleanup:
x52d_dev_exit();
x52d_command_exit();
x52d_notify_exit();
#if defined(HAVE_EVDEV)
x52d_mouse_evdev_exit();
x52d_mouse_handler_exit();
x52d_io_exit();
#endif
// Remove the PID file
PINELOG_TRACE("Removing PID file %s", pid_file);

View File

@ -30,9 +30,7 @@ void x52d_cfg_set_Mouse_Enabled(bool enabled)
{
PINELOG_DEBUG(_("Setting mouse enable to %s"),
enabled ? _("on") : _("off"));
#if defined HAVE_EVDEV
x52d_mouse_evdev_thread_control(enabled);
#endif
x52d_mouse_thread_control(enabled);
}
void x52d_cfg_set_Mouse_Speed(int speed)

View File

@ -18,9 +18,9 @@ extern volatile int mouse_scroll_dir;
#define MOUSE_MULT_FACTOR 4
void x52d_mouse_evdev_thread_control(bool enabled);
void x52d_mouse_evdev_init(void);
void x52d_mouse_evdev_exit(void);
void x52d_mouse_thread_control(bool enabled);
void x52d_mouse_handler_init(void);
void x52d_mouse_handler_exit(void);
void x52d_mouse_report_event(libx52io_report *report);
#endif // !defined X52D_MOUSE_H

View File

@ -1,7 +1,7 @@
/*
* Saitek X52 Pro MFD & LED driver - Mouse driver
*
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
* Copyright (C) 2021-2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
*
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
@ -12,10 +12,10 @@
#include <pthread.h>
#include <unistd.h>
#include "libevdev/libevdev.h"
#include "libevdev/libevdev-uinput.h"
#include "libx52io.h"
#include "vkm.h"
#define PINELOG_MODULE X52D_MOD_MOUSE
#include "pinelog.h"
#include "x52d_config.h"
#include "x52d_const.h"
@ -24,36 +24,47 @@
static pthread_t mouse_thr;
static bool mouse_thr_enabled = false;
static struct libevdev_uinput *mouse_uidev;
static bool mouse_uidev_created = false;
static vkm_context *mouse_context;
static volatile libx52io_report old_report;
static volatile libx52io_report new_report;
static int report_button_change(int button, int index)
static int report_button_change(vkm_mouse_button button, int index)
{
int rc = 1;
vkm_result rc = VKM_ERROR_NO_CHANGE;
bool old_button = old_report.button[index];
bool new_button = new_report.button[index];
vkm_button_state state;
if (old_button != new_button) {
rc = libevdev_uinput_write_event(mouse_uidev, EV_KEY, button,
(int)new_button);
if (rc != 0) {
PINELOG_ERROR(_("Error writing mouse button event (button %d, state %d)"),
button, (int)new_button);
state = new_button ? VKM_BUTTON_PRESSED : VKM_BUTTON_RELEASED;
rc = vkm_mouse_click(mouse_context, button, state);
if (rc != VKM_SUCCESS && rc != VKM_ERROR_NO_CHANGE) {
PINELOG_ERROR(_("Error %d writing mouse button event (button %d, state %d)"),
rc, button, (int)new_button);
}
}
return rc;
return (rc == VKM_SUCCESS);
}
static int report_wheel(void)
{
int rc = 1;
vkm_result rc = VKM_ERROR_NO_CHANGE;
int wheel = 0;
bool scroll_up = new_report.button[LIBX52IO_BTN_MOUSE_SCROLL_UP];
bool scroll_dn = new_report.button[LIBX52IO_BTN_MOUSE_SCROLL_DN];
bool old_scroll_up = old_report.button[LIBX52IO_BTN_MOUSE_SCROLL_UP];
bool old_scroll_dn = old_report.button[LIBX52IO_BTN_MOUSE_SCROLL_DN];
vkm_mouse_scroll_direction dir;
/*
* Handle multiple scroll button presses in sequence. This happens if a
* hardware axis is very noisy and the firmware sends a sequence of reports
* with button down, even though this is technically a momentary button.
*/
scroll_up = scroll_up && !old_scroll_up;
scroll_dn = scroll_dn && !old_scroll_dn;
if (scroll_up) {
// Scroll up event
@ -64,19 +75,18 @@ static int report_wheel(void)
}
if (wheel != 0) {
rc = libevdev_uinput_write_event(mouse_uidev, EV_REL, REL_WHEEL, wheel);
if (rc != 0) {
PINELOG_ERROR(_("Error writing mouse wheel event %d"), wheel);
dir = (wheel == 1) ? VKM_MOUSE_SCROLL_UP : VKM_MOUSE_SCROLL_DOWN;
rc = vkm_mouse_scroll(mouse_context, dir);
if (rc != VKM_SUCCESS) {
PINELOG_ERROR(_("Error writing mouse wheel event %d"), dir);
}
}
return rc;
return (rc == VKM_SUCCESS);
}
static int report_axis(int axis, int index)
static int get_axis_val(int index)
{
int rc = 1;
int axis_val = new_report.axis[index];
/*
@ -96,22 +106,29 @@ static int report_axis(int axis, int index)
*/
axis_val = (axis_val * mouse_mult) / MOUSE_MULT_FACTOR;
if (axis_val) {
rc = libevdev_uinput_write_event(mouse_uidev, EV_REL, axis, axis_val);
if (rc != 0) {
PINELOG_ERROR(_("Error writing mouse axis event (axis %d, value %d)"),
axis, axis_val);
}
return axis_val;
}
static int report_axis(void)
{
vkm_result rc;
int dx = get_axis_val(LIBX52IO_AXIS_THUMBX);
int dy = get_axis_val(LIBX52IO_AXIS_THUMBY);
rc = vkm_mouse_move(mouse_context, dx, dy);
if (rc != VKM_SUCCESS && rc != VKM_ERROR_NO_CHANGE) {
PINELOG_ERROR(_("Error %d writing mouse axis event (dx %d, dy %d)"),
rc, dx, dy);
}
return rc;
return (rc == VKM_SUCCESS);
}
static void report_sync(void)
{
int rc;
rc = libevdev_uinput_write_event(mouse_uidev, EV_SYN, SYN_REPORT, 0);
if (rc != 0) {
vkm_result rc;
rc = vkm_sync(mouse_context);
if (rc != VKM_SUCCESS) {
PINELOG_ERROR(_("Error writing mouse sync event"));
} else {
memcpy((void *)&old_report, (void *)&new_report, sizeof(old_report));
@ -129,15 +146,11 @@ static void reset_reports(void)
static void * x52_mouse_thr(void *param)
{
bool state_changed;
(void)param;
PINELOG_INFO(_("Starting X52 virtual mouse driver thread"));
for (;;) {
state_changed = false;
state_changed |= (0 == report_axis(REL_X, LIBX52IO_AXIS_THUMBX));
state_changed |= (0 == report_axis(REL_Y, LIBX52IO_AXIS_THUMBY));
if (state_changed) {
if (report_axis()) {
report_sync();
}
@ -165,9 +178,9 @@ static void x52d_mouse_thr_exit(void)
pthread_cancel(mouse_thr);
}
void x52d_mouse_evdev_thread_control(bool enabled)
void x52d_mouse_thread_control(bool enabled)
{
if (!mouse_uidev_created) {
if (!vkm_is_ready(mouse_context)) {
PINELOG_INFO(_("Virtual mouse not created. Ignoring thread state change"));
return;
}
@ -197,14 +210,14 @@ void x52d_mouse_report_event(libx52io_report *report)
if (report) {
memcpy((void *)&new_report, report, sizeof(new_report));
if (!mouse_uidev_created || !mouse_thr_enabled) {
if (!vkm_is_ready(mouse_context) || !mouse_thr_enabled) {
return;
}
state_changed = false;
state_changed |= (0 == report_button_change(BTN_LEFT, LIBX52IO_BTN_MOUSE_PRIMARY));
state_changed |= (0 == report_button_change(BTN_RIGHT, LIBX52IO_BTN_MOUSE_SECONDARY));
state_changed |= (0 == report_wheel());
state_changed = state_changed || (0 == report_button_change(VKM_MOUSE_BTN_LEFT, LIBX52IO_BTN_MOUSE_PRIMARY));
state_changed = state_changed || (0 == report_button_change(VKM_MOUSE_BTN_RIGHT, LIBX52IO_BTN_MOUSE_SECONDARY));
state_changed = state_changed || (0 == report_wheel());
if (state_changed) {
report_sync();
@ -214,35 +227,27 @@ void x52d_mouse_report_event(libx52io_report *report)
}
}
void x52d_mouse_evdev_init(void)
void x52d_mouse_handler_init(void)
{
int rc;
struct libevdev *dev;
vkm_result rc;
/* Create a new mouse device */
dev = libevdev_new();
libevdev_set_name(dev, "X52 virtual mouse");
libevdev_enable_event_type(dev, EV_REL);
libevdev_enable_event_code(dev, EV_REL, REL_X, NULL);
libevdev_enable_event_code(dev, EV_REL, REL_Y, NULL);
libevdev_enable_event_code(dev, EV_REL, REL_WHEEL, NULL);
libevdev_enable_event_type(dev, EV_KEY);
libevdev_enable_event_code(dev, EV_KEY, BTN_LEFT, NULL);
libevdev_enable_event_code(dev, EV_KEY, BTN_RIGHT, NULL);
rc = vkm_init(&mouse_context);
if (rc != VKM_SUCCESS) {
PINELOG_ERROR(_("Error %d creating X52 virtual mouse"), rc);
return;
}
rc = libevdev_uinput_create_from_device(dev, LIBEVDEV_UINPUT_OPEN_MANAGED,
&mouse_uidev);
if (rc != 0) {
PINELOG_ERROR(_("Error %d creating X52 virtual mouse: %s"),
-rc, strerror(-rc));
} else {
mouse_uidev_created = true;
vkm_set_option(mouse_context, VKM_OPT_DEVICE_NAME, "X52 virtual mouse");
rc = vkm_start(mouse_context);
if (rc != VKM_SUCCESS) {
PINELOG_ERROR(_("Error %d creating X52 virtual mouse"), rc);
}
}
void x52d_mouse_evdev_exit(void)
void x52d_mouse_handler_exit(void)
{
x52d_mouse_evdev_thread_control(false);
mouse_uidev_created = false;
libevdev_uinput_destroy(mouse_uidev);
x52d_mouse_thread_control(false);
vkm_exit(mouse_context);
mouse_context = NULL;
}

View File

@ -19,37 +19,34 @@
#include "x52d_const.h"
#include "x52d_mouse.h"
#if defined HAVE_EVDEV
/* Stub for evdev */
void x52d_mouse_evdev_thread_control(bool enabled)
/* Stub for handler */
void x52d_mouse_thread_control(bool enabled)
{
function_called();
check_expected(enabled);
}
#endif
static void test_mouse_thread_enabled(void **state)
{
#if defined HAVE_EVDEV
expect_function_calls(x52d_mouse_evdev_thread_control, 1);
expect_value(x52d_mouse_evdev_thread_control, enabled, true);
#endif
(void)state;
expect_function_calls(x52d_mouse_thread_control, 1);
expect_value(x52d_mouse_thread_control, enabled, true);
x52d_cfg_set_Mouse_Enabled(true);
}
static void test_mouse_thread_disabled(void **state)
{
#if defined HAVE_EVDEV
expect_function_calls(x52d_mouse_evdev_thread_control, 1);
expect_value(x52d_mouse_evdev_thread_control, enabled, false);
#endif
(void)state;
expect_function_calls(x52d_mouse_thread_control, 1);
expect_value(x52d_mouse_thread_control, enabled, false);
x52d_cfg_set_Mouse_Enabled(false);
}
static void test_mouse_speed_negative(void **state)
{
(void)state;
int orig_mouse_delay = mouse_delay;
int orig_mouse_mult = mouse_mult;
@ -61,6 +58,7 @@ static void test_mouse_speed_negative(void **state)
/* The following tests are dependent on the values in x52d_mouse.c */
static void test_mouse_speed_0(void **state)
{
(void)state;
x52d_cfg_set_Mouse_Speed(0);
assert_int_equal(mouse_delay, 70000);
assert_int_equal(mouse_mult, 4);
@ -68,6 +66,7 @@ static void test_mouse_speed_0(void **state)
static void test_mouse_speed_mid_base(void **state)
{
(void)state;
x52d_cfg_set_Mouse_Speed(6);
assert_int_equal(mouse_delay, 40000);
assert_int_equal(mouse_mult, 4);
@ -75,6 +74,7 @@ static void test_mouse_speed_mid_base(void **state)
static void test_mouse_speed_max_base(void **state)
{
(void)state;
x52d_cfg_set_Mouse_Speed(12);
assert_int_equal(mouse_delay, 10000);
assert_int_equal(mouse_mult, 4);
@ -82,6 +82,7 @@ static void test_mouse_speed_max_base(void **state)
static void test_mouse_speed_min_hyper(void **state)
{
(void)state;
x52d_cfg_set_Mouse_Speed(13);
assert_int_equal(mouse_delay, 10000);
assert_int_equal(mouse_mult, 5);
@ -89,6 +90,7 @@ static void test_mouse_speed_min_hyper(void **state)
static void test_mouse_speed_mid_hyper(void **state)
{
(void)state;
x52d_cfg_set_Mouse_Speed(22);
assert_int_equal(mouse_delay, 10000);
assert_int_equal(mouse_mult, 14);
@ -96,6 +98,7 @@ static void test_mouse_speed_mid_hyper(void **state)
static void test_mouse_speed_max_hyper(void **state)
{
(void)state;
x52d_cfg_set_Mouse_Speed(32);
assert_int_equal(mouse_delay, 10000);
assert_int_equal(mouse_mult, 24);
@ -105,6 +108,7 @@ static void test_mouse_speed_above_max(void **state)
{
int orig_mouse_delay = mouse_delay;
int orig_mouse_mult = mouse_mult;
(void)state;
x52d_cfg_set_Mouse_Speed(33);
assert_int_equal(mouse_delay, orig_mouse_delay);
@ -113,12 +117,14 @@ static void test_mouse_speed_above_max(void **state)
static void test_mouse_reverse_scroll_enabled(void **state)
{
(void)state;
x52d_cfg_set_Mouse_ReverseScroll(true);
assert_int_equal(mouse_scroll_dir, -1);
}
static void test_mouse_reverse_scroll_disabled(void **state)
{
(void)state;
x52d_cfg_set_Mouse_ReverseScroll(false);
assert_int_equal(mouse_scroll_dir, 1);
}

View File

@ -71,6 +71,7 @@ static void * x52_notify_thr(void * param)
char buffer[X52D_BUFSZ];
uint16_t bufsiz;
int rc;
(void)param;
for (;;) {
do {
@ -146,6 +147,7 @@ static void * x52_notify_loop(void * param)
{
struct pollfd pfd[MAX_CONN];
int rc;
(void)param;
for (;;) {
rc = x52d_client_poll(client_fd, pfd, notify_sock);

View File

@ -22,6 +22,16 @@
#include <stddef.h>
#ifndef X52DCOMM_API
# if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 303
# define X52DCOMM_API __attribute__((visibility("default")))
# elif defined(_WIN32)
# define X52DCOMM_API __declspec(dllexport)
# else
# define X52DCOMM_API
# endif
#endif
#ifdef __cplusplus
extern "C" {
#endif
@ -52,7 +62,7 @@ extern "C" {
*
* @exception E2BIG returned if the passed socket path is too big
*/
int x52d_dial_command(const char *sock_path);
X52DCOMM_API int x52d_dial_command(const char *sock_path);
/**
* @brief Open a connection to the daemon notify socket.
@ -73,7 +83,7 @@ int x52d_dial_command(const char *sock_path);
*
* @exception E2BIG returned if the passed socket path is too big
*/
int x52d_dial_notify(const char *sock_path);
X52DCOMM_API int x52d_dial_notify(const char *sock_path);
/**
* @brief Format a series of command strings into a buffer
@ -92,7 +102,7 @@ int x52d_dial_notify(const char *sock_path);
* @returns number of bytes in the formatted command
* @returns -1 on an error condition, and \c errno is set accordingly.
*/
int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen);
X52DCOMM_API int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen);
/**
* @brief Send a command to the daemon and retrieve the response.
@ -121,7 +131,7 @@ int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen
* @returns number of bytes returned from the server
* @returns -1 on an error condition, and \c errno is set accordingly.
*/
int x52d_send_command(int sock_fd, char *buffer, size_t bufin, size_t bufout);
X52DCOMM_API int x52d_send_command(int sock_fd, char *buffer, size_t bufin, size_t bufout);
/**
* @brief Notification callback function type
@ -147,7 +157,7 @@ typedef int (* x52d_notify_callback_fn)(int argc, char **argv);
* @returns return code of the callback function on success
* @returns -1 on an error condition, and \c errno is set accordingly.
*/
int x52d_recv_notification(int sock_fd, x52d_notify_callback_fn callback);
X52DCOMM_API int x52d_recv_notification(int sock_fd, x52d_notify_callback_fn callback);
/** @} */
#ifdef __cplusplus

View File

@ -0,0 +1,8 @@
FROM archlinux:base-devel
LABEL org.opencontainers.image.description="INTERNAL CI USE ONLY - Not intended for production"
LABEL org.opencontainers.image.authors="Nirenjan Krishnan"
COPY ./install-dependencies-archlinux.sh ./ci-setup.sh /tmp/
RUN /tmp/install-dependencies-archlinux.sh && /tmp/ci-setup.sh

View File

@ -0,0 +1,8 @@
FROM fedora:latest
LABEL org.opencontainers.image.description="INTERNAL CI USE ONLY - Not intended for production"
LABEL org.opencontainers.image.authors="Nirenjan Krishnan"
COPY ./install-dependencies-fedora.sh ./ci-setup.sh /tmp/
RUN /tmp/install-dependencies-fedora.sh && dnf clean all && /tmp/ci-setup.sh

View File

@ -0,0 +1,12 @@
FROM ubuntu:22.04
LABEL org.opencontainers.image.description="INTERNAL CI USE ONLY - Not intended for production"
LABEL org.opencontainers.image.authors="Nirenjan Krishnan"
ENV DEBIAN_FRONTEND=noninteractive
COPY ./install-dependencies-ubuntu.sh ./ci-setup.sh /tmp/
RUN /tmp/install-dependencies-ubuntu.sh && \
rm -rf /var/lib/apt/lists/* && \
/tmp/ci-setup.sh

View File

@ -0,0 +1,12 @@
FROM ubuntu:24.04
LABEL org.opencontainers.image.description="INTERNAL CI USE ONLY - Not intended for production"
LABEL org.opencontainers.image.authors="Nirenjan Krishnan"
ENV DEBIAN_FRONTEND=noninteractive
COPY ./install-dependencies-ubuntu.sh ./ci-setup.sh /tmp/
RUN /tmp/install-dependencies-ubuntu.sh && \
rm -rf /var/lib/apt/lists/* && \
/tmp/ci-setup.sh

View File

@ -0,0 +1,13 @@
FROM ubuntu:26.04
LABEL org.opencontainers.image.description="INTERNAL CI USE ONLY - Not intended for production"
LABEL org.opencontainers.image.authors="Nirenjan Krishnan"
LABEL com.project.ci.experimental="true"
ENV DEBIAN_FRONTEND=noninteractive
COPY ./install-dependencies-ubuntu.sh ./ci-setup.sh /tmp/
RUN /tmp/install-dependencies-ubuntu.sh && \
rm -rf /var/lib/apt/lists/* && \
/tmp/ci-setup.sh

37
docker/README.md 100644
View File

@ -0,0 +1,37 @@
Dockerfiles for CI builds
=========================
This repo contains a number of Dockerfiles for building libx52 against multiple
distros. Github only supports Ubuntu LTS for it's Linux runners, but libx52
users run multiple distros, including Arch Linux, Gentoo and Fedora.
For this reason, it's better to get a prebuilt Docker image that builds the CI
environment, pulling in all the relevant dependencies, and staging the CI build
environment as a container within Github. The regular build pipeline can then
run on these containers, and this will help ensure that the breakages are
reduced to a minimum.
Build Instructions
==================
In order to run the CI build on all the supported platforms, first run the
`build-containers.sh` script, it will find the Dockerfiles for all the
suppported platforms in the `docker` directory, and build the container images
for them with the necessary dependencies included.
Once the container images are built, you can build against one or all of the
container images by running `build-repo.sh`. If you don't specify the container
distro, which is basically the same as the extension after `Dockerfile.`, it
will run everything. If you specify a distro that doesn't exist, or has not been
built, the script will silently exit.
Extending to a new distro
=========================
To extend the builds to a new distro, create a `Dockerfile.<distro>` with the
necessary instructions to build a container image for that distro. Make sure you
install the necessary dependencies for the distro. It is strongly recommended to
add a `install-dependencies-<distro>.sh` so that you can build against multiple
versions of that distro, eg. Ubuntu 22.04 and Ubuntu 24.04. Make sure you copy
`ci-setup.sh` to allow setting up the environment, since it is highly likely
that the `meson dist` command will fail if you do not run that script.

View File

@ -0,0 +1,25 @@
#!/bin/bash
# Build containers for all the target environments
set -euo pipefail
SCRIPT_DIR=$(dirname "$0")
if command -v realpath &>/dev/null
then
SCRIPT_DIR=$(realpath "$SCRIPT_DIR")
fi
GITHUB_SCRIPTS_DIR="$SCRIPT_DIR/../.github/scripts"
if command -v realpath &>/dev/null
then
GITHUB_SCRIPTS_DIR=$(realpath "$GITHUB_SCRIPTS_DIR")
fi
for dockerfile in "$SCRIPT_DIR"/[Dd]ockerfile.*
do
if [[ -e "$dockerfile" ]]
then
SUFFIX="$(basename "$dockerfile" | cut -d. -f2)"
TAG="ghcr.io/nirenjan/libx52/ci-build-${SUFFIX}:latest"
docker build --tag "$TAG" -f "$dockerfile" "${SCRIPT_DIR}"
fi
done

View File

@ -0,0 +1,40 @@
#!/bin/bash
# Build the repository for all found directories
# Build containers for all the target environments
set -euo pipefail
GIT_ROOT=$(git rev-parse --show-toplevel)
CC=${CC:-gcc}
IMAGE="${1:-*}"
for image in $(docker images --filter "reference=ghcr.io/nirenjan/libx52/ci-build-${IMAGE}" --format '{{ .Repository}}')
do
distro=${image##*/ci-build-}
container_name="libx52-runner-${distro}"
if [[ "$(docker ps -aq -f name=$container_name)" ]]
then
echo "Cleaning up old container for '$distro'"
docker rm -f $container_name >/dev/null
fi
experimental=$(docker inspect --format='{{.Config.Labels}}' $image | \
grep -q 'experimental:true' && echo " (experimental)" || true)
if docker run --rm --name $container_name \
--device /dev/bus/usb:/dev/bus/usb \
-v "$GIT_ROOT":/code \
-w /code \
-e CC="${CC}" \
$image \
/bin/bash -c ".github/scripts/build-and-test.sh builddir/${distro}" \
&> "$GIT_ROOT/build-${distro}.log"
then
echo "=== ${distro}${experimental} OK ==="
else
echo "=== ${distro}${experimental} !!! FAIL !!! ==="
tail -20 "$GIT_ROOT/build-${distro}.log"
fi
done

10
docker/ci-setup.sh 100755
View File

@ -0,0 +1,10 @@
#!/bin/sh
# Common CI setup
git config --global --add safe.directory /code
git config --global --add safe.directory '*'
cat >> /root/.bashrc <<"EOF"
echo 'WARNING: This is an internal CI container'
echo 'Do not use for production'
EOF

View File

@ -0,0 +1,15 @@
# This file is deliberately named with a lowercase d, in order to avoid the
# Github action logic from picking this up as a supported distro. The Alpine
# image fails in the Github actions, because it needs the /dev/bus/usb device
# mounted inside the container, however, attempting to do so causes every
# build to fail. Therefore, we disable the Alpine image in CI, but keep it
# local, so that we can test the build against Alpine locally if necessary.
FROM alpine:latest
LABEL org.opencontainers.image.description="INTERNAL CI USE ONLY - Not intended for production"
LABEL org.opencontainers.image.authors="Nirenjan Krishnan"
LABEL com.project.ci.experimental="true"
COPY ./install-dependencies-alpine.sh ./ci-setup.sh /tmp/
RUN /tmp/install-dependencies-alpine.sh
RUN /tmp/ci-setup.sh

View File

@ -0,0 +1,19 @@
#!/bin/sh
# Setup for alpine
set -eux
apk update
apk add --no-cache \
build-base \
meson \
bash \
git \
gettext \
libusb-dev \
hidapi-dev \
libevdev-dev \
inih-dev \
cmocka-dev \
tzdata \
musl-libintl \
doxygen

View File

@ -0,0 +1,14 @@
#!/bin/bash
# Install dependencies on Archlinux container
# Assumes that it's running off a base-devel tag
set -euo pipefail
pacman -Syu --noconfirm \
git \
meson \
libusb \
hidapi \
libinih \
libevdev \
python \
gettext

View File

@ -0,0 +1,16 @@
#!/bin/bash
# Install dependencies on Fedora container
set -euo pipefail
dnf update -y
dnf install -y \
gcc \
git \
meson \
libusb1-devel \
hidapi-devel \
inih-devel \
libevdev-devel \
pkg-config \
python3 \
gettext-devel

View File

@ -0,0 +1,19 @@
#!/bin/bash
# Install dependencies to build and test on Ubuntu runners
apt-get update && apt-get upgrade -y
apt-get install -y \
git \
gcc clang \
meson \
gettext \
pkg-config \
python3 \
libusb-1.0-0-dev \
libhidapi-dev \
libevdev-dev \
libinih-dev \
doxygen \
libcmocka-dev \
tzdata
exit 0

View File

@ -48,5 +48,7 @@ functions.
- libx52.h
- libx52io.h
- libx52util.h
- vkm.h
- x52dcomm.h
*/

View File

@ -44,6 +44,7 @@ static bool exit_loop = false;
static void signal_handler(int sig)
{
(void)sig;
exit_loop = true;
}
@ -52,7 +53,7 @@ static bool denoise = true;
/* For i18n */
#define _(x) gettext(x)
int main(int argc, char **argv)
int main(void)
{
libx52io_context *ctx;
libx52io_report last, curr;

View File

@ -0,0 +1,25 @@
#!/bin/sh
# Install Doxygen HTML and man trees from the build directory.
# Arguments are paths relative to the install prefix (MESON_INSTALL_DESTDIR_PREFIX).
set -e
doc_html="$1"
mandir="$2"
WANTED_PAGES="man1/x52cli.1 man1/x52bugreport.1"
if [ -d "$MESON_BUILD_ROOT/docs/html" ]; then
mkdir -p "$MESON_INSTALL_DESTDIR_PREFIX/$doc_html"
cp -R "$MESON_BUILD_ROOT/docs/html"/. "$MESON_INSTALL_DESTDIR_PREFIX/$doc_html/"
fi
if [ -d "$MESON_BUILD_ROOT/docs/man" ]; then
MANDIR="$MESON_INSTALL_DESTDIR_PREFIX/$mandir"
mkdir -p "$MANDIR"
for manpage in $WANTED_PAGES
do
section=$(dirname "$manpage")
mkdir -p "$MANDIR/$section"
cp "$MESON_BUILD_ROOT/docs/man/$manpage" "$MANDIR/$section"
done
fi

View File

@ -24,6 +24,7 @@ libx52_la_CFLAGS = \
@LIBUSB_CFLAGS@ \
-DLOCALEDIR=\"$(localedir)\" \
-I $(top_srcdir) \
$(VISIBILITY_CFLAGS) \
$(WARN_CFLAGS)
libx52_la_LDFLAGS = \
-export-symbols-regex '^libx52_' \

View File

@ -24,6 +24,17 @@
#include <stdint.h>
#include <stdbool.h>
/** Applied to public library entry points (default visibility with hidden ELF default). */
#ifndef LIBX52_API
# if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 303
# define LIBX52_API __attribute__((visibility("default")))
# elif defined(_WIN32)
# define LIBX52_API __declspec(dllexport)
# else
# define LIBX52_API
# endif
#endif
#ifdef __cplusplus
extern "C" {
#endif
@ -275,7 +286,7 @@ typedef enum {
*
* @returns \ref libx52_error_code indicating status
*/
int libx52_init(libx52_device ** dev);
LIBX52_API int libx52_init(libx52_device ** dev);
/**
* @brief Exit the library and free up any resources used
@ -286,7 +297,7 @@ int libx52_init(libx52_device ** dev);
*
* @param[in] dev Pointer to the device context
*/
void libx52_exit(libx52_device *dev);
LIBX52_API void libx52_exit(libx52_device *dev);
/** @} */
@ -316,7 +327,7 @@ void libx52_exit(libx52_device *dev);
*
* @returns \ref libx52_error_code indicating status
*/
int libx52_connect(libx52_device *dev);
LIBX52_API int libx52_connect(libx52_device *dev);
/**
* @brief Disconnect from the X52 device
@ -329,7 +340,7 @@ int libx52_connect(libx52_device *dev);
*
* @returns \ref libx52_error_code indicating status
*/
int libx52_disconnect(libx52_device *dev);
LIBX52_API int libx52_disconnect(libx52_device *dev);
/**
* @brief Check if joystick is connected
@ -350,7 +361,7 @@ int libx52_disconnect(libx52_device *dev);
*
* @returns Boolean indicating if the internal device handle is valid.
*/
bool libx52_is_connected(libx52_device *dev);
LIBX52_API bool libx52_is_connected(libx52_device *dev);
/** @} */
@ -390,7 +401,7 @@ bool libx52_is_connected(libx52_device *dev);
* - \ref LIBX52_ERROR_INVALID_PARAM if either \p x52 is invalid, or \p line is
* outside the accepted range.
*/
int libx52_set_text(libx52_device *x52, uint8_t line, const char *text, uint8_t length);
LIBX52_API int libx52_set_text(libx52_device *x52, uint8_t line, const char *text, uint8_t length);
/**
* @brief Set the LED state
@ -415,7 +426,7 @@ int libx52_set_text(libx52_device *x52, uint8_t line, const char *text, uint8_t
* not a supported one. The API also returns \ref LIBX52_ERROR_NOT_SUPPORTED
* if the probed joystick is not an X52 Pro, but the non-Pro X52 variant.
*/
int libx52_set_led_state(libx52_device *x52,
LIBX52_API int libx52_set_led_state(libx52_device *x52,
libx52_led_id led,
libx52_led_state state);
@ -455,7 +466,7 @@ int libx52_set_led_state(libx52_device *x52,
* - \ref LIBX52_ERROR_TRY_AGAIN if no change from previous time
* - \ref LIBX52_ERROR_INVALID_PARAM if \p x52 is not valid.
*/
int libx52_set_clock(libx52_device *x52, time_t time, int local);
LIBX52_API int libx52_set_clock(libx52_device *x52, time_t time, int local);
/**
* @brief Set the timezone for the secondary and tertiary clocks.
@ -487,7 +498,7 @@ int libx52_set_clock(libx52_device *x52, time_t time, int local);
* - \ref LIBX52_ERROR_NOT_SUPPORTED if \p clock is \ref LIBX52_CLOCK_1
* - \ref LIBX52_ERROR_OUT_OF_RANGE if \p offset is more than &plusmn; 24 hours.
*/
int libx52_set_clock_timezone(libx52_device *x52,
LIBX52_API int libx52_set_clock_timezone(libx52_device *x52,
libx52_clock_id clock,
int offset);
@ -511,7 +522,7 @@ int libx52_set_clock_timezone(libx52_device *x52,
* - \ref LIBX52_ERROR_INVALID_PARAM if \p x52 is not valid, or if either of \p
* clock or \p format are outside their respective ranges.
*/
int libx52_set_clock_format(libx52_device *x52,
LIBX52_API int libx52_set_clock_format(libx52_device *x52,
libx52_clock_id clock,
libx52_clock_format format);
@ -530,7 +541,7 @@ int libx52_set_clock_format(libx52_device *x52,
* - 0 on success
* - \ref LIBX52_ERROR_INVALID_PARAM if \p x52 is not valid
*/
int libx52_set_time(libx52_device *x52, uint8_t hour, uint8_t minute);
LIBX52_API int libx52_set_time(libx52_device *x52, uint8_t hour, uint8_t minute);
/**
* @brief Set the date
@ -547,7 +558,7 @@ int libx52_set_time(libx52_device *x52, uint8_t hour, uint8_t minute);
* - 0 on success
* - \ref LIBX52_ERROR_INVALID_PARAM if \p x52 is not valid
*/
int libx52_set_date(libx52_device *x52, uint8_t dd, uint8_t mm, uint8_t yy);
LIBX52_API int libx52_set_date(libx52_device *x52, uint8_t dd, uint8_t mm, uint8_t yy);
/**
* @brief Set the date format for the MFD date display
@ -561,7 +572,7 @@ int libx52_set_date(libx52_device *x52, uint8_t dd, uint8_t mm, uint8_t yy);
* - 0 on success
* - \ref LIBX52_ERROR_INVALID_PARAM if \p x52 is not valid
*/
int libx52_set_date_format(libx52_device *x52, libx52_date_format format);
LIBX52_API int libx52_set_date_format(libx52_device *x52, libx52_date_format format);
/** @} */
@ -586,7 +597,7 @@ int libx52_set_date_format(libx52_device *x52, libx52_date_format format);
* - 0 on success
* - \ref LIBX52_ERROR_INVALID_PARAM if \p x52 is not valid
*/
int libx52_set_brightness(libx52_device *x52, uint8_t mfd, uint16_t brightness);
LIBX52_API int libx52_set_brightness(libx52_device *x52, uint8_t mfd, uint16_t brightness);
/**
* @brief Set the state of the shift indicator
@ -601,7 +612,7 @@ int libx52_set_brightness(libx52_device *x52, uint8_t mfd, uint16_t brightness);
* - 0 on success
* - \ref LIBX52_ERROR_INVALID_PARAM if \p x52 is not valid
*/
int libx52_set_shift(libx52_device *x52, uint8_t state);
LIBX52_API int libx52_set_shift(libx52_device *x52, uint8_t state);
/**
* @brief Set the blinking state
@ -615,7 +626,7 @@ int libx52_set_shift(libx52_device *x52, uint8_t state);
* - 0 on success
* - \ref LIBX52_ERROR_INVALID_PARAM if \p x52 is not valid
*/
int libx52_set_blink(libx52_device *x52, uint8_t state);
LIBX52_API int libx52_set_blink(libx52_device *x52, uint8_t state);
/** @} */
@ -638,7 +649,7 @@ int libx52_set_blink(libx52_device *x52, uint8_t state);
*
* @returns \ref libx52_error_code indicating status
*/
int libx52_update(libx52_device *x52);
LIBX52_API int libx52_update(libx52_device *x52);
/**
* @brief Write a raw vendor control packet
@ -655,7 +666,7 @@ int libx52_update(libx52_device *x52);
*
* @returns \ref libx52_error_code indicating status
*/
int libx52_vendor_command(libx52_device *x52, uint16_t index, uint16_t value);
LIBX52_API int libx52_vendor_command(libx52_device *x52, uint16_t index, uint16_t value);
/**
* @brief Check if the device supports the given feature.
@ -670,7 +681,7 @@ int libx52_vendor_command(libx52_device *x52, uint16_t index, uint16_t value);
*
* @returns \ref libx52_error_code indicating status
*/
int libx52_check_feature(libx52_device *x52, libx52_feature feature);
LIBX52_API int libx52_check_feature(libx52_device *x52, libx52_feature feature);
/** @} */
@ -690,7 +701,7 @@ int libx52_check_feature(libx52_device *x52, libx52_feature feature);
* @returns Pointer to a NULL terminated string describing the error.
* Returned pointer must not be freed.
*/
const char * libx52_strerror(libx52_error_code error);
LIBX52_API const char *libx52_strerror(libx52_error_code error);
/**
* @brief Returns a string representation of the clock ID
@ -700,7 +711,7 @@ const char * libx52_strerror(libx52_error_code error);
* @returns Pointer to a NULL terminated string describing the clock ID.
* Returned pointer must not be freed.
*/
const char * libx52_clock_id_to_str(libx52_clock_id id);
LIBX52_API const char *libx52_clock_id_to_str(libx52_clock_id id);
/**
* @brief Returns a string representation of the clock format
@ -710,7 +721,7 @@ const char * libx52_clock_id_to_str(libx52_clock_id id);
* @returns Pointer to a NULL terminated string describing the clock format.
* Returned pointer must not be freed.
*/
const char * libx52_clock_format_to_str(libx52_clock_format format);
LIBX52_API const char *libx52_clock_format_to_str(libx52_clock_format format);
/**
* @brief Returns a string representation of the date format
@ -720,7 +731,7 @@ const char * libx52_clock_format_to_str(libx52_clock_format format);
* @returns Pointer to a NULL terminated string describing the date format.
* Returned pointer must not be freed.
*/
const char * libx52_date_format_to_str(libx52_date_format format);
LIBX52_API const char *libx52_date_format_to_str(libx52_date_format format);
/**
* @brief Returns a string representation of the LED
@ -730,7 +741,7 @@ const char * libx52_date_format_to_str(libx52_date_format format);
* @returns Pointer to a NULL terminated string describing the LED.
* Returned pointer must not be freed.
*/
const char * libx52_led_id_to_str(libx52_led_id id);
LIBX52_API const char *libx52_led_id_to_str(libx52_led_id id);
/**
* @brief Returns a string representation of the LED state
@ -740,7 +751,7 @@ const char * libx52_led_id_to_str(libx52_led_id id);
* @returns Pointer to a NULL terminated string describing the LED state.
* Returned pointer must not be freed.
*/
const char * libx52_led_state_to_str(libx52_led_state state);
LIBX52_API const char *libx52_led_state_to_str(libx52_led_state state);
/** @} */

View File

@ -1,3 +1,5 @@
libx52_version = '2.4.2'
libx52_files = files(
'x52_control.c',
'x52_core.c',
@ -9,12 +11,17 @@ libx52_files = files(
lib_libx52 = library('x52', libx52_files,
install: true,
version: '2.4.2',
version: libx52_version,
c_args: sym_hidden_cargs,
dependencies: [dep_libusb, dep_intl],
include_directories: [includes])
install_headers('libx52.h', subdir: meson.project_name())
pkgconfig.generate(lib_libx52)
pkgconfig.generate(lib_libx52,
name: 'libx52',
description: 'Linux/Unix library to control Saitek X52/X52Pro joystick extended functionality.',
subdirs: meson.project_name(),
version: libx52_version)
# Unit tests for libx52
libx52_string_test = executable('libx52-string-test',

View File

@ -17,7 +17,7 @@
#define TEST_STRINGIFY(name) do { \
char expected[256]; \
for (int i=-1; i < sizeof(name ## _map) / sizeof(name ## _map[0]); i++) { \
for (int i=-1; i < (int)(sizeof(name ## _map) / sizeof(name ## _map[0])); i++) { \
if (i < 0) { \
snprintf(expected, sizeof(expected), unknown_fmt, i); \
} else if (name ## _map[i] != NULL) { \
@ -31,6 +31,8 @@
static void test_led_id_names(void **state)
{
(void)state; // Suppress unused parameter warning
static const char * led_id_map[21] = {
[LIBX52_LED_FIRE] = "Fire",
[LIBX52_LED_A] = "A",
@ -52,6 +54,8 @@ static void test_led_id_names(void **state)
static void test_led_state_names(void **state)
{
(void)state; // Suppress unused parameter warning
static const char * led_state_map[6] = {
[LIBX52_LED_STATE_OFF] = "off",
[LIBX52_LED_STATE_ON] = "on",
@ -66,6 +70,8 @@ static void test_led_state_names(void **state)
}
static void test_clock_id_names(void **state) {
(void)state; // Suppress unused parameter warning
static const char * clock_id_map[4] = {
[LIBX52_CLOCK_1] = "primary",
[LIBX52_CLOCK_2] = "secondary",
@ -78,6 +84,8 @@ static void test_clock_id_names(void **state) {
}
static void test_clock_format_names(void **state) {
(void)state; // Suppress unused parameter warning
static const char * clock_format_map[3] = {
[LIBX52_CLOCK_FORMAT_12HR] = "12 hour",
[LIBX52_CLOCK_FORMAT_24HR] = "24 hour",
@ -89,6 +97,8 @@ static void test_clock_format_names(void **state) {
}
static void test_date_format_names(void **state) {
(void)state; // Suppress unused parameter warning
static const char * date_format_map[4] = {
[LIBX52_DATE_FORMAT_DDMMYY] = "DD-MM-YY",
[LIBX52_DATE_FORMAT_MMDDYY] = "MM-DD-YY",
@ -103,6 +113,8 @@ static void test_date_format_names(void **state) {
#define libx52_error_to_str libx52_strerror
static void test_strerror(void **state) {
(void)state; // Suppress unused parameter warning
static const char *error_map[18] = {
[LIBX52_SUCCESS] = "Success",
[LIBX52_ERROR_INIT_FAILURE] = "Initialization failure",

View File

@ -103,6 +103,8 @@ int libx52_vendor_command(libx52_device *x52, uint16_t index, uint16_t value)
static int _x52_write_shift(libx52_device *x52, uint32_t bit)
{
uint16_t value;
(void)bit;
value = tst_bit(&x52->led_mask, X52_BIT_SHIFT) ? X52_SHIFT_ON : X52_SHIFT_OFF;
return libx52_vendor_command(x52, X52_SHIFT_INDICATOR, value);
}
@ -152,6 +154,7 @@ static int _x52_write_line(libx52_device *x52, uint32_t bit)
static int _x52_write_pov_blink(libx52_device *x52, uint32_t bit)
{
uint16_t value;
(void)bit;
value = tst_bit(&x52->led_mask, X52_BIT_POV_BLINK) ? X52_BLINK_ON : X52_BLINK_OFF;
return libx52_vendor_command(x52, X52_BLINK_INDICATOR, value);
}
@ -177,6 +180,7 @@ static int _x52_write_date(libx52_device *x52, uint32_t bit)
uint16_t value1; //dd-mm
uint16_t value2; //yy
int rc;
(void)bit;
switch (x52->date_format) {
case LIBX52_DATE_FORMAT_YYMMDD:
@ -288,7 +292,7 @@ static int _x52_write_time(libx52_device *x52, uint32_t bit)
typedef int (*x52_handler)(libx52_device *, uint32_t);
const x52_handler _x52_handlers[32] = {
static const x52_handler _x52_handlers[32] = {
[X52_BIT_SHIFT] = _x52_write_shift,
[X52_BIT_LED_FIRE] = _x52_write_led,
[X52_BIT_LED_A_RED] = _x52_write_led,

View File

@ -48,6 +48,8 @@ static int _x52_hotplug_callback(libusb_context *ctx,
{
libx52_device *dev = user_data;
(void)device; // Suppress unused parameter warning
if (dev == NULL) {
return 0;
}
@ -229,6 +231,8 @@ int libx52_init(libx52_device **dev)
void libx52_exit(libx52_device *dev)
{
volatile unsigned char *vp;
if (!dev) {
return;
}
@ -237,7 +241,10 @@ void libx52_exit(libx52_device *dev)
libusb_exit(dev->ctx);
/* Clear the memory to prevent reuse */
memset(dev, 0, sizeof(*dev));
vp = (volatile unsigned char *)dev;
for (int i = 0; i < sizeof(*dev); i++) {
vp[i] = (unsigned char)0;
}
free(dev);
}

View File

@ -39,7 +39,7 @@ static const char *error_string[] = {
N_("System call interrupted"),
};
const char * libx52_strerror(libx52_error_code error)
LIBX52_API const char * libx52_strerror(libx52_error_code error)
{
switch (error) {
case LIBX52_SUCCESS:

View File

@ -16,7 +16,7 @@
#define _(str) dgettext(PACKAGE, str)
#define STRINGIFY(name, max_id, errstr, ...) \
const char * libx52_ ## name ## _to_str (libx52_ ## name param) { \
LIBX52_API const char * libx52_ ## name ## _to_str (libx52_ ## name param) { \
static char invalid[256]; \
static const char *desc[] = { __VA_ARGS__ }; \
if (param >= 0 && param <= max_id) { \
@ -51,7 +51,7 @@ STRINGIFY(led_state, LIBX52_LED_STATE_GREEN, N_("Unknown LED state %d"),
N_("green"),
)
const char * libx52_led_id_to_str(libx52_led_id id)
LIBX52_API const char * libx52_led_id_to_str(libx52_led_id id)
{
static char invalid[256];

View File

@ -73,6 +73,7 @@ int __wrap_libusb_control_transfer(libusb_device_handle *dev_handle,
uint16_t wLength,
unsigned int timeout)
{
(void)dev_handle;
function_called();
check_expected(wIndex);
check_expected(wValue);

View File

@ -19,7 +19,7 @@ libx52io_la_SOURCES = \
libx52io/io_parser.c \
libx52io/io_strings.c \
libx52io/io_device.c
libx52io_la_CFLAGS = @HIDAPI_CFLAGS@ -DLOCALEDIR=\"$(localedir)\" -I $(top_srcdir) $(WARN_CFLAGS)
libx52io_la_CFLAGS = @HIDAPI_CFLAGS@ -DLOCALEDIR=\"$(localedir)\" -I $(top_srcdir) $(VISIBILITY_CFLAGS) $(WARN_CFLAGS)
libx52io_la_LDFLAGS = \
-export-symbols-regex '^libx52io_' \
-version-info $(libx52io_v_CUR):$(libx52io_v_REV):$(libx52io_v_AGE) @HIDAPI_LIBS@ \

View File

@ -23,6 +23,16 @@
#include <stdint.h>
#include <stdbool.h>
#ifndef LIBX52IO_API
# if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 303
# define LIBX52IO_API __attribute__((visibility("default")))
# elif defined(_WIN32)
# define LIBX52IO_API __declspec(dllexport)
# else
# define LIBX52IO_API
# endif
#endif
#ifdef __cplusplus
extern "C" {
#endif
@ -293,7 +303,7 @@ typedef struct libx52io_report libx52io_report;
*
* @returns \c libx52io_error_code indicating status
*/
int libx52io_init(libx52io_context **ctx);
LIBX52IO_API int libx52io_init(libx52io_context **ctx);
/**
* @brief Exit the library and free up any resources used
@ -304,7 +314,7 @@ int libx52io_init(libx52io_context **ctx);
*
* @param[in] ctx Pointer to the device context
*/
void libx52io_exit(libx52io_context *ctx);
LIBX52IO_API void libx52io_exit(libx52io_context *ctx);
/**
* @brief Open a connection to a supported joystick
@ -321,7 +331,7 @@ void libx52io_exit(libx52io_context *ctx);
* - \ref LIBX52IO_ERROR_NO_DEVICE if no supported joystick is found
* - \ref LIBX52IO_ERROR_CONN if the connection fails
*/
int libx52io_open(libx52io_context *ctx);
LIBX52IO_API int libx52io_open(libx52io_context *ctx);
/**
* @brief Close an existing connection to a supported joystick
@ -335,7 +345,7 @@ int libx52io_open(libx52io_context *ctx);
* - \ref LIBX52IO_SUCCESS on closing, or if the connection is already closed.
* - \ref LIBX52IO_ERROR_INVALID if the context pointer is not valid
*/
int libx52io_close(libx52io_context *ctx);
LIBX52IO_API int libx52io_close(libx52io_context *ctx);
/**
* @brief Read and parse a HID report
@ -356,7 +366,7 @@ int libx52io_close(libx52io_context *ctx);
* including if the device was disconnected during the read.
* - \ref LIBX52IO_ERROR_TIMEOUT if no report was read before timeout.
*/
int libx52io_read_timeout(libx52io_context *ctx, libx52io_report *report, int timeout);
LIBX52IO_API int libx52io_read_timeout(libx52io_context *ctx, libx52io_report *report, int timeout);
/**
* @brief Read and parse a HID report
@ -374,7 +384,7 @@ int libx52io_read_timeout(libx52io_context *ctx, libx52io_report *report, int ti
* - \ref LIBX52IO_ERROR_IO if there was an error reading from the device,
* including if the device was disconnected during the read.
*/
int libx52io_read(libx52io_context *ctx, libx52io_report *report);
LIBX52IO_API int libx52io_read(libx52io_context *ctx, libx52io_report *report);
/**
* @brief Retrieve the range of an axis
@ -393,7 +403,7 @@ int libx52io_read(libx52io_context *ctx, libx52io_report *report);
* valid, or the requested axis is not a valid axis identifier
* - \ref LIBX52IO_ERROR_NO_DEVICE if the device is disconnected
*/
int libx52io_get_axis_range(libx52io_context *ctx, libx52io_axis axis, int32_t *min, int32_t *max);
LIBX52IO_API int libx52io_get_axis_range(libx52io_context *ctx, libx52io_axis axis, int32_t *min, int32_t *max);
/**
* @brief Get the string representation of an error code
@ -402,7 +412,7 @@ int libx52io_get_axis_range(libx52io_context *ctx, libx52io_axis axis, int32_t *
*
* @returns String representation of the error. This pointer must not be freed.
*/
const char * libx52io_strerror(libx52io_error_code code);
LIBX52IO_API const char *libx52io_strerror(libx52io_error_code code);
/**
* @brief Get the string representation of an axis.
@ -412,7 +422,7 @@ const char * libx52io_strerror(libx52io_error_code code);
* @returns String representation of the axis. This pointer must not be freed.
* If axis is outside the defined range, then this returns NULL.
*/
const char * libx52io_axis_to_str(libx52io_axis axis);
LIBX52IO_API const char *libx52io_axis_to_str(libx52io_axis axis);
/**
* @brief Get the string representation of a button.
@ -422,7 +432,7 @@ const char * libx52io_axis_to_str(libx52io_axis axis);
* @returns String representation of the button. This pointer must not be freed.
* If button is outside the defined range, then this returns NULL.
*/
const char * libx52io_button_to_str(libx52io_button button);
LIBX52IO_API const char *libx52io_button_to_str(libx52io_button button);
/**
* @brief Get the vendor ID of the connected X52 device.
@ -431,7 +441,7 @@ const char * libx52io_button_to_str(libx52io_button button);
*
* @returns Vendor ID of the connected device. Returns 0 if no device is connected.
*/
uint16_t libx52io_get_vendor_id(libx52io_context *ctx);
LIBX52IO_API uint16_t libx52io_get_vendor_id(libx52io_context *ctx);
/**
* @brief Get the product ID of the connected X52 device.
@ -440,7 +450,7 @@ uint16_t libx52io_get_vendor_id(libx52io_context *ctx);
*
* @returns Product ID of the connected device. Returns 0 if no device is connected.
*/
uint16_t libx52io_get_product_id(libx52io_context *ctx);
LIBX52IO_API uint16_t libx52io_get_product_id(libx52io_context *ctx);
/**
* @brief Get the device version of the connected X52 device.
@ -449,7 +459,7 @@ uint16_t libx52io_get_product_id(libx52io_context *ctx);
*
* @returns Device version of the connected device. Returns 0 if no device is connected.
*/
uint16_t libx52io_get_device_version(libx52io_context *ctx);
LIBX52IO_API uint16_t libx52io_get_device_version(libx52io_context *ctx);
/**
* @brief Get the manufacturer string of the connected X52 device.
@ -462,7 +472,7 @@ uint16_t libx52io_get_device_version(libx52io_context *ctx);
* @returns Pointer to the manufacturer string, which may be NULL. Return value
* is always NULL if no device is connected.
*/
const char * libx52io_get_manufacturer_string(libx52io_context *ctx);
LIBX52IO_API const char *libx52io_get_manufacturer_string(libx52io_context *ctx);
/**
* @brief Get the product string of the connected X52 device.
@ -475,7 +485,7 @@ const char * libx52io_get_manufacturer_string(libx52io_context *ctx);
* @returns Pointer to the product string, which may be NULL. Return value
* is always NULL if no device is connected.
*/
const char * libx52io_get_product_string(libx52io_context *ctx);
LIBX52IO_API const char *libx52io_get_product_string(libx52io_context *ctx);
/**
* @brief Get the serial number of the connected X52 device.
@ -488,7 +498,7 @@ const char * libx52io_get_product_string(libx52io_context *ctx);
* @returns Pointer to the serial number string, which may be NULL. Return value
* is always NULL if no device is connected.
*/
const char * libx52io_get_serial_number_string(libx52io_context *ctx);
LIBX52IO_API const char *libx52io_get_serial_number_string(libx52io_context *ctx);
/** @} */

View File

@ -1,3 +1,5 @@
libx52io_version = '1.0.0'
libx52io_files = files(
'io_core.c',
'io_axis.c',
@ -8,12 +10,18 @@ libx52io_files = files(
lib_libx52io = library('x52io', libx52io_files,
install: true,
version: '1.0.0',
version: libx52io_version,
c_args: sym_hidden_cargs,
dependencies: [dep_hidapi, dep_intl],
include_directories: [includes])
install_headers('libx52io.h', subdir: meson.project_name())
pkgconfig.generate(lib_libx52io)
pkgconfig.generate(lib_libx52io,
name: 'libx52io',
description: 'Linux/Unix library to read and parse X52 input',
subdirs: meson.project_name(),
version: libx52io_version,
)
test_axis = executable('test-axis', 'test_axis.c', libx52io_files,
build_by_default: false,

View File

@ -38,9 +38,9 @@ static int group_setup(void **state)
return 0; \
}
TEST_SETUP_FUNCTION(_1);
TEST_SETUP_FUNCTION(_2);
TEST_SETUP_FUNCTION(PRO);
TEST_SETUP_FUNCTION(_1)
TEST_SETUP_FUNCTION(_2)
TEST_SETUP_FUNCTION(PRO)
#undef TEST_SETUP_FUNCTION

View File

@ -10,8 +10,11 @@ lib_LTLIBRARIES += libx52util.la
# This library provides extra utilities for ease of use
nodist_libx52util_la_SOURCES = libx52util/util_char_map.c
libx52util_la_SOURCES = libx52util/x52_char_map_lookup.c
libx52util_la_CFLAGS = -I $(top_srcdir)/libx52util $(WARN_CFLAGS)
libx52util_la_LDFLAGS = -version-info 1:1:0 $(WARN_LDFLAGS)
libx52util_la_CFLAGS = -I $(top_srcdir)/libx52util $(VISIBILITY_CFLAGS) $(WARN_CFLAGS)
libx52util_la_LDFLAGS = \
-export-symbols-regex '^libx52util_' \
-version-info 1:1:0 \
$(WARN_LDFLAGS)
# Header files that need to be copied
x52include_HEADERS += libx52util/libx52util.h
@ -20,33 +23,25 @@ x52include_HEADERS += libx52util/libx52util.h
pkgconfig_DATA += libx52util/libx52util.pc
# Autogenerated file that needs to be cleaned up
CLEANFILES += libx52util/util_char_map.c
char_map_TARGETS = \
libx52util/util_char_map.c \
libx52util/x52_char_map.bin
util_char_map_c_DEPENDS = \
CLEANFILES += $(char_map_TARGETS)
char_map_DEPENDS = \
$(srcdir)/libx52util/x52_char_map_gen.py \
$(srcdir)/libx52util/x52_char_map.cfg
libx52util/util_char_map.c: $(util_char_map_c_DEPENDS)
$(AM_V_GEN) $(PYTHON) $(util_char_map_c_DEPENDS) $@
$(char_map_TARGETS): $(char_map_DEPENDS)
$(AM_V_GEN) $(PYTHON) $(char_map_DEPENDS) $(char_map_TARGETS)
if HAVE_CMOCKA
TESTS += libx52util-map-test
TESTS += libx52util/test-runner.sh
check_PROGRAMS += libx52util-map-test
CLEANFILES += libx52util/x52_map_test.c
x52_map_test_c_DEPENDS = \
$(srcdir)/libx52util/x52_map_test_gen.py \
$(srcdir)/libx52util/x52_char_map.cfg
libx52util/x52_map_test.c: $(x52_map_test_c_DEPENDS)
$(AM_V_GEN) $(PYTHON) $(x52_map_test_c_DEPENDS) $@
libx52util_map_test_SOURCES = libx52util/x52_map_test.c
libx52util_map_test_CFLAGS = @CMOCKA_CFLAGS@ -I $(top_srcdir) -I $(top_srcdir)/libx52util
libx52util_map_test_LDFLAGS = @CMOCKA_LIBS@
libx52util_map_test_LDADD = libx52util.la
endif
check_PROGRAMS += libx52util-bmp-test
libx52util_bmp_test_SOURCES = libx52util/x52_char_map_test.c
libx52util_bmp_test_CFLAGS = -I $(top_srcdir)/libx52util
libx52util_bmp_test_LDADD = libx52util.la
# Extra files that need to be in the distribution
EXTRA_DIST += libx52util/x52_char_map.cfg \

View File

@ -20,6 +20,17 @@
#define LIBX52UTIL_H
#include <stddef.h>
#include <stdint.h>
#ifndef LIBX52UTIL_API
# if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 303
# define LIBX52UTIL_API __attribute__((visibility("default")))
# elif defined(_WIN32)
# define LIBX52UTIL_API __declspec(dllexport)
# else
# define LIBX52UTIL_API
# endif
#endif
#ifdef __cplusplus
extern "C" {
@ -46,8 +57,8 @@ extern "C" {
* @returns 0 on success, -EINVAL on invalid parameters, -E2BIG if the buffer
* filled up before converting the entire string.
*/
int libx52util_convert_utf8_string(const uint8_t *input,
uint8_t *output, size_t *len);
LIBX52UTIL_API int libx52util_convert_utf8_string(const uint8_t *input,
uint8_t *output, size_t *len);
/** @} */

View File

@ -1,37 +1,41 @@
# libx52util
libx52util_version = '1.0.2'
gen_script = files('x52_char_map_gen.py')[0]
util_char_map = custom_target('util-char-map',
build_by_default: false,
depend_files: ['x52_char_map_gen.py', 'x52_char_map.cfg'],
command: [python, gen_script, '@INPUT@', '@OUTPUT@'],
command: [python, gen_script, '@INPUT@', '@OUTPUT0@', '@OUTPUT1@'],
input: 'x52_char_map.cfg',
output: 'util_char_map.c')
output: ['util_char_map.c', 'x52_char_map.bin'])
lib_libx52util = library('x52util', util_char_map, 'x52_char_map_lookup.c',
install: true,
version: '1.0.1',
version: libx52util_version,
c_args: sym_hidden_cargs,
include_directories: [includes],
)
install_headers('libx52util.h', subdir: meson.project_name())
pkgconfig.generate(lib_libx52util)
pkgconfig.generate(lib_libx52util,
name: 'libx52util',
description: 'Extra utility functions to manage X52 extended functionality',
subdirs: meson.project_name(),
version: libx52util_version,
)
test_gen_script = files('x52_map_test_gen.py')[0]
libx52util_map_test_src = custom_target('libx52util-map-test-src',
build_by_default: false,
depend_files: ['x52_map_test_gen.py', 'x52_char_map.cfg'],
command: [python, test_gen_script, '@INPUT@', '@OUTPUT@'],
input: 'x52_char_map.cfg',
output: 'x52_map_test.c'
)
libx52util_map_test = executable('libx52util-map-test', libx52util_map_test_src,
dependencies: [dep_cmocka],
link_with: [lib_libx52util],
libx52util_bmp_test = executable(
'libx52util-bmp-test',
'x52_char_map_test.c',
build_by_default: false,
include_directories: [includes, lib_libx52util.private_dir_include()],
link_with: [lib_libx52util]
)
test('libx52util-map-test', libx52util_map_test, protocol: 'tap')
test('libx52util-bmp-test', libx52util_bmp_test,
protocol: 'tap',
args: [util_char_map[1]])
benchmark('libx52util-bmp-bench', libx52util_bmp_test,
protocol: 'tap',
args: [util_char_map[1]])

View File

@ -0,0 +1,12 @@
#!/bin/sh
TEST_RUNNER="./libx52util-bmp-test"
TEST_BIN="./libx52util/x52_char_map.bin"
if [ -e "${TEST_RUNNER}" ] && [ -e "${TEST_BIN}" ];
then
"${TEST_RUNNER}" "${TEST_BIN}"
else
echo "TAP version 13"
echo "1..0 # missing files"
fi

View File

@ -324,3 +324,13 @@
0xFF9E 0xDE # HALFWIDTH KATAKANA VOICED SOUND MARK
0xFF9F 0xDF # HALFWIDTH KATAKANA SEMI-VOICED SOUND MARK
# The following characters are manually added to aid in normalization to the
# X52 character map
0x2215 0x2F # DIVISION SLASH
0x2044 0x2F # FRACTION SLASH
0x00B0 0xDF # DEGREE SIGN
# Note: while Greek letters aren't actually supported by the MFD character map,
# this is manually addded to map the letter 'mu' to ASCII 'u'. This is needed
# in the CJK compatibility page (0x3300-0x33FF) to deal with the square latin
# abbreviations
0x03BC 0x75 # GREEK SMALL LETTER MU

View File

@ -12,20 +12,7 @@
#include <stddef.h>
#include <stdint.h>
enum {
TYPE_INVALID = 0, /* Invalid type (default) */
TYPE_POINTER, /* Pointer target */
TYPE_ENTRY /* Map entry value */
};
struct map_entry {
struct map_entry *next; /* Pointer to the next table */
uint8_t type; /* Type of entry */
uint8_t value; /* Value is valid if this is of TYPE_ENTRY */
};
extern struct map_entry map_root[];
extern const uint16_t *root_table[256];
extern const uint8_t *sequence_table[];
#endif /* !defined X52_CHAR_MAP_H */

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# Character map generator
#
# Copyright (C) 2012-2018 Nirenjan Krishnan (nirenjan@nirenjan.org)
# Copyright (C) 2012-2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
#
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
"""
@ -11,191 +11,267 @@ for the X52/X52 Pro MFD
import sys
import re
AUTOGEN_HEADER = """
/*
* Autogenerated character map file for Saitek X52 Pro
* Generated from %s
*/
#include "x52_char_map.h"
"""
class MapTable(object):
"""
Defines a MapTable entry, with each entry storing the value seen so far,
the type of the entry, and the value, if it's a value node.
"""
# Empty list
root = [None] * 256
def __init__(self, value_so_far, map_value=None):
self.next_level = [None] * 256
self.value_so_far = value_so_far
self.map_value = map_value
def output_nodes(self):
"""
Output the individual nodes
"""
output_lines = []
output_count = 0
for node in self.next_level:
if node is not None:
output_lines.extend(node.output_nodes())
output_count += 1
if output_count != 0:
struct_header = 'static struct map_entry table_%x[64] = {' % \
self.value_so_far
output_lines.append(struct_header)
for node_index in range(0, 256):
node = self.next_level[node_index]
if node is not None:
output_lines.append(self.dump_entry_line(0x80, node_index,
node.value_so_far,
node.map_value))
output_lines.extend(['};', ''])
return output_lines
@staticmethod
def dump_entry_line(offset, node_index, value_so_far, map_value):
"""
Dump the array entry for the current node
"""
if map_value is None:
node_entry_line = '\t[0x%02x] = { table_%x, TYPE_POINTER, 0 },' % \
(node_index - offset, value_so_far)
else:
node_entry_line = '\t[0x%02x] = { NULL, TYPE_ENTRY, 0x%02x },' % \
(node_index - offset, map_value)
return node_entry_line
@classmethod
def add_to_table(cls, input_val, map_val):
"""
Add a map value to the lookup table
"""
try:
uchr = unichr(input_val)
except NameError:
# Python 3 doesn't have unichr, but chr should work
uchr = chr(input_val)
utf8_str = uchr.encode('utf-8')
# Python2 returns the encoded result as a string, wheras
# Python3 returns the result as a bytearray. Converting
# the string (or bytearray) into a bytearray ensures that
# this can be run in both Python2 and Python3
utf8_vals = [c for c in bytearray(utf8_str)]
value_so_far = 0
level = cls.root
for index, char in enumerate(utf8_vals):
value_so_far = (value_so_far << 8) | char
if index < (len(utf8_vals) - 1):
node = level[char]
if node is None:
node = cls(value_so_far)
level[char] = node
level = level[char].next_level
else:
node = cls(value_so_far, map_val)
level[char] = node
@classmethod
def output_table_as_list(cls):
"""
Output the map table as a list of lines
"""
output_lines = []
for node in cls.root:
if node is not None:
output_lines.extend(node.output_nodes())
output_lines.append('struct map_entry map_root[256] = {')
for node_index in range(0, 256):
node = cls.root[node_index]
if node is not None:
output_lines.append(cls.dump_entry_line(0x0, node_index,
node.value_so_far,
node.map_value))
output_lines.extend(['};', ''])
return output_lines
import json
import unicodedata
class LineFormatError(ValueError):
"""
Error class for parser
"""
def parse_line(data):
class BMPTable:
"""
Parse a line containing a mapping descriptor. The mapping descriptor
must start with a hexadecimal unicode code point, followed by either a
single character, or a hexadecimal integer that corresponds to the map
value.
Sparse table for Basic Multilingual Plane
"""
# Strip off comments
data = re.sub(re.compile('#.*$'), '', data)
# Strip off leading and trailing whitespace
data = data.strip()
REPLACEMENT_CHAR = 0xDB
# If the line is empty, it is a comment line
if len(data) == 0:
return None, None
HEADER = f"""/*
* Autogenerated tables for X52 MFD character lookup
*
* DO NOT EDIT
*/
# Find the code point and the target value
try:
code_point, target = data.strip().split()
except ValueError:
# Raised when there are either too many, or not enough values in
# the string
raise LineFormatError('Invalid descriptor format "%s"' % data)
#include <stdint.h>
# Convert the string to its equivalent numeric value
try:
code_point = int(code_point, 0)
except ValueError:
raise LineFormatError('Invalid code point "%s"' % code_point)
"""
# Check if the target is a single character
if len(target) == 1:
target = ord(target)
else:
# Try parsing the target as an integer
TABLE_NAME_FORMAT = 'bmp_page_%02x'
TABLE_NAME_DEFAULT = 'bmp_page_default'
TABLE_FORMAT = 'const uint16_t %s[256] = {'
TABLE_FOOTER = '};\n'
def __init__(self, input_file, output_file, output_map):
self.input_file = input_file
self.output_file = output_file
self.output_map = output_map
self.mapping = {}
self.pages = {}
self.sequences = {}
self.root_table = []
self.read_map()
self.build_extended_map()
self.build_tables()
self.generate_test_tables()
@staticmethod
def parse_line(data):
"""
Parse a line containing a mapping descriptor. The mapping descriptor
must start with a hexadecimal unicode code point, followed by either a
single character, or a hexadecimal integer that corresponds to the map
value.
"""
# Strip off comments
data = re.sub(re.compile('#.*$'), '', data)
# Strip off leading and trailing whitespace
data = data.strip()
# If the line is empty, it is a comment line
if len(data) == 0:
return None, None
# Find the code point and the target value
try:
target = int(target, 0)
except ValueError:
raise LineFormatError('Invalid map value "%s"' % target)
code_point, target = data.strip().split()
except ValueError as exc:
# Raised when there are either too many, or not enough values in
# the string
raise LineFormatError(f'Invalid descriptor format "{data}"') from exc
return code_point, target
# Convert the string to its equivalent numeric value
try:
code_point = int(code_point, 0)
except ValueError as exc:
raise LineFormatError(f'Invalid code point "{code_point}"') from exc
# Check if the target is a single character
if len(target) == 1:
target = ord(target)
else:
# Try parsing the target as an integer
try:
target = int(target, 0)
except ValueError as exc:
raise LineFormatError(f'Invalid map value "{target}"') from exc
return code_point, target
def read_map(self):
"""Read the mapping tables from the config file"""
def map_normalized(char, dst):
# Try to normalize the unicode character as NFKC
normalized = unicodedata.normalize('NFKC', chr(char))
if normalized == char:
# This is already in normalized form
return
if len(normalized) == 1:
normalized_char = ord(normalized)
if normalized_char not in self.mapping:
# This is only needed to ensure that we get the normalized
# forms for example, half-width Katakana characters are
# normalized to their corresponding full width versions.
# However, we don't want to overwrite existing mappings,
# since something like Lowercase A with grave could be
# normalized to lowercase A, which would break the translation
self.mapping[normalized_char] = dst
with open(self.input_file, 'r', encoding='utf-8') as infile:
for line in infile:
src, dst = self.parse_line(line)
if src is None:
continue
self.mapping[src] = dst
map_normalized(src, dst)
def build_extended_map(self):
"""Build the extended map for every character in the BMP"""
self.mapping[0] = 0 # Handle NUL
for i in range(0x10000):
# Iterate over the basic multilingual plane
if i in self.mapping:
continue
if 0xD800 <= i <= 0xDFFF:
# UTF16 surrogate pairs - we want to mark it as a box character
self.mapping[i] = self.REPLACEMENT_CHAR
continue
normalized = unicodedata.normalize('NFKC', chr(i))
if len(normalized) == 1:
normalized_ord = ord(normalized)
if normalized_ord in self.mapping:
self.mapping[i] = self.mapping[normalized_ord]
else:
# No single character mapping exists
self.mapping[i] = self.REPLACEMENT_CHAR
continue
# Check that all characters in the normalized are in the mapping table:
sequence = []
for c in normalized:
if ord(c) in self.mapping:
sequence.append(self.mapping[ord(c)])
else:
sequence.append(self.REPLACEMENT_CHAR)
# Check if it only contains the box character, or box char and space,
# and reduce runs to a single instance
if all(c in (self.REPLACEMENT_CHAR, self.mapping[0x20])
for c in sequence):
self.mapping[i] = self.REPLACEMENT_CHAR
continue
sequence = tuple(sequence)
if sequence not in self.sequences:
if not self.sequences:
last_sequence = 256
else:
last_sequence = max(self.sequences.values()) + 1
self.sequences[sequence] = last_sequence
self.mapping[i] = self.sequences[sequence]
def output_c_table(self, page_tuple, out_fd):
"""Output the C table structure"""
page_name = self.pages[page_tuple]
print(self.TABLE_FORMAT % (page_name), file=out_fd)
for i, val in enumerate(page_tuple):
print(f"0x{val:02x}, ", end='', file=out_fd)
if i % 8 == 7:
print(f"// 0x{i-7:02x}-0x{i:02x}", file=out_fd)
print(self.TABLE_FOOTER, file=out_fd)
def build_tables(self):
"""Build the C Tables"""
with open(self.output_file, 'w', encoding='utf-8') as out_fd:
print(self.HEADER, file=out_fd)
default_page = tuple([self.REPLACEMENT_CHAR] * 256)
self.pages[default_page] = self.TABLE_NAME_DEFAULT
self.output_c_table(default_page, out_fd)
for root_idx in range(256):
base_idx = root_idx * 256
page = [self.mapping[idx] for idx in range(base_idx, base_idx+256)]
page_tuple = tuple(page)
if page_tuple not in self.pages:
page_name = self.TABLE_NAME_FORMAT % (root_idx)
self.pages[page_tuple] = page_name
self.output_c_table(page_tuple, out_fd)
self.root_table.append(self.pages[page_tuple])
print(self.TABLE_FORMAT % ('* root_table'), file=out_fd)
for page_id, page_name in enumerate(self.root_table):
print(f" {page_name}, // 0x{page_id:02x}", file=out_fd)
print(self.TABLE_FOOTER, file=out_fd)
print(f"const uint8_t *sequence_table[{len(self.sequences)}] = {{", file=out_fd)
for sequence, seq_id in self.sequences.items():
seq_len = len(sequence)
if seq_len >= 256:
raise RuntimeError("Sequence way too long")
line = [f"0x{seq_len:02X}"]
for seq_elem in sequence:
line.append(f"0x{seq_elem:02X}")
line = ', '.join(line)
print(f' [{seq_id-256}] = (const uint8_t[]){{ {line} }},', file=out_fd)
print(self.TABLE_FOOTER, file=out_fd)
def generate_test_tables(self):
"""Build the test tables used by the test suite"""
# Generate the expected output sequences for every table
# Mapping is a dict mapping the code point as a string to the output
# Sequence is a dict of <seq_tuple>:<seq_id> mappings (seq_id starts from 256)
output = []
sequences = [item[0] for item in sorted(self.sequences.items(),
key=lambda item: item[1])]
# The mapping for the NUL byte (\x00) should be an empty sequence
output.append([])
for i in range(1, 0x10000):
seq = self.mapping[i]
if seq >= 256:
# Pull from sequence table
seq = sequences[seq - 256]
else:
seq = [seq]
output.append(seq)
# Find the longest length sequence (add 1 for the length byte)
longest = max(len(seq) for seq in output) + 1
# Find the next power of two that can hold this sequence
if (longest & (longest - 1)) == 0:
record_length = longest
else:
record_length = 1 << longest.bit_length()
with open(self.output_map, 'wb') as output_map:
pad = [0] * record_length
for seq in output:
record = [len(seq)] + list(seq) + pad
output_map.write(bytes(record[:record_length]))
if __name__ == "__main__":
if len(sys.argv) != 3:
sys.stderr.write('Usage: %s <input-map> <output-c-file>\n' %
sys.argv[0])
if len(sys.argv) != 4:
sys.stderr.write(f"Usage: {sys.argv[0]} <input-map> <output-c-file> <output-json-map>\n")
sys.exit(1)
with open(sys.argv[1], 'r') as infile:
for line in infile:
src, dst = parse_line(line)
if src is not None:
MapTable.add_to_table(src, dst)
with open(sys.argv[2], 'w') as outfile:
outfile.write(AUTOGEN_HEADER % sys.argv[1])
for line in MapTable.output_table_as_list():
outfile.write(line + '\n')
BMPTable(sys.argv[1], sys.argv[2], sys.argv[3])

View File

@ -11,11 +11,64 @@
#include "config.h"
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include "libx52util.h"
#include "x52_char_map.h"
/**
* @brief Converts a UTF8 stream to a uint32_t
*
* @param[in] utf8in Pointer to UTF8 input stream. Must be NUL-terminated
* @param[out] unichr Output character pointer
*
* @returns number of bytes to advance stream by - 0 if NUL or input pointer is NULL
*/
static int utf8_to_u32(const uint8_t *utf8in, uint32_t *unichr)
{
uint8_t b;
if (!utf8in || !*utf8in) return 0;
b = utf8in[0];
// 1-byte (0xxxxxxx)
if (b < 0x80) {
*unichr = b;
return 1;
}
// Invalid leading bytes
if (b < 0xC2 || b > 0xF4) goto error;
// 2-byte (110xxxxx 10xxxxxx)
if ((b & 0xE0) == 0xC0) {
if ((utf8in[1] & 0xC0) != 0x80) goto error;
*unichr = ((b & 0x1F) << 6) | (utf8in[1] & 0x3F);
return 2;
}
// 3-byte (1110xxxx 10xxxxxx 10xxxxxx)
if ((b & 0xF0) == 0xE0) {
if ((utf8in[1] & 0xC0) != 0x80 || (utf8in[2] & 0xC0) != 0x80) goto error;
*unichr = ((b & 0x0F) << 12) | ((utf8in[1] & 0x3F) << 6) | (utf8in[2] & 0x3F);
return 3;
}
// 4-byte (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)
if ((b & 0xF8) == 0xF0) {
if ((utf8in[1] & 0xC0) != 0x80 || (utf8in[2] & 0xC0) != 0x80 ||
(utf8in[3] & 0xC0) != 0x80) goto error;
*unichr = ((b & 0x07) << 18) | ((utf8in[1] & 0x3F) << 12) |
((utf8in[2] & 0x3F) << 6) | (utf8in[3] & 0x3F);
return 4;
}
error:
*unichr = 0xFFFD; // Unicode Replacement Character
return 1; // Consume lead byte to attempt resync
}
/**
* @brief Convert UTF8 string to X52 character map.
*
@ -32,52 +85,61 @@
int libx52util_convert_utf8_string(const uint8_t *input,
uint8_t *output, size_t *len)
{
struct map_entry *entry;
size_t index;
int retval = 0;
unsigned char local_index;
uint32_t unichr;
int bytes_consumed;
uint16_t translated;
if (!input || !output || !len || !*len) {
return -EINVAL;
}
index = 0;
entry = &map_root[*input];
// Reset the output array
memset(output, 0, *len);
while (*input) {
input++;
if (entry->type == TYPE_ENTRY) {
output[index] = entry->value;
// Length check
if (index >= *len) {
retval = -E2BIG;
break;
}
bytes_consumed = utf8_to_u32(input, &unichr);
if (bytes_consumed == 0) {
// We should never get here, since the while loop should have
// caught it
retval = 0;
break;
}
input += bytes_consumed;
// Check for bytes in the Supplementary planes
if (unichr >= 0x10000) {
unichr = 0xFFFD; // Unicode replacement character
}
translated = root_table[unichr >> 8][unichr & 0xFF];
if (translated < 256) {
// Table entry, push to output
output[index] = (uint8_t)translated;
index++;
if (index >= *len && *input) {
} else {
// We have a sequence, output that
const uint8_t *sequence = sequence_table[translated - 256];
uint8_t seq_len = sequence[0];
// Let's make sure that we can actually output to the buffer
if ((index + seq_len) >= *len) {
retval = -E2BIG;
break;
}
entry = &map_root[*input];
} else if (entry->type == TYPE_POINTER) {
local_index = *input;
if (local_index < 0x80 || local_index >= 0xC0) {
/* Invalid input, skip till we find the start of another
* valid UTF-8 character
*/
while (*input >= 0x80 && *input < 0xC0) {
input++; /* Skip invalid characters */
}
/* New UTF-8 character, reset the entry pointer */
entry = &map_root[*input];
} else {
/* Mask off the upper bits, we only care about the lower 6 bits */
local_index &= 0x3F;
entry = &(entry->next[local_index]);
for (int i = 1; i <= seq_len; i++) {
output[index] = sequence[i];
index++;
}
} else {
/* Invalid value, skip */
while (*input >= 0x80 && *input < 0xC0) {
input++; /* Skip invalid characters */
}
/* New UTF-8 character, reset the entry pointer */
entry = &map_root[*input];
}
}

View File

@ -0,0 +1,215 @@
/*
* X52 character map lookup test
*
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
*
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include "libx52util.h"
// Fix this if we ever hit longer sequences
#define RECORD_SIZE 8
// Blindly encode a string into it's smallest UTF8 representation
static void encode_utf8(uint32_t cp, uint8_t *out)
{
if (cp <= 0x7F) {
out[0] = (uint8_t)cp;
} else if (cp <= 0x7FF) {
out[0] = (uint8_t)(0xC0 | (cp >> 6));
out[1] = (uint8_t)(0x80 | (cp & 0x3F));
} else if (cp <= 0xFFFF) {
out[0] = (uint8_t)(0xE0 | (cp >> 12));
out[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[2] = (uint8_t)(0x80 | (cp & 0x3F));
} else if (cp <= 0x1FFFFF) {
out[0] = (uint8_t)(0xF0 | (cp >> 18));
out[1] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
out[2] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[3] = (uint8_t)(0x80 | (cp & 0x3F));
} else if (cp <= 0x3FFFFFF) {
out[0] = (uint8_t)(0xF8 | (cp >> 24));
out[1] = (uint8_t)(0x80 | ((cp >> 18) & 0x3F));
out[2] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
out[3] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[4] = (uint8_t)(0x80 | (cp & 0x3F));
} else if (cp <= 0x7FFFFFFF) {
out[0] = (uint8_t)(0xFC | (cp >> 30));
out[1] = (uint8_t)(0x80 | ((cp >> 24) & 0x3F));
out[2] = (uint8_t)(0x80 | ((cp >> 18) & 0x3F));
out[3] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
out[4] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[5] = (uint8_t)(0x80 | (cp & 0x3F));
} else { // 0x80000000 to 0xFFFFFFFF (7 bytes)
out[0] = (uint8_t)0xFE; // Binary 11111110
out[1] = (uint8_t)(0x80 | ((cp >> 30) & 0x3F));
out[2] = (uint8_t)(0x80 | ((cp >> 24) & 0x3F));
out[3] = (uint8_t)(0x80 | ((cp >> 18) & 0x3F));
out[4] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
out[5] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[6] = (uint8_t)(0x80 | (cp & 0x3F));
}
}
static double get_time_diff(struct timespec start, struct timespec end)
{
return (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9;
}
int main(int argc, char *argv[])
{
uint8_t input[8] = {0};
uint8_t output[RECORD_SIZE];
size_t len;
int result;
int fd;
uint8_t *expected_blob;
bool smp_pages_ok;
struct timespec start, end;
// Argument check
if (argc != 2) {
puts("Bail out! Invalid number of arguments");
puts("# Usage: libx52util-bmp-test <path-to-bin>");
return 1;
}
fd = open(argv[1], O_RDONLY);
if (fd < 0) {
printf("Bail out! Error %d opening bin file %s: %s\n",
errno, argv[1], strerror(errno));
return 1;
}
expected_blob = mmap(NULL, 0x10000 * RECORD_SIZE,
PROT_READ, MAP_SHARED, fd, 0);
if (expected_blob == MAP_FAILED) {
printf("Bail out! MMAP failed with error %d: %s\n",
errno, strerror(errno));
}
puts("TAP version 13");
// Check the 256 BMP Pages, plus the supplementary pages
puts("1..257");
clock_gettime(CLOCK_MONOTONIC, &start);
for (uint32_t page = 0; page < 256; page++) {
bool page_ok = true;
for (uint32_t offset = 0; offset < 256; offset++) {
uint32_t cp = page * 256 + offset;
const uint8_t *rec = &expected_blob[cp * RECORD_SIZE];
memset(input, 0, sizeof(input));
memset(output, 0, sizeof(output));
encode_utf8(cp, input);
len = sizeof(output);
result = libx52util_convert_utf8_string(input, output, &len);
if (result != 0) {
page_ok = false;
printf("# Bad result @ %04X: %d\n", cp, result);
break;
}
// result is OK, check against the expected blob
if (len != rec[0]) {
page_ok = false;
printf("# Length mismatch @ %04X: expected %u, got %zu\n",
cp, rec[0], len);
break;
}
// Length is OK, check the bytes
if (memcmp(output, &rec[1], rec[0]) != 0) {
page_ok = false;
printf("# Output mismatch @ %04X:\n", cp);
printf("# exp/got:");
for (size_t i = 0; i < len; i++) {
printf("%02X/%02X ", rec[i+1], output[i]);
}
puts("");
break;
}
}
printf("%sok - %d Page 0x%02x\n", page_ok ? "": "not ",
page + 1, page);
}
clock_gettime(CLOCK_MONOTONIC, &end);
{
double time_spent = get_time_diff(start, end);
printf("# -- Benchmark results --\n");
printf("# Total time for 64K lookups: %.4f seconds\n", time_spent);
printf("# Throughput: %.2f Mchars/sec\n", (65536.0 / time_spent) / 1e6);
printf("# -----------------------\n");
}
// Handle the supplementary pages
smp_pages_ok = true;
for (uint32_t smp = 0x1; smp <= 0x10; smp++) {
const uint8_t *rec = &expected_blob[0xFFFD * RECORD_SIZE];
for (uint32_t offset = 0; offset < 0x100; offset += 0xFF) {
uint32_t cp = smp * 0x10000 + offset;
memset(input, 0, sizeof(input));
memset(output, 0, sizeof(output));
len = sizeof(output);
encode_utf8(cp, input);
result = libx52util_convert_utf8_string(input, output, &len);
if (result != 0) {
smp_pages_ok = false;
printf("# Bad result @ %08X: %d\n", cp, result);
break;
}
// result is OK, check against the expected blob
if (len != rec[0]) {
smp_pages_ok = false;
printf("# Length mismatch @ %08X: expected %u, got %zu\n",
cp, rec[0], len);
break;
}
// Length is OK, check the bytes
if (memcmp(output, &rec[1], rec[0]) != 0) {
smp_pages_ok = false;
printf("# Output mismatch @ %08X:\n", cp);
printf("# exp/got:");
for (size_t i = 0; i < len; i++) {
printf("%02X/%02X ", rec[i+1], output[i]);
}
puts("");
break;
}
}
if (!smp_pages_ok) {
break;
}
}
printf("%sok - 257 SMP tests\n", smp_pages_ok ? "" : "not ");
// Cleanup
munmap(expected_blob, 0x10000 * RECORD_SIZE);
close(fd);
return 0;
}

View File

@ -1,107 +0,0 @@
#!/usr/bin/env python3
"""Generate a test script for the convert function"""
import argparse
import re
def parse_file(map_file):
"""Read the map file, strip out comments, and return a dictionary that
maps the UTF-8 encoded string to the X52 MFD character"""
# If we are running this, then we know that the input map is likely
# in a sane format already.
char_dict = {}
with open(map_file, 'r', encoding='utf-8') as map_fd:
for line in map_fd:
line = re.sub(r'#.*$', '', line).strip()
if not line:
# Comment line, skip
continue
key, out = line.split()
in_char = int(key, 0)
if len(out) == 1:
out_byte = ord(out)
else:
out_byte = int(out, 0)
char_dict[in_char] = out_byte
return char_dict
def generate_positive_test_cases(char_dict):
"""Generate a set of positive test cases"""
# For every string in the dictionary, generate a test case that tests
# the input against the output
TEST_CASE_FMT = """
static void test_map_{in_char}(void **state) {{
(void)state;
const uint8_t input_array[] = {{ {in_bytes}, 0 }};
const uint8_t expected_output[2] = {{ {out_byte}, 0 }};
size_t out_len = 20;
uint8_t output[20] = {{ 0 }};
int rc;
rc = libx52util_convert_utf8_string(input_array, output, &out_len);
assert_int_equal(rc, 0);
assert_int_equal(out_len, 1);
assert_memory_equal(output, expected_output, 2);
}}
"""
output = ""
for in_char, out_byte in char_dict.items():
in_bytes = ", ".join(hex(c) for c in chr(in_char).encode('utf-8'))
in_tc = hex(in_char)
output += TEST_CASE_FMT.format(in_char=in_tc, in_bytes=in_bytes, out_byte=out_byte)
output += """
const struct CMUnitTest tests[] = {
"""
for in_char in sorted(char_dict.keys()):
output += f" cmocka_unit_test(test_map_{hex(in_char)}),\n"
output += '};\n'
return output
TEST_HEADER = """
#include <stdint.h>
#include <stddef.h>
#include <stdarg.h>
#include <setjmp.h>
#include <cmocka.h>
#include "libx52util.h"
"""
TEST_FOOTER = """
int main(void) {
cmocka_set_message_output(CM_OUTPUT_TAP);
cmocka_run_group_tests(tests, NULL, NULL);
return 0;
}
"""
def main():
"""Generate X52 map test suite"""
parser = argparse.ArgumentParser(description='Generate map test cases')
parser.add_argument('INPUT_FILE', help="Input character map file")
parser.add_argument('OUTPUT_FILE', help="Generated test script")
args = parser.parse_args()
char_dict = parse_file(args.INPUT_FILE)
test_cases = generate_positive_test_cases(char_dict)
with open(args.OUTPUT_FILE, 'w', encoding='utf-8') as out_fd:
print(TEST_HEADER, file=out_fd)
print(test_cases, file=out_fd)
print(TEST_FOOTER, file=out_fd)
if __name__ == '__main__':
main()

View File

@ -1,6 +1,7 @@
project('libx52', 'C',
license: 'GPL-2.0-only WITH Classpath-exception-2.0',
version: '0.3.3')
version: '0.3.3',
meson_version: '>=0.61')
dep_libusb = dependency('libusb-1.0', required: true)
dep_hidapi = dependency('hidapi-hidraw', required: false)
@ -8,7 +9,12 @@ if not dep_hidapi.found()
dep_hidapi = dependency('hidapi', required: true)
endif
dep_evdev = dependency('libevdev', required: false)
if host_machine.system() == 'linux'
dep_evdev = dependency('libevdev', required: false)
else
# Create a dummy dependency
dep_evdev = dependency('', required: false)
endif
dep_systemd = dependency('systemd', required: false)
dep_udev = dependency('udev', required: false)
@ -33,16 +39,20 @@ assert(pyversion[1].to_int() >= 5, 'Require Python >= 3.5')
# config.h
#######################################################################
compiler = meson.get_compiler('c')
sym_hidden_cargs = []
if compiler.has_argument('-fvisibility=hidden')
sym_hidden_cargs = ['-fvisibility=hidden']
endif
cdata = configuration_data()
cdata.set_quoted('PACKAGE', meson.project_name())
cdata.set_quoted('PACKAGE_BUGREPORT', 'https://github.com/nirenjan/libx52/issues')
cdata.set_quoted('PACKAGE_NAME', meson.project_name())
cdata.set_quoted('LOCALEDIR', get_option('localedir'))
cdata.set_quoted('SYSCONFDIR', get_option('sysconfdir'))
cdata.set_quoted('LOCALSTATEDIR', get_option('localstatedir'))
cdata.set_quoted('LOCALEDIR', get_option('prefix') / get_option('localedir'))
cdata.set_quoted('SYSCONFDIR', get_option('prefix') / get_option('sysconfdir'))
cdata.set_quoted('LOCALSTATEDIR', get_option('prefix') / get_option('localstatedir'))
cdata.set_quoted('PACKAGE_VERSION', meson.project_version())
cdata.set_quoted('VERSION', meson.project_version())
cdata.set10('ENABLE_NLS', get_option('nls').enabled())
cdata.set10('ENABLE_NLS', not get_option('nls').disabled())
cdata.set10('HAVE_FUNC_ATTRIBUTE_NORETURN', compiler.has_function_attribute('noreturn'))
cdata.set10('HAVE_STRUCT_TM_TM_GMTOFF',
compiler.has_member('struct tm', 'tm_gmtoff', prefix:'#include <time.h>'))
@ -73,7 +83,7 @@ if not dep_intl.found() and host_machine.system() == 'darwin'
endif
# # define GETTEXT_PACKAGE
if get_option('nls').enabled()
if not get_option('nls').disabled()
add_project_arguments(
'-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()),
language:'C')
@ -81,6 +91,8 @@ if get_option('nls').enabled()
subdir('po')
endif
add_project_arguments('-isystem', meson.current_source_dir() / 'sys', language: 'C')
#######################################################################
# Internal dependencies
#######################################################################
@ -105,11 +117,12 @@ dep_inih = dependency('inih')
# Shared libraries and programs
#######################################################################
# Includes
includes = include_directories('.', 'libx52', 'libx52io', 'libx52util')
includes = include_directories('.', 'libx52', 'libx52io', 'libx52util', 'vkm')
subdir('libx52')
subdir('libx52io')
subdir('libx52util')
subdir('vkm')
subdir('bugreport')
subdir('cli')
subdir('joytest')
@ -127,14 +140,20 @@ if doxygen_program.found()
configuration: {
'PACKAGE_NAME': meson.project_name(),
'PACKAGE_VERSION': meson.project_version(),
'abs_top_builddir': meson.build_root(),
'abs_top_srcdir': meson.source_root(),
'abs_top_builddir': meson.project_build_root(),
'abs_top_srcdir': meson.project_source_root(),
}
)
docs_tgt = custom_target('docs',
custom_target('docs',
depend_files: [doxyfile, 'DoxygenLayout.xml'],
command: [doxygen_program],
output: 'docs'
)
meson.add_install_script(
'install-doxygen-docs.sh',
get_option('datadir') / 'doc' / meson.project_name(),
get_option('mandir'),
)
endif

View File

@ -2,10 +2,20 @@ option('systemd-logs',
type: 'feature',
description: 'Hide timestamps in log messages, needed for systemd')
option('systemd-unit-dir',
type: 'string',
value: '',
description: 'Directory for systemd service files (leave empty for auto-detection)')
option('nls',
type: 'feature',
description: 'Enable message translations')
option('udev-rules-dir',
type: 'string',
value: '',
description: 'Directory for udev rules (leave empty for auto-detection)')
option('input-group',
type: 'string', value: 'plugdev',
description: 'Group for input devices')

View File

@ -4,6 +4,8 @@ libx52/x52_stringify.c
libx52io/io_strings.c
vkm/vkm_common.c
evtest/ev_test.c
joytest/x52_test.c
@ -22,6 +24,6 @@ daemon/x52d_config_parser.c
daemon/x52d_device.c
daemon/x52d_io.c
daemon/x52d_mouse.c
daemon/x52d_mouse_evdev.c
daemon/x52d_mouse_handler.c
daemon/x52d_notify.c
daemon/x52ctl.c

View File

@ -6,9 +6,9 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: libx52 0.3.2\n"
"Project-Id-Version: libx52 0.3.3\n"
"Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\n"
"POT-Creation-Date: 2026-03-12 08:31-0700\n"
"POT-Creation-Date: 2026-03-27 20:52-0700\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: libx52/x52_strerror.c:23 libx52io/io_strings.c:101
#: libx52/x52_strerror.c:23 libx52io/io_strings.c:101 vkm/vkm_common.c:25
msgid "Success"
msgstr ""
@ -29,7 +29,7 @@ msgstr ""
msgid "Insufficient memory"
msgstr ""
#: libx52/x52_strerror.c:26
#: libx52/x52_strerror.c:26 vkm/vkm_common.c:29
msgid "Invalid parameter"
msgstr ""
@ -85,7 +85,7 @@ msgstr ""
msgid "System call interrupted"
msgstr ""
#: libx52/x52_strerror.c:66 libx52io/io_strings.c:125
#: libx52/x52_strerror.c:66 libx52io/io_strings.c:125 vkm/vkm_common.c:52
#, c-format
msgid "Unknown error %d"
msgstr ""
@ -143,12 +143,12 @@ msgid "Unknown LED state %d"
msgstr ""
#: libx52/x52_stringify.c:47 daemon/x52d_clock.c:29 daemon/x52d_mouse.c:32
#: daemon/x52d_mouse.c:68
#: daemon/x52d_mouse.c:66
msgid "off"
msgstr ""
#: libx52/x52_stringify.c:48 daemon/x52d_clock.c:29 daemon/x52d_mouse.c:32
#: daemon/x52d_mouse.c:68
#: daemon/x52d_mouse.c:66
msgid "on"
msgstr ""
@ -233,26 +233,54 @@ msgstr ""
msgid "Read timeout"
msgstr ""
#: evtest/ev_test.c:109
#: vkm/vkm_common.c:26
msgid "Unknown error"
msgstr ""
#: vkm/vkm_common.c:27
msgid "Not ready"
msgstr ""
#: vkm/vkm_common.c:28
msgid "Out of memory"
msgstr ""
#: vkm/vkm_common.c:30
msgid "Not supported"
msgstr ""
#: vkm/vkm_common.c:31
msgid "Virtual device failure"
msgstr ""
#: vkm/vkm_common.c:32
msgid "Unable to write event"
msgstr ""
#: vkm/vkm_common.c:33
msgid "No state change"
msgstr ""
#: evtest/ev_test.c:110
#, c-format
msgid "Device ID: vendor 0x%04x product 0x%04x version 0x%04x\n"
msgstr ""
#: evtest/ev_test.c:113
#: evtest/ev_test.c:114
#, c-format
msgid "Device name: \"%s %s\"\n"
msgstr ""
#: evtest/ev_test.c:116
#: evtest/ev_test.c:117
#, c-format
msgid "Serial number: \"%s\"\n"
msgstr ""
#: evtest/ev_test.c:117
#: evtest/ev_test.c:118
msgid "Testing (interrupt to exit)\n"
msgstr ""
#: evtest/ev_test.c:157 evtest/ev_test.c:165
#: evtest/ev_test.c:158 evtest/ev_test.c:166
#, c-format
msgid "Event @ %ld.%06ld: %s, value %d\n"
msgstr ""
@ -493,17 +521,17 @@ msgstr ""
msgid "OK"
msgstr ""
#: daemon/x52d_main.c:64
#: daemon/x52d_main.c:67
#, c-format
msgid "Error %d setting log file: %s\n"
msgstr ""
#: daemon/x52d_main.c:80
#: daemon/x52d_main.c:83
#, c-format
msgid "Error %d installing handler for signal %d: %s"
msgstr ""
#: daemon/x52d_main.c:91
#: daemon/x52d_main.c:94
#, c-format
msgid ""
"Usage: %s [-f] [-v] [-q]\n"
@ -513,88 +541,88 @@ msgid ""
"\t[-b notify-socket-path]\n"
msgstr ""
#: daemon/x52d_main.c:124
#: daemon/x52d_main.c:129
#, c-format
msgid "Daemon is already running as PID %u"
msgstr ""
#: daemon/x52d_main.c:266
#: daemon/x52d_main.c:271
#, c-format
msgid "Unable to parse configuration override '%s'\n"
msgstr ""
#: daemon/x52d_main.c:298
#: daemon/x52d_main.c:303
#, c-format
msgid "Foreground = %s"
msgstr ""
#: daemon/x52d_main.c:298 daemon/x52d_main.c:299
#: daemon/x52d_main.c:303 daemon/x52d_main.c:304
msgid "true"
msgstr ""
#: daemon/x52d_main.c:298 daemon/x52d_main.c:299
#: daemon/x52d_main.c:303 daemon/x52d_main.c:304
msgid "false"
msgstr ""
#: daemon/x52d_main.c:299
#, c-format
msgid "Quiet = %s"
msgstr ""
#: daemon/x52d_main.c:300
#, c-format
msgid "Verbosity = %d"
msgstr ""
#: daemon/x52d_main.c:301
#, c-format
msgid "Log file = %s"
msgstr ""
#: daemon/x52d_main.c:302
#, c-format
msgid "Config file = %s"
msgstr ""
#: daemon/x52d_main.c:303
#, c-format
msgid "PID file = %s"
msgstr ""
#: daemon/x52d_main.c:304
#, c-format
msgid "Command socket = %s"
msgid "Quiet = %s"
msgstr ""
#: daemon/x52d_main.c:305
#, c-format
msgid "Verbosity = %d"
msgstr ""
#: daemon/x52d_main.c:306
#, c-format
msgid "Log file = %s"
msgstr ""
#: daemon/x52d_main.c:307
#, c-format
msgid "Config file = %s"
msgstr ""
#: daemon/x52d_main.c:308
#, c-format
msgid "PID file = %s"
msgstr ""
#: daemon/x52d_main.c:309
#, c-format
msgid "Command socket = %s"
msgstr ""
#: daemon/x52d_main.c:310
#, c-format
msgid "Notify socket = %s"
msgstr ""
#: daemon/x52d_main.c:316
#: daemon/x52d_main.c:321
#, c-format
msgid "Error %d blocking signals on child threads: %s"
msgstr ""
#: daemon/x52d_main.c:335
#: daemon/x52d_main.c:338
#, c-format
msgid "Error %d unblocking signals on child threads: %s"
msgstr ""
#: daemon/x52d_main.c:348
#: daemon/x52d_main.c:351
msgid "Reloading X52 configuration"
msgstr ""
#: daemon/x52d_main.c:355
#: daemon/x52d_main.c:358
msgid "Saving X52 configuration to disk"
msgstr ""
#: daemon/x52d_main.c:361
#: daemon/x52d_main.c:364
#, c-format
msgid "Received termination signal %s"
msgstr ""
#: daemon/x52d_main.c:378
#: daemon/x52d_main.c:379
msgid "Shutting down X52 daemon"
msgstr ""
@ -658,21 +686,21 @@ msgstr ""
msgid "Setting date format to %s"
msgstr ""
#: daemon/x52d_clock.c:173
#: daemon/x52d_clock.c:174
msgid "Starting X52 clock manager thread"
msgstr ""
#: daemon/x52d_clock.c:184
#: daemon/x52d_clock.c:185
#, c-format
msgid "Error %d retrieving current time: %s"
msgstr ""
#: daemon/x52d_clock.c:205
#: daemon/x52d_clock.c:206
#, c-format
msgid "Error %d initializing clock thread: %s"
msgstr ""
#: daemon/x52d_clock.c:212
#: daemon/x52d_clock.c:213
msgid "Shutting down X52 clock manager thread"
msgstr ""
@ -686,31 +714,31 @@ msgstr ""
msgid "Short write to client %d; expected %d bytes, wrote %d bytes"
msgstr ""
#: daemon/x52d_command.c:414
#: daemon/x52d_command.c:415
#, c-format
msgid "Error %d during command loop: %s"
msgstr ""
#: daemon/x52d_command.c:441
#: daemon/x52d_command.c:442
#, c-format
msgid "Error creating command socket: %s"
msgstr ""
#: daemon/x52d_command.c:449
#: daemon/x52d_command.c:450
#, c-format
msgid "Error marking command socket as nonblocking: %s"
msgstr ""
#: daemon/x52d_command.c:455
#: daemon/x52d_command.c:456
#, c-format
msgid "Error listening on command socket: %s"
msgstr ""
#: daemon/x52d_command.c:459
#: daemon/x52d_command.c:460
msgid "Starting command processing thread"
msgstr ""
#: daemon/x52d_command.c:477
#: daemon/x52d_command.c:478
msgid "Shutting down command processing thread"
msgstr ""
@ -772,67 +800,67 @@ msgstr ""
msgid "Error processing override '%s.%s=%s'"
msgstr ""
#: daemon/x52d_device.c:36
#: daemon/x52d_device.c:37
msgid "Starting X52 device manager thread"
msgstr ""
#: daemon/x52d_device.c:43
#: daemon/x52d_device.c:44
#, c-format
msgid "Error %d connecting to device: %s"
msgstr ""
#: daemon/x52d_device.c:52
#: daemon/x52d_device.c:53
msgid "Device connected, writing configuration"
msgstr ""
#: daemon/x52d_device.c:75
#: daemon/x52d_device.c:76
msgid "Initializing libx52"
msgstr ""
#: daemon/x52d_device.c:79
#: daemon/x52d_device.c:80
#, c-format
msgid "Failure %d initializing libx52: %s"
msgstr ""
#: daemon/x52d_device.c:90
#: daemon/x52d_device.c:91
msgid "Shutting down X52 device manager thread"
msgstr ""
#: daemon/x52d_device.c:103
#: daemon/x52d_device.c:104
#, c-format
msgid "Error %d when updating X52 parameter: %s"
msgstr ""
#: daemon/x52d_device.c:179
#: daemon/x52d_device.c:180
#, c-format
msgid "Error %d when updating X52 device: %s"
msgstr ""
#: daemon/x52d_io.c:42
#: daemon/x52d_io.c:43
msgid "Starting X52 I/O thread"
msgstr ""
#: daemon/x52d_io.c:64
#: daemon/x52d_io.c:65
#, c-format
msgid "Error %d opening X52 I/O device: %s"
msgstr ""
#: daemon/x52d_io.c:75
#: daemon/x52d_io.c:76
#, c-format
msgid "Error %d reading from X52 I/O device: %s"
msgstr ""
#: daemon/x52d_io.c:102
#: daemon/x52d_io.c:103
#, c-format
msgid "Error %d initializing X52 I/O library: %s"
msgstr ""
#: daemon/x52d_io.c:108
#: daemon/x52d_io.c:109
#, c-format
msgid "Error %d initializing I/O driver thread: %s"
msgstr ""
#: daemon/x52d_io.c:115
#: daemon/x52d_io.c:116
msgid "Shutting down X52 I/O driver thread"
msgstr ""
@ -841,60 +869,60 @@ msgstr ""
msgid "Setting mouse enable to %s"
msgstr ""
#: daemon/x52d_mouse.c:47
#: daemon/x52d_mouse.c:45
#, c-format
msgid "Ignoring mouse speed %d outside supported range (0-%d)"
msgstr ""
#: daemon/x52d_mouse.c:59
#: daemon/x52d_mouse.c:57
#, c-format
msgid "Setting mouse speed to %d (delay %d ms, multiplier %f)"
msgstr ""
#: daemon/x52d_mouse.c:67
#: daemon/x52d_mouse.c:65
#, c-format
msgid "Setting mouse reverse scroll to %s"
msgstr ""
#: daemon/x52d_mouse_evdev.c:43
#: daemon/x52d_mouse_handler.c:43
#, c-format
msgid "Error writing mouse button event (button %d, state %d)"
msgid "Error %d writing mouse button event (button %d, state %d)"
msgstr ""
#: daemon/x52d_mouse_evdev.c:69
#: daemon/x52d_mouse_handler.c:81
#, c-format
msgid "Error writing mouse wheel event %d"
msgstr ""
#: daemon/x52d_mouse_evdev.c:102
#: daemon/x52d_mouse_handler.c:120
#, c-format
msgid "Error writing mouse axis event (axis %d, value %d)"
msgid "Error %d writing mouse axis event (dx %d, dy %d)"
msgstr ""
#: daemon/x52d_mouse_evdev.c:115
#: daemon/x52d_mouse_handler.c:132
msgid "Error writing mouse sync event"
msgstr ""
#: daemon/x52d_mouse_evdev.c:134
#: daemon/x52d_mouse_handler.c:151
msgid "Starting X52 virtual mouse driver thread"
msgstr ""
#: daemon/x52d_mouse_evdev.c:157
#: daemon/x52d_mouse_handler.c:170
#, c-format
msgid "Error %d initializing mouse thread: %s"
msgstr ""
#: daemon/x52d_mouse_evdev.c:164
#: daemon/x52d_mouse_handler.c:177
msgid "Shutting down X52 virtual mouse driver thread"
msgstr ""
#: daemon/x52d_mouse_evdev.c:171
#: daemon/x52d_mouse_handler.c:184
msgid "Virtual mouse not created. Ignoring thread state change"
msgstr ""
#: daemon/x52d_mouse_evdev.c:236
#: daemon/x52d_mouse_handler.c:236 daemon/x52d_mouse_handler.c:244
#, c-format
msgid "Error %d creating X52 virtual mouse: %s"
msgid "Error %d creating X52 virtual mouse"
msgstr ""
#: daemon/x52d_notify.c:46
@ -916,27 +944,27 @@ msgstr ""
msgid "Error setting up notification socket"
msgstr ""
#: daemon/x52d_notify.c:80 daemon/x52d_notify.c:90
#: daemon/x52d_notify.c:81 daemon/x52d_notify.c:91
#, c-format
msgid "Error %d reading from pipe: %s"
msgstr ""
#: daemon/x52d_notify.c:127
#: daemon/x52d_notify.c:128
#, c-format
msgid "Error %d writing notification pipe: %s"
msgstr ""
#: daemon/x52d_notify.c:172
#: daemon/x52d_notify.c:174
#, c-format
msgid "Error %d creating notification pipe: %s"
msgstr ""
#: daemon/x52d_notify.c:181
#: daemon/x52d_notify.c:183
#, c-format
msgid "Error %d initializing notify thread: %s"
msgstr ""
#: daemon/x52d_notify.c:187
#: daemon/x52d_notify.c:189
#, c-format
msgid "Error %d initializing notify listener: %s"
msgstr ""
@ -951,7 +979,7 @@ msgstr ""
msgid "Argument length too long\n"
msgstr ""
#: daemon/x52ctl.c:142
#: daemon/x52ctl.c:180
#, c-format
msgid "Running in interactive mode, ignoring extra arguments\n"
msgstr ""

View File

@ -1,3 +1,14 @@
i18n.gettext(meson.project_name(),
args: '--directory=' + meson.source_root(),
args: [
'--directory=' + meson.project_source_root(),
'--msgid-bugs-address=https://github.com/nirenjan/libx52/issues',
'--package-name=' + meson.project_name(),
'--package-version=' + meson.project_version(),
'--copyright-holder=Nirenjan Krishnan',
'--keyword=_', '--keyword=N_',
],
languages: [
'xx_PL',
],
install: true,
)

View File

@ -7,17 +7,17 @@ msgid ""
msgstr ""
"Project-Id-Version: libx52 0.2.3\n"
"Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\n"
"POT-Creation-Date: 2026-03-12 08:31-0700\n"
"PO-Revision-Date: 2023-01-04 08:40-0800\n"
"POT-Creation-Date: 2026-03-27 20:52-0700\n"
"PO-Revision-Date: 2026-03-27 08:33-0700\n"
"Last-Translator: Nirenjan Krishnan <nirenjan@gmail.com>\n"
"Language-Team: Dummy Language for testing i18n\n"
"Language: xx_PL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.0.1\n"
"X-Generator: Poedit 3.4.2\n"
#: libx52/x52_strerror.c:23 libx52io/io_strings.c:101
#: libx52/x52_strerror.c:23 libx52io/io_strings.c:101 vkm/vkm_common.c:25
msgid "Success"
msgstr "Uccesssay"
@ -29,7 +29,7 @@ msgstr "Initializationay ailurefay"
msgid "Insufficient memory"
msgstr "Insufficientay emorymay"
#: libx52/x52_strerror.c:26
#: libx52/x52_strerror.c:26 vkm/vkm_common.c:29
msgid "Invalid parameter"
msgstr "Invaliday arameterpay"
@ -85,7 +85,7 @@ msgstr "Ipepay erroray"
msgid "System call interrupted"
msgstr "Ystemsay allcay interrupteday"
#: libx52/x52_strerror.c:66 libx52io/io_strings.c:125
#: libx52/x52_strerror.c:66 libx52io/io_strings.c:125 vkm/vkm_common.c:52
#, c-format
msgid "Unknown error %d"
msgstr "Unknownay erroray %d"
@ -143,12 +143,12 @@ msgid "Unknown LED state %d"
msgstr "Unknownay EDLay atestay %d"
#: libx52/x52_stringify.c:47 daemon/x52d_clock.c:29 daemon/x52d_mouse.c:32
#: daemon/x52d_mouse.c:68
#: daemon/x52d_mouse.c:66
msgid "off"
msgstr "offay"
#: libx52/x52_stringify.c:48 daemon/x52d_clock.c:29 daemon/x52d_mouse.c:32
#: daemon/x52d_mouse.c:68
#: daemon/x52d_mouse.c:66
msgid "on"
msgstr "onay"
@ -233,26 +233,56 @@ msgstr "I/O erroray"
msgid "Read timeout"
msgstr "Eadray imeouttay"
#: evtest/ev_test.c:109
#: vkm/vkm_common.c:26
#, fuzzy
msgid "Unknown error"
msgstr "Unknownay erroray %d"
#: vkm/vkm_common.c:27
msgid "Not ready"
msgstr ""
#: vkm/vkm_common.c:28
msgid "Out of memory"
msgstr ""
#: vkm/vkm_common.c:30
#, fuzzy
msgid "Not supported"
msgstr "Operationay otnay upportedsay"
#: vkm/vkm_common.c:31
msgid "Virtual device failure"
msgstr ""
#: vkm/vkm_common.c:32
msgid "Unable to write event"
msgstr ""
#: vkm/vkm_common.c:33
msgid "No state change"
msgstr ""
#: evtest/ev_test.c:110
#, c-format
msgid "Device ID: vendor 0x%04x product 0x%04x version 0x%04x\n"
msgstr "Eviceday IDay: endorvay 0x%04x oductpray 0x%04x ersionvay 0x%04x\n"
#: evtest/ev_test.c:113
#: evtest/ev_test.c:114
#, c-format
msgid "Device name: \"%s %s\"\n"
msgstr "Eviceday amenay: \"%s %s\"\n"
#: evtest/ev_test.c:116
#: evtest/ev_test.c:117
#, c-format
msgid "Serial number: \"%s\"\n"
msgstr "Erialsay umbernay: \"%s\"\n"
#: evtest/ev_test.c:117
#: evtest/ev_test.c:118
msgid "Testing (interrupt to exit)\n"
msgstr "Estingtay (interruptay otay exitay)\n"
#: evtest/ev_test.c:157 evtest/ev_test.c:165
#: evtest/ev_test.c:158 evtest/ev_test.c:166
#, c-format
msgid "Event @ %ld.%06ld: %s, value %d\n"
msgstr "Eventay @ %ld.%06ld: %s, aluevay %d\n"
@ -536,17 +566,17 @@ msgstr "Estingtay aracterchay 0x%02x..."
msgid "OK"
msgstr "OKay"
#: daemon/x52d_main.c:64
#: daemon/x52d_main.c:67
#, c-format
msgid "Error %d setting log file: %s\n"
msgstr "Erroray %d ettingsay oglay ilefay: %s\n"
#: daemon/x52d_main.c:80
#: daemon/x52d_main.c:83
#, c-format
msgid "Error %d installing handler for signal %d: %s"
msgstr "Erroray %d installingay andlerhay orfay ignalsay %d: %s"
#: daemon/x52d_main.c:91
#: daemon/x52d_main.c:94
#, c-format
msgid ""
"Usage: %s [-f] [-v] [-q]\n"
@ -562,88 +592,88 @@ msgstr ""
"\t[-b otifynay-ocketsay-athpay]\n"
"\n"
#: daemon/x52d_main.c:124
#: daemon/x52d_main.c:129
#, c-format
msgid "Daemon is already running as PID %u"
msgstr "Aemonday isay alreadyay unningray asay IDPay %u"
#: daemon/x52d_main.c:266
#: daemon/x52d_main.c:271
#, c-format
msgid "Unable to parse configuration override '%s'\n"
msgstr "Unableay otay arsepay onfigurationcay overrideay '%s'\n"
#: daemon/x52d_main.c:298
#: daemon/x52d_main.c:303
#, c-format
msgid "Foreground = %s"
msgstr "Oregroundfay = %s"
#: daemon/x52d_main.c:298 daemon/x52d_main.c:299
#: daemon/x52d_main.c:303 daemon/x52d_main.c:304
msgid "true"
msgstr "uetray"
#: daemon/x52d_main.c:298 daemon/x52d_main.c:299
#: daemon/x52d_main.c:303 daemon/x52d_main.c:304
msgid "false"
msgstr "alsefay"
#: daemon/x52d_main.c:299
#: daemon/x52d_main.c:304
#, c-format
msgid "Quiet = %s"
msgstr "Uietqay = %s"
#: daemon/x52d_main.c:300
#: daemon/x52d_main.c:305
#, c-format
msgid "Verbosity = %d"
msgstr "Erbosityvay = %d"
#: daemon/x52d_main.c:301
#: daemon/x52d_main.c:306
#, c-format
msgid "Log file = %s"
msgstr "Oglay ilefay = %s"
#: daemon/x52d_main.c:302
#: daemon/x52d_main.c:307
#, c-format
msgid "Config file = %s"
msgstr "Onfigcay ilefay = %s"
#: daemon/x52d_main.c:303
#: daemon/x52d_main.c:308
#, c-format
msgid "PID file = %s"
msgstr "IDPay ilefay = %s"
#: daemon/x52d_main.c:304
#: daemon/x52d_main.c:309
#, c-format
msgid "Command socket = %s"
msgstr "Ommandcay ocketsay = %s"
#: daemon/x52d_main.c:305
#: daemon/x52d_main.c:310
#, c-format
msgid "Notify socket = %s"
msgstr "Otifynay ocketsay = %s"
#: daemon/x52d_main.c:316
#: daemon/x52d_main.c:321
#, c-format
msgid "Error %d blocking signals on child threads: %s"
msgstr "Erroray %d ockingblay ignalssay onay ildchay eadsthray: %s"
#: daemon/x52d_main.c:335
#: daemon/x52d_main.c:338
#, c-format
msgid "Error %d unblocking signals on child threads: %s"
msgstr "Erroray %d unblockingay ignalssay onay ildchay eadsthray: %s"
#: daemon/x52d_main.c:348
#: daemon/x52d_main.c:351
msgid "Reloading X52 configuration"
msgstr "Eloadingray X52 onfigurationcay"
#: daemon/x52d_main.c:355
#: daemon/x52d_main.c:358
msgid "Saving X52 configuration to disk"
msgstr "Avingsay X52 onfigurationcay otay iskday"
#: daemon/x52d_main.c:361
#: daemon/x52d_main.c:364
#, c-format
msgid "Received termination signal %s"
msgstr "Eceivedray erminationtay ignalsay %s"
#: daemon/x52d_main.c:378
#: daemon/x52d_main.c:379
msgid "Shutting down X52 daemon"
msgstr "Uttingshay ownday X52 aemonday"
@ -710,21 +740,21 @@ msgstr "Ettingsay %s ockclay ormatfay otay %s"
msgid "Setting date format to %s"
msgstr "Ettingsay ateday ormatfay otay %s"
#: daemon/x52d_clock.c:173
#: daemon/x52d_clock.c:174
msgid "Starting X52 clock manager thread"
msgstr "Artingstay X52 ockclay anagermay eadthray"
#: daemon/x52d_clock.c:184
#: daemon/x52d_clock.c:185
#, c-format
msgid "Error %d retrieving current time: %s"
msgstr "Erroray %d etrievingray urrentcay imetay: %s"
#: daemon/x52d_clock.c:205
#: daemon/x52d_clock.c:206
#, c-format
msgid "Error %d initializing clock thread: %s"
msgstr "Erroray %d initializingay ockclay eadthray: %s"
#: daemon/x52d_clock.c:212
#: daemon/x52d_clock.c:213
msgid "Shutting down X52 clock manager thread"
msgstr "Uttingshay ownday X52 ockclay anagermay eadthray"
@ -739,31 +769,31 @@ msgid "Short write to client %d; expected %d bytes, wrote %d bytes"
msgstr ""
"Ortshay itewray otay ientclay %d; expecteday %d ytesbay, otewray %d ytesbay"
#: daemon/x52d_command.c:414
#: daemon/x52d_command.c:415
#, c-format
msgid "Error %d during command loop: %s"
msgstr "Erroray %d uringday ommandcay ooplay: %s"
#: daemon/x52d_command.c:441
#: daemon/x52d_command.c:442
#, c-format
msgid "Error creating command socket: %s"
msgstr "Erroray eatingcray ommandcay ocketsay: %s"
#: daemon/x52d_command.c:449
#: daemon/x52d_command.c:450
#, c-format
msgid "Error marking command socket as nonblocking: %s"
msgstr "Erroray arkingmay ommandcay ocketsay asay onblockingnay: %s"
#: daemon/x52d_command.c:455
#: daemon/x52d_command.c:456
#, c-format
msgid "Error listening on command socket: %s"
msgstr "Erroray isteninglay onay ommandcay ocketsay: %s"
#: daemon/x52d_command.c:459
#: daemon/x52d_command.c:460
msgid "Starting command processing thread"
msgstr "Artingstay ommandcay ocessingpray eadthray"
#: daemon/x52d_command.c:477
#: daemon/x52d_command.c:478
msgid "Shutting down command processing thread"
msgstr "Uttingshay ownday ommandcay ocessingpray eadthray"
@ -825,67 +855,67 @@ msgstr "Onay aluevay oundfay inay overrideay ingstray '%s'"
msgid "Error processing override '%s.%s=%s'"
msgstr "Erroray ocessingpray overriday '%s.%s=%s'"
#: daemon/x52d_device.c:36
#: daemon/x52d_device.c:37
msgid "Starting X52 device manager thread"
msgstr "Artingstay X52 eviceday anagermay eadthray"
#: daemon/x52d_device.c:43
#: daemon/x52d_device.c:44
#, c-format
msgid "Error %d connecting to device: %s"
msgstr "Erroray %d onnectingcay otay eviceday: %s"
#: daemon/x52d_device.c:52
#: daemon/x52d_device.c:53
msgid "Device connected, writing configuration"
msgstr "Eviceday onnectedcay, itingwray onfigurationcay"
#: daemon/x52d_device.c:75
#: daemon/x52d_device.c:76
msgid "Initializing libx52"
msgstr "Initializingay libx52"
#: daemon/x52d_device.c:79
#: daemon/x52d_device.c:80
#, c-format
msgid "Failure %d initializing libx52: %s"
msgstr "Ailurefay %d initializeay libx52: %s"
#: daemon/x52d_device.c:90
#: daemon/x52d_device.c:91
msgid "Shutting down X52 device manager thread"
msgstr "Uttingshay ownday X52 eviceday anagermay eadthray"
#: daemon/x52d_device.c:103
#: daemon/x52d_device.c:104
#, c-format
msgid "Error %d when updating X52 parameter: %s"
msgstr "Erroray %d enwhay updatingay X52 arameterpay: %s"
#: daemon/x52d_device.c:179
#: daemon/x52d_device.c:180
#, c-format
msgid "Error %d when updating X52 device: %s"
msgstr "Erroray %d enwhay updatingay X52 eviceday: %s"
#: daemon/x52d_io.c:42
#: daemon/x52d_io.c:43
msgid "Starting X52 I/O thread"
msgstr "Artingstay X52 I/O eadthray"
#: daemon/x52d_io.c:64
#: daemon/x52d_io.c:65
#, c-format
msgid "Error %d opening X52 I/O device: %s"
msgstr "Erroray %d openingay X52 I/O eviceday: %s"
#: daemon/x52d_io.c:75
#: daemon/x52d_io.c:76
#, c-format
msgid "Error %d reading from X52 I/O device: %s"
msgstr "Erroray %d eadingray omfray X52 I/O eviceday: %s"
#: daemon/x52d_io.c:102
#: daemon/x52d_io.c:103
#, c-format
msgid "Error %d initializing X52 I/O library: %s"
msgstr "Erroray %d initializingay X52 ibrarylay: %s"
#: daemon/x52d_io.c:108
#: daemon/x52d_io.c:109
#, c-format
msgid "Error %d initializing I/O driver thread: %s"
msgstr "Erroray %d initializingay I/O iverdray eadthray: %s"
#: daemon/x52d_io.c:115
#: daemon/x52d_io.c:116
msgid "Shutting down X52 I/O driver thread"
msgstr "Uttingshay ownday X52 I/O iverdray eadthray"
@ -894,61 +924,62 @@ msgstr "Uttingshay ownday X52 I/O iverdray eadthray"
msgid "Setting mouse enable to %s"
msgstr "Ettingsay ousemay enableay otay %s"
#: daemon/x52d_mouse.c:47
#: daemon/x52d_mouse.c:45
#, c-format
msgid "Ignoring mouse speed %d outside supported range (0-%d)"
msgstr "Ignoringay ousemay eedspay %d outsideay upportedsay angeray (0-%d)"
#: daemon/x52d_mouse.c:59
#: daemon/x52d_mouse.c:57
#, c-format
msgid "Setting mouse speed to %d (delay %d ms, multiplier %f)"
msgstr "Ettingsay ousemay eedspay otay %d (elayday %d ms, ultipliermay %f)"
#: daemon/x52d_mouse.c:67
#: daemon/x52d_mouse.c:65
#, c-format
msgid "Setting mouse reverse scroll to %s"
msgstr "Ettingsay ousemay everseray ollscray otay %s"
#: daemon/x52d_mouse_evdev.c:43
#: daemon/x52d_mouse_handler.c:43
#, c-format
msgid "Error writing mouse button event (button %d, state %d)"
msgstr "Erroray itingwray ousemay uttonbay eventay (uttonbay %d, atestay %d)"
msgid "Error %d writing mouse button event (button %d, state %d)"
msgstr ""
"Erroray %d itingwray ousemay uttonbay eventay (uttonbay %d, atestay %d)"
#: daemon/x52d_mouse_evdev.c:69
#: daemon/x52d_mouse_handler.c:81
#, c-format
msgid "Error writing mouse wheel event %d"
msgstr "Erroray itingwray ousemay eelwhay eventay %d"
#: daemon/x52d_mouse_evdev.c:102
#: daemon/x52d_mouse_handler.c:120
#, c-format
msgid "Error writing mouse axis event (axis %d, value %d)"
msgstr "Erroray itingwray ousemay axisay eventay (axisay %d, aluevay %d)"
msgid "Error %d writing mouse axis event (dx %d, dy %d)"
msgstr "Erroray %d itingwray ousemay axisay eventay (xday %d, yday %d)"
#: daemon/x52d_mouse_evdev.c:115
#: daemon/x52d_mouse_handler.c:132
msgid "Error writing mouse sync event"
msgstr "Erroray itingwray ousemay yncsay eventay"
#: daemon/x52d_mouse_evdev.c:134
#: daemon/x52d_mouse_handler.c:151
msgid "Starting X52 virtual mouse driver thread"
msgstr "Artingstay X52 irtualvay ousemay iverdray eadthray"
#: daemon/x52d_mouse_evdev.c:157
#: daemon/x52d_mouse_handler.c:170
#, c-format
msgid "Error %d initializing mouse thread: %s"
msgstr "Erroray %d initializingay ousemay eadthray: %s"
#: daemon/x52d_mouse_evdev.c:164
#: daemon/x52d_mouse_handler.c:177
msgid "Shutting down X52 virtual mouse driver thread"
msgstr "Uttingshay ownday X52 irtualvay ousemay iverdray eadthray"
#: daemon/x52d_mouse_evdev.c:171
#: daemon/x52d_mouse_handler.c:184
msgid "Virtual mouse not created. Ignoring thread state change"
msgstr "Irtualvay ousemay otnay eatedcray. Ignoringa eadthray atestay angechay"
#: daemon/x52d_mouse_evdev.c:236
#: daemon/x52d_mouse_handler.c:236 daemon/x52d_mouse_handler.c:244
#, c-format
msgid "Error %d creating X52 virtual mouse: %s"
msgstr "Erroray %d eatingcray X52 irtualvay ousemay: %s"
msgid "Error %d creating X52 virtual mouse"
msgstr "Erroray %d eatingcray X52 irtualvay ousemay"
#: daemon/x52d_notify.c:46
#, c-format
@ -969,27 +1000,27 @@ msgstr "Erroray isteninglay onay otificationnay ocketsay: %s"
msgid "Error setting up notification socket"
msgstr "Erroray ettingsay upay otificationnay ocketsay: %s"
#: daemon/x52d_notify.c:80 daemon/x52d_notify.c:90
#: daemon/x52d_notify.c:81 daemon/x52d_notify.c:91
#, c-format
msgid "Error %d reading from pipe: %s"
msgstr "Erroray eadingray omfray ipepay %d: %s"
#: daemon/x52d_notify.c:127
#: daemon/x52d_notify.c:128
#, c-format
msgid "Error %d writing notification pipe: %s"
msgstr "Erroray %d itingwray otificationnay ipepay: %s"
#: daemon/x52d_notify.c:172
#: daemon/x52d_notify.c:174
#, c-format
msgid "Error %d creating notification pipe: %s"
msgstr "Erroray %d eatingcray otificationnay ipepay: %s"
#: daemon/x52d_notify.c:181
#: daemon/x52d_notify.c:183
#, c-format
msgid "Error %d initializing notify thread: %s"
msgstr "Erroray %d initializingay otifynay eadthray: %s"
#: daemon/x52d_notify.c:187
#: daemon/x52d_notify.c:189
#, c-format
msgid "Error %d initializing notify listener: %s"
msgstr "Erroray %d initializingay otifynay istenerlay: %s"
@ -1004,7 +1035,7 @@ msgstr "Usageay: %s [-i] [-s ocketsay-athpay] [ommandcay]\n"
msgid "Argument length too long\n"
msgstr "Argumentay engthlay ootay onglay\n"
#: daemon/x52ctl.c:142
#: daemon/x52ctl.c:180
#, c-format
msgid "Running in interactive mode, ignoring extra arguments\n"
msgstr "Unningray inay interactiveay odemay, ignoringay extraay argumentsay\n"

View File

@ -41,7 +41,7 @@ static void print_time_difference(const char *type, struct timespec *ts)
type, tp_usec, tp_nsec, ret.tv_sec, ret.tv_nsec);
}
int main(int argc, char **argv)
int main(void)
{
struct timespec ts_wall[2];
struct timespec ts_cpu[2];

View File

@ -21,39 +21,41 @@ level_class = ['nolvl', 'lvl']
backtrace_class = ['notr', 'tr']
test_files = []
test_name_template = '@0@-@1@-@2@-@3@'
foreach test_type: ['bench', 'test']
test_src = test_type + '_pinelog.c'
foreach date_arg: [0, 1]
date_def = '-DPINELOG_SHOW_DATE=' + date_arg.to_string()
date_name = date_arg == 1 ? 'ts' : 'nots'
test_name_template = '-@0@-@1@-@2@'
foreach date_arg: [0, 1]
date_def = '-DPINELOG_SHOW_DATE=' + date_arg.to_string()
date_name = date_arg == 1 ? 'ts' : 'nots'
foreach level_arg: [0, 1]
level_def = '-DPINELOG_SHOW_LEVEL=' + level_arg.to_string()
level_name = level_arg == 1 ? 'lvl' : 'nolvl'
foreach level_arg: [0, 1]
level_def = '-DPINELOG_SHOW_LEVEL=' + level_arg.to_string()
level_name = level_arg == 1 ? 'lvl' : 'nolvl'
foreach backtrace_arg: [0, 1]
backtrace_def = '-DPINELOG_SHOW_BACKTRACE=' + backtrace_arg.to_string()
backtrace_name = backtrace_arg == 1 ? 'tr' : 'notr'
foreach backtrace_arg: [0, 1]
backtrace_def = '-DPINELOG_SHOW_BACKTRACE=' + backtrace_arg.to_string()
backtrace_name = backtrace_arg == 1 ? 'tr' : 'notr'
test_name = test_name_template.format(test_type,
date_name, level_name, backtrace_name)
test_exe = executable(test_name, test_src, 'pinelog.c',
c_args: [
'-DPINELOG_FATAL_STR="F"',
'-DPINELOG_ERROR_STR="E"',
'-DPINELOG_WARNING_STR="W"',
'-DPINELOG_INFO_STR="I"',
'-DPINELOG_DEBUG_STR="D"',
'-DPINELOG_TRACE_STR="T"',
'-DPINELOG_DEFAULT_LEVEL=PINELOG_LVL_TRACE',
'-DPINELOG_DEFAULT_STREAM=stderr',
'-DPINELOG_TEST',
date_def, level_def, backtrace_def
])
c_args = [
'-DPINELOG_FATAL_STR="F"',
'-DPINELOG_ERROR_STR="E"',
'-DPINELOG_WARNING_STR="W"',
'-DPINELOG_INFO_STR="I"',
'-DPINELOG_DEBUG_STR="D"',
'-DPINELOG_TRACE_STR="T"',
'-DPINELOG_DEFAULT_LEVEL=PINELOG_LVL_TRACE',
'-DPINELOG_DEFAULT_STREAM=stderr',
'-DPINELOG_TEST',
date_def, level_def, backtrace_def
]
test_name = test_name_template.format(
date_name, level_name, backtrace_name)
test(test_name, test_exe, protocol: 'tap')
endforeach
test_exe = executable('test' + test_name, 'test_pinelog.c', 'pinelog.c',
c_args: c_args)
test('test' + test_name, test_exe, protocol: 'tap')
bench_exe = executable('bench' + test_name, 'bench_pinelog.c', 'pinelog.c',
c_args: c_args)
benchmark('bench' + test_name, bench_exe, protocol: 'tap')
endforeach
endforeach
endforeach

View File

@ -348,6 +348,10 @@ void pinelog_log_message(int module, int level, const char *file, int line, cons
#else
fprintf(output_stream, "%s:%d ", file, line);
#endif
#else
// Suppress unused parameter warnings
(void)file;
(void)line;
#endif
/* Set the module name if it is not the root */

View File

@ -36,6 +36,7 @@ static size_t expected_len;
time_t time(time_t *p)
{
(void)p;
// Override the time function from libc
return 1636671600;
}
@ -196,7 +197,7 @@ static void tap_bailout(const char *msg)
exit(1);
}
int main(int argc, char **argv)
int main(void)
{
int fifo_fd[2];
int flags;

View File

@ -1,11 +1,10 @@
# udev rules
if dep_udev.found()
if meson.version().version_compare('>= 0.58.0')
udev_rules_dir = get_option('udev-rules-dir')
if udev_rules_dir == ''
udev_dir = dep_udev.get_variable('udevdir', default_value:'/lib/udev')
else
udev_dir = dep_udev.get_pkgconfig_variable('udevdir', default:'/lib/udev')
udev_rules_dir = join_paths(udev_dir, 'rules.d')
endif
udev_rules_dir = join_paths(udev_dir, 'rules.d')
udev_file = configure_file(
input: '60-saitek-x52-x52pro.rules.in',
output: '60-saitek-x52-x52pro.rules',

50
vkm/README.md 100644
View File

@ -0,0 +1,50 @@
Virtual keyboard/mouse infrastructure
=====================================
The virtual keyboard/mouse infrastructure (or VKM), is an API used by an
application to inject keyboard and mouse events into the OS stack. The advantage
of using a separate API for this is so that this can be handled in a
cross-platform manner without having to sprinkle `#ifdef`'s throughout the
program.
Base API
========
The API is based around a context, which is an opaque pointer returned by
`vkm_init`. All subsequent VKM calls will take in this pointer as the first
argument, and return a signed 32-bit status code. Once done, `vkm_exit` will
clean up any data structures and close any file descriptors that were opened as
part of the VKM calls.
VKM can also be configured through the `vkm_config` API call.
Device handling
===============
`vkm_new_device` is the API to use when creating a new VKM device. While VKM
will support both keyboard and mouse events from a single device, there may be
cases where the application needs to separate out keyboard and mouse events into
different devices. The flags will enable keyboard and/or mouse support.
Note that the supported event codes (on Linux) are fixed, and cannot be updated.
The keyboard will emulate a standard US keyboard, while the mouse will emulate a
standard 3 button mouse with a scroll wheel.
Mouse handling
==============
The mouse is handled as a single API call that passes in dx and dy to move the
mouse in the relative X and Y axes. VKM will take care of internally translating
the calls to the appropriate framework. This is handled in `vkm_mouse_move`
The scroll wheel is handled through `vkm_mouse_scroll`. By default, the mouse
motion uses standard scrolling, but high resolution scrolling may be enabled.
The buttons are handled by `vkm_mouse_click`. The API will send the state,
depending on the input (pressed or not)
Keyboard handling
=================
The keyboard is handled through a single call `vkm_keyboard_send`. This sends a
single key event, with modifiers enabled (Ctrl, Shift, Alt, GUI).

61
vkm/meson.build 100644
View File

@ -0,0 +1,61 @@
vkm_version = '0.1.0'
vkm_files = files(
'vkm_common.c'
)
vkm_stub_files = files(
'vkm_stub.c'
)
if host_machine.system() == 'linux'
vkm_dep = dep_evdev
if dep_evdev.found()
vkm_platform_files = files(
'vkm_linux_evdev.c'
)
else
vkm_platform_files = vkm_stub_files
endif
else
vkm_dep = dependency('', required: false)
vkm_platform_files = vkm_stub_files
endif
lib_vkm = library('vkm', vkm_files + vkm_platform_files,
install: true,
version: vkm_version,
c_args: sym_hidden_cargs,
dependencies: [vkm_dep, dep_intl],
include_directories: [includes])
vkm_strerror_test = executable('vkm-strerror-test',
'test_strerror.c',
'vkm_common.c',
build_by_default: false,
dependencies: [dep_cmocka, dep_intl],
include_directories: [includes],
)
test('vkm-strerror', vkm_strerror_test, protocol: 'tap')
if host_machine.system() == 'linux' and dep_evdev.found()
dep_evdev_headers = dep_evdev.partial_dependency(compile_args: true, link_args: false)
vkm_linux_evdev_test = executable('vkm_linux_evdev_test',
files(
'vkm_linux_evdev_test.c',
'vkm_linux_evdev.c',
'vkm_common.c',
),
include_directories: [includes],
dependencies: [dep_cmocka, dep_evdev_headers, dep_intl],
)
test('vkm_linux_evdev', vkm_linux_evdev_test)
endif
install_headers('vkm.h', subdir: 'vkm')
pkgconfig.generate(lib_vkm,
name: 'vkm',
description: 'Linux/Unix library to control Saitek X52/X52Pro joystick extended functionality.',
subdirs: meson.project_name(),
version: vkm_version)

View File

@ -0,0 +1,57 @@
/*
* VKM strerror unit tests
*
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
*
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <stdio.h>
#include <string.h>
#include "vkm.h"
static void test_strerror(void **state)
{
(void)state;
static const char *error_map[VKM_ERROR_MAX] = {
[VKM_SUCCESS] = "Success",
[VKM_ERROR_UNKNOWN] = "Unknown error",
[VKM_ERROR_NOT_READY] = "Not ready",
[VKM_ERROR_OUT_OF_MEMORY] = "Out of memory",
[VKM_ERROR_INVALID_PARAM] = "Invalid parameter",
[VKM_ERROR_NOT_SUPPORTED] = "Not supported",
[VKM_ERROR_DEV_FAILURE] = "Virtual device failure",
[VKM_ERROR_EVENT] = "Unable to write event",
[VKM_ERROR_NO_CHANGE] = "No state change",
};
static const char *unknown_fmt = "Unknown error %d";
char expected[256];
for (int i = -1; i <= (int)VKM_ERROR_MAX + 1; i++) {
if (i < 0 || i >= (int)VKM_ERROR_MAX || error_map[i] == NULL) {
snprintf(expected, sizeof(expected), unknown_fmt, i);
} else {
strncpy(expected, error_map[i], sizeof(expected));
}
assert_string_equal(expected, vkm_strerror(i));
}
}
static const struct CMUnitTest tests[] = {
cmocka_unit_test(test_strerror),
};
int main(void)
{
cmocka_set_message_output(CM_OUTPUT_TAP);
cmocka_run_group_tests(tests, NULL, NULL);
return 0;
}

22
vkm/vkm-internal.h 100644
View File

@ -0,0 +1,22 @@
/*
* VKM common functions
*
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
*
* SPDX-LicenseIdentifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#include <stdbool.h>
#include "vkm.h"
#ifndef VKM_INTERNAL_H
#define VKM_INTERNAL_H
struct vkm_mouse_button_state {
vkm_button_state pressed[VKM_MOUSE_BTN_MAX];
};
vkm_button_state _vkm_get_mouse_button_state(struct vkm_mouse_button_state *state, vkm_mouse_button button);
void _vkm_set_mouse_button_state(struct vkm_mouse_button_state *sstate, vkm_mouse_button button, vkm_button_state state);
#endif // !defined VKM_INTERNAL_H

619
vkm/vkm.h 100644
View File

@ -0,0 +1,619 @@
/*
* Virtual keyboard/mouse interface
*
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
*
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
/**
* @file vkm.h
* @brief Functions, structures and enumerations for the virtual
* keyboard/mouse interface library (VKM).
*
* This file contains the type, enum and function prototypes for VKM.
* These functions allow an application to inject keyboard/mouse events
* into the host OS, as long as it has the necessary permissions.
*
* @author Nirenjan Krishnan (nirenjan@nirenjan.org)
*/
#ifndef VKM_H
#define VKM_H
#include <stdint.h>
#include <stdbool.h>
#ifndef VKM_API
# if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 303
# define VKM_API __attribute__((visibility("default")))
# elif defined(_WIN32)
# define VKM_API __declspec(dllexport)
# else
# define VKM_API
# endif
#endif
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Opaque structure used by the VKM framework
*/
struct vkm_context;
/**
* @brief Virtual device context structure used by the VKM framework
*
* All VKM API functions require the application to pass in a pointer to
* a valid context structure. A pointer can be obtained by calling
* \ref vkm_init
*/
typedef struct vkm_context vkm_context;
/**
* @brief Return type used by VKM API functions
*/
typedef int32_t vkm_result;
/**
* @brief Feature identifiers for \ref vkm_feature_supported
*
* Bit flags describing optional VKM capabilities on the current platform.
* Pass one enumerator at a time to \ref vkm_feature_supported.
*/
typedef enum {
VKM_FEAT_MOUSE = (1 << 0), /**< Relative mouse move, buttons, and wheel */
/** Full \ref vkm_keyboard_send key map (platform virtual keyboard) */
VKM_FEAT_KEYBOARD = (1 << 1),
/** Separate left/right logical modifiers in \ref vkm_keyboard_send */
VKM_FEAT_KEYBOARD_MODIFIERS = (1 << 2),
/* Additional flags may be added in the future */
} vkm_feature;
/**
* @brief Error code list
*/
typedef enum {
/** No error, indicates success */
VKM_SUCCESS = 0,
/** Unknown error, used as a catch-all error state */
VKM_ERROR_UNKNOWN,
/** Not started, must call vkm_start first */
VKM_ERROR_NOT_READY,
/** Out of memory */
VKM_ERROR_OUT_OF_MEMORY,
/** Invalid parameter(s) */
VKM_ERROR_INVALID_PARAM,
/** Not supported */
VKM_ERROR_NOT_SUPPORTED,
/** Unable to create virtual devices */
VKM_ERROR_DEV_FAILURE,
/** Unable to write event */
VKM_ERROR_EVENT,
/** No state change in the event, please retry */
VKM_ERROR_NO_CHANGE,
/* Maximum error states, do not use in external code*/
VKM_ERROR_MAX,
} vkm_error_code;
/**
* @brief Return a short description for a \ref vkm_result / \ref vkm_error_code value
*
* The returned pointer refers to static storage and must not be freed. For
* unrecognized codes, the same static buffer may be overwritten by a later call.
*
* When native language support (NLS) is enabled at build time, these messages
* are translated like \ref libx52_strerror. Bind the \c libx52 text domain and
* set \c LC_MESSAGES as for other libx52 components.
*
* @param[in] code Value returned from a VKM API function
*
* @returns Pointer to a NUL-terminated description string
*/
VKM_API const char *vkm_strerror(vkm_result code);
/**
* @brief Option list
*/
typedef enum {
/**
* @brief Set the high resolution scrolling behavior of the mouse
*
* This option must be passed a boolean which lets VKM know whether to
* enable or disable high resolution scrolling.
*
* Defaults to false. When enabled together with \ref vkm_start,
* \ref vkm_mouse_scroll emits high-resolution REL_*_HI_RES events (120 units
* per step) in addition to discrete REL_WHEEL / REL_HWHEEL ticks.
*/
VKM_OPT_HI_RES_SCROLL,
/**
* @brief Enable or disable horizontal scrolling of the mouse
*
* This option must be passed in a boolean which lets VKM know whether to
* enable or disable horizontal scrolling. If horizontal scrolling is
* disabled, then any requests to \ref vkm_mouse_scroll with
* \ref VKM_MOUSE_SCROLL_LEFT or \ref VKM_MOUSE_SCROLL_RIGHT will return
* \ref VKM_ERROR_INVALID_PARAM.
*
* Defaults to false.
*/
VKM_OPT_HORIZONTAL_SCROLL,
/**
* @brief Set the virtual device name in the system.
*
* This option sets the name of the virtual input device in the system.
* If not set, the virtual device will have a name determined by the
* timestamp at which it was initialized.
*
* Only applicable on Linux.
*/
VKM_OPT_DEVICE_NAME,
/* Max number of options, do not use in external code */
VKM_OPT_MAX
} vkm_option;
/**
* @brief Button state
*/
typedef enum {
/** Button is released */
VKM_BUTTON_RELEASED,
/** Button is pressed */
VKM_BUTTON_PRESSED,
/* Max number of button states, do not use in external code */
VKM_BUTTON_STATE_MAX
} vkm_button_state;
/**
* @brief Mouse button identifiers
*/
typedef enum {
/** Mouse left button */
VKM_MOUSE_BTN_LEFT,
/** Mouse right button */
VKM_MOUSE_BTN_RIGHT,
/** Mouse middle button */
VKM_MOUSE_BTN_MIDDLE,
/* Max number of mouse buttons, do not use in external code */
VKM_MOUSE_BTN_MAX
} vkm_mouse_button;
/**
* @brief Scroll directions
*/
typedef enum {
/** Scroll up */
VKM_MOUSE_SCROLL_UP,
/** Scroll down */
VKM_MOUSE_SCROLL_DOWN,
/** Scroll left (horizontal scrolling) */
VKM_MOUSE_SCROLL_LEFT,
/** Scroll right (horizontal scrolling) */
VKM_MOUSE_SCROLL_RIGHT,
/* Maximum number of scroll states, do not use in external code */
VKM_MOUSE_SCROLL_MAX
} vkm_mouse_scroll_direction;
/**
* @brief Physical key identifiers (105-key ISO / international PC set)
*
* Logical key codes for \ref vkm_keyboard_send. Identifiers follow the USB HID
* Usage Tables (keyboard/keypad page) in meaning; enumerator values are
* arbitrary and are not HID usage IDs.
*
* Shift keys are listed with the alphanumeric block; other modifiers are grouped
* at the start of the enumerator list.
*/
typedef enum {
VKM_KEY_NONE = 0, /**< Sentinel: no key (e.g. modifier-only update) */
VKM_KEY_LEFT_CTRL, /**< Left Control */
VKM_KEY_LEFT_ALT, /**< Left Alt */
VKM_KEY_LEFT_GUI, /**< Left GUI / Meta / Windows key */
VKM_KEY_RIGHT_CTRL, /**< Right Control */
VKM_KEY_RIGHT_ALT, /**< Right Alt / AltGr */
VKM_KEY_RIGHT_GUI, /**< Right GUI / Meta */
VKM_KEY_APPLICATION, /**< Application / Menu key */
VKM_KEY_ESCAPE, /**< Escape */
VKM_KEY_F1, /**< F1 */
VKM_KEY_F2, /**< F2 */
VKM_KEY_F3, /**< F3 */
VKM_KEY_F4, /**< F4 */
VKM_KEY_F5, /**< F5 */
VKM_KEY_F6, /**< F6 */
VKM_KEY_F7, /**< F7 */
VKM_KEY_F8, /**< F8 */
VKM_KEY_F9, /**< F9 */
VKM_KEY_F10, /**< F10 */
VKM_KEY_F11, /**< F11 */
VKM_KEY_F12, /**< F12 */
VKM_KEY_GRAVE_ACCENT, /**< Grave accent / tilde (` and ~ on US) */
VKM_KEY_1, /**< \c 1 / ! */
VKM_KEY_2, /**< \c 2 / @ */
VKM_KEY_3, /**< \c 3 / # */
VKM_KEY_4, /**< \c 4 / $ */
VKM_KEY_5, /**< \c 5 / % */
VKM_KEY_6, /**< \c 6 / ^ */
VKM_KEY_7, /**< \c 7 / & */
VKM_KEY_8, /**< \c 8 / * */
VKM_KEY_9, /**< \c 9 / ( */
VKM_KEY_0, /**< \c 0 / ) */
VKM_KEY_MINUS, /**< Minus / underscore */
VKM_KEY_EQUAL, /**< Equals / plus */
VKM_KEY_BACKSPACE, /**< Backspace */
VKM_KEY_TAB, /**< Tab */
VKM_KEY_Q, /**< \c Q */
VKM_KEY_W, /**< \c W */
VKM_KEY_E, /**< \c E */
VKM_KEY_R, /**< \c R */
VKM_KEY_T, /**< \c T */
VKM_KEY_Y, /**< \c Y */
VKM_KEY_U, /**< \c U */
VKM_KEY_I, /**< \c I */
VKM_KEY_O, /**< \c O */
VKM_KEY_P, /**< \c P */
VKM_KEY_LEFT_BRACKET, /**< Left bracket / brace */
VKM_KEY_RIGHT_BRACKET, /**< Right bracket / brace */
VKM_KEY_BACKSLASH, /**< Backslash / pipe (US placement; JIS Yen) */
VKM_KEY_CAPS_LOCK, /**< Caps Lock */
VKM_KEY_A, /**< \c A */
VKM_KEY_S, /**< \c S */
VKM_KEY_D, /**< \c D */
VKM_KEY_F, /**< \c F */
VKM_KEY_G, /**< \c G */
VKM_KEY_H, /**< \c H */
VKM_KEY_J, /**< \c J */
VKM_KEY_K, /**< \c K */
VKM_KEY_L, /**< \c L */
VKM_KEY_SEMICOLON, /**< Semicolon / colon */
VKM_KEY_APOSTROPHE, /**< Apostrophe / quote */
VKM_KEY_NONUS_HASH, /**< ISO non-US # / ~ (HID usage) */
VKM_KEY_ENTER, /**< Return / Enter */
VKM_KEY_LEFT_SHIFT, /**< Left Shift */
VKM_KEY_INTL_BACKSLASH, /**< ISO extra key (e.g. \| between left Shift and Z) */
VKM_KEY_Z, /**< \c Z */
VKM_KEY_X, /**< \c X */
VKM_KEY_C, /**< \c C */
VKM_KEY_V, /**< \c V */
VKM_KEY_B, /**< \c B */
VKM_KEY_N, /**< \c N */
VKM_KEY_M, /**< \c M */
VKM_KEY_COMMA, /**< Comma / less-than */
VKM_KEY_PERIOD, /**< Period / greater-than */
VKM_KEY_SLASH, /**< Slash / question */
VKM_KEY_RIGHT_SHIFT, /**< Right Shift */
VKM_KEY_SPACE, /**< Space bar */
VKM_KEY_PRINT_SCREEN, /**< Print Screen */
VKM_KEY_SCROLL_LOCK, /**< Scroll Lock */
VKM_KEY_PAUSE, /**< Pause / Break */
VKM_KEY_INSERT, /**< Insert */
VKM_KEY_HOME, /**< Home */
VKM_KEY_PAGE_UP, /**< Page Up */
VKM_KEY_DELETE_FORWARD, /**< Delete (forward) */
VKM_KEY_END, /**< End */
VKM_KEY_PAGE_DOWN, /**< Page Down */
VKM_KEY_RIGHT_ARROW, /**< Arrow right */
VKM_KEY_LEFT_ARROW, /**< Arrow left */
VKM_KEY_DOWN_ARROW, /**< Arrow down */
VKM_KEY_UP_ARROW, /**< Arrow up */
VKM_KEY_KEYPAD_NUM_LOCK, /**< Keypad Num Lock */
VKM_KEY_KEYPAD_DIVIDE, /**< Keypad \c / */
VKM_KEY_KEYPAD_MULTIPLY,/**< Keypad \c * */
VKM_KEY_KEYPAD_MINUS, /**< Keypad \c - */
VKM_KEY_KEYPAD_PLUS, /**< Keypad \c + */
VKM_KEY_KEYPAD_ENTER, /**< Keypad Enter */
VKM_KEY_KEYPAD_1, /**< Keypad \c 1 / End */
VKM_KEY_KEYPAD_2, /**< Keypad \c 2 / Down */
VKM_KEY_KEYPAD_3, /**< Keypad \c 3 / Page Down */
VKM_KEY_KEYPAD_4, /**< Keypad \c 4 / Left */
VKM_KEY_KEYPAD_5, /**< Keypad \c 5 */
VKM_KEY_KEYPAD_6, /**< Keypad \c 6 / Right */
VKM_KEY_KEYPAD_7, /**< Keypad \c 7 / Home */
VKM_KEY_KEYPAD_8, /**< Keypad \c 8 / Up */
VKM_KEY_KEYPAD_9, /**< Keypad \c 9 / Page Up */
VKM_KEY_KEYPAD_0, /**< Keypad \c 0 / Insert */
VKM_KEY_KEYPAD_DECIMAL, /**< Keypad decimal / Delete */
VKM_KEY_KEYPAD_COMMA, /**< Keypad comma (locale-specific layouts) */
VKM_KEY_MAX /**< Past last key; do not use in application code */
} vkm_key;
/**
* @brief Modifier bitmask for \ref vkm_keyboard_send
*
* Left and right modifier keys use the same bit layout as the USB HID keyboard
* modifier byte. Combine values with bitwise OR. These are separate from
* physical modifier key events in \ref vkm_key.
*
* Convenience macros \c VKM_KEY_MOD_CTRL, \c VKM_KEY_MOD_SHIFT, \c VKM_KEY_MOD_ALT,
* and \c VKM_KEY_MOD_GUI alias the left-hand modifiers only; use
* \c VKM_KEY_MOD_L* / \c VKM_KEY_MOD_R* when a specific side is required.
*/
typedef enum {
VKM_KEY_MOD_NONE = 0, /**< No modifiers */
VKM_KEY_MOD_LCTRL = (1 << 0), /**< Left Control (HID modifier byte bit 0) */
VKM_KEY_MOD_LSHIFT = (1 << 1), /**< Left Shift (bit 1) */
VKM_KEY_MOD_LALT = (1 << 2), /**< Left Alt (bit 2) */
VKM_KEY_MOD_LGUI = (1 << 3), /**< Left GUI / Meta (bit 3) */
VKM_KEY_MOD_RCTRL = (1 << 4), /**< Right Control (bit 4) */
VKM_KEY_MOD_RSHIFT = (1 << 5), /**< Right Shift (bit 5) */
VKM_KEY_MOD_RALT = (1 << 6), /**< Right Alt / AltGr (bit 6) */
VKM_KEY_MOD_RGUI = (1 << 7), /**< Right GUI / Meta (bit 7) */
} vkm_key_modifiers;
/** Convenience alias for \ref VKM_KEY_MOD_LCTRL */
#define VKM_KEY_MOD_CTRL VKM_KEY_MOD_LCTRL
/** Convenience alias for \ref VKM_KEY_MOD_LSHIFT */
#define VKM_KEY_MOD_SHIFT VKM_KEY_MOD_LSHIFT
/** Convenience alias for \ref VKM_KEY_MOD_LALT */
#define VKM_KEY_MOD_ALT VKM_KEY_MOD_LALT
/** Convenience alias for \ref VKM_KEY_MOD_LGUI */
#define VKM_KEY_MOD_GUI VKM_KEY_MOD_LGUI
/**
* @brief Physical key action (press or release) for \ref vkm_keyboard_send
*
* Determines the \c value sent with the non-modifier \ref vkm_key (and, on
* release, how the modifier mask is reconciled after the key event).
*/
typedef enum {
VKM_KEY_STATE_RELEASED, /**< Key or button is up */
VKM_KEY_STATE_PRESSED, /**< Key or button is down */
VKM_KEY_STATE_MAX /**< Sentinel; do not use in application code */
} vkm_key_state;
/**
* @brief Initialize the VKM library
*
* This function initializes the VKM library, sets up any internal data
* structures to send input events, and returns a \ref vkm_context pointer
* in the output parameter. All calls to VKM use the returned pointer to
* inject keyboard/mouse events.
*
* @par Example
* @code
* vkm_result rc;
* vkm_context *ctx;
* rc = vkm_init(&ctx);
* if (rc != LIBX52_SUCCESS) {
* // Error handling omitted for brevity
* }
* // Save ctx for use later
* @endcode
*
* @param[out] ctx Pointer to a \ref vkm_context *. This function will
* allocate a context and return the pointer to the context in this variable.
*
* @returns \ref vkm_error_code indicating status
*/
VKM_API vkm_result vkm_init(vkm_context **ctx);
/**
* @brief Exit the VKM library and free up any resources used
*
* This function calls \ref vkm_reset, releases any resources allocated by
* \ref vkm_init, and terminates the library. Using the freed context after
* this returns is invalid and can cause errors.
*
* @param[in] ctx Context pointer
*/
VKM_API void vkm_exit(vkm_context *ctx);
/**
* @brief Release all virtual keys and mouse buttons that are still down
*
* Synthesizes release events for any keys pressed through \ref vkm_keyboard_send,
* clears the logical modifier mask from that API, and releases mouse buttons that
* are still pressed. Internal bookkeeping is cleared even if the virtual device
* is not ready (no events are written until \ref vkm_start succeeds).
*
* \ref vkm_exit calls this automatically before freeing the context.
*
* @param[in] ctx Context pointer
*
* @returns
* - \ref VKM_SUCCESS on success or if there was nothing to release
* - \ref VKM_ERROR_INVALID_PARAM on bad pointer
* - \ref VKM_ERROR_EVENT if writing a release event failed
*/
VKM_API vkm_result vkm_reset(vkm_context *ctx);
/**
* @brief Start any virtual keyboard/mouse devices on the platform
*
* This must be done before injecting any events, and after setting all
* options through \ref vkm_set_option.
*
* @param[in] ctx Context pointer
*
* @returns
* - \ref VKM_SUCCESS on successful start
* - \ref VKM_ERROR_INVALID_PARAM on bad pointer
* - \ref VKM_ERROR_UNKNOWN on other errors
*/
VKM_API vkm_result vkm_start(vkm_context *ctx);
/**
* @brief check if VKM is started and ready
*
* @param[in] ctx Context pointer
*
* @returns boolean indicating if ready or not.
*/
VKM_API bool vkm_is_ready(vkm_context *ctx);
/**
* @brief Check if VKM is supported on this platform
*
* On some platforms, there is no support yet for the virtual keyboard/mouse.
* This function will return a boolean indicating if it is supported or not.
*
* @returns boolean indicating support.
*/
VKM_API bool vkm_platform_supported(void);
/**
* @brief Check if a particular feature is enabled on this platform
*
* Features may be limited on a per-platform basis.
*
* @param[in] feat Feature identifier
*
* @returns boolean indicating if feature is supported or not.
*/
VKM_API bool vkm_feature_supported(vkm_feature feat);
/**
* @brief Set an option flag for VKM.
*
* Option flags control the behavior of VKM. All options must be set before
* calling \ref vkm_start.
*
* @param[in] ctx Context pointer
* @param[in] option Which option to set
* @param[in] ... Any required arguments for the specified option
*
* @returns
* - \ref VKM_SUCCESS on success
* - \ref VKM_ERROR_INVALID_PARAM if the option or arguments are invalid
* - \ref VKM_ERROR_NOT_SUPPORTED if the option is valid but not supported on this platform
*/
VKM_API vkm_result vkm_set_option(vkm_context *ctx, vkm_option option, ...);
/**
* @brief Move the mouse by the specified amount
*
* The move mouse takes in a delta of x and y coordinates that tell the system
* to move the mouse by those relative numbers.
*
* @param[in] ctx Context pointer
* @param[in] dx Delta by which to move the mouse in the horizontal axis
* @param[in] dy Delta by which to move the mouse in the vertical axis
*
* @returns
* - \ref VKM_SUCCESS on successful move
* - \ref VKM_ERROR_UNKNOWN on a generic error
* - \ref VKM_ERROR_NOT_SUPPORTED if the mouse move is not supported on this platform
* - \ref VKM_ERROR_NOT_READY if VKM is not started
*/
VKM_API vkm_result vkm_mouse_move(vkm_context *ctx, int dx, int dy);
/**
* @brief Click the mouse button
*
* Send a mouse button event, this may be either a button down or button up event.
*
* @param[in] ctx Context pointer
* @param[in] button Button identifier
* @param[in] state Button state (press or release)
*
* @returns
* - \ref VKM_SUCCESS on successful move
* - \ref VKM_ERROR_UNKNOWN on a generic error
* - \ref VKM_ERROR_NOT_SUPPORTED if the mouse button click is not supported on this platform
* - \ref VKM_ERROR_NOT_READY if VKM is not started
*/
VKM_API vkm_result vkm_mouse_click(vkm_context *ctx, vkm_mouse_button button, vkm_button_state state);
/**
* @brief Scroll the mouse wheel
*
* Send a single scroll event to the mouse wheel (one detent in the chosen
* direction). If \ref VKM_OPT_HI_RES_SCROLL was enabled before \ref vkm_start,
* also emits REL_WHEEL_HI_RES / REL_HWHEEL_HI_RES using the standard 120
* units per detent scale before the corresponding discrete REL_WHEEL /
* REL_HWHEEL event.
*
* @param[in] ctx Context pointer
* @param[in] dir Scroll direction
*
* @returns
* - \ref VKM_SUCCESS on successful move
* - \ref VKM_ERROR_UNKNOWN on a generic error
* - \ref VKM_ERROR_INVALID_PARAM if horizontal scrolling is not enabled
* and \p dir is \ref VKM_MOUSE_SCROLL_LEFT or \ref VKM_MOUSE_SCROLL_RIGHT
* - \ref VKM_ERROR_NOT_SUPPORTED if the mouse scrolling is not supported on this platform
* - \ref VKM_ERROR_NOT_READY if VKM is not started
*/
VKM_API vkm_result vkm_mouse_scroll(vkm_context *ctx, vkm_mouse_scroll_direction dir);
/**
* @brief Send a single keyboard event
*
* Send a single keyboard event to the OS. This will send a single key event,
* with modifiers enabled (Ctrl, Shift, Alt, GUI).
*
* @param[in] ctx Context pointer
* @param[in] key Key identifier
* @param[in] modifiers Modifier keys to enable (Ctrl, Shift, Alt, GUI)
* @param[in] state Key state (press or release)
*
* @returns
* - \ref VKM_SUCCESS on successful send
* - \ref VKM_ERROR_UNKNOWN on a generic error
* - \ref VKM_ERROR_INVALID_PARAM if parameters are invalid
* - \ref VKM_ERROR_NOT_SUPPORTED if the keyboard event is not enabled or
* supported on this platform
* - \ref VKM_ERROR_NOT_READY if VKM is not started
*/
VKM_API vkm_result vkm_keyboard_send(vkm_context *ctx, vkm_key key, vkm_key_modifiers modifiers, vkm_key_state state);
/**
* @brief Send a sync packet to the OS
*
* On some platforms, a sync packet is necessary for the previously injected
* events to actually get reflected in the system. For platforms where this
* is not needed, this is a noop.
*
* @param[in] ctx Context pointer
*
* @returns
* - \ref VKM_SUCCESS on successful move
* - \ref VKM_ERROR_UNKNOWN on a generic error
* - \ref VKM_ERROR_INVALID_PARAM if parameters are invalid
* - \ref VKM_ERROR_NOT_READY if VKM is not started
*/
VKM_API vkm_result vkm_sync(vkm_context *ctx);
#ifdef __cplusplus
}
#endif
#endif // !defined VKM_H

71
vkm/vkm_common.c 100644
View File

@ -0,0 +1,71 @@
/*
* VKM common functions
*
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
*
* SPDX-LicenseIdentifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include "gettext.h"
#include "vkm-internal.h"
#define N_(str) gettext_noop(str)
#define _(str) dgettext(PACKAGE, str)
/* Error buffer used for building custom error strings */
static char error_buffer[256];
/* List of error strings (indices must match \ref vkm_error_code) */
static const char *error_string[] = {
N_("Success"),
N_("Unknown error"),
N_("Not ready"),
N_("Out of memory"),
N_("Invalid parameter"),
N_("Not supported"),
N_("Virtual device failure"),
N_("Unable to write event"),
N_("No state change"),
};
const char *vkm_strerror(vkm_result code)
{
switch ((vkm_error_code)code) {
case VKM_SUCCESS:
case VKM_ERROR_UNKNOWN:
case VKM_ERROR_NOT_READY:
case VKM_ERROR_OUT_OF_MEMORY:
case VKM_ERROR_INVALID_PARAM:
case VKM_ERROR_NOT_SUPPORTED:
case VKM_ERROR_DEV_FAILURE:
case VKM_ERROR_EVENT:
case VKM_ERROR_NO_CHANGE:
return _(error_string[code]);
default:
snprintf(error_buffer, sizeof(error_buffer),
_("Unknown error %d"), (int)code);
return error_buffer;
}
}
vkm_button_state _vkm_get_mouse_button_state(struct vkm_mouse_button_state *state, vkm_mouse_button button)
{
if (state == NULL) {
return false;
}
return state->pressed[button];
}
void _vkm_set_mouse_button_state(struct vkm_mouse_button_state *sstate, vkm_mouse_button button, vkm_button_state state)
{
if (sstate != NULL) {
sstate->pressed[button] = state;
}
}

View File

@ -0,0 +1,706 @@
/*
* VKM Linux evdev implementation
*
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
*
* SPDX-LicenseIdentifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include <inttypes.h>
#include "libevdev/libevdev.h"
#include "libevdev/libevdev-uinput.h"
#include "vkm-internal.h"
/** evdev codes; index by \ref vkm_key. \c -1 for \ref VKM_KEY_NONE only. */
static const int vkm_key_to_evdev[VKM_KEY_MAX] = {
[VKM_KEY_NONE] = -1,
[VKM_KEY_LEFT_CTRL] = KEY_LEFTCTRL,
[VKM_KEY_LEFT_ALT] = KEY_LEFTALT,
[VKM_KEY_LEFT_GUI] = KEY_LEFTMETA,
[VKM_KEY_RIGHT_CTRL] = KEY_RIGHTCTRL,
[VKM_KEY_RIGHT_ALT] = KEY_RIGHTALT,
[VKM_KEY_RIGHT_GUI] = KEY_RIGHTMETA,
[VKM_KEY_APPLICATION] = KEY_MENU,
[VKM_KEY_ESCAPE] = KEY_ESC,
[VKM_KEY_F1] = KEY_F1,
[VKM_KEY_F2] = KEY_F2,
[VKM_KEY_F3] = KEY_F3,
[VKM_KEY_F4] = KEY_F4,
[VKM_KEY_F5] = KEY_F5,
[VKM_KEY_F6] = KEY_F6,
[VKM_KEY_F7] = KEY_F7,
[VKM_KEY_F8] = KEY_F8,
[VKM_KEY_F9] = KEY_F9,
[VKM_KEY_F10] = KEY_F10,
[VKM_KEY_F11] = KEY_F11,
[VKM_KEY_F12] = KEY_F12,
[VKM_KEY_GRAVE_ACCENT] = KEY_GRAVE,
[VKM_KEY_1] = KEY_1,
[VKM_KEY_2] = KEY_2,
[VKM_KEY_3] = KEY_3,
[VKM_KEY_4] = KEY_4,
[VKM_KEY_5] = KEY_5,
[VKM_KEY_6] = KEY_6,
[VKM_KEY_7] = KEY_7,
[VKM_KEY_8] = KEY_8,
[VKM_KEY_9] = KEY_9,
[VKM_KEY_0] = KEY_0,
[VKM_KEY_MINUS] = KEY_MINUS,
[VKM_KEY_EQUAL] = KEY_EQUAL,
[VKM_KEY_BACKSPACE] = KEY_BACKSPACE,
[VKM_KEY_TAB] = KEY_TAB,
[VKM_KEY_Q] = KEY_Q,
[VKM_KEY_W] = KEY_W,
[VKM_KEY_E] = KEY_E,
[VKM_KEY_R] = KEY_R,
[VKM_KEY_T] = KEY_T,
[VKM_KEY_Y] = KEY_Y,
[VKM_KEY_U] = KEY_U,
[VKM_KEY_I] = KEY_I,
[VKM_KEY_O] = KEY_O,
[VKM_KEY_P] = KEY_P,
[VKM_KEY_LEFT_BRACKET] = KEY_LEFTBRACE,
[VKM_KEY_RIGHT_BRACKET] = KEY_RIGHTBRACE,
[VKM_KEY_BACKSLASH] = KEY_BACKSLASH,
[VKM_KEY_CAPS_LOCK] = KEY_CAPSLOCK,
[VKM_KEY_A] = KEY_A,
[VKM_KEY_S] = KEY_S,
[VKM_KEY_D] = KEY_D,
[VKM_KEY_F] = KEY_F,
[VKM_KEY_G] = KEY_G,
[VKM_KEY_H] = KEY_H,
[VKM_KEY_J] = KEY_J,
[VKM_KEY_K] = KEY_K,
[VKM_KEY_L] = KEY_L,
[VKM_KEY_SEMICOLON] = KEY_SEMICOLON,
[VKM_KEY_APOSTROPHE] = KEY_APOSTROPHE,
[VKM_KEY_NONUS_HASH] = KEY_NUMERIC_POUND,
[VKM_KEY_ENTER] = KEY_ENTER,
[VKM_KEY_LEFT_SHIFT] = KEY_LEFTSHIFT,
[VKM_KEY_INTL_BACKSLASH] = KEY_102ND,
[VKM_KEY_Z] = KEY_Z,
[VKM_KEY_X] = KEY_X,
[VKM_KEY_C] = KEY_C,
[VKM_KEY_V] = KEY_V,
[VKM_KEY_B] = KEY_B,
[VKM_KEY_N] = KEY_N,
[VKM_KEY_M] = KEY_M,
[VKM_KEY_COMMA] = KEY_COMMA,
[VKM_KEY_PERIOD] = KEY_DOT,
[VKM_KEY_SLASH] = KEY_SLASH,
[VKM_KEY_RIGHT_SHIFT] = KEY_RIGHTSHIFT,
[VKM_KEY_SPACE] = KEY_SPACE,
[VKM_KEY_PRINT_SCREEN] = KEY_PRINT,
[VKM_KEY_SCROLL_LOCK] = KEY_SCROLLLOCK,
[VKM_KEY_PAUSE] = KEY_PAUSE,
[VKM_KEY_INSERT] = KEY_INSERT,
[VKM_KEY_HOME] = KEY_HOME,
[VKM_KEY_PAGE_UP] = KEY_PAGEUP,
[VKM_KEY_DELETE_FORWARD] = KEY_DELETE,
[VKM_KEY_END] = KEY_END,
[VKM_KEY_PAGE_DOWN] = KEY_PAGEDOWN,
[VKM_KEY_RIGHT_ARROW] = KEY_RIGHT,
[VKM_KEY_LEFT_ARROW] = KEY_LEFT,
[VKM_KEY_DOWN_ARROW] = KEY_DOWN,
[VKM_KEY_UP_ARROW] = KEY_UP,
[VKM_KEY_KEYPAD_NUM_LOCK] = KEY_NUMLOCK,
[VKM_KEY_KEYPAD_DIVIDE] = KEY_KPSLASH,
[VKM_KEY_KEYPAD_MULTIPLY] = KEY_KPASTERISK,
[VKM_KEY_KEYPAD_MINUS] = KEY_KPMINUS,
[VKM_KEY_KEYPAD_PLUS] = KEY_KPPLUS,
[VKM_KEY_KEYPAD_ENTER] = KEY_KPENTER,
[VKM_KEY_KEYPAD_1] = KEY_KP1,
[VKM_KEY_KEYPAD_2] = KEY_KP2,
[VKM_KEY_KEYPAD_3] = KEY_KP3,
[VKM_KEY_KEYPAD_4] = KEY_KP4,
[VKM_KEY_KEYPAD_5] = KEY_KP5,
[VKM_KEY_KEYPAD_6] = KEY_KP6,
[VKM_KEY_KEYPAD_7] = KEY_KP7,
[VKM_KEY_KEYPAD_8] = KEY_KP8,
[VKM_KEY_KEYPAD_9] = KEY_KP9,
[VKM_KEY_KEYPAD_0] = KEY_KP0,
[VKM_KEY_KEYPAD_DECIMAL] = KEY_KPDOT,
[VKM_KEY_KEYPAD_COMMA] = KEY_KPCOMMA,
};
struct vkm_context {
struct libevdev *dev;
struct libevdev_uinput *uidev;
char *name;
bool mouse_hi_res_scroll;
bool mouse_horizontal_scroll;
/** Last modifier byte emitted (\ref vkm_key_modifiers bits 07). */
uint8_t keyboard_modifier_mask;
/** Per-key depressed state from \ref vkm_keyboard_send (non-\c NONE keys). */
bool keyboard_key_pressed[VKM_KEY_MAX];
// Existing state of buttons
struct vkm_mouse_button_state mouse_buttons;
};
static const int mouse_button_map[VKM_MOUSE_BTN_MAX] = {
[VKM_MOUSE_BTN_LEFT] = BTN_LEFT,
[VKM_MOUSE_BTN_RIGHT] = BTN_RIGHT,
[VKM_MOUSE_BTN_MIDDLE] = BTN_MIDDLE,
};
static const int button_value_map[VKM_BUTTON_STATE_MAX] = {
[VKM_BUTTON_PRESSED] = 1,
[VKM_BUTTON_RELEASED] = 0,
};
/*
* High-resolution scroll uses the same scale as REL_WHEEL_HI_RES in the kernel
* (120 units per click / detent; aligns with libinput and the Windows convention).
*/
#define VKM_SCROLL_HI_RES_PER_DETENT 120
vkm_result vkm_init(vkm_context **ctx)
{
char name_buf[32];
char *name;
vkm_result rc;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
*ctx = calloc(1, sizeof(**ctx));
if (*ctx == NULL) {
return VKM_ERROR_OUT_OF_MEMORY;
}
rc = VKM_SUCCESS;
// Create a default name for the virtual device, this will be replaced when
// the user calls vkm_set_option with the appropriate name field.
snprintf(name_buf, sizeof(name_buf), "VKM virtual device @%08" PRIx64,
(uint64_t)(time(NULL)));
name = strdup(name_buf);
if (name == NULL) {
rc = VKM_ERROR_OUT_OF_MEMORY;
goto error;
}
(*ctx)->name = name;
// Set defaults for the flags
return VKM_SUCCESS;
error:
if (name) {
free(name);
}
free(*ctx);
*ctx = NULL;
return rc;
}
void vkm_exit(vkm_context *ctx)
{
volatile unsigned char *vp;
if (ctx == NULL) {
return;
}
(void)vkm_reset(ctx);
free(ctx->name);
if (ctx->uidev) {
libevdev_uinput_destroy(ctx->uidev);
}
if (ctx->dev) {
libevdev_free(ctx->dev);
}
/* Clear the memory to prevent reuse */
vp = (volatile unsigned char *)ctx;
for (int i = 0; i < sizeof(*ctx); i++) {
vp[i] = (unsigned char)0;
}
free(ctx);
}
// Enable mouse events
static void enable_mouse_events(vkm_context *ctx)
{
libevdev_enable_event_type(ctx->dev, EV_REL);
libevdev_enable_event_code(ctx->dev, EV_REL, REL_X, NULL);
libevdev_enable_event_code(ctx->dev, EV_REL, REL_Y, NULL);
libevdev_enable_event_code(ctx->dev, EV_REL, REL_WHEEL, NULL);
if (ctx->mouse_hi_res_scroll) {
libevdev_enable_event_code(ctx->dev, EV_REL, REL_WHEEL_HI_RES, NULL);
}
if (ctx->mouse_horizontal_scroll) {
libevdev_enable_event_code(ctx->dev, EV_REL, REL_HWHEEL, NULL);
if (ctx->mouse_hi_res_scroll) {
libevdev_enable_event_code(ctx->dev, EV_REL, REL_HWHEEL_HI_RES, NULL);
}
}
libevdev_enable_event_type(ctx->dev, EV_KEY);
libevdev_enable_event_code(ctx->dev, EV_KEY, BTN_LEFT, NULL);
libevdev_enable_event_code(ctx->dev, EV_KEY, BTN_RIGHT, NULL);
libevdev_enable_event_code(ctx->dev, EV_KEY, BTN_MIDDLE, NULL);
}
static void enable_keyboard_events(vkm_context *ctx)
{
vkm_key k;
for (k = (vkm_key)0; k < VKM_KEY_MAX; k++) {
int code = vkm_key_to_evdev[k];
if (code >= 0) {
libevdev_enable_event_code(ctx->dev, EV_KEY, (unsigned int)code, NULL);
}
}
}
static const int vkm_modifier_evdev[8] = {
KEY_LEFTCTRL,
KEY_LEFTSHIFT,
KEY_LEFTALT,
KEY_LEFTMETA,
KEY_RIGHTCTRL,
KEY_RIGHTSHIFT,
KEY_RIGHTALT,
KEY_RIGHTMETA,
};
static vkm_result apply_modifier_mask(vkm_context *ctx, uint32_t want)
{
uint32_t have = ctx->keyboard_modifier_mask;
uint32_t diff = want ^ have;
unsigned bit;
if (diff == 0) {
return VKM_SUCCESS;
}
for (bit = 0; bit < 8; bit++) {
uint32_t b = (uint32_t)1 << bit;
if (diff & b) {
int val = (want & b) ? 1 : 0;
int rc;
rc = libevdev_uinput_write_event(ctx->uidev, EV_KEY, vkm_modifier_evdev[bit], val);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
}
}
ctx->keyboard_modifier_mask = (uint8_t)want;
return VKM_SUCCESS;
}
vkm_result vkm_start(vkm_context *ctx)
{
int rc;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (ctx->uidev) {
// Already initialized, no need to do anything else
return VKM_SUCCESS;
}
if (!(ctx->dev)) {
// This will happen on the first time this function is called, but
// will get cleared up if the uinput device is not created, so this
// is just a safety check.
ctx->dev = libevdev_new();
if (ctx->dev == NULL) {
return VKM_ERROR_DEV_FAILURE;
}
libevdev_set_name(ctx->dev, ctx->name);
enable_mouse_events(ctx);
enable_keyboard_events(ctx);
}
rc = libevdev_uinput_create_from_device(ctx->dev, LIBEVDEV_UINPUT_OPEN_MANAGED,
&(ctx->uidev));
if (rc != 0) {
goto error;
}
return VKM_SUCCESS;
error:
libevdev_free(ctx->dev);
ctx->dev = NULL;
return VKM_ERROR_DEV_FAILURE;
}
bool vkm_is_ready(vkm_context *ctx) {
if (ctx == NULL) {
return false;
}
return (ctx->dev != NULL && ctx->uidev != NULL);
}
bool vkm_platform_supported(void)
{
return true;
}
bool vkm_feature_supported(vkm_feature feat)
{
switch (feat) {
case VKM_FEAT_MOUSE:
return true;
case VKM_FEAT_KEYBOARD:
case VKM_FEAT_KEYBOARD_MODIFIERS:
return true;
default:
return false;
}
}
vkm_result vkm_set_option(vkm_context *ctx, vkm_option option, ...)
{
va_list ap;
bool flag;
char *name;
char *name2;
vkm_result rc;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
va_start(ap, option);
rc = VKM_SUCCESS;
switch (option) {
case VKM_OPT_HI_RES_SCROLL:
// read as int, as bool is promoted to int when passed as a va_arg
flag = va_arg(ap, int);
ctx->mouse_hi_res_scroll = (bool)flag;
break;
case VKM_OPT_HORIZONTAL_SCROLL:
// read as int, as bool is promoted to int when passed as a va_arg
flag = va_arg(ap, int);
ctx->mouse_horizontal_scroll = (bool)flag;
break;
case VKM_OPT_DEVICE_NAME:
name = va_arg(ap, char *);
name2 = strdup(name);
if (name2 == NULL) {
rc = VKM_ERROR_OUT_OF_MEMORY;
} else {
free(ctx->name);
ctx->name = name2;
}
break;
default:
rc = VKM_ERROR_INVALID_PARAM;
break;
}
va_end(ap);
return rc;
}
vkm_result vkm_mouse_move(vkm_context *ctx, int dx, int dy)
{
int rc;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
if (!dx && !dy) {
return VKM_ERROR_NO_CHANGE;
}
if (dx) {
rc = libevdev_uinput_write_event(ctx->uidev, EV_REL, REL_X, dx);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
}
if (dy) {
rc = libevdev_uinput_write_event(ctx->uidev, EV_REL, REL_Y, dy);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
}
return VKM_SUCCESS;
}
vkm_result vkm_mouse_click(vkm_context *ctx, vkm_mouse_button button, vkm_button_state state)
{
int rc;
vkm_button_state existing;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (button >= VKM_MOUSE_BTN_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (state >= VKM_BUTTON_STATE_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
existing = _vkm_get_mouse_button_state(&(ctx->mouse_buttons), button);
if (existing == state) {
return VKM_ERROR_NO_CHANGE;
}
_vkm_set_mouse_button_state(&(ctx->mouse_buttons), button, state);
rc = libevdev_uinput_write_event(ctx->uidev, EV_KEY,
mouse_button_map[button], button_value_map[state]);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
return VKM_SUCCESS;
}
vkm_result vkm_mouse_scroll(vkm_context *ctx, vkm_mouse_scroll_direction dir)
{
int rc;
int sign;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (dir >= VKM_MOUSE_SCROLL_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
if (dir == VKM_MOUSE_SCROLL_LEFT || dir == VKM_MOUSE_SCROLL_RIGHT) {
if (!ctx->mouse_horizontal_scroll) {
return VKM_ERROR_NOT_SUPPORTED;
}
sign = (dir == VKM_MOUSE_SCROLL_LEFT) ? -1 : 1;
if (ctx->mouse_hi_res_scroll) {
rc = libevdev_uinput_write_event(ctx->uidev, EV_REL, REL_HWHEEL_HI_RES,
sign * VKM_SCROLL_HI_RES_PER_DETENT);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
}
rc = libevdev_uinput_write_event(ctx->uidev, EV_REL, REL_HWHEEL, sign);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
return VKM_SUCCESS;
}
sign = (dir == VKM_MOUSE_SCROLL_UP) ? 1 : -1;
if (ctx->mouse_hi_res_scroll) {
rc = libevdev_uinput_write_event(ctx->uidev, EV_REL, REL_WHEEL_HI_RES,
sign * VKM_SCROLL_HI_RES_PER_DETENT);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
}
rc = libevdev_uinput_write_event(ctx->uidev, EV_REL, REL_WHEEL, sign);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
return VKM_SUCCESS;
}
vkm_result vkm_keyboard_send(vkm_context *ctx, vkm_key key, vkm_key_modifiers modifiers,
vkm_key_state state)
{
vkm_result res;
int evcode;
int val;
uint32_t modmask = (uint32_t)modifiers & 0xffu;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (key >= VKM_KEY_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (state >= VKM_KEY_STATE_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
val = (state == VKM_KEY_STATE_PRESSED) ? 1 : 0;
if (state == VKM_KEY_STATE_PRESSED) {
res = apply_modifier_mask(ctx, modmask);
if (res != VKM_SUCCESS) {
return res;
}
if (key == VKM_KEY_NONE) {
return VKM_SUCCESS;
}
evcode = vkm_key_to_evdev[key];
if (evcode < 0) {
return VKM_ERROR_INVALID_PARAM;
}
if (libevdev_uinput_write_event(ctx->uidev, EV_KEY, (unsigned int)evcode, val) != 0) {
return VKM_ERROR_EVENT;
}
ctx->keyboard_key_pressed[key] = true;
} else {
if (key != VKM_KEY_NONE) {
evcode = vkm_key_to_evdev[key];
if (evcode < 0) {
return VKM_ERROR_INVALID_PARAM;
}
if (libevdev_uinput_write_event(ctx->uidev, EV_KEY, (unsigned int)evcode, val) != 0) {
return VKM_ERROR_EVENT;
}
ctx->keyboard_key_pressed[key] = false;
}
res = apply_modifier_mask(ctx, modmask);
if (res != VKM_SUCCESS) {
return res;
}
}
return VKM_SUCCESS;
}
static void keyboard_state_clear_tracking(vkm_context *ctx)
{
vkm_mouse_button b;
memset(ctx->keyboard_key_pressed, 0, sizeof(ctx->keyboard_key_pressed));
ctx->keyboard_modifier_mask = 0;
for (b = 0; b < VKM_MOUSE_BTN_MAX; b++) {
_vkm_set_mouse_button_state(&ctx->mouse_buttons, b, VKM_BUTTON_RELEASED);
}
}
vkm_result vkm_reset(vkm_context *ctx)
{
vkm_key k;
vkm_mouse_button b;
int rc;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
keyboard_state_clear_tracking(ctx);
return VKM_SUCCESS;
}
for (k = (vkm_key)0; k < VKM_KEY_MAX; k++) {
if (ctx->keyboard_key_pressed[k]) {
int code = vkm_key_to_evdev[k];
if (code >= 0) {
rc = libevdev_uinput_write_event(ctx->uidev, EV_KEY, (unsigned int)code, 0);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
}
ctx->keyboard_key_pressed[k] = false;
}
}
rc = apply_modifier_mask(ctx, 0);
if (rc != VKM_SUCCESS) {
return rc;
}
for (b = 0; b < VKM_MOUSE_BTN_MAX; b++) {
if (_vkm_get_mouse_button_state(&ctx->mouse_buttons, b) == VKM_BUTTON_PRESSED) {
rc = libevdev_uinput_write_event(ctx->uidev, EV_KEY,
mouse_button_map[b], button_value_map[VKM_BUTTON_RELEASED]);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
_vkm_set_mouse_button_state(&ctx->mouse_buttons, b, VKM_BUTTON_RELEASED);
}
}
return vkm_sync(ctx);
}
vkm_result vkm_sync(vkm_context *ctx)
{
int rc;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
rc = libevdev_uinput_write_event(ctx->uidev, EV_SYN, SYN_REPORT, 0);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
return VKM_SUCCESS;
}

File diff suppressed because it is too large Load Diff

222
vkm/vkm_stub.c 100644
View File

@ -0,0 +1,222 @@
/*
* VKM stub implementation
*
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
*
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#include <stdlib.h>
#include <stdarg.h>
#include "vkm.h"
struct vkm_context {
bool started;
bool mouse_hi_res_scroll;
bool mouse_horizontal_scroll;
};
vkm_result vkm_init(vkm_context **ctx)
{
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
*ctx = calloc(1, sizeof(**ctx));
if (*ctx == NULL) {
return VKM_ERROR_OUT_OF_MEMORY;
}
return VKM_SUCCESS;
}
vkm_result vkm_reset(vkm_context *ctx)
{
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (vkm_is_ready(ctx)) {
return vkm_sync(ctx);
}
return VKM_SUCCESS;
}
void vkm_exit(vkm_context *ctx)
{
if (ctx == NULL) {
return;
}
(void)vkm_reset(ctx);
free(ctx);
}
vkm_result vkm_start(vkm_context *ctx)
{
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
ctx->started = true;
return VKM_SUCCESS;
}
bool vkm_is_ready(vkm_context *ctx)
{
if (ctx == NULL) {
return false;
}
return ctx->started;
}
bool vkm_platform_supported(void)
{
return false;
}
bool vkm_feature_supported(vkm_feature feat)
{
(void)feat;
return false;
}
vkm_result vkm_set_option(vkm_context *ctx, vkm_option option, ...)
{
va_list ap;
bool flag;
char *name;
vkm_result rc;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
va_start(ap, option);
rc = VKM_SUCCESS;
switch (option) {
case VKM_OPT_HI_RES_SCROLL:
flag = (bool)va_arg(ap, int);
ctx->mouse_hi_res_scroll = flag;
break;
case VKM_OPT_HORIZONTAL_SCROLL:
flag = (bool)va_arg(ap, int);
ctx->mouse_horizontal_scroll = flag;
break;
case VKM_OPT_DEVICE_NAME:
name = va_arg(ap, char *);
(void)name;
rc = VKM_ERROR_NOT_SUPPORTED;
break;
default:
rc = VKM_ERROR_INVALID_PARAM;
break;
}
va_end(ap);
return rc;
}
vkm_result vkm_mouse_move(vkm_context *ctx, int dx, int dy)
{
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
if (dx == 0 && dy == 0) {
return VKM_ERROR_NO_CHANGE;
}
return VKM_ERROR_NOT_SUPPORTED;
}
vkm_result vkm_mouse_click(vkm_context *ctx, vkm_mouse_button button, vkm_button_state state)
{
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (button >= VKM_MOUSE_BTN_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (state >= VKM_BUTTON_STATE_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
return VKM_ERROR_NOT_SUPPORTED;
}
vkm_result vkm_mouse_scroll(vkm_context *ctx, vkm_mouse_scroll_direction dir)
{
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (dir >= VKM_MOUSE_SCROLL_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
if (dir == VKM_MOUSE_SCROLL_LEFT || dir == VKM_MOUSE_SCROLL_RIGHT) {
if (!ctx->mouse_horizontal_scroll) {
return VKM_ERROR_NOT_SUPPORTED;
}
}
return VKM_ERROR_NOT_SUPPORTED;
}
vkm_result vkm_sync(vkm_context *ctx)
{
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
return VKM_SUCCESS;
}
vkm_result vkm_keyboard_send(vkm_context *ctx, vkm_key key, vkm_key_modifiers modifiers,
vkm_key_state state)
{
(void)modifiers;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (key >= VKM_KEY_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (state >= VKM_KEY_STATE_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
return VKM_ERROR_NOT_SUPPORTED;
}