thingmenu

A simple graphical menu launcher for X11.
git clone git://r-36.net/thingmenu
Log | Files | Refs | LICENSE

thingmenu.c (14152B)


      1 /*
      2  * Copy me if you can.
      3  * by 20h
      4  */
      5 #include <unistd.h>
      6 #include <locale.h>
      7 #include <signal.h>
      8 #include <stdarg.h>
      9 #include <stdio.h>
     10 #include <string.h>
     11 #include <stdlib.h>
     12 #include <libgen.h>
     13 #include <sys/wait.h>
     14 #include <X11/keysym.h>
     15 #include <X11/Xatom.h>
     16 #include <X11/Xlib.h>
     17 #include <X11/Xutil.h>
     18 #include <X11/Xproto.h>
     19 #include <X11/extensions/XTest.h>
     20 
     21 /* macros */
     22 #define MAX(a, b)       ((a) > (b) ? (a) : (b))
     23 #define LENGTH(x)       (sizeof x / sizeof x[0])
     24 
     25 /* enums */
     26 enum { ColFG, ColBG, ColLast };
     27 enum { NetWMWindowType, NetLast };
     28 
     29 /* typedefs */
     30 typedef unsigned int uint;
     31 typedef unsigned long ulong;
     32 
     33 typedef struct {
     34 	ulong norm[ColLast];
     35 	ulong press[ColLast];
     36 	ulong high[ColLast];
     37 
     38 	Drawable drawable;
     39 	GC gc;
     40 	struct {
     41 		int ascent;
     42 		int descent;
     43 		int height;
     44 		XFontSet set;
     45 		XFontStruct *xfont;
     46 	} font;
     47 } DC; /* draw context */
     48 
     49 typedef struct {
     50 	char *label;
     51 	char *cmd;
     52 	uint width;
     53 	int x, y, w, h;
     54 	Bool highlighted;
     55 	Bool pressed;
     56 	Bool forceexit;
     57 } Entry;
     58 
     59 /* function declarations */
     60 static void motionnotify(XEvent *e);
     61 static void keyrelease(XEvent *e);
     62 static void buttonpress(XEvent *e);
     63 static void buttonrelease(XEvent *e);
     64 static void cleanup(void);
     65 static void configurenotify(XEvent *e);
     66 static void unmapnotify(XEvent *e);
     67 static void die(const char *errstr, ...);
     68 static void drawmenu(void);
     69 static void drawentry(Entry *e);
     70 static void expose(XEvent *e);
     71 static Entry *findentry(int x, int y);
     72 static ulong getcolor(const char *colstr);
     73 static void initfont(const char *fontstr);
     74 static void leavenotify(XEvent *e);
     75 static void press(Entry *e);
     76 static void run(void);
     77 static void setup(void);
     78 static void sigchld(int unused);
     79 static int textnw(const char *text, uint len);
     80 static void unpress(Entry *e);
     81 static void updateentries(void);
     82 
     83 /* variables */
     84 static int screen;
     85 static void (*handler[LASTEvent]) (XEvent *) = {
     86 	[KeyRelease] = keyrelease,
     87 	[ButtonPress] = buttonpress,
     88 	[ButtonRelease] = buttonrelease,
     89 	[ConfigureNotify] = configurenotify,
     90 	[UnmapNotify] = unmapnotify,
     91 	[Expose] = expose,
     92 	[LeaveNotify] = leavenotify,
     93 	[MotionNotify] = motionnotify
     94 };
     95 
     96 static Display *dpy;
     97 static DC dc;
     98 static Window root, win;
     99 static Bool running = True, horizontal = False;
    100 /*
    101  * ww = window width; www = wanted window width; wh = window height;
    102  * wx = window x position; wy = window y position;
    103  */
    104 static int ww = 0, www = 0, wh = 0, wx = 0, wy = 0;
    105 static char *name = "thingmenu";
    106 
    107 Entry **entries = NULL;
    108 int nentries = 0;
    109 int oneshot = 1;
    110 Bool ispressing = 0;
    111 
    112 char *argv0;
    113 
    114 #include "arg.h"
    115 
    116 /* configuration, allows nested code to access above variables */
    117 #include "config.h"
    118 
    119 void
    120 motionnotify(XEvent *e)
    121 {
    122 	XPointerMovedEvent *ev = &e->xmotion;
    123 	int i;
    124 
    125 	for(i = 0; i < nentries; i++) {
    126 		if(ev->x > entries[i]->x
    127 				&& ev->x < entries[i]->x + entries[i]->w
    128 				&& ev->y > entries[i]->y
    129 				&& ev->y < entries[i]->y + entries[i]->h) {
    130 			if (entries[i]->highlighted != True) {
    131 				if (ispressing) {
    132 					entries[i]->pressed = True;
    133 				} else {
    134 					entries[i]->highlighted = True;
    135 				}
    136 				drawentry(entries[i]);
    137 			}
    138 			continue;
    139 		}
    140 		if (entries[i]->pressed == True) {
    141 			entries[i]->pressed = False;
    142 			drawentry(entries[i]);
    143 		}
    144 		if (entries[i]->highlighted == True) {
    145 			entries[i]->highlighted = False;
    146 			drawentry(entries[i]);
    147 		}
    148 	}
    149 }
    150 
    151 void
    152 keyrelease(XEvent *e)
    153 {
    154 	int i;
    155 	XKeyEvent *xkey = &e->xkey;
    156 	KeySym key = XLookupKeysym(xkey, 0);
    157 
    158 	for (i = 0; i < nentries && !entries[i]->highlighted; i++);
    159 
    160 	switch (key) {
    161 	case XK_k:
    162 		key = XK_Up;
    163 	case XK_j:
    164 		if(key == XK_j)
    165 			key = XK_Down;
    166 	case XK_Up:
    167 	case XK_Down:
    168 		if (i < nentries) {
    169 			entries[i]->highlighted = False;
    170 			drawentry(entries[i]);
    171 		}
    172 
    173 		if (key == XK_Up) {
    174 			i = ((i - 1) + nentries) % nentries;
    175 		} else if(key == XK_Down) {
    176 			if (i < nentries) {
    177 				i = (i + 1) % nentries;
    178 			} else {
    179 				i = 0;
    180 			}
    181 		}
    182 
    183 		entries[i]->highlighted = True;
    184 		drawentry(entries[i]);
    185 		break;
    186 	case XK_Return:
    187 	case XK_space:
    188 		if (i < nentries) {
    189 			press(entries[i]);
    190 			unpress(entries[i]);
    191 		}
    192 		break;
    193 	case XK_Escape:
    194 		running = False;
    195 		break;
    196 	}
    197 }
    198 
    199 void
    200 buttonpress(XEvent *e)
    201 {
    202 	XButtonPressedEvent *ev = &e->xbutton;
    203 	Entry *en;
    204 
    205 	if(ev->button != Button1)
    206 		return;
    207 
    208 	ispressing = True;
    209 
    210 	if((en = findentry(ev->x, ev->y)))
    211 		press(en);
    212 }
    213 
    214 void
    215 buttonrelease(XEvent *e)
    216 {
    217 	XButtonPressedEvent *ev = &e->xbutton;
    218 	Entry *en;
    219 
    220 	if(ev->button != Button1)
    221 		return;
    222 
    223 	ispressing = False;
    224 
    225 	if((en = findentry(ev->x, ev->y)))
    226 		unpress(en);
    227 }
    228 
    229 void
    230 cleanup(void)
    231 {
    232 	if(dc.font.set)
    233 		XFreeFontSet(dpy, dc.font.set);
    234 	else
    235 		XFreeFont(dpy, dc.font.xfont);
    236 	XFreePixmap(dpy, dc.drawable);
    237 	XFreeGC(dpy, dc.gc);
    238 	XDestroyWindow(dpy, win);
    239 	XSync(dpy, False);
    240 	XSetInputFocus(dpy, PointerRoot, RevertToPointerRoot, CurrentTime);
    241 }
    242 
    243 void
    244 configurenotify(XEvent *e)
    245 {
    246 	XConfigureEvent *ev = &e->xconfigure;
    247 
    248 	if(ev->window == win && (ev->width != ww || ev->height != wh)) {
    249 		ww = ev->width;
    250 		wh = ev->height;
    251 		XFreePixmap(dpy, dc.drawable);
    252 		dc.drawable = XCreatePixmap(dpy, root, ww, wh,
    253 				DefaultDepth(dpy, screen));
    254 		updateentries();
    255 	}
    256 }
    257 
    258 void
    259 die(const char *errstr, ...)
    260 {
    261 	va_list ap;
    262 
    263 	va_start(ap, errstr);
    264 	vfprintf(stderr, errstr, ap);
    265 	va_end(ap);
    266 	exit(EXIT_FAILURE);
    267 }
    268 
    269 void
    270 drawmenu(void)
    271 {
    272 	int i;
    273 
    274 	for(i = 0; i < nentries; i++)
    275 		drawentry(entries[i]);
    276 	XSync(dpy, False);
    277 }
    278 
    279 void
    280 drawentry(Entry *e)
    281 {
    282 	int x, y, h, len;
    283 	XRectangle r = { e->x, e->y, e->w, e->h };
    284 	const char *l;
    285 	ulong *col;
    286 
    287 	if(e->pressed)
    288 		col = dc.press;
    289 	else if(e->highlighted)
    290 		col = dc.high;
    291 	else
    292 		col = dc.norm;
    293 
    294 	XSetForeground(dpy, dc.gc, col[ColBG]);
    295 	XFillRectangles(dpy, dc.drawable, dc.gc, &r, 1);
    296 	XSetForeground(dpy, dc.gc, dc.norm[ColFG]);
    297 	r.height -= 1;
    298 	r.width -= 1;
    299 	XDrawRectangles(dpy, dc.drawable, dc.gc, &r, 1);
    300 	XSetForeground(dpy, dc.gc, col[ColFG]);
    301 
    302 	l = e->label;
    303 	len = strlen(l);
    304 	h = dc.font.height;
    305 	y = e->y + (e->h / 2) - (h / 2) + dc.font.ascent;
    306 	x = e->x + (e->w / 2) - (textnw(l, len) / 2);
    307 	if(dc.font.set) {
    308 		XmbDrawString(dpy, dc.drawable, dc.font.set, dc.gc, x, y, l,
    309 				len);
    310 	} else
    311 		XDrawString(dpy, dc.drawable, dc.gc, x, y, l, len);
    312 	XCopyArea(dpy, dc.drawable, win, dc.gc, e->x, e->y, e->w, e->h,
    313 			e->x, e->y);
    314 }
    315 
    316 void
    317 unmapnotify(XEvent *e)
    318 {
    319 	running = False;
    320 }
    321 
    322 void
    323 expose(XEvent *e)
    324 {
    325 	XExposeEvent *ev = &e->xexpose;
    326 
    327 	if(ev->count == 0 && (ev->window == win))
    328 		drawmenu();
    329 }
    330 
    331 Entry *
    332 findentry(int x, int y)
    333 {
    334 	int i;
    335 
    336 	for(i = 0; i < nentries; i++) {
    337 		if(x > entries[i]->x && x < entries[i]->x + entries[i]->w
    338 				&& y > entries[i]->y
    339 				&& y < entries[i]->y + entries[i]->h) {
    340 			return entries[i];
    341 		}
    342 	}
    343 	return NULL;
    344 }
    345 
    346 ulong
    347 getcolor(const char *colstr)
    348 {
    349 	Colormap cmap = DefaultColormap(dpy, screen);
    350 	XColor color;
    351 
    352 	if(!XAllocNamedColor(dpy, cmap, colstr, &color, &color))
    353 		die("error, cannot allocate color '%s'\n", colstr);
    354 	return color.pixel;
    355 }
    356 
    357 void
    358 initfont(const char *fontstr)
    359 {
    360 	char *def, **missing;
    361 	int i, n;
    362 
    363 	missing = NULL;
    364 	if(dc.font.set)
    365 		XFreeFontSet(dpy, dc.font.set);
    366 	dc.font.set = XCreateFontSet(dpy, fontstr, &missing, &n, &def);
    367 	if(missing) {
    368 		while(n--) {
    369 			fprintf(stderr, "thingmenu: missing fontset: %s\n",
    370 					missing[n]);
    371 		}
    372 		XFreeStringList(missing);
    373 	}
    374 	if(dc.font.set) {
    375 		XFontStruct **xfonts;
    376 		char **font_names;
    377 		dc.font.ascent = dc.font.descent = 0;
    378 		n = XFontsOfFontSet(dc.font.set, &xfonts, &font_names);
    379 		for(i = 0, dc.font.ascent = 0, dc.font.descent = 0; i < n; i++) {
    380 			dc.font.ascent = MAX(dc.font.ascent, (*xfonts)->ascent);
    381 			dc.font.descent = MAX(dc.font.descent,(*xfonts)->descent);
    382 			xfonts++;
    383 		}
    384 	}
    385 	else {
    386 		if(dc.font.xfont)
    387 			XFreeFont(dpy, dc.font.xfont);
    388 		dc.font.xfont = NULL;
    389 		if(!(dc.font.xfont = XLoadQueryFont(dpy, fontstr))
    390 		&& !(dc.font.xfont = XLoadQueryFont(dpy, "fixed")))
    391 			die("error, cannot load font: '%s'\n", fontstr);
    392 		dc.font.ascent = dc.font.xfont->ascent;
    393 		dc.font.descent = dc.font.xfont->descent;
    394 	}
    395 	dc.font.height = dc.font.ascent + dc.font.descent;
    396 }
    397 
    398 void
    399 leavenotify(XEvent *e)
    400 {
    401 	unpress(NULL);
    402 }
    403 
    404 void
    405 run(void)
    406 {
    407 	XEvent ev;
    408 
    409 	/* main event loop */
    410 	XSync(dpy, False);
    411 	while(running) {
    412 		XNextEvent(dpy, &ev);
    413 		if(handler[ev.type])
    414 			(handler[ev.type])(&ev); /* call handler */
    415 	}
    416 }
    417 
    418 void
    419 setup(void)
    420 {
    421 	XSetWindowAttributes wa;
    422 	XTextProperty str;
    423 	XSizeHints *sizeh;
    424 	XClassHint *ch;
    425 	int i, sh, sw, ls;
    426 
    427 	/* clean up any zombies immediately */
    428 	sigchld(0);
    429 
    430 	/* init screen */
    431 	screen = DefaultScreen(dpy);
    432 	root = RootWindow(dpy, screen);
    433 	sw = DisplayWidth(dpy, screen) - 1;
    434 	sh = DisplayHeight(dpy, screen) - 1;
    435 	initfont(font);
    436 
    437 	/* init atoms */
    438 
    439 	/* init appearance */
    440 
    441 	for (i = 0, www = 0; i < nentries; i++) {
    442 		ls = textnw(entries[i]->label,
    443 				strlen(entries[i]->label));
    444 		if (ls > www)
    445 			www = ls;
    446 	}
    447 	www *= widthscaling;
    448 
    449 	if (!ww) {
    450 		if (horizontal) {
    451 			ww = www * nentries;
    452 		} else {
    453 			ww = www;
    454 		}
    455 	}
    456 	if (!wh) {
    457 		if (horizontal) {
    458 			wh = dc.font.height * heightscaling;
    459 		} else {
    460 			wh = nentries * dc.font.height * heightscaling;
    461 		}
    462 	}
    463 	if (!wy)
    464 		wy = (sh - wh) / 2;
    465 	if (wy < 0)
    466 		wy = sh + wy - wh;
    467 	if (!wx)
    468 		wx = (sw - ww) / 2;
    469 	if (wx < 0)
    470 		wx = sw + wx - ww;
    471 
    472 	dc.norm[ColBG] = getcolor(normbgcolor);
    473 	dc.norm[ColFG] = getcolor(normfgcolor);
    474 	dc.press[ColBG] = getcolor(pressbgcolor);
    475 	dc.press[ColFG] = getcolor(pressfgcolor);
    476 	dc.high[ColBG] = getcolor(highlightbgcolor);
    477 	dc.high[ColFG] = getcolor(highlightfgcolor);
    478 
    479 	dc.drawable = XCreatePixmap(dpy, root, ww, wh, DefaultDepth(dpy, screen));
    480 	dc.gc = XCreateGC(dpy, root, 0, 0);
    481 	if(!dc.font.set)
    482 		XSetFont(dpy, dc.gc, dc.font.xfont->fid);
    483 	for(i = 0; i < nentries; i++)
    484 		entries[i]->pressed = 0;
    485 
    486 	wa.override_redirect = !wmborder;
    487 	wa.border_pixel = dc.norm[ColFG];
    488 	wa.background_pixel = dc.norm[ColBG];
    489 	win = XCreateWindow(dpy, root, wx, wy, ww, wh, 0,
    490 			    CopyFromParent, CopyFromParent, CopyFromParent,
    491 			    CWOverrideRedirect | CWBorderPixel | CWBackingPixel, &wa);
    492 	XSelectInput(dpy, win, StructureNotifyMask|KeyReleaseMask|
    493 	                       ButtonReleaseMask|ButtonPressMask|
    494 	                       ExposureMask|LeaveWindowMask|PointerMotionMask);
    495 
    496 	sizeh = XAllocSizeHints();
    497 	sizeh->flags = PMaxSize | PMinSize;
    498 	sizeh->min_width = sizeh->max_width = ww;
    499 	sizeh->min_height = sizeh->max_height = wh;
    500 	XStringListToTextProperty(&name, 1, &str);
    501 	ch = XAllocClassHint();
    502 	ch->res_class = name;
    503 	ch->res_name = name;
    504 
    505 	XSetWMProperties(dpy, win, &str, &str, NULL, 0, sizeh, NULL,
    506 			ch);
    507 
    508 	XFree(ch);
    509 	XFree(str.value);
    510 	XFree(sizeh);
    511 
    512 	XMapRaised(dpy, win);
    513 	updateentries();
    514 	drawmenu();
    515 }
    516 
    517 void
    518 sigchld(int unused)
    519 {
    520 	if (signal(SIGCHLD, sigchld) == SIG_ERR)
    521 		die("can't install SIGCHLD handler:");
    522 	while (0 < waitpid(-1, NULL, WNOHANG));
    523 }
    524 
    525 int
    526 textnw(const char *text, uint len)
    527 {
    528 	XRectangle r;
    529 
    530 	if(dc.font.set) {
    531 		XmbTextExtents(dc.font.set, text, len, NULL, &r);
    532 		return r.width;
    533 	}
    534 	return XTextWidth(dc.font.xfont, text, len);
    535 }
    536 
    537 void
    538 runentry(Entry *e)
    539 {
    540 	char *shell;
    541 
    542 	if (oneshot || e->forceexit)
    543 		running = False;
    544 
    545 	switch (fork()) {
    546 	case -1:
    547 		break;
    548 	case 0:
    549 		shell = getenv("SHELL");
    550 		if (!shell)
    551 			shell = "/bin/sh";
    552 
    553 		execlp(shell, basename(shell), "-c", e->cmd, (char *)NULL);
    554 		break;
    555 	}
    556 }
    557 
    558 void
    559 press(Entry *e)
    560 {
    561 	e->pressed = !e->pressed;
    562 
    563 	drawentry(e);
    564 }
    565 
    566 void
    567 unpress(Entry *e)
    568 {
    569 	int i;
    570 
    571 	if (e != NULL) {
    572 		e->pressed = !e->pressed;
    573 
    574 		runentry(e);
    575 		drawentry(e);
    576 	} else {
    577 		for(i = 0; i < nentries; i++) {
    578 			if(entries[i]->pressed) {
    579 				entries[i]->pressed = 0;
    580 				drawentry(entries[i]);
    581 			}
    582 		}
    583 	}
    584 }
    585 
    586 void
    587 updateentries(void)
    588 {
    589 	int i, x, y, h, w;
    590 
    591 	x = 0;
    592 	y = 0;
    593 
    594 	if (horizontal) {
    595 		h = wh;
    596 		w = www;
    597 	} else {
    598 		h = wh / nentries;
    599 		w = ww;
    600 	}
    601 	for(i = 0; i < nentries; i++) {
    602 		entries[i]->x = x;
    603 		entries[i]->y = y;
    604 		entries[i]->w = w;
    605 		entries[i]->h = h;
    606 		if (horizontal) {
    607 			x += w;
    608 		} else {
    609 			y += h;
    610 		}
    611 	}
    612 }
    613 
    614 void
    615 usage(void)
    616 {
    617 	fprintf(stderr, "usage: %s [-hxso] [-g geometry] [-w widthscaling] "
    618 			"[-e heightscaling] [--] "
    619 			"label0 cmd0 [label1 cmd1 ...]\n", argv0);
    620 	exit(1);
    621 }
    622 
    623 int
    624 main(int argc, char *argv[])
    625 {
    626 	Bool addexit;
    627 	char *label, *cmd;
    628 	int i, xr, yr, bitm;
    629 	unsigned int wr, hr;
    630 
    631 	argv0 = argv[0];
    632 
    633 	addexit = True;
    634 
    635 	if (argc < 2)
    636 		usage();
    637 
    638 	ARGBEGIN {
    639 	case 'g':
    640 		bitm = XParseGeometry(EARGF(usage()), &xr, &yr, &wr, &hr);
    641 		if (bitm & XValue)
    642 			wx = xr;
    643 		if (bitm & YValue)
    644 			wy = yr;
    645 		if (bitm & WidthValue)
    646 			ww = (int)wr;
    647 		if (bitm & HeightValue)
    648 			wh = (int)hr;
    649 		if (bitm & XNegative && wx == 0)
    650 			wx = -1;
    651 		if (bitm & YNegative && wy == 0)
    652 			wy = -1;
    653 		break;
    654 	case 'e':
    655 		heightscaling = atof(EARGF(usage()));
    656 		break;
    657 	case 'o':
    658 		horizontal = True;
    659 		break;
    660 	case 's':
    661 		oneshot = 0;
    662 		break;
    663 	case 'w':
    664 		widthscaling = atof(EARGF(usage()));
    665 		break;
    666 	case 'x':
    667 		addexit = False;
    668 		break;
    669 	default:
    670 		usage();
    671 	} ARGEND;
    672 
    673 	for (i = 0; argv[i]; i++) {
    674 		label = argv[i];
    675 		if (!argv[i+1])
    676 			break;
    677 		i++;
    678 		cmd = argv[i];
    679 
    680 		if (!(entries = realloc(entries, sizeof(entries[0])*(++nentries))))
    681 			die("realloc returned NULL");
    682 		if (!(entries[nentries-1] = calloc(1, sizeof(*entries[0]))))
    683 			die("calloc returned NULL");
    684 		if (!(entries[nentries-1]->label = strdup(label)))
    685 			die("strdup returned NULL\n");
    686 		if (!(entries[nentries-1]->cmd = strdup(cmd)))
    687 			die("strdup returned NULL\n");
    688 		entries[nentries-1]->forceexit = False;
    689 	}
    690 	if (nentries < 1)
    691 		usage();
    692 
    693 	if (addexit) {
    694 		if (!(entries = realloc(entries, sizeof(entries[0])*(++nentries))))
    695 			die("realloc returned NULL");
    696 		if (!(entries[nentries-1] = calloc(1, sizeof(*entries[0]))))
    697 			die("calloc returned NULL");
    698 		if (!(entries[nentries-1]->label = strdup("cancel")))
    699 			die("strdup returned NULL\n");
    700 		if (!(entries[nentries-1]->cmd = strdup("exit")))
    701 			die("strdup returned NULL\n");
    702 		entries[nentries-1]->forceexit = True;
    703 	}
    704 
    705 	if(!setlocale(LC_CTYPE, "") || !XSupportsLocale())
    706 		fprintf(stderr, "warning: no locale support\n");
    707 	if(!(dpy = XOpenDisplay(0)))
    708 		die("thingmenu: cannot open display\n");
    709 
    710 	setup();
    711 	run();
    712 	cleanup();
    713 	XCloseDisplay(dpy);
    714 
    715 	for (i = 0; i < nentries; i++) {
    716 		free(entries[i]->label);
    717 		free(entries[i]->cmd);
    718 		free(entries[i]);
    719 	}
    720 	free(entries);
    721 
    722 	return 0;
    723 }
    724