5f81d253d4c50f3e9ab2891055278f1edd3b45c4
[dmenu.git] / dmenu.c
1 /* See LICENSE file for copyright and license details. */
2 #include <ctype.h>
3 #include <locale.h>
4 #include <stdarg.h>
5 #include <stdlib.h>
6 #include <stdio.h>
7 #include <string.h>
8 #include <unistd.h>
9 #include <X11/Xlib.h>
10 #include <X11/Xutil.h>
11 #include <X11/keysym.h>
12
13 /* macros */
14 #define CLEANMASK(mask) (mask & ~(numlockmask | LockMask))
15
16 /* enums */
17 enum { ColFG, ColBG, ColLast };
18
19 /* typedefs */
20 typedef struct {
21         int x, y, w, h;
22         unsigned long norm[ColLast];
23         unsigned long sel[ColLast];
24         Drawable drawable;
25         GC gc;
26         struct {
27                 XFontStruct *xfont;
28                 XFontSet set;
29                 int ascent;
30                 int descent;
31                 int height;
32         } font;
33 } DC; /* draw context */
34
35 typedef struct Item Item;
36 struct Item {
37         Item *next;             /* traverses all items */
38         Item *left, *right;     /* traverses items matching current search pattern */
39         char *text;
40 };
41
42 /* forward declarations */
43 Item *appenditem(Item *i, Item *last);
44 void calcoffsets(void);
45 void cleanup(void);
46 void drawmenu(void);
47 void drawtext(const char *text, unsigned long col[ColLast]);
48 void *emalloc(unsigned int size);
49 void eprint(const char *errstr, ...);
50 char *estrdup(const char *str);
51 unsigned long getcolor(const char *colstr);
52 Bool grabkeyboard(void);
53 void initfont(const char *fontstr);
54 void kpress(XKeyEvent * e);
55 void match(char *pattern);
56 void readstdin(void);
57 void run(void);
58 void setup(int x, int y, int w);
59 char *cistrstr(const char *s, const char *sub);
60 unsigned int textnw(const char *text, unsigned int len);
61 unsigned int textw(const char *text);
62
63 #include "config.h"
64
65 /* variables */
66 char *font = FONT;
67 char *maxname = NULL;
68 char *normbg = NORMBGCOLOR;
69 char *normfg = NORMFGCOLOR;
70 char *prompt = NULL;
71 char *selbg = SELBGCOLOR;
72 char *selfg = SELFGCOLOR;
73 char text[4096];
74 int screen;
75 int ret = 0;
76 unsigned int cmdw = 0;
77 unsigned int mw, mh;
78 unsigned int promptw = 0;
79 unsigned int nitem = 0;
80 unsigned int numlockmask = 0;
81 Bool running = True;
82 Display *dpy;
83 DC dc = {0};
84 Item *allitems = NULL;  /* first of all items */
85 Item *item = NULL;      /* first of pattern matching items */
86 Item *sel = NULL;
87 Item *next = NULL;
88 Item *prev = NULL;
89 Item *curr = NULL;
90 Window root, win;
91 int (*fstrncmp)(const char *, const char *, size_t n) = strncmp;
92 char *(*fstrstr)(const char *, const char *) = strstr;
93
94 Item *
95 appenditem(Item *i, Item *last) {
96         if(!last)
97                 item = i;
98         else
99                 last->right = i;
100         i->left = last;
101         i->right = NULL;
102         last = i;
103         nitem++;
104         return last;
105 }
106
107 void
108 calcoffsets(void) {
109         unsigned int tw, w;
110
111         if(!curr)
112                 return;
113         w = promptw + cmdw + 2 * SPACE;
114         for(next = curr; next; next=next->right) {
115                 tw = textw(next->text);
116                 if(tw > mw / 3)
117                         tw = mw / 3;
118                 w += tw;
119                 if(w > mw)
120                         break;
121         }
122         w = promptw + cmdw + 2 * SPACE;
123         for(prev = curr; prev && prev->left; prev=prev->left) {
124                 tw = textw(prev->left->text);
125                 if(tw > mw / 3)
126                         tw = mw / 3;
127                 w += tw;
128                 if(w > mw)
129                         break;
130         }
131 }
132
133 void
134 cleanup(void) {
135         Item *itm;
136
137         while(allitems) {
138                 itm = allitems->next;
139                 free(allitems->text);
140                 free(allitems);
141                 allitems = itm;
142         }
143         if(dc.font.set)
144                 XFreeFontSet(dpy, dc.font.set);
145         else
146                 XFreeFont(dpy, dc.font.xfont);
147         XFreePixmap(dpy, dc.drawable);
148         XFreeGC(dpy, dc.gc);
149         XDestroyWindow(dpy, win);
150         XUngrabKeyboard(dpy, CurrentTime);
151 }
152
153 void
154 drawmenu(void) {
155         Item *i;
156
157         dc.x = 0;
158         dc.y = 0;
159         dc.w = mw;
160         dc.h = mh;
161         drawtext(NULL, dc.norm);
162         /* print prompt? */
163         if(promptw) {
164                 dc.w = promptw;
165                 drawtext(prompt, dc.sel);
166         }
167         dc.x += promptw;
168         dc.w = mw - promptw;
169         /* print command */
170         if(cmdw && item)
171                 dc.w = cmdw;
172         drawtext(text[0] ? text : NULL, dc.norm);
173         dc.x += cmdw;
174         if(curr) {
175                 dc.w = SPACE;
176                 drawtext((curr && curr->left) ? "<" : NULL, dc.norm);
177                 dc.x += dc.w;
178                 /* determine maximum items */
179                 for(i = curr; i != next; i=i->right) {
180                         dc.w = textw(i->text);
181                         if(dc.w > mw / 3)
182                                 dc.w = mw / 3;
183                         drawtext(i->text, (sel == i) ? dc.sel : dc.norm);
184                         dc.x += dc.w;
185                 }
186                 dc.x = mw - SPACE;
187                 dc.w = SPACE;
188                 drawtext(next ? ">" : NULL, dc.norm);
189         }
190         XCopyArea(dpy, dc.drawable, win, dc.gc, 0, 0, mw, mh, 0, 0);
191         XFlush(dpy);
192 }
193
194 void
195 drawtext(const char *text, unsigned long col[ColLast]) {
196         int x, y, w, h;
197         static char buf[256];
198         unsigned int len, olen;
199         XRectangle r = { dc.x, dc.y, dc.w, dc.h };
200
201         XSetForeground(dpy, dc.gc, col[ColBG]);
202         XFillRectangles(dpy, dc.drawable, dc.gc, &r, 1);
203         if(!text)
204                 return;
205         w = 0;
206         olen = len = strlen(text);
207         if(len >= sizeof buf)
208                 len = sizeof buf - 1;
209         memcpy(buf, text, len);
210         buf[len] = 0;
211         h = dc.font.ascent + dc.font.descent;
212         y = dc.y + (dc.h / 2) - (h / 2) + dc.font.ascent;
213         x = dc.x + (h / 2);
214         /* shorten text if necessary */
215         while(len && (w = textnw(buf, len)) > dc.w - h)
216                 buf[--len] = 0;
217         if(len < olen) {
218                 if(len > 1)
219                         buf[len - 1] = '.';
220                 if(len > 2)
221                         buf[len - 2] = '.';
222                 if(len > 3)
223                         buf[len - 3] = '.';
224         }
225         if(w > dc.w)
226                 return; /* too long */
227         XSetForeground(dpy, dc.gc, col[ColFG]);
228         if(dc.font.set)
229                 XmbDrawString(dpy, dc.drawable, dc.font.set, dc.gc, x, y, buf, len);
230         else
231                 XDrawString(dpy, dc.drawable, dc.gc, x, y, buf, len);
232 }
233
234 void *
235 emalloc(unsigned int size) {
236         void *res = malloc(size);
237
238         if(!res)
239                 eprint("fatal: could not malloc() %u bytes\n", size);
240         return res;
241 }
242
243 void
244 eprint(const char *errstr, ...) {
245         va_list ap;
246
247         va_start(ap, errstr);
248         vfprintf(stderr, errstr, ap);
249         va_end(ap);
250         exit(EXIT_FAILURE);
251 }
252
253 char *
254 estrdup(const char *str) {
255         void *res = strdup(str);
256
257         if(!res)
258                 eprint("fatal: could not malloc() %u bytes\n", strlen(str));
259         return res;
260 }
261
262 unsigned long
263 getcolor(const char *colstr) {
264         Colormap cmap = DefaultColormap(dpy, screen);
265         XColor color;
266
267         if(!XAllocNamedColor(dpy, cmap, colstr, &color, &color))
268                 eprint("error, cannot allocate color '%s'\n", colstr);
269         return color.pixel;
270 }
271
272 Bool
273 grabkeyboard(void) {
274         unsigned int len;
275
276         for(len = 1000; len; len--) {
277                 if(XGrabKeyboard(dpy, root, True, GrabModeAsync, GrabModeAsync, CurrentTime)
278                         == GrabSuccess)
279                         break;
280                 usleep(1000);
281         }
282         return len > 0;
283 }
284
285 void
286 initfont(const char *fontstr) {
287         char *def, **missing;
288         int i, n;
289
290         if(!fontstr || fontstr[0] == '\0')
291                 eprint("error, cannot load font: '%s'\n", fontstr);
292         missing = NULL;
293         if(dc.font.set)
294                 XFreeFontSet(dpy, dc.font.set);
295         dc.font.set = XCreateFontSet(dpy, fontstr, &missing, &n, &def);
296         if(missing)
297                 XFreeStringList(missing);
298         if(dc.font.set) {
299                 XFontSetExtents *font_extents;
300                 XFontStruct **xfonts;
301                 char **font_names;
302                 dc.font.ascent = dc.font.descent = 0;
303                 font_extents = XExtentsOfFontSet(dc.font.set);
304                 n = XFontsOfFontSet(dc.font.set, &xfonts, &font_names);
305                 for(i = 0, dc.font.ascent = 0, dc.font.descent = 0; i < n; i++) {
306                         if(dc.font.ascent < (*xfonts)->ascent)
307                                 dc.font.ascent = (*xfonts)->ascent;
308                         if(dc.font.descent < (*xfonts)->descent)
309                                 dc.font.descent = (*xfonts)->descent;
310                         xfonts++;
311                 }
312         }
313         else {
314                 if(dc.font.xfont)
315                         XFreeFont(dpy, dc.font.xfont);
316                 dc.font.xfont = NULL;
317                 if(!(dc.font.xfont = XLoadQueryFont(dpy, fontstr))
318                 && !(dc.font.xfont = XLoadQueryFont(dpy, "fixed")))
319                         eprint("error, cannot load font: '%s'\n", fontstr);
320                 dc.font.ascent = dc.font.xfont->ascent;
321                 dc.font.descent = dc.font.xfont->descent;
322         }
323         dc.font.height = dc.font.ascent + dc.font.descent;
324 }
325
326 void
327 kpress(XKeyEvent * e) {
328         char buf[32];
329         int i, num;
330         unsigned int len;
331         KeySym ksym;
332
333         len = strlen(text);
334         buf[0] = 0;
335         num = XLookupString(e, buf, sizeof buf, &ksym, 0);
336         if(IsKeypadKey(ksym)) { 
337                 if(ksym == XK_KP_Enter) {
338                         ksym = XK_Return;
339                 } else if(ksym >= XK_KP_0 && ksym <= XK_KP_9) {
340                         ksym = (ksym - XK_KP_0) + XK_0;
341                 }
342         }
343         if(IsFunctionKey(ksym) || IsKeypadKey(ksym)
344                         || IsMiscFunctionKey(ksym) || IsPFKey(ksym)
345                         || IsPrivateKeypadKey(ksym))
346                 return;
347         /* first check if a control mask is omitted */
348         if(e->state & ControlMask) {
349                 switch (ksym) {
350                 default:        /* ignore other control sequences */
351                         return;
352                 case XK_bracketleft:
353                         ksym = XK_Escape;
354                         break;
355                 case XK_h:
356                 case XK_H:
357                         ksym = XK_BackSpace;
358                         break;
359                 case XK_i:
360                 case XK_I:
361                         ksym = XK_Tab;
362                         break;
363                 case XK_j:
364                 case XK_J:
365                         ksym = XK_Return;
366                         break;
367                 case XK_u:
368                 case XK_U:
369                         text[0] = 0;
370                         match(text);
371                         drawmenu();
372                         return;
373                 case XK_w:
374                 case XK_W:
375                         if(len) {
376                                 i = len - 1;
377                                 while(i >= 0 && text[i] == ' ')
378                                         text[i--] = 0;
379                                 while(i >= 0 && text[i] != ' ')
380                                         text[i--] = 0;
381                                 match(text);
382                                 drawmenu();
383                         }
384                         return;
385                 }
386         }
387         if(CLEANMASK(e->state) & Mod1Mask) {
388                 switch(ksym) {
389                 default: return;
390                 case XK_h:
391                         ksym = XK_Left;
392                         break;
393                 case XK_l:
394                         ksym = XK_Right;
395                         break;
396                 case XK_j:
397                         ksym = XK_Next;
398                         break;
399                 case XK_k:
400                         ksym = XK_Prior;
401                         break;
402                 case XK_g:
403                         ksym = XK_Home;
404                         break;
405                 case XK_G:
406                         ksym = XK_End;
407                         break;
408                 }
409         }
410         switch(ksym) {
411         default:
412                 if(num && !iscntrl((int) buf[0])) {
413                         buf[num] = 0;
414                         if(len > 0)
415                                 strncat(text, buf, sizeof text);
416                         else
417                                 strncpy(text, buf, sizeof text);
418                         match(text);
419                 }
420                 break;
421         case XK_BackSpace:
422                 if(len) {
423                         text[--len] = 0;
424                         match(text);
425                 }
426                 break;
427         case XK_End:
428                 if(!item)
429                         return;
430                 while(next) {
431                         sel = curr = next;
432                         calcoffsets();
433                 }
434                 while(sel && sel->right)
435                         sel = sel->right;
436                 break;
437         case XK_Escape:
438                 ret = 1;
439                 running = False;
440                 break;
441         case XK_Home:
442                 if(!item)
443                         return;
444                 sel = curr = item;
445                 calcoffsets();
446                 break;
447         case XK_Left:
448                 if(!(sel && sel->left))
449                         return;
450                 sel=sel->left;
451                 if(sel->right == curr) {
452                         curr = prev;
453                         calcoffsets();
454                 }
455                 break;
456         case XK_Next:
457                 if(!next)
458                         return;
459                 sel = curr = next;
460                 calcoffsets();
461                 break;
462         case XK_Prior:
463                 if(!prev)
464                         return;
465                 sel = curr = prev;
466                 calcoffsets();
467                 break;
468         case XK_Return:
469                 if((e->state & ShiftMask) && text)
470                         fprintf(stdout, "%s", text);
471                 else if(sel)
472                         fprintf(stdout, "%s", sel->text);
473                 else if(text)
474                         fprintf(stdout, "%s", text);
475                 fflush(stdout);
476                 running = False;
477                 break;
478         case XK_Right:
479                 if(!(sel && sel->right))
480                         return;
481                 sel=sel->right;
482                 if(sel == next) {
483                         curr = next;
484                         calcoffsets();
485                 }
486                 break;
487         case XK_Tab:
488                 if(!sel)
489                         return;
490                 strncpy(text, sel->text, sizeof text);
491                 match(text);
492                 break;
493         }
494         drawmenu();
495 }
496
497 void
498 match(char *pattern) {
499         unsigned int plen;
500         Item *i, *j;
501
502         if(!pattern)
503                 return;
504         plen = strlen(pattern);
505         item = j = NULL;
506         nitem = 0;
507         for(i = allitems; i; i = i->next)
508                 if(!fstrncmp(pattern, i->text, plen)
509                                 || fstrstr(i->text, pattern))
510                         j = appenditem(i, j);
511         curr = prev = next = sel = item;
512         calcoffsets();
513 }
514
515 void
516 readstdin(void) {
517         char *p, buf[1024];
518         unsigned int len = 0, max = 0;
519         Item *i, *new;
520
521         i = 0;
522         while(fgets(buf, sizeof buf, stdin)) {
523                 len = strlen(buf);
524                 if (buf[len - 1] == '\n')
525                         buf[len - 1] = 0;
526                 p = estrdup(buf);
527                 if(max < len) {
528                         maxname = p;
529                         max = len;
530                 }
531                 new = emalloc(sizeof(Item));
532                 new->next = new->left = new->right = NULL;
533                 new->text = p;
534                 if(!i)
535                         allitems = new;
536                 else 
537                         i->next = new;
538                 i = new;
539         }
540 }
541
542 void
543 run(void) {
544         XEvent ev;
545
546         /* main event loop */
547         while(running && !XNextEvent(dpy, &ev))
548                 switch (ev.type) {
549                 default:        /* ignore all crap */
550                         break;
551                 case KeyPress:
552                         kpress(&ev.xkey);
553                         break;
554                 case Expose:
555                         if(ev.xexpose.count == 0)
556                                 drawmenu();
557                         break;
558                 }
559 }
560
561 void
562 setup(int x, int y, int w) {
563         unsigned int i, j;
564         XModifierKeymap *modmap;
565         XSetWindowAttributes wa;
566
567         /* init modifier map */
568         modmap = XGetModifierMapping(dpy);
569         for(i = 0; i < 8; i++)
570                 for(j = 0; j < modmap->max_keypermod; j++) {
571                         if(modmap->modifiermap[i * modmap->max_keypermod + j]
572                         == XKeysymToKeycode(dpy, XK_Num_Lock))
573                                 numlockmask = (1 << i);
574                 }
575         XFreeModifiermap(modmap);
576
577         /* style */
578         dc.norm[ColBG] = getcolor(normbg);
579         dc.norm[ColFG] = getcolor(normfg);
580         dc.sel[ColBG] = getcolor(selbg);
581         dc.sel[ColFG] = getcolor(selfg);
582         initfont(font);
583
584         /* menu window */
585         wa.override_redirect = 1;
586         wa.background_pixmap = ParentRelative;
587         wa.event_mask = ExposureMask | ButtonPressMask | KeyPressMask;
588         mw = w ? w : DisplayWidth(dpy, screen);
589         mh = dc.font.height + 2;
590         win = XCreateWindow(dpy, root, x, y, mw, mh, 0,
591                         DefaultDepth(dpy, screen), CopyFromParent,
592                         DefaultVisual(dpy, screen),
593                         CWOverrideRedirect | CWBackPixmap | CWEventMask, &wa);
594
595         /* pixmap */
596         dc.drawable = XCreatePixmap(dpy, root, mw, mh, DefaultDepth(dpy, screen));
597         dc.gc = XCreateGC(dpy, root, 0, 0);
598         XSetLineAttributes(dpy, dc.gc, 1, LineSolid, CapButt, JoinMiter);
599         if(!dc.font.set)
600                 XSetFont(dpy, dc.gc, dc.font.xfont->fid);
601         if(maxname)
602                 cmdw = textw(maxname);
603         if(cmdw > mw / 3)
604                 cmdw = mw / 3;
605         if(prompt)
606                 promptw = textw(prompt);
607         if(promptw > mw / 5)
608                 promptw = mw / 5;
609         text[0] = 0;
610         match(text);
611         XMapRaised(dpy, win);
612 }
613
614 char *
615 cistrstr(const char *s, const char *sub) {
616         int c, csub;
617         unsigned int len;
618
619         if(!sub)
620                 return (char *)s;
621         if((c = *sub++) != 0) {
622                 c = tolower(c);
623                 len = strlen(sub);
624                 do {
625                         do {
626                                 if((csub = *s++) == 0)
627                                         return (NULL);
628                         }
629                         while(tolower(csub) != c);
630                 }
631                 while(strncasecmp(s, sub, len) != 0);
632                 s--;
633         }
634         return (char *)s;
635 }
636
637 unsigned int
638 textnw(const char *text, unsigned int len) {
639         XRectangle r;
640
641         if(dc.font.set) {
642                 XmbTextExtents(dc.font.set, text, len, NULL, &r);
643                 return r.width;
644         }
645         return XTextWidth(dc.font.xfont, text, len);
646 }
647
648 unsigned int
649 textw(const char *text) {
650         return textnw(text, strlen(text)) + dc.font.height;
651 }
652
653 int
654 main(int argc, char *argv[]) {
655         int x = 0, y = 0, w = 0;
656         unsigned int i;
657
658         /* command line args */
659         for(i = 1; i < argc; i++)
660                 if(!strcmp(argv[i], "-i")) {
661                         fstrncmp = strncasecmp;
662                         fstrstr = cistrstr;
663                 }
664                 else if(!strcmp(argv[i], "-fn")) {
665                         if(++i < argc) font = argv[i];
666                 }
667                 else if(!strcmp(argv[i], "-nb")) {
668                         if(++i < argc) normbg = argv[i];
669                 }
670                 else if(!strcmp(argv[i], "-nf")) {
671                         if(++i < argc) normfg = argv[i];
672                 }
673                 else if(!strcmp(argv[i], "-p")) {
674                         if(++i < argc) prompt = argv[i];
675                 }
676                 else if(!strcmp(argv[i], "-sb")) {
677                         if(++i < argc) selbg = argv[i];
678                 }
679                 else if(!strcmp(argv[i], "-sf")) {
680                         if(++i < argc) selfg = argv[i];
681                 }
682                 else if(!strcmp(argv[i], "-x")) {
683                         if(++i < argc) x = atoi(argv[i]);
684                 }
685                 else if(!strcmp(argv[i], "-y")) {
686                         if(++i < argc) y = atoi(argv[i]);
687                 }
688                 else if(!strcmp(argv[i], "-w")) {
689                         if(++i < argc) w = atoi(argv[i]);
690                 }
691                 else if(!strcmp(argv[i], "-v"))
692                         eprint("dmenu-"VERSION", © 2006-2008 dmenu engineers, see LICENSE for details\n");
693                 else
694                         eprint("usage: dmenu [-i] [-fn <font>] [-nb <color>] [-nf <color>]\n"
695                                "             [-p <prompt>] [-sb <color>] [-sf <color>]\n"
696                                "             [-x <x>] [-y <y>] [-w <w>] [-v]\n");
697         setlocale(LC_CTYPE, "");
698         dpy = XOpenDisplay(0);
699         if(!dpy)
700                 eprint("dmenu: cannot open display\n");
701         screen = DefaultScreen(dpy);
702         root = RootWindow(dpy, screen);
703
704         if(isatty(STDIN_FILENO)) {
705                 readstdin();
706                 running = grabkeyboard();
707         }
708         else { /* prevent keypress loss */
709                 running = grabkeyboard();
710                 readstdin();
711         }
712
713         setup(x, y, w);
714         drawmenu();
715         XSync(dpy, False);
716         run();
717         cleanup();
718         XCloseDisplay(dpy);
719         return ret;
720 }