mirror of https://github.com/nirenjan/libx52.git
feat: Add layout parsing and loading
This change adds logic to parse a keyboard layout file, and compile that into a layout structure that can be used later by the profile to map the characters into a key+modifiers structure.profile-support
parent
d8cc745d2d
commit
b87464be80
|
|
@ -0,0 +1,397 @@
|
|||
; US QWERTY keyboard layout for x52d profile keymap resolution
|
||||
;
|
||||
; Scope: map Unicode text (typed characters) to main-keyboard HID usages and
|
||||
; modifiers. This file is not where arrows, Home/End, function keys, or other
|
||||
; non-text keys live—bind those in profile actions using VKM key names (e.g.
|
||||
; UpArrow, Home, F1).
|
||||
;
|
||||
; Numeric keypad: numpad digits use the same character codes as the top row
|
||||
; (0-9). Those entries below emit VKM_KEY_0..9, not VKM_KEY_KEYPAD_* . To send
|
||||
; true keypad events, reference Keypad0..Keypad9 (etc.) from the profile layer.
|
||||
;
|
||||
; Sections: each block (except [Layout]) names one UTF-8 code point, or U+XXXX
|
||||
; when INI cannot express it (e.g. U+005D for ']').
|
||||
|
||||
[Layout]
|
||||
Name=us
|
||||
Description=US QWERTY (HID usage 0x07)
|
||||
|
||||
[ ]
|
||||
Key=Space
|
||||
Mods=
|
||||
|
||||
[a]
|
||||
Key=A
|
||||
Mods=
|
||||
|
||||
[b]
|
||||
Key=B
|
||||
Mods=
|
||||
|
||||
[c]
|
||||
Key=C
|
||||
Mods=
|
||||
|
||||
[d]
|
||||
Key=D
|
||||
Mods=
|
||||
|
||||
[e]
|
||||
Key=E
|
||||
Mods=
|
||||
|
||||
[f]
|
||||
Key=F
|
||||
Mods=
|
||||
|
||||
[g]
|
||||
Key=G
|
||||
Mods=
|
||||
|
||||
[h]
|
||||
Key=H
|
||||
Mods=
|
||||
|
||||
[i]
|
||||
Key=I
|
||||
Mods=
|
||||
|
||||
[j]
|
||||
Key=J
|
||||
Mods=
|
||||
|
||||
[k]
|
||||
Key=K
|
||||
Mods=
|
||||
|
||||
[l]
|
||||
Key=L
|
||||
Mods=
|
||||
|
||||
[m]
|
||||
Key=M
|
||||
Mods=
|
||||
|
||||
[n]
|
||||
Key=N
|
||||
Mods=
|
||||
|
||||
[o]
|
||||
Key=O
|
||||
Mods=
|
||||
|
||||
[p]
|
||||
Key=P
|
||||
Mods=
|
||||
|
||||
[q]
|
||||
Key=Q
|
||||
Mods=
|
||||
|
||||
[r]
|
||||
Key=R
|
||||
Mods=
|
||||
|
||||
[s]
|
||||
Key=S
|
||||
Mods=
|
||||
|
||||
[t]
|
||||
Key=T
|
||||
Mods=
|
||||
|
||||
[u]
|
||||
Key=U
|
||||
Mods=
|
||||
|
||||
[v]
|
||||
Key=V
|
||||
Mods=
|
||||
|
||||
[w]
|
||||
Key=W
|
||||
Mods=
|
||||
|
||||
[x]
|
||||
Key=X
|
||||
Mods=
|
||||
|
||||
[y]
|
||||
Key=Y
|
||||
Mods=
|
||||
|
||||
[z]
|
||||
Key=Z
|
||||
Mods=
|
||||
|
||||
[A]
|
||||
Key=A
|
||||
Mods=Shift
|
||||
|
||||
[B]
|
||||
Key=B
|
||||
Mods=Shift
|
||||
|
||||
[C]
|
||||
Key=C
|
||||
Mods=Shift
|
||||
|
||||
[D]
|
||||
Key=D
|
||||
Mods=Shift
|
||||
|
||||
[E]
|
||||
Key=E
|
||||
Mods=Shift
|
||||
|
||||
[F]
|
||||
Key=F
|
||||
Mods=Shift
|
||||
|
||||
[G]
|
||||
Key=G
|
||||
Mods=Shift
|
||||
|
||||
[H]
|
||||
Key=H
|
||||
Mods=Shift
|
||||
|
||||
[I]
|
||||
Key=I
|
||||
Mods=Shift
|
||||
|
||||
[J]
|
||||
Key=J
|
||||
Mods=Shift
|
||||
|
||||
[K]
|
||||
Key=K
|
||||
Mods=Shift
|
||||
|
||||
[L]
|
||||
Key=L
|
||||
Mods=Shift
|
||||
|
||||
[M]
|
||||
Key=M
|
||||
Mods=Shift
|
||||
|
||||
[N]
|
||||
Key=N
|
||||
Mods=Shift
|
||||
|
||||
[O]
|
||||
Key=O
|
||||
Mods=Shift
|
||||
|
||||
[P]
|
||||
Key=P
|
||||
Mods=Shift
|
||||
|
||||
[Q]
|
||||
Key=Q
|
||||
Mods=Shift
|
||||
|
||||
[R]
|
||||
Key=R
|
||||
Mods=Shift
|
||||
|
||||
[S]
|
||||
Key=S
|
||||
Mods=Shift
|
||||
|
||||
[T]
|
||||
Key=T
|
||||
Mods=Shift
|
||||
|
||||
[U]
|
||||
Key=U
|
||||
Mods=Shift
|
||||
|
||||
[V]
|
||||
Key=V
|
||||
Mods=Shift
|
||||
|
||||
[W]
|
||||
Key=W
|
||||
Mods=Shift
|
||||
|
||||
[X]
|
||||
Key=X
|
||||
Mods=Shift
|
||||
|
||||
[Y]
|
||||
Key=Y
|
||||
Mods=Shift
|
||||
|
||||
[Z]
|
||||
Key=Z
|
||||
Mods=Shift
|
||||
|
||||
[0]
|
||||
Key=0
|
||||
Mods=
|
||||
|
||||
[1]
|
||||
Key=1
|
||||
Mods=
|
||||
|
||||
[2]
|
||||
Key=2
|
||||
Mods=
|
||||
|
||||
[3]
|
||||
Key=3
|
||||
Mods=
|
||||
|
||||
[4]
|
||||
Key=4
|
||||
Mods=
|
||||
|
||||
[5]
|
||||
Key=5
|
||||
Mods=
|
||||
|
||||
[6]
|
||||
Key=6
|
||||
Mods=
|
||||
|
||||
[7]
|
||||
Key=7
|
||||
Mods=
|
||||
|
||||
[8]
|
||||
Key=8
|
||||
Mods=
|
||||
|
||||
[9]
|
||||
Key=9
|
||||
Mods=
|
||||
|
||||
[!]
|
||||
Key=1
|
||||
Mods=Shift
|
||||
|
||||
[@]
|
||||
Key=2
|
||||
Mods=Shift
|
||||
|
||||
[#]
|
||||
Key=3
|
||||
Mods=Shift
|
||||
|
||||
[$]
|
||||
Key=4
|
||||
Mods=Shift
|
||||
|
||||
[%]
|
||||
Key=5
|
||||
Mods=Shift
|
||||
|
||||
[^]
|
||||
Key=6
|
||||
Mods=Shift
|
||||
|
||||
[&]
|
||||
Key=7
|
||||
Mods=Shift
|
||||
|
||||
[*]
|
||||
Key=8
|
||||
Mods=Shift
|
||||
|
||||
[(]
|
||||
Key=9
|
||||
Mods=Shift
|
||||
|
||||
[)]
|
||||
Key=0
|
||||
Mods=Shift
|
||||
|
||||
[-]
|
||||
Key=Minus
|
||||
Mods=
|
||||
|
||||
[=]
|
||||
Key=Equal
|
||||
Mods=
|
||||
|
||||
[[]
|
||||
Key=LeftBracket
|
||||
Mods=
|
||||
|
||||
[U+005D]
|
||||
Key=RightBracket
|
||||
Mods=
|
||||
|
||||
[\]
|
||||
Key=Backslash
|
||||
Mods=
|
||||
|
||||
[;]
|
||||
Key=Semicolon
|
||||
Mods=
|
||||
|
||||
[']
|
||||
Key=Apostrophe
|
||||
Mods=
|
||||
|
||||
[,]
|
||||
Key=Comma
|
||||
Mods=
|
||||
|
||||
[.]
|
||||
Key=Period
|
||||
Mods=
|
||||
|
||||
[/]
|
||||
Key=Slash
|
||||
Mods=
|
||||
|
||||
[`]
|
||||
Key=GraveAccent
|
||||
Mods=
|
||||
|
||||
[_]
|
||||
Key=Minus
|
||||
Mods=Shift
|
||||
|
||||
[+]
|
||||
Key=Equal
|
||||
Mods=Shift
|
||||
|
||||
[{]
|
||||
Key=LeftBracket
|
||||
Mods=Shift
|
||||
|
||||
[}]
|
||||
Key=RightBracket
|
||||
Mods=Shift
|
||||
|
||||
[|]
|
||||
Key=Backslash
|
||||
Mods=Shift
|
||||
|
||||
[:]
|
||||
Key=Semicolon
|
||||
Mods=Shift
|
||||
|
||||
["]
|
||||
Key=Apostrophe
|
||||
Mods=Shift
|
||||
|
||||
[<]
|
||||
Key=Comma
|
||||
Mods=Shift
|
||||
|
||||
[>]
|
||||
Key=Period
|
||||
Mods=Shift
|
||||
|
||||
[?]
|
||||
Key=Slash
|
||||
Mods=Shift
|
||||
|
||||
[~]
|
||||
Key=GraveAccent
|
||||
Mods=Shift
|
||||
|
|
@ -29,6 +29,7 @@ x52d_sources = [
|
|||
'x52d_command.c',
|
||||
'x52d_io.c',
|
||||
'x52d_mouse_handler.c',
|
||||
'x52d_layout.c',
|
||||
]
|
||||
|
||||
dep_threads = dependency('threads')
|
||||
|
|
@ -67,6 +68,13 @@ x52d_mouse_test = executable('x52d-mouse-test', x52d_mouse_test_sources,
|
|||
|
||||
test('x52d-mouse-test', x52d_mouse_test, protocol: 'tap')
|
||||
|
||||
x52d_layout_test = executable('x52d-layout-test', 'x52d_layout_test.c', 'x52d_layout.c',
|
||||
include_directories: includes,
|
||||
dependencies: [dep_pinelog, dep_cmocka, dep_inih, dep_intl])
|
||||
|
||||
test('x52d-layout-test', x52d_layout_test, protocol: 'tap',
|
||||
workdir: meson.current_source_dir())
|
||||
|
||||
# Install service file
|
||||
if dep_systemd.found()
|
||||
systemd_system_unit_dir = get_option('systemd-unit-dir')
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ static void cmd_logging(char *buffer, int *buflen, int argc, char **argv)
|
|||
[X52D_MOD_IO] = "io",
|
||||
[X52D_MOD_LED] = "led",
|
||||
[X52D_MOD_MOUSE] = "mouse",
|
||||
[X52D_MOD_LAYOUT] = "layout",
|
||||
[X52D_MOD_COMMAND] = "command",
|
||||
[X52D_MOD_CLIENT] = "client",
|
||||
[X52D_MOD_NOTIFY] = "notify",
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ enum {
|
|||
X52D_MOD_IO,
|
||||
X52D_MOD_LED,
|
||||
X52D_MOD_MOUSE,
|
||||
X52D_MOD_LAYOUT,
|
||||
X52D_MOD_COMMAND,
|
||||
X52D_MOD_CLIENT,
|
||||
X52D_MOD_NOTIFY,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,676 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver — keyboard layout (.layout) loader
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
/*
|
||||
* INI grammar (UTF-8 text):
|
||||
*
|
||||
* [Layout]
|
||||
* Name=<short id> # e.g. us
|
||||
* Description=<optional>
|
||||
*
|
||||
* [<one UTF-8 code point>] | [U+XXXX] | [u+XXXX]
|
||||
* Key=<vkm_key name>|0xNN # HID usage (page 0x07), hex or symbolic (e.g. A, Enter)
|
||||
* Mods=<modifier list> # optional; empty = none. Tokens: Shift LeftShift RightShift
|
||||
* # Ctrl LeftCtrl RightCtrl Alt LeftAlt RightAlt
|
||||
* # Gui LeftGui RightGui Super Meta
|
||||
* # Separate tokens with ',' or '+' (whitespace trimmed).
|
||||
*
|
||||
* Section names (except Layout) must encode exactly one Unicode scalar value.
|
||||
* Later entries replace earlier ones for the same code point.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_LAYOUT
|
||||
#include "ini.h"
|
||||
#include "pinelog.h"
|
||||
#include "x52d_const.h"
|
||||
#include "x52d_layout.h"
|
||||
|
||||
struct x52d_layout_entry {
|
||||
uint32_t cp;
|
||||
vkm_key key;
|
||||
vkm_key_modifiers mods;
|
||||
};
|
||||
|
||||
struct x52d_layout {
|
||||
char *name;
|
||||
char *description;
|
||||
struct x52d_layout_entry *entries;
|
||||
size_t n_entries;
|
||||
};
|
||||
|
||||
void x52d_layout_free(struct x52d_layout *layout)
|
||||
{
|
||||
if (layout == NULL) {
|
||||
return;
|
||||
}
|
||||
free(layout->name);
|
||||
free(layout->description);
|
||||
free(layout->entries);
|
||||
free(layout);
|
||||
}
|
||||
|
||||
const char *x52d_layout_get_name(const struct x52d_layout *layout)
|
||||
{
|
||||
if (layout == NULL || layout->name == NULL) {
|
||||
return "";
|
||||
}
|
||||
return layout->name;
|
||||
}
|
||||
|
||||
static int cmp_entry_cp(const void *a, const void *b)
|
||||
{
|
||||
const struct x52d_layout_entry *ea = a;
|
||||
const struct x52d_layout_entry *eb = b;
|
||||
if (ea->cp < eb->cp) {
|
||||
return -1;
|
||||
}
|
||||
if (ea->cp > eb->cp) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool x52d_layout_lookup(const struct x52d_layout *layout, uint32_t cp,
|
||||
struct x52d_layout_recipe *out_recipe)
|
||||
{
|
||||
struct x52d_layout_entry key = {.cp = cp};
|
||||
struct x52d_layout_entry *found;
|
||||
|
||||
if (layout == NULL || out_recipe == NULL || layout->entries == NULL ||
|
||||
layout->n_entries == 0U) {
|
||||
return false;
|
||||
}
|
||||
|
||||
found = bsearch(&key, layout->entries, layout->n_entries, sizeof(key), cmp_entry_cp);
|
||||
if (found == NULL) {
|
||||
return false;
|
||||
}
|
||||
out_recipe->key = found->key;
|
||||
out_recipe->mods = found->mods;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int utf8_decode_one(const char *s, uint32_t *out_cp, size_t *out_len)
|
||||
{
|
||||
const unsigned char *u = (const unsigned char *)s;
|
||||
uint32_t cp;
|
||||
size_t n;
|
||||
|
||||
if (s == NULL || out_cp == NULL || out_len == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
if (u[0] < 0x80U) {
|
||||
if (u[0] == 0U) {
|
||||
return EINVAL;
|
||||
}
|
||||
*out_cp = u[0];
|
||||
*out_len = 1U;
|
||||
return 0;
|
||||
}
|
||||
if ((u[0] & 0xE0U) == 0xC0U) {
|
||||
if ((u[0] & 0x1EU) == 0U) {
|
||||
return EINVAL; /* overlong */
|
||||
}
|
||||
if (u[1] == 0U || (u[1] &0xC0U) != 0x80U) {
|
||||
return EINVAL;
|
||||
}
|
||||
cp = (uint32_t)(u[0] & 0x1FU) << 6 | (uint32_t)(u[1] & 0x3FU);
|
||||
n = 2U;
|
||||
} else if ((u[0] & 0xF0U) == 0xE0U) {
|
||||
if (u[1] == 0U || u[2] == 0U) {
|
||||
return EINVAL;
|
||||
}
|
||||
if ((u[1] & 0xC0U) != 0x80U || (u[2] & 0xC0U) != 0x80U) {
|
||||
return EINVAL;
|
||||
}
|
||||
cp = (uint32_t)(u[0] & 0x0FU) << 12 | (uint32_t)(u[1] & 0x3FU) << 6 |
|
||||
(uint32_t)(u[2] & 0x3FU);
|
||||
n = 3U;
|
||||
if (cp < 0x800U) {
|
||||
return EINVAL;
|
||||
}
|
||||
} else if ((u[0] & 0xF8U) == 0xF0U) {
|
||||
if (u[1] == 0U || u[2] == 0U || u[3] == 0U) {
|
||||
return EINVAL;
|
||||
}
|
||||
if ((u[1] & 0xC0U) != 0x80U || (u[2] & 0xC0U) != 0x80U ||
|
||||
(u[3] & 0xC0U) != 0x80U) {
|
||||
return EINVAL;
|
||||
}
|
||||
cp = (uint32_t)(u[0] & 0x07U) << 18 | (uint32_t)(u[1] & 0x3FU) << 12 |
|
||||
(uint32_t)(u[2] & 0x3FU) << 6 | (uint32_t)(u[3] & 0x3FU);
|
||||
n = 4U;
|
||||
if (cp < 0x10000U || cp > 0x10FFFFU) {
|
||||
return EINVAL;
|
||||
}
|
||||
} else {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
*out_cp = cp;
|
||||
*out_len = n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void trim_inplace(char *s)
|
||||
{
|
||||
char *end;
|
||||
size_t i;
|
||||
|
||||
if (s == NULL) {
|
||||
return;
|
||||
}
|
||||
i = 0;
|
||||
while (s[i] != '\0' && isspace((unsigned char)s[i])) {
|
||||
i++;
|
||||
}
|
||||
if (i > 0U) {
|
||||
memmove(s, s + i, strlen(s + i) + 1U);
|
||||
}
|
||||
end = s + strlen(s);
|
||||
while (end > s && isspace((unsigned char)end[-1])) {
|
||||
end--;
|
||||
*end = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static const struct vkm_key_name {
|
||||
const char *name;
|
||||
vkm_key key;
|
||||
} vkm_key_names[] = {
|
||||
{"0", VKM_KEY_0}, {"1", VKM_KEY_1},
|
||||
{"2", VKM_KEY_2}, {"3", VKM_KEY_3},
|
||||
{"4", VKM_KEY_4}, {"5", VKM_KEY_5},
|
||||
{"6", VKM_KEY_6}, {"7", VKM_KEY_7},
|
||||
{"8", VKM_KEY_8}, {"9", VKM_KEY_9},
|
||||
{"A", VKM_KEY_A}, {"Application", VKM_KEY_APPLICATION},
|
||||
{"Apostrophe", VKM_KEY_APOSTROPHE},
|
||||
{"B", VKM_KEY_B}, {"Backslash", VKM_KEY_BACKSLASH},
|
||||
{"Backspace", VKM_KEY_BACKSPACE},
|
||||
{"C", VKM_KEY_C}, {"CapsLock", VKM_KEY_CAPS_LOCK},
|
||||
{"Comma", VKM_KEY_COMMA}, {"D", VKM_KEY_D},
|
||||
{"Delete", VKM_KEY_DELETE_FORWARD},
|
||||
{"DownArrow", VKM_KEY_DOWN_ARROW},
|
||||
{"E", VKM_KEY_E}, {"End", VKM_KEY_END},
|
||||
{"Enter", VKM_KEY_ENTER}, {"Equal", VKM_KEY_EQUAL},
|
||||
{"Escape", VKM_KEY_ESCAPE}, {"F", VKM_KEY_F},
|
||||
{"F1", VKM_KEY_F1}, {"F10", VKM_KEY_F10},
|
||||
{"F11", VKM_KEY_F11}, {"F12", VKM_KEY_F12},
|
||||
{"F2", VKM_KEY_F2}, {"F3", VKM_KEY_F3},
|
||||
{"F4", VKM_KEY_F4}, {"F5", VKM_KEY_F5},
|
||||
{"F6", VKM_KEY_F6}, {"F7", VKM_KEY_F7},
|
||||
{"F8", VKM_KEY_F8}, {"F9", VKM_KEY_F9},
|
||||
{"G", VKM_KEY_G}, {"GraveAccent", VKM_KEY_GRAVE_ACCENT},
|
||||
{"H", VKM_KEY_H}, {"Home", VKM_KEY_HOME},
|
||||
{"I", VKM_KEY_I}, {"Insert", VKM_KEY_INSERT},
|
||||
{"IntlBackslash", VKM_KEY_INTL_BACKSLASH},
|
||||
{"J", VKM_KEY_J}, {"K", VKM_KEY_K},
|
||||
{"L", VKM_KEY_L}, {"LeftAlt", VKM_KEY_LEFT_ALT},
|
||||
{"LeftArrow", VKM_KEY_LEFT_ARROW},
|
||||
{"LeftBracket", VKM_KEY_LEFT_BRACKET},
|
||||
{"LeftCtrl", VKM_KEY_LEFT_CTRL},
|
||||
{"LeftGui", VKM_KEY_LEFT_GUI},
|
||||
{"LeftShift", VKM_KEY_LEFT_SHIFT},
|
||||
{"M", VKM_KEY_M}, {"Minus", VKM_KEY_MINUS},
|
||||
{"N", VKM_KEY_N}, {"NonUsHash", VKM_KEY_NONUS_HASH},
|
||||
{"O", VKM_KEY_O}, {"P", VKM_KEY_P},
|
||||
{"PageDown", VKM_KEY_PAGE_DOWN},
|
||||
{"PageUp", VKM_KEY_PAGE_UP}, {"Pause", VKM_KEY_PAUSE},
|
||||
{"Period", VKM_KEY_PERIOD}, {"PrintScreen", VKM_KEY_PRINT_SCREEN},
|
||||
{"Q", VKM_KEY_Q}, {"R", VKM_KEY_R},
|
||||
{"Return", VKM_KEY_ENTER}, {"RightAlt", VKM_KEY_RIGHT_ALT},
|
||||
{"RightArrow", VKM_KEY_RIGHT_ARROW},
|
||||
{"RightBracket", VKM_KEY_RIGHT_BRACKET},
|
||||
{"RightCtrl", VKM_KEY_RIGHT_CTRL},
|
||||
{"RightGui", VKM_KEY_RIGHT_GUI},
|
||||
{"RightShift", VKM_KEY_RIGHT_SHIFT},
|
||||
{"S", VKM_KEY_S}, {"ScrollLock", VKM_KEY_SCROLL_LOCK},
|
||||
{"Semicolon", VKM_KEY_SEMICOLON},
|
||||
{"Slash", VKM_KEY_SLASH}, {"Space", VKM_KEY_SPACE},
|
||||
{"T", VKM_KEY_T}, {"Tab", VKM_KEY_TAB},
|
||||
{"U", VKM_KEY_U}, {"UpArrow", VKM_KEY_UP_ARROW},
|
||||
{"V", VKM_KEY_V}, {"W", VKM_KEY_W},
|
||||
{"X", VKM_KEY_X}, {"Y", VKM_KEY_Y},
|
||||
{"Z", VKM_KEY_Z},
|
||||
{"Keypad0", VKM_KEY_KEYPAD_0},
|
||||
{"Keypad1", VKM_KEY_KEYPAD_1},
|
||||
{"Keypad2", VKM_KEY_KEYPAD_2},
|
||||
{"Keypad3", VKM_KEY_KEYPAD_3},
|
||||
{"Keypad4", VKM_KEY_KEYPAD_4},
|
||||
{"Keypad5", VKM_KEY_KEYPAD_5},
|
||||
{"Keypad6", VKM_KEY_KEYPAD_6},
|
||||
{"Keypad7", VKM_KEY_KEYPAD_7},
|
||||
{"Keypad8", VKM_KEY_KEYPAD_8},
|
||||
{"Keypad9", VKM_KEY_KEYPAD_9},
|
||||
{"KeypadComma", VKM_KEY_KEYPAD_COMMA},
|
||||
{"KeypadDecimal", VKM_KEY_KEYPAD_DECIMAL},
|
||||
{"KeypadDivide", VKM_KEY_KEYPAD_DIVIDE},
|
||||
{"KeypadEnter", VKM_KEY_KEYPAD_ENTER},
|
||||
{"KeypadMinus", VKM_KEY_KEYPAD_MINUS},
|
||||
{"KeypadMultiply", VKM_KEY_KEYPAD_MULTIPLY},
|
||||
{"KeypadNumLock", VKM_KEY_KEYPAD_NUM_LOCK},
|
||||
{"KeypadPlus", VKM_KEY_KEYPAD_PLUS},
|
||||
};
|
||||
|
||||
static int parse_key_token(const char *value, vkm_key *out_key)
|
||||
{
|
||||
char tmp[96];
|
||||
char *endptr;
|
||||
unsigned long u;
|
||||
size_t i;
|
||||
size_t n;
|
||||
|
||||
if (value == NULL || out_key == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
if (strlen(value) >= sizeof(tmp)) {
|
||||
return EINVAL;
|
||||
}
|
||||
memcpy(tmp, value, strlen(value) + 1U);
|
||||
trim_inplace(tmp);
|
||||
|
||||
if (tmp[0] == '0' && (tmp[1] == 'x' || tmp[1] == 'X')) {
|
||||
errno = 0;
|
||||
u = strtoul(tmp, &endptr, 16);
|
||||
if (errno != 0 || endptr == tmp) {
|
||||
return EINVAL;
|
||||
}
|
||||
while (*endptr != '\0' && isspace((unsigned char)*endptr)) {
|
||||
endptr++;
|
||||
}
|
||||
if (*endptr != '\0' || u >= (unsigned long)VKM_KEY_MAX) {
|
||||
return EINVAL;
|
||||
}
|
||||
*out_key = (vkm_key)u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
n = sizeof(vkm_key_names) / sizeof(vkm_key_names[0]);
|
||||
for (i = 0; i < n; i++) {
|
||||
if (!strcmp(tmp, vkm_key_names[i].name)) {
|
||||
*out_key = vkm_key_names[i].key;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
static int token_to_modifier(const char *tok, vkm_key_modifiers *acc)
|
||||
{
|
||||
if (!strcasecmp(tok, "Shift") || !strcasecmp(tok, "LeftShift")) {
|
||||
*acc = (vkm_key_modifiers)(*acc | VKM_KEY_MOD_SHIFT);
|
||||
} else if (!strcasecmp(tok, "RightShift")) {
|
||||
*acc = (vkm_key_modifiers)(*acc | VKM_KEY_MOD_RSHIFT);
|
||||
} else if (!strcasecmp(tok, "Ctrl") || !strcasecmp(tok, "Control") ||
|
||||
!strcasecmp(tok, "LeftCtrl")) {
|
||||
*acc = (vkm_key_modifiers)(*acc | VKM_KEY_MOD_CTRL);
|
||||
} else if (!strcasecmp(tok, "RightCtrl")) {
|
||||
*acc = (vkm_key_modifiers)(*acc | VKM_KEY_MOD_RCTRL);
|
||||
} else if (!strcasecmp(tok, "Alt") || !strcasecmp(tok, "LeftAlt")) {
|
||||
*acc = (vkm_key_modifiers)(*acc | VKM_KEY_MOD_ALT);
|
||||
} else if (!strcasecmp(tok, "RightAlt")) {
|
||||
*acc = (vkm_key_modifiers)(*acc | VKM_KEY_MOD_RALT);
|
||||
} else if (!strcasecmp(tok, "Gui") || !strcasecmp(tok, "LeftGui") ||
|
||||
!strcasecmp(tok, "Super") || !strcasecmp(tok, "Meta")) {
|
||||
*acc = (vkm_key_modifiers)(*acc | VKM_KEY_MOD_GUI);
|
||||
} else if (!strcasecmp(tok, "RightGui")) {
|
||||
*acc = (vkm_key_modifiers)(*acc | VKM_KEY_MOD_RGUI);
|
||||
} else if (tok[0] == '\0') {
|
||||
return 0;
|
||||
} else {
|
||||
return EINVAL;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_modifiers(const char *value, vkm_key_modifiers *out_mods)
|
||||
{
|
||||
char buf[128];
|
||||
char *p;
|
||||
char *saveptr = NULL;
|
||||
char *tok;
|
||||
|
||||
if (value == NULL || out_mods == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
*out_mods = VKM_KEY_MOD_NONE;
|
||||
if (value[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
if (strlen(value) >= sizeof(buf)) {
|
||||
return EINVAL;
|
||||
}
|
||||
memcpy(buf, value, strlen(value) + 1U);
|
||||
trim_inplace(buf);
|
||||
|
||||
/* Split on comma first */
|
||||
for (p = buf;; p = NULL) {
|
||||
char *seg = strtok_r(p, ",", &saveptr);
|
||||
if (seg == NULL) {
|
||||
break;
|
||||
}
|
||||
trim_inplace(seg);
|
||||
if (seg[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
/* Then '+' within each segment */
|
||||
char *subsave = NULL;
|
||||
char *q;
|
||||
for (q = seg;; q = NULL) {
|
||||
tok = strtok_r(q, "+", &subsave);
|
||||
if (tok == NULL) {
|
||||
break;
|
||||
}
|
||||
trim_inplace(tok);
|
||||
if (token_to_modifier(tok, out_mods) != 0) {
|
||||
return EINVAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int layout_entry_replace_or_append(struct x52d_layout *layout, uint32_t cp, vkm_key key,
|
||||
vkm_key_modifiers mods)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < layout->n_entries; i++) {
|
||||
if (layout->entries[i].cp == cp) {
|
||||
layout->entries[i].key = key;
|
||||
layout->entries[i].mods = mods;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
{
|
||||
struct x52d_layout_entry *ne =
|
||||
realloc(layout->entries, (layout->n_entries + 1U) * sizeof(*ne));
|
||||
if (ne == NULL) {
|
||||
return ENOMEM;
|
||||
}
|
||||
layout->entries = ne;
|
||||
layout->entries[layout->n_entries].cp = cp;
|
||||
layout->entries[layout->n_entries].key = key;
|
||||
layout->entries[layout->n_entries].mods = mods;
|
||||
layout->n_entries++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct layout_parse_ctx {
|
||||
struct x52d_layout *layout;
|
||||
char current_section[128];
|
||||
bool in_layout_meta;
|
||||
bool mapping_have_key;
|
||||
uint32_t mapping_cp;
|
||||
vkm_key mapping_key;
|
||||
vkm_key_modifiers mapping_mods;
|
||||
int error;
|
||||
};
|
||||
|
||||
static void layout_flush_mapping(struct layout_parse_ctx *ctx)
|
||||
{
|
||||
int rc;
|
||||
|
||||
if (!ctx->in_layout_meta && ctx->mapping_have_key && ctx->error == 0) {
|
||||
rc = layout_entry_replace_or_append(ctx->layout, ctx->mapping_cp, ctx->mapping_key,
|
||||
ctx->mapping_mods);
|
||||
if (rc != 0) {
|
||||
ctx->error = rc;
|
||||
}
|
||||
}
|
||||
ctx->mapping_have_key = false;
|
||||
ctx->mapping_mods = VKM_KEY_MOD_NONE;
|
||||
}
|
||||
|
||||
static int layout_begin_section(struct layout_parse_ctx *ctx, const char *section)
|
||||
{
|
||||
uint32_t cp;
|
||||
size_t consumed;
|
||||
int rc;
|
||||
size_t slen;
|
||||
|
||||
if (section == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
if (strlen(section) >= sizeof(ctx->current_section)) {
|
||||
PINELOG_ERROR(_("Layout section name too long"));
|
||||
return EINVAL;
|
||||
}
|
||||
memcpy(ctx->current_section, section, strlen(section) + 1U);
|
||||
|
||||
if (!strcasecmp(section, "Layout")) {
|
||||
ctx->in_layout_meta = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
ctx->in_layout_meta = false;
|
||||
|
||||
/* INI section names cannot express ']' as a lone code point (e.g. "[]]" is
|
||||
* parsed as an empty section). Allow U+ABCD or u+ABCD (1-6 hex digits). */
|
||||
if (section[0] == 'U' || section[0] == 'u') {
|
||||
if (section[1] == '+' && section[2] != '\0') {
|
||||
const char *p = section + 2;
|
||||
unsigned long uh = 0;
|
||||
char *endp = NULL;
|
||||
|
||||
errno = 0;
|
||||
uh = strtoul(p, &endp, 16);
|
||||
if (errno == 0 && endp != p && *endp == '\0' && uh <= 0x10FFFFUL &&
|
||||
(uh < 0xD800UL || uh > 0xDFFFUL)) {
|
||||
ctx->mapping_cp = (uint32_t)uh;
|
||||
ctx->mapping_mods = VKM_KEY_MOD_NONE;
|
||||
ctx->mapping_have_key = false;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rc = utf8_decode_one(section, &cp, &consumed);
|
||||
if (rc != 0) {
|
||||
PINELOG_ERROR(_("Invalid UTF-8 in layout section '%s'"), section);
|
||||
return EINVAL;
|
||||
}
|
||||
slen = strlen(section);
|
||||
if (consumed != slen) {
|
||||
PINELOG_ERROR(_("Layout section must encode exactly one code point: '%s'"), section);
|
||||
return EINVAL;
|
||||
}
|
||||
ctx->mapping_cp = cp;
|
||||
ctx->mapping_mods = VKM_KEY_MOD_NONE;
|
||||
ctx->mapping_have_key = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int layout_ini_handler(void *user, const char *section, const char *name, const char *value)
|
||||
{
|
||||
struct layout_parse_ctx *ctx = user;
|
||||
int rc;
|
||||
vkm_key pk;
|
||||
vkm_key_modifiers pm;
|
||||
|
||||
if (ctx->error != 0) {
|
||||
return 0;
|
||||
}
|
||||
if (section == NULL || name == NULL || value == NULL) {
|
||||
ctx->error = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(section, ctx->current_section) != 0) {
|
||||
layout_flush_mapping(ctx);
|
||||
rc = layout_begin_section(ctx, section);
|
||||
if (rc != 0) {
|
||||
ctx->error = rc;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx->in_layout_meta) {
|
||||
if (!strcasecmp(name, "Name")) {
|
||||
free(ctx->layout->name);
|
||||
ctx->layout->name = strdup(value);
|
||||
if (ctx->layout->name == NULL) {
|
||||
ctx->error = ENOMEM;
|
||||
}
|
||||
} else if (!strcasecmp(name, "Description")) {
|
||||
free(ctx->layout->description);
|
||||
ctx->layout->description = strdup(value);
|
||||
if (ctx->layout->description == NULL) {
|
||||
ctx->error = ENOMEM;
|
||||
}
|
||||
}
|
||||
return ctx->error != 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
if (!strcasecmp(name, "Key")) {
|
||||
rc = parse_key_token(value, &pk);
|
||||
if (rc != 0) {
|
||||
PINELOG_ERROR(_("Unknown Key value '%s'"), value);
|
||||
ctx->error = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
ctx->mapping_key = pk;
|
||||
ctx->mapping_have_key = true;
|
||||
} else if (!strcasecmp(name, "Mods")) {
|
||||
rc = parse_modifiers(value, &pm);
|
||||
if (rc != 0) {
|
||||
PINELOG_ERROR(_("Invalid Mods value '%s'"), value);
|
||||
ctx->error = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
ctx->mapping_mods = pm;
|
||||
}
|
||||
|
||||
/* unknown keys ignored for forward-compatible layout files */
|
||||
return ctx->error != 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
static int layout_finalize(struct layout_parse_ctx *ctx)
|
||||
{
|
||||
layout_flush_mapping(ctx);
|
||||
if (ctx->error != 0) {
|
||||
return ctx->error;
|
||||
}
|
||||
if (ctx->layout->n_entries > 0U) {
|
||||
qsort(ctx->layout->entries, ctx->layout->n_entries, sizeof(ctx->layout->entries[0]),
|
||||
cmp_entry_cp);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static struct x52d_layout *layout_new(void)
|
||||
{
|
||||
struct x52d_layout *layout = calloc(1, sizeof(*layout));
|
||||
return layout;
|
||||
}
|
||||
|
||||
int x52d_layout_load_buffer(struct x52d_layout **out, const char *data, size_t len)
|
||||
{
|
||||
struct layout_parse_ctx ctx;
|
||||
char *nulbuf;
|
||||
int ir;
|
||||
int rc;
|
||||
|
||||
if (out == NULL || data == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
*out = NULL;
|
||||
|
||||
ctx.layout = layout_new();
|
||||
if (ctx.layout == NULL) {
|
||||
return ENOMEM;
|
||||
}
|
||||
ctx.current_section[0] = '\0';
|
||||
ctx.in_layout_meta = false;
|
||||
ctx.mapping_have_key = false;
|
||||
ctx.mapping_cp = 0;
|
||||
ctx.mapping_key = VKM_KEY_NONE;
|
||||
ctx.mapping_mods = VKM_KEY_MOD_NONE;
|
||||
ctx.error = 0;
|
||||
|
||||
nulbuf = malloc(len + 1U);
|
||||
if (nulbuf == NULL) {
|
||||
x52d_layout_free(ctx.layout);
|
||||
return ENOMEM;
|
||||
}
|
||||
memcpy(nulbuf, data, len);
|
||||
nulbuf[len] = '\0';
|
||||
ir = ini_parse_string(nulbuf, layout_ini_handler, &ctx);
|
||||
free(nulbuf);
|
||||
|
||||
if (ir < 0) {
|
||||
x52d_layout_free(ctx.layout);
|
||||
return EIO;
|
||||
}
|
||||
if (ir > 0) {
|
||||
PINELOG_ERROR(_("Layout parse error at line %d"), ir);
|
||||
x52d_layout_free(ctx.layout);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
rc = layout_finalize(&ctx);
|
||||
if (rc != 0) {
|
||||
x52d_layout_free(ctx.layout);
|
||||
return rc;
|
||||
}
|
||||
|
||||
*out = ctx.layout;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int x52d_layout_load_file(struct x52d_layout **out, const char *path)
|
||||
{
|
||||
struct layout_parse_ctx ctx;
|
||||
int ir;
|
||||
int rc;
|
||||
|
||||
if (out == NULL || path == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
*out = NULL;
|
||||
|
||||
ctx.layout = layout_new();
|
||||
if (ctx.layout == NULL) {
|
||||
return ENOMEM;
|
||||
}
|
||||
ctx.current_section[0] = '\0';
|
||||
ctx.in_layout_meta = false;
|
||||
ctx.mapping_have_key = false;
|
||||
ctx.mapping_cp = 0;
|
||||
ctx.mapping_key = VKM_KEY_NONE;
|
||||
ctx.mapping_mods = VKM_KEY_MOD_NONE;
|
||||
ctx.error = 0;
|
||||
|
||||
ir = ini_parse(path, layout_ini_handler, &ctx);
|
||||
if (ir < 0) {
|
||||
x52d_layout_free(ctx.layout);
|
||||
return EIO;
|
||||
}
|
||||
if (ir > 0) {
|
||||
PINELOG_ERROR(_("Layout parse error in %s at line %d"), path, ir);
|
||||
x52d_layout_free(ctx.layout);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
rc = layout_finalize(&ctx);
|
||||
if (rc != 0) {
|
||||
x52d_layout_free(ctx.layout);
|
||||
return rc;
|
||||
}
|
||||
|
||||
*out = ctx.layout;
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver — keyboard layout (.layout) API
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef X52D_LAYOUT_H
|
||||
#define X52D_LAYOUT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "vkm.h"
|
||||
|
||||
/** Loaded keyboard layout: maps Unicode code points to HID key + modifier byte */
|
||||
struct x52d_layout;
|
||||
|
||||
/** One keystroke recipe for \ref vkm_keyboard_send */
|
||||
struct x52d_layout_recipe {
|
||||
vkm_key key;
|
||||
vkm_key_modifiers mods;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Load a layout from an INI file (see x52d_layout.c header for grammar).
|
||||
*
|
||||
* @param[out] out Receives the new layout; must be released with
|
||||
* \ref x52d_layout_free. Undefined on failure.
|
||||
* @param[in] path Filesystem path
|
||||
*
|
||||
* @returns 0 on success, EINVAL on parse/validation error, ENOMEM, or EIO
|
||||
*/
|
||||
int x52d_layout_load_file(struct x52d_layout **out, const char *path);
|
||||
|
||||
/**
|
||||
* @brief Load a layout from a memory buffer (NUL-terminated \p data is optional;
|
||||
* length is given explicitly).
|
||||
*/
|
||||
int x52d_layout_load_buffer(struct x52d_layout **out, const char *data, size_t len);
|
||||
|
||||
void x52d_layout_free(struct x52d_layout *layout);
|
||||
|
||||
const char *x52d_layout_get_name(const struct x52d_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief Look up the recipe for a Unicode code point.
|
||||
*
|
||||
* @returns true if found
|
||||
*/
|
||||
bool x52d_layout_lookup(const struct x52d_layout *layout, uint32_t cp,
|
||||
struct x52d_layout_recipe *out_recipe);
|
||||
|
||||
#endif /* X52D_LAYOUT_H */
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver — keyboard layout loader tests
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_LAYOUT
|
||||
#include "pinelog.h"
|
||||
#include "x52d_const.h"
|
||||
#include "x52d_layout.h"
|
||||
|
||||
static void test_sample_layout_lower_a(void **state)
|
||||
{
|
||||
static const char ini[] =
|
||||
"[Layout]\n"
|
||||
"Name=test\n"
|
||||
"[a]\n"
|
||||
"Key=A\n"
|
||||
"Mods=\n";
|
||||
struct x52d_layout *layout = NULL;
|
||||
struct x52d_layout_recipe r;
|
||||
int rc;
|
||||
|
||||
(void)state;
|
||||
rc = x52d_layout_load_buffer(&layout, ini, strlen(ini));
|
||||
assert_int_equal(rc, 0);
|
||||
assert_non_null(layout);
|
||||
assert_string_equal(x52d_layout_get_name(layout), "test");
|
||||
assert_true(x52d_layout_lookup(layout, (uint32_t)'a', &r));
|
||||
assert_int_equal((int)r.key, (int)VKM_KEY_A);
|
||||
assert_int_equal((int)r.mods, (int)VKM_KEY_MOD_NONE);
|
||||
x52d_layout_free(layout);
|
||||
}
|
||||
|
||||
static void test_hex_key(void **state)
|
||||
{
|
||||
static const char ini[] =
|
||||
"[Layout]\n"
|
||||
"Name=h\n"
|
||||
"[x]\n"
|
||||
"Key=0x04\n"
|
||||
"Mods=\n";
|
||||
struct x52d_layout *layout = NULL;
|
||||
struct x52d_layout_recipe r;
|
||||
int rc;
|
||||
|
||||
(void)state;
|
||||
rc = x52d_layout_load_buffer(&layout, ini, strlen(ini));
|
||||
assert_int_equal(rc, 0);
|
||||
assert_true(x52d_layout_lookup(layout, (uint32_t)'x', &r));
|
||||
assert_int_equal((int)r.key, (int)VKM_KEY_A);
|
||||
x52d_layout_free(layout);
|
||||
}
|
||||
|
||||
static void test_modifiers_combo(void **state)
|
||||
{
|
||||
static const char ini[] =
|
||||
"[Layout]\n"
|
||||
"Name=m\n"
|
||||
"[t]\n"
|
||||
"Key=T\n"
|
||||
"Mods=Ctrl+Shift\n";
|
||||
struct x52d_layout *layout = NULL;
|
||||
struct x52d_layout_recipe r;
|
||||
int rc;
|
||||
|
||||
(void)state;
|
||||
rc = x52d_layout_load_buffer(&layout, ini, strlen(ini));
|
||||
assert_int_equal(rc, 0);
|
||||
assert_true(x52d_layout_lookup(layout, (uint32_t)'t', &r));
|
||||
assert_int_equal((int)r.key, (int)VKM_KEY_T);
|
||||
assert_int_equal((int)r.mods,
|
||||
(int)(VKM_KEY_MOD_LCTRL | VKM_KEY_MOD_LSHIFT));
|
||||
x52d_layout_free(layout);
|
||||
}
|
||||
|
||||
static void test_utf8_codepoint(void **state)
|
||||
{
|
||||
static const char ini[] =
|
||||
"[Layout]\n"
|
||||
"Name=u\n"
|
||||
"[\xE2\x82\xAC]\n"
|
||||
"Key=Enter\n"
|
||||
"Mods=\n";
|
||||
struct x52d_layout *layout = NULL;
|
||||
struct x52d_layout_recipe r;
|
||||
int rc;
|
||||
|
||||
(void)state;
|
||||
rc = x52d_layout_load_buffer(&layout, ini, strlen(ini));
|
||||
assert_int_equal(rc, 0);
|
||||
assert_true(x52d_layout_lookup(layout, 0x20ACU, &r));
|
||||
assert_int_equal((int)r.key, (int)VKM_KEY_ENTER);
|
||||
x52d_layout_free(layout);
|
||||
}
|
||||
|
||||
static void test_unknown_key_errors(void **state)
|
||||
{
|
||||
static const char ini[] =
|
||||
"[Layout]\n"
|
||||
"Name=bad\n"
|
||||
"[q]\n"
|
||||
"Key=NotARealKey\n"
|
||||
"Mods=\n";
|
||||
struct x52d_layout *layout = NULL;
|
||||
int rc;
|
||||
|
||||
(void)state;
|
||||
rc = x52d_layout_load_buffer(&layout, ini, strlen(ini));
|
||||
assert_int_equal(rc, EINVAL);
|
||||
assert_null(layout);
|
||||
}
|
||||
|
||||
static void test_collision_last_section_wins(void **state)
|
||||
{
|
||||
static const char ini[] =
|
||||
"[Layout]\n"
|
||||
"Name=c\n"
|
||||
"[z]\n"
|
||||
"Key=A\n"
|
||||
"Mods=\n"
|
||||
"[z]\n"
|
||||
"Key=B\n"
|
||||
"Mods=\n";
|
||||
struct x52d_layout *layout = NULL;
|
||||
struct x52d_layout_recipe r;
|
||||
int rc;
|
||||
|
||||
(void)state;
|
||||
rc = x52d_layout_load_buffer(&layout, ini, strlen(ini));
|
||||
assert_int_equal(rc, 0);
|
||||
assert_true(x52d_layout_lookup(layout, (uint32_t)'z', &r));
|
||||
assert_int_equal((int)r.key, (int)VKM_KEY_B);
|
||||
x52d_layout_free(layout);
|
||||
}
|
||||
|
||||
static void test_unknown_modifier_errors(void **state)
|
||||
{
|
||||
static const char ini[] =
|
||||
"[Layout]\n"
|
||||
"Name=badmod\n"
|
||||
"[q]\n"
|
||||
"Key=A\n"
|
||||
"Mods=HyperMeta\n";
|
||||
struct x52d_layout *layout = NULL;
|
||||
int rc;
|
||||
|
||||
(void)state;
|
||||
rc = x52d_layout_load_buffer(&layout, ini, strlen(ini));
|
||||
assert_int_equal(rc, EINVAL);
|
||||
assert_null(layout);
|
||||
}
|
||||
|
||||
static void test_lookup_miss(void **state)
|
||||
{
|
||||
static const char ini[] =
|
||||
"[Layout]\n"
|
||||
"Name=m\n"
|
||||
"[a]\n"
|
||||
"Key=A\n"
|
||||
"Mods=\n";
|
||||
struct x52d_layout *layout = NULL;
|
||||
struct x52d_layout_recipe r;
|
||||
int rc;
|
||||
|
||||
(void)state;
|
||||
rc = x52d_layout_load_buffer(&layout, ini, strlen(ini));
|
||||
assert_int_equal(rc, 0);
|
||||
assert_false(x52d_layout_lookup(layout, (uint32_t)'b', &r));
|
||||
x52d_layout_free(layout);
|
||||
}
|
||||
|
||||
static void test_u_plus_section_escape(void **state)
|
||||
{
|
||||
static const char ini[] =
|
||||
"[Layout]\n"
|
||||
"Name=e\n"
|
||||
"[U+005D]\n"
|
||||
"Key=RightBracket\n"
|
||||
"Mods=\n";
|
||||
struct x52d_layout *layout = NULL;
|
||||
struct x52d_layout_recipe r;
|
||||
int rc;
|
||||
|
||||
(void)state;
|
||||
rc = x52d_layout_load_buffer(&layout, ini, strlen(ini));
|
||||
assert_int_equal(rc, 0);
|
||||
assert_true(x52d_layout_lookup(layout, 0x005DU, &r));
|
||||
assert_int_equal((int)r.key, (int)VKM_KEY_RIGHT_BRACKET);
|
||||
x52d_layout_free(layout);
|
||||
}
|
||||
|
||||
static void test_us_layout_file(void **state)
|
||||
{
|
||||
struct x52d_layout *layout = NULL;
|
||||
struct x52d_layout_recipe r;
|
||||
int rc;
|
||||
|
||||
(void)state;
|
||||
rc = x52d_layout_load_file(&layout, "layouts.d/us.layout");
|
||||
assert_int_equal(rc, 0);
|
||||
assert_non_null(layout);
|
||||
assert_string_equal(x52d_layout_get_name(layout), "us");
|
||||
assert_true(x52d_layout_lookup(layout, (uint32_t)'#', &r));
|
||||
assert_int_equal((int)r.key, (int)VKM_KEY_3);
|
||||
assert_int_equal((int)r.mods, (int)VKM_KEY_MOD_SHIFT);
|
||||
x52d_layout_free(layout);
|
||||
}
|
||||
|
||||
const struct CMUnitTest layout_tests[] = {
|
||||
cmocka_unit_test(test_sample_layout_lower_a),
|
||||
cmocka_unit_test(test_hex_key),
|
||||
cmocka_unit_test(test_modifiers_combo),
|
||||
cmocka_unit_test(test_utf8_codepoint),
|
||||
cmocka_unit_test(test_unknown_key_errors),
|
||||
cmocka_unit_test(test_collision_last_section_wins),
|
||||
cmocka_unit_test(test_unknown_modifier_errors),
|
||||
cmocka_unit_test(test_lookup_miss),
|
||||
cmocka_unit_test(test_u_plus_section_escape),
|
||||
cmocka_unit_test(test_us_layout_file),
|
||||
};
|
||||
|
||||
int main(void)
|
||||
{
|
||||
cmocka_set_message_output(CM_OUTPUT_TAP);
|
||||
cmocka_run_group_tests(layout_tests, NULL, NULL);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ 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')
|
||||
|
|
@ -112,8 +113,6 @@ sub_pinelog = subproject('pinelog', required: true,
|
|||
default_options: pinelog_options)
|
||||
dep_pinelog = sub_pinelog.get_variable('libpinelog_dep')
|
||||
|
||||
# inih
|
||||
# Use system inih
|
||||
dep_inih = dependency('inih')
|
||||
|
||||
#######################################################################
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ daemon/x52d_config_dump.c
|
|||
daemon/x52d_config_parser.c
|
||||
daemon/x52d_device.c
|
||||
daemon/x52d_io.c
|
||||
daemon/x52d_layout.c
|
||||
daemon/x52d_mouse.c
|
||||
daemon/x52d_mouse_handler.c
|
||||
daemon/x52d_notify.c
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: libx52 0.3.3\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\n"
|
||||
"POT-Creation-Date: 2026-04-02 23:32-0700\n"
|
||||
"POT-Creation-Date: 2026-04-03 08:36-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"
|
||||
|
|
@ -704,41 +704,41 @@ msgstr ""
|
|||
msgid "Shutting down X52 clock manager thread"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_command.c:379
|
||||
#: daemon/x52d_command.c:380
|
||||
#, c-format
|
||||
msgid "Error reading from client %d: %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_command.c:390
|
||||
#: daemon/x52d_command.c:391
|
||||
#, c-format
|
||||
msgid "Short write to client %d; expected %d bytes, wrote %d bytes"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_command.c:415
|
||||
#: daemon/x52d_command.c:416
|
||||
#, c-format
|
||||
msgid "Error %d during command loop: %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_command.c:442
|
||||
#: daemon/x52d_command.c:443
|
||||
#, c-format
|
||||
msgid "Error creating command socket: %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_command.c:450
|
||||
#: daemon/x52d_command.c:451
|
||||
#, c-format
|
||||
msgid "Error marking command socket as nonblocking: %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_command.c:456
|
||||
#: daemon/x52d_command.c:457
|
||||
#, c-format
|
||||
msgid "Error listening on command socket: %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_command.c:460
|
||||
#: daemon/x52d_command.c:461
|
||||
msgid "Starting command processing thread"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_command.c:478
|
||||
#: daemon/x52d_command.c:479
|
||||
msgid "Shutting down command processing thread"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -864,6 +864,40 @@ msgstr ""
|
|||
msgid "Shutting down X52 I/O driver thread"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_layout.c:449
|
||||
msgid "Layout section name too long"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_layout.c:483
|
||||
#, c-format
|
||||
msgid "Invalid UTF-8 in layout section '%s'"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_layout.c:488
|
||||
#, c-format
|
||||
msgid "Layout section must encode exactly one code point: '%s'"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_layout.c:541
|
||||
#, c-format
|
||||
msgid "Unknown Key value '%s'"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_layout.c:550
|
||||
#, c-format
|
||||
msgid "Invalid Mods value '%s'"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_layout.c:619
|
||||
#, c-format
|
||||
msgid "Layout parse error at line %d"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_layout.c:663
|
||||
#, c-format
|
||||
msgid "Layout parse error in %s at line %d"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/x52d_mouse.c:38 daemon/x52d_mouse.c:44
|
||||
#, c-format
|
||||
msgid "Clamping %s value %d to range [%d..%d]"
|
||||
|
|
|
|||
55
po/xx_PL.po
55
po/xx_PL.po
|
|
@ -7,8 +7,8 @@ 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-04-02 23:32-0700\n"
|
||||
"PO-Revision-Date: 2026-04-01 20:50-0700\n"
|
||||
"POT-Creation-Date: 2026-04-03 08:36-0700\n"
|
||||
"PO-Revision-Date: 2026-04-03 00:23-0700\n"
|
||||
"Last-Translator: Nirenjan Krishnan <nirenjan@gmail.com>\n"
|
||||
"Language-Team: Dummy Language for testing i18n\n"
|
||||
"Language: xx_PL\n"
|
||||
|
|
@ -756,42 +756,42 @@ msgstr "Erroray %d initializingay ockclay eadthray: %s"
|
|||
msgid "Shutting down X52 clock manager thread"
|
||||
msgstr "Uttingshay ownday X52 ockclay anagermay eadthray"
|
||||
|
||||
#: daemon/x52d_command.c:379
|
||||
#: daemon/x52d_command.c:380
|
||||
#, c-format
|
||||
msgid "Error reading from client %d: %s"
|
||||
msgstr "Erroray eadingray omfray ientclay %d: %s"
|
||||
|
||||
#: daemon/x52d_command.c:390
|
||||
#: daemon/x52d_command.c:391
|
||||
#, c-format
|
||||
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:415
|
||||
#: daemon/x52d_command.c:416
|
||||
#, c-format
|
||||
msgid "Error %d during command loop: %s"
|
||||
msgstr "Erroray %d uringday ommandcay ooplay: %s"
|
||||
|
||||
#: daemon/x52d_command.c:442
|
||||
#: daemon/x52d_command.c:443
|
||||
#, c-format
|
||||
msgid "Error creating command socket: %s"
|
||||
msgstr "Erroray eatingcray ommandcay ocketsay: %s"
|
||||
|
||||
#: daemon/x52d_command.c:450
|
||||
#: daemon/x52d_command.c:451
|
||||
#, c-format
|
||||
msgid "Error marking command socket as nonblocking: %s"
|
||||
msgstr "Erroray arkingmay ommandcay ocketsay asay onblockingnay: %s"
|
||||
|
||||
#: daemon/x52d_command.c:456
|
||||
#: daemon/x52d_command.c:457
|
||||
#, c-format
|
||||
msgid "Error listening on command socket: %s"
|
||||
msgstr "Erroray isteninglay onay ommandcay ocketsay: %s"
|
||||
|
||||
#: daemon/x52d_command.c:460
|
||||
#: daemon/x52d_command.c:461
|
||||
msgid "Starting command processing thread"
|
||||
msgstr "Artingstay ommandcay ocessingpray eadthray"
|
||||
|
||||
#: daemon/x52d_command.c:478
|
||||
#: daemon/x52d_command.c:479
|
||||
msgid "Shutting down command processing thread"
|
||||
msgstr "Uttingshay ownday ommandcay ocessingpray eadthray"
|
||||
|
||||
|
|
@ -917,6 +917,41 @@ msgstr "Erroray %d initializingay I/O iverdray eadthray: %s"
|
|||
msgid "Shutting down X52 I/O driver thread"
|
||||
msgstr "Uttingshay ownday X52 I/O iverdray eadthray"
|
||||
|
||||
#: daemon/x52d_layout.c:449
|
||||
msgid "Layout section name too long"
|
||||
msgstr "Ayoutlay ectionsay amenay ootay onglay"
|
||||
|
||||
#: daemon/x52d_layout.c:483
|
||||
#, c-format
|
||||
msgid "Invalid UTF-8 in layout section '%s'"
|
||||
msgstr "Invaliday UTFay-8 inay ayoutlay ectionsay '%s'"
|
||||
|
||||
#: daemon/x52d_layout.c:488
|
||||
#, c-format
|
||||
msgid "Layout section must encode exactly one code point: '%s'"
|
||||
msgstr ""
|
||||
"Ayoutlay ectionsay ustmay encodeay exactlyay oneay odecay ointpay: '%s'"
|
||||
|
||||
#: daemon/x52d_layout.c:541
|
||||
#, c-format
|
||||
msgid "Unknown Key value '%s'"
|
||||
msgstr "Unknownay eyKay aluevay '%s'"
|
||||
|
||||
#: daemon/x52d_layout.c:550
|
||||
#, c-format
|
||||
msgid "Invalid Mods value '%s'"
|
||||
msgstr "Invaliday odsMay aluevay '%s'"
|
||||
|
||||
#: daemon/x52d_layout.c:619
|
||||
#, c-format
|
||||
msgid "Layout parse error at line %d"
|
||||
msgstr "Ayoutlay arsepay erroray atay inelay %d"
|
||||
|
||||
#: daemon/x52d_layout.c:663
|
||||
#, c-format
|
||||
msgid "Layout parse error in %s at line %d"
|
||||
msgstr "Ayoutlay arsepay erroray inay %s atay inelay %d"
|
||||
|
||||
#: daemon/x52d_mouse.c:38 daemon/x52d_mouse.c:44
|
||||
#, c-format
|
||||
msgid "Clamping %s value %d to range [%d..%d]"
|
||||
|
|
|
|||
Loading…
Reference in New Issue