From d399f2edd7703da5f394cab2639816347d089c1b Mon Sep 17 00:00:00 2001 From: Eric Messick Date: Tue, 7 May 2013 11:06:51 -0700 Subject: [PATCH] Initial commit --- 99-ShuttlePRO.rules | 6 + Makefile | 28 ++ README | 45 +++ TODO | 18 + example.shuttlerc | 119 +++++++ keys.sed | 5 + readconfig.c | 809 ++++++++++++++++++++++++++++++++++++++++++++ shuttle | 3 + shuttle.h | 94 +++++ shuttlepro.c | 330 ++++++++++++++++++ 10 files changed, 1457 insertions(+) create mode 100644 99-ShuttlePRO.rules create mode 100644 Makefile create mode 100644 README create mode 100644 TODO create mode 100644 example.shuttlerc create mode 100644 keys.sed create mode 100644 readconfig.c create mode 100755 shuttle create mode 100644 shuttle.h create mode 100644 shuttlepro.c 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); + } + } + } +}