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 */