feat: Add action name support

This change adds an optional name at the end of each profile action,
this can be used in the future to update the MFD.
clutch-profile-support
nirenjan 2026-03-03 12:48:01 -08:00
parent 541f8f739c
commit b556d34778
4 changed files with 271 additions and 3 deletions

View File

@ -5,6 +5,7 @@
# Button.X = key KEY_Y [KEY_Z ...] (single key or combo, down on press/up on release) # 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_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) # 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 # Fallback: ModeN.Shift -> ModeN; Mode2 -> Mode1; Mode3 -> Mode2 -> Mode1
[Profile] [Profile]
@ -12,10 +13,10 @@ Name=Example profile
ShiftButton=BTN_PINKY ShiftButton=BTN_PINKY
[Mode1] [Mode1]
# Single key: Fire -> E # Single key: Fire -> E (optional name for UI)
Button.BTN_FIRE = key KEY_E Button.BTN_FIRE = key KEY_E name "Primary Fire"
# Key combo: A -> Ctrl+X (modifiers first, then key) # 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 # Alt+Y
Button.BTN_B = key KEY_LEFTALT KEY_Y Button.BTN_B = key KEY_LEFTALT KEY_Y
# Macro: T1 up -> Ctrl+Shift+T (on button down only) # Macro: T1 up -> Ctrl+Shift+T (on button down only)

View File

@ -36,6 +36,7 @@
#define DEFAULT_SHIFT_BUTTON "BTN_PINKY" #define DEFAULT_SHIFT_BUTTON "BTN_PINKY"
#define PROFILE_NAME_LEN 128 #define PROFILE_NAME_LEN 128
#define SHIFT_BUTTON_STR_LEN 64 #define SHIFT_BUTTON_STR_LEN 64
#define ACTION_NAME_LEN 128
#define BUTTON_PREFIX_LEN (sizeof(BUTTON_PREFIX) - 1) #define BUTTON_PREFIX_LEN (sizeof(BUTTON_PREFIX) - 1)
#define KEY_PREFIX "key" #define KEY_PREFIX "key"
#define MACRO_PREFIX "macro" #define MACRO_PREFIX "macro"
@ -59,6 +60,7 @@ typedef struct {
size_t macro_step_count; size_t macro_step_count;
size_t *macro_step_len; /* length of each step */ size_t *macro_step_len; /* length of each step */
uint16_t *macro_keys; /* flat key codes, may be NULL */ 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; } profile_action_t;
static profile_action_t layers[NUM_LAYERS][LIBX52IO_BUTTON_MAX]; 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; 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). * 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. * Returns 0 on success, -1 on parse error.
*/ */
static int parse_action_value(const char *value, profile_action_t *out) 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; if (*p == '\0') break;
tok = (char *)p; tok = (char *)p;
while (*p != '\0' && *p != ' ') 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++; } if (*p != '\0') { *p = '\0'; p++; }
code = key_name_to_code(tok); code = key_name_to_code(tok);
if (code < 0) return -1; 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)); memcpy(out->key_codes, keys, n * sizeof(uint16_t));
out->key_len = n; out->key_len = n;
out->type = ACTION_KEY; out->type = ACTION_KEY;
parse_optional_action_name(p, out);
return 0; return 0;
} }
@ -173,6 +217,10 @@ static int parse_action_value(const char *value, profile_action_t *out)
if (*p == '\0') break; if (*p == '\0') break;
tok = (char *)p; tok = (char *)p;
while (*p != '\0' && *p != ' ') 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'; if (*p != '\0') *p++ = '\0';
code = key_name_to_code(tok); code = key_name_to_code(tok);
if (code < 0) return -1; 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_len = step_len_alloc;
out->macro_step_count = step_count; out->macro_step_count = step_count;
out->type = ACTION_MACRO; out->type = ACTION_MACRO;
/* p points past last segment; parse optional name from remainder */
parse_optional_action_name(p, out);
return 0; return 0;
} }
@ -272,6 +322,7 @@ static void free_action(profile_action_t *a)
a->macro_len = 0; a->macro_len = 0;
a->macro_step_count = 0; a->macro_step_count = 0;
} }
a->action_name[0] = '\0';
a->type = ACTION_NONE; a->type = ACTION_NONE;
} }
@ -648,6 +699,31 @@ void x52d_profile_macro_wait_drained(void)
pthread_mutex_unlock(&macro_queue.mutex); pthread_mutex_unlock(&macro_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) static void emit_macro(const profile_action_t *a)
{ {
if (a->type != ACTION_MACRO || a->macro_keys == NULL || a->macro_step_len == NULL || if (a->type != ACTION_MACRO || a->macro_keys == NULL || a->macro_step_len == NULL ||

View File

@ -63,6 +63,20 @@ void x52d_profile_apply(const libx52io_report *report,
*/ */
void x52d_profile_macro_wait_drained(void); 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 #else
static inline void x52d_profile_init(void) { (void)0; } 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; (void)prev;
} }
static inline void x52d_profile_macro_wait_drained(void) { (void)0; } 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 */ #endif /* HAVE_EVDEV */

View File

@ -362,6 +362,172 @@ static void test_profile_get_name(void **state)
rmdir(tmpdir); 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) int main(void)
{ {
const struct CMUnitTest tests[] = { 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_fallback_mode2_to_mode1),
cmocka_unit_test(test_profile_shift_layer), cmocka_unit_test(test_profile_shift_layer),
cmocka_unit_test(test_profile_get_name), 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); cmocka_set_message_output(CM_OUTPUT_TAP);
return cmocka_run_group_tests(tests, NULL, NULL); return cmocka_run_group_tests(tests, NULL, NULL);