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..ccb41a9 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 cairo, libusb, zlib)
+if [ "$SPACELCD" = auto ]; 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 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
+
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 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
+
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..ebc4619
--- /dev/null
+++ b/src/lcd.c
@@ -0,0 +1,182 @@
+#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 void render_bitmap(uint8_t *buffer)
+{
+ const int cols = 6;
+ const int rows = 2;
+ int btn = 0;
+ int r, c;
+ cairo_surface_t *surface;
+ cairo_t *cr;
+
+ surface = cairo_image_surface_create(CAIRO_FORMAT_RGB16_565, LCD_WIDTH, LCD_HEIGHT);
+ cr = cairo_create(surface);
+
+ /* 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);
+}
+
+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;
+
+void lcd_update_mappings(void)
+{
+#ifdef HAVE_SPACELCD
+ uint8_t *bitmap, *usbdata;
+ int compressed_size;
+
+ bitmap = malloc(LCD_BITMAP_BYTES);
+ usbdata = calloc(1, LCD_DEFLATED_MAX + LCD_HEADER_SIZE);
+ if(!bitmap || !usbdata) {
+ free(bitmap);
+ free(usbdata);
+ return;
+ }
+
+ render_bitmap(bitmap);
+
+ 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 */