mirror of https://github.com/nirenjan/libx52.git
feat: Add support for keycombos in macros and move to worker thread
This change adds support for key combinations in macro sequences. This allows a profile to support a sequence of key combos such as Ctrl-X, Alt-Y, Shift-Z, Ctrl-Alt-Shift-A, etc. Previously, the profile could only support a single key or key combo in the `key` mode. This allows for a longer key combo sequence. In addition, the macro sequences have been moved to a worker thread, with a fixed delay in between each key press. This simulates a human entering keys on a keyboard, albeit one who is super fast. Also, this avoids blocking the main IO handler for long key sequences.clutch-profile-support
parent
966c6efd5f
commit
541f8f739c
|
|
@ -3,7 +3,8 @@
|
|||
# [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)
|
||||
# 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)
|
||||
# Fallback: ModeN.Shift -> ModeN; Mode2 -> Mode1; Mode3 -> Mode2 -> Mode1
|
||||
|
||||
[Profile]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
#include <stdio.h>
|
||||
#include <limits.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "libevdev/libevdev.h"
|
||||
#include "libx52io.h"
|
||||
|
|
@ -38,6 +40,9 @@
|
|||
#define KEY_PREFIX "key"
|
||||
#define MACRO_PREFIX "macro"
|
||||
#define MAX_MACRO_KEYS 32
|
||||
#define MAX_MACRO_STEPS 32
|
||||
#define MACRO_JOB_QUEUE_SIZE 32
|
||||
#define MACRO_DELAY_MS 20
|
||||
|
||||
typedef enum {
|
||||
ACTION_NONE,
|
||||
|
|
@ -49,8 +54,11 @@ 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 */
|
||||
/* ACTION_MACRO: steps separated by |; each step is one or more keys (combo) */
|
||||
size_t macro_len; /* total key count (flat) */
|
||||
size_t macro_step_count;
|
||||
size_t *macro_step_len; /* length of each step */
|
||||
uint16_t *macro_keys; /* flat key codes, may be NULL */
|
||||
} profile_action_t;
|
||||
|
||||
static profile_action_t layers[NUM_LAYERS][LIBX52IO_BUTTON_MAX];
|
||||
|
|
@ -145,32 +153,85 @@ static int parse_action_value(const char *value, profile_action_t *out)
|
|||
}
|
||||
|
||||
if (strcasecmp(tok, MACRO_PREFIX) == 0) {
|
||||
while (n < MAX_MACRO_KEYS) {
|
||||
size_t step_len_buf[MAX_MACRO_STEPS];
|
||||
size_t step_count = 0;
|
||||
size_t total_keys = 0;
|
||||
size_t *step_len_alloc = NULL;
|
||||
size_t keys_in_step;
|
||||
char *segment_start;
|
||||
char *segment_end;
|
||||
char seg_buf[128];
|
||||
size_t seg_len;
|
||||
char *seg_p;
|
||||
bool has_pipe = (strchr(value, '|') != NULL);
|
||||
|
||||
/* If no |, legacy format: each key is its own step (sequence of single keys) */
|
||||
if (!has_pipe) {
|
||||
n = 0;
|
||||
while (n < MAX_MACRO_KEYS && step_count < MAX_MACRO_STEPS) {
|
||||
while (*p == ' ') p++;
|
||||
if (*p == '\0') break;
|
||||
tok = (char *)p;
|
||||
while (*p != '\0' && *p != ' ') p++;
|
||||
if (*p != '\0') *p++ = '\0';
|
||||
code = key_name_to_code(tok);
|
||||
if (code < 0) return -1;
|
||||
keys[n++] = (uint16_t)code;
|
||||
step_len_buf[step_count++] = 1;
|
||||
total_keys++;
|
||||
}
|
||||
} else {
|
||||
/* Steps separated by |; each step is space-separated key names (single key or combo) */
|
||||
/* First token "macro" was NUL-terminated in buf; skip past it to the rest of the value */
|
||||
p = buf;
|
||||
while (*p != '\0') p++;
|
||||
p++;
|
||||
while (*p == ' ') p++;
|
||||
if (*p == '\0') {
|
||||
break;
|
||||
while (step_count < MAX_MACRO_STEPS && total_keys < MAX_MACRO_KEYS) {
|
||||
while (*p == ' ') p++;
|
||||
if (*p == '\0') break;
|
||||
segment_start = p;
|
||||
while (*p != '\0' && *p != '|') p++;
|
||||
segment_end = p;
|
||||
if (*p == '|') p++;
|
||||
seg_len = (size_t)(segment_end - segment_start);
|
||||
while (seg_len > 0 && segment_start[seg_len - 1] == ' ') seg_len--;
|
||||
if (seg_len == 0) return -1;
|
||||
if (seg_len >= sizeof(seg_buf)) return -1;
|
||||
memcpy(seg_buf, segment_start, seg_len);
|
||||
seg_buf[seg_len] = '\0';
|
||||
seg_p = seg_buf;
|
||||
keys_in_step = 0;
|
||||
while (*seg_p != '\0') {
|
||||
while (*seg_p == ' ') seg_p++;
|
||||
if (*seg_p == '\0') break;
|
||||
tok = seg_p;
|
||||
while (*seg_p != '\0' && *seg_p != ' ') seg_p++;
|
||||
if (*seg_p != '\0') *seg_p++ = '\0';
|
||||
code = key_name_to_code(tok);
|
||||
if (code < 0) return -1;
|
||||
if (total_keys >= MAX_MACRO_KEYS) return -1;
|
||||
keys[total_keys++] = (uint16_t)code;
|
||||
keys_in_step++;
|
||||
}
|
||||
if (keys_in_step == 0) return -1;
|
||||
step_len_buf[step_count++] = keys_in_step;
|
||||
}
|
||||
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) {
|
||||
if (step_count == 0 || total_keys == 0) return -1;
|
||||
out->macro_keys = malloc(total_keys * sizeof(uint16_t));
|
||||
if (out->macro_keys == NULL) return -1;
|
||||
memcpy(out->macro_keys, keys, total_keys * sizeof(uint16_t));
|
||||
step_len_alloc = malloc(step_count * sizeof(size_t));
|
||||
if (step_len_alloc == NULL) {
|
||||
free(out->macro_keys);
|
||||
out->macro_keys = NULL;
|
||||
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;
|
||||
memcpy(step_len_alloc, step_len_buf, step_count * sizeof(size_t));
|
||||
out->macro_len = total_keys;
|
||||
out->macro_step_len = step_len_alloc;
|
||||
out->macro_step_count = step_count;
|
||||
out->type = ACTION_MACRO;
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -199,10 +260,17 @@ static void free_action(profile_action_t *a)
|
|||
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;
|
||||
if (a->type == ACTION_MACRO) {
|
||||
if (a->macro_keys != NULL) {
|
||||
free(a->macro_keys);
|
||||
a->macro_keys = NULL;
|
||||
}
|
||||
if (a->macro_step_len != NULL) {
|
||||
free(a->macro_step_len);
|
||||
a->macro_step_len = NULL;
|
||||
}
|
||||
a->macro_len = 0;
|
||||
a->macro_step_count = 0;
|
||||
}
|
||||
a->type = ACTION_NONE;
|
||||
}
|
||||
|
|
@ -320,8 +388,164 @@ static void load_profile(void)
|
|||
}
|
||||
}
|
||||
|
||||
/* Macro job queue: each entry is one macro (steps; each step is single key or combo). */
|
||||
struct macro_job {
|
||||
uint16_t *keys;
|
||||
size_t *step_len;
|
||||
size_t step_count;
|
||||
};
|
||||
|
||||
static struct {
|
||||
struct macro_job ring[MACRO_JOB_QUEUE_SIZE];
|
||||
unsigned int head;
|
||||
unsigned int tail;
|
||||
unsigned int count;
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t cond;
|
||||
pthread_cond_t drained;
|
||||
pthread_t thread;
|
||||
bool shutdown;
|
||||
bool thread_started;
|
||||
} macro_queue;
|
||||
|
||||
static bool macro_queue_push_job(const uint16_t *keys, const size_t *step_len,
|
||||
size_t step_count)
|
||||
{
|
||||
size_t total_keys = 0;
|
||||
size_t s;
|
||||
uint16_t *keys_copy = NULL;
|
||||
size_t *step_len_copy = NULL;
|
||||
bool ok = false;
|
||||
|
||||
if (step_count == 0 || keys == NULL || step_len == NULL) {
|
||||
return false;
|
||||
}
|
||||
for (s = 0; s < step_count; s++) {
|
||||
total_keys += step_len[s];
|
||||
}
|
||||
if (total_keys == 0) {
|
||||
return false;
|
||||
}
|
||||
keys_copy = malloc(total_keys * sizeof(uint16_t));
|
||||
if (keys_copy == NULL) {
|
||||
return false;
|
||||
}
|
||||
memcpy(keys_copy, keys, total_keys * sizeof(uint16_t));
|
||||
step_len_copy = malloc(step_count * sizeof(size_t));
|
||||
if (step_len_copy == NULL) {
|
||||
free(keys_copy);
|
||||
return false;
|
||||
}
|
||||
memcpy(step_len_copy, step_len, step_count * sizeof(size_t));
|
||||
|
||||
pthread_mutex_lock(¯o_queue.mutex);
|
||||
if (macro_queue.count < MACRO_JOB_QUEUE_SIZE) {
|
||||
macro_queue.ring[macro_queue.tail].keys = keys_copy;
|
||||
macro_queue.ring[macro_queue.tail].step_len = step_len_copy;
|
||||
macro_queue.ring[macro_queue.tail].step_count = step_count;
|
||||
macro_queue.tail = (macro_queue.tail + 1) % MACRO_JOB_QUEUE_SIZE;
|
||||
macro_queue.count++;
|
||||
ok = true;
|
||||
pthread_cond_signal(¯o_queue.cond);
|
||||
}
|
||||
pthread_mutex_unlock(¯o_queue.mutex);
|
||||
if (!ok) {
|
||||
free(step_len_copy);
|
||||
free(keys_copy);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void *macro_worker_thread(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
struct macro_job job = { NULL, 0 };
|
||||
bool got = false;
|
||||
|
||||
pthread_mutex_lock(¯o_queue.mutex);
|
||||
while (macro_queue.count == 0 && !macro_queue.shutdown) {
|
||||
pthread_cond_wait(¯o_queue.cond, ¯o_queue.mutex);
|
||||
}
|
||||
if (macro_queue.shutdown && macro_queue.count == 0) {
|
||||
pthread_cond_broadcast(¯o_queue.drained);
|
||||
pthread_mutex_unlock(¯o_queue.mutex);
|
||||
break;
|
||||
}
|
||||
if (macro_queue.count > 0) {
|
||||
job = macro_queue.ring[macro_queue.head];
|
||||
macro_queue.ring[macro_queue.head].keys = NULL;
|
||||
macro_queue.ring[macro_queue.head].step_len = NULL;
|
||||
macro_queue.ring[macro_queue.head].step_count = 0;
|
||||
macro_queue.head = (macro_queue.head + 1) % MACRO_JOB_QUEUE_SIZE;
|
||||
macro_queue.count--;
|
||||
got = true;
|
||||
}
|
||||
pthread_mutex_unlock(¯o_queue.mutex);
|
||||
|
||||
if (got && job.keys != NULL && job.step_len != NULL) {
|
||||
size_t offset = 0;
|
||||
size_t s;
|
||||
for (s = 0; s < job.step_count; s++) {
|
||||
size_t len = job.step_len[s];
|
||||
size_t i;
|
||||
/* Combo: all keys down in order, delay, all keys up in reverse */
|
||||
for (i = 0; i < len; i++) {
|
||||
x52d_keyboard_evdev_key(job.keys[offset + i], 1);
|
||||
}
|
||||
usleep((useconds_t)MACRO_DELAY_MS * 1000);
|
||||
for (i = len; i > 0; i--) {
|
||||
x52d_keyboard_evdev_key(job.keys[offset + i - 1], 0);
|
||||
}
|
||||
offset += len;
|
||||
usleep((useconds_t)MACRO_DELAY_MS * 1000);
|
||||
}
|
||||
free(job.step_len);
|
||||
free(job.keys);
|
||||
/* Signal drained only after all keys emitted, so wait_drained is accurate */
|
||||
pthread_mutex_lock(¯o_queue.mutex);
|
||||
pthread_cond_broadcast(¯o_queue.drained);
|
||||
pthread_mutex_unlock(¯o_queue.mutex);
|
||||
}
|
||||
}
|
||||
pthread_mutex_lock(¯o_queue.mutex);
|
||||
pthread_cond_broadcast(¯o_queue.drained);
|
||||
pthread_mutex_unlock(¯o_queue.mutex);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void x52d_profile_init(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
macro_queue.head = 0;
|
||||
macro_queue.tail = 0;
|
||||
macro_queue.count = 0;
|
||||
macro_queue.shutdown = false;
|
||||
rc = pthread_mutex_init(¯o_queue.mutex, NULL);
|
||||
if (rc != 0) {
|
||||
PINELOG_ERROR(_("Failed to create macro queue mutex: %s"), strerror(rc));
|
||||
} else {
|
||||
rc = pthread_cond_init(¯o_queue.cond, NULL);
|
||||
if (rc != 0) {
|
||||
pthread_mutex_destroy(¯o_queue.mutex);
|
||||
PINELOG_ERROR(_("Failed to create macro queue cond: %s"), strerror(rc));
|
||||
} else if (pthread_cond_init(¯o_queue.drained, NULL) != 0) {
|
||||
pthread_cond_destroy(¯o_queue.cond);
|
||||
pthread_mutex_destroy(¯o_queue.mutex);
|
||||
PINELOG_ERROR(_("Failed to create macro queue drained cond"));
|
||||
} else {
|
||||
rc = pthread_create(¯o_queue.thread, NULL, macro_worker_thread, NULL);
|
||||
if (rc != 0) {
|
||||
pthread_cond_destroy(¯o_queue.drained);
|
||||
pthread_cond_destroy(¯o_queue.cond);
|
||||
pthread_mutex_destroy(¯o_queue.mutex);
|
||||
PINELOG_ERROR(_("Failed to start macro worker thread: %s"), strerror(rc));
|
||||
} else {
|
||||
macro_queue.thread_started = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
load_profile();
|
||||
PINELOG_INFO(_("Profile module initialized"));
|
||||
}
|
||||
|
|
@ -336,6 +560,17 @@ const char *x52d_profile_get_name(void)
|
|||
|
||||
void x52d_profile_exit(void)
|
||||
{
|
||||
if (macro_queue.thread_started) {
|
||||
pthread_mutex_lock(¯o_queue.mutex);
|
||||
macro_queue.shutdown = true;
|
||||
pthread_cond_broadcast(¯o_queue.cond);
|
||||
pthread_mutex_unlock(¯o_queue.mutex);
|
||||
pthread_join(macro_queue.thread, NULL);
|
||||
pthread_cond_destroy(¯o_queue.drained);
|
||||
pthread_cond_destroy(¯o_queue.cond);
|
||||
pthread_mutex_destroy(¯o_queue.mutex);
|
||||
macro_queue.thread_started = false;
|
||||
}
|
||||
clear_all_layers();
|
||||
shift_button_index = -1;
|
||||
profile_name_str[0] = '\0';
|
||||
|
|
@ -401,16 +636,26 @@ static const profile_action_t *get_action_for_button(const libx52io_report *repo
|
|||
return NULL;
|
||||
}
|
||||
|
||||
static void emit_macro(const profile_action_t *a)
|
||||
void x52d_profile_macro_wait_drained(void)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (a->type != ACTION_MACRO || a->macro_keys == NULL || !x52d_keyboard_evdev_available()) {
|
||||
if (!macro_queue.thread_started) {
|
||||
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);
|
||||
pthread_mutex_lock(¯o_queue.mutex);
|
||||
while (macro_queue.count > 0) {
|
||||
pthread_cond_wait(¯o_queue.drained, ¯o_queue.mutex);
|
||||
}
|
||||
pthread_mutex_unlock(¯o_queue.mutex);
|
||||
}
|
||||
|
||||
static void emit_macro(const profile_action_t *a)
|
||||
{
|
||||
if (a->type != ACTION_MACRO || a->macro_keys == NULL || a->macro_step_len == NULL ||
|
||||
!x52d_keyboard_evdev_available()) {
|
||||
return;
|
||||
}
|
||||
if (!macro_queue_push_job(a->macro_keys, a->macro_step_len, a->macro_step_count)) {
|
||||
PINELOG_WARN(_("Macro queue full, dropping macro"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ const char *x52d_profile_get_name(void);
|
|||
* 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.
|
||||
* Macro mappings: sequence of key down/up on button down only (queued
|
||||
* and emitted by a worker thread with a short delay between events).
|
||||
*
|
||||
* @param report Current joystick report.
|
||||
* @param prev Previous report (for edge detection).
|
||||
|
|
@ -55,6 +56,13 @@ const char *x52d_profile_get_name(void);
|
|||
void x52d_profile_apply(const libx52io_report *report,
|
||||
const libx52io_report *prev);
|
||||
|
||||
/**
|
||||
* @brief Block until all queued macro key events have been emitted.
|
||||
*
|
||||
* Useful for tests or before switching profile so macros finish playing.
|
||||
*/
|
||||
void x52d_profile_macro_wait_drained(void);
|
||||
|
||||
#else
|
||||
|
||||
static inline void x52d_profile_init(void) { (void)0; }
|
||||
|
|
@ -65,6 +73,7 @@ static inline void x52d_profile_apply(const libx52io_report *report,
|
|||
(void)report;
|
||||
(void)prev;
|
||||
}
|
||||
static inline void x52d_profile_macro_wait_drained(void) { (void)0; }
|
||||
|
||||
#endif /* HAVE_EVDEV */
|
||||
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ static void test_profile_macro_button_down_only(void **state)
|
|||
|
||||
key_record_reset();
|
||||
x52d_profile_apply(&report, &prev);
|
||||
|
||||
x52d_profile_macro_wait_drained();
|
||||
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);
|
||||
|
|
@ -204,6 +204,58 @@ static void test_profile_macro_button_down_only(void **state)
|
|||
rmdir(tmpdir);
|
||||
}
|
||||
|
||||
static void test_profile_macro_pipe_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_T1_UP = macro KEY_LEFTCTRL KEY_C | KEY_A\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);
|
||||
x52d_profile_macro_wait_drained();
|
||||
/* Step 1: Ctrl+C (down order: Ctrl, C; up order: C, Ctrl). Step 2: A (down, up) */
|
||||
assert_int_equal(key_record_n, 6);
|
||||
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_C"));
|
||||
assert_int_equal(key_record[1].value, 1);
|
||||
assert_int_equal(key_record[2].code, key_code_from_name("KEY_C"));
|
||||
assert_int_equal(key_record[2].value, 0);
|
||||
assert_int_equal(key_record[3].code, key_code_from_name("KEY_LEFTCTRL"));
|
||||
assert_int_equal(key_record[3].value, 0);
|
||||
assert_int_equal(key_record[4].code, key_code_from_name("KEY_A"));
|
||||
assert_int_equal(key_record[4].value, 1);
|
||||
assert_int_equal(key_record[5].code, key_code_from_name("KEY_A"));
|
||||
assert_int_equal(key_record[5].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";
|
||||
|
|
@ -316,6 +368,7 @@ int main(void)
|
|||
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_macro_pipe_combo),
|
||||
cmocka_unit_test(test_profile_fallback_mode2_to_mode1),
|
||||
cmocka_unit_test(test_profile_shift_layer),
|
||||
cmocka_unit_test(test_profile_get_name),
|
||||
|
|
|
|||
Loading…
Reference in New Issue