diff --git a/daemon/default.conf.example b/daemon/default.conf.example index 7e79ded..e058d3c 100644 --- a/daemon/default.conf.example +++ b/daemon/default.conf.example @@ -5,6 +5,7 @@ # 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: A then B) # Button.X = macro KEY_LEFTCTRL KEY_C | KEY_A (steps separated by |: combo Ctrl+C then key A) +# Optional " name Display Name" or name \"Quoted Name\" for UI/documentation (e.g. "Yaw Left") # Fallback: ModeN.Shift -> ModeN; Mode2 -> Mode1; Mode3 -> Mode2 -> Mode1 [Profile] @@ -12,10 +13,10 @@ Name=Example profile ShiftButton=BTN_PINKY [Mode1] -# Single key: Fire -> E -Button.BTN_FIRE = key KEY_E +# Single key: Fire -> E (optional name for UI) +Button.BTN_FIRE = key KEY_E name "Primary Fire" # Key combo: A -> Ctrl+X (modifiers first, then key) -Button.BTN_A = key KEY_LEFTCTRL KEY_X +Button.BTN_A = key KEY_LEFTCTRL KEY_X name "Cut" # Alt+Y Button.BTN_B = key KEY_LEFTALT KEY_Y # Macro: T1 up -> Ctrl+Shift+T (on button down only) diff --git a/daemon/x52d_profile.c b/daemon/x52d_profile.c index 2762be6..9aea1a9 100644 --- a/daemon/x52d_profile.c +++ b/daemon/x52d_profile.c @@ -36,6 +36,7 @@ #define DEFAULT_SHIFT_BUTTON "BTN_PINKY" #define PROFILE_NAME_LEN 128 #define SHIFT_BUTTON_STR_LEN 64 +#define ACTION_NAME_LEN 128 #define BUTTON_PREFIX_LEN (sizeof(BUTTON_PREFIX) - 1) #define KEY_PREFIX "key" #define MACRO_PREFIX "macro" @@ -59,6 +60,7 @@ typedef struct { size_t macro_step_count; size_t *macro_step_len; /* length of each step */ uint16_t *macro_keys; /* flat key codes, may be NULL */ + char action_name[ACTION_NAME_LEN]; /* optional display name (e.g. "Yaw Left") */ } profile_action_t; static profile_action_t layers[NUM_LAYERS][LIBX52IO_BUTTON_MAX]; @@ -96,8 +98,45 @@ static int key_name_to_code(const char *name) return (code >= 0) ? code : -1; } +/** + * Parse optional " name ..." or " name \"...\"" from remainder at p into out->action_name. + */ +static void parse_optional_action_name(char *p, profile_action_t *out) +{ + char *dst; + size_t n; + + out->action_name[0] = '\0'; + while (*p == ' ') p++; + if (strncasecmp(p, "name", 4) != 0 || (p[4] != ' ' && p[4] != '\0')) { + return; + } + p += 4; + while (*p == ' ') p++; + dst = out->action_name; + n = ACTION_NAME_LEN - 1; + if (*p == '"') { + p++; + while (n > 0 && *p != '\0' && *p != '"') { + *dst++ = *p++; + n--; + } + } else { + while (n > 0 && *p != '\0' && *p != '\n' && *p != '\r') { + *dst++ = *p++; + n--; + } + } + *dst = '\0'; + /* Trim trailing space */ + while (dst > out->action_name && (dst[-1] == ' ' || dst[-1] == '\t')) { + *--dst = '\0'; + } +} + /** * Parse value into a single key code (action KEY) or macro (action MACRO). + * Optional trailing " name Display Name" or " name \"Quoted Name\"". * Returns 0 on success, -1 on parse error. */ static int parse_action_value(const char *value, profile_action_t *out) @@ -138,6 +177,10 @@ static int parse_action_value(const char *value, profile_action_t *out) if (*p == '\0') break; tok = (char *)p; while (*p != '\0' && *p != ' ') p++; + if (n > 0 && (size_t)(p - tok) == 4 && strncasecmp(tok, "name", 4) == 0) { + p = tok; + break; + } if (*p != '\0') { *p = '\0'; p++; } code = key_name_to_code(tok); if (code < 0) return -1; @@ -149,6 +192,7 @@ static int parse_action_value(const char *value, profile_action_t *out) memcpy(out->key_codes, keys, n * sizeof(uint16_t)); out->key_len = n; out->type = ACTION_KEY; + parse_optional_action_name(p, out); return 0; } @@ -173,6 +217,10 @@ static int parse_action_value(const char *value, profile_action_t *out) if (*p == '\0') break; tok = (char *)p; while (*p != '\0' && *p != ' ') p++; + if (n > 0 && (size_t)(p - tok) == 4 && strncasecmp(tok, "name", 4) == 0) { + p = tok; + break; + } if (*p != '\0') *p++ = '\0'; code = key_name_to_code(tok); if (code < 0) return -1; @@ -233,6 +281,8 @@ static int parse_action_value(const char *value, profile_action_t *out) out->macro_step_len = step_len_alloc; out->macro_step_count = step_count; out->type = ACTION_MACRO; + /* p points past last segment; parse optional name from remainder */ + parse_optional_action_name(p, out); return 0; } @@ -272,6 +322,7 @@ static void free_action(profile_action_t *a) a->macro_len = 0; a->macro_step_count = 0; } + a->action_name[0] = '\0'; a->type = ACTION_NONE; } @@ -648,6 +699,31 @@ void x52d_profile_macro_wait_drained(void) pthread_mutex_unlock(¯o_queue.mutex); } +const char *x52d_profile_get_action_name(const libx52io_report *report, + libx52io_button btn) +{ + unsigned int chain_index; + int layer; + const profile_action_t *a; + int i; + + if (!profile_loaded || report == NULL || btn >= LIBX52IO_BUTTON_MAX) { + return NULL; + } + 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->action_name[0] != '\0') ? a->action_name : NULL; + } + } + return NULL; +} + static void emit_macro(const profile_action_t *a) { if (a->type != ACTION_MACRO || a->macro_keys == NULL || a->macro_step_len == NULL || diff --git a/daemon/x52d_profile.h b/daemon/x52d_profile.h index 04b2981..41f36c6 100644 --- a/daemon/x52d_profile.h +++ b/daemon/x52d_profile.h @@ -63,6 +63,20 @@ void x52d_profile_apply(const libx52io_report *report, */ void x52d_profile_macro_wait_drained(void); +/** + * @brief Return the optional display name for the action bound to a button. + * + * Uses the same (mode, shift) layer and fallback as x52d_profile_apply(). + * The name is set in the profile with e.g. "key KEY_A name Yaw Left". + * + * @param report Current joystick report (for mode and shift). + * @param btn Button index. + * @return The action name, or NULL if no mapping or no name set. + * Valid until x52d_profile_exit() or next x52d_profile_init(). + */ +const char *x52d_profile_get_action_name(const libx52io_report *report, + libx52io_button btn); + #else static inline void x52d_profile_init(void) { (void)0; } @@ -74,6 +88,12 @@ static inline void x52d_profile_apply(const libx52io_report *report, (void)prev; } static inline void x52d_profile_macro_wait_drained(void) { (void)0; } +static inline const char *x52d_profile_get_action_name(const libx52io_report *report, + libx52io_button btn) { + (void)report; + (void)btn; + return NULL; +} #endif /* HAVE_EVDEV */ diff --git a/daemon/x52d_profile_test.c b/daemon/x52d_profile_test.c index 4632fc3..655eee1 100644 --- a/daemon/x52d_profile_test.c +++ b/daemon/x52d_profile_test.c @@ -362,6 +362,172 @@ static void test_profile_get_name(void **state) rmdir(tmpdir); } +static void test_profile_action_name_key(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report, prev; + const char *action_name; + + (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 name \"Primary Fire\"\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; + action_name = x52d_profile_get_action_name(&report, LIBX52IO_BTN_FIRE); + assert_non_null(action_name); + assert_string_equal(action_name, "Primary Fire"); + + 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")); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + +static void test_profile_action_name_unquoted(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report; + const char *action_name; + + (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_X name YawLeft\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + memset(&report, 0, sizeof(report)); + report.mode = 1; + action_name = x52d_profile_get_action_name(&report, LIBX52IO_BTN_A); + assert_non_null(action_name); + assert_string_equal(action_name, "YawLeft"); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + +static void test_profile_action_name_none(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report; + const char *action_name; + + (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)); + report.mode = 1; + action_name = x52d_profile_get_action_name(&report, LIBX52IO_BTN_FIRE); + assert_null(action_name); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + +static void test_profile_action_name_shift_layer(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report; + const char *action_name; + + (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 name Unshifted\n" + "[Mode1.Shift]\n" + "Button.BTN_FIRE = key KEY_F name Shifted\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + memset(&report, 0, sizeof(report)); + report.mode = 1; + report.button[LIBX52IO_BTN_PINKY] = false; + action_name = x52d_profile_get_action_name(&report, LIBX52IO_BTN_FIRE); + assert_non_null(action_name); + assert_string_equal(action_name, "Unshifted"); + + report.button[LIBX52IO_BTN_PINKY] = true; + action_name = x52d_profile_get_action_name(&report, LIBX52IO_BTN_FIRE); + assert_non_null(action_name); + assert_string_equal(action_name, "Shifted"); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + +static void test_profile_action_name_macro(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report; + const char *action_name; + + (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 name \"Simple macro\"\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + memset(&report, 0, sizeof(report)); + report.mode = 1; + action_name = x52d_profile_get_action_name(&report, LIBX52IO_BTN_T1_UP); + assert_non_null(action_name); + assert_string_equal(action_name, "Simple macro"); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + int main(void) { const struct CMUnitTest tests[] = { @@ -372,6 +538,11 @@ int main(void) cmocka_unit_test(test_profile_fallback_mode2_to_mode1), cmocka_unit_test(test_profile_shift_layer), cmocka_unit_test(test_profile_get_name), + cmocka_unit_test(test_profile_action_name_key), + cmocka_unit_test(test_profile_action_name_unquoted), + cmocka_unit_test(test_profile_action_name_none), + cmocka_unit_test(test_profile_action_name_shift_layer), + cmocka_unit_test(test_profile_action_name_macro), }; cmocka_set_message_output(CM_OUTPUT_TAP); return cmocka_run_group_tests(tests, NULL, NULL);