From 966c6efd5feaf9b589db299a128da2f09ef0f9f6 Mon Sep 17 00:00:00 2001 From: nirenjan Date: Thu, 26 Feb 2026 13:30:03 -0800 Subject: [PATCH] feat: Add profile support to x52d This change adds initial support for profiles, which map joystick button events into keyboard events. This supports single key/key combo events which map directly to the button state, as well as keyboard macro events which fire only on button down. This commit also adds the clutch button functionality to the daemon, allowing the user to disengage the profiles temporarily, and selecting a profile or clearing the profile using the joystick hat and MFD. --- daemon/Makefile.am | 71 ++++- daemon/daemon.dox | 49 ++++ daemon/default.conf.example | 27 ++ daemon/x52d.conf | 11 +- daemon/x52d_clutch.c | 285 +++++++++++++++++++ daemon/x52d_clutch.h | 46 ++++ daemon/x52d_clutch_test.c | 165 +++++++++++ daemon/x52d_command.c | 3 + daemon/x52d_config.c | 1 + daemon/x52d_config.def | 4 + daemon/x52d_config.h | 2 + daemon/x52d_config_apply_stubs.c | 45 +++ daemon/x52d_const.h | 3 + daemon/x52d_io.c | 12 +- daemon/x52d_keyboard.h | 62 +++++ daemon/x52d_keyboard_evdev.c | 93 +++++++ daemon/x52d_main.c | 6 + daemon/x52d_profile.c | 460 +++++++++++++++++++++++++++++++ daemon/x52d_profile.h | 71 +++++ daemon/x52d_profile_test.c | 325 ++++++++++++++++++++++ po/POTFILES.in | 3 + po/libx52.pot | 160 ++++++++--- po/xx_PL.po | 147 +++++++--- 23 files changed, 1967 insertions(+), 84 deletions(-) create mode 100644 daemon/default.conf.example create mode 100644 daemon/x52d_clutch.c create mode 100644 daemon/x52d_clutch.h create mode 100644 daemon/x52d_clutch_test.c create mode 100644 daemon/x52d_config_apply_stubs.c create mode 100644 daemon/x52d_keyboard.h create mode 100644 daemon/x52d_keyboard_evdev.c create mode 100644 daemon/x52d_profile.c create mode 100644 daemon/x52d_profile.h create mode 100644 daemon/x52d_profile_test.c diff --git a/daemon/Makefile.am b/daemon/Makefile.am index 98e6244..dc889df 100644 --- a/daemon/Makefile.am +++ b/daemon/Makefile.am @@ -44,7 +44,10 @@ x52d_LDADD = \ if HAVE_EVDEV x52d_SOURCES += \ daemon/x52d_io.c \ - daemon/x52d_mouse_evdev.c + daemon/x52d_mouse_evdev.c \ + daemon/x52d_keyboard_evdev.c \ + daemon/x52d_profile.c \ + daemon/x52d_clutch.c x52d_CFLAGS += -DHAVE_EVDEV @EVDEV_CFLAGS@ x52d_LDFLAGS += @EVDEV_LIBS@ @@ -84,6 +87,7 @@ install-exec-hook: EXTRA_DIST += \ daemon/daemon.dox \ + daemon/default.conf.example \ daemon/protocol.dox \ daemon/x52d.service.in \ daemon/x52d_client.h \ @@ -93,7 +97,10 @@ EXTRA_DIST += \ daemon/x52d_const.h \ daemon/x52d_device.h \ daemon/x52d_io.h \ + daemon/x52d_clutch.h \ + daemon/x52d_keyboard.h \ daemon/x52d_mouse.h \ + daemon/x52d_profile.h \ daemon/x52d_notify.h \ daemon/x52d_command.h \ daemon/x52dcomm.h \ @@ -103,6 +110,9 @@ EXTRA_DIST += \ # Test cases EXTRA_DIST += \ daemon/test_daemon_comm.py \ + daemon/x52d_config_apply_stubs.c \ + daemon/x52d_profile_test.c \ + daemon/x52d_clutch_test.c \ daemon/tests/config/args.tc \ daemon/tests/config/clock.tc \ daemon/tests/config/led.tc \ @@ -135,6 +145,65 @@ x52d_mouse_test_LDADD = \ TESTS += x52d-mouse-test endif +if HAVE_CMOCKA +if HAVE_EVDEV +check_PROGRAMS += x52d-profile-test x52d-clutch-test + +x52d_profile_test_SOURCES = \ + daemon/x52d_profile_test.c \ + daemon/x52d_profile.c \ + daemon/x52d_config.c \ + daemon/x52d_config_parser.c \ + daemon/x52d_config_dump.c \ + daemon/x52d_config_apply_stubs.c +x52d_profile_test_CFLAGS = \ + -DHAVE_EVDEV \ + -DSYSCONFDIR=\"$(sysconfdir)\" \ + -DLOCALEDIR=\"$(localedir)\" \ + -DLOGDIR=\"$(localstatedir)/log\" \ + -DRUNDIR=\"$(localstatedir)/run\" \ + -I $(top_srcdir) \ + -I $(top_srcdir)/daemon \ + -I $(top_srcdir)/libx52 \ + -I $(top_srcdir)/libx52io \ + -I $(top_srcdir)/libx52util \ + -I $(top_srcdir)/lib/pinelog \ + -I $(top_srcdir)/lib/inih \ + $(WARN_CFLAGS) @CMOCKA_CFLAGS@ @EVDEV_CFLAGS@ +x52d_profile_test_LDFLAGS = $(WARN_LDFLAGS) +x52d_profile_test_LDADD = \ + lib/pinelog/libpinelog.la \ + lib/inih/libinih.la \ + libx52io.la \ + libx52.la \ + @EVDEV_LIBS@ \ + @CMOCKA_LIBS@ \ + @LTLIBINTL@ + +x52d_clutch_test_SOURCES = \ + daemon/x52d_clutch_test.c \ + daemon/x52d_clutch.c +x52d_clutch_test_CFLAGS = \ + -DHAVE_EVDEV \ + -DLOCALEDIR=\"$(localedir)\" \ + -I $(top_srcdir) \ + -I $(top_srcdir)/daemon \ + -I $(top_srcdir)/libx52 \ + -I $(top_srcdir)/libx52io \ + -I $(top_srcdir)/lib/pinelog \ + $(WARN_CFLAGS) @CMOCKA_CFLAGS@ +x52d_clutch_test_LDFLAGS = $(WARN_LDFLAGS) +x52d_clutch_test_LDADD = \ + lib/pinelog/libpinelog.la \ + libx52.la \ + libx52io.la \ + @CMOCKA_LIBS@ \ + @LTLIBINTL@ + +TESTS += x52d-profile-test x52d-clutch-test +endif +endif + if HAVE_SYSTEMD if !IS_MAKE_DISTCHECK SED_ARGS = s,%bindir%,$(bindir),g diff --git a/daemon/daemon.dox b/daemon/daemon.dox index 5d4a04f..8dfbe4f 100644 --- a/daemon/daemon.dox +++ b/daemon/daemon.dox @@ -44,4 +44,53 @@ overridden. @code{.unparsed} -o clock.secondary=America/New_York @endcode + +# Profile files (Linux, when profiles are implemented) + +Profiles map joystick buttons to keyboard events only. They are selected by +the main config option Profiles.Profile and loaded from Profiles.Directory. +Mouse (thumbstick, mouse buttons, scroll) is handled separately by the +virtual mouse and is not driven by profiles. + +## Mode and shift + +- The joystick has **3 modes**; only one is active at a time. Mode is determined + by BTN_MODE_1, BTN_MODE_2, BTN_MODE_3 (the physical mode switch). +- The **shift key** is configured in the profile file ([Profile] section, + key ShiftButton, e.g. BTN_PINKY). When held, the profile uses the shift + layer for the current mode. Default is BTN_PINKY if not set in the profile. + +So at runtime the effective layer is (mode 1/2/3) × (shift released or held). +Profile files use INI sections to define mappings per layer: + +- [Profile] — profile options: Name = display name; ShiftButton = BTN_PINKY (or another button name) +- [Mode1], [Mode2], [Mode3] — mappings when shift is not held +- [Mode1.Shift], [Mode2.Shift], [Mode3.Shift] — mappings when shift is held + (override the non-shift layer for that mode) + +Within each section, button mappings use the Button. = ... syntax: +- \c key \c KEY_X [\c KEY_Y ...] — single key or combo (e.g. Ctrl-X: \c key \c KEY_LEFTCTRL \c KEY_X). All keys go down on press and up on release in reverse order. +- \c macro \c KEY_A \c KEY_B ... — sequence of key down/up on button down only. The shift and mode selector buttons can be left unmapped +(recommended) or mapped like any other button. + +Fallback: if a mapping is not defined for the current layer, the next layer +in the chain is used. ModeN.Shift falls back to ModeN; Mode2 falls back to +Mode1; Mode3 falls back to Mode2 then Mode1. So you can define only Mode1 +and override in Mode1.Shift or Mode2 where needed. + +# Clutch (profile selection on MFD) + +When \c Profiles.ClutchEnabled is set, the clutch button enters a profile +selection UI on the MFD: + +- **ClutchLatched yes**: First press-and-release of the clutch engages clutch + mode; next press-and-release exits. **ClutchLatched no**: Clutch mode is + active only while the clutch button is held; releasing exits. + +In clutch mode, profile names (from \c Profiles.Directory *.conf files) are +shown on the MFD. Use the **hat** (AXIS_HATX / AXIS_HATY): **up/down** (HATY) to +scroll through profiles, **left** (HATX -1, swipe left) to clear and exit without +changing profile, **right** (HATX 1, swipe right) to select the highlighted +profile and load it. +The selected profile is applied immediately and in-memory config is updated. */ diff --git a/daemon/default.conf.example b/daemon/default.conf.example new file mode 100644 index 0000000..c37d108 --- /dev/null +++ b/daemon/default.conf.example @@ -0,0 +1,27 @@ +# Example X52 profile: keyboard mappings per mode and shift +# Copy to e.g. /etc/x52d/profiles.d/default.conf and edit. +# [Profile]: Name = display name; ShiftButton = button name (default BTN_PINKY if omitted) +# [Mode1], [Mode2], [Mode3], [Mode1.Shift], [Mode2.Shift], [Mode3.Shift]: +# Button.X = key KEY_Y [KEY_Z ...] (single key or combo, down on press/up on release) +# Button.X = macro KEY_A KEY_B (sequence on button down only) +# Fallback: ModeN.Shift -> ModeN; Mode2 -> Mode1; Mode3 -> Mode2 -> Mode1 + +[Profile] +Name=Example profile +ShiftButton=BTN_PINKY + +[Mode1] +# Single key: Fire -> E +Button.BTN_FIRE = key KEY_E +# Key combo: A -> Ctrl+X (modifiers first, then key) +Button.BTN_A = key KEY_LEFTCTRL KEY_X +# Alt+Y +Button.BTN_B = key KEY_LEFTALT KEY_Y +# Macro: T1 up -> Ctrl+Shift+T (on button down only) +Button.BTN_T1_UP = macro KEY_LEFTCTRL KEY_LEFTSHIFT KEY_T + +[Mode1.Shift] +Button.BTN_FIRE = key KEY_F + +[Mode2] +Button.BTN_A = key KEY_SPACE diff --git a/daemon/x52d.conf b/daemon/x52d.conf index b6a11cb..75c81f9 100644 --- a/daemon/x52d.conf +++ b/daemon/x52d.conf @@ -92,13 +92,18 @@ ReverseScroll=no # Profiles - only valid on Linux ###################################################################### [Profiles] -# TODO: Profiles are used to map the buttons and axis to keyboard events, and -# can be used to write macros. This is a placeholder only for now, and is not -# supported yet. +# Profiles map joystick buttons to keyboard events only. The joystick has +# 3 modes (only one active; selected by BTN_MODE_1/2/3) and an optional +# shift key. When shift is held, the profile's shift layer for the current +# mode is used. Profile files use sections [Mode1], [Mode2], [Mode3], and +# optionally [Mode1.Shift], [Mode2.Shift], [Mode3.Shift]. # Directory is the location of the folder containing the individual profiles. Directory=/etc/x52d/profiles.d +# Profile is the name of the active profile (e.g. file default.conf in Directory). +Profile=default + # ClutchEnabled determines if the clutch button is treated specially ClutchEnabled=no diff --git a/daemon/x52d_clutch.c b/daemon/x52d_clutch.c new file mode 100644 index 0000000..8fe9021 --- /dev/null +++ b/daemon/x52d_clutch.c @@ -0,0 +1,285 @@ +/* + * Saitek X52 Pro MFD & LED driver - Clutch (profile selection UI) + * + * Copyright (C) 2021 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 "libx52.h" +#include "libx52io.h" + +#define PINELOG_MODULE X52D_MOD_CLUTCH +#include "pinelog.h" + +#include "x52d_config.h" +#include "x52d_const.h" +#include "x52d_device.h" +#include "x52d_clutch.h" +#include "x52d_profile.h" + +#define MFD_LINE_SIZE 16 +#define MFD_LINES 3 +#define MAX_PROFILES 32 +#define MAX_PROFILE_NAME 16 /* truncate for MFD display */ + +static bool clutch_engaged; +static char *profile_list[MAX_PROFILES]; +static int profile_count; +static int highlight_index; +static bool clutch_ui_active; /* true after we've shown the list at least once */ + +static bool config_clutch_enabled(void) +{ + const char *v = x52d_config_get("Profiles", "ClutchEnabled"); + if (v == NULL) return false; + return (strcasecmp(v, "yes") == 0 || strcasecmp(v, "true") == 0); +} + +static bool config_clutch_latched(void) +{ + const char *v = x52d_config_get("Profiles", "ClutchLatched"); + if (v == NULL) return false; + return (strcasecmp(v, "yes") == 0 || strcasecmp(v, "true") == 0); +} + +static void clutch_update_led(bool enabled, bool engaged) +{ + if (!enabled) { + x52d_dev_set_blink(0); + x52d_dev_set_led_state(LIBX52_LED_CLUTCH, LIBX52_LED_STATE_RED); + return; + } + if (!engaged) { + x52d_dev_set_blink(0); + x52d_dev_set_led_state(LIBX52_LED_CLUTCH, LIBX52_LED_STATE_GREEN); + return; + } + /* Engaged: amber with hardware blink */ + x52d_dev_set_led_state(LIBX52_LED_CLUTCH, LIBX52_LED_STATE_AMBER); + x52d_dev_set_blink(1); +} + +static void free_profile_list(void) +{ + int i; + for (i = 0; i < profile_count; i++) { + free(profile_list[i]); + profile_list[i] = NULL; + } + profile_count = 0; +} + +static int scan_profiles(void) +{ + const char *dir_path; + DIR *dir; + struct dirent *ent; + size_t len; + char *stem; + + free_profile_list(); + dir_path = x52d_config_get("Profiles", "Directory"); + if (dir_path == NULL || dir_path[0] == '\0') { + return 0; + } + dir = opendir(dir_path); + if (dir == NULL) { + PINELOG_WARN(_("Cannot open profiles directory %s: %s"), dir_path, strerror(errno)); + return 0; + } + while (profile_count < MAX_PROFILES && (ent = readdir(dir)) != NULL) { + len = strlen(ent->d_name); + if (len <= 4) continue; + if (strcasecmp(ent->d_name + len - 4, ".conf") != 0) continue; + stem = malloc(len - 4); /* strip .conf, need len-5+1 for NUL */ + if (stem == NULL) continue; + memcpy(stem, ent->d_name, len - 5); + stem[len - 5] = '\0'; + if (stem[0] == '\0') { + free(stem); + continue; + } + profile_list[profile_count++] = stem; + } + closedir(dir); + /* Sort by name (simple bubble sort) */ + { + int i, j; + for (i = 0; i < profile_count - 1; i++) { + for (j = i + 1; j < profile_count; j++) { + if (strcasecmp(profile_list[i], profile_list[j]) > 0) { + char *t = profile_list[i]; + profile_list[i] = profile_list[j]; + profile_list[j] = t; + } + } + } + } + return profile_count; +} + +static void clutch_clear_mfd(void) +{ + static const char blank[MFD_LINE_SIZE] = { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }; + x52d_dev_set_text(0, blank, MFD_LINE_SIZE); + x52d_dev_set_text(1, blank, MFD_LINE_SIZE); + x52d_dev_set_text(2, blank, MFD_LINE_SIZE); +} + +static void clutch_refresh_mfd(void) +{ + char line_buf[MFD_LINE_SIZE]; + int i; + size_t len; + + if (profile_count == 0) { + clutch_clear_mfd(); + return; + } + if (highlight_index >= profile_count) highlight_index = profile_count - 1; + if (highlight_index < 0) highlight_index = 0; + + for (i = 0; i < MFD_LINES; i++) { + int idx = highlight_index - 1 + i; /* line 0 = above, 1 = current, 2 = below */ + if (idx < 0 || idx >= profile_count) { + memset(line_buf, ' ', MFD_LINE_SIZE); + x52d_dev_set_text(i, line_buf, MFD_LINE_SIZE); + continue; + } + len = strlen(profile_list[idx]); + if (len > MAX_PROFILE_NAME) len = MAX_PROFILE_NAME; + memset(line_buf, ' ', MFD_LINE_SIZE); + if (idx == highlight_index) { + line_buf[0] = '>'; + if (len + 1 < MFD_LINE_SIZE) { + memcpy(line_buf + 1, profile_list[idx], len); + len++; + } else { + memcpy(line_buf + 1, profile_list[idx], MFD_LINE_SIZE - 1); + len = MFD_LINE_SIZE; + } + } else { + memcpy(line_buf, profile_list[idx], len); + } + x52d_dev_set_text(i, line_buf, MFD_LINE_SIZE); + } +} + +static void clutch_enter_ui(void) +{ + scan_profiles(); + highlight_index = 0; + clutch_ui_active = true; + clutch_refresh_mfd(); +} + +static void clutch_exit_ui(bool restore_mfd) +{ + clutch_ui_active = false; + if (restore_mfd) { + clutch_clear_mfd(); + } + free_profile_list(); +} + +static void clutch_do_clear(void) +{ + clutch_exit_ui(true); + clutch_engaged = false; +} + +static void clutch_do_select(void) +{ + if (profile_count > 0 && highlight_index >= 0 && highlight_index < profile_count) { + const char *name = profile_list[highlight_index]; + if (x52d_config_set("Profiles", "Profile", name) == 0) { + x52d_profile_init(); + PINELOG_INFO(_("Profile switched to: %s"), name); + } + } + clutch_exit_ui(true); + clutch_engaged = false; +} + +bool x52d_clutch_process(const libx52io_report *report, + const libx52io_report *prev) +{ + bool enabled = config_clutch_enabled(); + bool latched = config_clutch_latched(); + bool clutch_pressed = report->button[LIBX52IO_BTN_CLUTCH]; + bool prev_clutch = prev->button[LIBX52IO_BTN_CLUTCH]; + + /* Update clutch_engaged */ + if (latched) { + if (prev_clutch && !clutch_pressed) { + clutch_engaged = !clutch_engaged; + } + } else { + clutch_engaged = clutch_pressed; + } + + /* Clutch LED: disabled=red, enabled+not engaged=green, engaged=blink amber/off 250ms */ + clutch_update_led(enabled, clutch_engaged); + + if (!enabled) { + return false; + } + + if (!clutch_engaged) { + if (clutch_ui_active) { + clutch_exit_ui(true); + } + return false; + } + + /* Clutch engaged: show UI and handle hat/swipe */ + if (!clutch_ui_active) { + clutch_enter_ui(); + } + + /* + * Safety: if both HATX and HATY are non-zero (diagonal), use HATY alone + * and treat HATX as 0 to avoid accidental clear/select on diagonals. + */ + { + int32_t prev_x = prev->axis[LIBX52IO_AXIS_HATX]; + int32_t prev_y = prev->axis[LIBX52IO_AXIS_HATY]; + int32_t cur_x = report->axis[LIBX52IO_AXIS_HATX]; + int32_t cur_y = report->axis[LIBX52IO_AXIS_HATY]; + if (prev_x != 0 && prev_y != 0) prev_x = 0; + if (cur_x != 0 && cur_y != 0) cur_x = 0; + /* Edge detection: HATY -1=up, HATY 1=down, HATX -1=left, HATX 1=right */ + if (prev_x != -1 && cur_x == -1) { + clutch_do_clear(); + return true; + } + if (profile_count > 0) { + if (prev_y != -1 && cur_y == -1) { + highlight_index--; + if (highlight_index < 0) highlight_index = profile_count - 1; + clutch_refresh_mfd(); + } + if (prev_y != 1 && cur_y == 1) { + highlight_index++; + if (highlight_index >= profile_count) highlight_index = 0; + clutch_refresh_mfd(); + } + if (prev_x != 1 && cur_x == 1) { + clutch_do_select(); + return true; + } + } + } + + return true; /* consumed: in clutch UI, skip profile/mouse */ +} diff --git a/daemon/x52d_clutch.h b/daemon/x52d_clutch.h new file mode 100644 index 0000000..abb1323 --- /dev/null +++ b/daemon/x52d_clutch.h @@ -0,0 +1,46 @@ +/* + * Saitek X52 Pro MFD & LED driver - Clutch (profile selection UI) + * + * Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#ifndef X52D_CLUTCH_H +#define X52D_CLUTCH_H + +#include "libx52io.h" + +/** + * @file x52d_clutch.h + * @brief Clutch mode: profile selection on MFD (hat scroll, swipe left/right) + */ + +#if defined(HAVE_EVDEV) + +/** + * @brief Process clutch button and, when engaged, clutch UI (hat/swipe). + * + * When ClutchEnabled and the clutch is engaged (latched toggle or held), + * displays profile names on the MFD. Hat up/down scrolls; swipe left + * clears (exit without change); swipe right selects the highlighted profile. + * + * @param report Current joystick report. + * @param prev Previous report (for edge detection). + * @return true if clutch UI consumed the report (caller should skip profile/mouse). + */ +bool x52d_clutch_process(const libx52io_report *report, + const libx52io_report *prev); + +#else + +static inline bool x52d_clutch_process(const libx52io_report *report, + const libx52io_report *prev) { + (void)report; + (void)prev; + return false; +} + +#endif /* HAVE_EVDEV */ + +#endif /* !defined X52D_CLUTCH_H */ diff --git a/daemon/x52d_clutch_test.c b/daemon/x52d_clutch_test.c new file mode 100644 index 0000000..102270d --- /dev/null +++ b/daemon/x52d_clutch_test.c @@ -0,0 +1,165 @@ +/* + * Saitek X52 Pro MFD & LED driver - Clutch module tests + * + * Copyright (C) 2021 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 "libx52.h" +#include "libx52io.h" + +#define PINELOG_MODULE X52D_MOD_CLUTCH +#include "pinelog.h" + +#include "x52d_config.h" +#include "x52d_const.h" +#include "x52d_device.h" +#include "x52d_profile.h" +#include "x52d_clutch.h" + +/* Stub config: return values set by test */ +static const char *stub_clutch_enabled = "no"; +static const char *stub_clutch_latched = "no"; + +const char *x52d_config_get(const char *section, const char *key) +{ + if (section != NULL && key != NULL) { + if (strcasecmp(section, "Profiles") == 0) { + if (strcasecmp(key, "ClutchEnabled") == 0) return stub_clutch_enabled; + if (strcasecmp(key, "ClutchLatched") == 0) return stub_clutch_latched; + } + } + return NULL; +} + +int x52d_config_set(const char *section, const char *key, const char *value) +{ + (void)section; + (void)key; + (void)value; + return 0; +} + +void x52d_profile_init(void) +{ +} + +/* Stub device: record and optionally check expected */ +int x52d_dev_set_led_state(libx52_led_id led, libx52_led_state state) +{ + check_expected(led); + check_expected(state); + return 0; +} + +int x52d_dev_set_blink(uint8_t state) +{ + (void)check_expected(state); + return 0; +} + +int x52d_dev_set_text(uint8_t line, const char *text, uint8_t length) +{ + (void)line; + (void)text; + (void)length; + return 0; +} + +static void test_clutch_disabled_red_led(void **state) +{ + libx52io_report report, prev; + + (void)state; + stub_clutch_enabled = "no"; + stub_clutch_latched = "no"; + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + + expect_value(x52d_dev_set_blink, state, 0); + expect_value(x52d_dev_set_led_state, led, LIBX52_LED_CLUTCH); + expect_value(x52d_dev_set_led_state, state, LIBX52_LED_STATE_RED); + + assert_false(x52d_clutch_process(&report, &prev)); +} + +static void test_clutch_enabled_not_engaged_green_led(void **state) +{ + libx52io_report report, prev; + + (void)state; + stub_clutch_enabled = "yes"; + stub_clutch_latched = "no"; + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + report.button[LIBX52IO_BTN_CLUTCH] = false; + + expect_value(x52d_dev_set_blink, state, 0); + expect_value(x52d_dev_set_led_state, led, LIBX52_LED_CLUTCH); + expect_value(x52d_dev_set_led_state, state, LIBX52_LED_STATE_GREEN); + + assert_false(x52d_clutch_process(&report, &prev)); +} + +static void test_clutch_engaged_amber_blink(void **state) +{ + libx52io_report report, prev; + + (void)state; + stub_clutch_enabled = "yes"; + stub_clutch_latched = "yes"; + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + + /* First: press clutch (no toggle yet for latched) */ + report.button[LIBX52IO_BTN_CLUTCH] = true; + expect_value(x52d_dev_set_blink, state, 0); + expect_value(x52d_dev_set_led_state, led, LIBX52_LED_CLUTCH); + expect_value(x52d_dev_set_led_state, state, LIBX52_LED_STATE_GREEN); + assert_false(x52d_clutch_process(&report, &prev)); + + /* Second: release clutch -> toggle engaged */ + memcpy(&prev, &report, sizeof(report)); + report.button[LIBX52IO_BTN_CLUTCH] = false; + expect_value(x52d_dev_set_led_state, led, LIBX52_LED_CLUTCH); + expect_value(x52d_dev_set_led_state, state, LIBX52_LED_STATE_AMBER); + expect_value(x52d_dev_set_blink, state, 1); + assert_true(x52d_clutch_process(&report, &prev)); +} + +static void test_clutch_held_not_latched(void **state) +{ + libx52io_report report, prev; + + (void)state; + stub_clutch_enabled = "yes"; + stub_clutch_latched = "no"; + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + report.button[LIBX52IO_BTN_CLUTCH] = true; + + /* Held (not latched): directly to engaged = amber + blink, no prior blink(0) */ + expect_value(x52d_dev_set_led_state, led, LIBX52_LED_CLUTCH); + expect_value(x52d_dev_set_led_state, state, LIBX52_LED_STATE_AMBER); + expect_value(x52d_dev_set_blink, state, 1); + assert_true(x52d_clutch_process(&report, &prev)); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_clutch_disabled_red_led), + cmocka_unit_test(test_clutch_enabled_not_engaged_green_led), + cmocka_unit_test(test_clutch_engaged_amber_blink), + cmocka_unit_test(test_clutch_held_not_latched), + }; + cmocka_set_message_output(CM_OUTPUT_TAP); + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/daemon/x52d_command.c b/daemon/x52d_command.c index b4d2b59..176163f 100644 --- a/daemon/x52d_command.c +++ b/daemon/x52d_command.c @@ -261,6 +261,9 @@ 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_KEYBOARD] = "keyboard", + [X52D_MOD_PROFILE] = "profile", + [X52D_MOD_CLUTCH] = "clutch", [X52D_MOD_COMMAND] = "command", [X52D_MOD_CLIENT] = "client", [X52D_MOD_NOTIFY] = "notify", diff --git a/daemon/x52d_config.c b/daemon/x52d_config.c index a5bcff7..5b1f935 100644 --- a/daemon/x52d_config.c +++ b/daemon/x52d_config.c @@ -86,6 +86,7 @@ const char *x52d_config_get(const char *section, const char *key) * TODO: Remove the ones below when their implementation is complete */ void x52d_cfg_set_Profiles_Directory(char* param) { (void)param; } +void x52d_cfg_set_Profiles_Profile(char* param) { (void)param; } void x52d_cfg_set_Profiles_ClutchEnabled(bool param) { (void)param; } void x52d_cfg_set_Profiles_ClutchLatched(bool param) { (void)param; } diff --git a/daemon/x52d_config.def b/daemon/x52d_config.def index 3ba713b..0a073ca 100644 --- a/daemon/x52d_config.def +++ b/daemon/x52d_config.def @@ -84,6 +84,10 @@ CFG(Mouse, ReverseScroll, mouse_reverse_scroll, bool, false) // Directory is the location of the folder containing the individual profiles. CFG(Profiles, Directory, profiles_dir, string, /etc/x52d/profiles.d) +// Profile is the name of the active profile (file profiles_dir/Profile.conf). +// Profiles generate keyboard events only (single key or macro). +CFG(Profiles, Profile, profile_name, string, default) + // ClutchEnabled determines if the clutch button is treated specially CFG(Profiles, ClutchEnabled, clutch_enabled, bool, false) diff --git a/daemon/x52d_config.h b/daemon/x52d_config.h index 4ba7f3d..bbd55ff 100644 --- a/daemon/x52d_config.h +++ b/daemon/x52d_config.h @@ -45,6 +45,7 @@ struct x52d_config { bool clutch_latched; char profiles_dir[NAME_MAX]; + char profile_name[NAME_MAX]; }; /* Callback functions for configuration */ @@ -74,6 +75,7 @@ void x52d_cfg_set_Mouse_Enabled(bool param); void x52d_cfg_set_Mouse_Speed(int param); void x52d_cfg_set_Mouse_ReverseScroll(bool param); void x52d_cfg_set_Profiles_Directory(char* param); +void x52d_cfg_set_Profiles_Profile(char* param); void x52d_cfg_set_Profiles_ClutchEnabled(bool param); void x52d_cfg_set_Profiles_ClutchLatched(bool param); diff --git a/daemon/x52d_config_apply_stubs.c b/daemon/x52d_config_apply_stubs.c new file mode 100644 index 0000000..edf983c --- /dev/null +++ b/daemon/x52d_config_apply_stubs.c @@ -0,0 +1,45 @@ +/* + * Saitek X52 Pro MFD & LED driver - No-op config apply stubs for unit tests + * + * When linking the profile test we use x52d_config.c but not the full daemon + * (clock, led, mouse). x52d_config_apply() and x52d_config_apply_immediate() + * call every x52d_cfg_set_* from x52d_config.def. This file provides no-op + * implementations for Clock, LED, Brightness, and Mouse so the test links. + * Profiles_* are already defined as no-ops in x52d_config.c. + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#include "x52d_config.h" + +/* Clock */ +void x52d_cfg_set_Clock_Enabled(bool param) { (void)param; } +void x52d_cfg_set_Clock_PrimaryIsLocal(bool param) { (void)param; } +void x52d_cfg_set_Clock_Secondary(char *param) { (void)param; } +void x52d_cfg_set_Clock_Tertiary(char *param) { (void)param; } +void x52d_cfg_set_Clock_FormatPrimary(libx52_clock_format param) { (void)param; } +void x52d_cfg_set_Clock_FormatSecondary(libx52_clock_format param) { (void)param; } +void x52d_cfg_set_Clock_FormatTertiary(libx52_clock_format param) { (void)param; } +void x52d_cfg_set_Clock_DateFormat(libx52_date_format param) { (void)param; } + +/* LED */ +void x52d_cfg_set_LED_Fire(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_Throttle(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_A(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_B(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_D(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_E(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_T1(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_T2(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_T3(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_POV(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_Clutch(libx52_led_state param) { (void)param; } + +/* Brightness */ +void x52d_cfg_set_Brightness_MFD(uint16_t param) { (void)param; } +void x52d_cfg_set_Brightness_LED(uint16_t param) { (void)param; } + +/* Mouse */ +void x52d_cfg_set_Mouse_Enabled(bool param) { (void)param; } +void x52d_cfg_set_Mouse_Speed(int param) { (void)param; } +void x52d_cfg_set_Mouse_ReverseScroll(bool param) { (void)param; } diff --git a/daemon/x52d_const.h b/daemon/x52d_const.h index 5a24e6b..5b0bbc4 100644 --- a/daemon/x52d_const.h +++ b/daemon/x52d_const.h @@ -33,6 +33,9 @@ enum { X52D_MOD_IO, X52D_MOD_LED, X52D_MOD_MOUSE, + X52D_MOD_KEYBOARD, + X52D_MOD_PROFILE, + X52D_MOD_CLUTCH, X52D_MOD_COMMAND, X52D_MOD_CLIENT, X52D_MOD_NOTIFY, diff --git a/daemon/x52d_io.c b/daemon/x52d_io.c index 1cc30f1..b45526a 100644 --- a/daemon/x52d_io.c +++ b/daemon/x52d_io.c @@ -14,7 +14,10 @@ #include "x52d_const.h" #include "x52d_config.h" #include "x52d_io.h" +#include "x52d_clutch.h" +#include "x52d_keyboard.h" #include "x52d_mouse.h" +#include "x52d_profile.h" #include "libx52io.h" #define PINELOG_MODULE X52D_MOD_IO @@ -26,7 +29,14 @@ static pthread_t io_thr; static void process_report(libx52io_report *report, libx52io_report *prev) { - // TODO: Process changes +#if defined(HAVE_EVDEV) + if (x52d_clutch_process(report, prev)) { + /* Clutch UI consumed input (profile selection mode) */ + memcpy(prev, report, sizeof(*prev)); + return; + } + x52d_profile_apply(report, prev); +#endif x52d_mouse_report_event(report); memcpy(prev, report, sizeof(*prev)); } diff --git a/daemon/x52d_keyboard.h b/daemon/x52d_keyboard.h new file mode 100644 index 0000000..728d39c --- /dev/null +++ b/daemon/x52d_keyboard.h @@ -0,0 +1,62 @@ +/* + * Saitek X52 Pro MFD & LED driver - Virtual keyboard + * + * Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#ifndef X52D_KEYBOARD_H +#define X52D_KEYBOARD_H + +#include +#include + +/** + * @file x52d_keyboard.h + * @brief Virtual keyboard (uinput) for profile-generated key events + */ + +#if defined(HAVE_EVDEV) + +/** + * @brief Initialize the virtual keyboard device. + * + * Creates a uinput keyboard that can emit key events. Must be called + * before x52d_keyboard_evdev_key(). + */ +void x52d_keyboard_evdev_init(void); + +/** + * @brief Shut down and destroy the virtual keyboard device. + */ +void x52d_keyboard_evdev_exit(void); + +/** + * @brief Emit a key event (down or up). + * + * @param key_code Linux input event key code (e.g. KEY_A). + * @param value 1 for key down, 0 for key up. + * @return 0 on success, negative errno on failure. + */ +int x52d_keyboard_evdev_key(uint16_t key_code, int value); + +/** + * @brief Return whether the virtual keyboard is available. + */ +bool x52d_keyboard_evdev_available(void); + +#else + +static inline void x52d_keyboard_evdev_init(void) { (void)0; } +static inline void x52d_keyboard_evdev_exit(void) { (void)0; } +static inline int x52d_keyboard_evdev_key(uint16_t key_code, int value) { + (void)key_code; + (void)value; + return 0; +} +static inline bool x52d_keyboard_evdev_available(void) { return false; } + +#endif /* HAVE_EVDEV */ + +#endif /* !defined X52D_KEYBOARD_H */ diff --git a/daemon/x52d_keyboard_evdev.c b/daemon/x52d_keyboard_evdev.c new file mode 100644 index 0000000..255a351 --- /dev/null +++ b/daemon/x52d_keyboard_evdev.c @@ -0,0 +1,93 @@ +/* + * Saitek X52 Pro MFD & LED driver - Virtual keyboard (evdev/uinput) + * + * Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#include "config.h" +#include +#include +#include + +#include "libevdev/libevdev.h" +#include "libevdev/libevdev-uinput.h" +#include "x52d_const.h" +#include "x52d_keyboard.h" + +#define PINELOG_MODULE X52D_MOD_KEYBOARD +#include "pinelog.h" + +/* Maximum key code to enable on the virtual keyboard (covers KEY_* range). */ +#ifndef KEY_MAX +#define KEY_MAX 0x2ff +#endif + +static struct libevdev_uinput *kbd_uidev; +static bool kbd_uidev_created = false; + +int x52d_keyboard_evdev_key(uint16_t key_code, int value) +{ + int rc; + + if (!kbd_uidev_created || kbd_uidev == NULL) { + return -ENODEV; + } + + rc = libevdev_uinput_write_event(kbd_uidev, EV_KEY, key_code, value); + if (rc != 0) { + PINELOG_ERROR(_("Error writing key event (code %u, value %d): %s"), + (unsigned)key_code, value, strerror(-rc)); + return rc; + } + + rc = libevdev_uinput_write_event(kbd_uidev, EV_SYN, SYN_REPORT, 0); + if (rc != 0) { + PINELOG_ERROR(_("Error writing keyboard sync event")); + return rc; + } + + return 0; +} + +bool x52d_keyboard_evdev_available(void) +{ + return kbd_uidev_created; +} + +void x52d_keyboard_evdev_init(void) +{ + int rc; + struct libevdev *dev; + unsigned int code; + + dev = libevdev_new(); + libevdev_set_name(dev, "X52 virtual keyboard"); + libevdev_enable_event_type(dev, EV_KEY); + + for (code = 1; code <= KEY_MAX; code++) { + libevdev_enable_event_code(dev, EV_KEY, code, NULL); + } + + rc = libevdev_uinput_create_from_device(dev, LIBEVDEV_UINPUT_OPEN_MANAGED, + &kbd_uidev); + libevdev_free(dev); + + if (rc != 0) { + PINELOG_ERROR(_("Error %d creating X52 virtual keyboard: %s"), + -rc, strerror(-rc)); + } else { + kbd_uidev_created = true; + PINELOG_INFO(_("X52 virtual keyboard created")); + } +} + +void x52d_keyboard_evdev_exit(void) +{ + kbd_uidev_created = false; + if (kbd_uidev != NULL) { + libevdev_uinput_destroy(kbd_uidev); + kbd_uidev = NULL; + } +} diff --git a/daemon/x52d_main.c b/daemon/x52d_main.c index 9b70164..7ffef23 100644 --- a/daemon/x52d_main.c +++ b/daemon/x52d_main.c @@ -21,7 +21,9 @@ #include "x52d_config.h" #include "x52d_device.h" #include "x52d_io.h" +#include "x52d_keyboard.h" #include "x52d_mouse.h" +#include "x52d_profile.h" #include "x52d_command.h" #include "x52d_notify.h" #include "x52dcomm-internal.h" @@ -327,6 +329,8 @@ int main(int argc, char **argv) #if defined(HAVE_EVDEV) x52d_io_init(); x52d_mouse_evdev_init(); + x52d_keyboard_evdev_init(); + x52d_profile_init(); #endif // Re-enable signals @@ -367,6 +371,8 @@ cleanup: x52d_command_exit(); x52d_notify_exit(); #if defined(HAVE_EVDEV) + x52d_profile_exit(); + x52d_keyboard_evdev_exit(); x52d_mouse_evdev_exit(); x52d_io_exit(); #endif diff --git a/daemon/x52d_profile.c b/daemon/x52d_profile.c new file mode 100644 index 0000000..3184a6d --- /dev/null +++ b/daemon/x52d_profile.c @@ -0,0 +1,460 @@ +/* + * Saitek X52 Pro MFD & LED driver - Profile (button-to-keyboard mapping) + * + * Copyright (C) 2021 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 "libevdev/libevdev.h" +#include "libx52io.h" +#include "ini.h" + +#define PINELOG_MODULE X52D_MOD_PROFILE +#include "pinelog.h" + +#include "x52d_config.h" +#include "x52d_const.h" +#include "x52d_keyboard.h" +#include "x52d_profile.h" + +#define NUM_LAYERS 6 /* 3 modes × 2 (no shift / shift) */ +#define MAX_FALLBACK 4 /* max layers in a fallback chain */ +#define BUTTON_PREFIX "Button." +#define PROFILE_SECTION "Profile" +#define PROFILE_NAME_KEY "Name" +#define SHIFT_BUTTON_KEY "ShiftButton" +#define DEFAULT_SHIFT_BUTTON "BTN_PINKY" +#define PROFILE_NAME_LEN 128 +#define SHIFT_BUTTON_STR_LEN 64 +#define BUTTON_PREFIX_LEN (sizeof(BUTTON_PREFIX) - 1) +#define KEY_PREFIX "key" +#define MACRO_PREFIX "macro" +#define MAX_MACRO_KEYS 32 + +typedef enum { + ACTION_NONE, + ACTION_KEY, + ACTION_MACRO +} action_type_t; + +typedef struct { + action_type_t type; + size_t key_len; /* for ACTION_KEY: number of keys in combo */ + uint16_t *key_codes; /* for ACTION_KEY, may be NULL */ + size_t macro_len; + uint16_t *macro_keys; /* for ACTION_MACRO, may be NULL */ +} profile_action_t; + +static profile_action_t layers[NUM_LAYERS][LIBX52IO_BUTTON_MAX]; +static int shift_button_index = -1; /* -1 = no shift button resolved */ +static bool profile_loaded = false; +/* [Profile] section options; set during parse, resolved after. */ +static char profile_name_str[PROFILE_NAME_LEN]; +static char profile_shift_button_str[SHIFT_BUTTON_STR_LEN]; + +static int button_name_to_index(const char *name) +{ + libx52io_button b; + const char *str; + + if (name == NULL || *name == '\0') { + return -1; + } + for (b = LIBX52IO_BTN_TRIGGER; b < LIBX52IO_BUTTON_MAX; b++) { + str = libx52io_button_to_str(b); + if (str != NULL && strcasecmp(str, name) == 0) { + return (int)b; + } + } + return -1; +} + +static int key_name_to_code(const char *name) +{ + int code; + + if (name == NULL || *name == '\0') { + return -1; + } + code = libevdev_event_code_from_name(EV_KEY, name); + return (code >= 0) ? code : -1; +} + +/** + * Parse value into a single key code (action KEY) or macro (action MACRO). + * Returns 0 on success, -1 on parse error. + */ +static int parse_action_value(const char *value, profile_action_t *out) +{ + char buf[256]; + char *tok; + char *p; + uint16_t keys[MAX_MACRO_KEYS]; + size_t n = 0; + int code; + + memset(out, 0, sizeof(*out)); + out->type = ACTION_NONE; + + if (value == NULL) { + return -1; + } + strncpy(buf, value, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + p = buf; + while (*p == ' ') p++; + if (*p == '\0') { + return -1; + } + + tok = (char *)p; + while (*p != '\0' && *p != ' ') p++; + if (*p != '\0') { + *p = '\0'; + p++; + } + + if (strcasecmp(tok, KEY_PREFIX) == 0) { + /* Parse key combo: key KEY_MOD KEY_KEY [KEY_KEY ...] e.g. key KEY_LEFTCTRL KEY_X */ + n = 0; + while (n < MAX_MACRO_KEYS) { + while (*p == ' ') p++; + if (*p == '\0') break; + tok = (char *)p; + while (*p != '\0' && *p != ' ') p++; + if (*p != '\0') { *p = '\0'; p++; } + code = key_name_to_code(tok); + if (code < 0) return -1; + keys[n++] = (uint16_t)code; + } + if (n == 0) return -1; + out->key_codes = malloc(n * sizeof(uint16_t)); + if (out->key_codes == NULL) return -1; + memcpy(out->key_codes, keys, n * sizeof(uint16_t)); + out->key_len = n; + out->type = ACTION_KEY; + return 0; + } + + if (strcasecmp(tok, MACRO_PREFIX) == 0) { + while (n < MAX_MACRO_KEYS) { + while (*p == ' ') p++; + if (*p == '\0') { + break; + } + tok = (char *)p; + while (*p != '\0' && *p != ' ') p++; + if (*p != '\0') { + *p = '\0'; + p++; + } + code = key_name_to_code(tok); + if (code < 0) { + return -1; + } + keys[n++] = (uint16_t)code; + } + if (n == 0) { + return -1; + } + out->macro_keys = malloc(n * sizeof(uint16_t)); + if (out->macro_keys == NULL) { + return -1; + } + memcpy(out->macro_keys, keys, n * sizeof(uint16_t)); + out->macro_len = n; + out->type = ACTION_MACRO; + return 0; + } + + return -1; +} + +static int section_to_layer(const char *section) +{ + if (section == NULL) { + return -1; + } + if (strcasecmp(section, "Mode1") == 0) return 0; + if (strcasecmp(section, "Mode2") == 0) return 2; + if (strcasecmp(section, "Mode3") == 0) return 4; + if (strcasecmp(section, "Mode1.Shift") == 0) return 1; + if (strcasecmp(section, "Mode2.Shift") == 0) return 3; + if (strcasecmp(section, "Mode3.Shift") == 0) return 5; + return -1; +} + +static void free_action(profile_action_t *a) +{ + if (a->type == ACTION_KEY && a->key_codes != NULL) { + free(a->key_codes); + a->key_codes = NULL; + a->key_len = 0; + } + if (a->type == ACTION_MACRO && a->macro_keys != NULL) { + free(a->macro_keys); + a->macro_keys = NULL; + a->macro_len = 0; + } + a->type = ACTION_NONE; +} + +static int profile_ini_handler(void *user, const char *section, const char *key, + const char *value) +{ + int layer; + int btn; + profile_action_t action; + profile_action_t (*arr)[NUM_LAYERS][LIBX52IO_BUTTON_MAX] = user; + + /* [Profile] section: profile-wide options (Name, ShiftButton) */ + if (section != NULL && strcasecmp(section, PROFILE_SECTION) == 0) { + if (key != NULL && value != NULL) { + if (strcasecmp(key, PROFILE_NAME_KEY) == 0) { + strncpy(profile_name_str, value, PROFILE_NAME_LEN - 1); + profile_name_str[PROFILE_NAME_LEN - 1] = '\0'; + } else if (strcasecmp(key, SHIFT_BUTTON_KEY) == 0) { + strncpy(profile_shift_button_str, value, SHIFT_BUTTON_STR_LEN - 1); + profile_shift_button_str[SHIFT_BUTTON_STR_LEN - 1] = '\0'; + } + } + return 1; + } + + layer = section_to_layer(section); + if (layer < 0) { + return 1; /* unknown section, ignore */ + } + + if (key == NULL || value == NULL) { + return 1; + } + if (strncasecmp(key, BUTTON_PREFIX, BUTTON_PREFIX_LEN) != 0) { + return 1; + } + key += BUTTON_PREFIX_LEN; + btn = button_name_to_index(key); + if (btn < 0) { + PINELOG_INFO(_("Unknown button name in profile: %s"), key); + return 1; + } + + if (parse_action_value(value, &action) != 0) { + PINELOG_INFO(_("Invalid action value for %s.%s: %s"), section, key, value); + return 1; + } + + free_action(&(*arr)[layer][btn]); + (*arr)[layer][btn] = action; + return 1; +} + +static void clear_all_layers(void) +{ + int layer; + libx52io_button btn; + + for (layer = 0; layer < NUM_LAYERS; layer++) { + for (btn = LIBX52IO_BTN_TRIGGER; btn < LIBX52IO_BUTTON_MAX; btn++) { + free_action(&layers[layer][btn]); + } + } +} + +static void load_profile(void) +{ + const char *dir; + const char *name; + char path[PATH_MAX]; + int rc; + + clear_all_layers(); + profile_loaded = false; + shift_button_index = -1; + profile_name_str[0] = '\0'; + profile_shift_button_str[0] = '\0'; + + dir = x52d_config_get("Profiles", "Directory"); + name = x52d_config_get("Profiles", "Profile"); + if (dir == NULL || *dir == '\0' || name == NULL || *name == '\0') { + PINELOG_INFO(_("Profile directory or name not set, using empty profile")); + return; + } + + rc = snprintf(path, sizeof(path), "%s/%s.conf", dir, name); + if (rc < 0 || (size_t)rc >= sizeof(path)) { + PINELOG_ERROR(_("Profile path too long")); + return; + } + + rc = ini_parse(path, profile_ini_handler, &layers); + if (rc < 0) { + PINELOG_ERROR(_("Failed to load profile %s: %s"), path, strerror(errno)); + return; + } + if (rc > 0) { + PINELOG_WARN(_("Profile %s had %d parse errors"), path, rc); + } + + profile_loaded = true; + + if (profile_name_str[0] != '\0') { + PINELOG_INFO(_("Loaded profile: %s"), profile_name_str); + } + + /* Resolve shift button from [Profile] ShiftButton; default BTN_PINKY if not set */ + name = (profile_shift_button_str[0] != '\0') + ? profile_shift_button_str + : DEFAULT_SHIFT_BUTTON; + shift_button_index = button_name_to_index(name); + if (shift_button_index < 0) { + PINELOG_WARN(_("Unknown ShiftButton '%s' in profile, shift disabled"), name); + } +} + +void x52d_profile_init(void) +{ + load_profile(); + PINELOG_INFO(_("Profile module initialized")); +} + +const char *x52d_profile_get_name(void) +{ + if (!profile_loaded || profile_name_str[0] == '\0') { + return NULL; + } + return profile_name_str; +} + +void x52d_profile_exit(void) +{ + clear_all_layers(); + shift_button_index = -1; + profile_name_str[0] = '\0'; + profile_shift_button_str[0] = '\0'; + profile_loaded = false; + PINELOG_INFO(_("Profile module shut down")); +} + +/* + * Fallback chain per (mode, shift): try these layer indices in order; + * use the first that has a mapping for the button. -1 terminates. + * Layer order: 0=Mode1, 1=Mode1.Shift, 2=Mode2, 3=Mode2.Shift, 4=Mode3, 5=Mode3.Shift. + * - Shift: ModeN.Shift falls back to ModeN. + * - Mode: Mode2 falls back to Mode1; Mode3 falls back to Mode2 then Mode1. + */ +static const int fallback_chain[NUM_LAYERS][MAX_FALLBACK] = { + { 0, -1, -1, -1 }, /* mode 1, no shift: Mode1 only */ + { 1, 0, -1, -1 }, /* mode 1, shift: Mode1.Shift then Mode1 */ + { 2, 0, -1, -1 }, /* mode 2, no shift: Mode2 then Mode1 */ + { 3, 2, 0, -1 }, /* mode 2, shift: Mode2.Shift then Mode2 then Mode1 */ + { 4, 2, 0, -1 }, /* mode 3, no shift: Mode3 then Mode2 then Mode1 */ + { 5, 4, 2, 0 }, /* mode 3, shift: Mode3.Shift then Mode3 then Mode2 then Mode1 */ +}; + +static unsigned int get_layer_index(const libx52io_report *report) +{ + unsigned int mode; + bool shift; + + mode = report->mode; + if (mode < 1) { + mode = 1; + } + if (mode > 3) { + mode = 3; + } + shift = (shift_button_index >= 0 && + shift_button_index < LIBX52IO_BUTTON_MAX && + report->button[shift_button_index]); + return (mode - 1u) * 2u + (shift ? 1u : 0u); +} + +/* Return the first non-NONE action for btn in the fallback chain for this report. */ +static const profile_action_t *get_action_for_button(const libx52io_report *report, + libx52io_button btn) +{ + unsigned int chain_index; + int layer; + const profile_action_t *a; + int i; + + chain_index = get_layer_index(report); + for (i = 0; i < MAX_FALLBACK; i++) { + layer = fallback_chain[chain_index][i]; + if (layer < 0) { + break; + } + a = &layers[layer][btn]; + if (a->type != ACTION_NONE) { + return a; + } + } + return NULL; +} + +static void emit_macro(const profile_action_t *a) +{ + size_t i; + + if (a->type != ACTION_MACRO || a->macro_keys == NULL || !x52d_keyboard_evdev_available()) { + return; + } + for (i = 0; i < a->macro_len; i++) { + x52d_keyboard_evdev_key(a->macro_keys[i], 1); + x52d_keyboard_evdev_key(a->macro_keys[i], 0); + } +} + +void x52d_profile_apply(const libx52io_report *report, + const libx52io_report *prev) +{ + libx52io_button btn; + const profile_action_t *a; + bool pressed; + bool was_pressed; + + if (!profile_loaded || !x52d_keyboard_evdev_available()) { + return; + } + + for (btn = LIBX52IO_BTN_TRIGGER; btn < LIBX52IO_BUTTON_MAX; btn++) { + was_pressed = prev->button[btn]; + pressed = report->button[btn]; + if (pressed == was_pressed) { + continue; + } + + a = get_action_for_button(report, btn); + if (a == NULL) { + continue; + } + + if (pressed) { + if (a->type == ACTION_KEY && a->key_codes != NULL) { + size_t i; + for (i = 0; i < a->key_len; i++) { + x52d_keyboard_evdev_key(a->key_codes[i], 1); + } + } else if (a->type == ACTION_MACRO) { + emit_macro(a); + } + } else { + if (a->type == ACTION_KEY && a->key_codes != NULL) { + size_t i; + for (i = a->key_len; i > 0; i--) { + x52d_keyboard_evdev_key(a->key_codes[i - 1], 0); + } + } + /* macro: nothing on release */ + } + } +} diff --git a/daemon/x52d_profile.h b/daemon/x52d_profile.h new file mode 100644 index 0000000..9c56f4e --- /dev/null +++ b/daemon/x52d_profile.h @@ -0,0 +1,71 @@ +/* + * Saitek X52 Pro MFD & LED driver - Profile (button-to-keyboard mapping) + * + * Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#ifndef X52D_PROFILE_H +#define X52D_PROFILE_H + +#include "libx52io.h" + +/** + * @file x52d_profile.h + * @brief Profile loading and application: map joystick buttons to keyboard events + */ + +#if defined(HAVE_EVDEV) + +/** + * @brief Initialize the profile module and load the active profile. + * + * Reads Profiles.Directory and Profiles.Profile from config, loads + * the profile file, and resolves ShiftButton from the [Profile] section. If loading + * fails, the profile is empty (no mappings). + */ +void x52d_profile_init(void); + +/** + * @brief Shut down the profile module and free resources. + */ +void x52d_profile_exit(void); + +/** + * @brief Return the profile display name from the loaded profile file. + * + * The name is set in the profile file with [Profile] Name = ... + * @return The profile name, or NULL if none set or no profile loaded. + * Valid until x52d_profile_exit() or next x52d_profile_init(). + */ +const char *x52d_profile_get_name(void); + +/** + * @brief Apply profile mappings to button state changes. + * + * Compares @a report with @a prev and emits keyboard events for any + * button that has a mapping in the current (mode, shift) layer. + * Single-key mappings: key down on press, key up on release. + * Macro mappings: sequence of key down/up on button down only. + * + * @param report Current joystick report. + * @param prev Previous report (for edge detection). + */ +void x52d_profile_apply(const libx52io_report *report, + const libx52io_report *prev); + +#else + +static inline void x52d_profile_init(void) { (void)0; } +static inline void x52d_profile_exit(void) { (void)0; } +static inline const char *x52d_profile_get_name(void) { return NULL; } +static inline void x52d_profile_apply(const libx52io_report *report, + const libx52io_report *prev) { + (void)report; + (void)prev; +} + +#endif /* HAVE_EVDEV */ + +#endif /* !defined X52D_PROFILE_H */ diff --git a/daemon/x52d_profile_test.c b/daemon/x52d_profile_test.c new file mode 100644 index 0000000..791250a --- /dev/null +++ b/daemon/x52d_profile_test.c @@ -0,0 +1,325 @@ +/* + * Saitek X52 Pro MFD & LED driver - Profile module tests + * + * Copyright (C) 2021 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 + +#include "libevdev/libevdev.h" +#include "libx52io.h" + +#define PINELOG_MODULE X52D_MOD_PROFILE +#include "pinelog.h" + +#include "x52d_config.h" +#include "x52d_const.h" +#include "x52d_keyboard.h" +#include "x52d_profile.h" + +/* Stub keyboard: record (key_code, value) for assertions */ +#define MAX_RECORDED 32 +static struct { uint16_t code; int value; } key_record[MAX_RECORDED]; +static size_t key_record_n; + +int x52d_keyboard_evdev_key(uint16_t key_code, int value) +{ + if (key_record_n < MAX_RECORDED) { + key_record[key_record_n].code = key_code; + key_record[key_record_n].value = value; + key_record_n++; + } + return 0; +} + +bool x52d_keyboard_evdev_available(void) +{ + return true; +} + +static void key_record_reset(void) +{ + key_record_n = 0; +} + +static int key_code_from_name(const char *name) +{ + int c = libevdev_event_code_from_name(EV_KEY, name); + return (c >= 0) ? c : -1; +} + +static void write_profile_file(const char *path, const char *content) +{ + FILE *f = fopen(path, "w"); + assert_non_null(f); + assert_true(fputs(content, f) >= 0); + fclose(f); +} + +static void test_profile_single_key(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report, prev; + + (void)state; + assert_non_null(mkdtemp(tmpdir)); + + snprintf(path, sizeof(path), "%s/test.conf", tmpdir); + write_profile_file(path, + "[Mode1]\n" + "Button.BTN_FIRE = key KEY_E\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + report.mode = 1; + report.button[LIBX52IO_BTN_FIRE] = true; + + key_record_reset(); + x52d_profile_apply(&report, &prev); + + assert_int_equal(key_record_n, 1); + assert_int_equal(key_record[0].code, key_code_from_name("KEY_E")); + assert_int_equal(key_record[0].value, 1); + + memcpy(&prev, &report, sizeof(report)); + report.button[LIBX52IO_BTN_FIRE] = false; + key_record_reset(); + x52d_profile_apply(&report, &prev); + + assert_int_equal(key_record_n, 1); + assert_int_equal(key_record[0].code, key_code_from_name("KEY_E")); + assert_int_equal(key_record[0].value, 0); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + +static void test_profile_key_combo(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report, prev; + + (void)state; + assert_non_null(mkdtemp(tmpdir)); + + snprintf(path, sizeof(path), "%s/test.conf", tmpdir); + write_profile_file(path, + "[Mode1]\n" + "Button.BTN_A = key KEY_LEFTCTRL KEY_X\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + report.mode = 1; + report.button[LIBX52IO_BTN_A] = true; + + key_record_reset(); + x52d_profile_apply(&report, &prev); + + assert_int_equal(key_record_n, 2); + assert_int_equal(key_record[0].code, key_code_from_name("KEY_LEFTCTRL")); + assert_int_equal(key_record[0].value, 1); + assert_int_equal(key_record[1].code, key_code_from_name("KEY_X")); + assert_int_equal(key_record[1].value, 1); + + memcpy(&prev, &report, sizeof(report)); + report.button[LIBX52IO_BTN_A] = false; + key_record_reset(); + x52d_profile_apply(&report, &prev); + + assert_int_equal(key_record_n, 2); + assert_int_equal(key_record[0].code, key_code_from_name("KEY_X")); + assert_int_equal(key_record[0].value, 0); + assert_int_equal(key_record[1].code, key_code_from_name("KEY_LEFTCTRL")); + assert_int_equal(key_record[1].value, 0); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + +static void test_profile_macro_button_down_only(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report, prev; + + (void)state; + assert_non_null(mkdtemp(tmpdir)); + + snprintf(path, sizeof(path), "%s/test.conf", tmpdir); + write_profile_file(path, + "[Mode1]\n" + "Button.BTN_T1_UP = macro KEY_A KEY_B\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + report.mode = 1; + report.button[LIBX52IO_BTN_T1_UP] = true; + + key_record_reset(); + x52d_profile_apply(&report, &prev); + + assert_int_equal(key_record_n, 4); + assert_int_equal(key_record[0].code, key_code_from_name("KEY_A")); + assert_int_equal(key_record[0].value, 1); + assert_int_equal(key_record[1].code, key_code_from_name("KEY_A")); + assert_int_equal(key_record[1].value, 0); + assert_int_equal(key_record[2].code, key_code_from_name("KEY_B")); + assert_int_equal(key_record[2].value, 1); + assert_int_equal(key_record[3].code, key_code_from_name("KEY_B")); + assert_int_equal(key_record[3].value, 0); + + memcpy(&prev, &report, sizeof(report)); + report.button[LIBX52IO_BTN_T1_UP] = false; + key_record_reset(); + x52d_profile_apply(&report, &prev); + assert_int_equal(key_record_n, 0); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + +static void test_profile_fallback_mode2_to_mode1(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report, prev; + + (void)state; + assert_non_null(mkdtemp(tmpdir)); + + snprintf(path, sizeof(path), "%s/test.conf", tmpdir); + write_profile_file(path, + "[Mode1]\n" + "Button.BTN_FIRE = key KEY_E\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + report.mode = 2; + report.button[LIBX52IO_BTN_FIRE] = true; + + key_record_reset(); + x52d_profile_apply(&report, &prev); + + assert_int_equal(key_record_n, 1); + assert_int_equal(key_record[0].code, key_code_from_name("KEY_E")); + assert_int_equal(key_record[0].value, 1); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + +static void test_profile_shift_layer(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report, prev; + + (void)state; + assert_non_null(mkdtemp(tmpdir)); + + snprintf(path, sizeof(path), "%s/test.conf", tmpdir); + write_profile_file(path, + "[Profile]\n" + "ShiftButton=BTN_PINKY\n" + "[Mode1]\n" + "Button.BTN_FIRE = key KEY_E\n" + "[Mode1.Shift]\n" + "Button.BTN_FIRE = key KEY_F\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + report.mode = 1; + report.button[LIBX52IO_BTN_PINKY] = true; + report.button[LIBX52IO_BTN_FIRE] = true; + + key_record_reset(); + x52d_profile_apply(&report, &prev); + + assert_int_equal(key_record_n, 1); + assert_int_equal(key_record[0].code, key_code_from_name("KEY_F")); + assert_int_equal(key_record[0].value, 1); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + +static void test_profile_get_name(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + const char *name; + + (void)state; + assert_non_null(mkdtemp(tmpdir)); + + snprintf(path, sizeof(path), "%s/test.conf", tmpdir); + write_profile_file(path, + "[Profile]\n" + "Name=My Test Profile\n" + "[Mode1]\n" + "Button.BTN_FIRE = key KEY_E\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + name = x52d_profile_get_name(); + assert_non_null(name); + assert_string_equal(name, "My Test Profile"); + + x52d_profile_exit(); + name = x52d_profile_get_name(); + assert_null(name); + unlink(path); + rmdir(tmpdir); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_profile_single_key), + cmocka_unit_test(test_profile_key_combo), + cmocka_unit_test(test_profile_macro_button_down_only), + cmocka_unit_test(test_profile_fallback_mode2_to_mode1), + cmocka_unit_test(test_profile_shift_layer), + cmocka_unit_test(test_profile_get_name), + }; + cmocka_set_message_output(CM_OUTPUT_TAP); + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/po/POTFILES.in b/po/POTFILES.in index 70c78d3..3302f68 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -14,6 +14,7 @@ joytest/x52_test_mfd.c daemon/x52d_main.c daemon/x52d_client.c +daemon/x52d_clutch.c daemon/x52d_clock.c daemon/x52d_command.c daemon/x52d_config.c @@ -21,7 +22,9 @@ daemon/x52d_config_dump.c daemon/x52d_config_parser.c daemon/x52d_device.c daemon/x52d_io.c +daemon/x52d_keyboard_evdev.c daemon/x52d_mouse.c daemon/x52d_mouse_evdev.c daemon/x52d_notify.c +daemon/x52d_profile.c daemon/x52ctl.c diff --git a/po/libx52.pot b/po/libx52.pot index 2b72451..238f8cb 100644 --- a/po/libx52.pot +++ b/po/libx52.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: libx52 0.3.2\n" "Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\n" -"POT-Creation-Date: 2024-06-09 20:24-0700\n" +"POT-Creation-Date: 2026-02-26 22:35-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -493,17 +493,17 @@ msgstr "" msgid "OK" msgstr "" -#: daemon/x52d_main.c:64 +#: daemon/x52d_main.c:66 #, c-format msgid "Error %d setting log file: %s\n" msgstr "" -#: daemon/x52d_main.c:80 +#: daemon/x52d_main.c:82 #, c-format msgid "Error %d installing handler for signal %d: %s" msgstr "" -#: daemon/x52d_main.c:91 +#: daemon/x52d_main.c:93 #, c-format msgid "" "Usage: %s [-f] [-v] [-q]\n" @@ -513,88 +513,88 @@ msgid "" "\t[-b notify-socket-path]\n" msgstr "" -#: daemon/x52d_main.c:124 +#: daemon/x52d_main.c:126 #, c-format msgid "Daemon is already running as PID %u" msgstr "" -#: daemon/x52d_main.c:266 +#: daemon/x52d_main.c:268 #, c-format msgid "Unable to parse configuration override '%s'\n" msgstr "" -#: daemon/x52d_main.c:298 +#: daemon/x52d_main.c:300 #, c-format msgid "Foreground = %s" msgstr "" -#: daemon/x52d_main.c:298 daemon/x52d_main.c:299 +#: daemon/x52d_main.c:300 daemon/x52d_main.c:301 msgid "true" msgstr "" -#: daemon/x52d_main.c:298 daemon/x52d_main.c:299 +#: daemon/x52d_main.c:300 daemon/x52d_main.c:301 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" +msgid "Quiet = %s" msgstr "" #: daemon/x52d_main.c:302 #, c-format -msgid "Config file = %s" +msgid "Verbosity = %d" msgstr "" #: daemon/x52d_main.c:303 #, c-format -msgid "PID file = %s" +msgid "Log file = %s" msgstr "" #: daemon/x52d_main.c:304 #, c-format -msgid "Command socket = %s" +msgid "Config file = %s" msgstr "" #: daemon/x52d_main.c:305 #, c-format +msgid "PID file = %s" +msgstr "" + +#: daemon/x52d_main.c:306 +#, c-format +msgid "Command socket = %s" +msgstr "" + +#: daemon/x52d_main.c:307 +#, c-format msgid "Notify socket = %s" msgstr "" -#: daemon/x52d_main.c:316 +#: daemon/x52d_main.c:318 #, c-format msgid "Error %d blocking signals on child threads: %s" msgstr "" -#: daemon/x52d_main.c:335 +#: daemon/x52d_main.c:339 #, c-format msgid "Error %d unblocking signals on child threads: %s" msgstr "" -#: daemon/x52d_main.c:348 +#: daemon/x52d_main.c:352 msgid "Reloading X52 configuration" msgstr "" -#: daemon/x52d_main.c:355 +#: daemon/x52d_main.c:359 msgid "Saving X52 configuration to disk" msgstr "" -#: daemon/x52d_main.c:361 +#: daemon/x52d_main.c:365 #, c-format msgid "Received termination signal %s" msgstr "" -#: daemon/x52d_main.c:378 +#: daemon/x52d_main.c:384 msgid "Shutting down X52 daemon" msgstr "" @@ -622,6 +622,16 @@ msgstr "" msgid "Timed out when polling" msgstr "" +#: daemon/x52d_clutch.c:96 +#, c-format +msgid "Cannot open profiles directory %s: %s" +msgstr "" + +#: daemon/x52d_clutch.c:207 +#, c-format +msgid "Profile switched to: %s" +msgstr "" + #: daemon/x52d_clock.c:28 #, c-format msgid "Setting clock enable to %s" @@ -676,41 +686,41 @@ msgstr "" msgid "Shutting down X52 clock manager thread" msgstr "" -#: daemon/x52d_command.c:367 +#: daemon/x52d_command.c:370 #, c-format msgid "Error reading from client %d: %s" msgstr "" -#: daemon/x52d_command.c:378 +#: daemon/x52d_command.c:381 #, c-format msgid "Short write to client %d; expected %d bytes, wrote %d bytes" msgstr "" -#: daemon/x52d_command.c:402 +#: daemon/x52d_command.c:405 #, c-format msgid "Error %d during command loop: %s" msgstr "" -#: daemon/x52d_command.c:429 +#: daemon/x52d_command.c:432 #, c-format msgid "Error creating command socket: %s" msgstr "" -#: daemon/x52d_command.c:437 +#: daemon/x52d_command.c:440 #, c-format msgid "Error marking command socket as nonblocking: %s" msgstr "" -#: daemon/x52d_command.c:443 +#: daemon/x52d_command.c:446 #, c-format msgid "Error listening on command socket: %s" msgstr "" -#: daemon/x52d_command.c:447 +#: daemon/x52d_command.c:450 msgid "Starting command processing thread" msgstr "" -#: daemon/x52d_command.c:465 +#: daemon/x52d_command.c:468 msgid "Shutting down command processing thread" msgstr "" @@ -808,34 +818,52 @@ msgstr "" msgid "Error %d when updating X52 device: %s" msgstr "" -#: daemon/x52d_io.c:42 +#: daemon/x52d_io.c:52 msgid "Starting X52 I/O thread" msgstr "" -#: daemon/x52d_io.c:64 +#: daemon/x52d_io.c:74 #, c-format msgid "Error %d opening X52 I/O device: %s" msgstr "" -#: daemon/x52d_io.c:75 +#: daemon/x52d_io.c:85 #, c-format msgid "Error %d reading from X52 I/O device: %s" msgstr "" -#: daemon/x52d_io.c:102 +#: daemon/x52d_io.c:112 #, c-format msgid "Error %d initializing X52 I/O library: %s" msgstr "" -#: daemon/x52d_io.c:108 +#: daemon/x52d_io.c:118 #, c-format msgid "Error %d initializing I/O driver thread: %s" msgstr "" -#: daemon/x52d_io.c:115 +#: daemon/x52d_io.c:125 msgid "Shutting down X52 I/O driver thread" msgstr "" +#: daemon/x52d_keyboard_evdev.c:40 +#, c-format +msgid "Error writing key event (code %u, value %d): %s" +msgstr "" + +#: daemon/x52d_keyboard_evdev.c:47 +msgid "Error writing keyboard sync event" +msgstr "" + +#: daemon/x52d_keyboard_evdev.c:78 +#, c-format +msgid "Error %d creating X52 virtual keyboard: %s" +msgstr "" + +#: daemon/x52d_keyboard_evdev.c:82 +msgid "X52 virtual keyboard created" +msgstr "" + #: daemon/x52d_mouse.c:31 #, c-format msgid "Setting mouse enable to %s" @@ -941,6 +969,52 @@ msgstr "" msgid "Error %d initializing notify listener: %s" msgstr "" +#: daemon/x52d_profile.c:246 +#, c-format +msgid "Unknown button name in profile: %s" +msgstr "" + +#: daemon/x52d_profile.c:251 +#, c-format +msgid "Invalid action value for %s.%s: %s" +msgstr "" + +#: daemon/x52d_profile.c:288 +msgid "Profile directory or name not set, using empty profile" +msgstr "" + +#: daemon/x52d_profile.c:294 +msgid "Profile path too long" +msgstr "" + +#: daemon/x52d_profile.c:300 +#, c-format +msgid "Failed to load profile %s: %s" +msgstr "" + +#: daemon/x52d_profile.c:304 +#, c-format +msgid "Profile %s had %d parse errors" +msgstr "" + +#: daemon/x52d_profile.c:310 +#, c-format +msgid "Loaded profile: %s" +msgstr "" + +#: daemon/x52d_profile.c:319 +#, c-format +msgid "Unknown ShiftButton '%s' in profile, shift disabled" +msgstr "" + +#: daemon/x52d_profile.c:326 +msgid "Profile module initialized" +msgstr "" + +#: daemon/x52d_profile.c:344 +msgid "Profile module shut down" +msgstr "" + #: daemon/x52ctl.c:60 #, c-format msgid "Usage: %s [-i] [-s socket-path] [command]\n" diff --git a/po/xx_PL.po b/po/xx_PL.po index 6fd1039..a0a161d 100644 --- a/po/xx_PL.po +++ b/po/xx_PL.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: libx52 0.2.3\n" "Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\n" -"POT-Creation-Date: 2024-06-09 20:24-0700\n" +"POT-Creation-Date: 2026-02-26 22:35-0800\n" "PO-Revision-Date: 2023-01-04 08:40-0800\n" "Last-Translator: Nirenjan Krishnan \n" "Language-Team: Dummy Language for testing i18n\n" @@ -536,17 +536,17 @@ msgstr "Estingtay aracterchay 0x%02x..." msgid "OK" msgstr "OKay" -#: daemon/x52d_main.c:64 +#: daemon/x52d_main.c:66 #, 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:82 #, 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:93 #, c-format msgid "" "Usage: %s [-f] [-v] [-q]\n" @@ -562,88 +562,88 @@ msgstr "" "\t[-b otifynay-ocketsay-athpay]\n" "\n" -#: daemon/x52d_main.c:124 +#: daemon/x52d_main.c:126 #, 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:268 #, 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:300 #, c-format msgid "Foreground = %s" msgstr "Oregroundfay = %s" -#: daemon/x52d_main.c:298 daemon/x52d_main.c:299 +#: daemon/x52d_main.c:300 daemon/x52d_main.c:301 msgid "true" msgstr "uetray" -#: daemon/x52d_main.c:298 daemon/x52d_main.c:299 +#: daemon/x52d_main.c:300 daemon/x52d_main.c:301 msgid "false" msgstr "alsefay" -#: daemon/x52d_main.c:299 +#: daemon/x52d_main.c:301 #, c-format msgid "Quiet = %s" msgstr "Uietqay = %s" -#: daemon/x52d_main.c:300 +#: daemon/x52d_main.c:302 #, c-format msgid "Verbosity = %d" msgstr "Erbosityvay = %d" -#: daemon/x52d_main.c:301 +#: daemon/x52d_main.c:303 #, c-format msgid "Log file = %s" msgstr "Oglay ilefay = %s" -#: daemon/x52d_main.c:302 +#: daemon/x52d_main.c:304 #, c-format msgid "Config file = %s" msgstr "Onfigcay ilefay = %s" -#: daemon/x52d_main.c:303 +#: daemon/x52d_main.c:305 #, c-format msgid "PID file = %s" msgstr "IDPay ilefay = %s" -#: daemon/x52d_main.c:304 +#: daemon/x52d_main.c:306 #, c-format msgid "Command socket = %s" msgstr "Ommandcay ocketsay = %s" -#: daemon/x52d_main.c:305 +#: daemon/x52d_main.c:307 #, c-format msgid "Notify socket = %s" msgstr "Otifynay ocketsay = %s" -#: daemon/x52d_main.c:316 +#: daemon/x52d_main.c:318 #, 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:339 #, 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:352 msgid "Reloading X52 configuration" msgstr "Eloadingray X52 onfigurationcay" -#: daemon/x52d_main.c:355 +#: daemon/x52d_main.c:359 msgid "Saving X52 configuration to disk" msgstr "Avingsay X52 onfigurationcay otay iskday" -#: daemon/x52d_main.c:361 +#: daemon/x52d_main.c:365 #, c-format msgid "Received termination signal %s" msgstr "Eceivedray erminationtay ignalsay %s" -#: daemon/x52d_main.c:378 +#: daemon/x52d_main.c:384 msgid "Shutting down X52 daemon" msgstr "Uttingshay ownday X52 aemonday" @@ -671,6 +671,16 @@ msgstr "Erroray %d enwhay ollingpay %d escriptorsday: %s" msgid "Timed out when polling" msgstr "Imedtay outay enwhay ollingpay" +#: daemon/x52d_clutch.c:96 +#, c-format +msgid "Cannot open profiles directory %s: %s" +msgstr "Annotcay openay ofilespray irectoryday %s: %s" + +#: daemon/x52d_clutch.c:207 +#, c-format +msgid "Profile switched to: %s" +msgstr "Ofilepray itchedsway otay: %s" + #: daemon/x52d_clock.c:28 #, c-format msgid "Setting clock enable to %s" @@ -728,42 +738,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:367 +#: daemon/x52d_command.c:370 #, c-format msgid "Error reading from client %d: %s" msgstr "Erroray eadingray omfray ientclay %d: %s" -#: daemon/x52d_command.c:378 +#: daemon/x52d_command.c:381 #, 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:402 +#: daemon/x52d_command.c:405 #, c-format msgid "Error %d during command loop: %s" msgstr "Erroray %d uringday ommandcay ooplay: %s" -#: daemon/x52d_command.c:429 +#: daemon/x52d_command.c:432 #, c-format msgid "Error creating command socket: %s" msgstr "Erroray eatingcray ommandcay ocketsay: %s" -#: daemon/x52d_command.c:437 +#: daemon/x52d_command.c:440 #, c-format msgid "Error marking command socket as nonblocking: %s" msgstr "Erroray arkingmay ommandcay ocketsay asay onblockingnay: %s" -#: daemon/x52d_command.c:443 +#: daemon/x52d_command.c:446 #, c-format msgid "Error listening on command socket: %s" msgstr "Erroray isteninglay onay ommandcay ocketsay: %s" -#: daemon/x52d_command.c:447 +#: daemon/x52d_command.c:450 msgid "Starting command processing thread" msgstr "Artingstay ommandcay ocessingpray eadthray" -#: daemon/x52d_command.c:465 +#: daemon/x52d_command.c:468 msgid "Shutting down command processing thread" msgstr "Uttingshay ownday ommandcay ocessingpray eadthray" @@ -861,34 +871,52 @@ msgstr "Erroray %d enwhay updatingay X52 arameterpay: %s" 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:52 msgid "Starting X52 I/O thread" msgstr "Artingstay X52 I/O eadthray" -#: daemon/x52d_io.c:64 +#: daemon/x52d_io.c:74 #, 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:85 #, 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:112 #, 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:118 #, 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:125 msgid "Shutting down X52 I/O driver thread" msgstr "Uttingshay ownday X52 I/O iverdray eadthray" +#: daemon/x52d_keyboard_evdev.c:40 +#, c-format +msgid "Error writing key event (code %u, value %d): %s" +msgstr "Erroray itingwray eykay eventay (odecay %u, aluevay %d): %s" + +#: daemon/x52d_keyboard_evdev.c:47 +msgid "Error writing keyboard sync event" +msgstr "Erroray itingwray eyboardkay yncsay eventay" + +#: daemon/x52d_keyboard_evdev.c:78 +#, c-format +msgid "Error %d creating X52 virtual keyboard: %s" +msgstr "Erroray %d eatingcray X52 irtualvay eyboardkay: %s" + +#: daemon/x52d_keyboard_evdev.c:82 +msgid "X52 virtual keyboard created" +msgstr "X52 irtualvay eyboardkey eatedcray" + #: daemon/x52d_mouse.c:31 #, c-format msgid "Setting mouse enable to %s" @@ -994,6 +1022,53 @@ msgstr "Erroray %d initializingay otifynay eadthray: %s" msgid "Error %d initializing notify listener: %s" msgstr "Erroray %d initializingay otifynay istenerlay: %s" +#: daemon/x52d_profile.c:246 +#, c-format +msgid "Unknown button name in profile: %s" +msgstr "Unknownay uttonbay amenay inay ofilepray: %s" + +#: daemon/x52d_profile.c:251 +#, c-format +msgid "Invalid action value for %s.%s: %s" +msgstr "Invaliday actionay aluevay orfay %s.%s: %s" + +#: daemon/x52d_profile.c:288 +msgid "Profile directory or name not set, using empty profile" +msgstr "" +"Ofilepray irectoryday oray amenay otnay etsay, usingay emptyay ofilepray" + +#: daemon/x52d_profile.c:294 +msgid "Profile path too long" +msgstr "Ofilepray athpay ootay onglay" + +#: daemon/x52d_profile.c:300 +#, c-format +msgid "Failed to load profile %s: %s" +msgstr "Ailedfay otay oadlay ofilepray %s: %s" + +#: daemon/x52d_profile.c:304 +#, c-format +msgid "Profile %s had %d parse errors" +msgstr "Ofilepray %s adhay %d arsepay errorsay" + +#: daemon/x52d_profile.c:310 +#, c-format +msgid "Loaded profile: %s" +msgstr "Oadedlay ofilepray: %s" + +#: daemon/x52d_profile.c:319 +#, c-format +msgid "Unknown ShiftButton '%s' in profile, shift disabled" +msgstr "Unknownay iftButtonShay '%s' inay ofilepray, iftshay isabledday" + +#: daemon/x52d_profile.c:326 +msgid "Profile module initialized" +msgstr "Ofilepray odulemay initializeday" + +#: daemon/x52d_profile.c:344 +msgid "Profile module shut down" +msgstr "Ofilepray odulemay utshay ownday" + #: daemon/x52ctl.c:60 #, c-format msgid "Usage: %s [-i] [-s socket-path] [command]\n"