Blob


1 #include <ctype.h>
2 #include <errno.h>
3 #include <stdarg.h>
4 #include <stdbool.h>
5 #include <stdio.h>
6 #include <stdlib.h>
7 #include <string.h>
8 #include <time.h>
9 #include <unistd.h>
11 #include <X11/Xlib.h>
12 #include <X11/Xutil.h>
13 #include <X11/Xos.h>
14 #include <X11/Xatom.h>
15 #include <X11/Xft/Xft.h>
17 #define PROGNAME "xitems"
18 #define MAXKS 32
20 /* usage -- print usage information and die. */
21 static void
22 usage(void)
23 {
24 printf(
25 "usage: " PROGNAME " [-font font] [-bg colour] [-fg colour]\n"
26 " [-sbg colour] [-sfg colour] [-bc colour] [-bw width]\n"
27 " [-hp padding] [-vp padding] [-x x] [-y y]\n");
28 exit(1);
29 }
31 /* die -- print formatted string, and exit with the status eval. */
32 static void
33 die(int eval, const char *fmt, ...)
34 {
35 fputs(PROGNAME ": ", stderr);
36 if (fmt) {
37 va_list argp;
38 va_start(argp, fmt);
39 vfprintf(stderr, fmt, argp);
40 va_end(argp);
41 }
42 fputc('\n', stderr);
43 exit(eval);
44 }
46 /* warn -- print formatted string to stderr. */
47 static void
48 warn(const char *fmt, ...)
49 {
50 fputs(PROGNAME ": ", stderr);
51 if (fmt) {
52 va_list argp;
53 va_start(argp, fmt);
54 vfprintf(stderr, fmt, argp);
55 va_end(argp);
56 }
57 fputc('\n', stderr);
58 }
60 enum direction {
61 DIR_UP,
62 DIR_DOWN
63 };
65 /* doubly-linked cyclic list */
66 struct item {
67 struct item *prev;
68 struct item *next;
69 char *s;
70 KeySym ks[MAXKS]; /* array of associated keysyms */
71 size_t len; /* length of string */
72 size_t nks; /* number of associated keysyms */
73 bool dirty; /* should be redrawn */
74 };
76 static struct item *first = NULL; /* first member of the list */
77 static struct item *selected = NULL;
79 /* command-line options and X resources */
80 static char *o_font = NULL;
81 static char *o_bg = NULL, *o_fg = NULL, *o_sbg = NULL, *o_sfg = NULL,
82 *o_bc = NULL;
83 static int o_x = -1, o_y = -1;
84 static int o_bw = -1;
85 static int o_hp = -1, o_vp = -1;
87 /* X globals */
88 static Display *dpy = NULL;
89 static int screen;
90 static Window win;
91 static int height, width; /* height and width of one item */
92 static XftFont *font;
93 static XftColor c_fg, c_sfg, c_sbg;
95 /* selpos -- mark the item at position y (from top) as selected. */
96 static void
97 selpos(int y)
98 {
99 struct item *unselected = selected;
100 int top = 1;
102 selected = first;
104 if (y <= 0)
105 goto end;
107 for (selected = first; selected != first->prev;
108 selected = selected->next) {
109 if (y >= top && y < top+height)
110 break;
111 top += height;
114 end:
115 if (unselected != selected)
116 unselected->dirty = selected->dirty = true;
119 /* redraw -- redraw the entire window */
120 static void
121 redraw(void)
123 struct item *it = first;
124 int y = 0;
126 static XftDraw *d = NULL;
128 if (!d && !(d = XftDrawCreate(dpy, win, DefaultVisual(dpy, screen),
129 DefaultColormap(dpy, screen))))
130 die(1, "couldn't create XftDraw");
132 do {
133 if (it->dirty) {
134 XftColor *xftc;
136 XClearArea(dpy, win, 0, y, width, height, False);
138 if (it == selected) {
139 xftc = &c_sfg;
140 XftDrawRect(d, &c_sbg, 0, y, width, height);
141 } else
142 xftc = &c_fg;
144 XftDrawStringUtf8(d, xftc, font,
145 o_hp, y + o_vp+font->ascent,
146 (FcChar8 *)it->s, it->len);
148 it->dirty = false;
151 y += height;
152 it = it->next;
153 } while (it != first);
156 /* succeed -- exit successfully. If print is true, also print selected item. */
157 static void
158 succeed(bool print)
160 if (print && selected)
161 puts(selected->s);
162 exit(0);
165 /* scroll -- select the previous, or next item, depending on dir. */
166 static void
167 scroll(enum direction dir)
169 if (selected) {
170 selected->dirty = true;
171 selected = (dir == DIR_UP) ? selected->prev : selected->next;
172 } else
173 selected = (dir == DIR_UP) ? first->prev : first;
174 selected->dirty = true;
177 /*
178 * keyselectd -- compare ks with keysyms stored in the items list, and mark
179 * the first match as selected. Return true on match, and false otherwise.
180 */
181 static bool
182 keyselect(KeySym ks)
184 struct item *it = first;
185 KeySym k, dummy;
187 XConvertCase(ks, &k, &dummy);
189 do {
190 size_t i;
191 for (i = 0; i < it->nks; ++i)
192 if (it->ks[i] == k) {
193 selected->dirty = true;
194 selected = it;
195 selected->dirty = true;
196 return true;
198 it = it->next;
199 } while (it != first);
201 return false;
204 /* proc -- body of the main event-reading loop. */
205 static void
206 proc(void)
208 XKeyEvent ke;
209 KeySym ks;
210 char *dummy = "";
211 XEvent ev;
213 static bool inwin = false;
215 XNextEvent(dpy, &ev);
217 switch (ev.type) {
218 case EnterNotify:
219 inwin = true;
220 /* FALLTHRU */
221 case MotionNotify:
222 selpos(ev.xbutton.y);
223 /* FALLTHRU */
224 case Expose:
225 redraw();
226 break;
227 case LeaveNotify:
228 inwin = false;
229 break;
230 case ButtonPress:
231 if (ev.xbutton.button == Button4) {
232 scroll(DIR_UP);
233 redraw();
234 } else if (ev.xbutton.button == Button5) {
235 scroll(DIR_DOWN);
236 redraw();
237 } else if (inwin)
238 succeed(true);
239 else
240 succeed(false);
241 break;
242 case KeyPress:
243 ke = ev.xkey;
244 XLookupString(&ke, dummy, 0, &ks, NULL);
246 if (ke.state & ControlMask) {
247 switch (ks) {
248 case XK_bracketleft:
249 case XK_C:
250 case XK_c:
251 ks = XK_Escape;
252 break;
253 case XK_M:
254 case XK_m:
255 case XK_J:
256 case XK_j:
257 ks = XK_Return;
258 break;
259 case XK_N:
260 case XK_n:
261 ks = XK_j;
262 return;
263 case XK_P:
264 case XK_p:
265 ks = XK_k;
266 return;
268 } else if (keyselect(ks))
269 succeed(true);
271 switch (ks) {
272 case XK_j:
273 case XK_J:
274 case XK_Down:
275 scroll(DIR_DOWN);
276 redraw();
277 break;
278 case XK_k:
279 case XK_K:
280 case XK_Up:
281 scroll(DIR_UP);
282 redraw();
283 break;
284 case XK_Return:
285 succeed(true);
286 /* NOTREACHED */
287 case XK_Escape:
288 succeed(false);
289 /* NOTREACHED */
292 break;
296 /* grabptr -- try to grab pointer for a second */
297 static void
298 grabptr(void)
300 struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000 };
301 int i;
303 for (i = 0; i < 1000; ++i) {
304 if (XGrabPointer(dpy, RootWindow(dpy, screen), True,
305 ButtonPressMask, GrabModeAsync, GrabModeAsync, None, None,
306 CurrentTime) == GrabSuccess)
307 return;
308 nanosleep(&ts, NULL);
310 die(1, "couldn't grab pointer");
313 /* grabkb -- try to grab keyboard for a second */
314 static void
315 grabkb(void)
317 struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000 };
318 int i;
320 for (i = 0; i < 1000; ++i) {
321 if (XGrabKeyboard(dpy, RootWindow(dpy, screen), True,
322 GrabModeAsync, GrabModeAsync, CurrentTime) == GrabSuccess)
323 return;
324 nanosleep(&ts, NULL);
326 die(1, "couldn't grab keyboard");
329 /* setfocus -- try setting focus to the menu for a second */
330 static void
331 setfocus(void)
333 struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000 };
334 Window focuswin;
335 int i, dummy;
337 for (i = 0; i < 1000; ++i) {
338 XGetInputFocus(dpy, &focuswin, &dummy);
339 if (focuswin == win)
340 return;
341 XSetInputFocus(dpy, win, RevertToParent, CurrentTime);
342 nanosleep(&ts, NULL);
344 die(1, "couldn't grab keyboard");
347 /* alloccol_xft -- safely allocate new Xft colour c from string s. */
348 static void
349 alloccol_xft(char *s, XftColor *c)
351 if (!(XftColorAllocName(dpy, DefaultVisual(dpy, screen),
352 DefaultColormap(dpy, screen), s, c)))
353 die(1, "couldn't allocate Xft colour %s\n", s);
356 /* alloccol -- safely allocate new colour c from string s. */
357 static void
358 alloccol(char *s, XColor *c)
360 XColor dummy;
361 if (!(XAllocNamedColor(dpy, DefaultColormap(dpy, screen), s, c, &dummy)))
362 die(1, "couldn't allocate colour %s\n", s);
365 /*
366 * setupx -- create and map a window for n items; assign values to the X
367 * globals.
368 */
369 static void
370 setupx(int n)
372 struct item *it;
373 XColor col;
374 XClassHint ch = {PROGNAME, PROGNAME};
375 XSetWindowAttributes swa = {
376 .override_redirect = True,
377 .save_under = True,
378 .event_mask = ExposureMask | StructureNotifyMask |
379 KeyPressMask | ButtonPressMask | ButtonReleaseMask |
380 PointerMotionMask | LeaveWindowMask | EnterWindowMask,
381 };
383 if (!(font = XftFontOpenName(dpy, screen, o_font)))
384 die(1, "couldn't load font");
385 height = font->height + o_vp;
387 it = first;
388 do {
389 XGlyphInfo gi;
390 XftTextExtentsUtf8(dpy, font, (FcChar8 *)it->s, it->len, &gi);
391 if (gi.xOff + o_hp*2 > width)
392 width = gi.xOff + o_hp*2;
393 it = it->next;
394 } while (it != first);
396 if (o_x + width > DisplayWidth(dpy, screen))
397 o_x = DisplayWidth(dpy, screen) - width;
398 if (o_y + height*n > DisplayHeight(dpy, screen))
399 o_y = DisplayHeight(dpy, screen) - height*n;
401 alloccol(o_bc, &col);
402 swa.border_pixel = col.pixel;
403 alloccol(o_bg, &col);
404 swa.background_pixel = col.pixel;
406 win = XCreateWindow(dpy, RootWindow(dpy, screen), o_x, o_y,
407 width, n*height, o_bw, CopyFromParent, CopyFromParent,
408 CopyFromParent, CWOverrideRedirect | CWBackPixel | CWBorderPixel |
409 CWEventMask | CWSaveUnder, &swa);
410 XSetClassHint(dpy, win, &ch);
412 alloccol_xft(o_fg, &c_fg);
413 alloccol_xft(o_sfg, &c_sfg);
414 alloccol_xft(o_sbg, &c_sbg);
416 grabkb();
417 grabptr();
419 XMapRaised(dpy, win);
421 setfocus();
424 /*
425 * Create a new struct item, and insert it a after the it item. The rest of the
426 * arguments are values that the new item receives. On success return pointer
427 * to the new item. Return NULL otherwise.
428 */
429 static struct item *
430 insitem(struct item *it, char *s) {
431 struct item *new;
432 char *p, *end;
434 if (!(new = calloc(1, sizeof *new)))
435 return NULL;
436 if (it) {
437 new->prev = it;
438 new->next = it->next;
439 it->next->prev = new;
440 it->next = new;
441 } else
442 new->prev = new->next = new;
443 new->nks = 0;
444 new->dirty = true;
446 for (p = s; ; p = end) {
447 size_t n;
448 char c;
449 KeySym ks;
451 n = strspn(p, " ");
452 p = p + n;
453 if (!(n = strcspn(p, " \t"))) {
454 p += strspn(p, " \t");
455 break;
458 end = p + n;
459 c = *end;
460 *end = '\0';
462 if ((ks = XStringToKeysym(p)) == NoSymbol)
463 warn("no such keysym: %s\n", p);
464 else if (new->nks >= MAXKS)
465 warn("too many keysyms (%s)\n", p);
466 else {
467 KeySym k, dummy;
468 XConvertCase(ks, &k, &dummy);
469 new->ks[new->nks++] = k;
472 *end = c;
475 new->len = strlen(p);
476 new->s = strdup(p);
478 return new;
481 /*
482 * mkitems -- create a list of items from stdin, and return pointer to the
483 * new list's first element. If np is not NULL, set its value to the number of
484 * elements in the list. Return NULL on error.
485 */
486 static struct item *
487 mkitems(int *np)
489 struct item *last = NULL;
490 size_t n = 0;
491 char *line = NULL;
492 size_t linesize = 0;
493 ssize_t linelen;
495 while ((linelen = getline(&line, &linesize, stdin)) != -1) {
496 struct item *it;
497 if (line[linelen-1] == '\n')
498 line[--linelen] = '\0';
499 if (!(it = insitem(last, line)))
500 die(1, "couldn't insert new item");
501 n++;
502 last = it;
504 free(line);
506 if (np)
507 *np = n;
509 return last ? last->next : NULL;
512 /*
513 * {s,i}default -- return the {char *,int} value of the X default, or def
514 * otherwise.
515 */
516 static char *
517 sdefault(const char *opt, const char *def)
519 char *val = XGetDefault(dpy, PROGNAME, opt);
520 return val ? val : (char *)def;
523 static int
524 idefault(const char *opt, int def)
526 char *val = XGetDefault(dpy, PROGNAME, opt);
527 return val ? atoi(val) : def;
530 /* nextarg -- safely skip the current command-line option */
531 static void
532 nextarg(int *argcp, char **argvp[])
534 if (--*argcp <= 0)
535 usage();
536 ++*argvp;
539 /*
540 * {s,i}arg -- return the {char *,int} argument of the current command-line
541 * option.
542 */
543 static char *
544 sarg(int *argcp, char **argvp[])
546 nextarg(argcp, argvp);
547 return **argvp;
550 static int
551 iarg(int *argcp, char **argvp[])
553 nextarg(argcp, argvp);
554 return atoi(**argvp);
557 /*
558 * xitems -- pop-up menu for X, constructed from stdin, and printing user choice
559 * to stdout.
560 */
561 int
562 main(int argc, char *argv[])
564 int n = 0;
566 for (--argc, ++argv; argc > 0; --argc, ++argv)
567 if (argv[0][0] == '-')
568 switch (argv[0][1]) {
569 case 'b':
570 switch (argv[0][2]) {
571 case 'c': /* -bc */
572 o_bc = sarg(&argc, &argv);
573 break;
574 case 'g': /* -bg */
575 o_bg = sarg(&argc, &argv);
576 break;
577 case 'w': /* -bw */
578 o_bw = iarg(&argc, &argv);
579 break;
580 default:
581 usage();
582 /* NOTREACHED */
584 break;
585 case 'f':
586 switch (argv[0][2]) {
587 case 'g': /* -fg */
588 o_fg = sarg(&argc, &argv);
589 break;
590 case 'o': /* -font */
591 o_font = sarg(&argc, &argv);
592 break;
593 default:
594 usage();
595 /* NOTREACHED */
597 break;
598 case 'h': /* -hp */
599 o_hp = iarg(&argc, &argv);
600 break;
601 case 's':
602 switch (argv[0][2]) {
603 case 'b': /* -sbg */
604 o_sbg = sarg(&argc, &argv);
605 break;
606 case 'f': /* -sfg */
607 o_sfg = sarg(&argc, &argv);
608 break;
609 default:
610 usage();
611 /* NOTREACHED */
613 break;
614 case 'v': /* -vp */
615 o_vp = iarg(&argc, &argv);
616 break;
617 case 'x': /* -x */
618 o_x = iarg(&argc, &argv);
619 break;
620 case 'y': /* -y */
621 o_y = iarg(&argc, &argv);
622 break;
623 default:
624 usage();
625 /* NOTREACHED */
628 if (!(dpy = XOpenDisplay(NULL)))
629 die(1, "couldn't open display");
630 screen = DefaultScreen(dpy);
632 if (!o_bg)
633 o_bg = sdefault("background", "white");
634 if (!o_fg)
635 o_fg = sdefault("foreground", "black");
636 if (!o_font)
637 o_font = sdefault("font", "DejaVu Sans Mono-10");
638 if (!o_sbg)
639 o_sbg = sdefault("selectedBackground", "black");
640 if (!o_sfg)
641 o_sfg = sdefault("selectedForeground", "white");
642 if (!o_bc)
643 o_bc = sdefault("borderColour", "black");
644 if (o_bw == -1)
645 o_bw = idefault("borderWidth", 1);
646 if (o_hp == -1)
647 o_hp = idefault("horizontalPadding", 2);
648 if (o_vp == -1)
649 o_vp = idefault("verticalPadding", 1);
651 if (o_x == -1 || o_y == -1) {
652 Window w;
653 int i;
654 unsigned int ui;
655 int *xp, *yp;
657 xp = (o_x == -1) ? &o_x : &i;
658 yp = (o_y == -1) ? &o_y : &i;
660 XQueryPointer(dpy, RootWindow(dpy, screen), &w, &w, xp, yp, &i,
661 &i, &ui);
664 if (!(first = selected = mkitems(&n))) {
665 if (n)
666 die(1, "coulnd't create items list");
667 else
668 exit(0);
670 setupx(n);
672 for (;;)
673 proc();
674 /* NOTREACHED */