1 /*
2 Parsing options/resources, top-level keygrab functions and main().
3 
4 Copyright 2017-2021 Alexander Kulak.
5 This file is part of alttab program.
6 
7 alttab is free software: you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation, either version 3 of the License, or
10 (at your option) any later version.
11 
12 alttab is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 GNU General Public License for more details.
16 
17 You should have received a copy of the GNU General Public License
18 along with alttab.  If not, see <http://www.gnu.org/licenses/>.
19 */
20 
21 #include <X11/Xlib.h>
22 #include <X11/Xutil.h>
23 #include <X11/Xresource.h>
24 #include <X11/Xft/Xft.h>
25 #include <stdbool.h>
26 #include <stdio.h>
27 #include <string.h>
28 #include <time.h>
29 #include <signal.h>
30 #include "alttab.h"
31 #include "util.h"
32 #include "config.h"
33 
34 // PUBLIC
35 
36 Globals g;
37 // globals common for alttab, util and icon
38 Display *dpy;
39 int scr;
40 Window root;
41 
42 // PRIVATE
43 static XrmDatabase db;
44 
45 //
46 // help and exit
47 //
helpexit()48 static void helpexit()
49 {
50     msg(-1, "the task switcher, v%s\n\
51 Options:\n\
52     -w N      window manager: 0=no, 1=ewmh-compatible, 2=ratpoison, 3=old fashion\n\
53     -d N      desktop: 0=current 1=all, 2=all but special, 3=all but current\n\
54    -sc N      screen: 0=current 1=all\n\
55    -kk str    keysym of main key\n\
56    -mk str    keysym of main modifier\n\
57    -bk str    keysym of backscroll modifier\n\
58    -pk str    keysym of 'prev' key\n\
59    -nk str    keysym of 'next' key\n\
60    -mm N      (obsoleted) main modifier mask\n\
61    -bm N      (obsoleted) backward scroll modifier mask\n\
62     -t NxM    tile geometry\n\
63     -i NxM    icon geometry\n\
64    -vp str    switcher viewport: focus, pointer, total, WxH+X+Y\n\
65     -p str    switcher position: center, none, +X+Y\n\
66     -s N      icon source: 0=X11 only, 1=fallback to files, 2=best size, 3=files only\n\
67 -theme name   icon theme\n\
68    -bg color  background color\n\
69    -fg color  foreground color\n\
70 -frame color  active frame color\n\
71  -font name   font name in the form xft:fontconfig_pattern\n\
72   -v|-vv      verbose\n\
73     -h        help\n\
74 See man alttab for details.\n", PACKAGE_VERSION);
75     exit(0);
76 }
77 
78 //
79 // initialize globals based on executable agruments and Xresources
80 // return 1 if success, 0 otherwise
81 // on fatal failure, calls die/exit
82 //
use_args_and_xrm(int * argc,char ** argv)83 static int use_args_and_xrm(int *argc, char **argv)
84 {
85 // set debug level early
86     g.debug = 0;
87     char *errmsg;
88     int ksi;
89     KeyCode BC;
90     unsigned int wmindex, dsindex, scindex, isrc;
91     char *gtile, *gicon, *gview, *gpos;
92     int x, y;
93     unsigned int w, h;
94     int xpg;
95     char *s;
96     char *rm;
97     char *empty = "";
98     int uo;
99     Atom nwm_prop, atype;
100     unsigned char *nwm;
101     int form;
102     unsigned long remain, len;
103     XrmOptionDescRec xrmTable[] = {
104         {"-w", "*windowmanager", XrmoptionSepArg, NULL},
105         {"-d", "*desktops", XrmoptionSepArg, NULL},
106         {"-sc", "*screens", XrmoptionSepArg, NULL},
107         {"-mm", "*modifier.mask", XrmoptionSepArg, NULL},
108         {"-bm", "*backscroll.mask", XrmoptionSepArg, NULL},
109         {"-mk", "*modifier.keysym", XrmoptionSepArg, NULL},
110         {"-kk", "*key.keysym", XrmoptionSepArg, NULL},
111         {"-bk", "*backscroll.keysym", XrmoptionSepArg, NULL},
112         {"-pk", "*prevkey.keysym", XrmoptionSepArg, NULL},
113         {"-nk", "*nextkey.keysym", XrmoptionSepArg, NULL},
114         {"-ck", "*cancelkey.keysym", XrmoptionSepArg, NULL},
115         {"-t", "*tile.geometry", XrmoptionSepArg, NULL},
116         {"-i", "*icon.geometry", XrmoptionSepArg, NULL},
117         {"-vp", "*viewport", XrmoptionSepArg, NULL},
118         {"-p", "*position", XrmoptionSepArg, NULL},
119         {"-s", "*icon.source", XrmoptionSepArg, NULL},
120         {"-theme", "*theme", XrmoptionSepArg, NULL},
121         {"-bg", "*background", XrmoptionSepArg, NULL},
122         {"-fg", "*foreground", XrmoptionSepArg, NULL},
123         {"-frame", "*framecolor", XrmoptionSepArg, NULL},
124         {"-font", "*font", XrmoptionSepArg, NULL},
125     };
126     const char *inv = "invalid %s, use -h for help\n";
127     const char *rmb = "can't figure out modmask from keycode 0x%x\n";
128 
129 // not using getopt() because of need for "-v" before Xrm
130     int arg;
131     for (arg = 0; arg < (*argc); arg++) {
132         if ((strcmp(argv[arg], "-v") == 0)) {
133             g.debug = 1;
134             remove_arg(argc, argv, arg);
135         } else if ((strcmp(argv[arg], "-vv") == 0)) {
136             g.debug = 2;
137             remove_arg(argc, argv, arg);
138         } else if ((strcmp(argv[arg], "-h") == 0)) {
139             helpexit();
140             remove_arg(argc, argv, arg);
141         }
142     }
143     msg(0, "%s\n", PACKAGE_STRING);
144     msg(0, "debug level %d\n", g.debug);
145 
146     XrmInitialize();
147     rm = XResourceManagerString(dpy);
148     msg(1, "resource manager: \"%s\"\n", rm);
149     if (!rm) {
150         msg(0, "can't get resource manager, using empty db\n");
151         //return 0;  // we can do it
152         //db = XrmGetDatabase (dpy);
153         rm = empty;
154     }
155     db = XrmGetStringDatabase(rm);
156     if (!db) {
157         msg(-1, "can't get resource database\n");
158         return 0;
159     }
160     XrmParseCommand(&db, xrmTable, sizeof(xrmTable) / sizeof(xrmTable[0]),
161                     XRMAPPNAME, argc, argv);
162     if ((*argc) > 1) {
163         g.debug = 1;
164         msg(-1, "unknown options or wrong arguments:");
165         for (uo = 1; uo < (*argc); uo++) {
166             msg(0, " \"%s\"", argv[uo]);
167         }
168         msg(0, ", use -h for help\n");
169         exit(1);
170     }
171 
172     switch (xresource_load_int(&db, XRMAPPNAME, "windowmanager", &wmindex)) {
173     case 1:
174         if (wmindex >= WM_MIN && wmindex <= WM_MAX) {
175             g.option_wm = wmindex;
176             goto wmDone;
177         } else {
178             die(inv, "windowmanager argument range");
179         }
180         break;
181     case 0:
182         msg(0, "no WM index or unknown, guessing\n");
183         break;
184     case -1:
185         die(inv, "windowmanager argument");
186         break;
187     }
188 // EWMH?
189     if (ewmh_detectFeatures(&(g.ewmh))) {
190         msg(0, "EWMH-compatible WM detected: %s\n", g.ewmh.wmname);
191         g.option_wm = WM_EWMH;
192         goto wmDone;
193     }
194 // ratpoison?
195     nwm_prop = XInternAtom(dpy, "_NET_WM_NAME", false);
196     if (XGetWindowProperty(dpy, root, nwm_prop, 0, MAXNAMESZ, false,
197                            AnyPropertyType, &atype, &form, &len, &remain,
198                            &nwm) == Success && nwm) {
199         msg(0, "_NET_WM_NAME root property present: %s\n", nwm);
200         if (strstr((char *)nwm, "ratpoison") != NULL) {
201             g.option_wm = WM_RATPOISON;
202             XFree(nwm);
203             goto wmDone;
204         }
205         XFree(nwm);
206     }
207     msg(0, "unknown WM, using WM_TWM\n");
208     g.option_wm = WM_TWM;
209  wmDone:
210     msg(0, "WM: %d\n", g.option_wm);
211 
212     switch (xresource_load_int(&db, XRMAPPNAME, "desktops", &dsindex)) {
213     case 1:
214         if (dsindex >= DESK_MIN && dsindex <= DESK_MAX)
215             g.option_desktop = dsindex;
216         else
217             die(inv, "desktops argument range");
218         break;
219     case 0:
220         g.option_desktop = DESK_DEFAULT;
221         break;
222     case -1:
223         die(inv, "desktops argument");
224         break;
225     }
226     msg(0, "desktops: %d\n", g.option_desktop);
227 
228     switch (xresource_load_int(&db, XRMAPPNAME, "screens", &scindex)) {
229     case 1:
230         if (scindex >= SCR_MIN && scindex <= SCR_MAX)
231             g.option_screen = scindex;
232         else
233             die(inv, "screens argument range");
234         break;
235     case 0:
236         g.option_screen = SCR_DEFAULT;
237         break;
238     case -1:
239         die(inv, "screens argument");
240         break;
241     }
242     msg(0, "screens: %d\n", g.option_screen);
243 
244 #define  MC  g.option_modCode
245 #define  KC  g.option_keyCode
246 #define  prevC  g.option_prevCode
247 #define  nextC  g.option_nextCode
248 #define  cancelC  g.option_cancelCode
249 #define  GMM  g.option_modMask
250 #define  GBM  g.option_backMask
251 
252     ksi = ksym_option_to_keycode(&db, XRMAPPNAME, "modifier", &errmsg);
253     if (ksi == -1)
254         die("%s\n", errmsg);
255     MC = ksi != 0 ? ksi : XKeysymToKeycode(dpy, DEFMODKS);
256 
257     ksi = ksym_option_to_keycode(&db, XRMAPPNAME, "key", &errmsg);
258     if (ksi == -1)
259         die("%s\n", errmsg);
260     KC = ksi != 0 ? ksi : XKeysymToKeycode(dpy, DEFKEYKS);
261 
262     ksi = ksym_option_to_keycode(&db, XRMAPPNAME, "prevkey", &errmsg);
263     if (ksi == -1)
264         die("%s\n", errmsg);
265     prevC = ksi != 0 ? ksi : XKeysymToKeycode(dpy, DEFPREVKEYKS);
266 
267     ksi = ksym_option_to_keycode(&db, XRMAPPNAME, "nextkey", &errmsg);
268     if (ksi == -1)
269         die("%s\n", errmsg);
270     nextC = ksi != 0 ? ksi : XKeysymToKeycode(dpy, DEFNEXTKEYKS);
271 
272     ksi = ksym_option_to_keycode(&db, XRMAPPNAME, "cancelkey", &errmsg);
273     if (ksi == -1)
274         die("%s\n", errmsg);
275     cancelC = ksi != 0 ? ksi : XKeysymToKeycode(dpy, DEFCANCELKS);
276 
277     switch (xresource_load_int(&db, XRMAPPNAME, "modifier.mask", &(GMM))) {
278     case 1:
279         msg(-1,
280             "Using obsoleted -mm option or modifier.mask resource, see man page for upgrade\n");
281         break;
282     case 0:
283         GMM = keycode_to_modmask(MC);
284         if (GMM == 0)
285             die(rmb, MC);
286         break;
287     case -1:
288         die(inv, "modifier mask");
289         break;
290     }
291 
292     switch (xresource_load_int(&db, XRMAPPNAME, "backscroll.mask", &(GBM))) {
293     case 1:
294         msg(-1,
295             "Using obsoleted -bm option or backscroll.mask resource, see man page for upgrade\n");
296         break;
297     case 0:
298         BC = ksym_option_to_keycode(&db, XRMAPPNAME, "backscroll", &errmsg);
299         if (BC != 0) {
300             GBM = keycode_to_modmask(BC);
301             if (GBM == 0)
302                 die(rmb, BC);
303         } else {
304             GBM = DEFBACKMASK;
305         }
306         break;
307     case -1:
308         die(inv, "backscroll mask");
309         break;
310     }
311 
312     msg(0, "modMask %d, backMask %d, modCode %d, keyCode %d\n",
313         GMM, GBM, MC, KC);
314 
315     g.option_tileW = DEFTILEW;
316     g.option_tileH = DEFTILEH;
317     gtile = xresource_load_string(&db, XRMAPPNAME, "tile.geometry");
318     if (gtile != NULL) {
319         xpg = XParseGeometry(gtile, &x, &y, &w, &h);
320         if (xpg & WidthValue)
321             g.option_tileW = w;
322         else
323             die(inv, "tile width");
324         if (xpg & HeightValue)
325             g.option_tileH = h;
326         else
327             die(inv, "tile height");
328     }
329 
330     g.option_iconW = DEFICONW;
331     g.option_iconH = DEFICONH;
332     gicon = xresource_load_string(&db, XRMAPPNAME, "icon.geometry");
333     if (gicon) {
334         xpg = XParseGeometry(gicon, &x, &y, &w, &h);
335         if (xpg & WidthValue)
336             g.option_iconW = w;
337         else
338             die(inv, "icon width");
339         if (xpg & HeightValue)
340             g.option_iconH = h;
341         else
342             die(inv, "icon height");
343     }
344 
345     msg(0, "%dx%d tile, %dx%d icon\n",
346         g.option_tileW, g.option_tileH, g.option_iconW, g.option_iconH);
347 
348     bzero(&(g.option_vp), sizeof(g.option_vp));
349     g.option_vp_mode = VP_DEFAULT;
350     gview = xresource_load_string(&db, XRMAPPNAME, "viewport");
351     if (gview) {
352         if (strncmp(gview, "focus", 6) == 0) {
353             g.option_vp_mode = VP_FOCUS;
354         } else if (strncmp(gview, "pointer", 8) == 0) {
355             g.option_vp_mode = VP_POINTER;
356         } else if (strncmp(gview, "total", 6) == 0) {
357             g.option_vp_mode = VP_TOTAL;
358         } else {
359             g.option_vp_mode = VP_SPECIFIC;
360             xpg = XParseGeometry(gview, &x, &y, &w, &h);
361             if (xpg & (XValue | YValue | WidthValue | HeightValue)) {
362                 g.option_vp.w = w;
363                 g.option_vp.h = h;
364                 g.option_vp.x = x;
365                 g.option_vp.y = y;
366             } else {
367                 die(inv, "viewport");
368             }
369         }
370     }
371     msg(0, "viewport: mode %d, %dx%d+%d+%d\n",
372         g.option_vp_mode,
373         g.option_vp.w, g.option_vp.h, g.option_vp.x, g.option_vp.y);
374 
375     g.option_positioning = POS_DEFAULT;
376     g.option_posX = 0;
377     g.option_posY = 0;
378     gpos = xresource_load_string(&db, XRMAPPNAME, "position");
379     if (gpos) {
380         if (strncmp(gpos, "center", 7) == 0) {
381             g.option_positioning = POS_CENTER;
382         } else if (strncmp(gpos, "none", 5) == 0) {
383             g.option_positioning = POS_NONE;
384         } else {
385             g.option_positioning = POS_SPECIFIC;
386             xpg = XParseGeometry(gpos, &x, &y, &w, &h);
387             if (xpg & (XValue | YValue)) {
388                 g.option_posX = x;
389                 g.option_posY = y;
390             } else {
391                 die(inv, "position");
392             }
393         }
394     }
395     msg(0, "positioning policy: %d, position: +%d+%d\n",
396         g.option_positioning, g.option_posX, g.option_posY);
397 
398     g.option_iconSrc = ISRC_DEFAULT;
399     switch (xresource_load_int(&db, XRMAPPNAME, "icon.source", &isrc)) {
400     case 1:
401         if (isrc >= ISRC_MIN && isrc <= ISRC_MAX)
402             g.option_iconSrc = isrc;
403         else
404             die("icon source argument must be from %d to %d\n",
405                 ISRC_MIN, ISRC_MAX);
406         break;
407     case 0:
408         g.option_iconSrc = ISRC_DEFAULT;
409         break;
410     case -1:
411         die(inv, "icon source");
412         break;
413     }
414     msg(0, "icon source: %d\n", g.option_iconSrc);
415 
416     s = xresource_load_string(&db, XRMAPPNAME, "theme");
417     g.option_theme = s ? s : DEFTHEME;
418     msg(0, "icon theme: %s\n", g.option_theme);
419 
420     s = xresource_load_string(&db, XRMAPPNAME, "background");
421     g.color[COLBG].name = s ? s : DEFCOLBG;
422     s = xresource_load_string(&db, XRMAPPNAME, "foreground");
423     g.color[COLFG].name = s ? s : DEFCOLFG;
424     s = xresource_load_string(&db, XRMAPPNAME, "framecolor");
425     g.color[COLFRAME].name = s ? s : DEFCOLFRAME;
426 
427     s = xresource_load_string(&db, XRMAPPNAME, "font");
428     if (s) {
429         if ((strncmp(s, "xft:", 4) == 0)
430             && (*(s + 4) != '\0')) {
431             g.option_font = s + 4;
432         } else {
433             // resource may indeed be valid but non-xft
434             msg(-1, "invalid font: %s, using default: %s\n", s, DEFFONT);
435             g.option_font = DEFFONT + 4;
436         }
437     } else {
438         g.option_font = DEFFONT + 4;
439     }
440 
441 // max recursion for searching windows
442 // -1 is "everything"
443 // in raw X this returns too much windows, "1" is probably sufficient
444 // no need for an option
445     g.option_max_reclevel = (g.option_wm == WM_NO) ? 1 : -1;
446 
447     return 1;
448 }
449 
450 //
451 // grab Alt-Tab and Alt-Shift-Tab
452 // note: exit() on failure
453 //
grabKeysAtStartup(bool grabUngrab)454 static int grabKeysAtStartup(bool grabUngrab)
455 {
456     g.ignored_modmask = getOffendingModifiersMask(dpy); // or 0 for g.debug
457     char *grabhint =
458         "Error while (un)grabbing key 0x%x with mask 0x%x/0x%x.\nProbably other program already grabbed this combination.\nCheck: xdotool keydown alt+Tab; xdotool key XF86LogGrabInfo; xdotool keyup Tab; sleep 1; xdotool keyup alt\nand then look for active device grabs in /var/log/Xorg.0.log\nOr try Ctrl-Tab instead of Alt-Tab:  alttab -mk Control_L\n";
459 // attempt XF86Ungrab? probably too invasive
460     if (!changeKeygrab
461         (root, grabUngrab, g.option_keyCode, g.option_modMask,
462          g.ignored_modmask)) {
463         die(grabhint, g.option_keyCode, g.option_modMask, g.ignored_modmask);
464     }
465     if (!changeKeygrab
466         (root, grabUngrab, g.option_keyCode,
467          g.option_modMask | g.option_backMask, g.ignored_modmask)) {
468         die(grabhint, g.option_keyCode,
469             g.option_modMask | g.option_backMask, g.ignored_modmask);
470     }
471 
472     return 1;
473 }
474 
475 //
476 // Returns 0 if not an extra prev/next keycode, 1 if extra prev keycode, and 2 if extra next keycode.
477 //
isPrevNextKey(unsigned int keycode)478 static int isPrevNextKey(unsigned int keycode)
479 {
480     if (keycode == g.option_prevCode) {
481         return 1;
482     }
483     if (keycode == g.option_nextCode) {
484         return 2;
485     }
486     // if here then is neither
487     return 0;
488 }
489 
490 
main(int argc,char ** argv)491 int main(int argc, char **argv)
492 {
493 
494     XEvent ev;
495     dpy = XOpenDisplay(NULL);
496     if (!dpy)
497         die("can't open display");
498     scr = DefaultScreen(dpy);
499     root = DefaultRootWindow(dpy);
500 
501     ee_complain = true;
502     //hnd = (XErrorHandler)0;
503     XErrorHandler hnd = XSetErrorHandler(zeroErrorHandler); // for entire program
504     if (hnd) ;;                 // make -Wunused happy
505 
506     signal(SIGUSR1, sighandler);
507 
508     if (!use_args_and_xrm(&argc, argv))
509         die("use_args_and_xrm failed");
510     if (!startupWintasks())
511         die("startupWintasks failed");
512     if (!startupGUItasks())
513         die("startupGUItasks failed");
514 
515     grabKeysAtStartup(true);
516     g.uiShowHasRun = false;
517 
518     struct timespec nanots;
519     nanots.tv_sec = 0;
520     nanots.tv_nsec = 1E7;
521     char keys_pressed[32];
522     int octet = g.option_modCode / 8;
523     int kmask = 1 << (g.option_modCode - octet * 8);
524 
525     while (true) {
526         memset(&(ev.xkey), 0, sizeof(ev.xkey));
527 
528         if (g.uiShowHasRun) {
529             // poll: lag and consume cpu, but necessary because of bug #1 and #2
530             XQueryKeymap(dpy, keys_pressed);
531             if (!(keys_pressed[octet] & kmask)) {   // Alt released
532                 uiHide();
533                 continue;
534             }
535             if (!XCheckIfEvent(dpy, &ev, *predproc_true, NULL)) {
536                 nanosleep(&nanots, NULL);
537                 continue;
538             }
539         } else {
540             // event: immediate, when we don't care about Alt release
541             XNextEvent(dpy, &ev);
542         }
543 
544         switch (ev.type) {
545         case KeyPress:
546             msg(1, "Press %lx: %d-%d\n",
547                 ev.xkey.window, ev.xkey.state, ev.xkey.keycode);
548             if (ev.xkey.state & g.option_modMask) {  // alt
549                 if (ev.xkey.keycode == g.option_keyCode) {  // tab
550                     // additional check, see #97
551                     XQueryKeymap(dpy, keys_pressed);
552                     if (!(keys_pressed[octet] & kmask)) {
553                         msg(1, "Wrong modifier, skip event\n");
554                         continue;
555                     }
556                     if (!g.uiShowHasRun) {
557                         uiShow((ev.xkey.state & g.option_backMask));
558                     } else {
559                         if (ev.xkey.state & g.option_backMask) {
560                             uiPrevWindow();
561                         } else {
562                             uiNextWindow();
563                         }
564                     }
565                 } else if (ev.xkey.keycode == g.option_cancelCode) { // escape
566                     // additional check, see #97
567                     XQueryKeymap(dpy, keys_pressed);
568                     if (!(keys_pressed[octet] & kmask)) {
569                         msg(1, "Wrong modifier, skip event\n");
570                         continue;
571                     }
572                     uiSelectWindow(0);
573                 } else {  // non-tab
574                     switch (isPrevNextKey(ev.xkey.keycode)) {
575                     case 1:
576                         uiPrevWindow();
577                         break;
578                     case 2:
579                         uiNextWindow();
580                         break;
581                     }
582                 }
583             }
584             break;
585 
586         case KeyRelease:
587             msg(1, "Release %lx: %d-%d\n",
588                 ev.xkey.window, ev.xkey.state, ev.xkey.keycode);
589             // interested only in "final" release
590             if (!((ev.xkey.state & g.option_modMask)
591                   && ev.xkey.keycode == g.option_modCode && g.uiShowHasRun)) {
592                 break;
593             }
594             uiHide();
595             break;
596 
597         case Expose:
598             if (g.uiShowHasRun) {
599                 uiExpose();
600             }
601             break;
602 
603         case ButtonPress:
604         case ButtonRelease:
605             uiButtonEvent(ev.xbutton);
606             break;
607 
608         case PropertyNotify:
609             winPropChangeEvent(ev.xproperty);
610             break;
611 
612         case DestroyNotify:
613             winDestroyEvent(ev.xdestroywindow);
614             break;
615 
616         case FocusIn:
617             winFocusChangeEvent(ev.xfocus);
618             break;
619 
620         default:
621             msg(1, "Event type %d\n", ev.type);
622             break;
623         }
624 
625     }
626 
627 // this is probably never reached
628     shutdownWin();
629     shutdownGUI();
630     XrmDestroyDatabase(db);
631     grabKeysAtStartup(false);
632 // not restoring error handler
633     XCloseDisplay(dpy);
634     return 0;
635 } // main
636