Merge branch 'master' into type
authorphillbush <phillbush@cock.li>
Fri, 8 Jan 2021 21:39:28 +0000 (18:39 -0300)
committerphillbush <phillbush@cock.li>
Fri, 8 Jan 2021 21:39:28 +0000 (18:39 -0300)
config.h
xmenu.1
xmenu.c
xmenu.h

index 6eb9b7c..9886a72 100644 (file)
--- a/config.h
+++ b/config.h
@@ -34,6 +34,9 @@ static struct Config config = {
 
        /* area around the icon, the triangle and the separator */
        .horzpadding = 8,
 
        /* area around the icon, the triangle and the separator */
        .horzpadding = 8,
+
+       /* if nonzero, enable type-to-select feature, can be togglet with -t */
+       .typetoselect = 0
 };
 
 /*
 };
 
 /*
diff --git a/xmenu.1 b/xmenu.1
index c2495a1..3f45a0b 100644 (file)
--- a/xmenu.1
+++ b/xmenu.1
@@ -3,7 +3,7 @@
 xmenu \- menu utility for X
 .SH SYNOPSIS
 .B xmenu
 xmenu \- menu utility for X
 .SH SYNOPSIS
 .B xmenu
-.RB [ \-irw ]
+.RB [ \-irtw ]
 .RB [ -p
 .IR position ]
 .RI [ title ]
 .RB [ -p
 .IR position ]
 .RI [ title ]
@@ -58,6 +58,10 @@ must spawn at the position 100x500 of the monitor 0.
 If this option is set, the right mouse button is disabled;
 so pressing it will not trigger any menu item.
 .TP
 If this option is set, the right mouse button is disabled;
 so pressing it will not trigger any menu item.
 .TP
+.B -t
+If this option is set, the type-to-select feature is enabled,
+so typing a string will select the first item matching it.
+.TP
 .B -w
 Asks the window manager to draw a border around the menus.
 This option may be buggy in some window managers,
 .B -w
 Asks the window manager to draw a border around the menus.
 This option may be buggy in some window managers,
@@ -108,11 +112,21 @@ Select the first item in the menu.
 .BR End
 Select the last item in the menu.
 .TP
 .BR End
 Select the last item in the menu.
 .TP
-.BR Down ", " Tab
+.BR Down
+Cycle through the items in the regular direction.
+.TP
+.BR Tab
 Cycle through the items in the regular direction.
 Cycle through the items in the regular direction.
+If the type-to-select feature is enabled, and there is a typed string in memory,
+cycle through matching items instead.
+.TP
+.BR Up
+Cycle through the items in the reverse direction.
 .TP
 .TP
-.BR Up ", " Shift-Tab
+.BR Shift-Tab
 Cycle through the items in the reverse direction.
 Cycle through the items in the reverse direction.
+If the type-to-select feature is enabled, and there is a typed string in memory,
+cycle through matching items instead.
 .TP
 .BR Right ", " Enter
 Select the highlighted item.
 .TP
 .BR Right ", " Enter
 Select the highlighted item.
diff --git a/xmenu.c b/xmenu.c
index 88d834f..acdabc2 100644 (file)
--- a/xmenu.c
+++ b/xmenu.c
@@ -5,6 +5,7 @@
 #include <stdlib.h>
 #include <string.h>
 #include <limits.h>
 #include <stdlib.h>
 #include <string.h>
 #include <limits.h>
+#include <locale.h>
 #include <time.h>
 #include <unistd.h>
 #include <X11/Xlib.h>
 #include <time.h>
 #include <unistd.h>
 #include <X11/Xlib.h>
@@ -30,6 +31,7 @@ static struct Monitor mon;
 static Atom utf8string;
 static Atom wmdelete;
 static Atom netatom[NetLast];
 static Atom utf8string;
 static Atom wmdelete;
 static Atom netatom[NetLast];
+static XIM xim;
 
 /* flags */
 static int iflag = 0;   /* whether to disable icons */
 
 /* flags */
 static int iflag = 0;   /* whether to disable icons */
