Blob


1 /* mymenu -- simple dmenu alternative */
3 /* Copyright (C) 2018 Omar Polo <omar.polo@europecom.net>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
19 #include <stdio.h>
20 #include <stdlib.h>
21 #include <string.h> // strdup, strnlen, ...
22 #include <ctype.h> // isalnum
23 #include <locale.h> // setlocale
24 #include <unistd.h>
25 #include <sysexits.h>
26 #include <stdbool.h>
27 #include <limits.h>
28 #include <errno.h>
30 #include <X11/Xlib.h>
31 #include <X11/Xutil.h> // XLookupString
32 #include <X11/Xresource.h>
33 #include <X11/Xcms.h> // colors
35 #ifdef USE_XINERAMA
36 # include <X11/extensions/Xinerama.h>
37 #endif
39 #define nil NULL
41 #define MIN(X, Y) (((X) < (Y)) ? (X) : (Y))
42 #define MAX(a, b) ((a) > (b) ? (a) : (b))
43 #define OVERLAP(a,b,c,d) (((a)==(c) && (b)==(d)) || MIN((a)+(b), (c)+(d)) - MAX((a), (c)) > 0)
44 #define INTERSECT(x,y,w,h,x1,y1,w1,h1) (OVERLAP((x),(w),(x1),(w1)) && OVERLAP((y),(h),(y1),(h1)))
46 #define update_completions(cs, text, lines) { \
47 compl_delete(cs); \
48 cs = filter(text, lines); \
49 }
51 // TODO: dynamic?
52 #define MAX_ITEMS 256
54 #define TODO(s) { \
55 fprintf(stderr, "TODO! " s "\n"); \
56 }
58 #define cannot_allocate_memory { \
59 fprintf(stderr, "Could not allocate memory\n"); \
60 exit(EX_UNAVAILABLE); \
61 }
63 #define check_allocation(a) { \
64 if (a == nil) \
65 cannot_allocate_memory; \
66 }
68 enum state {LOOPING, OK, ERR};
70 struct rendering {
71 Display *d;
72 Window w;
73 GC prompt;
74 GC prompt_bg;
75 GC completion;
76 GC completion_bg;
77 GC completion_highlighted;
78 GC completion_highlighted_bg;
79 int width;
80 int height;
81 XFontStruct *font;
82 };
84 struct completions {
85 char *completion;
86 bool selected;
87 struct completions *next;
88 };
90 struct completions *compl_new() {
91 struct completions *c = malloc(sizeof(struct completions));
93 if (c == nil)
94 return c;
96 c->completion = nil;
97 c->selected = false;
98 c->next = nil;
99 return c;
102 void compl_delete(struct completions *c) {
103 free(c);
106 struct completions *filter(char *text, char **lines) {
107 int i = 0;
108 struct completions *root = compl_new();
109 struct completions *c = root;
111 for (;;) {
112 char *l = lines[i];
113 if (l == nil)
114 break;
116 if (strstr(l, text) != nil) {
117 c->next = compl_new();
118 c = c->next;
119 c->completion = l;
122 ++i;
125 struct completions *r = root->next;
126 compl_delete(root);
127 return r;
130 // push the character c at the end of the string pointed by p
131 int pushc(char **p, int maxlen, char c) {
132 int len = strnlen(*p, maxlen);
134 if (!(len < maxlen -2)) {
135 maxlen += maxlen >> 1;
136 char *newptr = realloc(*p, maxlen);
137 if (newptr == nil) { // bad!
138 return -1;
140 *p = newptr;
143 (*p)[len] = c;
144 (*p)[len+1] = '\0';
145 return maxlen;
148 void popc(char *p, int maxlen) {
149 int len = strnlen(p, maxlen);
150 p[len-1] = '\0';
153 // read an arbitrary long line from stdin and return a pointer to it
154 // TODO: resize the allocated memory to exactly fit the string once
155 // read?
156 char *readline(bool *eof) {
157 int maxlen = 8;
158 char *str = calloc(maxlen, sizeof(char));
159 if (str == nil) {
160 fprintf(stderr, "Cannot allocate memory!\n");
161 exit(EX_UNAVAILABLE);
164 int c;
165 while((c = getchar()) != EOF) {
166 if (c == '\n')
167 return str;
168 else
169 maxlen = pushc(&str, maxlen, c);
171 if (maxlen == -1) {
172 fprintf(stderr, "Cannot allocate memory!\n");
173 exit(EX_UNAVAILABLE);
176 *eof = true;
177 return str;
180 int readlines (char **lines) {
181 bool finished = false;
182 int n = 0;
183 while (n < MAX_ITEMS) {
184 lines[n] = readline(&finished);
186 if (strlen(lines[n]) == 0 || lines[n][0] == '\n')
187 --n; // forget about this line
189 if (finished)
190 break;
192 ++n;
194 /* for (n = 0; n < MAX_ITEMS -1; ++n) { */
195 /* lines[n] = readline(&finished); */
196 /* if (finished) */
197 /* break; */
198 /* } */
199 n++;
200 lines[n] = nil;
201 return n;
204 // |------------------|----------------------------------------------|
205 // | 20 char text | completion | completion | completion | compl |
206 // |------------------|----------------------------------------------|
207 void draw(struct rendering *r, char *text, struct completions *cs) {
208 int texty = (r->height + r->font->ascent) >> 1;
210 // TODO: make these dynamic?
211 int prompt_width = 20; // char
212 int padding = 10;
213 int start_at = XTextWidth(r->font, " ", 1) * prompt_width + padding;
215 XFillRectangle(r->d, r->w, r->prompt_bg, 0, 0, start_at, r->height);
217 int text_len = strlen(text);
218 if (text_len > prompt_width)
219 text = text + (text_len - prompt_width);
220 XDrawString(r->d, r->w, r->prompt, padding, texty, text, MIN(text_len, prompt_width));
222 XFillRectangle(r->d, r->w, r->completion_bg, start_at, 0, r->width, r->height);
224 while (cs != nil) {
225 GC g = cs->selected ? r->completion_highlighted : r->completion;
226 GC h = cs->selected ? r->completion_highlighted_bg : r->completion_bg;
228 int len = strlen(cs->completion);
229 int text_width = XTextWidth(r->font, cs->completion, len);
231 XFillRectangle(r->d, r->w, h, start_at, 0, text_width + padding*2, r->height);
232 XDrawString(r->d, r->w, g, start_at + padding, texty, cs->completion, len);
234 start_at += text_width + padding * 2;
236 cs = cs->next;
239 XFlush(r->d);
242 /* Set some WM stuff */
243 void set_win_atoms_hints(Display *d, Window w, int width, int height) {
244 Atom type;
245 type = XInternAtom(d, "_NET_WM_WINDOW_TYPE_DOCK", false);
246 XChangeProperty(
247 d,
248 w,
249 XInternAtom(d, "_NET_WM_WINDOW_TYPE", false),
250 XInternAtom(d, "ATOM", false),
251 32,
252 PropModeReplace,
253 (unsigned char *)&type,
255 );
257 /* some window managers honor this properties */
258 type = XInternAtom(d, "_NET_WM_STATE_ABOVE", false);
259 XChangeProperty(d,
260 w,
261 XInternAtom(d, "_NET_WM_STATE", false),
262 XInternAtom(d, "ATOM", false),
263 32,
264 PropModeReplace,
265 (unsigned char *)&type,
267 );
269 type = XInternAtom(d, "_NET_WM_STATE_FOCUSED", false);
270 XChangeProperty(d,
271 w,
272 XInternAtom(d, "_NET_WM_STATE", false),
273 XInternAtom(d, "ATOM", false),
274 32,
275 PropModeAppend,
276 (unsigned char *)&type,
278 );
280 // setting window hints
281 XClassHint *class_hint = XAllocClassHint();
282 if (class_hint == nil) {
283 fprintf(stderr, "Could not allocate memory for class hint\n");
284 exit(EX_UNAVAILABLE);
286 class_hint->res_name = "mymenu";
287 class_hint->res_class = "mymenu";
288 XSetClassHint(d, w, class_hint);
289 XFree(class_hint);
291 XSizeHints *size_hint = XAllocSizeHints();
292 if (size_hint == nil) {
293 fprintf(stderr, "Could not allocate memory for size hint\n");
294 exit(EX_UNAVAILABLE);
296 size_hint->min_width = width;
297 size_hint->base_width = width;
298 size_hint->min_height = height;
299 size_hint->base_height = height;
301 XFlush(d);
304 void get_wh(Display *d, Window *w, int *width, int *height) {
305 XWindowAttributes win_attr;
306 XGetWindowAttributes(d, *w, &win_attr);
307 *height = win_attr.height;
308 *width = win_attr.width;
311 // I know this may seem a little hackish BUT is the only way I managed
312 // to actually grab that goddam keyboard. Only one call to
313 // XGrabKeyboard does not always end up with the keyboard grabbed!
314 int take_keyboard(Display *d, Window w) {
315 int i;
316 for (i = 0; i < 100; i++) {
317 if (XGrabKeyboard(d, w, True, GrabModeAsync, GrabModeAsync, CurrentTime) == GrabSuccess)
318 return 1;
319 usleep(1000);
321 return 0;
324 void release_keyboard(Display *d) {
325 XUngrabKeyboard(d, CurrentTime);
328 int parse_integer(const char *str, int default_value, int max) {
329 int len = strlen(str);
330 if (len > 0 && str[len-1] == '%') {
331 char *cpy = strdup(str);
332 check_allocation(cpy);
333 cpy[len-1] = '\0';
334 int val = parse_integer(cpy, default_value, max);
335 free(cpy);
336 return val * max / 100;
339 errno = 0;
340 char *ep;
341 long lval = strtol(str, &ep, 10);
342 if (str[0] == '\0' || *ep != '\0') { // NaN
343 fprintf(stderr, "'%s' is not a valid number! Using %d as default.\n", str, default_value);
344 return default_value;
346 if ((errno == ERANGE && (lval == LONG_MAX || lval == LONG_MIN)) ||
347 (lval > INT_MAX || lval < INT_MIN)) {
348 fprintf(stderr, "%s out of range! Using %d as default.\n", str, default_value);
349 return default_value;
351 return lval;
354 int parse_int_with_middle(const char *str, int default_value, int max, int self) {
355 if (!strcmp(str, "middle")) {
356 return (max - self)/2;
358 return parse_integer(str, default_value, max);
361 int main() {
362 char *lines[MAX_ITEMS] = {0};
363 readlines(lines);
365 setlocale(LC_ALL, getenv("LANG"));
367 enum state status = LOOPING;
369 // where the monitor start (used only with xinerama)
370 int offset_x = 0;
371 int offset_y = 0;
373 // width and height of the window
374 int width = 400;
375 int height = 20;
377 // position on the screen
378 int x = 0;
379 int y = 0;
381 char *fontname = strdup("fixed");
382 check_allocation(fontname);
384 int textlen = 10;
385 char *text = malloc(textlen * sizeof(char));
386 check_allocation(text);
388 struct completions *cs = filter(text, lines);
389 bool nothing_selected = true;
391 // start talking to xorg
392 Display *d = XOpenDisplay(nil);
393 if (d == nil) {
394 fprintf(stderr, "Could not open display!\n");
395 return EX_UNAVAILABLE;
398 // get display size
399 // XXX: is getting the default root window dimension correct?
400 XWindowAttributes xwa;
401 XGetWindowAttributes(d, DefaultRootWindow(d), &xwa);
402 int d_width = xwa.width;
403 int d_height = xwa.height;
405 #ifdef USE_XINERAMA
406 // TODO: this bit still needs to be improved
407 if (XineramaIsActive(d)) {
408 int monitors;
409 XineramaScreenInfo *info = XineramaQueryScreens(d, &monitors);
410 if (info)
411 for (int i = 0; i < monitors; ++i) {
412 if (INTERSECT(x, y, 1, 1, info[i].x_org, info[i].y_org, info[i].width, info[i].height)) {
413 offset_x = info[x].x_org;
414 offset_y = info[y].y_org;
415 d_width = info[i].width;
416 d_height = info[i].height;
419 XFree(info);
421 #endif
423 /* fprintf(stderr, "offset_x:\t%d\n" */
424 /* "offset_y:\t%d\n" */
425 /* "d_width:\t%d\n" */
426 /* "d_height:\t%d\n" */
427 /* , offset_x, offset_y, d_width, d_height); */
429 Colormap cmap = DefaultColormap(d, DefaultScreen(d));
430 XColor p_fg, p_bg,
431 compl_fg, compl_bg,
432 compl_highlighted_fg, compl_highlighted_bg;
434 // read resource
435 XrmInitialize();
436 char *xrm = XResourceManagerString(d);
437 if (xrm != nil) {
438 XrmDatabase xdb = XrmGetStringDatabase(xrm);
439 XrmValue value;
440 char *datatype[20];
442 if (XrmGetResource(xdb, "MyMenu.font", "*", datatype, &value) == true) {
443 fontname = strdup(value.addr);
444 check_allocation(fontname);
446 else
447 fprintf(stderr, "no font defined, using %s\n", fontname);
449 if (XrmGetResource(xdb, "MyMenu.width", "*", datatype, &value) == true)
450 width = parse_integer(value.addr, width, d_width);
451 else
452 fprintf(stderr, "no width defined, using %d\n", width);
454 if (XrmGetResource(xdb, "MyMenu.height", "*", datatype, &value) == true)
455 height = parse_integer(value.addr, height, d_height);
456 else
457 fprintf(stderr, "no height defined, using %d\n", height);
459 if (XrmGetResource(xdb, "MyMenu.x", "*", datatype, &value) == true)
460 x = parse_int_with_middle(value.addr, x, d_width, width);
461 else
462 fprintf(stderr, "no x defined, using %d\n", width);
464 if (XrmGetResource(xdb, "MyMenu.y", "*", datatype, &value) == true)
465 y = parse_int_with_middle(value.addr, y, d_height, height);
466 else
467 fprintf(stderr, "no y defined, using %d\n", height);
469 XColor tmp;
470 // TODO: tmp needs to be free'd after every allocation?
472 // prompt
473 if (XrmGetResource(xdb, "MyMenu.prompt.foreground", "*", datatype, &value) == true)
474 XAllocNamedColor(d, cmap, value.addr, &p_fg, &tmp);
475 else
476 XAllocNamedColor(d, cmap, "white", &p_fg, &tmp);
478 if (XrmGetResource(xdb, "MyMenu.prompt.background", "*", datatype, &value) == true)
479 XAllocNamedColor(d, cmap, value.addr, &p_bg, &tmp);
480 else
481 XAllocNamedColor(d, cmap, "black", &p_bg, &tmp);
483 // completion
484 if (XrmGetResource(xdb, "MyMenu.completion.foreground", "*", datatype, &value) == true)
485 XAllocNamedColor(d, cmap, value.addr, &compl_fg, &tmp);
486 else
487 XAllocNamedColor(d, cmap, "white", &compl_fg, &tmp);
489 if (XrmGetResource(xdb, "MyMenu.completion.background", "*", datatype, &value) == true)
490 XAllocNamedColor(d, cmap, value.addr, &compl_bg, &tmp);
491 else
492 XAllocNamedColor(d, cmap, "black", &compl_bg, &tmp);
494 // completion highlighted
495 if (XrmGetResource(xdb, "MyMenu.completion_highlighted.foreground", "*", datatype, &value) == true)
496 XAllocNamedColor(d, cmap, value.addr, &compl_highlighted_fg, &tmp);
497 else
498 XAllocNamedColor(d, cmap, "black", &compl_highlighted_fg, &tmp);
500 if (XrmGetResource(xdb, "MyMenu.completion_highlighted.background", "*", datatype, &value) == true)
501 XAllocNamedColor(d, cmap, value.addr, &compl_highlighted_bg, &tmp);
502 else
503 XAllocNamedColor(d, cmap, "white", &compl_highlighted_bg, &tmp);
504 } else {
505 XColor tmp;
506 XAllocNamedColor(d, cmap, "white", &p_fg, &tmp);
507 XAllocNamedColor(d, cmap, "black", &p_bg, &tmp);
508 XAllocNamedColor(d, cmap, "white", &compl_fg, &tmp);
509 XAllocNamedColor(d, cmap, "black", &compl_bg, &tmp);
510 XAllocNamedColor(d, cmap, "black", &compl_highlighted_fg, &tmp);
511 XAllocNamedColor(d, cmap, "white", &compl_highlighted_bg, &tmp);
514 // load the font
515 XFontStruct *font = XLoadQueryFont(d, fontname);
516 if (font == nil) {
517 fprintf(stderr, "Unable to load %s font\n", fontname);
518 font = XLoadQueryFont(d, "fixed");
521 // create the window
522 XSetWindowAttributes attr;
524 Window w = XCreateWindow(d, // display
525 DefaultRootWindow(d), // parent
526 x + offset_x, y + offset_y, // x y
527 width, height, // w h
528 0, // border width
529 DefaultDepth(d, DefaultScreen(d)), // depth
530 InputOutput, // class
531 DefaultVisual(d, DefaultScreen(d)), // visual
532 0, // value mask
533 &attr);
535 set_win_atoms_hints(d, w, width, height);
537 // we want some events
538 XSelectInput(d, w, StructureNotifyMask | KeyPressMask | KeyReleaseMask | KeymapStateMask);
540 // make the window appear on the screen
541 XMapWindow(d, w);
543 // wait for the MapNotify event (i.e. the event "window rendered")
544 for (;;) {
545 XEvent e;
546 XNextEvent(d, &e);
547 if (e.type == MapNotify)
548 break;
551 // get the *real* width & height after the window was rendered
552 get_wh(d, &w, &width, &height);
554 // grab keyboard
555 take_keyboard(d, w);
557 // Create some graphics contexts
558 XGCValues values;
559 values.font = font->fid;
561 struct rendering r = {
562 .d = d,
563 .w = w,
564 .prompt = XCreateGC(d, w, GCFont, &values),
565 .prompt_bg = XCreateGC(d, w, GCFont, &values),
566 .completion = XCreateGC(d, w, GCFont, &values),
567 .completion_bg = XCreateGC(d, w, GCFont, &values),
568 .completion_highlighted = XCreateGC(d, w, GCFont, &values),
569 .completion_highlighted_bg = XCreateGC(d, w, GCFont, &values),
570 .width = width,
571 .height = height,
572 .font = font
573 };
575 // load the colors in our GCs
576 XSetForeground(d, r.prompt, p_fg.pixel);
577 XSetForeground(d, r.prompt_bg, p_bg.pixel);
578 XSetForeground(d, r.completion, compl_fg.pixel);
579 XSetForeground(d, r.completion_bg, compl_bg.pixel);
580 XSetForeground(d, r.completion_highlighted, compl_highlighted_fg.pixel);
581 XSetForeground(d, r.completion_highlighted_bg, compl_highlighted_bg.pixel);
583 // draw the window for the first time
584 draw(&r, text, cs);
586 // main loop
587 while (status == LOOPING) {
588 XEvent e;
589 XNextEvent(d, &e);
591 switch (e.type) {
592 case KeymapNotify:
593 XRefreshKeyboardMapping(&e.xmapping);
594 break;
596 case KeyRelease: break; // ignore this
598 case KeyPress:
599 switch (e.xkey.keycode) {
600 case 0x024: // enter
601 status = OK;
602 break;
604 case 0x09: // esc
605 status = ERR;
606 break;
608 case 0x017: { // tab
609 // TODO: re-organize this mess of code.
610 // FIXME: shift detection does not work!
611 if ((e.xkey.state | ShiftMask) == 0) { // shift?
612 fprintf(stderr, "SHIFT\n");
613 if (nothing_selected || (cs != nil && cs->selected)) { // select the last one
614 if (cs != nil && cs->selected)
615 cs->selected = false;
617 struct completions *cc = cs;
618 while (cc != nil) {
619 if (cc->next == nil) {
620 cc->selected = true;
621 free(text);
622 text = strdup(cc->completion);
623 if (text == nil) {
624 fprintf(stderr, "Memory allocation error!");
625 status = ERR;
626 break;
628 textlen = strlen(text);
629 break;
631 cc = cc->next;
633 nothing_selected = false;
634 } else {
635 puts("select the the previous one");
636 struct completions *cc = cs;
637 while (cc != nil) {
638 if (cc->next != nil && cc->next->selected) {
639 cc->selected = true;
640 cc->next->selected = false;
641 free(text);
642 text = strdup(cc->next->completion);
643 if (text == nil) {
644 fprintf(stderr, "Memory allocation error!");
645 status = ERR;
646 break;
648 textlen = strlen(text);
649 break;
651 cc = cc ->next;
654 } else {
655 fprintf(stderr, "no SHIFT\n");
656 if (nothing_selected) {
657 if (cs != nil) {
658 cs->selected = true;
659 nothing_selected = false;
660 free(text);
661 text = strdup(cs->completion);
662 if (text == nil) {
663 fprintf(stderr, "Memory allocation error!");
664 status = ERR;
665 break;
667 textlen = strlen(text);
669 } else {
670 struct completions *cc = cs;
671 while (cc != nil) {
672 if (cc->selected) {
673 struct completions *n = cc->next != nil ? cc->next : cs;
675 cc->selected = false;
676 n->selected = true;
678 free(text);
679 text = strdup(n->completion);
680 if (text == nil) {
681 fprintf(stderr, "Memory allocation error!");
682 status = ERR;
683 break;
685 textlen = strlen(text);
686 break;
688 cc = cc->next;
693 draw(&r, text, cs);
694 break;
697 case 0x16: // backspace
698 nothing_selected = true;
699 popc(text, textlen);
701 update_completions(cs, text, lines);
703 draw(&r, text, cs);
704 break;
706 default: {
707 char string[255] = {0};
708 KeySym keysym;
709 int len = XLookupString(&e.xkey, string, 25, &keysym, nil);
710 if (len > 0 && (isalnum(string[0]) || string[0] == ' ' || string[0] == '-')) {
711 textlen = pushc(&text, textlen, string[0]);
713 if (textlen == -1) { // uh oh
714 fprintf(stderr, "Memory allocation error!");
715 status = ERR;
716 break;
719 nothing_selected = true;
720 update_completions(cs, text, lines);
722 break;
724 } // keypress
726 draw(&r, text, cs);
727 break;
729 default:
730 fprintf(stderr, "unknown event %d\n", e.type);
734 release_keyboard(d);
736 if (status == OK)
737 printf("%s\n", text);
739 free(fontname);
740 free(text);
741 compl_delete(cs);
743 /* XDestroyWindow(d, w); */
744 XCloseDisplay(d);
746 return status == OK ? 0 : 1;