git.strcat.st

/strcat/hkd.git/ - summarytreelogarchive

subject
init
commit
b4e0d69d58a5f7b7f7c35940a44169fb22bb8ec5
date
2026-04-20T22:58:56Z
message
diff
 Makefile      |  12 +++
 README.md     |   1 +
 hkd.c         | 277 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 hkdrc.example |  20 +++++
 4 files changed, 310 insertions(+)

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e543ad9
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,12 @@
+CFLAGS = -std=c89 -Wall -Wextra -Werror -pedantic
+CPPFLAGS = -I/usr/X11R6/include
+LDFLAGS = -L/usr/X11R6/lib
+LDLIBS = -lX11
+
+all: hkd
+
+hkd: hkd.c
+	cc $(CPPFLAGS) $(CFLAGS) -o hkd hkd.c $(LDFLAGS) $(LDLIBS)
+
+clean:
+	rm -f hkd
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9f099b6
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+hot key daemon
diff --git a/hkd.c b/hkd.c
new file mode 100644
index 0000000..dcb3540
--- /dev/null
+++ b/hkd.c
@@ -0,0 +1,277 @@
+#include <X11/Xlib.h>
+#include <X11/keysym.h>
+
+#include <ctype.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+enum {
+	MAX_LINE = 2048
+};
+
+typedef struct {
+	unsigned int mod;
+	KeyCode code;
+	char *cmd;
+} binding;
+
+static unsigned int numlock_mask;
+
+static char *
+trim(char *s)
+{
+	char *e;
+
+	while (*s != '\0' && isspace((unsigned char)*s))
+	    s++;
+	e = s + strlen(s);
+	while (e > s && isspace((unsigned char)e[-1]))
+	    *--e = '\0';
+	return s;
+}
+
+static int
+x_strdup(char **out, const char *s)
+{
+	size_t n;
+	char *p;
+
+	n = strlen(s) + 1;
+	p = malloc(n);
+	if (p == NULL)
+	    return 0;
+	memcpy(p, s, n);
+	*out = p;
+	return 1;
+}
+
+static int
+add_binding(binding **b, int *n, unsigned int mod, KeyCode code, const char *cmd)
+{
+	binding *nb;
+
+	nb = realloc(*b, (size_t)(*n + 1) * sizeof(*nb));
+	if (nb == NULL)
+	    return 0;
+	*b = nb;
+	nb[*n].mod = mod;
+	nb[*n].code = code;
+	if (!x_strdup(&nb[*n].cmd, cmd))
+	    return 0;
+	(*n)++;
+	return 1;
+}
+
+static int
+mod_from_token(const char *tok, unsigned int *mod)
+{
+	if (!strcmp(tok, "Shift"))
+	    *mod |= ShiftMask;
+	else if (!strcmp(tok, "Control") || !strcmp(tok, "Ctrl"))
+	    *mod |= ControlMask;
+	else if (!strcmp(tok, "Alt") || !strcmp(tok, "Mod1"))
+	    *mod |= Mod1Mask;
+	else if (!strcmp(tok, "Mod2"))
+	    *mod |= Mod2Mask;
+	else if (!strcmp(tok, "Mod3"))
+	    *mod |= Mod3Mask;
+	else if (!strcmp(tok, "Super") || !strcmp(tok, "Mod4"))
+	    *mod |= Mod4Mask;
+	else if (!strcmp(tok, "Mod5"))
+	    *mod |= Mod5Mask;
+	else
+	    return 0;
+	return 1;
+}
+
+static int
+parse_combo(Display *dpy, char *combo, unsigned int *mod, KeyCode *code)
+{
+	char *p, *tok, *key;
+	KeySym ks;
+
+	*mod = 0;
+	key = NULL;
+	for (tok = strtok(combo, "+"); tok != NULL; tok = strtok(NULL, "+")) {
+	    tok = trim(tok);
+	    if (*tok == '\0')
+	        return 0;
+	    if (mod_from_token(tok, mod))
+	        continue;
+	    key = tok;
+	}
+	if (key == NULL)
+	    return 0;
+	for (p = key; *p != '\0'; p++)
+	    if ((unsigned char)*p == ' ' || (unsigned char)*p == '\t')
+	        return 0;
+	ks = XStringToKeysym(key);
+	if (ks == NoSymbol)
+	    return 0;
+	*code = XKeysymToKeycode(dpy, ks);
+	return *code != 0;
+}
+
+static void
+find_numlock_mask(Display *dpy)
+{
+	XModifierKeymap *map;
+	KeyCode nl;
+	int i, j;
+
+	nl = XKeysymToKeycode(dpy, XK_Num_Lock);
+	numlock_mask = 0;
+	map = XGetModifierMapping(dpy);
+	if (map == NULL)
+	    return;
+	for (i = 0; i < 8; i++) {
+	    for (j = 0; j < map->max_keypermod; j++) {
+	        if (map->modifiermap[i * map->max_keypermod + j] == nl)
+	            numlock_mask = (unsigned int)(1U << i);
+	    }
+	}
+	XFreeModifiermap(map);
+}
+
+static void
+grab(Display *dpy, Window root, binding *b)
+{
+	unsigned int masks[4];
+	int i;
+
+	masks[0] = b->mod;
+	masks[1] = b->mod | LockMask;
+	masks[2] = b->mod | numlock_mask;
+	masks[3] = b->mod | numlock_mask | LockMask;
+	for (i = 0; i < 4; i++)
+	    XGrabKey(dpy, b->code, masks[i], root, True, GrabModeAsync,
+	        GrabModeAsync);
+}
+
+static unsigned int
+clean_mask(unsigned int m)
+{
+	return m & ~(LockMask | numlock_mask);
+}
+
+static void
+run(const char *cmd)
+{
+	if (fork() == 0) {
+	    execl("/bin/sh", "sh", "-c", cmd, (char *)NULL);
+	    _exit(127);
+	}
+}
+
+static int
+load_bindings(Display *dpy, const char *path, binding **out, int *out_n)
+{
+	FILE *f;
+	binding *b;
+	int n;
+	char line[MAX_LINE];
+	int line_no;
+
+	f = fopen(path, "r");
+	if (f == NULL)
+	    return 0;
+	b = NULL;
+	n = 0;
+	line_no = 0;
+	while (fgets(line, sizeof(line), f) != NULL) {
+	    char *p, *combo, *cmd, *sep;
+	    unsigned int mod;
+	    KeyCode code;
+
+	    line_no++;
+	    p = trim(line);
+	    if (*p == '\0' || *p == '#')
+	        continue;
+	    sep = p;
+	    while (*sep != '\0' && !isspace((unsigned char)*sep))
+	        sep++;
+	    if (*sep == '\0') {
+	        fprintf(stderr, "hkd: %s:%d missing command\n", path, line_no);
+	        continue;
+	    }
+	    *sep++ = '\0';
+	    combo = trim(p);
+	    cmd = trim(sep);
+	    if (*cmd == '\0') {
+	        fprintf(stderr, "hkd: %s:%d missing command\n", path, line_no);
+	        continue;
+	    }
+	    if (!parse_combo(dpy, combo, &mod, &code)) {
+	        fprintf(stderr, "hkd: %s:%d bad combo '%s'\n", path, line_no,
+	            combo);
+	        continue;
+	    }
+	    if (!add_binding(&b, &n, mod, code, cmd)) {
+	        fclose(f);
+	        return 0;
+	    }
+	}
+	fclose(f);
+	*out = b;
+	*out_n = n;
+	return 1;
+}
+
+int
+main(int argc, char **argv)
+{
+	Display *dpy;
+	Window root;
+	binding *b;
+	XEvent ev;
+	int i, n;
+	char path[MAX_LINE];
+	const char *cfg;
+
+	signal(SIGCHLD, SIG_IGN);
+	dpy = XOpenDisplay(NULL);
+	if (dpy == NULL) {
+	    fprintf(stderr, "hkd: cannot open display\n");
+	    return 1;
+	}
+	root = DefaultRootWindow(dpy);
+	if (argc > 1) {
+	    cfg = argv[1];
+	} else {
+	    const char *home;
+
+	    home = getenv("HOME");
+	    if (home == NULL)
+	        home = ".";
+	    snprintf(path, sizeof(path), "%s/.hkdrc", home);
+	    cfg = path;
+	}
+	if (!load_bindings(dpy, cfg, &b, &n)) {
+	    fprintf(stderr, "hkd: cannot load %s\n", cfg);
+	    XCloseDisplay(dpy);
+	    return 1;
+	}
+	if (n == 0) {
+	    fprintf(stderr, "hkd: no bindings in %s\n", cfg);
+	    XCloseDisplay(dpy);
+	    return 1;
+	}
+	find_numlock_mask(dpy);
+	for (i = 0; i < n; i++)
+	    grab(dpy, root, &b[i]);
+	XSync(dpy, False);
+
+	for (;;) {
+	    XNextEvent(dpy, &ev);
+	    if (ev.type != KeyPress)
+	        continue;
+	    for (i = 0; i < n; i++) {
+	        if (ev.xkey.keycode == b[i].code &&
+	            clean_mask(ev.xkey.state) == b[i].mod)
+	            run(b[i].cmd);
+	    }
+	}
+}
diff --git a/hkdrc.example b/hkdrc.example
new file mode 100644
index 0000000..5df5873
--- /dev/null
+++ b/hkdrc.example
@@ -0,0 +1,20 @@
+Alt+Return st 
+Super+d dmenu_run
+Alt+1 wmc 1
+Alt+2 wmc 2
+Alt+3 wmc 3
+Alt+4 wmc 4
+Alt+5 wmc 5
+Alt+6 wmc 6
+Alt+7 wmc 7
+Alt+8 wmc 8
+Alt+9 wmc 9
+Alt+Shift+1 wmc move 1
+Alt+Shift+2 wmc move 2
+Alt+Shift+3 wmc move 3
+Alt+Shift+4 wmc move 4
+Alt+Shift+5 wmc move 5
+Alt+Shift+6 wmc move 6
+Alt+Shift+7 wmc move 7
+Alt+Shift+8 wmc move 8
+Alt+Shift+9 wmc move 9