libx52/daemon/x52d_profile.c

461 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* 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 <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <limits.h>
#include <errno.h>
#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 */
}
}
}