From b87464be80e376acc036ceb929dd1e56907bab69 Mon Sep 17 00:00:00 2001 From: nirenjan Date: Fri, 3 Apr 2026 00:23:51 -0700 Subject: [PATCH] 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. --- daemon/layouts.d/us.layout | 397 ++++++++++++++++++++++ daemon/meson.build | 8 + daemon/x52d_command.c | 1 + daemon/x52d_const.h | 1 + daemon/x52d_layout.c | 676 +++++++++++++++++++++++++++++++++++++ daemon/x52d_layout.h | 56 +++ daemon/x52d_layout_test.c | 239 +++++++++++++ meson.build | 3 +- po/POTFILES.in | 1 + po/libx52.pot | 52 ++- po/xx_PL.po | 55 ++- 11 files changed, 1468 insertions(+), 21 deletions(-) create mode 100644 daemon/layouts.d/us.layout create mode 100644 daemon/x52d_layout.c create mode 100644 daemon/x52d_layout.h create mode 100644 daemon/x52d_layout_test.c diff --git a/daemon/layouts.d/us.layout b/daemon/layouts.d/us.layout new file mode 100644 index 0000000..7c5bae8 --- /dev/null +++ b/daemon/layouts.d/us.layout @@ -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 diff --git a/daemon/meson.build b/daemon/meson.build index e9fd931..d5bbf57 100644 --- a/daemon/meson.build +++ b/daemon/meson.build @@ -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') diff --git a/daemon/x52d_command.c b/daemon/x52d_command.c index af83641..1f49379 100644 --- a/daemon/x52d_command.c +++ b/daemon/x52d_command.c @@ -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", diff --git a/daemon/x52d_const.h b/daemon/x52d_const.h index 985725b..4a8a74f 100644 --- a/daemon/x52d_const.h +++ b/daemon/x52d_const.h @@ -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, diff --git a/daemon/x52d_layout.c b/daemon/x52d_layout.c new file mode 100644 index 0000000..bf7187d --- /dev/null +++ b/daemon/x52d_layout.c @@ -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= # e.g. us + * Description= + * + * [] | [U+XXXX] | [u+XXXX] + * Key=|0xNN # HID usage (page 0x07), hex or symbolic (e.g. A, Enter) + * Mods= # 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 +#include +#include +#include +#include + +#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; +} diff --git a/daemon/x52d_layout.h b/daemon/x52d_layout.h new file mode 100644 index 0000000..83901bb --- /dev/null +++ b/daemon/x52d_layout.h @@ -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 +#include +#include + +#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 */ diff --git a/daemon/x52d_layout_test.c b/daemon/x52d_layout_test.c new file mode 100644 index 0000000..e1a48c6 --- /dev/null +++ b/daemon/x52d_layout_test.c @@ -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 +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/meson.build b/meson.build index 7124f00..638e03b 100644 --- a/meson.build +++ b/meson.build @@ -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') ####################################################################### diff --git a/po/POTFILES.in b/po/POTFILES.in index 39b9cc0..e27a3f4 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -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 diff --git a/po/libx52.pot b/po/libx52.pot index dc2a67d..d37124b 100644 --- a/po/libx52.pot +++ b/po/libx52.pot @@ -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 \n" "Language-Team: LANGUAGE \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]" diff --git a/po/xx_PL.po b/po/xx_PL.po index 707d97b..a4fbceb 100644 --- a/po/xx_PL.po +++ b/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 \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]"