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>
45 #define CONTROL(ch) (ch ^ 0x40)
46 #define MIN(a,b) ((a) < (b) ? (a) : (b))
47 #define MAX(a,b) ((a) > (b) ? (a) : (b))
54 typedef struct Item Item
;
60 static char text
[BUFSIZ
] = "";
61 static int barpos
= 0;
64 static int inputw
, promptw
;
66 static char *prompt
= NULL
;
67 static Item
*items
= NULL
;
68 static Item
*matches
, *matchend
;
69 static Item
*prev
, *curr
, *next
, *sel
;
70 static struct termios tio_old
, tio_new
;
71 static int (*fstrncmp
)(const char *, const char *, size_t) = strncmp
;
74 appenditem(Item
*item
, Item
**list
, Item
**last
) {
78 (*last
)->right
= item
;
85 textwn(const char *s
, int l
) {
86 int b
, c
; /* bytes and UTF-8 characters */
88 for(b
=c
=0; s
&& s
[b
] && (l
<0 || b
<l
); b
++) if((s
[b
] & 0xc0) != 0x80) c
++;
89 return c
+4; /* Accomodate for the leading and trailing spaces */
93 textw(const char *s
) {
104 n
= mw
- (promptw
+ inputw
+ textw("<") + textw(">"));
106 for (i
= 0, next
= curr
; next
; next
= next
->right
)
107 if ((i
+= (lines
>0 ? 1 : MIN(textw(next
->text
), n
))) > n
)
109 for (i
= 0, prev
= curr
; prev
&& prev
->left
; prev
= prev
->left
)
110 if ((i
+= (lines
>0 ? 1 : MIN(textw(prev
->left
->text
), n
))) > n
)
116 if (barpos
== 0) fprintf(stderr
, "\n");
117 else fprintf(stderr
, "\033[G\033[K");
118 tcsetattr(0, TCSANOW
, &tio_old
);
123 tcsetattr(0, TCSANOW
, &tio_old
);
124 fprintf(stderr
, "%s\n", s
);
129 drawtext(const char *t
, size_t w
, Color col
) {
130 const char *prestr
, *poststr
;
134 if (w
<5) return; /* This is the minimum size needed to write a label: 1 char + 4 padding spaces */
135 tw
= w
-4; /* This is the text width, without the padding */
136 if (!(buf
= calloc(1, tw
+1))) die("Can't calloc.");
147 memset(buf
, ' ', tw
);
149 memcpy(buf
, t
, MIN(strlen(t
), tw
));
150 if (textw(t
) > w
) /* Remember textw returns the width WITH padding */
151 for (i
= MAX((tw
-4), 0); i
< tw
; i
++) buf
[i
] = '.';
153 fprintf(stderr
, "%s %s %s", prestr
, buf
, poststr
);
159 if (barpos
!= 0) fprintf(stderr
, "\033[%iH", barpos
> 0 ? 0 : (mh
-lines
));
160 else fprintf(stderr
, "\033[%iF", lines
);
168 /* use default colors */
169 fprintf(stderr
, "\033[0m");
171 /* place cursor in first column, clear it */
172 fprintf(stderr
, "\033[0G");
173 fprintf(stderr
, "\033[K");
176 drawtext(prompt
, promptw
, C_Reverse
);
178 drawtext(text
, (lines
==0 && matches
) ? inputw
: mw
-promptw
, C_Normal
);
181 if (barpos
!= 0) resetline();
182 for (rw
= 0, item
= curr
; item
!= next
; rw
++, item
= item
->right
) {
183 fprintf(stderr
, "\n");
184 drawtext(item
->text
, mw
, (item
== sel
) ? C_Reverse
: C_Normal
);
186 for (; rw
< lines
; rw
++)
187 fprintf(stderr
, "\n\033[K");
189 } else if (matches
) {
190 rw
= mw
-(4+promptw
+inputw
);
192 drawtext("<", 5 /*textw("<")*/, C_Normal
);
193 for (item
= curr
; item
!= next
; item
= item
->right
) {
194 drawtext(item
->text
, MIN(textw(item
->text
), rw
), (item
== sel
) ? C_Reverse
: C_Normal
);
195 if ((rw
-= textw(item
->text
)) <= 0) break;
198 fprintf(stderr
, "\033[%iG", mw
-5);
199 drawtext(">", 5 /*textw(">")*/, C_Normal
);
203 fprintf(stderr
, "\033[%iG", (int)(promptw
+textwn(text
, cursor
)-1));
208 fstrstr(const char *s
, const char *sub
) {
209 for (size_t len
= strlen(sub
); *s
; s
++)
210 if (!fstrncmp(s
, sub
, len
))
218 static char **tokv
= NULL
;
221 char buf
[sizeof text
], *s
;
223 size_t len
, textsize
;
224 Item
*item
, *lprefix
, *lsubstr
, *prefixend
, *substrend
;
227 /* separate input text into tokens to be matched individually */
228 for (s
= strtok(buf
, " "); s
; tokv
[tokc
- 1] = s
, s
= strtok(NULL
, " "))
229 if (++tokc
> tokn
&& !(tokv
= realloc(tokv
, ++tokn
* sizeof *tokv
)))
230 die("Can't realloc.");
231 len
= tokc
? strlen(tokv
[0]) : 0;
233 matches
= lprefix
= lsubstr
= matchend
= prefixend
= substrend
= NULL
;
234 textsize
= strlen(text
) + 1;
235 for (item
= items
; item
&& item
->text
; item
++) {
236 for (i
= 0; i
< tokc
; i
++)
237 if (!fstrstr(item
->text
, tokv
[i
]))
239 if (i
!= tokc
) /* not all tokens match */
241 /* exact matches go first, then prefixes, then substrings */
242 if (!tokc
|| !fstrncmp(text
, item
->text
, textsize
))
243 appenditem(item
, &matches
, &matchend
);
244 else if (!fstrncmp(tokv
[0], item
->text
, len
))
245 appenditem(item
, &lprefix
, &prefixend
);
247 appenditem(item
, &lsubstr
, &substrend
);
251 matchend
->right
= lprefix
;
252 lprefix
->left
= matchend
;
255 matchend
= prefixend
;
259 matchend
->right
= lsubstr
;
260 lsubstr
->left
= matchend
;
263 matchend
= substrend
;
265 curr
= sel
= matches
;
270 insert(const char *str
, ssize_t n
) {
271 if (strlen(text
) + n
> sizeof text
- 1)
273 memmove(&text
[cursor
+ n
], &text
[cursor
], sizeof text
- cursor
- MAX(n
, 0));
275 memcpy(&text
[cursor
], str
, n
);
284 for(n
= cursor
+ inc
; n
+ inc
>= 0 && (text
[n
] & 0xc0) == 0x80; n
+= inc
);
290 char buf
[sizeof text
], *p
, *maxstr
= NULL
;
291 size_t i
, max
= 0, size
= 0;
293 for(i
= 0; fgets(buf
, sizeof buf
, stdin
); i
++) {
294 if (i
+1 >= size
/ sizeof *items
)
295 if (!(items
= realloc(items
, (size
+= BUFSIZ
))))
296 die("Can't realloc.");
297 if ((p
= strchr(buf
, '\n')))
299 if (!(items
[i
].text
= strdup(buf
)))
300 die("Can't strdup.");
301 if (strlen(items
[i
].text
) > max
)
302 max
= textw(maxstr
= items
[i
].text
);
305 items
[i
].text
= NULL
;
306 inputw
= textw(maxstr
);
310 xread(int fd
, void *buf
, size_t nbyte
) {
311 ssize_t r
= read(fd
, buf
, nbyte
);
312 if (r
< 0 || (size_t)r
!= nbyte
)
313 die("Can not read.");
321 /* re-open stdin to read keyboard */
322 if (!freopen("/dev/tty", "r", stdin
)) die("Can't reopen tty as stdin.");
323 if (!freopen("/dev/tty", "w", stderr
)) die("Can't reopen tty as stderr.");
325 /* ioctl() the tty to get size */
326 fd
= open("/dev/tty", O_RDWR
);
331 result
= ioctl(fd
, TIOCGWINSZ
, &ws
);
342 /* change terminal attributes, save old */
343 tcgetattr(0, &tio_old
);
345 tio_new
.c_iflag
&= ~(BRKINT
|PARMRK
|ISTRIP
|INLCR
|IGNCR
|ICRNL
|IXON
);
346 tio_new
.c_lflag
&= ~(ECHO
|ECHONL
|ICANON
|ISIG
|IEXTEN
);
347 tio_new
.c_cflag
&= ~(CSIZE
|PARENB
);
348 tio_new
.c_cflag
|= CS8
;
349 tio_new
.c_cc
[VMIN
] = 1;
350 tcsetattr(0, TCSANOW
, &tio_new
);
352 lines
= MIN(MAX(lines
, 0), mh
);
353 promptw
= prompt
? textw(prompt
) : 0;
354 inputw
= MIN(inputw
, mw
/3);
356 if (barpos
!= 0) resetline();
367 memset(buf
, '\0', sizeof buf
);
375 case CONTROL('['): /* ESC, need to press twice due to console limitations */
384 if (c
!= 'H') xread(0, &c
, 1); /* Remove trailing '~' from stdin */
387 case '2': /* Insert */
388 xread(0, &c
, 1); /* Remove trailing '~' from stdin */
391 case '3': /* Delete */
392 xread(0, &c
, 1); /* Remove trailing '~' from stdin */
398 if (c
!= 'F') xread(0, &c
, 1); /* Remove trailing '~' from stdin */
401 case '5': /* PageUp */
402 xread(0, &c
, 1); /* Remove trailing '~' from stdin */
405 case '6': /* PageDown */
406 xread(0, &c
, 1); /* Remove trailing '~' from stdin */
409 case 'A': /* Up arrow */
412 case 'B': /* Down arrow */
415 case 'C': /* Right arrow */
418 case 'D': /* Left arrow */
424 while (cursor
> 0 && text
[nextrune(-1)] == ' ')
425 cursor
= nextrune(-1);
426 while (cursor
> 0 && text
[nextrune(-1)] != ' ')
427 cursor
= nextrune(-1);
430 while (text
[cursor
] != '\0' && text
[nextrune(+1)] == ' ')
431 cursor
= nextrune(+1);
432 if (text
[cursor
] != '\0') {
434 cursor
= nextrune(+1);
435 } while (text
[cursor
] != '\0' && text
[cursor
] != ' ');
439 while (text
[cursor
] != '\0' && text
[nextrune(+1)] == ' ') {
440 cursor
= nextrune(+1);
441 insert(NULL
, nextrune(-1) - cursor
);
443 if (text
[cursor
] != '\0') {
445 cursor
= nextrune(+1);
446 insert(NULL
, nextrune(-1) - cursor
);
447 } while (text
[cursor
] != '\0' && text
[cursor
] != ' ');
462 case CONTROL('M'): /* Return */
464 if (sel
) strncpy(text
, sel
->text
, sizeof(text
)-1); /* Complete the input first, when hitting return */
465 cursor
= strlen(text
);
470 case CONTROL('\\'): /* These are usually close enough to RET to replace Shift+RET, again due to console limitations */
474 if (sel
== matches
) {
478 sel
= curr
= matches
;
482 if (text
[cursor
] != '\0') {
483 cursor
= strlen(text
);
491 while(next
&& (curr
= curr
->right
))
497 if (cursor
> 0 && (!sel
|| !sel
->left
|| lines
> 0)) {
498 cursor
= nextrune(-1);
503 if (sel
&& sel
->left
&& (sel
= sel
->left
)->right
== curr
) {
509 if (text
[cursor
] != '\0') {
510 cursor
= nextrune(+1);
515 if (sel
&& sel
->right
&& (sel
= sel
->right
) == next
) {
521 if (text
[cursor
] == '\0')
523 cursor
= nextrune(+1);
526 case CONTROL('?'): /* Backspace */
529 insert(NULL
, nextrune(-1) - cursor
);
531 case CONTROL('I'): /* TAB */
534 strncpy(text
, sel
->text
, sizeof text
);
535 cursor
= strlen(text
);
543 insert(NULL
, 0 - cursor
);
546 while (cursor
> 0 && text
[nextrune(-1)] == ' ')
547 insert(NULL
, nextrune(-1) - cursor
);
548 while (cursor
> 0 && text
[nextrune(-1)] != ' ')
549 insert(NULL
, nextrune(-1) - cursor
);
559 insert(buf
, strlen(buf
));
568 fputs("usage: vis-menu [-b|-t] [-i] [-l lines] [-p prompt] [initial selection]\n", stderr
);
573 main(int argc
, char **argv
) {
574 for (int i
= 1; i
< argc
; i
++) {
575 if (!strcmp(argv
[i
], "-v")) {
576 puts("vis-menu " VERSION
);
578 } else if (!strcmp(argv
[i
], "-i")) {
579 fstrncmp
= strncasecmp
;
580 } else if (!strcmp(argv
[i
], "-t")) {
582 } else if (!strcmp(argv
[i
], "-b")) {
584 } else if (argv
[i
][0] != '-') {
585 strncpy(text
, argv
[i
], sizeof(text
)-1);
586 cursor
= strlen(text
);
587 } else if (i
+ 1 == argc
) {
589 } else if (!strcmp(argv
[i
], "-p")) {
591 if (prompt
&& !prompt
[0])
593 } else if (!strcmp(argv
[i
], "-l")) {
594 lines
= atoi(argv
[++i
]);