@@ -139,7 +141,7 @@ getoptions(int argc, char *argv[])
 {
        int ch;
 
 {
        int ch;
 
-       while ((ch = getopt(argc, argv, "ip:rw")) != -1) {
+       while ((ch = getopt(argc, argv, "ip:rtw")) != -1) {
                switch (ch) {
                case 'i':
                        iflag = 1;
                switch (ch) {
                case 'i':
                        iflag = 1;
@@ -151,6 +153,9 @@ getoptions(int argc, char *argv[])
                case 'r':
                        rflag = 1;
                        break;
                case 'r':
                        rflag = 1;
                        break;
+               case 't':
+                       config.typetoselect = !config.typetoselect;
+                       break;
                case 'w':
                        wflag = 1;
                        break;
                case 'w':
                        wflag = 1;
                        break;
@@ -372,6 +377,11 @@ allocmenu(struct Menu *parent, struct Item *list, unsigned level)
                                  CWBorderPixel | CWEventMask | CWSaveUnder,
                                  &swa);
 
                                  CWBorderPixel | CWEventMask | CWSaveUnder,
                                  &swa);
 
+       menu->xic = XCreateIC(xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
+                             XNClientWindow, menu->win, XNFocusWindow, menu->win, NULL);
+       if (menu->xic == NULL)
+               errx(1, "XCreateIC: could not obtain input method");
+
        return menu;
 }
 
        return menu;
 }
 
@@ -769,6 +779,24 @@ grabkeyboard(void)
        errx(1, "could not grab keyboard");
 }
 
        errx(1, "could not grab keyboard");
 }
 
+/* try to grab focus, we may have to wait for another process to ungrab */
+static void
+grabfocus(Window win)
+{
+       struct timespec ts = { .tv_sec = 0, .tv_nsec = 10000000  };
+       Window focuswin;
+       int i, revertwin;
+
+       for (i = 0; i < 100; ++i) {
+               XGetInputFocus(dpy, &focuswin, &revertwin);
+               if (focuswin == win)
+                       return;
+               XSetInputFocus(dpy, win, RevertToParent, CurrentTime);
+               nanosleep(&ts, NULL);
+       }
+       errx(1, "cannot grab focus");
+}
+
 /* ungrab pointer and keyboard */
 static void
 ungrab(void)
 /* ungrab pointer and keyboard */
 static void
 ungrab(void)
@@ -1016,6 +1044,7 @@ mapmenu(struct Menu *currmenu)
        }
 
        prevmenu = currmenu;
        }
 
        prevmenu = currmenu;
+       grabfocus(currmenu->win);
 }
 
 /* get menu of given window */
 }
 
 /* get menu of given window */
@@ -1114,24 +1143,120 @@ isclickbutton(unsigned int button)
        return 0;
 }
 
        return 0;
 }
 
