├── .gitignore ├── LICENSE ├── README.md ├── demo.gif ├── highlight-pointer.c └── makefile /.gitignore: -------------------------------------------------------------------------------- 1 | highlight-pointer 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sven Willner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # highlight-pointer 2 | 3 | Highlight mouse pointer/cursor using a dot - useful for presentations, 4 | screen sharing, ... 5 | 6 | ## Demo 7 | 8 | ![](demo.gif) 9 | 10 | ## Features 11 | 12 | - Very lightweight, should work on any Linux/Unix system running an X 13 | server 14 | - Should work with any software capturing/sharing the screen 15 | regardless if it shows the cursor (like Zoom) or not (like Skype) 16 | - Set color for mouse button released and/or pressed state 17 | - Highlight using a filled or outlined dot 18 | - Auto-hide highlight and/or cursor after a time when not moving and 19 | re-show when moving again 20 | - Global hotkeys for toggling cursor or highlighter and for toggling 21 | auto-hiding 22 | 23 | ## Installation 24 | 25 | Download the `highlight-pointer` binary from the [releases 26 | page](https://github.com/swillner/highlight-pointer/releases/latest) 27 | or see below to build yourself. 28 | 29 | ### Prerequisites 30 | 31 | To build `highlight-pointer` you need the X11, Xext, Xfixes, and Xi 32 | libraries. On Debian/Ubuntu, just install these using 33 | 34 | ``` 35 | sudo apt-get install libx11-dev libxext-dev libxfixes-dev libxi-dev 36 | ``` 37 | 38 | ### Building 39 | 40 | Just build the `highlight-pointer` binary using 41 | 42 | ``` 43 | make 44 | ``` 45 | 46 | ## Usage 47 | 48 | Just call the `highlight-pointer` binary and include command line 49 | options if you want to change color, size, etc. (see below). 50 | 51 | To quit the program press `Ctrl+C` in the terminal where you started 52 | it, or run `killall highlight-pointer`. 53 | 54 | ### Options 55 | 56 | ``` 57 | Usage: 58 | highlight-pointer [options] 59 | 60 | -h, --help show this help message 61 | 62 | DISPLAY OPTIONS 63 | -c, --released-color COLOR dot color when mouse button released [default: #d62728] 64 | -p, --pressed-color COLOR dot color when mouse button pressed [default: #1f77b4] 65 | -o, --outline OUTLINE line width of outline or 0 for filled dot [default: 0] 66 | -r, --radius RADIUS dot radius in pixels [default: 5] 67 | --hide-highlight start with highlighter hidden 68 | --show-cursor start with cursor shown 69 | 70 | TIMEOUT OPTIONS 71 | --auto-hide-cursor hide cursor when not moving after timeout 72 | --auto-hide-highlight hide highlighter when not moving after timeout 73 | -t, --hide-timeout TIMEOUT timeout for hiding when idle, in seconds [default: 3] 74 | 75 | HOTKEY OPTIONS 76 | --key-quit KEY quit 77 | --key-toggle-cursor KEY toggle cursor visibility 78 | --key-toggle-highlight KEY toggle highlight visibility 79 | --key-toggle-auto-hide-cursor KEY toggle auto-hiding cursor when not moving 80 | --key-toggle-auto-hide-highlight KEY toggle auto-hiding highlight when not moving 81 | 82 | Hotkeys are global and can only be used if not set yet by a different process. 83 | Keys can be given with modifiers 84 | 'S' (shift key), 'C' (ctrl key), 'M' (alt/meta key), 'H' (super/"windows" key) 85 | delimited by a '-'. 86 | Keys themselves are parsed by X, so chars like a...z can be set directly, 87 | special keys are named as in /usr/include/X11/keysymdef.h 88 | or see, e.g. http://xahlee.info/linux/linux_show_keycode_keysym.html 89 | 90 | Examples: 'H-Left', 'C-S-a' 91 | ``` 92 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swillner/highlight-pointer/32bf4c60696a4764e8060574ca3031f4fb4ca20d/demo.gif -------------------------------------------------------------------------------- /highlight-pointer.c: -------------------------------------------------------------------------------- 1 | /* 2 | highlight-ponter 3 | 4 | Highlight mouse pointer/cursor using a dot - useful for 5 | presentations, screen sharing, ... 6 | 7 | MIT License 8 | 9 | Copyright (c) 2020 Sven Willner 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | */ 29 | 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | 45 | #define TARGET_FPS 0 46 | 47 | static Display* dpy; 48 | static GC gc = 0; 49 | static Window win; 50 | static Window root; 51 | static int screen; 52 | static int selfpipe[2]; /* for self-pipe trick to cancel select() call */ 53 | 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 | 84 | static XColor pressed_color; 85 | static XColor released_color; 86 | static int button_pressed = 0; 87 | static int cursor_visible = 1; 88 | static int highlight_visible = 0; 89 | 90 | static struct { 91 | char* pressed_color_string; 92 | char* released_color_string; 93 | int auto_hide_cursor; 94 | int auto_hide_highlight; 95 | int cursor_visible; 96 | int hide_timeout; 97 | int highlight_visible; 98 | int outline; 99 | int radius; 100 | } options; 101 | 102 | static void redraw(); 103 | static int get_pointer_position(int* x, int* y); 104 | 105 | static void show_cursor() { 106 | XFixesShowCursor(dpy, root); 107 | cursor_visible = 1; 108 | } 109 | 110 | static void hide_cursor() { 111 | XFixesHideCursor(dpy, root); 112 | cursor_visible = 0; 113 | } 114 | 115 | static void show_highlight() { 116 | int x, y; 117 | int total_radius = options.radius + options.outline; 118 | get_pointer_position(&x, &y); 119 | XMoveWindow(dpy, win, x - total_radius - 1, y - total_radius - 1); 120 | XMapWindow(dpy, win); 121 | redraw(); 122 | highlight_visible = 1; 123 | } 124 | 125 | static void hide_highlight() { 126 | XUnmapWindow(dpy, win); 127 | highlight_visible = 0; 128 | } 129 | 130 | static int init_events() { 131 | XIEventMask events; 132 | unsigned char mask[(XI_LASTEVENT + 7) / 8]; 133 | memset(mask, 0, sizeof(mask)); 134 | 135 | XISetMask(mask, XI_RawButtonPress); 136 | XISetMask(mask, XI_RawButtonRelease); 137 | XISetMask(mask, XI_RawMotion); 138 | 139 | events.deviceid = XIAllMasterDevices; 140 | events.mask = mask; 141 | events.mask_len = sizeof(mask); 142 | 143 | XISelectEvents(dpy, root, &events, 1); 144 | 145 | return 0; 146 | } 147 | 148 | static int get_pointer_position(int* x, int* y) { 149 | Window w; 150 | int i; 151 | unsigned int ui; 152 | return XQueryPointer(dpy, root, &w, &w, x, y, &i, &i, &ui); 153 | } 154 | 155 | static void set_window_mask() { 156 | XGCValues gc_values; 157 | int total_radius = options.radius + options.outline; 158 | Pixmap mask = XCreatePixmap(dpy, win, 2 * total_radius + 2, 2 * total_radius + 2, 1); 159 | GC mask_gc = XCreateGC(dpy, mask, 0, &gc_values); 160 | XSetForeground(dpy, mask_gc, 0); 161 | XFillRectangle(dpy, mask, mask_gc, options.outline, options.outline, 2 * total_radius + 2, 2 * total_radius + 2); 162 | 163 | XSetForeground(dpy, mask_gc, 1); 164 | if (options.outline) { 165 | XSetLineAttributes(dpy, mask_gc, options.outline, LineSolid, CapButt, JoinBevel); 166 | XDrawArc(dpy, mask, mask_gc, options.outline, options.outline, 2 * options.radius + 1, 2 * options.radius + 1, 0, 360 * 64); 167 | } else { 168 | XFillArc(dpy, mask, mask_gc, options.outline, options.outline, 2 * options.radius + 1, 2 * options.radius + 1, 0, 360 * 64); 169 | } 170 | 171 | XShapeCombineMask(dpy, win, ShapeBounding, 0, 0, mask, ShapeSet); 172 | 173 | XFreeGC(dpy, mask_gc); 174 | XFreePixmap(dpy, mask); 175 | } 176 | 177 | static int init_window() { 178 | int total_radius = options.radius + options.outline; 179 | XSetWindowAttributes win_attributes; 180 | win_attributes.event_mask = ExposureMask | VisibilityChangeMask; 181 | win_attributes.override_redirect = True; 182 | 183 | win = XCreateWindow(dpy, root, options.outline, options.outline, 2 * total_radius + 2, 2 * total_radius + 2, 0, DefaultDepth(dpy, screen), InputOutput, DefaultVisual(dpy, screen), 184 | CWEventMask | CWOverrideRedirect, &win_attributes); 185 | if (!win) { 186 | fprintf(stderr, "Can't create highlight window\n"); 187 | return 1; 188 | } 189 | 190 | XClassHint class_hint; 191 | XStoreName(dpy, win, "highlight-pointer"); 192 | class_hint.res_name = "highlight-pointer"; 193 | class_hint.res_class = "HighlightPointer"; 194 | XSetClassHint(dpy, win, &class_hint); 195 | 196 | Atom window_type_atom = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DND", False); 197 | XChangeProperty(dpy, win, XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False), XA_ATOM, 32, PropModeReplace, (unsigned char*)&window_type_atom, 1); 198 | 199 | /* hide window decorations */ 200 | /* after https://github.com/akkana/moonroot */ 201 | Atom motif_wm_hints = XInternAtom(dpy, "_MOTIF_WM_HINTS", True); 202 | struct { 203 | CARD32 flags; 204 | CARD32 functions; 205 | CARD32 decorations; 206 | INT32 input_mode; 207 | CARD32 status; 208 | } mwmhints; 209 | mwmhints.flags = 1L << 1 /* MWM_HINTS_DECORATIONS */; 210 | mwmhints.decorations = 0; 211 | XChangeProperty(dpy, win, motif_wm_hints, motif_wm_hints, 32, PropModeReplace, (unsigned char*)&mwmhints, 5 /* PROP_MWM_HINTS_ELEMENTS */); 212 | 213 | /* always stay on top */ 214 | /* after gdk_wmspec_change_state */ 215 | XClientMessageEvent xclient; 216 | memset(&xclient, 0, sizeof(xclient)); 217 | xclient.type = ClientMessage; 218 | xclient.window = win; 219 | xclient.message_type = XInternAtom(dpy, "_NET_WM_STATE", False); 220 | xclient.format = 32; 221 | xclient.data.l[0] = 1 /* _NET_WM_STATE_ADD */; 222 | xclient.data.l[1] = XInternAtom(dpy, "_NET_WM_STATE_STAYS_ON_TOP", False); 223 | xclient.data.l[2] = 0; 224 | xclient.data.l[3] = 1; 225 | xclient.data.l[4] = 0; 226 | XSendEvent(dpy, root, False, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent*)&xclient); 227 | 228 | /* let clicks fall through */ 229 | /* after https://stackoverflow.com/a/9279747 */ 230 | XRectangle rect; 231 | XserverRegion region = XFixesCreateRegion(dpy, &rect, 1); 232 | XFixesSetWindowShapeRegion(dpy, win, ShapeInput, 0, 0, region); 233 | XFixesDestroyRegion(dpy, region); 234 | 235 | XGCValues gc_values; 236 | gc_values.foreground = WhitePixel(dpy, screen); 237 | gc_values.background = BlackPixel(dpy, screen); 238 | gc = XCreateGC(dpy, win, GCForeground | GCBackground, &gc_values); 239 | 240 | set_window_mask(); 241 | 242 | return 0; 243 | } 244 | 245 | static void redraw() { 246 | XSetForeground(dpy, gc, button_pressed ? pressed_color.pixel : released_color.pixel); 247 | if (options.outline) { 248 | XSetLineAttributes(dpy, gc, options.outline, LineSolid, CapButt, JoinBevel); 249 | XDrawArc(dpy, win, gc, options.outline, options.outline, 2 * options.radius + 1, 2 * options.radius + 1, 0, 360 * 64); 250 | } else { 251 | XFillArc(dpy, win, gc, options.outline, options.outline, 2 * options.radius + 1, 2 * options.radius + 1, 0, 360 * 64); 252 | } 253 | } 254 | 255 | static void quit() { write(selfpipe[1], "", 1); } 256 | 257 | static void handle_key(KeySym keysym, unsigned int modifiers) { 258 | modifiers = modifiers & ~(numlockmask | LockMask); 259 | int k; 260 | for (k = 0; k < KEY_ARRAY_SIZE; ++k) { 261 | if (keys[k].keysym == keysym && keys[k].modifiers == modifiers) { 262 | break; 263 | } 264 | } 265 | switch (k) { 266 | case KEY_QUIT: 267 | quit(); 268 | break; 269 | 270 | case KEY_TOGGLE_CURSOR: 271 | options.cursor_visible = 1 - options.cursor_visible; 272 | if (options.cursor_visible && !cursor_visible) { 273 | show_cursor(); 274 | } else if (!options.cursor_visible && cursor_visible) { 275 | hide_cursor(); 276 | } 277 | break; 278 | 279 | case KEY_TOGGLE_HIGHLIGHT: 280 | if (options.highlight_visible) { 281 | hide_highlight(); 282 | } else { 283 | show_highlight(); 284 | } 285 | options.highlight_visible = 1 - options.highlight_visible; 286 | break; 287 | 288 | case KEY_TOGGLE_AUTOHIDE_CURSOR: 289 | options.auto_hide_cursor = 1 - options.auto_hide_cursor; 290 | break; 291 | 292 | case KEY_TOGGLE_AUTOHIDE_HIGHLIGHT: 293 | options.auto_hide_highlight = 1 - options.auto_hide_highlight; 294 | break; 295 | } 296 | } 297 | 298 | static void main_loop() { 299 | XEvent ev; 300 | fd_set fds; 301 | int fd = ConnectionNumber(dpy); 302 | struct timeval timeout; 303 | int x, y, n; 304 | int total_radius = options.radius + options.outline; 305 | XGenericEventCookie* cookie; 306 | #if TARGET_FPS > 0 307 | Time lasttime = 0; 308 | #endif 309 | 310 | pipe(selfpipe); 311 | 312 | while (1) { 313 | XFlush(dpy); 314 | FD_ZERO(&fds); 315 | FD_SET(fd, &fds); 316 | FD_SET(selfpipe[0], &fds); 317 | timeout.tv_usec = 0; 318 | timeout.tv_sec = options.hide_timeout; 319 | n = select((fd > selfpipe[0] ? fd : selfpipe[0]) + 1, &fds, NULL, NULL, &timeout); 320 | if (n < 0) { 321 | if (errno != EINTR) { 322 | perror("select() failed"); 323 | } 324 | break; 325 | } 326 | if (n > 0) { 327 | if (FD_ISSET(selfpipe[0], &fds)) { 328 | break; 329 | } 330 | while (XPending(dpy)) { 331 | XNextEvent(dpy, &ev); 332 | 333 | if (ev.type == GenericEvent) { 334 | cookie = &ev.xcookie; 335 | #if TARGET_FPS > 0 336 | if (!XGetEventData(dpy, cookie)) { 337 | continue; 338 | } 339 | const XIRawEvent* data = (const XIRawEvent*)cookie->data; 340 | if (data->time - lasttime <= 1000 / TARGET_FPS) { 341 | XFreeEventData(dpy, cookie); 342 | continue; 343 | } 344 | lasttime = data->time; 345 | XFreeEventData(dpy, cookie); 346 | #endif 347 | if (cookie->evtype == XI_RawMotion) { 348 | if (options.auto_hide_cursor && options.cursor_visible && !cursor_visible) { 349 | show_cursor(); 350 | } 351 | if (options.auto_hide_highlight && options.highlight_visible && !highlight_visible) { 352 | show_highlight(); 353 | } else if (highlight_visible) { 354 | get_pointer_position(&x, &y); 355 | XMoveWindow(dpy, win, x - total_radius - 1, y - total_radius - 1); 356 | /* unfortunately, this causes increase of the X server's cpu usage */ 357 | } 358 | continue; 359 | } 360 | if (cookie->evtype == XI_RawButtonPress) { 361 | button_pressed = 1; 362 | redraw(); 363 | continue; 364 | } 365 | if (cookie->evtype == XI_RawButtonRelease) { 366 | button_pressed = 0; 367 | redraw(); 368 | continue; 369 | } 370 | continue; 371 | } 372 | 373 | if (ev.type == KeyPress) { 374 | KeySym keysym = XLookupKeysym(&ev.xkey, 0); 375 | if (keysym != NoSymbol) { 376 | handle_key(keysym, ev.xkey.state); 377 | } 378 | continue; 379 | } 380 | if (ev.type == Expose) { 381 | if (ev.xexpose.count < 1) { 382 | redraw(); 383 | } 384 | continue; 385 | } 386 | if (ev.type == VisibilityNotify) { 387 | /* needed to deal with menus, etc. overlapping the hightlight win */ 388 | XRaiseWindow(dpy, win); 389 | continue; 390 | } 391 | } 392 | } else { 393 | if (options.auto_hide_cursor && cursor_visible) { 394 | hide_cursor(); 395 | } 396 | if (options.auto_hide_highlight && highlight_visible) { 397 | hide_highlight(); 398 | } 399 | } 400 | } 401 | } 402 | 403 | static int init_colors() { 404 | int res; 405 | 406 | Colormap colormap = DefaultColormap(dpy, screen); 407 | 408 | res = XAllocNamedColor(dpy, colormap, options.pressed_color_string, &pressed_color, &pressed_color); 409 | if (!res) { 410 | fprintf(stderr, "Can't allocate color: %s\n", options.pressed_color_string); 411 | return 1; 412 | } 413 | 414 | res = XAllocNamedColor(dpy, colormap, options.released_color_string, &released_color, &released_color); 415 | if (!res) { 416 | fprintf(stderr, "Can't allocate color: %s\n", options.released_color_string); 417 | return 1; 418 | } 419 | 420 | return 0; 421 | } 422 | 423 | static int grab_keys() { 424 | /* after https://git.suckless.org/dwm/file/dwm.c.html */ 425 | numlockmask = 0; 426 | unsigned int numlockkeycode = XKeysymToKeycode(dpy, XK_Num_Lock); 427 | if (numlockkeycode) { 428 | XModifierKeymap* modmap = XGetModifierMapping(dpy); 429 | for (int i = 0; i < 8; ++i) { 430 | for (int j = 0; j < modmap->max_keypermod; ++j) { 431 | if (modmap->modifiermap[i * modmap->max_keypermod + j] == numlockkeycode) { 432 | numlockmask = (1 << i); 433 | } 434 | } 435 | } 436 | XFreeModifiermap(modmap); 437 | } 438 | 439 | unsigned int modifiers[] = {0, LockMask, numlockmask, numlockmask | LockMask}; 440 | for (int i = 0; i < KEY_ARRAY_SIZE; ++i) { 441 | if (keys[i].keysym != NoSymbol) { 442 | KeyCode c = XKeysymToKeycode(dpy, keys[i].keysym); 443 | if (!c) { 444 | fprintf(stderr, "Could not convert key to keycode\n"); 445 | return 1; 446 | } 447 | for (int j = 0; j < 2; ++j) { 448 | XGrabKey(dpy, c, keys[i].modifiers | modifiers[j], root, 1, GrabModeAsync, GrabModeAsync); 449 | } 450 | } 451 | } 452 | return 0; 453 | } 454 | 455 | static void sig_handler(int sig) { 456 | (void)sig; 457 | quit(); 458 | } 459 | 460 | static int parse_key(const char* s, int k) { 461 | keys[k].modifiers = 0; 462 | 463 | int i; 464 | while (s[0] != '\0' && s[1] == '-') { 465 | for (i = 0; i < KEY_MODMAP_SIZE; ++i) { 466 | if (key_modifier_mapping[i].symbol == s[0]) { 467 | keys[k].modifiers |= key_modifier_mapping[i].modifiers; 468 | break; 469 | } 470 | } 471 | if (i == KEY_MODMAP_SIZE) { 472 | return 1; 473 | } 474 | s += 2; 475 | } 476 | 477 | keys[k].keysym = XStringToKeysym(s); 478 | if (keys[k].keysym == NoSymbol) { 479 | return 1; 480 | } 481 | return 0; 482 | } 483 | 484 | static void print_usage(const char* name) { 485 | printf( 486 | "Usage:\n" 487 | " %s [options]\n" 488 | "\n" 489 | " -h, --help show this help message\n" 490 | "\n" 491 | "DISPLAY OPTIONS\n" 492 | " -c, --released-color COLOR dot color when mouse button released [default: #d62728]\n" 493 | " -p, --pressed-color COLOR dot color when mouse button pressed [default: #1f77b4]\n" 494 | " -o, --outline OUTLINE line width of outline or 0 for filled dot [default: 0]\n" 495 | " -r, --radius RADIUS dot radius in pixels [default: 5]\n" 496 | " --hide-highlight start with highlighter hidden\n" 497 | " --show-cursor start with cursor shown\n" 498 | "\n" 499 | "TIMEOUT OPTIONS\n" 500 | " --auto-hide-cursor hide cursor when not moving after timeout\n" 501 | " --auto-hide-highlight hide highlighter when not moving after timeout\n" 502 | " -t, --hide-timeout TIMEOUT timeout for hiding when idle, in seconds [default: 3]\n" 503 | "\n" 504 | "HOTKEY OPTIONS\n" 505 | " --key-quit KEY quit\n" 506 | " --key-toggle-cursor KEY toggle cursor visibility\n" 507 | " --key-toggle-highlight KEY toggle highlight visibility\n" 508 | " --key-toggle-auto-hide-cursor KEY toggle auto-hiding cursor when not moving\n" 509 | " --key-toggle-auto-hide-highlight KEY toggle auto-hiding highlight when not moving\n" 510 | "\n" 511 | " Hotkeys are global and can only be used if not set yet by a different process.\n" 512 | " Keys can be given with modifiers\n" 513 | " 'S' (shift key), 'C' (ctrl key), 'M' (alt/meta key), 'H' (super/\"windows\" key)\n" 514 | " delimited by a '-'.\n" 515 | " Keys themselves are parsed by X, so chars like a...z can be set directly,\n" 516 | " special keys are named as in /usr/include/X11/keysymdef.h\n" 517 | " or see, e.g. http://xahlee.info/linux/linux_show_keycode_keysym.html\n" 518 | "\n" 519 | " Examples: 'H-Left', 'C-S-a'\n", 520 | name); 521 | } 522 | 523 | static struct option long_options[] = {{"auto-hide-cursor", no_argument, &options.auto_hide_cursor, 1}, 524 | {"auto-hide-highlight", no_argument, &options.auto_hide_highlight, 1}, 525 | {"help", no_argument, NULL, 'h'}, 526 | {"hide-highlight", no_argument, &options.highlight_visible, 0}, 527 | {"hide-timeout", required_argument, NULL, 't'}, 528 | {"outline", required_argument, NULL, 'o'}, 529 | {"pressed-color", required_argument, NULL, 'p'}, 530 | {"radius", required_argument, NULL, 'r'}, 531 | {"released-color", required_argument, NULL, 'c'}, 532 | {"show-cursor", no_argument, &options.cursor_visible, 1}, 533 | {"key-quit", required_argument, NULL, KEY_QUIT + KEY_OPTION_OFFSET}, 534 | {"key-toggle-cursor", required_argument, NULL, KEY_TOGGLE_CURSOR + KEY_OPTION_OFFSET}, 535 | {"key-toggle-highlight", required_argument, NULL, KEY_TOGGLE_HIGHLIGHT + KEY_OPTION_OFFSET}, 536 | {"key-toggle-auto-hide-cursor", required_argument, NULL, KEY_TOGGLE_AUTOHIDE_CURSOR + KEY_OPTION_OFFSET}, 537 | {"key-toggle-auto-hide-highlight", required_argument, NULL, KEY_TOGGLE_AUTOHIDE_HIGHLIGHT + KEY_OPTION_OFFSET}, 538 | {NULL, 0, NULL, 0}}; 539 | 540 | static int set_options(int argc, char* argv[]) { 541 | options.auto_hide_cursor = 0; 542 | options.auto_hide_highlight = 0; 543 | options.cursor_visible = 0; 544 | options.highlight_visible = 1; 545 | options.radius = 5; 546 | options.outline = 0; 547 | options.hide_timeout = 3; 548 | options.pressed_color_string = "#1f77b4"; 549 | options.released_color_string = "#d62728"; 550 | 551 | while (1) { 552 | int c = getopt_long(argc, argv, "c:ho:p:r:t:", long_options, NULL); 553 | if (c < 0) { 554 | break; 555 | } 556 | if (c >= KEY_OPTION_OFFSET && c < KEY_OPTION_OFFSET + KEY_ARRAY_SIZE) { 557 | int res = parse_key(optarg, c - KEY_OPTION_OFFSET); 558 | if (res) { 559 | fprintf(stderr, "Could not parse key value %s\n", optarg); 560 | return 1; 561 | } 562 | continue; 563 | } 564 | switch (c) { 565 | case 0: 566 | break; 567 | 568 | case 'c': 569 | options.released_color_string = optarg; 570 | break; 571 | 572 | case 'h': 573 | print_usage(argv[0]); 574 | return -1; 575 | 576 | case 'o': 577 | options.outline = atoi(optarg); 578 | if (options.outline < 0) { 579 | fprintf(stderr, "Invalid outline value %s\n", optarg); 580 | return 1; 581 | } 582 | break; 583 | 584 | case 'p': 585 | options.pressed_color_string = optarg; 586 | break; 587 | 588 | case 'r': 589 | options.radius = atoi(optarg); 590 | if (options.radius <= 0) { 591 | fprintf(stderr, "Invalid radius value %s\n", optarg); 592 | return 1; 593 | } 594 | break; 595 | 596 | case 't': 597 | options.hide_timeout = atoi(optarg); 598 | if (options.hide_timeout <= 0) { 599 | fprintf(stderr, "Invalid timeout value %s\n", optarg); 600 | return 1; 601 | } 602 | break; 603 | 604 | default: 605 | print_usage(argv[0]); 606 | return 1; 607 | } 608 | } 609 | return 0; 610 | } 611 | 612 | static int xerror_handler(Display* dpy_p, XErrorEvent* err) { 613 | if (err->request_code == 33 /* XGrabKey */ && err->error_code == BadAccess) { 614 | fprintf(stderr, "Key combination already grabbed by a different process\n"); 615 | exit(1); 616 | } 617 | if (err->error_code == BadAtom) { 618 | fprintf(stderr, "X warning: BadAtom for %d-%d\n", err->request_code, err->minor_code); 619 | return 0; 620 | } 621 | char buf[1024]; 622 | XGetErrorText(dpy_p, err->error_code, buf, 1024); 623 | fprintf(stderr, "X error: %s\n", buf); 624 | exit(1); 625 | } 626 | 627 | int main(int argc, char* argv[]) { 628 | int res; 629 | 630 | res = set_options(argc, argv); 631 | if (res < 0) { 632 | return 0; 633 | } else if (res > 0) { 634 | return res; 635 | } 636 | 637 | dpy = XOpenDisplay(NULL); /* defaults to DISPLAY env var */ 638 | if (!dpy) { 639 | fprintf(stderr, "Can't open display\n"); 640 | return 1; 641 | } 642 | XSetErrorHandler(xerror_handler); 643 | screen = DefaultScreen(dpy); 644 | root = RootWindow(dpy, screen); 645 | 646 | int event, error, opcode; 647 | if (!XShapeQueryExtension(dpy, &event, &error)) { 648 | fprintf(stderr, "XShape extension not supported\n"); 649 | return 1; 650 | } 651 | 652 | if (!XQueryExtension(dpy, "XInputExtension", &opcode, &event, &error)) { 653 | fprintf(stderr, "XInput extension not supported\n"); 654 | return 1; 655 | } 656 | 657 | int major_version = 2; 658 | int minor_version = 2; 659 | res = XIQueryVersion(dpy, &major_version, &minor_version); 660 | if (res == BadRequest) { 661 | fprintf(stderr, "XInput2 extension version 2.2 not supported\n"); 662 | return 1; 663 | } else if (res != Success) { 664 | fprintf(stderr, "Can't query XInput version\n"); 665 | return 1; 666 | } 667 | 668 | res = init_window(); 669 | if (res) { 670 | return res; 671 | } 672 | 673 | res = init_events(); 674 | if (res) { 675 | return res; 676 | } 677 | 678 | res = init_colors(); 679 | if (res) { 680 | return res; 681 | } 682 | 683 | res = grab_keys(); 684 | if (res) { 685 | return res; 686 | } 687 | 688 | XAllowEvents(dpy, SyncBoth, CurrentTime); 689 | XSync(dpy, False); 690 | 691 | if (options.highlight_visible) { 692 | show_highlight(); 693 | } 694 | 695 | if (!options.cursor_visible) { 696 | hide_cursor(); 697 | } 698 | 699 | signal(SIGINT, sig_handler); 700 | signal(SIGTERM, sig_handler); 701 | 702 | main_loop(); 703 | 704 | if (!cursor_visible) { 705 | show_cursor(); 706 | } 707 | XUngrabKey(dpy, AnyKey, AnyModifier, root); 708 | XUnmapWindow(dpy, win); 709 | XFreeGC(dpy, gc); 710 | XDestroyWindow(dpy, win); 711 | XCloseDisplay(dpy); 712 | 713 | return 0; 714 | } 715 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | highlight-pointer: highlight-pointer.c 2 | $(CC) $^ -o $@ -flto -O3 -Wall -Wextra -Wshadow -std=c99 -lX11 -lXext -lXfixes -lXi 3 | --------------------------------------------------------------------------------