iomenu.c - iomenu - interactive terminal-based selection menu
(HTM) git clone git://bitreich.org/iomenu git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws65d7roiv6bfj7d652fid.onion/iomenu
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) Tags
(DIR) README
(DIR) 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 }