diff --git a/99-ShuttlePRO.rules b/99-ShuttlePRO.rules new file mode 100644 index 0000000..fde83fb --- /dev/null +++ b/99-ShuttlePRO.rules @@ -0,0 +1,6 @@ + +# Allows global read access to any ShuttlePRO device + +# install this file in /etc/udev/rules.d + +ATTRS{name}=="Contour Design ShuttlePRO v2" MODE="0644" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dec68cb --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ + +# Copyright 2013 Eric Messick (FixedImagePhoto.com/Contact) + +#CFLAGS=-g -W -Wall +CFLAGS=-O3 -W -Wall + +INSTALL_DIR=/usr/local/bin + +OBJ=\ + readconfig.o \ + shuttlepro.o + +all: shuttlepro + +install: all + install shuttle shuttlepro ${INSTALL_DIR} + +shuttlepro: ${OBJ} + gcc ${CFLAGS} ${OBJ} -o shuttlepro -L /usr/X11R6/lib -lX11 -lXtst + +clean: + rm -f shuttlepro keys.h $(OBJ) + +keys.h: keys.sed /usr/include/X11/keysymdef.h + sed -f keys.sed < /usr/include/X11/keysymdef.h > keys.h + +readconfig.o: shuttle.h keys.h +shuttlepro.o: shuttle.h diff --git a/README b/README new file mode 100644 index 0000000..4d474c9 --- /dev/null +++ b/README @@ -0,0 +1,45 @@ + +Copyright 2013 Eric Messick (FixedImagePhoto.com/Contact) + +This is a user program for interpreting key, shuttle, and jog events +from a Contour Design ShuttlePRO v2. It translates these events into +X keystrokes, mouse button presses, or scroll wheel events. It was +developed and tested on kubuntu 12.10. + +ShuttlePRO events can generate sequences of multiple keystrokes, +including the pressing and releasing of modifier keys. The binding +can be selected based on the title of the window which is focused. + +Build instructions: + +# apt-get install build-essential libx11-dev libxtst-dev + +$ make + +Install instructions: + +# cp 99-ShuttlePRO.rules /etc/udev/rules.d +# make install + +This will install the binary "shuttlepro" and a script "shuttle" in +/usr/local/bin. The udev .rules file allows global read access to any +ShuttlePRO v2 devices plugged into the system. The script passes the +name of the shuttle device to the binary, so you can configure it +there if it is different on your system. + +Configuration instructions: + +Copy the example.shuttlerc file to $HOME/.shuttlerc and edit it +there. While you are configuring this file, you probably want to run +the "shuttle" script from a terminal to see the program output. Make +sure /usr/local/bin is in your $PATH. + +The program re-reads the .shuttlerc file whenever it notices that the +file has been changed, but it only checks when a shuttle event is sent +with a different window focused from the previous shuttle event. When +you save a new version of the .shuttlerc file, it is a good idea to +deliver shuttle events to two different windows to insure that the new +copy of your file is loaded. + +See the example.shuttlerc file for information about the file. You +may also want to look at the comment at the top of readconfig.c. diff --git a/TODO b/TODO new file mode 100644 index 0000000..b34f171 --- /dev/null +++ b/TODO @@ -0,0 +1,18 @@ + +Should be wrapped up in an installation package. + +Should start automatically on login. + But note that debugging output is useful when configuring. + +Should restart on hotplug. + This would help with crashes caused by static discharges. + If it starts on reboot, needs to notice current user to find config + file. + +Should probably use uinput instead of XTest. + +Might want to allow shuttle positions to allow repeated outputs at a +specified time interval. + +A GUI for setting keystroke bindings would be nice. + diff --git a/example.shuttlerc b/example.shuttlerc new file mode 100644 index 0000000..a0adb75 --- /dev/null +++ b/example.shuttlerc @@ -0,0 +1,119 @@ + +# Copyright 2013 Eric Messick (FixedImagePhoto.com/Contact) +# +# Lines in this file starting with # are comments. +# +# +# This file is divided into paragraphs, each specifying the bindings to +# be used when the keyboard focus is on a specific window. The +# paragraph is introduced with a line starting with [. That line +# contains the paragraph name (which is only used for debugging output +# to help you in editing this file) followed by ], followed by a regular +# expression. When the title bar of the focused window matches the +# regular expression (see regex(7)), the bindings in the paragraph will +# be in effect. The program tries these regular expressions in order, +# and the first match is used. + +# If there is no regex on the line, like the [Default] line near the +# bottom, the paragraph acts as a default. Any window title which does +# not match any regex will use the default bindings. Any keys which are +# not specified in the paragraph which does match will use the default +# bindings for those keys. + +# While you are working on regular expressions to match your window +# names, is is useful to see the window names and paragraph names which +# the program finds as you generate ShuttlePRO events. Run the shuttle +# program in a terminal window and remove the comment character from the +# following line: + +#DEBUG_REGEX + +# Within a paragraph, key bindings are introduced with the name of the +# key or event being defined. Keys are named K1 through K15. Positions +# of the shuttle wheel are named S-7 through S-1 for counter-clockwise +# positions, S0 for the rest position in the center, and S1 through S7 +# for the clockwise positions. The jog wheel emits two events named JL +# and JR, for counter-clockwise and clockwise rotations respectively. + +# The keys on the Contour Shuttle Pro v2 are arranged like this: +# +# K1 K2 K3 K4 +# K5 K6 K7 K8 K9 +# +# K14 Jog K15 +# +# K10 K11 +# K12 K13 + +# After the name of the key being bound, the remainder of the line is +# the sequence of X KeySyms which will be generated when that event is +# received. Look up the KeySyms in /usr/include/X11/keysymdef.h. In +# addition to the KeySym names found there, you can also use XK_Button_1 +# for the left mouse button, XK_Button_2 for the middle mouse button, +# XK_Button_3 for the right mouse button, XK_Scroll_Up and +# XK_Scroll_Down for mouse scroll wheel events. For sequences of one or +# more printable characters, you can just enclose them in double quotes. + +# Each KeySym you specify will be pressed and released before the next +# KeySym is pressed. If you wish a key to be held down, you can add a +# /D to the end of the KeySym. For example: XK_Shift_L/D, +# XK_Control_L/D or XK_Alt_L/D. Such keys will be held down until you +# specify they should be released with a /U on the same KeySym name. +# They will all be released at the end of the binding anyway, so you +# usually won't have to use /U. + +# Key bindings, whose names start with a K, allow for some extra +# options. Since they generate separate events when pressed and +# released, you can control that as well. Each non-modifier key is +# pressed and released in sequence except for the last which is not +# released until the shuttle key is released. If you want to press more +# keys during the release sequence, you can put them after the special +# word "RELEASE". Modifier keys specified with /D are released at the +# end of the press sequence, and re-pressed if there are any keys to be +# pressed after RELEASE. If you don't want the modifier keys to be +# released (you want to use a ShuttlePRO key as Shift, for example) you +# can follow it with a /H instead of /D. + +# If you want to see exactly how this file is parsed and converted into +# KeySym strokes, run the shuttle program in a terminal window and +# remove the comment character from the following line: + +#DEBUG_STROKES + +# As one of the main reasons to use a ShuttlePRO is video editing, I've +# included a sample set of bindings for Cinelerra as an example. + +[Cinelerra Resources] ^Cinelerra: Resources$ + # use [Default], avoiding main Cinelerra rule + +[Cinelerra Load] ^Cinelerra: Load$ + # use [Default], avoiding main Cinelerra rule + +[Cinelerra] ^Cinelerra: [^[:space:]]*$ + + K5 XK_KP_0 # Stop + K9 XK_KP_3 # Play + K12 XK_Home # Beginning + K13 XK_End # End + K14 "[" # Toggle in + K15 "]" # Toggle out + + S-3 XK_KP_Add # Fast reverse + S-2 XK_KP_6 # Play reverse + S-1 XK_KP_5 # Slow reverse + S0 XK_KP_0 # Stop + S1 XK_KP_2 # Slow forward + S2 XK_KP_3 # Play forward + S3 XK_KP_Enter # Fast forward + + JL XK_KP_4 # Frame reverse + JR XK_KP_1 # Frame forward + + + +[Default] + K6 XK_Button_1 + K7 XK_Button_2 + K8 XK_Button_3 + JL XK_Scroll_Up + JR XK_Scroll_Down diff --git a/keys.sed b/keys.sed new file mode 100644 index 0000000..1996b8b --- /dev/null +++ b/keys.sed @@ -0,0 +1,5 @@ +/^\#ifdef/p +/^\#endif/p +/^\#define/!d +s/^\#define // +s/^\([^[:space:]]*\).*$/{ "\1", \1 }, / diff --git a/readconfig.c b/readconfig.c new file mode 100644 index 0000000..787e273 --- /dev/null +++ b/readconfig.c @@ -0,0 +1,809 @@ + +/* + + Copyright 2013 Eric Messick (FixedImagePhoto.com/Contact) + + Read and process the configuration file ~/.shuttlepro + + Lines starting with # are comments. + + Sequence of sections defining translation classes, each section is: + + [name] regex + K<1..15> output + S<-7..7> output + J output + + When focus is on a window whose title matches regex, the following + translation class is in effect. An empty regex for the last class + will always match, allowing default translations. Any output + sequences not bound in a matched section will be loaded from the + default section if they are bound there. + + Each "[name] regex" line introduces the list of key and shuttle + translations for the named translation class. The name is only used + for debugging output, and needn't be unique. The following lines + with K, S, and J labels indicate what output should be produced for + the given keypress, shuttle position, or jog direction. + + output is a sequence of one or more key codes with optional up/down + indicators, or strings of printable characters enclosed in double + quotes, separated by whitespace. Sequences bound to keys may have + separate press and release sequences, separated by the word RELEASE. + + Examples: + + K1 "qwer" + K2 XK_Right + K3 XK_Alt_L/D XK_Right + K4 "V" XK_Left XK_Page_Up "v" + K5 XK_Alt_L/D "v" XK_Alt_L/U "x" RELEASE "q" + + Any keycode can be followed by an optional /D, /U, or /H, indicating + that the key is just going down (without being released), going up, + or going down and being held until the shuttlepro key is released. + + So, in general, modifier key codes will be followed by /D, and + precede the keycodes they are intended to modify. If a sequence + requires different sets of modifiers for different keycodes, /U can + be used to release a modifier that was previously pressed with /D. + + At the end of shuttle and jog sequences, all down keys will be + released. + + Keypresses translate to separate press and release sequences. + + At the end of the press sequence for key sequences, all down keys + marked by /D will be released, and the last key not marked by /D, + /U, or /H will remain pressed. The release sequence will begin by + releasing the last held key. If keys are to be pressed as part of + the release sequence, then any keys marked with /D will be repressed + before continuing the sequence. Keycodes marked with /H remain held + between the press and release sequences. + + */ + +#include "shuttle.h" + +int debug_regex = 0; +int debug_strokes = 0; + +char * +allocate(size_t len) +{ + char *ret = (char *)malloc(len); + if (ret == NULL) { + fprintf(stderr, "Out of memory!\n"); + exit(1); + } + return ret; +} + +char * +alloc_strcat(char *a, char *b) +{ + size_t len = 0; + char *result; + + if (a != NULL) { + len += strlen(a); + } + if (b != NULL) { + len += strlen(b); + } + result = allocate(len+1); + result[0] = '\0'; + if (a != NULL) { + strcpy(result, a); + } + if (b != NULL) { + strcat(result, b); + } + return result; +} + +static char *read_line_buffer = NULL; +static int read_line_buffer_length = 0; + +#define BUF_GROWTH_STEP 1024 + + +// read a line of text from the given file into a managed buffer. +// returns a partial line at EOF if the file does not end with \n. +// exits with error message on read error. +char * +read_line(FILE *f, char *name) +{ + int pos = 0; + char *new_buffer; + int new_buffer_length; + + if (read_line_buffer == NULL) { + read_line_buffer_length = BUF_GROWTH_STEP; + read_line_buffer = allocate(read_line_buffer_length); + read_line_buffer[0] = '\0'; + } + + while (1) { + read_line_buffer[read_line_buffer_length-1] = '\377'; + if (fgets(read_line_buffer+pos, read_line_buffer_length-pos, f) == NULL) { + if (feof(f)) { + if (pos > 0) { + // partial line at EOF + return read_line_buffer; + } else { + return NULL; + } + } + perror(name); + exit(1); + } + if (read_line_buffer[read_line_buffer_length-1] != '\0') { + return read_line_buffer; + } + if (read_line_buffer[read_line_buffer_length-2] == '\n') { + return read_line_buffer; + } + new_buffer_length = read_line_buffer_length + BUF_GROWTH_STEP; + new_buffer = allocate(new_buffer_length); + memcpy(new_buffer, read_line_buffer, read_line_buffer_length); + free(read_line_buffer); + pos = read_line_buffer_length-1; + read_line_buffer = new_buffer; + read_line_buffer_length = new_buffer_length; + } +} + +static translation *first_translation_section = NULL; +static translation *last_translation_section = NULL; + +translation *default_translation; + +translation * +new_translation_section(char *name, char *regex) +{ + translation *ret = (translation *)allocate(sizeof(translation)); + int err; + int i; + + if (debug_strokes) { + printf("------------------------\n[%s] %s\n\n", name, regex); + } + ret->next = NULL; + ret->name = alloc_strcat(name, NULL); + if (regex == NULL || *regex == '\0') { + ret->is_default = 1; + default_translation = ret; + } else { + ret->is_default = 0; + err = regcomp(&ret->regex, regex, REG_NOSUB); + if (err != 0) { + regerror(err, &ret->regex, read_line_buffer, read_line_buffer_length); + fprintf(stderr, "error compiling regex for [%s]: %s\n", name, read_line_buffer); + regfree(&ret->regex); + free(ret->name); + free(ret); + return NULL; + } + } + for (i=0; ikey_down[i] = NULL; + ret->key_up[i] = NULL; + } + for (i=0; ishuttle[i] = NULL; + } + for (i=0; ijog[i] = NULL; + } + if (first_translation_section == NULL) { + first_translation_section = ret; + last_translation_section = ret; + } else { + last_translation_section->next = ret; + last_translation_section = ret; + } + return ret; +} + +void +free_strokes(stroke *s) +{ + stroke *next; + while (s != NULL) { + next = s->next; + free(s); + s = next; + } +} + +void +free_translation_section(translation *tr) +{ + int i; + + if (tr != NULL) { + free(tr->name); + if (!tr->is_default) { + regfree(&tr->regex); + } + for (i=0; ikey_down[i]); + free_strokes(tr->key_up[i]); + } + for (i=0; ishuttle[i]); + } + for (i=0; ijog[i]); + } + free(tr); + } +} + +void +free_all_translations(void) +{ + translation *tr = first_translation_section; + translation *next; + + while (tr != NULL) { + next = tr->next; + free_translation_section(tr); + tr = next; + } + first_translation_section = NULL; + last_translation_section = NULL; +} + +static char *config_file_name = NULL; +static time_t config_file_modification_time; + +static char *token_src = NULL; + +// similar to strtok, but it tells us what delimiter was found at the +// end of the token, handles double quoted strings specially, and +// hardcodes the delimiter set. +char * +token(char *src, char *delim_found) +{ + char *delims = " \t\n/\""; + char *d; + char *token_start; + + if (src == NULL) { + src = token_src; + } + if (src == NULL) { + *delim_found = '\0'; + return NULL; + } + token_start = src; + while (*src) { + d = delims; + while (*d && *src != *d) { + d++; + } + if (*d) { + if (src == token_start) { + src++; + token_start = src; + if (*d == '"') { + while (*src && *src != '"' && *src != '\n') { + src++; + } + } else { + continue; + } + } + *delim_found = *d; + if (*src) { + *src = '\0'; + token_src = src+1; + } else { + token_src = NULL; + } + return token_start; + } + src++; + } + token_src = NULL; + *delim_found = '\0'; + if (src == token_start) { + return NULL; + } + return token_start; +} + +typedef struct _keysymmapping { + char *str; + KeySym sym; +} keysymmapping; + +static keysymmapping key_sym_mapping[] = { +#include "keys.h" + { "XK_Button_1", XK_Button_1 }, + { "XK_Button_2", XK_Button_2 }, + { "XK_Button_3", XK_Button_3 }, + { "XK_Scroll_Up", XK_Scroll_Up }, + { "XK_Scroll_Down", XK_Scroll_Down }, + { NULL, 0 } +}; + +KeySym +string_to_KeySym(char *str) +{ + size_t len = strlen(str) + 1; + int i = 0; + + while (key_sym_mapping[i].str != NULL) { + if (!strncmp(str, key_sym_mapping[i].str, len)) { + return key_sym_mapping[i].sym; + } + i++; + } + return 0; +} + +char * +KeySym_to_string(KeySym ks) +{ + int i = 0; + + while (key_sym_mapping[i].sym != 0) { + if (key_sym_mapping[i].sym == ks) { + return key_sym_mapping[i].str; + } + i++; + } + return NULL; +} + +void +print_stroke(stroke *s) +{ + char *str; + + if (s != NULL) { + str = KeySym_to_string(s->keysym); + if (str == NULL) { + printf("0x%x", (int)s->keysym); + str = "???"; + } + printf("%s/%c ", str, s->press ? 'D' : 'U'); + } +} + +void +print_stroke_sequence(char *name, char *up_or_down, stroke *s) +{ + printf("%s[%s]: ", name, up_or_down); + while (s) { + print_stroke(s); + s = s->next; + } + printf("\n"); +} + +stroke **first_stroke; +stroke *last_stroke; +stroke **press_first_stroke; +stroke **release_first_stroke; +int is_keystroke; +char *current_translation; +char *key_name; +int first_release_stroke; // is this the first stroke of a release? +KeySym regular_key_down; + +#define NUM_MODIFIERS 64 + +stroke modifiers_down[NUM_MODIFIERS]; +int modifier_count; + +void +append_stroke(KeySym sym, int press) +{ + stroke *s = (stroke *)allocate(sizeof(stroke)); + + s->next = NULL; + s->keysym = sym; + s->press = press; + if (*first_stroke) { + last_stroke->next = s; + } else { + *first_stroke = s; + } + last_stroke = s; +} + +// s->press values in modifiers_down: +// PRESS -> down +// HOLD -> held +// PRESS_RELEASE -> released, but to be re-pressed if necessary +// RELEASE -> up + +void +mark_as_down(KeySym sym, int hold) +{ + int i; + + for (i=0; i NUM_MODIFIERS) { + fprintf(stderr, "too many modifiers down in [%s]%s\n", current_translation, key_name); + return; + } + modifiers_down[modifier_count].keysym = sym; + modifiers_down[modifier_count].press = hold ? HOLD : PRESS; + modifier_count++; +} + +void +mark_as_up(KeySym sym) +{ + int i; + + for (i=0; iname; + key_name = which_key; + is_keystroke = 0; + first_release_stroke = 0; + regular_key_down = 0; + modifier_count = 0; + // JL, JR + if (tolower(which_key[0]) == 'j' && + (tolower(which_key[1]) == 'l' || tolower(which_key[1]) == 'r') && + which_key[2] == '\0') { + k = tolower(which_key[1]) == 'l' ? 0 : 1; + first_stroke = &(tr->jog[k]); + } else { + n = 0; + sscanf(which_key, "%c%d%n", &c, &k, &n); + if (n != (int)strlen(which_key)) { + fprintf(stderr, "bad key name: [%s]%s\n", current_translation, which_key); + return 1; + } + switch (c) { + case 'k': + case 'K': + // K1 .. K15 + k = k - 1; + if (k < 0 || k >= NUM_KEYS) { + fprintf(stderr, "bad key name: [%s]%s\n", current_translation, which_key); + return 1; + } + first_stroke = &(tr->key_down[k]); + release_first_stroke = &(tr->key_up[k]); + is_keystroke = 1; + break; + case 's': + case 'S': + // S-7 .. S7 + if (k < -7 || k > 7) { + fprintf(stderr, "bad key name: [%s]%s\n", current_translation, which_key); + return 1; + } + first_stroke = &(tr->shuttle[k+7]); + break; + default: + fprintf(stderr, "bad key name: [%s]%s\n", current_translation, which_key); + return 1; + } + } + if (*first_stroke != NULL) { + fprintf(stderr, "can't redefine key: [%s]%s\n", current_translation, which_key); + return 1; + } + press_first_stroke = first_stroke; + return 0; +} + +void +add_keysym(KeySym sym, int press_release) +{ + //printf("add_keysym(0x%x, %d)\n", (int)sym, press_release); + switch (press_release) { + case PRESS: + append_stroke(sym, 1); + mark_as_down(sym, 0); + break; + case RELEASE: + append_stroke(sym, 0); + mark_as_up(sym); + break; + case HOLD: + append_stroke(sym, 1); + mark_as_down(sym, 1); + break; + case PRESS_RELEASE: + default: + if (first_release_stroke) { + re_press_temp_modifiers(); + } + if (regular_key_down != 0) { + append_stroke(regular_key_down, 0); + } + append_stroke(sym, 1); + regular_key_down = sym; + first_release_stroke = 0; + break; + } +} + +void +add_release(int all_keys) +{ + //printf("add_release(%d)\n", all_keys); + release_modifiers(all_keys); + if (!all_keys) { + first_stroke = release_first_stroke; + } + if (regular_key_down != 0) { + append_stroke(regular_key_down, 0); + } + regular_key_down = 0; + first_release_stroke = 1; +} + +void +add_keystroke(char *keySymName, int press_release) +{ + KeySym sym; + + if (is_keystroke && !strncmp(keySymName, "RELEASE", 8)) { + add_release(0); + return; + } + sym = string_to_KeySym(keySymName); + if (sym != 0) { + add_keysym(sym, press_release); + } else { + fprintf(stderr, "unrecognized KeySym: %s\n", keySymName); + } +} + +void +add_string(char *str) +{ + while (str && *str) { + if (*str >= ' ' && *str <= '~') { + add_keysym((KeySym)(*str), PRESS_RELEASE); + } + str++; + } +} + +void +finish_translation(void) +{ + //printf("finish_translation()\n"); + if (is_keystroke) { + add_release(0); + } + add_release(1); + if (debug_strokes) { + if (is_keystroke) { + print_stroke_sequence(key_name, "D", *press_first_stroke); + print_stroke_sequence(key_name, "U", *release_first_stroke); + } else { + print_stroke_sequence(key_name, "", *first_stroke); + } + printf("\n"); + } +} + +void +read_config_file(void) +{ + struct stat buf; + char *home; + char *line; + char *s; + char *name; + char *regex; + char *tok; + char *which_key; + char *updown; + char delim; + translation *tr = NULL; + FILE *f; + + if (config_file_name == NULL) { + config_file_name = getenv("SHUTTLE_CONFIG_FILE"); + if (config_file_name == NULL) { + home = getenv("HOME"); + config_file_name = alloc_strcat(home, "/.shuttlerc"); + } else { + config_file_name = alloc_strcat(config_file_name, NULL); + } + config_file_modification_time = 0; + } + if (stat(config_file_name, &buf) < 0) { + perror(config_file_name); + return; + } + if (buf.st_mtime == 0) { + buf.st_mtime = 1; + } + if (buf.st_mtime > config_file_modification_time) { + config_file_modification_time = buf.st_mtime; + + f = fopen(config_file_name, "r"); + if (f == NULL) { + perror(config_file_name); + return; + } + + free_all_translations(); + debug_regex = 0; + debug_strokes = 0; + + while ((line=read_line(f, config_file_name)) != NULL) { + //printf("line: %s", line); + + s = line; + while (*s && isspace(*s)) { + s++; + } + if (*s == '#') { + continue; + } + if (*s == '[') { + // [name] regex\n + name = ++s; + while (*s && *s != ']') { + s++; + } + regex = NULL; + if (*s) { + *s = '\0'; + s++; + while (*s && isspace(*s)) { + s++; + } + regex = s; + while (*s) { + s++; + } + s--; + while (s > regex && isspace(*s)) { + s--; + } + s[1] = '\0'; + } + tr = new_translation_section(name, regex); + continue; + } + + tok = token(s, &delim); + if (tok == NULL) { + continue; + } + if (!strcmp(tok, "DEBUG_REGEX")) { + debug_regex = 1; + continue; + } + if (!strcmp(tok, "DEBUG_STROKES")) { + debug_strokes = 1; + continue; + } + which_key = tok; + if (start_translation(tr, which_key)) { + continue; + } + tok = token(NULL, &delim); + while (tok != NULL) { + if (delim != '"' && tok[0] == '#') { + break; // skip rest as comment + } + //printf("token: [%s] delim [%d]\n", tok, delim); + switch (delim) { + case ' ': + case '\t': + case '\n': + add_keystroke(tok, PRESS_RELEASE); + break; + case '"': + add_string(tok); + break; + default: // should be slash + updown = token(NULL, &delim); + if (updown != NULL) { + switch (updown[0]) { + case 'U': + add_keystroke(tok, RELEASE); + break; + case 'D': + add_keystroke(tok, PRESS); + break; + case 'H': + add_keystroke(tok, HOLD); + break; + default: + fprintf(stderr, "invalid up/down modifier [%s]%s: %s\n", name, which_key, updown); + add_keystroke(tok, PRESS); + break; + } + } + } + tok = token(NULL, &delim); + } + finish_translation(); + } + + fclose(f); + + } +} + +translation * +get_translation(char *win_title) +{ + translation *tr; + + read_config_file(); + tr = first_translation_section; + while (tr != NULL) { + if (tr->is_default) { + return tr; + } + if (regexec(&tr->regex, win_title, 0, NULL, 0) == 0) { + return tr; + } + tr = tr->next; + } + return NULL; +} diff --git a/shuttle b/shuttle new file mode 100755 index 0000000..405eea5 --- /dev/null +++ b/shuttle @@ -0,0 +1,3 @@ +#!/bin/bash + +exec shuttlepro /dev/input/by-id/usb-Contour_Design_ShuttlePRO_v2-event-if00 diff --git a/shuttle.h b/shuttle.h new file mode 100644 index 0000000..dd185a6 --- /dev/null +++ b/shuttle.h @@ -0,0 +1,94 @@ + +// Copyright 2013 Eric Messick (FixedImagePhoto.com/Contact) + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include + + +// delay in ms before processing each XTest event +// CurrentTime means no delay +#define DELAY CurrentTime + +// protocol for events from the shuttlepro HUD device +// +// ev.type values: +#define EVENT_TYPE_DONE 0 +#define EVENT_TYPE_KEY 1 +#define EVENT_TYPE_JOGSHUTTLE 2 +#define EVENT_TYPE_ACTIVE_KEY 4 + +// ev.code when ev.type == KEY +#define EVENT_CODE_KEY1 256 +// KEY2 257, etc... + +// ev.value when ev.type == KEY +// 1 -> PRESS; 0 -> RELEASE + +// ev.code when ev.type == JOGSHUTTLE +#define EVENT_CODE_JOG 7 +#define EVENT_CODE_SHUTTLE 8 + +// ev.value when ev.code == JOG +// 8 bit value changing by one for each jog step + +// ev.value when ev.code == SHUTTLE +// -7 .. 7 encoding shuttle position + +// we define these as extra KeySyms to represent mouse events +#define XK_Button_0 0x2000000 // just an offset, not a real button +#define XK_Button_1 0x2000001 +#define XK_Button_2 0x2000002 +#define XK_Button_3 0x2000003 +#define XK_Scroll_Up 0x2000004 +#define XK_Scroll_Down 0x2000005 + +#define PRESS 1 +#define RELEASE 2 +#define PRESS_RELEASE 3 +#define HOLD 4 + +#define NUM_KEYS 15 +#define NUM_SHUTTLES 15 +#define NUM_JOGS 2 + +typedef struct _stroke { + struct _stroke *next; + KeySym keysym; + int press; // zero -> release, non-zero -> press +} stroke; + +#define KJS_KEY_DOWN 1 +#define KJS_KEY_UP 2 +#define KJS_SHUTTLE 3 +#define KJS_JOG 4 + +typedef struct _translation { + struct _translation *next; + char *name; + int is_default; + regex_t regex; + stroke *key_down[NUM_KEYS]; + stroke *key_up[NUM_KEYS]; + stroke *shuttle[NUM_SHUTTLES]; + stroke *jog[NUM_JOGS]; +} translation; + +extern translation *get_translation(char *win_title); diff --git a/shuttlepro.c b/shuttlepro.c new file mode 100644 index 0000000..238ad94 --- /dev/null +++ b/shuttlepro.c @@ -0,0 +1,330 @@ + +/* + + Contour ShuttlePro v2 interface + + Copyright 2013 Eric Messick (FixedImagePhoto.com/Contact) + + Based on a version (c) 2006 Trammell Hudson + + which was in turn + + Based heavily on code by Arendt David + +*/ + +#include "shuttle.h" + +typedef struct input_event EV; + +extern int debug_regex; +extern translation *default_translation; + +unsigned short jogvalue = 0xffff; +int shuttlevalue = 0xffff; +struct timeval last_shuttle; +int need_synthetic_shuttle; +Display *display; + + +void +initdisplay(void) +{ + int event, error, major, minor; + + display = XOpenDisplay(0); + if (!display) { + fprintf(stderr, "unable to open X display\n"); + exit(1); + } + if (!XTestQueryExtension(display, &event, &error, &major, &minor)) { + fprintf(stderr, "Xtest extensions not supported\n"); + XCloseDisplay(display); + exit(1); + } +} + +void +send_button(unsigned int button, int press) +{ + XTestFakeButtonEvent(display, button, press ? True : False, DELAY); +} + +void +send_key(KeySym key, int press) +{ + KeyCode keycode; + + if (key >= XK_Button_1 && key <= XK_Scroll_Down) { + send_button((unsigned int)key - XK_Button_0, press); + return; + } + keycode = XKeysymToKeycode(display, key); + XTestFakeKeyEvent(display, keycode, press ? True : False, DELAY); +} + +stroke * +fetch_stroke(translation *tr, int kjs, int index) +{ + if (tr != NULL) { + switch (kjs) { + case KJS_SHUTTLE: + return tr->shuttle[index]; + case KJS_JOG: + return tr->jog[index]; + case KJS_KEY_UP: + return tr->key_up[index]; + case KJS_KEY_DOWN: + default: + return tr->key_down[index]; + } + } + return NULL; +} + +void +send_stroke_sequence(translation *tr, int kjs, int index) +{ + stroke *s; + + s = fetch_stroke(tr, kjs, index); + if (s == NULL) { + s = fetch_stroke(default_translation, kjs, index); + } + while (s) { + send_key(s->keysym, s->press); + s = s->next; + } + XFlush(display); +} + +void +key(unsigned short code, unsigned int value, translation *tr) +{ + code -= EVENT_CODE_KEY1; + + if (code <= NUM_KEYS) { + send_stroke_sequence(tr, value ? KJS_KEY_DOWN : KJS_KEY_UP, code); + } else { + fprintf(stderr, "key(%d, %d) out of range\n", code + EVENT_CODE_KEY1, value); + } +} + + +void +shuttle(int value, translation *tr) +{ + if (value < -7 || value > 7) { + fprintf(stderr, "shuttle(%d) out of range\n", value); + } else { + gettimeofday(&last_shuttle, 0); + need_synthetic_shuttle = value != 0; + if( value != shuttlevalue ) { + shuttlevalue = value; + send_stroke_sequence(tr, KJS_SHUTTLE, value+7); + } + } +} + +// Due to a bug (?) in the way Linux HID handles the ShuttlePro, the +// center position is not reported for the shuttle wheel. Instead, +// a jog event is generated immediately when it returns. We check to +// see if the time since the last shuttle was more than a few ms ago +// and generate a shuttle of 0 if so. +// +// Note, this fails if jogvalue happens to be 0, as we don't see that +// event either! +void +jog(unsigned int value, translation *tr) +{ + int direction; + struct timeval now; + struct timeval delta; + + // We should generate a synthetic event for the shuttle going + // to the home position if we have not seen one recently + if (need_synthetic_shuttle) { + gettimeofday( &now, 0 ); + timersub( &now, &last_shuttle, &delta ); + + if (delta.tv_sec >= 1 || delta.tv_usec >= 5000) { + shuttle(0, tr); + need_synthetic_shuttle = 0; + } + } + + if (jogvalue != 0xffff) { + value = value & 0xff; + direction = ((value - jogvalue) & 0x80) ? -1 : 1; + while (jogvalue != value) { + // driver fails to send an event when jogvalue == 0 + if (jogvalue != 0) { + send_stroke_sequence(tr, KJS_JOG, direction > 0 ? 1 : 0); + } + jogvalue = (jogvalue + direction) & 0xff; + } + } + jogvalue = value; +} + +void +jogshuttle(unsigned short code, unsigned int value, translation *tr) +{ + switch (code) { + case EVENT_CODE_JOG: + jog(value, tr); + break; + case EVENT_CODE_SHUTTLE: + shuttle(value, tr); + break; + default: + fprintf(stderr, "jogshuttle(%d, %d) invalid code\n", code, value); + break; + } +} + +char * +get_window_name(Window win) +{ + Atom prop = XInternAtom(display, "WM_NAME", False); + Atom type; + int form; + unsigned long remain, len; + unsigned char *list; + + if (XGetWindowProperty(display, win, prop, 0, 1024, False, + AnyPropertyType, &type, &form, &len, &remain, + &list) != Success) { + fprintf(stderr, "XGetWindowProperty failed for window 0x%x\n", (int)win); + return NULL; + } + + return (char*)list; +} + +char * +walk_window_tree(Window win) +{ + char *window_name; + Window root = 0; + Window parent; + Window *children; + unsigned int nchildren; + + while (win != root) { + window_name = get_window_name(win); + if (window_name != NULL) { + return window_name; + } + if (XQueryTree(display, win, &root, &parent, &children, &nchildren)) { + win = parent; + XFree(children); + } else { + fprintf(stderr, "XQueryTree failed for window 0x%x\n", (int)win); + return NULL; + } + } + return NULL; +} + +static Window last_focused_window = 0; +static translation *last_window_translation = NULL; + +translation * +get_focused_window_translation() +{ + Window focus; + int revert_to; + char *window_name = NULL; + char *name; + + XGetInputFocus(display, &focus, &revert_to); + if (focus != last_focused_window) { + last_focused_window = focus; + window_name = walk_window_tree(focus); + if (window_name == NULL) { + name = "-- Unlabeled Window --"; + } else { + name = window_name; + } + last_window_translation = get_translation(name); + if (debug_regex) { + if (last_window_translation != NULL) { + printf("translation: %s for %s\n", last_window_translation->name, name); + } else { + printf("no translation found for %s\n", name); + } + } + if (window_name != NULL) { + XFree(window_name); + } + } + return last_window_translation; +} + +void +handle_event(EV ev) +{ + translation *tr = get_focused_window_translation(); + + //fprintf(stderr, "event: (%d, %d, 0x%x)\n", ev.type, ev.code, ev.value); + if (tr != NULL) { + switch (ev.type) { + case EVENT_TYPE_DONE: + case EVENT_TYPE_ACTIVE_KEY: + break; + case EVENT_TYPE_KEY: + key(ev.code, ev.value, tr); + break; + case EVENT_TYPE_JOGSHUTTLE: + jogshuttle(ev.code, ev.value, tr); + break; + default: + fprintf(stderr, "handle_event() invalid type code\n"); + break; + } + } +} + + +int +main(int argc, char **argv) +{ + EV ev; + int nread; + char *dev_name; + int fd; + + if (argc != 2) { + fprintf(stderr, "usage: shuttlepro \n" ); + exit(1); + } + + dev_name = argv[1]; + fd = open(dev_name, O_RDONLY); + if (fd < 0) { + perror(dev_name); + exit(1); + } + + // Flag it as exclusive access + if(ioctl( fd, EVIOCGRAB, 1 ) < 0) { + perror( "evgrab ioctl" ); + exit(1); + } + + initdisplay(); + + while (1) { + nread = read(fd, &ev, sizeof(ev)); + if (nread == sizeof(ev)) { + handle_event(ev); + } else { + if (nread < 0) { + perror("read event"); + } else { + fprintf(stderr, "short read: %d\n", nread); + } + } + } +}