Skip to content

Commit 0278c6c

Browse files
committed
Add hotkeys support
1 parent d91b800 commit 0278c6c

File tree

2 files changed

+190
-8
lines changed

2 files changed

+190
-8
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ screen sharing, ...
1313
- Highlight using a filled or outlined dot
1414
- Auto-hide highlight and/or cursor after a time when not moving and
1515
re-show when moving again
16+
- Global hotkeys for toggling cursor or highlighter and for toggling
17+
auto-hiding
1618

1719
## Installation
1820

highlight-pointer.c

Lines changed: 188 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,40 @@
4747
static Display* dpy;
4848
static GC gc = 0;
4949
static Window win;
50+
static Window root;
5051
static int screen;
5152
static int selfpipe[2]; /* for self-pipe trick to cancel select() call */
5253

54+
#define KEY_MODMAP_SIZE 4
55+
static struct {
56+
char symbol;
57+
unsigned int modifiers;
58+
} key_modifier_mapping[KEY_MODMAP_SIZE] = {
59+
{'S', ShiftMask}, /* shift */
60+
{'C', ControlMask}, /* control */
61+
{'M', Mod1Mask}, /* alt/meta */
62+
{'H', Mod4Mask} /* super/"windows" */
63+
};
64+
65+
#define KEY_OPTION_OFFSET 1000
66+
#define KEY_ARRAY_SIZE 5
67+
struct {
68+
KeySym keysym;
69+
unsigned int modifiers;
70+
} keys[KEY_ARRAY_SIZE] = {
71+
#define KEY_QUIT 0
72+
{NoSymbol, 0},
73+
#define KEY_TOGGLE_CURSOR 1
74+
{NoSymbol, 0},
75+
#define KEY_TOGGLE_HIGHLIGHT 2
76+
{NoSymbol, 0},
77+
#define KEY_TOGGLE_AUTOHIDE_CURSOR 3
78+
{NoSymbol, 0},
79+
#define KEY_TOGGLE_AUTOHIDE_HIGHLIGHT 4
80+
{NoSymbol, 0}};
81+
82+
static unsigned int numlockmask = 0;
83+
5384
static XColor pressed_color;
5485
static XColor released_color;
5586
static int button_pressed = 0;
@@ -108,7 +139,7 @@ int init_events() {
108139
events.mask = mask;
109140
events.mask_len = sizeof(mask);
110141

111-
XISelectEvents(dpy, RootWindow(dpy, screen), &events, 1);
142+
XISelectEvents(dpy, root, &events, 1);
112143

113144
return 0;
114145
}
@@ -117,7 +148,7 @@ int get_pointer_position(int* x, int* y) {
117148
Window w;
118149
int i;
119150
unsigned int ui;
120-
return XQueryPointer(dpy, XRootWindow(dpy, screen), &w, &w, x, y, &i, &i, &ui);
151+
return XQueryPointer(dpy, root, &w, &w, x, y, &i, &i, &ui);
121152
}
122153

