sxhkd-rc

[fork] simple X hotkey daemon (but for the rc shell)
Log | Files | Refs | README | LICENSE

commit 8e57c539a0b7b4480294c3c47aca624afdf6718d
parent 7f4005ba04487801a40d69e6229f19841ff0e967
Author: Bastien Dejean <nihilhill@gmail.com>
Date:   Tue, 18 Jun 2013 14:41:52 +0200

Implement chord chains

And also:
- Allow commands and hotkeys to be spread across multiple lines.
- Allow sequences to be present in the hotkey and not in the command and
  vice versa.
- Handle literal braces and commas via the backslash inhibitor.

Diffstat:
MREADME.md | 46+++++++++++++++++++++++++++++-----------------
DTODO.md | 4----
Mhelpers.c | 25+++++++++++++++++++++++++
Mhelpers.h | 2++
Mhotkeys.c | 391+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mhotkeys.h | 29++++++++++++++++++++++-------
Msxhkd.1 | 47+++++++++++++++++++++++++----------------------
Msxhkd.c | 156++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msxhkd.h | 34++++++++++++++++++++++------------
9 files changed, 488 insertions(+), 246 deletions(-)

diff --git a/README.md b/README.md @@ -6,40 +6,49 @@ sxhkd is a simple X hotkey daemon. ### Synopsis - sxhkd [OPTIONS] [EXTRA_CONFIG ...] + sxhkd [OPTIONS] [EXTRA_CONFIG …] ### Options * `-h`: Print the synopsis to standard output and exit. * `-v`: Print the version information to standard output and exit. +* `-t TIMEOUT`: Timeout in seconds for the recording of chord chains. * `-c CONFIG_FILE`: Read the main configuration from the given file. * `-r REDIR_FILE`: Redirect the commands output to the given file. ## Configuration Each line of the configuration file is interpreted as so: -- If it starts with `#`, it is ignored. -- If it starts with one or more white space characters, it is read as a command. -- Otherwise, it is parsed as a hotkey: each key name is separated by spaces and/or `+` characters. +- If it is empty or starts with `#`, it is ignored. +- If it starts with a space, it is read as a command. +- Otherwise, it is read as a hotkey. General syntax: - [MODIFIER + ]*[@|!|:]KEYSYM + HOTKEY COMMAND -Where `MODIFIER` is one of the following names: `super`, `hyper`, `meta`, `alt`, `control`, `ctrl`, `shift`, `mode_switch`, `lock`, `mod1`, `mod2`, `mod3`, `mod4`, `mod5`. + HOTKEY := CHORD_1 ; CHORD_2 ; … ; CHORD_n + CHORD_i := [MODIFIER_i][@|!|:]KEYSYM_i + MODIFIER_i := MODIFIER_i1 + MODIFIER_i2 + … + MODIFIER_ik + +The valid modifier names are: `super`, `hyper`, `meta`, `alt`, `control`, `ctrl`, `shift`, `mode_switch`, `lock`, `mod1`, `mod2`, `mod3`, `mod4` and `mod5`. + +The keysym names are given by the output of `xev`. + +Hotkeys and commands can be spread across multiple lines by ending each partial line with a backslash character. + +When multiple chords are separated by semicolons, the hotkey is a chord chain: the command will only by executed after receiving each chord of the chain in consecutive order. If `@` is added at the beginning of the keysym, the command will be run on key release events, otherwise on key press events. -If `!` is added at the beginning of the keysym, the command will be run on motion notify events and must contain two integer conversion specifications which will be replaced by the *x* and *y* coordinates of the pointer relative to the root window referential (the only valid button keysyms for this type of hotkeys are: `button1`, ..., `button5`). +If `!` is added at the beginning of the keysym, the command will be run on motion notify events and must contain two integer conversion specifications which will be replaced by the *x* and *y* coordinates of the pointer relative to the root window referential (the only valid button keysyms for this type of hotkeys are: `button1`, …, `button5`). If `:` is added at the beginning of the keysym, the captured event will be replayed for the other clients. -The keysym names are those your will get from `xev`. - -Mouse hotkeys can be defined by using one of the following special keysym names: `button1`, `button2`, `button3`, ..., `button24`. +Mouse hotkeys can be defined by using one of the following special keysym names: `button1`, `button2`, `button3`, …, `button24`. -The hotkey can contain a sequence of the form `{STRING_1,…,STRING_N}`, in which case, the command must also contain a sequence with *N* elements: the pairing of the two sequences generates *N* hotkeys. +The hotkey and the command may contain a sequence of the form `{STRING_1,…,STRING_N}`. In addition, the sequences can contain ranges of the form `A-Z` where *A* and *Z* are alphanumeric characters. @@ -49,7 +58,7 @@ What is actually executed is `SHELL -c COMMAND`, which means you can use environ If *sxhkd* receives a `SIGUSR1` signal, it will reload its configuration file. -If no configuration file is specified through the `-c` option, the following is used: `$XDG_CONFIG_HOME/sxhkd/sxhkdrc`. +If no configuration file is specified via the `-c` option, the following is used: `$XDG_CONFIG_HOME/sxhkd/sxhkdrc`. ## Example Configuration @@ -77,14 +86,17 @@ If no configuration file is specified through the `-c` option, the following is :button1 bspc grab_pointer focus - super + button{1,2,3} + super + button{1-3} bspc grab_pointer {move,resize_side,resize_corner} - super + !button{1,2,3} - bspc {track_pointer,track_pointer,track_pointer} %i %i + super + !button{1-3} + bspc track_pointer %i %i + + super + @button{1-3} + bspc ungrab_pointer - super + @button{1,2,3} - bspc {ungrab_pointer,ungrab_pointer,ungrab_pointer} + super + o ; {e,w,m} + {gvim,firefox,thunderbird} ## Installation diff --git a/TODO.md b/TODO.md @@ -1,4 +0,0 @@ -- Multi-line commands. -- Disymmetric sequences. -- Handle magic chars inhibition via backslash. -- Chained bindings? diff --git a/helpers.c b/helpers.c @@ -1,7 +1,9 @@ #include <stdio.h> #include <stdlib.h> #include <stdarg.h> +#include <string.h> #include <unistd.h> +#include <ctype.h> #include <fcntl.h> #include <errno.h> #include <sys/wait.h> @@ -50,3 +52,26 @@ void run(char *command) char *cmd[] = {shell, "-c", command, NULL}; spawn(cmd); } + +char *lgraph(char *s) +{ + size_t len = strlen(s); + unsigned int i = 0; + while (i < len && !isgraph(s[i])) + i++; + if (i < len) + return (s + i); + else + return NULL; +} + +char *rgraph(char *s) +{ + int i = strlen(s) - 1; + while (i >= 0 && !isgraph(s[i])) + i--; + if (i >= 0) + return (s + i); + else + return NULL; +} diff --git a/helpers.h b/helpers.h @@ -17,5 +17,7 @@ __attribute__((noreturn)) void err(char *, ...); void spawn(char *[]); void run(char *); +char *lgraph(char *); +char *rgraph(char *); #endif diff --git a/hotkeys.c b/hotkeys.c @@ -1,6 +1,8 @@ #include <stdlib.h> #include <string.h> #include <stdbool.h> +#include <unistd.h> +#include <inttypes.h> #include "locales.h" #include "hotkeys.h" #include "helpers.h" @@ -2340,19 +2342,24 @@ keysym_dict_t nks_dict[] = {/*{{{*/ void grab(void) { - PUTS("grab"); - for (hotkey_t *hk = hotkeys; hk != NULL; hk = hk->next) { - if (hk->button == XCB_NONE) { - xcb_keycode_t *keycodes = keycodes_from_keysym(hk->keysym); + for (hotkey_t *hk = hotkeys; hk != NULL; hk = hk->next) + for (chord_t *chord = hk->chain->head; chord != NULL; chord = chord->next) + grab_chord(chord); +} + +void grab_chord(chord_t *chord) +{ + for (chord_t *c = chord; c != NULL; c = c->more) { + if (c->button == XCB_NONE) { + xcb_keycode_t *keycodes = keycodes_from_keysym(c->keysym); if (keycodes != NULL) for (xcb_keycode_t *kc = keycodes; *kc != XCB_NO_SYMBOL; kc++) - if (hk->keysym == xcb_key_symbols_get_keysym(symbols, *kc, 0)) { - PRINTF("keycode for %u is %u\n", hk->keysym, *kc); - grab_key_button(*kc, hk->button, hk->modfield); + if (c->keysym == xcb_key_symbols_get_keysym(symbols, *kc, 0)) { + grab_key_button(*kc, c->button, c->modfield); } free(keycodes); } else { - grab_key_button(XCB_NONE, hk->button, hk->modfield); + grab_key_button(XCB_NONE, c->button, c->modfield); } } } @@ -2390,7 +2397,7 @@ void grab_key_button_checked(xcb_keycode_t keycode, xcb_button_t button, uint16_ if (err->error_code == XCB_ACCESS) warn("the combination is already grabbed.\n"); else - warn("error no %u encountered.\n", err->error_code); + warn("error %u encountered.\n", err->error_code); free(err); } else { PRINTF("grab %s %u %u\n", type, value, modfield); @@ -2462,28 +2469,111 @@ xcb_keycode_t *keycodes_from_keysym(xcb_keysym_t keysym) return result; } -bool parse_hotkey(char *string, xcb_keysym_t *keysym, xcb_button_t *button, uint16_t *modfield, uint8_t *event_type, bool *replay_event) { - char backup[MAXLEN]; - strncpy(backup, string, sizeof(backup)); - backup[strlen(string)] = '\0'; - for (char *name = strtok(string, TOK_SEP); name != NULL; name = strtok(NULL, TOK_SEP)) { - if (name[0] == RELEASE_PREFIX) { - *event_type = XCB_KEY_RELEASE; - name++; - } else if (name[0] == MOTION_PREFIX) { - *event_type = XCB_MOTION_NOTIFY; - name++; - } else if (name[0] == REPLAY_PREFIX) { - *replay_event = true; - name++; - } - if (!parse_modifier(name, modfield) && !parse_keysym(name, keysym) && !parse_button(name, button)) { - warn("Unrecognized key name: '%s'.\n", name); - } +void add_chord(chain_t *chain, chord_t *chord) +{ + if (chain->head == NULL) { + chain->head = chain->tail = chain->state = chord; + } else { + chain->tail->next = chord; + chain->tail = chord; } - if (*keysym == XCB_NO_SYMBOL && *button == XCB_NONE) { - warn("Invalid hotkey: '%s'.\n", backup); - return false; +} + +chord_t *make_chord(xcb_keysym_t keysym, xcb_button_t button, uint16_t modfield, uint8_t event_type, bool replay_event) +{ + chord_t *chord; + if (button == XCB_NONE) { + chord_t *prev = NULL; + chord_t *orig = NULL; + xcb_keycode_t *keycodes = keycodes_from_keysym(keysym); + if (keycodes != NULL) + for (xcb_keycode_t *kc = keycodes; *kc != XCB_NO_SYMBOL; kc++) { + xcb_keysym_t natural_keysym = xcb_key_symbols_get_keysym(symbols, *kc, 0); + for (unsigned char col = 0; col < KEYSYMS_PER_KEYCODE; col++) { + xcb_keysym_t ks = xcb_key_symbols_get_keysym(symbols, *kc, col); + if (ks == keysym) { + uint16_t implicit_modfield = (col & 1 ? XCB_MOD_MASK_SHIFT : 0) | (col & 2 ? modfield_from_keysym(Mode_switch) : 0); + uint16_t explicit_modfield = modfield | implicit_modfield; + chord = malloc(sizeof(chord_t)); + bool unique = true; + for (chord_t *c = orig; unique && c != NULL; c = c->more) + if (c->modfield == explicit_modfield && c->keysym == natural_keysym) + unique = false; + if (!unique) + break; + chord->keysym = natural_keysym; + chord->button = button; + chord->modfield = explicit_modfield; + chord->next = chord->more = NULL; + chord->event_type = event_type; + chord->replay_event = replay_event; + if (prev != NULL) + prev->more = chord; + else + orig = chord; + prev = chord; + PRINTF("key chord %u %u\n", natural_keysym, explicit_modfield); + break; + } + } + } + free(keycodes); + chord = orig; + } else { + chord = malloc(sizeof(chord_t)); + chord->keysym = keysym; + chord->button = button; + chord->modfield = modfield; + chord->event_type = event_type; + chord->replay_event = replay_event; + chord->next = chord->more = NULL; + PRINTF("button chord %u %u\n", button, modfield); + } + return chord; +} + +chain_t *make_chain(void) +{ + chain_t *chain = malloc(sizeof(chain_t)); + chain->head = chain->tail = chain->state = NULL; + return chain; +} + +bool parse_chain(char *string, chain_t *chain) +{ + xcb_keysym_t keysym = XCB_NO_SYMBOL; + xcb_button_t button = XCB_NONE; + uint16_t modfield = 0; + uint8_t event_type = XCB_KEY_PRESS; + bool replay_event = false; + char *outerptr; + char *innerptr; + for (char *s = strtok_r(string, LNK_SEP, &outerptr); s != NULL; s = strtok_r(NULL, LNK_SEP, &outerptr)) { + for (char *name = strtok_r(s, SYM_SEP, &innerptr); name != NULL; name = strtok_r(NULL, SYM_SEP, &innerptr)) { + if (name[0] == RELEASE_PREFIX) { + event_type = XCB_KEY_RELEASE; + name++; + } else if (name[0] == MOTION_PREFIX) { + event_type = XCB_MOTION_NOTIFY; + name++; + } else if (name[0] == REPLAY_PREFIX) { + replay_event = true; + name++; + } + if (!parse_modifier(name, &modfield) && !parse_keysym(name, &keysym) && !parse_button(name, &button)) { + warn("Unknown name: '%s'.\n", name); + return false; + } + } + if (button != XCB_NONE) + event_type = key_to_button(event_type); + chord_t *chord = make_chord(keysym, button, modfield, event_type, replay_event); + add_chord(chain, chord); + keysym = XCB_NO_SYMBOL; + button = XCB_NONE; + modfield = 0; + event_type = XCB_KEY_PRESS; + replay_event = false; } return true; } @@ -2503,7 +2593,7 @@ bool parse_keysym(char *name, xcb_keysym_t *keysym) bool parse_button(char *name, xcb_button_t *butidx) { /* X handles up to 24 buttons */ - return (sscanf(name, "button%hhu", butidx) == 1); + return (sscanf(name, "button%" SCNu8, butidx) == 1); } bool parse_modifier(char *name, uint16_t *modfield) @@ -2597,123 +2687,186 @@ void get_lock_fields(void) PRINTF("lock fields %u %u %u\n", num_lock, caps_lock, scroll_lock); } -bool extract_sequence(char *string, char *prefix, char *sequence, char *suffix) -{ - char *begin = strchr(string, SEQ_BEGIN); - char *end = strrchr(string, SEQ_END); - if (begin == NULL || end == NULL || ((end - begin - 1) < SEQ_MIN_LEN)) - return false; - strncpy(sequence, begin + 1, end - begin - 1); - strncpy(prefix, string, begin - string); - strncpy(suffix, end + 1, strlen(string) - (1 + end - string)); - sequence[end - begin - 1] = '\0'; - prefix[begin - string] = '\0'; - suffix[strlen(string) - (1 + end - string)] = '\0'; - return true; -} - -void unfold_hotkeys(char *folded_hotkey, char *folded_command) +void process_hotkey(char *hotkey_string, char *command_string) { - char hotkey_sequence[MAXLEN]; - char hotkey_prefix[MAXLEN]; - char hotkey_suffix[MAXLEN]; - char command_sequence[MAXLEN]; - char command_prefix[MAXLEN]; - char command_suffix[MAXLEN]; - if (!extract_sequence(folded_hotkey, hotkey_prefix, hotkey_sequence, hotkey_suffix) || !extract_sequence(folded_command, command_prefix, command_sequence, command_suffix)) { - warn("Couldn't extract sequence from '%s' or '%s'.\n", folded_hotkey, folded_command); - return; - } + char hotkey[MAXLEN] = {0}; + char command[MAXLEN] = {0}; + char hotkey_sequence[MAXLEN] = {0}; + char hotkey_prefix[MAXLEN] = {0}; + char hotkey_suffix[MAXLEN] = {0}; + char command_sequence[MAXLEN] = {0}; + char command_prefix[MAXLEN] = {0}; + char command_suffix[MAXLEN] = {0}; + char hk_item[MAXLEN] = {0}; + char cm_item[MAXLEN] = {0}; + bool hk_loop = extract_sequence(hotkey_string, hotkey_prefix, hotkey_sequence, hotkey_suffix); + bool cm_loop = extract_sequence(command_string, command_prefix, command_sequence, command_suffix); char unfolded_hotkey[MAXLEN], unfolded_command[MAXLEN]; - xcb_keysym_t keysym = XCB_NO_SYMBOL; - xcb_button_t button = XCB_NONE; - uint16_t modfield = 0; - uint8_t event_type = XCB_KEY_PRESS; - bool replay_event = false; - char *hk_ptr, *cmd_ptr; - char hk_a = 1, hk_z = 0, cmd_a = 1, cmd_z = 0; - char *hk_item = strtok_r(hotkey_sequence, SEQ_SEP, &hk_ptr); - char *cmd_item = strtok_r(command_sequence, SEQ_SEP, &cmd_ptr); + char *hk_ptr, *cm_ptr; + char hk_a = 1, hk_z = 0, cm_a = 1, cm_z = 0; + hk_ptr = gettok(hk_item, hotkey_sequence, SEQ_SEP); + cm_ptr = gettok(cm_item, command_sequence, SEQ_SEP); - while ((hk_item != NULL || hk_a <= hk_z) && (cmd_item != NULL || cmd_a <= cmd_z)) { -#define PREGEN(elt, ra, rz, prefix, suffix, unf) \ - if (ra > rz && strlen(elt) == 3 && sscanf(elt, "%c-%c", &ra, &rz) == 2 && ra >= rz) \ + while ((!hk_loop || hk_item[0] != '\0' || hk_a <= hk_z) && (!cm_loop || cm_item[0] != '\0' || cm_a <= cm_z)) { +#define PREPROC(itm, ra, rz, prefix, suffix, unf) \ + if (ra > rz && strlen(itm) == 3 && sscanf(itm, "%c-%c", &ra, &rz) == 2 && ra >= rz) \ ra = 1, rz = 0; \ if (ra <= rz) \ snprintf(unf, sizeof(unf), "%s%c%s", prefix, ra, suffix); \ else \ - snprintf(unf, sizeof(unf), "%s%s%s", prefix, elt, suffix); - PREGEN(hk_item, hk_a, hk_z, hotkey_prefix, hotkey_suffix, unfolded_hotkey) - PREGEN(cmd_item, cmd_a, cmd_z, command_prefix, command_suffix, unfolded_command) -#undef PREGEN + snprintf(unf, sizeof(unf), "%s%s%s", prefix, itm, suffix); + PREPROC(hk_item, hk_a, hk_z, hotkey_prefix, hotkey_suffix, unfolded_hotkey) + PREPROC(cm_item, cm_a, cm_z, command_prefix, command_suffix, unfolded_command) +#undef PREPROC - if (parse_hotkey(unfolded_hotkey, &keysym, &button, &modfield, &event_type, &replay_event)) - generate_hotkeys(keysym, button, modfield, event_type, replay_event, unfolded_command); + strncpy(hotkey, hk_loop ? unfolded_hotkey : hotkey_string, sizeof(hotkey)); + strncpy(command, cm_loop ? unfolded_command : command_string, sizeof(command)); + PRINTF(" %s\n %s\n", hotkey, command); + chain_t *chain = make_chain(); + if (parse_chain(hotkey, chain)) { + hotkey_t *hk = make_hotkey(chain, command); + add_hotkey(hk); + } else { + free(chain); + } - keysym = XCB_NO_SYMBOL; - button = XCB_NONE; - modfield = 0; - event_type = XCB_KEY_PRESS; - replay_event = false; + if (!hk_loop && !cm_loop) + break; -#define POSTGEN(elt, ra, rz, ptr) \ +#define POSTPROC(itm, ra, rz, ptr) \ if (ra >= rz) \ - elt = strtok_r(NULL, SEQ_SEP, &ptr), ra = 1, rz = 0; \ + ptr = gettok(itm, ptr, SEQ_SEP), ra = 1, rz = 0; \ else \ ra++; - POSTGEN(hk_item, hk_a, hk_z, hk_ptr) - POSTGEN(cmd_item, cmd_a, cmd_z, cmd_ptr) -#undef POSTGEN + POSTPROC(hk_item, hk_a, hk_z, hk_ptr) + POSTPROC(cm_item, cm_a, cm_z, cm_ptr) +#undef POSTPROC } } -void generate_hotkeys(xcb_keysym_t keysym, xcb_button_t button, uint16_t modfield, uint8_t event_type, bool replay_event, char *command) +char *gettok(char *dst, char *src, char c) { - if (button == XCB_NONE) { - xcb_keycode_t *keycodes = keycodes_from_keysym(keysym); - if (keycodes != NULL) - for (xcb_keycode_t *kc = keycodes; *kc != XCB_NO_SYMBOL; kc++) { - xcb_keysym_t natural_keysym = xcb_key_symbols_get_keysym(symbols, *kc, 0); - for (unsigned char col = 0; col < KEYSYMS_PER_KEYCODE; col++) { - xcb_keysym_t ks = xcb_key_symbols_get_keysym(symbols, *kc, col); - if (ks == keysym) { - uint16_t implicit_modfield = (col & 1 ? XCB_MOD_MASK_SHIFT : 0) | (col & 2 ? modfield_from_keysym(Mode_switch) : 0); - uint16_t explicit_modfield = modfield | implicit_modfield; - hotkey_t *hk = make_hotkey(natural_keysym, button, explicit_modfield, event_type, replay_event, command); - add_hotkey(hk); - break; - } - } - } - free(keycodes); - } else { - hotkey_t *hk = make_hotkey(keysym, button, modfield, event_type, replay_event, command); - add_hotkey(hk); + size_t len = strlen(src); + unsigned int i = 0, j = 0; + bool inhibit = false; + bool found = false; + while (i < len && !found) { + if (inhibit) { + dst[j++] = src[i]; + inhibit = false; + } else if (src[i] == MAGIC_INHIBIT) { + inhibit = true; + if (src[i+1] != MAGIC_INHIBIT && src[i+1] != c) + dst[j++] = src[i]; + } else if (src[i] == c) { + found = true; + } else { + dst[j++] = src[i]; + } + i++; } + dst[j] = '\0'; + return src + i; } -hotkey_t *make_hotkey(xcb_keysym_t keysym, xcb_button_t button, uint16_t modfield, uint8_t event_type, bool replay_event, char *command) +bool extract_sequence(char *s, char *prefix, char *sequence, char *suffix) +{ + char *cur = prefix; + size_t len = strlen(s); + unsigned int i = 0, j = 0; + bool inhibit = false; + while (i < len) { + if (inhibit) { + cur[j++] = s[i]; + inhibit = false; + } else if (s[i] == MAGIC_INHIBIT) { + inhibit = true; + if ((s[i+1] != MAGIC_INHIBIT || cur == sequence) + && s[i+1] != SEQ_BEGIN + && s[i+1] != SEQ_END) + cur[j++] = s[i]; + } else if (s[i] == SEQ_BEGIN) { + cur[j] = '\0'; + j = 0; + cur = sequence; + } else if (s[i] == SEQ_END) { + cur[j] = '\0'; + j = 0; + cur = suffix; + } else { + cur[j++] = s[i]; + } + i++; + /* printf("i %u j %u\n", i, j); */ + } + cur[j] = '\0'; + return (cur == suffix); +} + +hotkey_t *make_hotkey(chain_t *chain, char *command) { - PRINTF("hotkey %u %u %u %u %s\n", keysym, button, modfield, event_type, command); hotkey_t *hk = malloc(sizeof(hotkey_t)); - hk->keysym = keysym; - hk->button = button; - hk->modfield = modfield; - if (button != XCB_NONE) - hk->event_type = key_to_button(event_type); - else - hk->event_type = event_type; - hk->replay_event = replay_event; + hk->chain = chain; strncpy(hk->command, command, sizeof(hk->command)); hk->next = NULL; return hk; } -hotkey_t *find_hotkey(xcb_keysym_t keysym, xcb_button_t button, uint16_t modfield, uint8_t event_type) +void abort_chain(void) { + PUTS("abort chain"); for (hotkey_t *hk = hotkeys; hk != NULL; hk = hk->next) - if (hk->event_type == event_type && hk->keysym == keysym && hk->button == button && hk->modfield == modfield) - return hk; + hk->chain->state = hk->chain->head; + chained = false; + if (timeout > 0) + alarm(0); +} + +bool match_chord(chord_t *chord, uint8_t event_type, xcb_keysym_t keysym, xcb_button_t button, uint16_t modfield) { + for (chord_t *c = chord; c != NULL; c = c->more) + if (c->event_type == event_type && c->keysym == keysym && c->button == button && c->modfield == modfield) + return true; + return false; +} + +hotkey_t *find_hotkey(xcb_keysym_t keysym, xcb_button_t button, uint16_t modfield, uint8_t event_type, bool *replay_event) +{ + int hits = 0; + + for (hotkey_t *hk = hotkeys; hk != NULL; hk = hk->next) { + chain_t *c = hk->chain; + if (chained && c->state == c->head) + continue; + if (match_chord(c->state, event_type, keysym, button, modfield)) { + if (replay_event != NULL && c->state->replay_event) + *replay_event = true; + if (c->state == c->tail) { + if (chained) + abort_chain(); + return hk; + } else { + c->state = c->state->next; + } + hits++; + } else if (chained && c->state->event_type == event_type) { + c->state = c->head; + } + } + + if (hits > 0) { + chained = true; + if (timeout > 0) + alarm(timeout); + } else if (!chained) { + *replay_event = true; + for (hotkey_t *hk = hotkeys; *replay_event && hk != NULL; hk = hk->next) { + chord_t *c = hk->chain->head; + for (chord_t *cc = c; cc != NULL; cc = cc->more) + if (cc->keysym == keysym && cc->button == button && cc->modfield == modfield) + *replay_event = false; + } + } + return NULL; } diff --git a/hotkeys.h b/hotkeys.h @@ -11,21 +11,36 @@ #define MOTION_PREFIX '!' #define REPLAY_PREFIX ':' #define START_COMMENT '#' -#define TOK_SEP "+ \n" -#define SEQ_SEP "," +#define MAGIC_INHIBIT '\\' +#define PARTIAL_LINE '\\' +#define LNK_SEP ";" +#define SYM_SEP "+ " #define SEQ_BEGIN '{' #define SEQ_END '}' +#define SEQ_SEP ',' + +typedef struct { + char *name; + xcb_keysym_t keysym; +} keysym_dict_t; xcb_keysym_t Alt_L, Alt_R, Super_L, Super_R, Hyper_L, Hyper_R, Meta_L, Meta_R, Mode_switch, Num_Lock, Scroll_Lock; void grab(void); +void grab_chord(chord_t *); void grab_key_button(xcb_keycode_t, xcb_button_t, uint16_t); void grab_key_button_checked(xcb_keycode_t, xcb_button_t, uint16_t); void ungrab(void); int16_t modfield_from_keysym(xcb_keysym_t); xcb_keycode_t *keycodes_from_keysym(xcb_keysym_t); -bool parse_hotkey(char *, xcb_keysym_t *, xcb_button_t *, uint16_t *, uint8_t *, bool *); +chord_t *make_chord(xcb_keysym_t, xcb_button_t, uint16_t, uint8_t, bool); +chain_t *make_chain(void); +void add_chord(chain_t *, chord_t *); +void process_hotkey(char *, char *); +char *gettok(char *, char *, char); +bool extract_sequence(char *, char *, char *, char *); +bool parse_chain(char *, chain_t *); bool parse_keysym(char *, xcb_keysym_t *); bool parse_button(char *, xcb_button_t *); bool parse_modifier(char *, uint16_t *); @@ -33,10 +48,10 @@ bool parse_fold(char *, char *); uint8_t key_to_button(uint8_t); void get_standard_keysyms(void); void get_lock_fields(void); -void unfold_hotkeys(char *, char *); -void generate_hotkeys(xcb_keysym_t, xcb_button_t, uint16_t, uint8_t, bool, char *); -hotkey_t *make_hotkey(xcb_keysym_t, xcb_button_t, uint16_t, uint8_t, bool, char *); -hotkey_t *find_hotkey(xcb_keysym_t, xcb_button_t, uint16_t, uint8_t); +bool match_chord(chord_t *, uint8_t, xcb_keysym_t, xcb_button_t, uint16_t); +hotkey_t *find_hotkey(xcb_keysym_t, xcb_button_t, uint16_t, uint8_t, bool *); +hotkey_t *make_hotkey(chain_t *, char *); +void abort_chain(void); void add_hotkey(hotkey_t *); #endif diff --git a/sxhkd.1 b/sxhkd.1 @@ -16,6 +16,9 @@ Print the synopsis to standard output and exit. .BI -v Print the version information to standard output and exit. .TP +.BI -t " TIMEOUT" +Timeout in seconds for the recording of chord chains. +.TP .BI -c " CONFIG_FILE" Read the configuration from the given file. .TP @@ -25,26 +28,34 @@ Redirect the commands output to the given file. .PP Each line of the configuration file is interpreted as so: .IP \(bu 2 -If it starts with +If it is empty or starts with .IR # , it is ignored. .IP \(bu 2 -If it starts with one or more white space characters, it is read as a command. +If it starts with a space, it is read as a command. .IP \(bu 2 -Otherwise, it is parsed as a hotkey: each key name is separated by spaces and/or -.IR + -characters. +Otherwise, it is read as a hotkey. .PP General Syntax: .EX - [MODIFIER + ]*[@|!|:]KEYSYM + + HOTKEY COMMAND + + HOTKEY := CHORD_1 ; CHORD_2 ; ... ; CHORD_n + CHORD_i := [MODIFIER_i][@|!|:]KEYSYM_i + MODIFIER_i := MODIFIER_i1 + MODIFIER_i2 + ... + MODIFIER_ik .EE .PP -Where -.I MODIFIER -is one of the following names: -.BR super , " hyper", " meta", " alt", " control", " ctrl", " shift", " mode_switch", " lock", " mod1", " mod2", " mod3", " mod4", " mod5" . +The valid modifier names are: +.BR super , " hyper", " meta", " alt", " control", " ctrl", " shift", " mode_switch", " lock", " mod1", " mod2", " mod3", " mod4" ", and" " mod5" . +.PP +The keysym names are given by the output of +.BR xev (1). +.PP +Hotkeys and commands can be spread across multiple lines by ending each partial line with a backslash character. +.PP +When multiple chords are separated by semicolons, the hotkey is a chord chain: the command will only by executed after receiving each chord of the chain in consecutive order. .PP If .I @ @@ -55,28 +66,20 @@ If is added at the beginning of the keysym, the command will be run on motion notify events and must contain two integer conversion specifications which will be replaced by the .BR x " and " y coordinates of the pointer relative to the root window referential (the only valid button keysyms for this type of hotkeys are: -.BR button1 ",…, " button5 ). +.BR button1 ",..., " button5 ). .PP If .I : is added at the beginning of the keysym, the captured event will be replayed for the other clients. .PP -The keysym names are those your will get from -.BR xev (1). -.PP Mouse hotkeys can be defined by using one of the following special keysym names: .BR button1 ", " button2 ", " button3 ", ..., " button24 . .PP -The hotkey can contain a sequence of the form -.IR {STRING_1,…,STRING_N} , -in which case, the command must also contain a sequence with -.I N -elements: the pairing of the two sequences generates -.I N -hotkeys. +The hotkey and the command may contain a sequence of the form +.IR {STRING_1,...,STRING_N} . .PP In addition, the sequences can contain ranges of the form -.I A-Z +.IB A - Z where .IR A " and " Z are alphanumeric characters. diff --git a/sxhkd.c b/sxhkd.c @@ -22,6 +22,8 @@ void hold(int sig) running = false; else if (sig == SIGUSR1) reload = true; + else if (sig == SIGALRM) + bell = true; } void setup(void) @@ -41,14 +43,32 @@ void setup(void) void cleanup(void) { + PUTS("cleanup"); hotkey_t *hk = hotkeys; while (hk != NULL) { hotkey_t *tmp = hk->next; + destroy_chain(hk->chain); free(hk); hk = tmp; } } +void destroy_chain(chain_t *chain) +{ + chord_t *c = chain->head; + while (c != NULL) { + chord_t *n = c->next; + chord_t *cc = c->more; + while (cc != NULL) { + chord_t *nn = cc->more; + free(cc); + cc = nn; + } + free(c); + c = n; + } +} + void reload_cmd(void) { PUTS("reload"); @@ -64,94 +84,89 @@ void reload_cmd(void) void load_config(char *config_file) { PRINTF("load configuration '%s'\n", config_file); - - FILE *cfg = fopen(config_file, "r"); if (cfg == NULL) err("Can't open configuration file: '%s'.\n", config_file); - char line[MAXLEN]; - xcb_keysym_t keysym = XCB_NO_SYMBOL; - xcb_button_t button = XCB_NONE; - uint16_t modfield = 0; - uint8_t event_type = XCB_KEY_PRESS; - bool replay_event = false; - char folded_hotkey[MAXLEN] = {'\0'}; + char buf[MAXLEN]; + char chain[MAXLEN] = {0}; + char command[MAXLEN] = {0}; + int offset = 0; + char first; - while (fgets(line, sizeof(line), cfg) != NULL) { - if (strlen(line) < 2 || line[0] == START_COMMENT) { + while (fgets(buf, sizeof(buf), cfg) != NULL) { + first = buf[0]; + if (strlen(buf) < 2 || first == START_COMMENT) { continue; - } else if (isspace(line[0])) { - if (keysym == XCB_NO_SYMBOL && button == XCB_NONE && strlen(folded_hotkey) == 0) + } else { + char *start = lgraph(buf); + if (start == NULL) continue; - unsigned int i = strlen(line) - 1; - while (i > 0 && isspace(line[i])) - line[i--] = '\0'; - i = 1; - while (i < strlen(line) && isspace(line[i])) - i++; - if (i < strlen(line)) { - char *command = line + i; - if (strlen(folded_hotkey) == 0) - generate_hotkeys(keysym, button, modfield, event_type, replay_event, command); - else - unfold_hotkeys(folded_hotkey, command); + char *end = rgraph(buf); + *(end + 1) = '\0'; + + if (isgraph(first)) + strncpy(chain + offset, start, sizeof(chain) - offset); + else + strncpy(command + offset, start, sizeof(command) - offset); + + if (*end == PARTIAL_LINE) { + offset += end - start; + continue; + } else { + offset = 0; + } + + if (isspace(first) && strlen(chain) > 0 && strlen(command) > 0) { + process_hotkey(chain, command); + chain[0] = '\0'; + command[0] = '\0'; } - keysym = XCB_NO_SYMBOL; - button = XCB_NONE; - modfield = 0; - event_type = XCB_KEY_PRESS; - replay_event = false; - folded_hotkey[0] = '\0'; - } else { - unsigned int i = strlen(line) - 1; - while (i > 0 && isspace(line[i])) - line[i--] = '\0'; - if (!parse_fold(line, folded_hotkey)) - parse_hotkey(line, &keysym, &button, &modfield, &event_type, &replay_event); } } fclose(cfg); } -void key_button_event(xcb_generic_event_t *evt, uint8_t event_type) -{ - xcb_keysym_t keysym = XCB_NO_SYMBOL; - xcb_keycode_t keycode = XCB_NONE; - xcb_button_t button = XCB_NONE; - bool replay_event = false; - uint16_t modfield = 0; - uint16_t lockfield = num_lock | caps_lock | scroll_lock; +void parse_event(xcb_generic_event_t *evt, uint8_t event_type, xcb_keysym_t *keysym, xcb_button_t *button, uint16_t *modfield) { if (event_type == XCB_KEY_PRESS) { xcb_key_press_event_t *e = (xcb_key_press_event_t *) evt; - keycode = e->detail; - modfield = e->state; - keysym = xcb_key_symbols_get_keysym(symbols, keycode, 0); - PRINTF("key press %u %u\n", keycode, modfield); + xcb_keycode_t keycode = e->detail; + *modfield = e->state; + *keysym = xcb_key_symbols_get_keysym(symbols, keycode, 0); + PRINTF("key press %u %u\n", keycode, *modfield); } else if (event_type == XCB_KEY_RELEASE) { xcb_key_release_event_t *e = (xcb_key_release_event_t *) evt; - keycode = e->detail; - modfield = e->state; - keysym = xcb_key_symbols_get_keysym(symbols, keycode, 0); - PRINTF("key release %u %u\n", keycode, modfield); + xcb_keycode_t keycode = e->detail; + *modfield = e->state; + *keysym = xcb_key_symbols_get_keysym(symbols, keycode, 0); + PRINTF("key release %u %u\n", keycode, *modfield); } else if (event_type == XCB_BUTTON_PRESS) { xcb_button_press_event_t *e = (xcb_button_press_event_t *) evt; - button = e->detail; - modfield = e->state; - PRINTF("button press %u %u\n", button, modfield); + *button = e->detail; + *modfield = e->state; + PRINTF("button press %u %u\n", *button, *modfield); } else if (event_type == XCB_BUTTON_RELEASE) { xcb_button_release_event_t *e = (xcb_button_release_event_t *) evt; - button = e->detail; - modfield = e->state; - PRINTF("button release %u %u\n", button, modfield); + *button = e->detail; + *modfield = e->state; + PRINTF("button release %u %u\n", *button, *modfield); } +} + +void key_button_event(xcb_generic_event_t *evt, uint8_t event_type) +{ + xcb_keysym_t keysym = XCB_NO_SYMBOL; + xcb_button_t button = XCB_NONE; + bool replay_event = false; + uint16_t modfield = 0; + uint16_t lockfield = num_lock | caps_lock | scroll_lock; + parse_event(evt, event_type, &keysym, &button, &modfield); modfield &= ~lockfield & MOD_STATE_FIELD; if (keysym != XCB_NO_SYMBOL || button != XCB_NONE) { - hotkey_t *hk = find_hotkey(keysym, button, modfield, event_type); + hotkey_t *hk = find_hotkey(keysym, button, modfield, event_type, &replay_event); if (hk != NULL) { run(hk->command); - replay_event = hk->replay_event; } } switch (event_type) { @@ -185,7 +200,7 @@ void motion_notify(xcb_generic_event_t *evt, uint8_t event_type) buttonfield = buttonfield >> 1; button++; } - hotkey_t *hk = find_hotkey(XCB_NO_SYMBOL, button, modfield, event_type); + hotkey_t *hk = find_hotkey(XCB_NO_SYMBOL, button, modfield, event_type, NULL); if (hk != NULL) { char command[MAXLEN]; snprintf(command, sizeof(command), hk->command, e->root_x, e->root_y); @@ -197,17 +212,21 @@ int main(int argc, char *argv[]) { char opt; config_path = NULL; + timeout = TIMEOUT; - while ((opt = getopt(argc, argv, "vhc:r:")) != -1) { + while ((opt = getopt(argc, argv, "vht:c:r:")) != -1) { switch (opt) { case 'v': printf("%s\n", VERSION); exit(EXIT_SUCCESS); break; case 'h': - printf("sxhkd [-h|-v|-c CONFIG_FILE|-r REDIR_FILE] [EXTRA_CONFIG ...]\n"); + printf("sxhkd [-h|-v|-t TIMEOUT|-c CONFIG_FILE|-r REDIR_FILE] [EXTRA_CONFIG ...]\n"); exit(EXIT_SUCCESS); break; + case 't': + timeout = atoi(optarg); + break; case 'r': redir_fd = open(optarg, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (redir_fd == -1) @@ -236,6 +255,7 @@ int main(int argc, char *argv[]) signal(SIGHUP, hold); signal(SIGTERM, hold); signal(SIGUSR1, hold); + signal(SIGALRM, hold); setup(); get_standard_keysyms(); @@ -250,7 +270,7 @@ int main(int argc, char *argv[]) fd_set descriptors; - reload = false; + reload = bell = chained = false; running = true; xcb_flush(dpy); @@ -286,6 +306,12 @@ int main(int argc, char *argv[]) reload = false; } + if (bell) { + signal(SIGALRM, hold); + abort_chain(); + bell = false; + } + if (xcb_connection_has_error(dpy)) { warn("The server closed the connection.\n"); running = false; diff --git a/sxhkd.h b/sxhkd.h @@ -10,24 +10,32 @@ #define SXHKD_SHELL_ENV "SXHKD_SHELL" #define SHELL_ENV "SHELL" #define CONFIG_PATH "sxhkd/sxhkdrc" -#define TOK_SEP "+ \n" #define NUM_MOD 8 +#define TIMEOUT 3 -typedef struct hotkey_t hotkey_t; -struct hotkey_t { +typedef struct chord_t chord_t; +struct chord_t { xcb_keysym_t keysym; xcb_button_t button; uint16_t modfield; uint8_t event_type; bool replay_event; - char command[MAXLEN]; - hotkey_t *next; + chord_t *next; + chord_t *more; }; typedef struct { - char *name; - xcb_keysym_t keysym; -} keysym_dict_t; + chord_t *head; + chord_t *tail; + chord_t *state; +} chain_t; + +typedef struct hotkey_t hotkey_t; +struct hotkey_t { + chain_t *chain; + char command[MAXLEN]; + hotkey_t *next; +}; xcb_connection_t *dpy; xcb_window_t root; @@ -40,8 +48,9 @@ char *config_path; char **extra_confs; int num_extra_confs; int redir_fd; +int timeout; -bool running, reload; +bool running, reload, bell, chained; uint16_t num_lock; uint16_t caps_lock; @@ -49,11 +58,12 @@ uint16_t scroll_lock; void hold(int); void setup(void); +void load_config(char *); void cleanup(void); +void destroy_chain(chain_t *); void reload_cmd(void); -void load_config(char *config_file); -void mapping_notify(xcb_generic_event_t *); -void key_event(xcb_generic_event_t *, uint8_t); +void parse_event(xcb_generic_event_t *, uint8_t, xcb_keysym_t *, xcb_button_t *, uint16_t *); +void key_button_event(xcb_generic_event_t *, uint8_t); void motion_notify(xcb_generic_event_t *, uint8_t); #endif