Add a “-c” option which enables tab-completion for file-names.  With
it enabled, when entered text contains multiple words (for example
“soffice docs”) or contains a slash (for example “~/bin”), pressing
C-i or Tab will expand the last argument as a file name.

If there were multiple expansions the longest common prefix is
substituted.  Otherwise, if there was exactly one expansion a slash or
space is added ofter it depending on whether it expanded to
a directory name or not.

One known limitation is that if expanded file name contains
white-space, user will have to quote the argument herself or otherwise
when executing it will likely be interpreted as separate arguments.

---
 LICENSE |  2 ++
 dmenu.1 | 11 +++++++-
 dmenu.c | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 3 files changed, 106 insertions(+), 4 deletions(-)

 This is based on 2009 patch by Jeremy Jay which can be found at
 <http://lists.suckless.org/dwm/0901/7355.html>, but lacks buffer
 overflow bug. ;)

diff --git a/LICENSE b/LICENSE
index 39c4b6e..8658346 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,10 +1,12 @@
 MIT/X Consortium License
 
+© 2014 Google Inc.  // author: Michal Nazarewicz <min...@mina86.com>
 © 2006-2014 Anselm R Garbe <ans...@garbe.us>
 © 2010-2012 Connor Lane Smith <c...@lubutu.com>
 © 2009 Gottox <got...@s01.de>
 © 2009 Markus Schnalke <mei...@marmaro.de>
 © 2009 Evan Gates <evan.ga...@gmail.com>
+© 2009 Jeremy Jay
 © 2006-2008 Sander van Dijk <a dot h dot vandijk at gmail dot com>
 © 2006-2007 Michał Janeczek <janeczek at gmail dot com>
 
diff --git a/dmenu.1 b/dmenu.1
index bbee17d..af78643 100644
--- a/dmenu.1
+++ b/dmenu.1
@@ -6,6 +6,7 @@ dmenu \- dynamic menu
 .RB [ \-b ]
 .RB [ \-f ]
 .RB [ \-i ]