+/* append buf into text */
+static int
+append(char *text, char *buf, size_t textsize, size_t buflen)
+{
+       size_t textlen;
+
+       textlen = strlen(text);
+       if (iscntrl(*buf))
+               return 0;
+       if (textlen + buflen > textsize - 1)
+               return 0;
+       if (buflen < 1)
+               return 0;
+       memcpy(text + textlen, buf, buflen);
+       text[textlen + buflen] = '\0';
+       return 1;
+}
+
+/* get item in menu matching text from given direction (or from beginning, if dir = 0) */
+static struct Item *
+matchitem(struct Menu *menu, char *text, int dir)
+{
+       struct Item *item, *lastitem;
+       char *s;
+       size_t textlen;
+
+       for (lastitem = menu->list; lastitem && lastitem->next; lastitem = lastitem->next)
+               ;
+       textlen = strlen(text);
+       if (dir < 0) {
+               if (menu->selected && menu->selected->prev)
+                       item = menu->selected->prev;
+               else
+                       item = lastitem;
+       } else if (dir > 0) {
+               if (menu->selected && menu->selected->next)
+                       item = menu->selected->next;
+               else
+                       item = menu->list;
+       } else {
+               item = menu->list;
+       }
+       /* find next item from selected item */
+       for ( ; item; item = (dir < 0) ? item->prev : item->next)
+               for (s = item->label; s && *s; s++)
+                       if (strncasecmp(s, text, textlen) == 0)
+                               return item;
+       /* if not found, try to find from the beginning/end of list */
+       if (dir > 0) {
+               for (item = menu->list ; item; item = item->next) {
+                       for (s = item->label; s && *s; s++) {
+                               if (strncasecmp(s, text, textlen) == 0) {
+                                       return item;
+                               }
+                       }
+               }
+       } else {
+               for (item = lastitem ; item; item = item->prev) {
+                       for (s = item->label; s && *s; s++) {
+                               if (strncasecmp(s, text, textlen) == 0) {
+                                       return item;
+                               }
+                       }
+               }
+       }
+       return NULL;
+}
+
+/* check keysyms defined on config.h */
+static KeySym
+normalizeksym(KeySym ksym)
+{
+       if (ksym == KSYMFIRST)
+               return XK_Home;
+       if (ksym == KSYMLAST)
+               return XK_End;
+       if (ksym == KSYMUP)
+               return XK_Up;
+       if (ksym == KSYMDOWN)
+               return XK_Down;
+       if (ksym == KSYMLEFT)
+               return XK_Left;
+       if (ksym == KSYMRIGHT)
+               return XK_Right;
+       return ksym;
+}
+
 /* run event loop */
 static void
 run(struct Menu *currmenu)
 {
 /* run event loop */
 static void
 run(struct Menu *currmenu)
 {
+       char text[BUFSIZ];
+       char buf[32];
        struct Menu *menu;
        struct Item *item;
        struct Item *previtem = NULL;
        struct Menu *menu;
        struct Item *item;
        struct Item *previtem = NULL;
-       struct Item *lastitem;
+       struct Item *lastitem, *select;
        KeySym ksym;
        KeySym ksym;
+       Status status;
        XEvent ev;
        XEvent ev;
+       int action;
+       int len;
+       int i;
 
 
+       text[0] = '\0';
        mapmenu(currmenu);
        mapmenu(currmenu);
-
        while (!XNextEvent(dpy, &ev)) {
        while (!XNextEvent(dpy, &ev)) {
+               if (XFilterEvent(&ev, None))
+                       continue;
+               action = ACTION_NOP;
                switch(ev.type) {
                case Expose:
                        if (ev.xexpose.count == 0)
                switch(ev.type) {
                case Expose:
                        if (ev.xexpose.count == 0)
-                               drawmenus(currmenu);
+                               action = ACTION_DRAW;
                        break;
                case MotionNotify:
                        menu = getmenu(currmenu, ev.xbutton.window);
                        break;
                case MotionNotify:
                        menu = getmenu(currmenu, ev.xbutton.window);
@@ -1139,15 +1264,14 @@ run(struct Menu *currmenu)
                        if (menu == NULL || item == NULL || previtem == item)
                                break;
                        previtem = item;
                        if (menu == NULL || item == NULL || previtem == item)
                                break;
                        previtem = item;
-                       menu->selected = item;
+                       select = menu->selected = item;
                        if (item->submenu != NULL) {
                                currmenu = item->submenu;
                        if (item->submenu != NULL) {
                                currmenu = item->submenu;
-                               currmenu->selected = NULL;
+                               select = NULL;
                        } else {
                                currmenu = menu;
                        }
                        } else {
                                currmenu = menu;
                        }
-                       mapmenu(currmenu);
-                       drawmenus(currmenu);
+                       action = ACTION_CLEAR | ACTION_SELECT | ACTION_MAP | ACTION_DRAW;
                        break;
                case ButtonRelease:
                        if (!isclickbutton(ev.xbutton.button))
                        break;
                case ButtonRelease:
                        if (!isclickbutton(ev.xbutton.button))
@@ -1156,7 +1280,7 @@ run(struct Menu *currmenu)
                        item = getitem(menu, ev.xbutton.y);
                        if (menu == NULL || item == NULL)
                                break;
                        item = getitem(menu, ev.xbutton.y);
                        if (menu == NULL || item == NULL)
                                break;
-selectitem:
+enteritem:
                        if (item->label == NULL)
                                break;  /* ignore separators */
                        if (item->submenu != NULL) {
                        if (item->label == NULL)
                                break;  /* ignore separators */
                        if (item->submenu != NULL) {
@@ -1165,9 +1289,8 @@ selectitem:
                                printf("%s\n", item->output);
                                return;
                        }
                                printf("%s\n", item->output);
                                return;
                        }
-                       mapmenu(currmenu);
-                       currmenu->selected = currmenu->list;
-                       drawmenus(currmenu);
+                       select = currmenu->list;
+                       action = ACTION_CLEAR | ACTION_SELECT | ACTION_MAP | ACTION_DRAW;
                        break;
                case ButtonPress:
                        menu = getmenu(currmenu, ev.xbutton.window);
                        break;
                case ButtonPress:
                        menu = getmenu(currmenu, ev.xbutton.window);
@@ -1175,7 +1298,16 @@ selectitem:
                                return;
                        break;
                case KeyPress:
                                return;
                        break;
                case KeyPress:
-                       ksym = XkbKeycodeToKeysym(dpy, ev.xkey.keycode, 0, 0);
+                       len = XmbLookupString(currmenu->xic, &ev.xkey, buf, sizeof buf, &ksym, &status);
+                       switch(status) {
+                       default:                /* XLookupNone, XBufferOverflow */
+                               continue;
+                       case XLookupChars:
+                               goto append;
+                       case XLookupKeySym:     /* FALLTHROUGH */
+                       case XLookupBoth:
+                               break;
+                       }
 
                        /* esc closes xmenu when current menu is the root menu */
                        if (ksym == XK_Escape && currmenu->parent == NULL)
 
                        /* esc closes xmenu when current menu is the root menu */
                        if (ksym == XK_Escape && currmenu->parent == NULL)
@@ -1187,39 +1319,80 @@ selectitem:
 
                        /* cycle through menu */
                        item = NULL;
 
                        /* cycle through menu */
                        item = NULL;
-                       if (ksym == XK_Home || ksym == KSYMFIRST) {
+                       ksym = normalizeksym(ksym);
+                       switch (ksym) {
+                       case XK_Home:
                                item = itemcycle(currmenu, ITEMFIRST);
                                item = itemcycle(currmenu, ITEMFIRST);
-                       } else if (ksym == XK_End || ksym == KSYMLAST) {
+                               action = ACTION_CLEAR;
+                               break;
+                       case XK_End:
                                item = itemcycle(currmenu, ITEMLAST);
                                item = itemcycle(currmenu, ITEMLAST);
-                       } else if (ksym == XK_ISO_Left_Tab || ksym == XK_Up || ksym == KSYMUP) {
+                               action = ACTION_CLEAR;
+                               break;
+                       case XK_ISO_Left_Tab:
+                               if (*text) {
+                                       item = matchitem(currmenu, text, -1);
+                                       break;
+                               }
+                               /* FALLTHROUGH */
+                       case XK_Up:
                                item = itemcycle(currmenu, ITEMPREV);
                                item = itemcycle(currmenu, ITEMPREV);
-                       } else if (ksym == XK_Tab || ksym == XK_Down || ksym == KSYMDOWN) {
+                               action = ACTION_CLEAR;
+                               break;
+                       case XK_Tab:
+                               if (*text) {
+                                       item = matchitem(currmenu, text, 1);
+                                       break;
+                               }
+                               /* FALLTHROUGH */
+                       case XK_Down:
                                item = itemcycle(currmenu, ITEMNEXT);
                                item = itemcycle(currmenu, ITEMNEXT);
-                       } else if (ksym >= XK_1 && ksym <= XK_9){
+                               action = ACTION_CLEAR;
+                               break;
+                       case XK_1: case XK_2: case XK_3: case XK_4: case XK_5: case XK_6: case XK_7: case XK_8: case XK_9:
                                item = itemcycle(currmenu, ITEMFIRST);
                                lastitem = itemcycle(currmenu, ITEMLAST);
                                for (int i = ksym - XK_1; i > 0 && item != lastitem; i--) {
                                        currmenu->selected = item;
                                        item = itemcycle(currmenu, ITEMNEXT);
                                }
                                item = itemcycle(currmenu, ITEMFIRST);
                                lastitem = itemcycle(currmenu, ITEMLAST);
                                for (int i = ksym - XK_1; i > 0 && item != lastitem; i--) {
                                        currmenu->selected = item;
                                        item = itemcycle(currmenu, ITEMNEXT);
                                }
-                       } else if ((ksym == XK_Return || ksym == XK_Right || ksym == KSYMRIGHT) &&
-                                   currmenu->selected != NULL) {
-                               item = currmenu->selected;
-                               goto selectitem;
-                       } else if ((ksym == XK_Escape || ksym == XK_Left || ksym == KSYMLEFT) &&
-                                  currmenu->parent != NULL) {
-                               item = currmenu->parent->selected;
-                               currmenu = currmenu->parent;
-                               mapmenu(currmenu);
-                       } else
+                               action = ACTION_CLEAR;
                                break;
                                break;
-                       currmenu->selected = item;
-                       drawmenus(currmenu);
+                       case XK_Return: case XK_Right:
+                               if (currmenu->selected) {
+                                       item = currmenu->selected;
+                                       goto enteritem;
+                               }
+                               break;
+                       case XK_Escape: case XK_Left:
+                               if (currmenu->parent) {
+                                       item = currmenu->parent->selected;
+                                       currmenu = currmenu->parent;
+                                       action = ACTION_CLEAR | ACTION_MAP;
+                               }
+                               break;
+                       case XK_BackSpace: case XK_Clear: case XK_Delete:
+                               action = ACTION_CLEAR;
+                               break;
+                       default:
+append:
+                               if (!config.typetoselect)
+                                       break;
+                               for (i = 0; i < 2; i++) {
+                                       append(text, buf, sizeof text, len);
+                                       if ((item = matchitem(currmenu, text, 0)))
+                                               break;
+                                       text[0] = '\0';
+                               }
+                               break;
+                       }
+                       select = item;
+                       action |= ACTION_SELECT | ACTION_DRAW;
                        break;
                case LeaveNotify:
                        previtem = NULL;
                        break;
                case LeaveNotify:
                        previtem = NULL;
-                       currmenu->selected = NULL;
-                       drawmenus(currmenu);
+                       select = NULL;
+                       action = ACTION_CLEAR | ACTION_SELECT | ACTION_DRAW;
                        break;
                case ConfigureNotify:
                        menu = getmenu(currmenu, ev.xconfigure.window);
                        break;
                case ConfigureNotify:
                        menu = getmenu(currmenu, ev.xconfigure.window);
@@ -1236,9 +1409,17 @@ selectitem:
                        if (menu->parent == NULL)
                                return;     /* closing the root menu closes the program */
                        currmenu = menu->parent;
                        if (menu->parent == NULL)
                                return;     /* closing the root menu closes the program */
                        currmenu = menu->parent;
-                       mapmenu(currmenu);
+                       action = ACTION_MAP;
                        break;
                }
                        break;
                }
+               if (action & ACTION_CLEAR)
+                       text[0] = '\0';
+               if (action & ACTION_SELECT)
+                       currmenu->selected = select;
+               if (action & ACTION_MAP)
+                       mapmenu(currmenu);
+               if (action & ACTION_DRAW)
+                       drawmenus(currmenu);
        }
 }
 
        }
 }
 