123154
void set_window_mask() {
@@ -146,8 +177,8 @@ int init_window() {
146177
win_attributes.event_mask = ExposureMask | VisibilityChangeMask;
147178
win_attributes.override_redirect = True;
148179

149-
win = XCreateWindow(dpy, XRootWindow(dpy, screen), 0, 0, 2 * options.radius + 2, 2 * options.radius + 2, 0, DefaultDepth(dpy, screen), InputOutput,
150-
DefaultVisual(dpy, screen), CWEventMask | CWOverrideRedirect, &win_attributes);
180+
win = XCreateWindow(dpy, root, 0, 0, 2 * options.radius + 2, 2 * options.radius + 2, 0, DefaultDepth(dpy, screen), InputOutput, DefaultVisual(dpy, screen),
181+
CWEventMask | CWOverrideRedirect, &win_attributes);
151182
if (!win) {
152183
fprintf(stderr, "Can't create highlight window\n");
153184
return 1;
@@ -189,7 +220,7 @@ int init_window() {
189220
xclient.data.l[2] = 0;
190221
xclient.data.l[3] = 1;
191222
xclient.data.l[4] = 0;
192-
XSendEvent(dpy, XRootWindow(dpy, screen), False, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent*)&xclient);
223+
XSendEvent(dpy, root, False, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent*)&xclient);
193224

194225
/* let clicks fall through */
195226
/* after https://stackoverflow.com/a/9279747 */
@@ -220,6 +251,45 @@ void redraw() {
220251

221252
void quit() { write(selfpipe[1], "\0", 1); }
222253

254+
void handle_key(KeySym keysym, unsigned int modifiers) {
255+
modifiers = modifiers & ~(numlockmask | LockMask);
256+
int k;
257+
for (k = 0; k < KEY_ARRAY_SIZE; ++k) {
258+
if (keys[k].keysym == keysym && keys[k].modifiers == modifiers) {
259+
break;
260+
}
261+
}
262+
switch (k) {
263+
case KEY_QUIT:
264+
quit();
265+
break;
266+
267+
case KEY_TOGGLE_CURSOR:
268+
if (cursor_visible) {
269+
hide_cursor();
270+
} else {
271+
show_cursor();
272+
}
273+
break;
274+
275+
case KEY_TOGGLE_HIGHLIGHT:
276+
if (highlight_visible) {
277+
hide_highlight();
278+
} else {
279+
show_highlight();
280+
}
281+
break;
282+
283+
case KEY_TOGGLE_AUTOHIDE_CURSOR:
284+
options.auto_hide_cursor = 1 - options.auto_hide_cursor;
285+
break;
286+
287+
case KEY_TOGGLE_AUTOHIDE_HIGHLIGHT:
288+
options.auto_hide_highlight = 1 - options.auto_hide_highlight;
289+
break;
290+
}
291+
}
292+
223293
void main_loop() {
224294
XEvent ev;
225295
fd_set fds;
@@ -294,6 +364,13 @@ void main_loop() {
294364
continue;
295365
}
296366

367+
if (ev.type == KeyPress) {
368+
KeySym keysym = XLookupKeysym(&ev.xkey, 0);
369+
if (keysym != NoSymbol) {
370+
handle_key(keysym, ev.xkey.state);
371+
}
372+
continue;
373+
}
297374
if (ev.type == Expose) {
298375
redraw();
299376
continue;
@@ -335,12 +412,67 @@ int init_colors() {
335412
return 0;
336413
}
337414

415+
int grab_keys() {
416+
/* after https://git.suckless.org/dwm/file/dwm.c.html */
417+
numlockmask = 0;
418+
unsigned int numlockkeycode = XKeysymToKeycode(dpy, XK_Num_Lock);
419+
if (numlockkeycode) {
420+
XModifierKeymap* modmap = XGetModifierMapping(dpy);
421+
for (int i = 0; i < 8; ++i) {
422+
for (int j = 0; j < modmap->max_keypermod; ++j) {
423+
if (modmap->modifiermap[i * modmap->max_keypermod + j] == numlockkeycode) {
424+
numlockmask = (1 << i);
425+
}
426+
}
427+
}
428+
XFreeModifiermap(modmap);
429+
}
430+
431+
unsigned int modifiers[] = {0, LockMask, numlockmask, numlockmask | LockMask};
432+
for (int i = 0; i < KEY_ARRAY_SIZE; ++i) {
433+
if (keys[i].keysym != NoSymbol) {
434+
KeyCode c = XKeysymToKeycode(dpy, keys[i].keysym);
435+
if (!c) {
436+
fprintf(stderr, "Could not convert key to keycode\n");
437+
return 1;
438+
}
439+
for (int j = 0; j < 2; ++j) {
440+
XGrabKey(dpy, c, keys[i].modifiers | modifiers[j], root, 1, GrabModeAsync, GrabModeAsync);
441+
}
442+
}
443+
}
444+
return 0;
445+
}
446+
338447
void sig_handler(int sig) {
339448
(void)sig;
340-
fprintf(stderr, "Quitting...\n");
341449
quit();
342450
}
343451

452+
int parse_key(const char* s, int k) {
453+
keys[k].modifiers = 0;
454+
455+
int i;
456+
while (s[0] != '\0' && s[1] == '-') {
457+
for (i = 0; i < KEY_MODMAP_SIZE; ++i) {
458+
if (key_modifier_mapping[i].symbol == s[0]) {
459+
keys[k].modifiers |= key_modifier_mapping[i].modifiers;
460+
break;
461+
}
462+
}
463+
if (i == KEY_MODMAP_SIZE) {
464+
return 1;
465+
}
466+
s += 2;
467+
}
468+
469+
keys[k].keysym = XStringToKeysym(s);
470+
if (keys[k].keysym == NoSymbol) {
471+
return 1;
472+
}
473+
return 0;
474+
}
475+
344476
void print_usage(const char* name) {
345477
printf(
346478
"Usage:\n"
@@ -349,7 +481,7 @@ void print_usage(const char* name) {
349481
" -h, --help show this help message\n"
350482
"\n"
351483
"DISPLAY OPTIONS\n"
352-
" -c, --released-color COLOR dot color when mouse button releaed [default: #d62728]\n"
484+
" -c, --released-color COLOR dot color when mouse button released [default: #d62728]\n"
353485
" -p, --pressed-color COLOR dot color when mouse button pressed [default: #1f77b4]\n"
354486
" -o, --outline OUTLINE line width of outline or 0 for filled dot [default: 0]\n"
355487
" -r, --radius RADIUS dot radius in pixels [default: 5]\n"
@@ -360,7 +492,23 @@ void print_usage(const char* name) {
360492
" --auto-hide-cursor hide cursor when not moving after timeout\n"
361493
" --auto-hide-highlight hide highlighter when not moving after timeout\n"
362494
" -t, --hide-timeout TIMEOUT timeout for hiding when idle, in seconds [default: 3]\n"
363-
"",
495+
"\n"
496+
"HOTKEY OPTIONS\n"
497+
" --key-quit KEY quit\n"
498+
" --key-toggle-cursor KEY toggle cursor visibility\n"
499+
" --key-toggle-highlight KEY toggle highlight visibility\n"
500+
" --key-toggle-auto-hide-cursor KEY toggle auto-hiding cursor when not moving\n"
501+
" --key-toggle-auto-hide-highlight KEY toggle auto-hiding highlight when not moving\n"
502+
"\n"
503+
" Hotkeys are global and can only be used if not set yet by a different process.\n"
504+
" Keys can be given with modifiers\n"
505+
" 'S' (shift key), 'C' (ctrl key), 'M' (alt/meta key), 'H' (super/\"windows\" key)\n"
506+
" delimited by a '-'.\n"
507+
" Keys themselves are parsed by X, so chars like a...z can be set directly,\n"
508+
" special keys are named as in /usr/include/X11/keysymdef.h\n"
509+
" or see, e.g. http://xahlee.info/linux/linux_show_keycode_keysym.html\n"
510+
"\n"
511+
" Examples: 'H-Left', 'C-S-a'\n",
364512
name);
365513
}
366514

@@ -374,6 +522,11 @@ static struct option long_options[] = {{"auto-hide-cursor", no_argument, &option
374522
{"radius", required_argument, NULL, 'r'},
375523
{"released-color", required_argument, NULL, 'c'},
376524
{"show-cursor", no_argument, &options.cursor_visible, 1},
525+
{"key-quit", required_argument, NULL, KEY_QUIT + KEY_OPTION_OFFSET},
526+
{"key-toggle-cursor", required_argument, NULL, KEY_TOGGLE_CURSOR + KEY_OPTION_OFFSET},
527+
{"key-toggle-highlight", required_argument, NULL, KEY_TOGGLE_HIGHLIGHT + KEY_OPTION_OFFSET},
528+
{"key-toggle-auto-hide-cursor", required_argument, NULL, KEY_TOGGLE_AUTOHIDE_CURSOR + KEY_OPTION_OFFSET},
529+
{"key-toggle-auto-hide-highlight", required_argument, NULL, KEY_TOGGLE_AUTOHIDE_HIGHLIGHT + KEY_OPTION_OFFSET},
377530
{NULL, 0, NULL, 0}};
378531

379532
int set_options(int argc, char* argv[]) {
@@ -389,6 +542,14 @@ int set_options(int argc, char* argv[]) {
389542
if (c < 0) {
390543
break;
391544
}
545+
if (c >= KEY_OPTION_OFFSET && c < KEY_OPTION_OFFSET + KEY_ARRAY_SIZE) {
546+
int res = parse_key(optarg, c - KEY_OPTION_OFFSET);
547+
if (res) {
548+
fprintf(stderr, "Could not parse key value %s\n", optarg);
549+
return 1;
550+
}
551+
continue;
552+
}
392553
switch (c) {
393554
case 0:
394555
break;
@@ -437,6 +598,17 @@ int set_options(int argc, char* argv[]) {
437598
return 0;
438599
}
439600

601+
int xerror_handler(Display* dpy_p, XErrorEvent* err) {
602+
if (err->request_code == 33 /* XGrabKey */ && err->error_code == BadAccess) {
603+
fprintf(stderr, "Key combination already grabbed by a different process\n");
604+
exit(1);
605+
}
606+
char buf[1024];
607+
XGetErrorText(dpy_p, err->error_code, buf, 1024);
608+
fprintf(stderr, "X error: %s\n", buf);
609+
exit(1);
610+
}
611+
440612
int main(int argc, char* argv[]) {
441613
int res;
442614

@@ -452,7 +624,9 @@ int main(int argc, char* argv[]) {
452624
fprintf(stderr, "Can't open display");
453625
return 1;
454626
}
627+
XSetErrorHandler(xerror_handler);
455628
screen = DefaultScreen(dpy);
629+
root = RootWindow(dpy, screen);
456630

457631
int event, error, opcode;
458632
if (!XShapeQueryExtension(dpy, &event, &error)) {
@@ -491,6 +665,11 @@ int main(int argc, char* argv[]) {
491665
return res;
492666
}
493667

668+
res = grab_keys();
669+
if (res) {
670+
return res;
671+
}
672+
494673
XAllowEvents(dpy, SyncBoth, CurrentTime);
495674
XSync(dpy, False);
496675

@@ -510,6 +689,7 @@ int main(int argc, char* argv[]) {
510689
if (!cursor_visible) {
511690
show_cursor();
512691
}
692+
XUngrabKey(dpy, AnyKey, AnyModifier, root);
513693
XUnmapWindow(dpy, win);
514694
XFreeGC(dpy, gc);
515695
XDestroyWindow(dpy, win);

0 commit comments

Comments
 (0)