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