replaced promptw
[dmenu.git] / dmenu.c
1 /* See LICENSE file for copyright and license details. */
2 #include <ctype.h>
3 #include <stdio.h>
4 #include <stdlib.h>
5 #include <string.h>
6 #include <unistd.h>
7 #include <X11/Xatom.h>
8 #include <X11/Xlib.h>
9 #include <X11/Xutil.h>
10 #ifdef XINERAMA
11 #include <X11/extensions/Xinerama.h>
12 #endif
13 #include <draw.h>
14
15 #define INRECT(x,y,rx,ry,rw,rh) ((x) >= (rx) && (x) < (rx)+(rw) && (y) >= (ry) && (y) < (ry)+(rh))
16 #define MIN(a,b)                ((a) < (b) ? (a) : (b))
17 #define MAX(a,b)                ((a) > (b) ? (a) : (b))
18 #define UTF8_CODEPOINT(c)       (((c) & 0xc0) != 0x80)
19
20 typedef struct Item Item;
21 struct Item {
22         char *text;
23         Item *next;          /* traverses all items */
24         Item *left, *right;  /* traverses matching items */
25 };
26
27 static void appenditem(Item *item, Item **list, Item **last);
28 static void calcoffsetsh(void);
29 static void calcoffsetsv(void);
30 static char *cistrstr(const char *s, const char *sub);
31 static void drawmenu(void);
32 static void drawmenuh(void);
33 static void drawmenuv(void);
34 static void grabkeyboard(void);
35 static void insert(const char *s, ssize_t n);
36 static void keypress(XKeyEvent *e);
37 static void match(void);
38 static void paste(void);
39 static void readstdin(void);
40 static void run(void);
41 static void setup(void);
42 static void usage(void);
43
44 static char text[4096];
45 static int promptw;
46 static size_t cursor = 0;
47 static const char *prompt = NULL;
48 static const char *normbgcolor = "#cccccc";
49 static const char *normfgcolor = "#000000";
50 static const char *selbgcolor  = "#0066ff";
51 static const char *selfgcolor  = "#ffffff";
52 static unsigned int inputw = 0;
53 static unsigned int lines = 0;
54 static unsigned int mw, mh;
55 static unsigned long normcol[ColLast];
56 static unsigned long selcol[ColLast];
57 static Atom utf8;
58 static Bool topbar = True;
59 static DC *dc;
60 static Item *allitems, *matches;
61 static Item *curr, *prev, *next, *sel;
62 static Window root, win;
63
64 static int (*fstrncmp)(const char *, const char *, size_t) = strncmp;
65 static char *(*fstrstr)(const char *, const char *) = strstr;
66 static void (*calcoffsets)(void) = calcoffsetsh;
67
68 void
69 appenditem(Item *item, Item **list, Item **last) {
70         if(!*last)
71                 *list = item;
72         else
73                 (*last)->right = item;
74         item->left = *last;
75         item->right = NULL;
76         *last = item;
77 }
78
79 void
80 calcoffsetsh(void) {
81         unsigned int w, x;
82
83         w = promptw + inputw + textw(dc, "<") + textw(dc, ">");
84         for(x = w, next = curr; next; next = next->right)
85                 if((x += MIN(textw(dc, next->text), mw / 3)) > mw)
86                         break;
87         for(x = w, prev = curr; prev && prev->left; prev = prev->left)
88                 if((x += MIN(textw(dc, prev->left->text), mw / 3)) > mw)
89                         break;
90 }
91
92 void
93 calcoffsetsv(void) {
94         unsigned int i;
95
96         next = prev = curr;
97         for(i = 0; i < lines && next; i++)
98                 next = next->right;
99         for(i = 0; i < lines && prev && prev->left; i++)
100                 prev = prev->left;
101 }
102
103 char *
104 cistrstr(const char *s, const char *sub) {
105         size_t len;
106
107         for(len = strlen(sub); *s; s++)
108                 if(!strncasecmp(s, sub, len))
109                         return (char *)s;
110         return NULL;
111 }
112
113 void
114 drawmenu(void) {
115         dc->x = 0;
116         dc->y = 0;
117         drawrect(dc, 0, 0, mw, mh, BG(dc, normcol));
118         dc->h = dc->font.height + 2;
119         dc->y = topbar ? 0 : mh - dc->h;
120         /* print prompt? */
121         if(prompt) {
122                 dc->w = promptw;
123                 drawtext(dc, prompt, selcol);
124                 dc->x = dc->w;
125         }
126         dc->w = mw - dc->x;
127         /* print input area */
128         if(matches && lines == 0 && textw(dc, text) <= inputw)
129                 dc->w = inputw;
130         drawtext(dc, text, normcol);
131         drawrect(dc, textnw(dc, text, cursor) + dc->h/2 - 2, 2, 1, dc->h - 4, FG(dc, normcol));
132         if(lines > 0)
133                 drawmenuv();
134         else if(curr && (dc->w == inputw || curr->next))
135                 drawmenuh();
136         commitdraw(dc, win);
137 }
138
139 void
140 drawmenuh(void) {
141         Item *item;
142
143         dc->x += inputw;
144         dc->w = textw(dc, "<");
145         if(curr->left)
146                 drawtext(dc, "<", normcol);
147         for(item = curr; item != next; item = item->right) {
148                 dc->x += dc->w;
149                 dc->w = MIN(textw(dc, item->text), mw / 3);
150                 drawtext(dc, item->text, (item == sel) ? selcol : normcol);
151         }
152         dc->w = textw(dc, ">");
153         dc->x = mw - dc->w;
154         if(next)
155                 drawtext(dc, ">", normcol);
156 }
157
158 void
159 drawmenuv(void) {
160         Item *item;
161
162         dc->y = topbar ? dc->h : 0;
163         dc->w = mw - dc->x;
164         for(item = curr; item != next; item = item->right) {
165                 drawtext(dc, item->text, (item == sel) ? selcol : normcol);
166                 dc->y += dc->h;
167         }
168 }
169
170 void
171 grabkeyboard(void) {
172         int i;
173
174         for(i = 0; i < 1000; i++) {
175                 if(!XGrabKeyboard(dc->dpy, root, True, GrabModeAsync, GrabModeAsync, CurrentTime))
176                         return;
177                 usleep(1000);
178         }
179         eprintf("cannot grab keyboard\n");
180 }
181
182 void
183 insert(const char *s, ssize_t n) {
184         memmove(text + cursor + n, text + cursor, sizeof text - cursor - n);
185         if(n > 0)
186                 memcpy(text + cursor, s, n);
187         cursor += n;
188         match();
189 }
190
191 void
192 keypress(XKeyEvent *e) {
193         char buf[sizeof text];
194         int n;
195         size_t len;
196         KeySym ksym;
197
198         len = strlen(text);
199         XLookupString(e, buf, sizeof buf, &ksym, NULL);
200         if(e->state & ControlMask) {
201                 switch(tolower(ksym)) {
202                 default:
203                         return;
204                 case XK_a:
205                         ksym = XK_Home;
206                         break;
207                 case XK_b:
208                         ksym = XK_Left;
209                         break;
210                 case XK_c:
211                         ksym = XK_Escape;
212                         break;
213                 case XK_e:
214                         ksym = XK_End;
215                         break;
216                 case XK_f:
217                         ksym = XK_Right;
218                         break;
219                 case XK_h:
220                         ksym = XK_BackSpace;
221                         break;
222                 case XK_i:
223                         ksym = XK_Tab;
224                         break;
225                 case XK_j:
226                 case XK_m:
227                         ksym = XK_Return;
228                         break;
229                 case XK_k:  /* delete right */
230                         text[cursor] = '\0';
231                         match();
232                         break;
233                 case XK_n:
234                         ksym = XK_Down;
235                         break;
236                 case XK_p:
237                         ksym = XK_Up;
238                         break;
239                 case XK_u:  /* delete left */
240                         insert(NULL, -cursor);
241                         break;
242                 case XK_w:  /* delete word */
243                         if(cursor == 0)
244                                 return;
245                         n = 0;
246                         while(cursor - n++ > 0 && text[cursor - n] == ' ');
247                         while(cursor - n++ > 0 && text[cursor - n] != ' ');
248                         insert(NULL, 1-n);
249                         break;
250                 case XK_y:  /* paste selection */
251                         XConvertSelection(dc->dpy, XA_PRIMARY, utf8, None, win, CurrentTime);
252                         /* causes SelectionNotify event */
253                         return;
254                 }
255         }
256         switch(ksym) {
257         default:
258                 if(!iscntrl((int)*buf))
259                         insert(buf, MIN(strlen(buf), sizeof text - cursor));
260                 break;
261         case XK_BackSpace:
262                 if(cursor == 0)
263                         return;
264                 for(n = 1; cursor - n > 0 && !UTF8_CODEPOINT(text[cursor - n]); n++);
265                 insert(NULL, -n);
266                 break;
267         case XK_Delete:
268                 if(cursor == len)
269                         return;
270                 for(n = 1; cursor + n < len && !UTF8_CODEPOINT(text[cursor + n]); n++);
271                 cursor += n;
272                 insert(NULL, -n);
273                 break;
274         case XK_End:
275                 if(cursor < len) {
276                         cursor = len;
277                         break;
278                 }
279                 while(next) {
280                         sel = curr = next;
281                         calcoffsets();
282                 }
283                 while(sel && sel->right)
284                         sel = sel->right;
285                 break;
286         case XK_Escape:
287                 exit(EXIT_FAILURE);
288         case XK_Home:
289                 if(sel == matches) {
290                         cursor = 0;
291                         break;
292                 }
293                 sel = curr = matches;
294                 calcoffsets();
295                 break;
296         case XK_Left:
297                 if(cursor > 0 && (!sel || !sel->left || lines > 0)) {
298                         while(cursor-- > 0 && !UTF8_CODEPOINT(text[cursor]));
299                         break;
300                 }
301                 else if(lines > 0)
302                         return;
303         case XK_Up:
304                 if(!sel || !sel->left)
305                         return;
306                 sel = sel->left;
307                 if(sel->right == curr) {
308                         curr = prev;
309                         calcoffsets();
310                 }
311                 break;
312         case XK_Next:
313                 if(!next)
314                         return;
315                 sel = curr = next;
316                 calcoffsets();
317                 break;
318         case XK_Prior:
319                 if(!prev)
320                         return;
321                 sel = curr = prev;
322                 calcoffsets();
323                 break;
324         case XK_Return:
325         case XK_KP_Enter:
326                 fputs((sel && !(e->state & ShiftMask)) ? sel->text : text, stdout);
327                 fflush(stdout);
328                 exit(EXIT_SUCCESS);
329         case XK_Right:
330                 if(cursor < len) {
331                         while(cursor++ < len && !UTF8_CODEPOINT(text[cursor]));
332                         break;
333                 }
334                 else if(lines > 0)
335                         return;
336         case XK_Down:
337                 if(!sel || !sel->right)
338                         return;
339                 sel = sel->right;
340                 if(sel == next) {
341                         curr = next;
342                         calcoffsets();
343                 }
344                 break;
345         case XK_Tab:
346                 if(!sel)
347                         return;
348                 strncpy(text, sel->text, sizeof text);
349                 cursor = strlen(text);
350                 match();
351                 break;
352         }
353         drawmenu();
354 }
355
356 void
357 match(void) {
358         unsigned int len;
359         Item *item, *itemend, *lexact, *lprefix, *lsubstr, *exactend, *prefixend, *substrend;
360
361         len = strlen(text);
362         matches = lexact = lprefix = lsubstr = itemend = exactend = prefixend = substrend = NULL;
363         for(item = allitems; item; item = item->next)
364                 if(!fstrncmp(text, item->text, len + 1))
365                         appenditem(item, &lexact, &exactend);
366                 else if(!fstrncmp(text, item->text, len))
367                         appenditem(item, &lprefix, &prefixend);
368                 else if(fstrstr(item->text, text))
369                         appenditem(item, &lsubstr, &substrend);
370         if(lexact) {
371                 matches = lexact;
372                 itemend = exactend;
373         }
374         if(lprefix) {
375                 if(itemend) {
376                         itemend->right = lprefix;
377                         lprefix->left = itemend;
378                 }
379                 else
380                         matches = lprefix;
381                 itemend = prefixend;
382         }
383         if(lsubstr) {
384                 if(itemend) {
385                         itemend->right = lsubstr;
386                         lsubstr->left = itemend;
387                 }
388                 else
389                         matches = lsubstr;
390         }
391         curr = prev = next = sel = matches;
392         calcoffsets();
393 }
394
395 void
396 paste(void) {
397         char *p, *q;
398         int di;
399         unsigned long dl;
400         Atom da;
401
402         XGetWindowProperty(dc->dpy, win, utf8, 0, sizeof text - cursor, True,
403                            utf8, &da, &di, &dl, &dl, (unsigned char **)&p);
404         insert(p, (q = strchr(p, '\n')) ? q-p : strlen(p));
405         XFree(p);
406         drawmenu();
407 }
408
409 void
410 readstdin(void) {
411         char buf[sizeof text], *p;
412         Item *item, *new;
413
414         allitems = NULL;
415         for(item = NULL; fgets(buf, sizeof buf, stdin); item = new) {
416                 if((p = strchr(buf, '\n')))
417                         *p = '\0';
418                 if(!(new = malloc(sizeof *new)))
419                         eprintf("cannot malloc %u bytes\n", sizeof *new);
420                 if(!(new->text = strdup(buf)))
421                         eprintf("cannot strdup %u bytes\n", strlen(buf));
422                 inputw = MAX(inputw, textw(dc, new->text));
423                 new->next = new->left = new->right = NULL;
424                 if(item)
425                         item->next = new;
426                 else
427                         allitems = new;
428         }
429 }
430
431 void
432 run(void) {
433         XEvent ev;
434
435         while(!XNextEvent(dc->dpy, &ev))
436                 switch(ev.type) {
437                 case Expose:
438                         if(ev.xexpose.count == 0)
439                                 drawmenu();
440                         break;
441                 case KeyPress:
442                         keypress(&ev.xkey);
443                         break;
444                 case SelectionNotify:
445                         if(ev.xselection.property == utf8)
446                                 paste();
447                         break;
448                 case VisibilityNotify:
449                         if(ev.xvisibility.state != VisibilityUnobscured)
450                                 XRaiseWindow(dc->dpy, win);
451                         break;
452                 }
453 }
454
455 void
456 setup(void) {
457         int x, y, screen;
458         XSetWindowAttributes wa;
459 #ifdef XINERAMA
460         int n;
461         XineramaScreenInfo *info;
462 #endif
463
464         screen = DefaultScreen(dc->dpy);
465         root = RootWindow(dc->dpy, screen);
466         utf8 = XInternAtom(dc->dpy, "UTF8_STRING", False);
467
468         normcol[ColBG] = getcolor(dc, normbgcolor);
469         normcol[ColFG] = getcolor(dc, normfgcolor);
470         selcol[ColBG] = getcolor(dc, selbgcolor);
471         selcol[ColFG] = getcolor(dc, selfgcolor);
472
473         /* input window geometry */
474         mh = (dc->font.height + 2) * (lines + 1);
475 #ifdef XINERAMA
476         if((info = XineramaQueryScreens(dc->dpy, &n))) {
477                 int i, di;
478                 unsigned int du;
479                 Window dw;
480
481                 XQueryPointer(dc->dpy, root, &dw, &dw, &x, &y, &di, &di, &du);
482                 for(i = 0; i < n; i++)
483                         if(INRECT(x, y, info[i].x_org, info[i].y_org, info[i].width, info[i].height))
484                                 break;
485                 x = info[i].x_org;
486                 y = info[i].y_org + (topbar ? 0 : info[i].height - mh);
487                 mw = info[i].width;
488                 XFree(info);
489         }
490         else
491 #endif
492         {
493                 x = 0;
494                 y = topbar ? 0 : DisplayHeight(dc->dpy, screen) - mh;
495                 mw = DisplayWidth(dc->dpy, screen);
496         }
497
498         /* input window */
499         wa.override_redirect = True;
500         wa.background_pixmap = ParentRelative;
501         wa.event_mask = ExposureMask | KeyPressMask | VisibilityChangeMask;
502         win = XCreateWindow(dc->dpy, root, x, y, mw, mh, 0,
503                         DefaultDepth(dc->dpy, screen), CopyFromParent,
504                         DefaultVisual(dc->dpy, screen),
505                         CWOverrideRedirect | CWBackPixmap | CWEventMask, &wa);
506
507         grabkeyboard();
508         setcanvas(dc, win, mw, mh);
509         inputw = MIN(inputw, mw/3);
510         promptw = prompt ? MIN(textw(dc, prompt), mw/5) : 0;
511         XMapRaised(dc->dpy, win);
512         text[0] = '\0';
513         match();
514 }
515
516 void
517 usage(void) {
518         fputs("usage: dmenu [-b] [-i] [-l lines] [-p prompt] [-fn font] [-nb color]\n"
519               "             [-nf color] [-sb color] [-sf color] [-v]\n", stderr);
520         exit(EXIT_FAILURE);
521 }
522
523 int
524 main(int argc, char *argv[]) {
525         int i;
526
527         progname = "dmenu";
528         dc = initdraw();
529
530         for(i = 1; i < argc; i++)
531                 /* single flags */
532                 if(!strcmp(argv[i], "-v")) {
533                         fputs("dmenu-"VERSION", © 2006-2010 dmenu engineers, see LICENSE for details\n", stdout);
534                         exit(EXIT_SUCCESS);
535                 }
536                 else if(!strcmp(argv[i], "-b"))
537                         topbar = False;
538                 else if(!strcmp(argv[i], "-i")) {
539                         fstrncmp = strncasecmp;
540                         fstrstr = cistrstr;
541                 }
542                 else if(i == argc-1)
543                         usage();
544                 /* double flags */
545                 else if(!strcmp(argv[i], "-l")) {
546                         if((lines = atoi(argv[++i])) > 0)
547                                 calcoffsets = calcoffsetsv;
548                 }
549                 else if(!strcmp(argv[i], "-p"))
550                         prompt = argv[++i];
551                 else if(!strcmp(argv[i], "-fn"))
552                         initfont(dc, argv[i++]);
553                 else if(!strcmp(argv[i], "-nb"))
554                         normbgcolor = argv[++i];
555                 else if(!strcmp(argv[i], "-nf"))
556                         normfgcolor = argv[++i];
557                 else if(!strcmp(argv[i], "-sb"))
558                         selbgcolor = argv[++i];
559                 else if(!strcmp(argv[i], "-sf"))
560                         selfgcolor = argv[++i];
561                 else
562                         usage();
563
564         readstdin();
565         setup();
566         run();
567
568         return EXIT_FAILURE;  /* should not reach */
569 }