From 0583405a06a1870fd9e7f0a24bc3661c67ad10a1 Mon Sep 17 00:00:00 2001 From: jl1990 Date: Mon, 16 Mar 2026 17:41:48 +0100 Subject: [PATCH 1/2] Add multi-profile support with LCD display for SpaceMouse Enterprise Per-application configuration profiles that auto-switch based on WM_CLASS (X11 polling) or client-reported app_id (Wayland-compatible). Each profile stores a full cfg copy so switching is transparent to existing code. New protocol requests: SET_APP_ID, SET_PROFILE, GET_PROFILE for client-side profile detection and manual override. LCD support renders profile name and button mappings as SVG, converts to RGB565 bitmap, compresses with zlib, and sends via USB to the SpaceMouse Enterprise screen. Enabled with --enable-spacelcd (requires librsvg, libusb, zlib). Config syntax: profile "Name" class=match ... end blocks in spnavrc. --- Makefile.in | 4 +- configure | 33 +++++++ doc/example-spnavrc | 12 +++ src/cfgfile.c | 178 +++++++++++++++++++++++++++++--------- src/cfgfile.h | 11 +++ src/client.c | 1 + src/client.h | 1 + src/lcd.c | 204 ++++++++++++++++++++++++++++++++++++++++++++ src/lcd.h | 9 ++ src/profile.c | 126 +++++++++++++++++++++++++++ src/profile.h | 34 ++++++++ src/proto.h | 8 +- src/proto_unix.c | 34 ++++++++ src/proto_x11.c | 39 +++++++++ src/proto_x11.h | 2 + src/spnavd.c | 39 +++++++++ 16 files changed, 690 insertions(+), 45 deletions(-) create mode 100644 src/lcd.c create mode 100644 src/lcd.h create mode 100644 src/profile.c create mode 100644 src/profile.h diff --git a/Makefile.in b/Makefile.in index 4076fbe..47d40a8 100644 --- a/Makefile.in +++ b/Makefile.in @@ -6,8 +6,8 @@ bin = spacenavd ctl = spnavd_ctl CC ?= gcc -CFLAGS = $(cc_cflags) $(dbg) $(opt) -I$(srcdir)/src $(xinc) $(add_cflags) -LDFLAGS = $(xlib) $(add_ldflags) -lm +CFLAGS = $(cc_cflags) $(dbg) $(opt) -I$(srcdir)/src $(xinc) $(lcd_cflags) $(add_cflags) +LDFLAGS = $(xlib) $(lcd_ldflags) $(add_ldflags) -lm $(bin): $(obj) $(CC) -o $@ $(obj) $(LDFLAGS) diff --git a/configure b/configure index 798664a..a600337 100755 --- a/configure +++ b/configure @@ -76,6 +76,7 @@ X11=yes HOTPLUG=yes XINPUT=yes UINPUT=yes +SPACELCD=auto VER=`git describe --tags 2>/dev/null` CFGDIR=/etc @@ -146,6 +147,11 @@ for arg in $*; do --disable-uinput) UINPUT=no;; + --enable-spacelcd) + SPACELCD=yes;; + --disable-spacelcd) + SPACELCD=no;; + --help) echo 'usage: ./configure [options]' echo 'options:' @@ -156,6 +162,7 @@ for arg in $*; do echo ' x11: X11 support, needed for 3dxsrv compatibility (default: on)' echo ' hotplug: enable hotplug device detection (default: on)' echo ' uinput: use uinput for keyboard emulation on linux (default: on)' + echo ' spacelcd: SpaceMouse Enterprise LCD support (default: auto-detect)' echo 'all invalid options are silently ignored' exit 0 ;; @@ -218,6 +225,20 @@ less reliable (fallback to XSendEvent)." fi fi +# Check for SpaceMouse Enterprise LCD support (needs librsvg, libusb, zlib) +if [ "$SPACELCD" = auto ]; then + if pkg-config --exists librsvg-2.0 libusb-1.0 zlib 2>/dev/null; then + SPACELCD=yes + else + SPACELCD=no + fi +elif [ "$SPACELCD" = yes ]; then + if ! pkg-config --exists librsvg-2.0 libusb-1.0 zlib 2>/dev/null; then + echo "WARNING: LCD dependencies not found (librsvg-2.0, libusb-1.0, zlib), disabling" + SPACELCD=no + fi +fi + HAVE_VSNPRINTF=`check_func vsnprintf` # print configuration results @@ -232,6 +253,7 @@ echo " use hotplug: $HOTPLUG" if [ "$sys" = Linux ]; then echo " uinput for keyboard emulation: $UINPUT" fi +echo " SpaceLCD support: $SPACELCD" if [ "$UINPUT" != yes -a "$X11" = yes ]; then [ -n "$HAVE_XTEST_H" ] && foo=yes || foo=no echo " XTest for keyboard emulation: $foo" @@ -269,6 +291,13 @@ if [ "$X11" = 'yes' ]; then echo 'xlib += -lX11 -lXext' >>Makefile fi +if [ "$SPACELCD" = yes ]; then + lcd_cflags=`pkg-config --cflags librsvg-2.0 libusb-1.0 zlib` + lcd_ldflags=`pkg-config --libs librsvg-2.0 libusb-1.0 zlib` + echo "lcd_cflags = $lcd_cflags" >>Makefile + echo "lcd_ldflags = $lcd_ldflags" >>Makefile +fi + if $cc_is_gcc; then echo 'cc_cflags = -pedantic -Wall -MMD' >>Makefile fi @@ -301,6 +330,10 @@ fi echo '#define VERSION "'$VER'"' >>$cfgheader echo >>$cfgheader +if [ "$SPACELCD" = yes ]; then + echo '#define HAVE_SPACELCD' >>$cfgheader + echo >>$cfgheader +fi [ -n "$HAVE_ALLOCA_H" ] && echo $HAVE_ALLOCA_H >>$cfgheader [ -n "$HAVE_MALLOC_H" ] && echo $HAVE_MALLOC_H >>$cfgheader [ -n "$HAVE_STDINT_H" ] && echo $HAVE_STDINT_H >>$cfgheader diff --git a/doc/example-spnavrc b/doc/example-spnavrc index 765c7b2..b519034 100644 --- a/doc/example-spnavrc +++ b/doc/example-spnavrc @@ -144,3 +144,15 @@ # the re-centering power of the device. # #repeat-interval = -1 + + +# Per-application profiles +# Profiles override global settings for specific applications. +# The "class" value is matched (case-insensitive substring) against +# the WM_CLASS of the currently focused window. +# +#profile "Blender" class=blender +# kbmap0 = Escape +# kbmap1 = Return +# sensitivity = 2.0 +#end diff --git a/src/cfgfile.c b/src/cfgfile.c index 95256c8..2de057c 100644 --- a/src/cfgfile.c +++ b/src/cfgfile.c @@ -32,6 +32,9 @@ along with this program. If not, see . struct cfg cfg, prev_cfg; +struct profile profiles[MAX_PROFILES]; +int num_profiles; + /* all parsable config options... some of them might map to the same cfg field */ enum { CFG_REPEAT, @@ -83,6 +86,16 @@ void default_cfg(struct cfg *cfg) { int i, j; + for(i = 0; i < num_profiles; i++) { + free(profiles[i].name); + free(profiles[i].match_class); + for(j = 0; j < MAX_BUTTONS; j++) { + free(profiles[i].pcfg.kbmap_str[j]); + } + } + num_profiles = 0; + memset(profiles, 0, sizeof profiles); + memset(cfg, 0, sizeof *cfg); cfg->sensitivity = 1.0; @@ -152,6 +165,7 @@ int read_cfg(const char *fname, struct cfg *cfg) struct flock flk; int num_devid = 0; struct cfgline *lptr; + int cur_profile = -1; default_cfg(cfg); @@ -191,6 +205,7 @@ int read_cfg(const char *fname, struct cfg *cfg) int isint, isfloat, isbool, ival, bnidx, axisidx; float fval; char *endp, *key_str, *val_str, *line = buf; + struct cfg *target; lptr = cfglines + num_lines++; @@ -208,6 +223,79 @@ int read_cfg(const char *fname, struct cfg *cfg) continue; /* ignore comments and empty lines */ } + /* check for profile block start: profile "Name" class=match */ + if(strncmp(line, "profile", 7) == 0 && (line[7] == ' ' || line[7] == '\t' || line[7] == '"')) { + char *p, *name_start, *name_end, *class_val; + + if(num_profiles >= MAX_PROFILES) { + logmsg(LOG_WARNING, "too many profiles (max %d), ignoring\n", MAX_PROFILES); + continue; + } + + /* parse quoted name */ + p = line + 7; + while(*p == ' ' || *p == '\t') p++; + if(*p == '"') { + name_start = ++p; + name_end = strchr(p, '"'); + if(!name_end) { + logmsg(LOG_WARNING, "unterminated profile name string\n"); + continue; + } + *name_end = 0; + p = name_end + 1; + } else { + name_start = p; + while(*p && *p != ' ' && *p != '\t') p++; + if(*p) *p++ = 0; + } + + /* parse class=value */ + while(*p == ' ' || *p == '\t') p++; + class_val = 0; + if(strncmp(p, "class=", 6) == 0) { + class_val = p + 6; + /* trim trailing whitespace */ + endp = class_val + strlen(class_val) - 1; + while(endp > class_val && (*endp == ' ' || *endp == '\t' || *endp == '\n' || *endp == '\r')) { + *endp-- = 0; + } + } + if(!class_val || !*class_val) { + logmsg(LOG_WARNING, "profile missing class= specifier, ignoring\n"); + continue; + } + + /* initialize profile with a copy of the current global config */ + profiles[num_profiles].pcfg = *cfg; + /* deep copy kbmap_str pointers */ + for(i = 0; i < MAX_BUTTONS; i++) { + profiles[num_profiles].pcfg.kbmap_str[i] = cfg->kbmap_str[i] ? strdup(cfg->kbmap_str[i]) : 0; + } + /* deep copy devname pointers */ + for(i = 0; i < MAX_CUSTOM; i++) { + profiles[num_profiles].pcfg.devname[i] = cfg->devname[i] ? strdup(cfg->devname[i]) : 0; + } + profiles[num_profiles].name = strdup(name_start); + profiles[num_profiles].match_class = strdup(class_val); + cur_profile = num_profiles++; + logmsg(LOG_INFO, "profile \"%s\" class=%s\n", profiles[cur_profile].name, profiles[cur_profile].match_class); + continue; + } + + /* check for end of profile block */ + if(strcmp(line, "end") == 0) { + if(cur_profile >= 0) { + cur_profile = -1; + } else { + logmsg(LOG_WARNING, "unexpected 'end' outside profile block\n"); + } + continue; + } + + /* select target config: profile or global */ + target = (cur_profile >= 0) ? &profiles[cur_profile].pcfg : cfg; + if(!(key_str = strtok(line, " =\n\t\r"))) { logmsg(LOG_WARNING, "invalid config line: %s, skipping.\n", line); continue; @@ -236,13 +324,13 @@ int read_cfg(const char *fname, struct cfg *cfg) if(strcmp(key_str, "repeat-interval") == 0) { lptr->opt = CFG_REPEAT; EXPECT(isint); - cfg->repeat_msec = ival; + target->repeat_msec = ival; } else if(strcmp(key_str, "dead-zone") == 0) { lptr->opt = CFG_DEADZONE; EXPECT(isint); for(i=0; idead_threshold[i] = ival; + target->dead_threshold[i] = ival; } } else if(sscanf(key_str, "dead-zone%d", &axisidx) == 1) { @@ -252,117 +340,117 @@ int read_cfg(const char *fname, struct cfg *cfg) } lptr->opt = CFG_DEADZONE_N; lptr->idx = axisidx; - cfg->dead_threshold[axisidx] = ival; + target->dead_threshold[axisidx] = ival; } else if(strcmp(key_str, "dead-zone-translation-x") == 0) { logmsg(LOG_WARNING, "Deprecated option: %s. You are encouraged to use dead-zoneN instead\n", key_str); lptr->opt = CFG_DEADZONE_TX; EXPECT(isint); - cfg->dead_threshold[0] = ival; + target->dead_threshold[0] = ival; } else if(strcmp(key_str, "dead-zone-translation-y") == 0) { logmsg(LOG_WARNING, "Deprecated option: %s. You are encouraged to use dead-zoneN instead\n", key_str); lptr->opt = CFG_DEADZONE_TY; EXPECT(isint); - cfg->dead_threshold[1] = ival; + target->dead_threshold[1] = ival; } else if(strcmp(key_str, "dead-zone-translation-z") == 0) { logmsg(LOG_WARNING, "Deprecated option: %s. You are encouraged to use dead-zoneN instead\n", key_str); lptr->opt = CFG_DEADZONE_TZ; EXPECT(isint); - cfg->dead_threshold[2] = ival; + target->dead_threshold[2] = ival; } else if(strcmp(key_str, "dead-zone-rotation-x") == 0) { logmsg(LOG_WARNING, "Deprecated option: %s. You are encouraged to use dead-zoneN instead\n", key_str); lptr->opt = CFG_DEADZONE_RX; EXPECT(isint); - cfg->dead_threshold[3] = ival; + target->dead_threshold[3] = ival; } else if(strcmp(key_str, "dead-zone-rotation-y") == 0) { logmsg(LOG_WARNING, "Deprecated option: %s. You are encouraged to use dead-zoneN instead\n", key_str); lptr->opt = CFG_DEADZONE_RY; EXPECT(isint); - cfg->dead_threshold[4] = ival; + target->dead_threshold[4] = ival; } else if(strcmp(key_str, "dead-zone-rotation-z") == 0) { logmsg(LOG_WARNING, "Deprecated option: %s. You are encouraged to use dead-zoneN instead\n", key_str); lptr->opt = CFG_DEADZONE_RZ; EXPECT(isint); - cfg->dead_threshold[5] = ival; + target->dead_threshold[5] = ival; } else if(strcmp(key_str, "sensitivity") == 0) { lptr->opt = CFG_SENS; EXPECT(isfloat); - cfg->sensitivity = fval; + target->sensitivity = fval; } else if(strcmp(key_str, "sensitivity-translation") == 0) { lptr->opt = CFG_SENS_TRANS; EXPECT(isfloat); - cfg->sens_trans[0] = cfg->sens_trans[1] = cfg->sens_trans[2] = fval; + target->sens_trans[0] = target->sens_trans[1] = target->sens_trans[2] = fval; } else if(strcmp(key_str, "sensitivity-translation-x") == 0) { lptr->opt = CFG_SENS_TX; EXPECT(isfloat); - cfg->sens_trans[0] = fval; + target->sens_trans[0] = fval; } else if(strcmp(key_str, "sensitivity-translation-y") == 0) { lptr->opt = CFG_SENS_TY; EXPECT(isfloat); - cfg->sens_trans[1] = fval; + target->sens_trans[1] = fval; } else if(strcmp(key_str, "sensitivity-translation-z") == 0) { lptr->opt = CFG_SENS_TZ; EXPECT(isfloat); - cfg->sens_trans[2] = fval; + target->sens_trans[2] = fval; } else if(strcmp(key_str, "sensitivity-rotation") == 0) { lptr->opt = CFG_SENS_ROT; EXPECT(isfloat); - cfg->sens_rot[0] = cfg->sens_rot[1] = cfg->sens_rot[2] = fval; + target->sens_rot[0] = target->sens_rot[1] = target->sens_rot[2] = fval; } else if(strcmp(key_str, "sensitivity-rotation-x") == 0) { lptr->opt = CFG_SENS_RX; EXPECT(isfloat); - cfg->sens_rot[0] = fval; + target->sens_rot[0] = fval; } else if(strcmp(key_str, "sensitivity-rotation-y") == 0) { lptr->opt = CFG_SENS_RY; EXPECT(isfloat); - cfg->sens_rot[1] = fval; + target->sens_rot[1] = fval; } else if(strcmp(key_str, "sensitivity-rotation-z") == 0) { lptr->opt = CFG_SENS_RZ; EXPECT(isfloat); - cfg->sens_rot[2] = fval; + target->sens_rot[2] = fval; } else if(strcmp(key_str, "invert-rot") == 0) { lptr->opt = CFG_INVROT; if(strchr(val_str, 'x')) { - cfg->invert[RX] = 1; + target->invert[RX] = 1; } if(strchr(val_str, 'y')) { - cfg->invert[RY] = 1; + target->invert[RY] = 1; } if(strchr(val_str, 'z')) { - cfg->invert[RZ] = 1; + target->invert[RZ] = 1; } } else if(strcmp(key_str, "invert-trans") == 0) { lptr->opt = CFG_INVTRANS; if(strchr(val_str, 'x')) { - cfg->invert[TX] = 1; + target->invert[TX] = 1; } if(strchr(val_str, 'y')) { - cfg->invert[TY] = 1; + target->invert[TY] = 1; } if(strchr(val_str, 'z')) { - cfg->invert[TZ] = 1; + target->invert[TZ] = 1; } } else if(strcmp(key_str, "swap-yz") == 0) { lptr->opt = CFG_SWAPYZ; if(isint || isbool) { - cfg->swapyz = ival; + target->swapyz = ival; } else { logmsg(LOG_WARNING, "invalid configuration value for %s, expected a boolean value.\n", key_str); continue; @@ -380,7 +468,7 @@ int read_cfg(const char *fname, struct cfg *cfg) } lptr->opt = CFG_AXISMAP_N; lptr->idx = axisidx; - cfg->map_axis[axisidx] = ival; + target->map_axis[axisidx] = ival; } else if(sscanf(key_str, "bnmap%d", &bnidx) == 1) { EXPECT(isint); @@ -388,12 +476,12 @@ int read_cfg(const char *fname, struct cfg *cfg) logmsg(LOG_WARNING, "invalid configuration value for %s, expected a number from 0 to %d\n", key_str, MAX_BUTTONS); continue; } - if(cfg->map_button[bnidx] != bnidx) { + if(target->map_button[bnidx] != bnidx) { logmsg(LOG_WARNING, "warning: multiple mappings for button %d\n", bnidx); } lptr->opt = CFG_BNMAP_N; lptr->idx = bnidx; - cfg->map_button[bnidx] = ival; + target->map_button[bnidx] = ival; } else if(sscanf(key_str, "bnact%d", &bnidx) == 1) { if(bnidx < 0 || bnidx >= MAX_BUTTONS) { @@ -402,8 +490,8 @@ int read_cfg(const char *fname, struct cfg *cfg) } lptr->opt = CFG_BNACT_N; lptr->idx = bnidx; - if((cfg->bnact[bnidx] = parse_bnact(val_str)) == -1) { - cfg->bnact[bnidx] = BNACT_NONE; + if((target->bnact[bnidx] = parse_bnact(val_str)) == -1) { + target->bnact[bnidx] = BNACT_NONE; logmsg(LOG_WARNING, "invalid button action: \"%s\"\n", val_str); continue; } @@ -415,20 +503,20 @@ int read_cfg(const char *fname, struct cfg *cfg) } lptr->opt = CFG_KBMAP_N; lptr->idx = bnidx; - if(cfg->kbmap_str[bnidx]) { - logmsg(LOG_WARNING, "warning: multiple keyboard mappings for button %d: %s -> %s\n", bnidx, cfg->kbmap_str[bnidx], val_str); - free(cfg->kbmap_str[bnidx]); + if(target->kbmap_str[bnidx]) { + logmsg(LOG_WARNING, "warning: multiple keyboard mappings for button %d: %s -> %s\n", bnidx, target->kbmap_str[bnidx], val_str); + free(target->kbmap_str[bnidx]); } - cfg->kbmap_str[bnidx] = strdup(val_str); - cfg->kbmap_count[bnidx] = parse_kbmap(val_str, cfg->kbmap[bnidx], MAX_KEYS_PER_BUTTON); + target->kbmap_str[bnidx] = strdup(val_str); + target->kbmap_count[bnidx] = parse_kbmap(val_str, target->kbmap[bnidx], MAX_KEYS_PER_BUTTON); } else if(strcmp(key_str, "led") == 0) { lptr->opt = CFG_LED; if(isint || isbool) { - cfg->led = ival; + target->led = ival; } else { if(strcmp(val_str, "auto") == 0) { - cfg->led = LED_AUTO; + target->led = LED_AUTO; } else { logmsg(LOG_WARNING, "invalid configuration value for %s, expected a boolean value or \"auto\".\n", key_str); continue; @@ -438,7 +526,7 @@ int read_cfg(const char *fname, struct cfg *cfg) } else if(strcmp(key_str, "kbmap_use_x11") == 0) { lptr->opt = CFG_KBMAP_USE_X11; if(isint || isbool) { - cfg->kbemu_use_x11 = ival; + target->kbemu_use_x11 = ival; } else { logmsg(LOG_WARNING, "invalid configuration value for %s, expected a boolean value.\n", key_str); continue; @@ -447,7 +535,7 @@ int read_cfg(const char *fname, struct cfg *cfg) } else if(strcmp(key_str, "grab") == 0) { lptr->opt = CFG_GRAB; if(isint || isbool) { - cfg->grab_device = ival; + target->grab_device = ival; } else { logmsg(LOG_WARNING, "invalid configuration value for %s, expected a boolean value.\n", key_str); continue; @@ -455,14 +543,14 @@ int read_cfg(const char *fname, struct cfg *cfg) } else if(strcmp(key_str, "serial") == 0) { lptr->opt = CFG_SERIAL; - strncpy(cfg->serial_dev, val_str, PATH_MAX - 1); + strncpy(target->serial_dev, val_str, PATH_MAX - 1); } else if(strcmp(key_str, "device-id") == 0) { unsigned int vendor, prod; lptr->opt = CFG_DEVID; if(sscanf(val_str, "%x:%x", &vendor, &prod) == 2) { - cfg->devid[num_devid][0] = (int)vendor; - cfg->devid[num_devid][1] = (int)prod; + target->devid[num_devid][0] = (int)vendor; + target->devid[num_devid][1] = (int)prod; num_devid++; } else { logmsg(LOG_WARNING, "invalid configuration value for %s, expected a vendorid:productid pair\n", key_str); @@ -474,6 +562,12 @@ int read_cfg(const char *fname, struct cfg *cfg) } } + if(cur_profile >= 0) { + logmsg(LOG_WARNING, "unterminated profile block at end of config file\n"); + } + + logmsg(LOG_INFO, "%d profiles loaded\n", num_profiles); + unlock_cfgfile(fd); fclose(fp); return 0; diff --git a/src/cfgfile.h b/src/cfgfile.h index d3da11f..66d93bd 100644 --- a/src/cfgfile.h +++ b/src/cfgfile.h @@ -67,6 +67,17 @@ struct cfg { int kbemu_use_x11; /* force X11 for kbemu, instead of uinput */ }; +#define MAX_PROFILES 16 + +struct profile { + char *name; /* display name */ + char *match_class; /* WM_CLASS substring to match (case-insensitive) */ + struct cfg pcfg; /* full config with profile overrides applied */ +}; + +extern struct profile profiles[MAX_PROFILES]; +extern int num_profiles; + void default_cfg(struct cfg *cfg); int read_cfg(const char *fname, struct cfg *cfg); int write_cfg(const char *fname, struct cfg *cfg); diff --git a/src/client.c b/src/client.c index 0216e6e..d12b1bf 100644 --- a/src/client.c +++ b/src/client.c @@ -110,6 +110,7 @@ void free_client(struct client *client) { if(client) { free(client->name); + free(client->app_id); free(client->strbuf.buf); free(client); } diff --git a/src/client.h b/src/client.h index 726eea8..dbee449 100644 --- a/src/client.h +++ b/src/client.h @@ -58,6 +58,7 @@ struct client { struct device *dev; char *name; /* client name (not unique) */ + char *app_id; /* application identifier for profile matching */ unsigned int evmask; /* event selection mask */ char reqbuf[64]; diff --git a/src/lcd.c b/src/lcd.c new file mode 100644 index 0000000..5e0af06 --- /dev/null +++ b/src/lcd.c @@ -0,0 +1,204 @@ +#include "config.h" +#include "lcd.h" +#include "profile.h" +#include "cfgfile.h" +#include "logger.h" +#include +#include +#include + +#ifdef HAVE_SPACELCD +#include +#include +#include +#include + +#define LCD_WIDTH 640 +#define LCD_HEIGHT 150 +#define LCD_BPP 2 +#define LCD_BITMAP_BYTES (LCD_WIDTH * LCD_HEIGHT * LCD_BPP) +#define LCD_HEADER_SIZE 0x200 +#define LCD_DEFLATED_MAX 0xffff +#define LCD_EFFECT_CUT 0x11 + +#define LCD_USB_VENDOR 0x256f +#define LCD_USB_PRODUCT 0xc633 +#define LCD_USB_PACKET_MAX 64 +#define LCD_USB_TIMEOUT 1000 + +static void rgb_to_bgr(uint8_t *dst, const uint8_t *src, int size) +{ + int i; + for(i = 0; i < size; i += 2) { + dst[i] = (src[i + 1] >> 3) | (src[i] & 0xe0); + dst[i + 1] = (src[i] << 3) | (src[i + 1] & 0x07); + } +} + +static int svg_to_rgb565(const char *svg, int svglen, uint8_t *buffer) +{ + GError *error = NULL; + RsvgHandle *handle; + cairo_surface_t *surface; + cairo_t *cr; + + handle = rsvg_handle_new_from_data((const guint8 *)svg, (gsize)svglen, &error); + if(error) { + logmsg(LOG_WARNING, "lcd: failed to parse SVG: %s\n", error->message); + g_error_free(error); + return -1; + } + + surface = cairo_image_surface_create(CAIRO_FORMAT_RGB16_565, LCD_WIDTH, LCD_HEIGHT); + cr = cairo_create(surface); + + { + RsvgRectangle viewport = {0, 0, LCD_WIDTH, LCD_HEIGHT}; + rsvg_handle_render_document(handle, cr, &viewport, &error); + } + if(error) { + logmsg(LOG_WARNING, "lcd: render failed: %s\n", error->message); + g_error_free(error); + cairo_destroy(cr); + cairo_surface_destroy(surface); + g_object_unref(handle); + return -1; + } + + rgb_to_bgr(buffer, cairo_image_surface_get_data(surface), LCD_BITMAP_BYTES); + + cairo_destroy(cr); + cairo_surface_destroy(surface); + g_object_unref(handle); + return 0; +} + +static int lcd_compress(const uint8_t *src, uint8_t *dst, int srclen) +{ + z_stream stream; + int result, outsize; + + memset(&stream, 0, sizeof stream); + stream.avail_in = srclen; + stream.next_in = (Bytef *)src; + stream.avail_out = LCD_DEFLATED_MAX; + stream.next_out = dst; + + deflateInit2(&stream, -1, Z_DEFLATED, -15, 9, Z_FIXED); + result = deflate(&stream, Z_FINISH); + outsize = LCD_DEFLATED_MAX - stream.avail_out; + deflateEnd(&stream); + + return (result == Z_STREAM_END) ? outsize : -1; +} + +static int lcd_usb_send(uint8_t *data, int size) +{ + libusb_device_handle *handle; + int transferred, i, rc; + + rc = libusb_init(NULL); + if(rc < 0) { + logmsg(LOG_WARNING, "lcd: libusb_init failed: %s\n", libusb_strerror(rc)); + return -1; + } + + handle = libusb_open_device_with_vid_pid(NULL, LCD_USB_VENDOR, LCD_USB_PRODUCT); + if(!handle) { + libusb_exit(NULL); + return -1; /* device not present, not an error worth logging */ + } + + libusb_set_auto_detach_kernel_driver(handle, 1); + for(i = 0; i < 2; i++) { + libusb_claim_interface(handle, i); + libusb_reset_device(handle); + } + + while(size > 0) { + int chunk = size > LCD_USB_PACKET_MAX ? LCD_USB_PACKET_MAX : size; + libusb_bulk_transfer(handle, 0x01, data, chunk, &transferred, LCD_USB_TIMEOUT); + data += transferred; + size -= transferred; + } + + for(i = 0; i < 2; i++) { + libusb_release_interface(handle, i); + } + libusb_close(handle); + libusb_exit(NULL); + return 0; +} +#endif /* HAVE_SPACELCD */ + +extern struct cfg cfg; + +static void build_svg(char *out, size_t outsz) +{ + const int cols = 6; + const int rows = 2; + int btn = 0; + int r, c; + size_t off = 0; + + off += snprintf(out + off, outsz - off, + "" + ""); + off += snprintf(out + off, outsz - off, + "%s", profile_get_name()); + + for(r = 0; r < rows; r++) { + for(c = 0; c < cols; c++, btn++) { + const char *lbl = profile_get_button_label(btn); + int x = 10 + c * (640 / cols); + int y = 50 + r * 45; + off += snprintf(out + off, outsz - off, + "%d: %s", + x, y, lbl[0] ? "cyan" : "#555", btn + 1, lbl[0] ? lbl : "None"); + if(off >= outsz) return; + } + } + off += snprintf(out + off, outsz - off, ""); +} + +void lcd_update_mappings(void) +{ +#ifdef HAVE_SPACELCD + char svg[4096]; + uint8_t *bitmap, *usbdata; + int compressed_size; + + build_svg(svg, sizeof svg); + + bitmap = malloc(LCD_BITMAP_BYTES); + usbdata = calloc(1, LCD_DEFLATED_MAX + LCD_HEADER_SIZE); + if(!bitmap || !usbdata) { + free(bitmap); + free(usbdata); + return; + } + + if(svg_to_rgb565(svg, strlen(svg), bitmap) != 0) { + free(bitmap); + free(usbdata); + return; + } + + compressed_size = lcd_compress(bitmap, usbdata + LCD_HEADER_SIZE, LCD_BITMAP_BYTES); + free(bitmap); + if(compressed_size < 0 || compressed_size > 65535) { + logmsg(LOG_WARNING, "lcd: compression failed\n"); + free(usbdata); + return; + } + + /* header: effect byte, flags, compressed length (16-bit LE) */ + usbdata[0] = LCD_EFFECT_CUT; + usbdata[1] = 0x0f; + usbdata[2] = compressed_size & 0xff; + usbdata[3] = (compressed_size >> 8) & 0xff; + + lcd_usb_send(usbdata, compressed_size + LCD_HEADER_SIZE); + free(usbdata); +#endif +} diff --git a/src/lcd.h b/src/lcd.h new file mode 100644 index 0000000..bf24716 --- /dev/null +++ b/src/lcd.h @@ -0,0 +1,9 @@ +#ifndef LCD_H_ +#define LCD_H_ + +/* Update SpaceMouse Enterprise LCD with current profile name and mapped keys. + * No-op if device is not present or libraries are unavailable. + */ +void lcd_update_mappings(void); + +#endif diff --git a/src/profile.c b/src/profile.c new file mode 100644 index 0000000..daf76d6 --- /dev/null +++ b/src/profile.c @@ -0,0 +1,126 @@ +#include "config.h" +#include "profile.h" +#include "logger.h" +#include "kbemu.h" +#ifdef USE_X11 +#include "proto_x11.h" +#endif +#include +#include + +extern struct cfg cfg; + +static struct cfg base_cfg; +static int active_profile = -1; +static int manual_override; /* when set, auto-detection is suppressed */ + +void profile_on_cfg_reload(struct cfg *c) +{ + base_cfg = *c; + active_profile = -1; + manual_override = 0; +} + +static int match_class(const char *str, const char *match) +{ + char buf1[256], buf2[256]; + size_t i; + + if(!str || !match || !*str || !*match) return 0; + + for(i = 0; i < sizeof(buf1) - 1 && str[i]; i++) buf1[i] = (char)tolower((unsigned char)str[i]); + buf1[i] = 0; + for(i = 0; i < sizeof(buf2) - 1 && match[i]; i++) buf2[i] = (char)tolower((unsigned char)match[i]); + buf2[i] = 0; + return strstr(buf1, buf2) != NULL; +} + +static int activate_profile(int new_index) +{ + if(new_index != active_profile) { + if(new_index >= 0) { + logmsg(LOG_INFO, "Profile switch: %s\n", profiles[new_index].name ? profiles[new_index].name : "(unnamed)"); + cfg = profiles[new_index].pcfg; + } else { + logmsg(LOG_INFO, "Profile switch: Default\n"); + cfg = base_cfg; + } + active_profile = new_index; + return 1; + } + return 0; +} + +static int find_profile(const char *id) +{ + int i; + + if(id && *id) { + for(i = 0; i < num_profiles; i++) { + if(profiles[i].match_class && match_class(id, profiles[i].match_class)) { + return i; + } + } + } + return -1; +} + +int profile_refresh_active(void) +{ + if(manual_override) return 0; + +#ifdef USE_X11 + { + char cls[256] = {0}; + x11_get_focused_wm_class(cls, sizeof cls); + return activate_profile(find_profile(cls)); + } +#else + return 0; +#endif +} + +int profile_match_app_id(const char *app_id) +{ + if(manual_override) return 0; + return activate_profile(find_profile(app_id)); +} + +int profile_set_manual(int index) +{ + if(index < -1 || index >= num_profiles) return -1; + + if(index == -1) { + /* return to auto mode */ + manual_override = 0; + logmsg(LOG_INFO, "Profile mode: auto\n"); + return 0; + } + + manual_override = 1; + return activate_profile(index); +} + +int profile_active_index(void) +{ + return active_profile; +} + +const char *profile_get_button_label(int button) +{ + if(button < 0 || button >= MAX_BUTTONS) return ""; + if(cfg.kbmap_count[button] <= 0) return ""; + if(kbemu_keyname) { + const char *nm = kbemu_keyname(cfg.kbmap[button][0]); + return nm ? nm : ""; + } + return ""; +} + +const char *profile_get_name(void) +{ + if(active_profile >= 0 && active_profile < num_profiles) { + if(profiles[active_profile].name) return profiles[active_profile].name; + } + return "Default"; +} diff --git a/src/profile.h b/src/profile.h new file mode 100644 index 0000000..5fdc8c0 --- /dev/null +++ b/src/profile.h @@ -0,0 +1,34 @@ +/* Profile management for per-application mappings */ +#ifndef PROFILE_H_ +#define PROFILE_H_ + +#include "cfgfile.h" + +/* initialize/reset internal state after config reload */ +void profile_on_cfg_reload(struct cfg *c); + +/* Refresh active profile based on current focused window (X11 polling). + * Returns 1 if profile changed. + */ +int profile_refresh_active(void); + +/* Try to match a profile by app_id string (client-reported). + * Returns 1 if profile changed. + */ +int profile_match_app_id(const char *app_id); + +/* Manually set active profile by index. -1 returns to auto mode. + * Returns 1 if profile changed, 0 if unchanged, -1 on invalid index. + */ +int profile_set_manual(int index); + +/* Get the active profile index, or -1 if none */ +int profile_active_index(void); + +/* Get a user-presentable label for a given button mapping under active profile. */ +const char *profile_get_button_label(int button); + +/* Get the current profile name (or "Default" if none) */ +const char *profile_get_name(void); + +#endif diff --git a/src/proto.h b/src/proto.h index 8c983ab..01e220e 100644 --- a/src/proto.h +++ b/src/proto.h @@ -54,6 +54,9 @@ enum { REQ_GET_SENS, /* get client sensitivity: R[0] float R[6] status */ REQ_SET_EVMASK, /* set event mask: Q[0] mask - R[6] status */ REQ_GET_EVMASK, /* get event mask: R[0] mask R[6] status */ + REQ_SET_APP_ID, /* set app id for profile matching: Q[0-5] next 24 bytes Q[6] remaining length - R[6] status */ + REQ_SET_PROFILE, /* set active profile: Q[0] index (-1 for auto) - R[6] status */ + REQ_GET_PROFILE, /* get active profile: R[0] index (-1 if none) R[1] num_profiles R[6] status */ /* device queries */ REQ_DEV_NAME = 0x2000, /* get device name: R[0-5] next 24 bytes R[6] remaining length or -1 for failure */ @@ -144,7 +147,10 @@ const char *spnav_reqnames_1000[] = { "SET_SENS", "GET_SENS", "SET_EVMASK", - "GET_EVMASK" + "GET_EVMASK", + "SET_APP_ID", + "SET_PROFILE", + "GET_PROFILE" }; const char *spnav_reqnames_2000[] = { "DEV_NAME", diff --git a/src/proto_unix.c b/src/proto_unix.c index d1c3788..911885f 100644 --- a/src/proto_unix.c +++ b/src/proto_unix.c @@ -30,6 +30,8 @@ along with this program. If not, see . #include "proto.h" #include "proto_unix.h" #include "spnavd.h" +#include "profile.h" +#include "lcd.h" #ifdef USE_X11 #include "kbemu.h" #endif @@ -307,6 +309,38 @@ static int handle_request(struct client *c, struct reqresp *req) } break; + case REQ_SET_APP_ID: + if((res = spnav_recv_str(&c->strbuf, req)) == -1) { + logmsg(LOG_ERR, "SET_APP_ID: failed to receive string\n"); + break; + } + if(res) { + free(c->app_id); + c->app_id = c->strbuf.buf; + c->strbuf.buf = 0; + logmsg(LOG_INFO, "client app_id: %s\n", c->app_id); + if(profile_match_app_id(c->app_id)) { + lcd_update_mappings(); + } + } + break; + + case REQ_SET_PROFILE: + res = profile_set_manual(req->data[0]); + if(res >= 0) { + if(res) lcd_update_mappings(); + sendresp(c, req, 0); + } else { + sendresp(c, req, -1); + } + break; + + case REQ_GET_PROFILE: + req->data[0] = profile_active_index(); + req->data[1] = num_profiles; + sendresp(c, req, 0); + break; + case REQ_SET_SENS: fval = *(float*)req->data; if(isfinite(fval)) { diff --git a/src/proto_x11.c b/src/proto_x11.c index c5791d4..13f1492 100644 --- a/src/proto_x11.c +++ b/src/proto_x11.c @@ -430,6 +430,45 @@ static int xioerr(Display *display) return 0; } +int x11_get_focused_wm_class(char *buf, int bufsz) +{ + Window focused; + int revert; + Window w, root, parent, *children; + unsigned int nchildren; + XClassHint hint; + + if(!dpy || bufsz <= 0) return -1; + + if(setjmp(jbuf)) { + return -1; + } + + XGetInputFocus(dpy, &focused, &revert); + if(focused == None || focused == PointerRoot) return -1; + + /* walk up the window tree looking for WM_CLASS */ + w = focused; + for(;;) { + if(XGetClassHint(dpy, w, &hint)) { + if(hint.res_class) { + strncpy(buf, hint.res_class, bufsz - 1); + buf[bufsz - 1] = 0; + } + if(hint.res_name) XFree(hint.res_name); + if(hint.res_class) XFree(hint.res_class); + return 0; + } + + if(!XQueryTree(dpy, w, &root, &parent, &children, &nchildren)) { + return -1; + } + if(children) XFree(children); + if(parent == root || parent == None) return -1; + w = parent; + } +} + #else int spacenavd_proto_x11_shut_up_empty_source_warning; #endif /* USE_X11 */ diff --git a/src/proto_x11.h b/src/proto_x11.h index 6cceadb..0913394 100644 --- a/src/proto_x11.h +++ b/src/proto_x11.h @@ -39,5 +39,7 @@ void remove_client_window(Window win); void drop_xinput(void); +int x11_get_focused_wm_class(char *buf, int bufsz); + #endif /* PROTO_X11_H_ */ diff --git a/src/spnavd.c b/src/spnavd.c index f817fb6..0beae38 100644 --- a/src/spnavd.c +++ b/src/spnavd.c @@ -24,6 +24,7 @@ along with this program. If not, see . #include #include #include +#include #include #include #include @@ -38,6 +39,8 @@ along with this program. If not, see . #ifdef USE_X11 #include "proto_x11.h" #endif +#include "profile.h" +#include "lcd.h" static void print_usage(const char *argv0); static void cleanup(void); @@ -162,6 +165,9 @@ opt_pidfile: if(!argv[++i]) { logmsg(LOG_INFO, "Spacenav daemon " VERSION "\n"); read_cfg(cfgfile, &cfg); + profile_on_cfg_reload(&cfg); + profile_refresh_active(); + lcd_update_mappings(); prev_cfg = cfg; pipe(pfd); @@ -185,6 +191,10 @@ opt_pidfile: if(!argv[++i]) { atexit(cleanup); + { + struct timeval last_profile_check; + gettimeofday(&last_profile_check, 0); + for(;;) { fd_set rset; int fd, max_fd = 0; @@ -256,9 +266,33 @@ opt_pidfile: if(!argv[++i]) { } } + /* cap timeout to 500ms for periodic profile polling */ + if(num_profiles > 0) { + if(!timeout || tv.tv_sec > 0 || tv.tv_usec > 500000) { + tv.tv_sec = 0; + tv.tv_usec = 500000; + timeout = &tv; + } + } + ret = select(max_fd + 1, &rset, 0, 0, timeout); } while(ret == -1 && errno == EINTR); + /* periodic profile polling */ + if(num_profiles > 0) { + struct timeval now; + long elapsed_ms; + gettimeofday(&now, 0); + elapsed_ms = (now.tv_sec - last_profile_check.tv_sec) * 1000 + + (now.tv_usec - last_profile_check.tv_usec) / 1000; + if(elapsed_ms >= 500) { + last_profile_check = now; + if(profile_refresh_active()) { + lcd_update_mappings(); + } + } + } + if(ret > 0) { handle_events(&rset); } else { @@ -273,6 +307,7 @@ opt_pidfile: if(!argv[++i]) { } } } + } /* end of scope for last_profile_check */ return 0; /* unreachable */ } @@ -432,6 +467,10 @@ static void handle_events(fd_set *rset) read_cfg(cfgfile, &cfg); cfg_changed(); + /* re-evaluate profiles and refresh LCD after config reload */ + profile_on_cfg_reload(&cfg); + profile_refresh_active(); + lcd_update_mappings(); } /* handle anything coming through the UNIX socket */ From acffe75994a5bb21e59c0d4d442aba19bc77d8f8 Mon Sep 17 00:00:00 2001 From: jl1990 Date: Mon, 16 Mar 2026 18:15:22 +0100 Subject: [PATCH 2/2] Remove SVG dependency --- configure | 12 +++---- src/lcd.c | 98 +++++++++++++++++++++---------------------------------- 2 files changed, 44 insertions(+), 66 deletions(-) diff --git a/configure b/configure index a600337..ccb41a9 100755 --- a/configure +++ b/configure @@ -225,16 +225,16 @@ less reliable (fallback to XSendEvent)." fi fi -# Check for SpaceMouse Enterprise LCD support (needs librsvg, libusb, zlib) +# Check for SpaceMouse Enterprise LCD support (needs cairo, libusb, zlib) if [ "$SPACELCD" = auto ]; then - if pkg-config --exists librsvg-2.0 libusb-1.0 zlib 2>/dev/null; then + if pkg-config --exists cairo libusb-1.0 zlib 2>/dev/null; then SPACELCD=yes else SPACELCD=no fi elif [ "$SPACELCD" = yes ]; then - if ! pkg-config --exists librsvg-2.0 libusb-1.0 zlib 2>/dev/null; then - echo "WARNING: LCD dependencies not found (librsvg-2.0, libusb-1.0, zlib), disabling" + if ! pkg-config --exists cairo libusb-1.0 zlib 2>/dev/null; then + echo "WARNING: LCD dependencies not found (cairo, libusb-1.0, zlib), disabling" SPACELCD=no fi fi @@ -292,8 +292,8 @@ if [ "$X11" = 'yes' ]; then fi if [ "$SPACELCD" = yes ]; then - lcd_cflags=`pkg-config --cflags librsvg-2.0 libusb-1.0 zlib` - lcd_ldflags=`pkg-config --libs librsvg-2.0 libusb-1.0 zlib` + lcd_cflags=`pkg-config --cflags cairo libusb-1.0 zlib` + lcd_ldflags=`pkg-config --libs cairo libusb-1.0 zlib` echo "lcd_cflags = $lcd_cflags" >>Makefile echo "lcd_ldflags = $lcd_ldflags" >>Makefile fi diff --git a/src/lcd.c b/src/lcd.c index 5e0af06..ebc4619 100644 --- a/src/lcd.c +++ b/src/lcd.c @@ -9,7 +9,7 @@ #ifdef HAVE_SPACELCD #include -#include +#include #include #include @@ -35,42 +35,55 @@ static void rgb_to_bgr(uint8_t *dst, const uint8_t *src, int size) } } -static int svg_to_rgb565(const char *svg, int svglen, uint8_t *buffer) +static void render_bitmap(uint8_t *buffer) { - GError *error = NULL; - RsvgHandle *handle; + const int cols = 6; + const int rows = 2; + int btn = 0; + int r, c; cairo_surface_t *surface; cairo_t *cr; - handle = rsvg_handle_new_from_data((const guint8 *)svg, (gsize)svglen, &error); - if(error) { - logmsg(LOG_WARNING, "lcd: failed to parse SVG: %s\n", error->message); - g_error_free(error); - return -1; - } - surface = cairo_image_surface_create(CAIRO_FORMAT_RGB16_565, LCD_WIDTH, LCD_HEIGHT); cr = cairo_create(surface); - { - RsvgRectangle viewport = {0, 0, LCD_WIDTH, LCD_HEIGHT}; - rsvg_handle_render_document(handle, cr, &viewport, &error); - } - if(error) { - logmsg(LOG_WARNING, "lcd: render failed: %s\n", error->message); - g_error_free(error); - cairo_destroy(cr); - cairo_surface_destroy(surface); - g_object_unref(handle); - return -1; + /* black background */ + cairo_set_source_rgb(cr, 0, 0, 0); + cairo_paint(cr); + + /* profile name */ + cairo_select_font_face(cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD); + cairo_set_font_size(cr, 18); + cairo_set_source_rgb(cr, 1, 1, 1); + cairo_move_to(cr, 10, 20); + cairo_show_text(cr, profile_get_name()); + + /* button labels */ + cairo_set_font_size(cr, 16); + for(r = 0; r < rows; r++) { + for(c = 0; c < cols; c++, btn++) { + const char *lbl = profile_get_button_label(btn); + int x = 10 + c * (LCD_WIDTH / cols); + int y = 50 + r * 45; + char text[64]; + + if(lbl[0]) { + cairo_set_source_rgb(cr, 0, 1, 1); /* cyan */ + snprintf(text, sizeof text, "%d: %s", btn + 1, lbl); + } else { + cairo_set_source_rgb(cr, 0.33, 0.33, 0.33); /* #555 */ + snprintf(text, sizeof text, "%d: None", btn + 1); + } + cairo_move_to(cr, x, y); + cairo_show_text(cr, text); + } } + cairo_surface_flush(surface); rgb_to_bgr(buffer, cairo_image_surface_get_data(surface), LCD_BITMAP_BYTES); cairo_destroy(cr); cairo_surface_destroy(surface); - g_object_unref(handle); - return 0; } static int lcd_compress(const uint8_t *src, uint8_t *dst, int srclen) @@ -133,43 +146,12 @@ static int lcd_usb_send(uint8_t *data, int size) extern struct cfg cfg; -static void build_svg(char *out, size_t outsz) -{ - const int cols = 6; - const int rows = 2; - int btn = 0; - int r, c; - size_t off = 0; - - off += snprintf(out + off, outsz - off, - "" - ""); - off += snprintf(out + off, outsz - off, - "%s", profile_get_name()); - - for(r = 0; r < rows; r++) { - for(c = 0; c < cols; c++, btn++) { - const char *lbl = profile_get_button_label(btn); - int x = 10 + c * (640 / cols); - int y = 50 + r * 45; - off += snprintf(out + off, outsz - off, - "%d: %s", - x, y, lbl[0] ? "cyan" : "#555", btn + 1, lbl[0] ? lbl : "None"); - if(off >= outsz) return; - } - } - off += snprintf(out + off, outsz - off, ""); -} - void lcd_update_mappings(void) { #ifdef HAVE_SPACELCD - char svg[4096]; uint8_t *bitmap, *usbdata; int compressed_size; - build_svg(svg, sizeof svg); - bitmap = malloc(LCD_BITMAP_BYTES); usbdata = calloc(1, LCD_DEFLATED_MAX + LCD_HEADER_SIZE); if(!bitmap || !usbdata) { @@ -178,11 +160,7 @@ void lcd_update_mappings(void) return; } - if(svg_to_rgb565(svg, strlen(svg), bitmap) != 0) { - free(bitmap); - free(usbdata); - return; - } + render_bitmap(bitmap); compressed_size = lcd_compress(bitmap, usbdata + LCD_HEADER_SIZE, LCD_BITMAP_BYTES); free(bitmap);