Use standard stdin rather than hardcoded /dev/stdin
[iomenu.git] / main.c
blobffa20df11812fdcb8968b1e522a8773f3830b3ee
1 #include <ctype.h>
2 #include <fcntl.h>
3 #include <stdio.h>
4 #include <stdlib.h>
5 #include <string.h>
6 #include <sys/ioctl.h>
7 #include <termios.h>
8 #include <unistd.h>
10 #include "main.h"
11 #include "config.h"
13 /* add abstraction at no cost, but may add more complexity as well? */
14 enum { FALSE = 0, TRUE = 1 };
15 enum { NEXT = 0, PREV = 1, BOTH = 2 };
17 /* preprocessor macros */
18 #define LENGTH(x) (sizeof(x) / sizeof(*x))
19 #define CONTROL(char) (char ^ 0x40)
20 #define MIN(X, Y) (((X) < (Y)) ? (X) : (Y))
21 #define MAX(X, Y) (((X) > (Y)) ? (X) : (Y))
23 /* command line options */
24 int opt_line_numbers = FALSE;
25 int opt_complete_mode = FALSE;
26 int opt_print_numbers = FALSE;
27 char opt_validate_key = CONTROL('M');
28 char* opt_separator = NULL;
29 int opt_lines = 30;
30 char* opt_prompt = "";
33 * Fill the buffer apropriately with the lines and headers.
35 Buffer *
36 fill_buffer(char *separator)
38 /* fill buffer with string */
39 char string[LINE_SIZE];
40 Line *last = NULL;
41 Buffer *buffer = malloc(sizeof(Buffer));
42 /* FILE *fp = fopen("/dev/stdin", "r"); */
43 FILE *fp = stdin;
45 if (!fp) {
46 die("Can not open file for reading.");
49 /* read the file into a doubly linked list of lines */
50 while (fgets(string, LINE_SIZE, fp)) {
51 buffer->total++;
52 last = add_line(buffer, buffer->total, string, separator, last);
55 /* set the buffer stats */
56 buffer->current = buffer->first;
58 /* empty line */
59 buffer->empty = malloc(sizeof(Line));
60 buffer->empty->content = "";
61 buffer->empty->comment = "";
62 buffer->empty->number = 0;
63 buffer->empty->matches = 0;
64 buffer->empty->next = buffer->first;
65 buffer->empty->prev = buffer->first;
67 return buffer;
71 * Add a line to the end of the current buffer.
73 * This requires to create a new line with a link to the previous line
74 * and to NULL as the next line.
76 * The previous line's 'next' should be relinked to this new line.
78 * The header's last line have to point to this last line
80 Line *
81 add_line(Buffer *buffer, int number, char *string, char *separator, Line *prev)
83 /* allocate new line */
84 Line *line = malloc(sizeof(Line));
85 line = parse_line(string, separator);
86 line->next = NULL;
87 line->prev = NULL;
88 buffer->last = line;
89 line->number = number;
91 /* interlink with previous line if exists */
92 if (number == 1) {
93 buffer->first = line;
94 } else {
95 prev->next = line;
96 line->prev = prev;
99 return line;
103 * Parse the line content to determine if it is a header and identify the
104 * separator if any.
106 Line *
107 parse_line(char *s, char *separator)
109 Line *line = malloc(sizeof(Line));
110 char *sep = separator ? strstr(s, separator) : NULL;
111 int pos = sep ? (int) (sep - s) : (int) strlen(s) - 1;
113 /* strip trailing newline */
114 s[strlen(s) - 1] = '\0';
116 /* fill line->content */
117 line->content = malloc((pos + 1) * sizeof(char));
118 strncpy(line->content, s, pos);
120 /* fill line->comment */
121 if (sep) {
122 line->comment = malloc((strlen(s) - pos) * sizeof(char));
123 strcpy(line->comment, s + pos);
124 } else {
125 line->comment = "";
128 return line;
132 * Set buffer->candidates to an array of lines that match and update
133 * buffer->matching to number of matching candidates.
135 void
136 filter_lines(Buffer *buffer)
138 Line * line = buffer->first;
139 buffer->matching = 0;
141 while (line) {
142 line->matches = line_match_input(line, buffer->input);
143 buffer->matching += line->matches;
145 line = line->next;
150 * Check if line matches and return TRUE or FALSE
153 line_match_input(Line *line, char *input)
155 if (opt_complete_mode) {
156 if (!strncmp(input, line->content, strlen(input))) {
157 return TRUE;
159 } else {
160 if (strstr(line->content, input)) {
161 return TRUE;
165 return FALSE;
169 * Seek the previous matching line, or NULL if none matches.
171 Line *
172 matching_prev(Line *line)
174 while ((line = line->prev) && !line->matches)
176 return line;
180 * Seek the next matching line, or NULL if none matches.
182 Line *
183 matching_next(Line *line)
185 while ((line = line->next) && !line->matches)
187 return line;
191 * Replace tab as a multiple of 8 spaces in a line.
193 char *
194 expand_tabs(char *line)
196 size_t i, n;
197 char *converted = malloc(sizeof(char) * (strlen(line) * 8 + 1));
199 for (i = 0, n = 0; i < strlen(line); i++, n++) {
200 if (line[i] == '\t') {
201 converted[n] = ' ';
202 n++;
204 for (; (n) % 8 != 0; n++) {
205 converted[n] = ' ';
208 n--;
209 } else {
210 converted[n] = line[i];
214 converted[n] = '\0';
216 return converted;
220 * Print a line to stderr.
222 void
223 print_line(Line *line, int current, int cols)
225 size_t i;
226 int n = 0;
227 char *content = expand_tabs(line->content);
228 char *comment = expand_tabs(line->comment);
230 /* clean the line in case it was not empty */
231 fputs("\033[K", stderr);
233 /* line number if option set */
234 if (opt_line_numbers) {
235 if (current) {
236 fputs("\033[1m", stderr);
237 } else {
238 fputs("\033[1;30m", stderr);
241 fprintf(stderr, "%7d\033[0m ", line->number);
244 n += 8;
247 /* highlight current line */
248 if (current) {
249 fputs("\033[1;33m", stderr);
252 /* print content without overflowing terminal width */
253 for (i = 0; i < strlen(content) && n < cols; n++, i++) {
254 fputc(content[i], stderr);
257 /* print spaces without overflowing terminal width */
258 for (i = n; i <= 40 && n < cols; n++, i++) {
259 fputc(' ', stderr);
262 /* comments in grey */
263 fputs("\033[1;30m", stderr);
265 /* print comment without overflowing terminal width */
266 for (i = 0; i < strlen(comment) && n < cols; n++, i++) {
267 fputc(comment[i], stderr);
270 fputs("\033[0m\n", stderr);
272 free(content);
273 free(comment);
277 * Print a header title.
279 void
280 print_header()
285 * Print all the lines from an array of pointer to lines.
287 * The total number oflines printed shall not excess 'count'.
289 void
290 print_lines(Buffer *buffer, int count, int offset, int cols)
292 Line *line = buffer->current;
293 int i = 0;
294 int j = 0;
296 /* seek back from current line to the first line to print */
297 while (line && i < count - offset) {
298 i = line->matches ? i + 1 : i;
299 line = line->prev;
301 line = line ? line : buffer->first;
303 /* print up to count lines that match the input */
304 while (line && j < count) {
305 if (line->matches) {
306 print_line(line, line == buffer->current, cols);
307 j++;
310 line = line->next;
313 /* continue up to the end of the screen clearing it */
314 for (; j < count; j++) {
315 fputs("\r\033[K\n", stderr);
320 * Update the screen interface and print all candidates.
322 * This also has to clear the previous lines.
324 void
325 update_screen(Buffer *buffer, int count, int offset, int tty)
327 struct winsize w;
328 ioctl(tty, TIOCGWINSZ, &w);
329 count = MIN(count, w.ws_row - 2);
331 fputs("\n", stderr);
332 print_lines(buffer, count, offset, w.ws_col);
334 /* go up to the prompt position and update it */
335 fprintf(stderr, "\033[%dA", count + 1);
336 print_prompt(buffer, w.ws_col);
339 void clear_screen(int count)
341 int i;
342 for (i = 0; i < count + 1; i++) {
343 fputs("\r\033[K\n", stderr);
346 fprintf(stderr, "\033[%dA", count + 1);
350 * Set terminal to send one char at a time for interactive mode, and return the
351 * last terminal state.
353 struct termios
354 terminal_set(int tty)
356 struct termios termio_old;
357 struct termios termio_new;
359 /* set the terminal to send one key at a time. */
361 /* get the terminal's state */
362 if (tcgetattr(tty, &termio_old) < 0) {
363 die("Can not get terminal attributes with tcgetattr().");
366 /* create a new modified state by switching the binary flags */
367 termio_new = termio_old;
368 termio_new.c_lflag &= ~(ICANON | ECHO | IGNBRK);
370 /* apply this state to current terminal now (TCSANOW) */
371 tcsetattr(tty, TCSANOW, &termio_new);
373 return termio_old;
377 * Listen for the user input and call the appropriate functions.
379 void
380 get_input(Buffer *buffer, int count, int offset, int tty)
382 FILE *tty_fd = fopen("/dev/tty", "r");
384 /* receive one character at a time from the terminal */
385 struct termios termio_old = terminal_set(tty);
387 /* get input char by char from the keyboard. */
388 while (do_key(fgetc(tty_fd), buffer)) {
389 update_screen(buffer, count, offset, tty);
392 /* resets the terminal to the previous state. */
393 tcsetattr(tty, TCSANOW, &termio_old);
395 fclose(tty_fd);
399 * Perform action associated with key
402 do_key(char key, Buffer *buffer)
404 if (key == opt_validate_key) {
405 do_print_selection(buffer);
406 return FALSE;
409 switch (key) {
411 case CONTROL('C'):
412 return FALSE;
414 case CONTROL('U'):
415 buffer->input[0] = '\0';
416 buffer->current = buffer->first;
417 filter_lines(buffer);
418 break;
420 case CONTROL('W'):
421 do_remove_word_input(buffer);
422 filter_lines(buffer);
423 break;
425 case 127:
426 case CONTROL('H'): /* backspace */
427 buffer->input[strlen(buffer->input) - 1] = '\0';
428 filter_lines(buffer);
430 if (!buffer->current->matches) {
431 do_jump(buffer, BOTH);
433 break;
435 case CONTROL('N'):
436 do_jump(buffer, NEXT);
437 break;
439 case CONTROL('P'):
440 do_jump(buffer, PREV);
441 break;
443 case CONTROL('I'): /* tab */
444 strcpy(buffer->input, buffer->current->content);
445 filter_lines(buffer);
446 break;
448 case CONTROL('M'):
449 case CONTROL('J'): /* enter */
450 do_print_selection(buffer);
451 return FALSE;
453 default:
454 do_add_character(buffer, key);
457 return TRUE;
461 * Set the current line to the next/previous/closest matching line.
463 void
464 do_jump(Buffer *buffer, int direction)
466 Line * line = buffer->current;
468 if (direction == BOTH) {
469 line = matching_next(buffer->current);
470 line = (line) ? line : matching_prev(buffer->current);
471 } else if (direction == NEXT) {
472 line = matching_next(line);
473 } else if (direction == PREV) {
474 line = matching_prev(line);
477 buffer->current = (line) ? line : buffer->current;
479 if (opt_print_numbers)
480 do_print_selection(buffer);
484 * Send the selection to stdout.
486 void
487 do_print_selection(Buffer *buffer)
489 fputs("\r\033[K", stderr);
491 if (opt_print_numbers) {
492 printf("%d\n", buffer->current->number);
493 } else {
494 if (opt_complete_mode) {
495 puts(buffer->input);
496 } else {
497 puts(buffer->current->content);
503 * Remove the last word from the buffer's input
505 void
506 do_remove_word_input(Buffer *buffer)
508 size_t length = strlen(buffer->input) - 1;
509 int i;
511 for (i = length; i >= 0 && isspace(buffer->input[i]); i--) {
512 buffer->input[i] = '\0';
515 length = strlen(buffer->input) - 1;
516 for (i = length; i >= 0 && !isspace(buffer->input[i]); i--) {
517 buffer->input[i] = '\0';
522 * Add a character to the buffer input and filter lines again.
524 void
525 do_add_character(Buffer *buffer, char key)
527 size_t length = strlen(buffer->input);
529 if (isprint(key)) {
530 buffer->input[length] = key;
531 buffer->input[length + 1] = '\0';
534 filter_lines(buffer);
536 if (!buffer->current->matches) {
537 do_jump(buffer, BOTH);
540 if (!buffer->current->matches) {
541 buffer->current = buffer->empty;
546 * Print the prompt, before the input, with the number of candidates that
547 * match.
549 void
550 print_prompt(Buffer *buffer, int cols)
552 size_t i;
553 int matching = buffer->matching;
554 int total = buffer->total;
555 char *input = expand_tabs(buffer->input);
556 char *suggest = expand_tabs(buffer->current->content);
558 /* for the '/' separator between the numbers */
559 cols--;
561 /* number of digits */
562 for (i = matching; i; i /= 10, cols--)
564 for (i = total; i; i /= 10, cols--)
567 /* 0 also has one digit*/
568 cols -= !matching ? 1 : 0;
570 /* actual prompt */
571 fprintf(stderr, "\r%s\033[K> ", opt_prompt);
572 cols -= 2 + strlen(opt_prompt);
574 /* input without overflowing terminal width */
575 for (i = 0; i < strlen(input) && cols > 0; cols--, i++) {
576 fputc(input[i], stderr);
579 /* save the cursor position at the end of the input */
580 fputs("\033[s", stderr);
582 /* grey */
583 fputs("\033[1;30m", stderr);
585 /* suggest without overflowing terminal width */
586 if (opt_complete_mode) {
587 for (; i < strlen(suggest) && cols > 0; cols--, i++) {
588 fputc(suggest[i], stderr);
592 /* go to the end of the line */
593 for (i = 0; cols > 0; cols--, i++) {
594 fputc(' ', stderr);
597 /* total match and line count at the end of the line */
598 fprintf(stderr, "%d/%d", matching, total);
600 /* restore cursor position at the end of the input */
601 fputs("\033[0m\033[u", stderr);
603 free(input);
604 free(suggest);
608 * Reset the terminal state and exit with error.
610 void die(const char *s)
612 /* tcsetattr(STDIN_FILENO, TCSANOW, &termio_old); */
613 fprintf(stderr, "%s\n", s);
614 exit(EXIT_FAILURE);
619 main(int argc, char *argv[])
621 int i;
622 Buffer *buffer = NULL;
623 int offset = 3;
624 int tty = open("/dev/tty", O_RDWR);
626 /* command line arguments */
627 for (i = 0; i < argc; i++) {
628 if (argv[i][0] == '-') {
629 switch (argv[i][1]) {
630 case 'n':
631 opt_line_numbers = TRUE;
632 break;
633 case 'c':
634 opt_complete_mode = TRUE;
635 break;
636 case 'N':
637 opt_print_numbers = TRUE;
638 opt_line_numbers = TRUE;
639 break;
640 case 'k':
641 opt_validate_key = (argv[i++][0] == '^') ?
642 CONTROL(argv[i][1]):
643 argv[i][0];
644 break;
645 case 's':
646 opt_separator = argv[++i];
647 break;
648 case 'l':
649 if (sscanf(argv[++i], "%d", &opt_lines) <= 0)
650 die("Wrong number format after -l.");
651 break;
652 case 'p':
653 opt_prompt = argv[++i];
654 break;
659 /* command line arguments */
660 buffer = fill_buffer(opt_separator);
662 /* set the interface */
663 filter_lines(buffer);
664 update_screen(buffer, opt_lines, offset, tty);
666 /* listen and interact to input */
667 get_input(buffer, opt_lines, offset, tty);
669 clear_screen(opt_lines);
671 return 0;