2 * MIT/X Consortium License
4 * © 2011 Rafael Garcia Gallego <rafael.garcia.gallego@gmail.com>
7 * © 2010-2011 Connor Lane Smith <cls@lubutu.com>
8 * © 2006-2011 Anselm R Garbe <anselm@garbe.us>
9 * © 2009 Gottox <gottox@s01.de>
10 * © 2009 Markus Schnalke <meillo@marmaro.de>
11 * © 2009 Evan Gates <evan.gates@gmail.com>
12 * © 2006-2008 Sander van Dijk <a dot h dot vandijk at gmail dot com>
13 * © 2006-2007 Michał Janeczek <janeczek at gmail dot com>
15 * Permission is hereby granted, free of charge, to any person obtaining a
16 * copy of this software and associated documentation files (the "Software"),
17 * to deal in the Software without restriction, including without limitation
18 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
19 * and/or sell copies of the Software, and to permit persons to whom the
20 * Software is furnished to do so, subject to the following conditions:
22 * The above copyright notice and this permission notice shall be included in
23 * all copies or substantial portions of the Software.
25 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
28 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
30 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
31 * DEALINGS IN THE SOFTWARE.
39 #include <sys/ioctl.h>
41 #include <sys/types.h>
46 #define CONTROL(ch) (ch ^ 0x40)
47 #define MIN(a,b) ((a) < (b) ? (a) : (b))
48 #define MAX(a,b) ((a) > (b) ? (a) : (b))
55 typedef struct Item Item
;
61 static char text
[BUFSIZ
] = "";
62 static int barpos
= 0;
64 static size_t lines
= 0;
65 static size_t inputw
, promptw
;
67 static char *prompt
= NULL
;
68 static Item
*items
= NULL
;
69 static Item
*matches
, *matchend
;
70 static Item
*prev
, *curr
, *next
, *sel
;
71 static struct termios tio_old
, tio_new
;
72 static int (*fstrncmp
)(const char *, const char *, size_t) = strncmp
;
75 appenditem(Item
*item
, Item
**list
, Item
**last
) {
79 (*last
)->right
= item
;
86 textwn(const char *s
, int l
) {
87 int b
, c
; /* bytes and UTF-8 characters */
89 for(b
=c
=0; s
&& s
[b
] && (l
<0 || b
<l
); b
++) if((s
[b
] & 0xc0) != 0x80) c
++;
90 return c
+4; /* Accomodate for the leading and trailing spaces */
94 textw(const char *s
) {
105 n
= mw
- (promptw
+ inputw
+ textw("<") + textw(">"));
107 for (i
= 0, next
= curr
; next
; next
= next
->right
)
108 if ((i
+= (lines
>0 ? 1 : MIN(textw(next
->text
), n
))) > n
)
110 for (i
= 0, prev
= curr
; prev
&& prev
->left
; prev
= prev
->left
)
111 if ((i
+= (lines
>0 ? 1 : MIN(textw(prev
->left
->text
), n
))) > n
)
117 if (barpos
== 0) fprintf(stderr
, "\n");
118 else fprintf(stderr
, "\033[G\033[K");
119 tcsetattr(0, TCSANOW
, &tio_old
);
124 tcsetattr(0, TCSANOW
, &tio_old
);
125 fprintf(stderr
, "%s\n", s
);
130 drawtext(const char *t
, size_t w
, Color col
) {
131 const char *prestr
, *poststr
;
135 if (w
<5) return; /* This is the minimum size needed to write a label: 1 char + 4 padding spaces */
136 tw
= w
-4; /* This is the text width, without the padding */
137 if (!(buf
= calloc(1, tw
+1))) die("Can't calloc.");
148 memset(buf
, ' ', tw
);
150 memcpy(buf
, t
, MIN(strlen(t
), tw
));
151 if (textw(t
) > w
) /* Remember textw returns the width WITH padding */
152 for (i
= MAX((tw
-4), 0); i
< tw
; i
++) buf
[i
] = '.';
154 fprintf(stderr
, "%s %s %s", prestr
, buf
, poststr
);
160 if (barpos
!= 0) fprintf(stderr
, "\033[%ldH", (long)(barpos
> 0 ? 0 : (mh
-lines
)));
161 else fprintf(stderr
, "\033[%zuF", lines
);
169 /* use default colors */
170 fprintf(stderr
, "\033[0m");
172 /* place cursor in first column, clear it */
173 fprintf(stderr
, "\033[0G");
174 fprintf(stderr
, "\033[K");
177 drawtext(prompt
, promptw
, C_Reverse
);
179 drawtext(text
, (lines
==0 && matches
) ? inputw
: mw
-promptw
, C_Normal
);
182 if (barpos
!= 0) resetline();
183 for (rw
= 0, item
= curr
; item
!= next
; rw
++, item
= item
->right
) {
184 fprintf(stderr
, "\n");
185 drawtext(item
->text
, mw
, (item
== sel
) ? C_Reverse
: C_Normal
);
187 for (; rw
< lines
; rw
++)
188 fprintf(stderr
, "\n\033[K");
190 } else if (matches
) {
191 rw
= mw
-(4+promptw
+inputw
);
193 drawtext("<", 5 /*textw("<")*/, C_Normal
);
194 for (item
= curr
; item
!= next
; item
= item
->right
) {
195 drawtext(item
->text
, MIN(textw(item
->text
), rw
), (item
== sel
) ? C_Reverse
: C_Normal
);
196 if ((rw
-= textw(item
->text
)) <= 0) break;
199 fprintf(stderr
, "\033[%zuG", mw
-5);
200 drawtext(">", 5 /*textw(">")*/, C_Normal
);
204 fprintf(stderr
, "\033[%ldG", (long)(promptw
+textwn(text
, cursor
)-1));
209 fstrstr(const char *s
, const char *sub
) {
210 for (size_t len
= strlen(sub
); *s
; s
++)
211 if (!fstrncmp(s
, sub
, len
))
219 static char **tokv
= NULL
;
222 char buf
[sizeof text
], *s
;
224 size_t len
, textsize
;
225 Item
*item
, *lprefix
, *lsubstr
, *prefixend
, *substrend
;
228 /* separate input text into tokens to be matched individually */
229 for (s
= strtok(buf
, " "); s
; tokv
[tokc
- 1] = s
, s
= strtok(NULL
, " "))
230 if (++tokc
> tokn
&& !(tokv
= realloc(tokv
, ++tokn
* sizeof *tokv
)))
231 die("Can't realloc.");
232 len
= tokc
? strlen(tokv
[0]) : 0;
234 matches
= lprefix
= lsubstr
= matchend
= prefixend
= substrend
= NULL
;
235 textsize
= strlen(text
) + 1;
236 for (item
= items
; item
&& item
->text
; item
++) {
237 for (i
= 0; i
< tokc
; i
++)
238 if (!fstrstr(item
->text
, tokv
[i
]))
240 if (i
!= tokc
) /* not all tokens match */
242 /* exact matches go first, then prefixes, then substrings */
243 if (!tokc
|| !fstrncmp(text
, item
->text
, textsize
))
244 appenditem(item
, &matches
, &matchend
);
245 else if (!fstrncmp(tokv
[0], item
->text
, len
))
246 appenditem(item
, &lprefix
, &prefixend
);
248 appenditem(item
, &lsubstr
, &substrend
);
252 matchend
->right
= lprefix
;
253 lprefix
->left
= matchend
;
256 matchend
= prefixend
;
260 matchend
->right
= lsubstr
;
261 lsubstr
->left
= matchend
;
264 matchend
= substrend
;
266 curr
= sel
= matches
;
271 insert(const char *str
, ssize_t n
) {
272 if (strlen(text
) + n
> sizeof text
- 1)
274 memmove(&text
[cursor
+ n
], &text
[cursor
], sizeof text
- cursor
- MAX(n
, 0));
276 memcpy(&text
[cursor
], str
, n
);
285 for(n
= cursor
+ inc
; n
+ inc
>= 0 && (text
[n
] & 0xc0) == 0x80; n
+= inc
);
291 char buf
[sizeof text
], *p
, *maxstr
= NULL
;
292 size_t i
, max
= 0, size
= 0;
294 for(i
= 0; fgets(buf
, sizeof buf
, stdin
); i
++) {
295 if (i
+1 >= size
/ sizeof *items
)
296 if (!(items
= realloc(items
, (size
+= BUFSIZ
))))
297 die("Can't realloc.");
298 if ((p
= strchr(buf
, '\n')))
300 if (!(items
[i
].text
= strdup(buf
)))
301 die("Can't strdup.");
302 if (strlen(items
[i
].text
) > max
)
303 max
= textw(maxstr
= items
[i
].text
);
306 items
[i
].text
= NULL
;
307 inputw
= textw(maxstr
);
311 xread(int fd
, void *buf
, size_t nbyte
) {
312 ssize_t r
= read(fd
, buf
, nbyte
);
313 if (r
< 0 || (size_t)r
!= nbyte
)
314 die("Can not read.");
322 /* re-open stdin to read keyboard */
323 if (!freopen("/dev/tty", "r", stdin
)) die("Can't reopen tty as stdin.");
324 if (!freopen("/dev/tty", "w", stderr
)) die("Can't reopen tty as stderr.");
326 /* ioctl() the tty to get size */
327 fd
= open("/dev/tty", O_RDWR
);
332 result
= ioctl(fd
, TIOCGWINSZ
, &ws
);
343 /* change terminal attributes, save old */
344 tcgetattr(0, &tio_old
);
346 tio_new
.c_iflag
&= ~(BRKINT
|PARMRK
|ISTRIP
|INLCR
|IGNCR
|ICRNL
|IXON
);
347 tio_new
.c_lflag
&= ~(ECHO
|ECHONL
|ICANON
|ISIG
|IEXTEN
);
348 tio_new
.c_cflag
&= ~(CSIZE
|PARENB
);
349 tio_new
.c_cflag
|= CS8
;
350 tio_new
.c_cc
[VMIN
] = 1;
351 tcsetattr(0, TCSANOW
, &tio_new
);
353 lines
= MIN(MAX(lines
, 0), mh
);
354 promptw
= prompt
? textw(prompt
) : 0;
355 inputw
= MIN(inputw
, mw
/3);
357 if (barpos
!= 0) resetline();
368 memset(buf
, '\0', sizeof buf
);
376 case CONTROL('['): /* ESC, need to press twice due to console limitations */
385 if (c
!= 'H') xread(0, &c
, 1); /* Remove trailing '~' from stdin */
388 case '2': /* Insert */
389 xread(0, &c
, 1); /* Remove trailing '~' from stdin */
392 case '3': /* Delete */
393 xread(0, &c
, 1); /* Remove trailing '~' from stdin */
399 if (c
!= 'F') xread(0, &c
, 1); /* Remove trailing '~' from stdin */
402 case '5': /* PageUp */
403 xread(0, &c
, 1); /* Remove trailing '~' from stdin */
406 case '6': /* PageDown */
407 xread(0, &c
, 1); /* Remove trailing '~' from stdin */
410 case 'A': /* Up arrow */
413 case 'B': /* Down arrow */
416 case 'C': /* Right arrow */
419 case 'D': /* Left arrow */
425 while (cursor
> 0 && text
[nextrune(-1)] == ' ')
426 cursor
= nextrune(-1);
427 while (cursor
> 0 && text
[nextrune(-1)] != ' ')
428 cursor
= nextrune(-1);
431 while (text
[cursor
] != '\0' && text
[nextrune(+1)] == ' ')
432 cursor
= nextrune(+1);
433 if (text
[cursor
] != '\0') {
435 cursor
= nextrune(+1);
436 } while (text
[cursor
] != '\0' && text
[cursor
] != ' ');
440 while (text
[cursor
] != '\0' && text
[nextrune(+1)] == ' ') {
441 cursor
= nextrune(+1);
442 insert(NULL
, nextrune(-1) - cursor
);
444 if (text
[cursor
] != '\0') {
446 cursor
= nextrune(+1);
447 insert(NULL
, nextrune(-1) - cursor
);
448 } while (text
[cursor
] != '\0' && text
[cursor
] != ' ');
463 case CONTROL('M'): /* Return */
465 if (sel
) strncpy(text
, sel
->text
, sizeof(text
)-1); /* Complete the input first, when hitting return */
466 cursor
= strlen(text
);
471 case CONTROL('\\'): /* These are usually close enough to RET to replace Shift+RET, again due to console limitations */
475 if (sel
== matches
) {
479 sel
= curr
= matches
;
483 if (text
[cursor
] != '\0') {
484 cursor
= strlen(text
);
492 while(next
&& (curr
= curr
->right
))
498 if (cursor
> 0 && (!sel
|| !sel
->left
|| lines
> 0)) {
499 cursor
= nextrune(-1);
504 if (sel
&& sel
->left
&& (sel
= sel
->left
)->right
== curr
) {
510 if (text
[cursor
] != '\0') {
511 cursor
= nextrune(+1);
516 if (sel
&& sel
->right
&& (sel
= sel
->right
) == next
) {
522 if (text
[cursor
] == '\0')
524 cursor
= nextrune(+1);
527 case CONTROL('?'): /* Backspace */
530 insert(NULL
, nextrune(-1) - cursor
);
532 case CONTROL('I'): /* TAB */
535 strncpy(text
, sel
->text
, sizeof text
);
536 cursor
= strlen(text
);
544 insert(NULL
, 0 - cursor
);
547 while (cursor
> 0 && text
[nextrune(-1)] == ' ')
548 insert(NULL
, nextrune(-1) - cursor
);
549 while (cursor
> 0 && text
[nextrune(-1)] != ' ')
550 insert(NULL
, nextrune(-1) - cursor
);
560 insert(buf
, strlen(buf
));
569 fputs("usage: vis-menu [-b|-t] [-i] [-l lines] [-p prompt] [initial selection]\n", stderr
);
574 main(int argc
, char **argv
) {
575 for (int i
= 1; i
< argc
; i
++) {
576 if (!strcmp(argv
[i
], "-v")) {
577 puts("vis-menu " VERSION
);
579 } else if (!strcmp(argv
[i
], "-i")) {
580 fstrncmp
= strncasecmp
;
581 } else if (!strcmp(argv
[i
], "-t")) {
583 } else if (!strcmp(argv
[i
], "-b")) {
585 } else if (argv
[i
][0] != '-') {
586 strncpy(text
, argv
[i
], sizeof(text
)-1);
587 cursor
= strlen(text
);
588 } else if (i
+ 1 == argc
) {
590 } else if (!strcmp(argv
[i
], "-p")) {
592 if (prompt
&& !prompt
[0])
594 } else if (!strcmp(argv
[i
], "-l")) {
596 lines
= strtoul(argv
[++i
], NULL
, 10);