|
|
iomenu.c - iomenu - interactive terminal-based selection menu |
|
|
 |
git clone git://bitreich.org/iomenu git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws65d7roiv6bfj7d652fid.onion/iomenu (git://bitreich.org) |
|
|
 |
Log |
|
|
 |
Files |
|
|
 |
Refs |
|
|
 |
Tags |
|
|
 |
README |
|
|
 |
LICENSE |
|
|
|
--- |
|
|
|
iomenu.c (8228B) |
|
|
|
--- |
|
|
|
1 #include <ctype.h> |
|
|
|
2 #include <errno.h> |
|
|
|
3 #include <fcntl.h> |
|
|
|
4 #include <limits.h> |
|
|
|
5 #include <signal.h> |
|
|
|
6 #include <stddef.h> |
|
|
|
7 #include <stdio.h> |
|
|
|
8 #include <stdlib.h> |
|
|
|
9 #include <string.h> |
|
|
|
10 #include <sys/ioctl.h> |
|
|
|
11 #include <termios.h> |
|
|
|
12 #include <unistd.h> |
|
|
|
13 #include <assert.h> |
|
|
|
14 #include "compat.h" |
|
|
|
15 #include "term.h" |
|
|
|
16 #include "utf8.h" |
|
|
|
17 |
|
|
|
18 struct { |
|
|
|
19 char input[256]; |
|
|
|
20 size_t cur; |
|
|
|
21 |
|
|
|
22 char **lines_buf; |
|
|
|
23 size_t lines_count; |
|
|
|
24 |
|
|
|
25 char **match_buf; |
|
|
|
26 size_t match_count; |
|
|
|
27 } ctx; |
|
|
|
28 |
|
|
|
29 int flag_comment; |
|
|
|
30 |
|
|
|
31 /* |
|
|
|
32 * Keep the line if it match every token (in no particular order, |
|
|
|
33 * and allowed to be overlapping). |
|
|
|
34 */ |
|
|
|
35 static int |
|
|
|
36 match_line(char *line, char **tokv) |
|
|
|
37 { |
|
|
|
38 if (flag_comment && line[0] == '#') |
|
|
|
39 return 2; |
|
|
|
40 for (; *tokv != NULL; tokv++) |
|
|
|
41 if (strcasestr(line, *tokv) == NULL) |
|
|
|
42 return 0; |
|
|
|
43 return 1; |
|
|
|
44 } |
|
|
|
45 |
|
|
|
46 /* |
|
|
|
47 * Free the structures, reset the terminal state and exit with an |
|
|
|
48 * error message. |
|
|
|
49 */ |
|
|
|
50 static void |
|
|
|
51 die(const char *msg) |
|
|
|
52 { |
|
|
|
53 int e = errno; |
|
|
|
54 |
|
|
|
55 term_raw_off(2); |
|
|
|
56 |
|
|
|
57 fprintf(stderr, "iomenu: "); |
|
|
|
58 errno = e; |
|
|
|
59 perror(msg); |
|
|
|
60 |
|
|
|
61 exit(1); |
|
|
|
62 } |
|
|
|
63 |
|
|
|
64 void * |
|
|
|
65 xrealloc(void *ptr, size_t sz) |
|
|
|
66 { |
|
|
|
67 ptr = realloc(ptr, sz); |
|
|
|
68 if (ptr == NULL) |
|
|
|
69 die("realloc"); |
|
|
|
70 return ptr; |
|
|
|
71 } |
|
|
|
72 |
|
|
|
73 void * |
|
|
|
74 xmalloc(size_t sz) |
|
|
|
75 { |
|
|
|
76 void *ptr; |
|
|
|
77 |
|
|
|
78 ptr = malloc(sz); |
|
|
|
79 if (ptr == NULL) |
|
|
|
80 die("malloc"); |
|
|
|
81 return ptr; |
|
|
|
82 } |
|
|
|
83 |
|
|
|
84 static void |
|
|
|
85 do_move(int sign) |
|
|
|
86 { |
|
|
|
87 /* integer overflow will do what we need */ |
|
|
|
88 for (size_t i = ctx.cur + sign; i < ctx.match_count; i += sign) { |
|
|
|
89 if (flag_comment == 0 || ctx.match_buf[i][0] != '#') { |
|
|
|
90 ctx.cur = i; |
|
|
|
91 break; |
|
|
|
92 } |
|
|
|
93 } |
|
|
|
94 } |
|
|
|
95 |
|
|
|
96 /* |
|
|
|
97 * First split input into token, then match every token independently against |
|
|
|
98 * every line. The matching lines fills matches. Matches are searched inside |
|
|
|
99 * of `searchv' of size `searchc' |
|
|
|
100 */ |
|
|
|
101 static void |
|
|
|
102 do_filter(char **search_buf, size_t search_count) |
|
|
|
103 { |
|
|
|
104 char **t, *tokv[(sizeof ctx.input + 1) * sizeof(char *)]; |
|
|
|
105 char *b, buf[sizeof ctx.input]; |
|
|
|
106 |
|
|
|
107 strlcpy(buf, ctx.input, sizeof buf); |
|
|
|
108 |
|
|
|
109 for (b = buf, t = tokv; (*t = strsep(&b, " \t")) != NULL; t++) |
|
|
|
110 continue; |
|
|
|
111 *t = NULL; |
|
|
|
112 |
|
|
|
113 ctx.cur = ctx.match_count = 0; |
|
|
|
114 for (size_t n = 0; n < search_count; n++) |
|
|
|
115 if (match_line(search_buf[n], tokv)) |
|
|
|
116 ctx.match_buf[ctx.match_count++] = search_buf[n]; |
|
|
|
117 if (flag_comment && ctx.match_buf[ctx.cur][0] == '#') |
|
|
|
118 do_move(+1); |
|
|
|
119 } |
|
|
|
120 |
|
|
|
121 static void |
|
|
|
122 do_move_page(signed int sign) |
|
|
|
123 { |
|
|
|
124 int rows = term.winsize.ws_row - 1; |
|
|
|
125 size_t i = ctx.cur - ctx.cur % rows + rows * sign; |
|
|
|
126 |
|
|
|
127 if (i >= ctx.match_count) |
|
|
|
128 return; |
|
|
|
129 ctx.cur = i - 1; |
|
|
|
130 |
|
|
|
131 do_move(+1); |
|
|
|
132 } |
|
|
|
133 |
|
|
|
134 static void |
|
|
|
135 do_move_header(signed int sign) |
|
|
|
136 { |
|
|
|
137 do_move(sign); |
|
|
|
138 |
|
|
|
139 if (flag_comment == 0) |
|
|
|
140 return; |
|
|
|
141 for (ctx.cur += sign;; ctx.cur += sign) { |
|
|
|
142 char *cur = ctx.match_buf[ctx.cur]; |
|
|
|
143 |
|
|
|
144 if (ctx.cur >= ctx.match_count) { |
|
|
|
145 ctx.cur--; |
|
|
|
146 break; |
|
|
|
147 } |
|
|
|
148 if (cur[0] == '#') |
|
|
|
149 break; |
|
|
|
150 } |
|
|
|
151 |
|
|
|
152 do_move(+1); |
|
|
|
153 } |
|
|
|
154 |
|
|
|
155 static void |
|
|
|
156 do_remove_word(void) |
|
|
|
157 { |
|
|
|
158 int len, i; |
|
|
|
159 |
|
|
|
160 len = strlen(ctx.input) - 1; |
|
|
|
161 for (i = len; i >= 0 && isspace(ctx.input[i]); i--) |
|
|
|
162 ctx.input[i] = '\0'; |
|
|
|
163 len = strlen(ctx.input) - 1; |
|
|
|
164 for (i = len; i >= 0 && !isspace(ctx.input[i]); i--) |
|
|
|
165 ctx.input[i] = '\0'; |
|
|
|
166 do_filter(ctx.lines_buf, ctx.lines_count); |
|
|
|
167 } |
|
|
|
168 |
|
|
|
169 static void |
|
|
|
170 do_add_char(char c) |
|
|
|
171 { |
|
|
|
172 int len; |
|
|
|
173 |
|
|
|
174 len = strlen(ctx.input); |
|
|
|
175 if (len + 1 == sizeof ctx.input) |
|
|
|
176 return; |
|
|
|
177 if (isprint(c)) { |
|
|
|
178 ctx.input[len] = c; |
|
|
|
179 ctx.input[len + 1] = '\0'; |
|
|
|
180 } |
|
|
|
181 do_filter(ctx.match_buf, ctx.match_count); |
|
|
|
182 } |
|
|
|
183 |
|
|
|
184 static void |
|
|
|
185 do_print_selection(void) |
|
|
|
186 { |
|
|
|
187 if (flag_comment) { |
|
|
|
188 char **match = ctx.match_buf + ctx.cur; |
|
|
|
189 |
|
|
|
190 while (--match >= ctx.match_buf) { |
|
|
|
191 if ((*match)[0] == '#') { |
|
|
|
192 fprintf(stdout, "%s", *match + 1); |
|
|
|
193 break; |
|
|
|
194 } |
|
|
|
195 } |
|
|
|
196 fprintf(stdout, "%c", '\t'); |
|
|
|
197 } |
|
|
|
198 term_raw_off(2); |
|
|
|
199 if (ctx.match_count == 0 |
|
|
|
200 || (flag_comment && ctx.match_buf[ctx.cur][0] == '#')) |
|
|
|
201 fprintf(stdout, "%s\n", ctx.input); |
|
|
|
202 else |
|
|
|
203 fprintf(stdout, "%s\n", ctx.match_buf[ctx.cur]); |
|
|
|
204 term_raw_on(2); |
|
|
|
205 } |
|
|
|
206 |
|
|
|
207 /* |
|
|
|
208 * Big case table, that calls itself back for with TERM_KEY_ALT (aka Esc), TERM_KEY_CSI |
|
|
|
209 * (aka Esc + [). These last two have values above the range of ASCII. |
|
|
|
210 */ |
|
|
|
211 static int |
|
|
|
212 key_action(void) |
|
|
|
213 { |
|
|
|
214 int key; |
|
|
|
215 |
|
|
|
216 key = term_get_key(stderr); |
|
|
|
217 switch (key) { |
|
|
|
218 case TERM_KEY_CTRL('Z'): |
|
|
|
219 term_raw_off(2); |
|
|
|
220 kill(getpid(), SIGSTOP); |
|
|
|
221 term_raw_on(2); |
|
|
|
222 break; |
|
|
|
223 case TERM_KEY_CTRL('C'): |
|
|
|
224 case TERM_KEY_CTRL('D'): |
|
|
|
225 return -1; |
|
|
|
226 case TERM_KEY_CTRL('U'): |
|
|
|
227 ctx.input[0] = '\0'; |
|
|
|
228 do_filter(ctx.lines_buf, ctx.lines_count); |
|
|
|
229 break; |
|
|
|
230 case TERM_KEY_CTRL('W'): |
|
|
|
231 do_remove_word(); |
|
|
|
232 break; |
|
|
|
233 case TERM_KEY_DELETE: |
|
|
|
234 case TERM_KEY_BACKSPACE: |
|
|
|
235 ctx.input[strlen(ctx.input) - 1] = '\0'; |
|
|
|
236 do_filter(ctx.lines_buf, ctx.lines_count); |
|
|
|
237 break; |
|
|
|
238 case TERM_KEY_ARROW_UP: |
|
|
|
239 case TERM_KEY_CTRL('P'): |
|
|
|
240 do_move(-1); |
|
|
|
241 break; |
|
|
|
242 case TERM_KEY_ALT('p'): |
|
|
|
243 do_move_header(-1); |
|
|
|
244 break; |
|
|
|
245 case TERM_KEY_ARROW_DOWN: |
|
|
|
246 case TERM_KEY_CTRL('N'): |
|
|
|
247 do_move(+1); |
|
|
|
248 break; |
|
|
|
249 case TERM_KEY_ALT('n'): |
|
|
|
250 do_move_header(+1); |
|
|
|
251 break; |
|
|
|
252 case TERM_KEY_PAGE_UP: |
|
|
|
253 case TERM_KEY_ALT('v'): |
|
|
|
254 do_move_page(-1); |
|
|
|
255 break; |
|
|
|
256 case TERM_KEY_PAGE_DOWN: |
|
|
|
257 case TERM_KEY_CTRL('V'): |
|
|
|
258 do_move_page(+1); |
|
|
|
259 break; |
|
|
|
260 case TERM_KEY_TAB: |
|
|
|
261 if (ctx.match_count == 0) |
|
|
|
262 break; |
|
|
|
263 strlcpy(ctx.input, ctx.match_buf[ctx.cur], sizeof(ctx.input)); |
|
|
|
264 do_filter(ctx.match_buf, ctx.match_count); |
|
|
|
265 break; |
|
|
|
266 case TERM_KEY_ENTER: |
|
|
|
267 case TERM_KEY_CTRL('M'): |
|
|
|
268 do_print_selection(); |
|
|
|
269 return 0; |
|
|
|
270 default: |
|
|
|
271 do_add_char(key); |
|
|
|
272 } |
|
|
|
273 |
|
|
|
274 return 1; |
|
|
|
275 } |
|
|
|
276 |
|
|
|
277 static void |
|
|
|
278 print_line(char *line, int highlight) |
|
|
|
279 { |
|
|
|
280 if (flag_comment && line[0] == '#') { |
|
|
|
281 fprintf(stderr, "\n\x1b[1m\r%.*s\x1b[m", |
|
|
|
282 term_at_width(line + 1, term.winsize.ws_col, 0), line + 1); |
|
|
|
283 } else if (highlight) { |
|
|
|
284 fprintf(stderr, "\n\x1b[47;30m\x1b[K\r%.*s\x1b[m", |
|
|
|
285 term_at_width(line, term.winsize.ws_col, 0), line); |
|
|
|
286 } else { |
|
|
|
287 fprintf(stderr, "\n%.*s", |
|
|
|
288 term_at_width(line, term.winsize.ws_col, 0), line); |
|
|
|
289 } |
|
|
|
290 } |
|
|
|
291 |
|
|
|
292 static void |
|
|
|
293 do_print_screen(void) |
|
|
|
294 { |
|
|
|
295 char **m; |
|
|
|
296 int p, c, cols, rows; |
|
|
|
297 size_t i; |
|
|
|
298 |
|
|
|
299 cols = term.winsize.ws_col; |
|
|
|
300 rows = term.winsize.ws_row - 1; /* -1 to keep one line for user input */ |
|
|
|
301 p = c = 0; |
|
|
|
302 i = ctx.cur - ctx.cur % rows; |
|
|
|
303 m = ctx.match_buf + i; |
|
|
|
304 fprintf(stderr, "\x1b[2J"); |
|
|
|
305 while (p < rows && i < ctx.match_count) { |
|
|
|
306 print_line(*m, i == ctx.cur); |
|
|
|
307 p++, i++, m++; |
|
|
|
308 } |
|
|
|
309 fprintf(stderr, "\x1b[H%.*s", |
|
|
|
310 term_at_width(ctx.input, cols, c), ctx.input); |
|
|
|
311 fflush(stderr); |
|
|
|
312 } |
|
|
|
313 |
|
|
|
314 static void |
|
|
|
315 sig_winch(int sig) |
|
|
|
316 { |
|
|
|
317 if (ioctl(STDERR_FILENO, TIOCGWINSZ, &term.winsize) == -1) |
|
|
|
318 die("ioctl"); |
|
|
|
319 do_print_screen(); |
|
|
|
320 signal(sig, sig_winch); |
|
|
|
321 } |
|
|
|
322 |
|
|
|
323 static void |
|
|
|
324 usage(char const *arg0) |
|
|
|
325 { |
|
|
|
326 fprintf(stderr, "usage: %s [-#] <lines\n", arg0); |
|
|
|
327 exit(1); |
|
|
|
328 } |
|
|
|
329 |
|
|
|
330 static int |
|
|
|
331 read_stdin(char **buf) |
|
|
|
332 { |
|
|
|
333 size_t len = 0; |
|
|
|
334 |
|
|
|
335 assert(*buf == NULL); |
|
|
|
336 |
|
|
|
337 for (int c; (c = fgetc(stdin)) != EOF;) { |
|
|
|
338 if (c == '\0') { |
|
|
|
339 fprintf(stderr, "iomenu: ignoring '\\0' byte in input\r\n"); |
|
|
|
340 continue; |
|
|
|
341 } |
|
|
|
342 *buf = xrealloc(*buf, sizeof *buf + len + 1); |
|
|
|
343 (*buf)[len++] = c; |
|
|
|
344 } |
|
|
|
345 *buf = xrealloc(*buf, sizeof *buf + len + 1); |
|
|
|
346 (*buf)[len] = '\0'; |
|
|
|
347 |
|
|
|
348 return 0; |
|
|
|
349 } |
|
|
|
350 |
|
|
|
351 /* |
|
|
|
352 * Split a buffer into an array of lines, without allocating memory for every |
|
|
|
353 * line, but using the input buffer and replacing '\n' by '\0'. |
|
|
|
354 */ |
|
|
|
355 static void |
|
|
|
356 split_lines(char *s) |
|
|
|
357 { |
|
|
|
358 size_t sz; |
|
|
|
359 |
|
|
|
360 ctx.lines_count = 0; |
|
|
|
361 for (;;) { |
|
|
|
362 sz = (ctx.lines_count + 1) * sizeof s; |
|
|
|
363 ctx.lines_buf = xrealloc(ctx.lines_buf, sz); |
|
|
|
364 ctx.lines_buf[ctx.lines_count++] = s; |
|
|
|
365 |
|
|
|
366 s = strchr(s, '\n'); |
|
|
|
367 if (s == NULL) |
|
|
|
368 break; |
|
|
|
369 *s++ = '\0'; |
|
|
|
370 } |
|
|
|
371 sz = ctx.lines_count * sizeof s; |
|
|
|
372 ctx.match_buf = xmalloc(sz); |
|
|
|
373 memcpy(ctx.match_buf, ctx.lines_buf, sz); |
|
|
|
374 } |
|
|
|
375 |
|
|
|
376 /* |
|
|
|
377 * Read stdin in a buffer, filling a table of lines, then re-open stdin to |
|
|
|
378 * /dev/tty for an interactive (raw) session to let the user filter and select |
|
|
|
379 * one line by searching words within stdin. This was inspired from dmenu. |
|
|
|
380 */ |
|
|
|
381 int |
|
|
|
382 main(int argc, char *argv[]) |
|
|
|
383 { |
|
|
|
384 char *buf = NULL, *arg0; |
|
|
|
385 |
|
|
|
386 arg0 = *argv; |
|
|
|
387 for (int opt; (opt = getopt(argc, argv, "#v")) > 0;) { |
|
|
|
388 switch (opt) { |
|
|
|
389 case 'v': |
|
|
|
390 fprintf(stdout, "%s\n", VERSION); |
|
|
|
391 exit(0); |
|
|
|
392 case '#': |
|
|
|
393 flag_comment = 1; |
|
|
|
394 break; |
|
|
|
395 default: |
|
|
|
396 usage(arg0); |
|
|
|
397 } |
|
|
|
398 } |
|
|
|
399 argc -= optind; |
|
|
|
400 argv += optind; |
|
|
|
401 |
|
|
|
402 read_stdin(&buf); |
|
|
|
403 split_lines(buf); |
|
|
|
404 |
|
|
|
405 do_filter(ctx.lines_buf, ctx.lines_count); |
|
|
|
406 |
|
|
|
407 if (!isatty(2)) |
|
|
|
408 die("file descriptor 2 (stderr)"); |
|
|
|
409 |
|
|
|
410 freopen("/dev/tty", "w+", stderr); |
|
|
|
411 if (stderr == NULL) |
|
|
|
412 die("re-opening standard error read/write"); |
|
|
|
413 |
|
|
|
414 term_raw_on(2); |
|
|
|
415 sig_winch(SIGWINCH); |
|
|
|
416 |
|
|
|
417 #ifdef __OpenBSD__ |
|
|
|
418 pledge("stdio tty", NULL); |
|
|
|
419 #endif |
|
|
|
420 |
|
|
|
421 while (key_action() > 0) |
|
|
|
422 do_print_screen(); |
|
|
|
423 |
|
|
|
424 term_raw_off(2); |
|
|
|
425 |
|
|
|
426 return 0; |
|
|
|
427 } |
|