4747static Display * dpy ;
4848static GC gc = 0 ;
4949static Window win ;
50+ static Window root ;
5051static int screen ;
5152static 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+
5384static XColor pressed_color ;
5485static XColor released_color ;
5586static 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
123154void 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
221252void 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+
223293void 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+
338447void 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+
344476void 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
379532int 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+
440612int 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