@@ -1295,6 +1476,8 @@ main(int argc, char *argv[])
        XClassHint classh;
 
        /* open connection to server and set X variables */
        XClassHint classh;
 
        /* open connection to server and set X variables */
+       if (!setlocale(LC_CTYPE, "") || !XSupportsLocale())
+               warnx("warning: no locale support");
        if ((dpy = XOpenDisplay(NULL)) == NULL)
                errx(1, "could not open display");
        screen = DefaultScreen(dpy);
        if ((dpy = XOpenDisplay(NULL)) == NULL)
                errx(1, "could not open display");
        screen = DefaultScreen(dpy);
@@ -1304,6 +1487,8 @@ main(int argc, char *argv[])
        XrmInitialize();
        if ((xrm = XResourceManagerString(dpy)) != NULL)
                xdb = XrmGetStringDatabase(xrm);
        XrmInitialize();
        if ((xrm = XResourceManagerString(dpy)) != NULL)
                xdb = XrmGetStringDatabase(xrm);
+       if ((xim = XOpenIM(dpy, NULL, NULL, NULL)) == NULL)
+               errx(1, "XOpenIM: could not open input device");
 
        /* get configuration */
        getresources();
 
        /* get configuration */
        getresources();
diff --git a/xmenu.h b/xmenu.h
index 051bc4a..385eb82 100644 (file)
--- a/xmenu.h
+++ b/xmenu.h
@@ -1,5 +1,12 @@
 #define PROGNAME "xmenu"
 
 #define PROGNAME "xmenu"
 