+.RB [ \-c ]
 .RB [ \-l
 .RB [ \-m
 .IR monitor ]
@@ -48,6 +49,9 @@ X until stdin reaches end\-of\-file.
 .B \-i
 dmenu matches menu items case insensitively.
 .TP
+.B \-c
+enables file-name expansion when C\-i (or Tab) is pressed.
+.TP
 .BI \-l " lines"
 dmenu lists items vertically, with the given number of lines.
 .TP
@@ -82,7 +86,12 @@ dmenu is completely controlled by the keyboard.  Items are 
selected using the
 arrow keys, page up, page down, home, and end.
 .TP
 .B Tab
-Copy the selected item to the input field.
+If \-c option is in effect and the text consists of at least two words
+or contains a slash character, perform a word expansion on the last
+word (which may be the only word if it contains a slash).
+
+If \-c is not given or the above conditions are not met, copy the
+selected item to the input field.
 .TP
 .B Return
 Confirm selection.  Prints the selected item to stdout and exits, returning
diff --git a/dmenu.c b/dmenu.c
index dd2c128..4073fb8 100644
--- a/dmenu.c
+++ b/dmenu.c
@@ -4,7 +4,10 @@
 #include <stdlib.h>
 #include <string.h>
 #include <strings.h>
+#include <sys/types.h>
+#include <sys/stat.h>
 #include <unistd.h>
+#include <wordexp.h>
 #include <X11/Xlib.h>
 #include <X11/Xatom.h>
 #include <X11/Xutil.h>
@@ -35,6 +38,7 @@ static void keypress(XKeyEvent *ev);
 static void match(void);
 static size_t nextrune(int inc);
 static void paste(void);
+static Bool matchfile_maybe(void);
 static void readstdin(void);
 static void run(void);
 static void setup(void);
@@ -47,6 +51,7 @@ static size_t cursor = 0;
 static unsigned long normcol[ColLast];
 static unsigned long selcol[ColLast];
 static unsigned long outcol[ColLast];
+static Bool matchfile_enabled = False;
 static Atom clip, utf8;
 static DC *dc;
 static Item *items = NULL;
@@ -80,6 +85,9 @@ main(int argc, char *argv[]) {
                        fstrncmp = strncasecmp;
                        fstrstr = cistrstr;
                }
+               else if(!strcmp(argv[i], "-c")) { /* file name tab completion */
+                       matchfile_enabled = True;
+               }
                else if(i+1 == argc)
                        usage();
                /* these options take one argument */
@@ -227,7 +235,8 @@ insert(const char *str, ssize_t n) {
        if(strlen(text) + n > sizeof text - 1)
                return;
        /* move existing text out of the way, insert new text, and update 
cursor */
-       memmove(&text[cursor + n], &text[cursor], sizeof text - cursor - MAX(n, 
0));
+       memmove(&text[cursor + n], &text[cursor],
+               sizeof text - cursor - MAX(n, 0));
        if(n > 0)
                memcpy(&text[cursor], str, n);
        cursor += n;
@@ -387,6 +396,8 @@ keypress(XKeyEvent *ev) {
                }
                break;
        case XK_Tab:
+               if (matchfile_maybe())
+                       break;
                if(!sel)
                        return;
                strncpy(text, sel->text, sizeof text - 1);
@@ -476,6 +487,86 @@ paste(void) {
        drawmenu();
 }
 
+Bool
+matchfile_maybe(void) {
+       static int wrde_flags;
+       static wordexp_t exp;
+
+       char *const end = text + (sizeof text - 1);
+       char *ch, *src, *word = NULL;
+       struct stat buf;
+       unsigned i;
+
+       if (!matchfile_enabled) {
+               return False;
+       }
+
+       /* Expansion supported only at the end of line at this point. */
+       if (text[cursor]) {
+               return False;
+       }
+
+       /* Need enough space to insert star. */
+       if (cursor + 1 >= sizeof text) {
+               return False;
+       }
+
+       /* Do file match expansion if text consists of multiple words
+        * or the first word contains slashes. */
+       for (ch = text; *ch; ++ch) {
+               if (isspace(*ch)) {
+                       word = ch + 1;
+               } else if (!word && *ch == '/') {
+                       word = text;
+               }
+       }
+
+       if (!word || !*word) {
+               return False;
+       }
+
+       /* Perform expansion */
+       ch[0] = '*';
+       ch[1] = 0;
+
+       if (wordexp(word, &exp, wrde_flags) ||
+           (wrde_flags |= WRDE_REUSE, !exp.we_wordc) ||
+           !strcmp(word, exp.we_wordv[0])) {
+               *ch = 0;  /* Eat "*" */
+               return True;
+       }
+
+       /* Check if anything actually changed */
+
+       /* Copy the first expansion */
+       ch = word;
+       src = exp.we_wordv[0];
+       while (ch != end && (*ch = *src++)) {
+               ++ch;
+       }
+       *ch = 0;
+
+       /* Compare with all the other expansions so we get the common part. */
+       for (i = 1; i < exp.we_wordc; ++i) {
+               ch = word;
+               src = exp.we_wordv[i];
+               while (*ch && *ch == *src++) {
+                       ++ch;
+               }
+               *ch = 0;
+       }
+
+       /* If there was only one match, add slash or space */
+       if (exp.we_wordc == 1 && ch != end && *ch != '/' &&
+           stat(word, &buf) == 0) {
+               *ch++ = S_ISDIR(buf.st_mode) ? '/' : ' ';
+               *ch = 0;
+       }
+
+       cursor = ch - text;
+       return True;
+}
+
 void
 readstdin(void) {
        char buf[sizeof text], *p, *maxstr = NULL;
@@ -619,7 +710,7 @@ setup(void) {
 
 void
 usage(void) {
-       fputs("usage: dmenu [-b] [-f] [-i] [-l lines] [-p prompt] [-fn font] 
[-m monitor]\n"
-             "             [-nb color] [-nf color] [-sb color] [-sf color] 
[-v]\n", stderr);
+       fputs("usage: dmenu [-b] [-c] [-f] [-i] [-v] [-l lines] [-p prompt] 
[-fn font]\n"
+             "             [-m monitor] [-nb color] [-nf color] [-sb color] 
[-sf color]\n", stderr);
        exit(EXIT_FAILURE);
 }
-- 
2.0.0.526.g5318336


Reply via email to