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:
M | README.md | | | 46 | +++++++++++++++++++++++++++++----------------- |
D | TODO.md | | | 4 | ---- |
M | helpers.c | | | 25 | +++++++++++++++++++++++++ |
M | helpers.h | | | 2 | ++ |
M | hotkeys.c | | | 391 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------ |
M | hotkeys.h | | | 29 | ++++++++++++++++++++++------- |
M | sxhkd.1 | | | 47 | +++++++++++++++++++++++++---------------------- |
M | sxhkd.c | | | 156 | ++++++++++++++++++++++++++++++++++++++++++++++--------------------------------- |
M | sxhkd.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