+/* Actions for the main loop */
+#define ACTION_NOP    0
+#define ACTION_CLEAR  1<<0      /* clear text */
+#define ACTION_SELECT 1<<1      /* select item */
+#define ACTION_MAP    1<<2      /* remap menu windows */
+#define ACTION_DRAW   1<<3      /* redraw menu windows */
+
 /* enum for keyboard menu navigation */
 enum { ITEMPREV, ITEMNEXT, ITEMFIRST, ITEMLAST };
 
 /* enum for keyboard menu navigation */
 enum { ITEMPREV, ITEMNEXT, ITEMFIRST, ITEMLAST };
 
@@ -43,6 +50,7 @@ struct Config {
        int iconpadding;
        int horzpadding;
        int alignment;
        int iconpadding;
        int horzpadding;
        int alignment;
+       int typetoselect;
 
        /* the values below are set by options */
        int monitor;
 
        /* the values below are set by options */
        int monitor;
@@ -98,4 +106,5 @@ struct Menu {
        int maxtextw;           /* maximum text width */
        unsigned level;         /* menu level relative to root */
        Window win;             /* menu window to map on the screen */
        int maxtextw;           /* maximum text width */
        unsigned level;         /* menu level relative to root */
        Window win;             /* menu window to map on the screen */
+       XIC xic;                /* input context */
 };
 };