Added draft for the completion framework
[iomenu.git] / main.c
bloba9cb9e831a9497120eac9f0d9ab0e204ccefc073
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 = stdin;
44 if (!fp) {
45 die("Can not open file for reading.");
48 /* read the file into a doubly linked list of lines */
49 while (fgets(string, LINE_SIZE, fp)) {
50 buffer->total++;
51 last = add_line(buffer, buffer->total, string, separator, last);
54 /* set the buffer stats */
55 buffer->current = buffer->first;
57 /* empty line */
58 buffer->empty = malloc(sizeof(Line));
59 buffer->empty->content = "";
60 buffer->empty->comment = "";
61 buffer->empty->number = 0;
62 buffer->empty->matches = 0;
63 buffer->empty->next = buffer->first;
64 buffer->empty->prev = buffer->first;
66 return buffer;
70 * Add a line to the end of the current buffer.
72 * This requires to create a new line with a link to the previous line
73 * and to NULL as the next line.
75 * The previous line's 'next' should be relinked to this new line.
77 * The header's last line have to point to this last line
79 Line *
80 add_line(Buffer *buffer, int number, char *string, char *separator, Line *prev)
82 /* allocate new line */
83 Line *line = malloc(sizeof(Line));
84 line = parse_line(string, separator);
85 line->next = NULL;
86 line->prev = NULL;
87 buffer->last = line;
88 line->number = number;
90 /* interlink with previous line if exists */
91 if (number == 1) {
92 buffer->first = line;
93 } else {
94 prev->next = line;
95 line->prev = prev;
98 return line;
102 * Parse the line content to determine if it is a header and identify the
103 * separator if any.
105 Line *
106 parse_line(char *s, char *separator)
108 Line *line = malloc(sizeof(Line));
109 char *sep = separator ? strstr(s, separator) : NULL;
110 int pos = sep ? (int) (sep - s) : (int) strlen(s) - 1;
112 /* strip trailing newline */
113 s[strlen(s) - 1] = '\0';
115 /* fill line->content */
116 line->content = malloc((pos + 1) * sizeof(char));
117 strncpy(line->content, s, pos);
119 /* fill line->comment */
120 if (sep) {
121 line->comment = malloc((strlen(s) - pos) * sizeof(char));
122 strcpy(line->comment, s + pos + strlen(separator));
123 } else {
124 line->comment = "";
127 /* strip trailing whitespaces from line->content */
128 for (pos--; pos > 0 && isspace(line->content[pos]); pos--) {
129 line->content[pos] = '\0';
132 /* strip leading whitespaces from line->comment */
133 for (pos = 0; isspace(line->comment[pos]); pos++)
135 line->comment += pos;
137 return line;
141 * Set buffer->candidates to an array of lines that match and update
142 * buffer->matching to number of matching candidates.
144 void
145 filter_lines(Buffer *buffer)
147 Line * line = buffer->first;
148 buffer->matching = 0;
150 while (line) {
151 line->matches = line_match_input(line, buffer->input);
152 buffer->matching += line->matches;
154 line = line->next;
159 * Check if line matches and return TRUE or FALSE
162 line_match_input(Line *line, char *input)
164 if (opt_complete_mode) {
165 if (!strncmp(input, line->content, strlen(input))) {
166 return TRUE;
168 } else {
169 if (strstr(line->content, input)) {
170 return TRUE;
174 return FALSE;
178 * Seek the previous matching line, or NULL if none matches.
180 Line *
181 matching_prev(Line *line)
183 while ((line = line->prev) && !line->matches)
185 return line;
189 * Seek the next matching line, or NULL if none matches.
191 Line *
192 matching_next(Line *line)
194 while ((line = line->next) && !line->matches)
196 return line;
200 * Replace tab as a multiple of 8 spaces in a line.
202 char *
203 expand_tabs(char *line)
205 size_t i, n;
206 char *converted = malloc(sizeof(char) * (strlen(line) * 8 + 1));
208 for (i = 0, n = 0; i < strlen(line); i++, n++) {
209 if (line[i] == '\t') {
210 converted[n] = ' ';
211 n++;
213 for (; (n) % 8 != 0; n++) {
214 converted[n] = ' ';
217 n--;
218 } else {
219 converted[n] = line[i];
223 converted[n] = '\0';
225 return converted;
229 * Print a line to stderr.
231 void
232 print_line(Line *line, int current, int cols)
234 size_t i;
235 int n = 0;
236 char *content = expand_tabs(line->content);
237 char *comment = expand_tabs(line->comment);
239 /* clean the line in case it was not empty */
240 fputs("\033[K", stderr);
242 /* line number if option set */
243 if (opt_line_numbers) {
244 if (current) {
245 fputs("\033[1m", stderr);
246 } else {
247 fputs("\033[1;30m", stderr);
250 fprintf(stderr, "%7d\033[0m ", line->number);
253 n += 8;
256 /* highlight current line */
257 if (current) {
258 fputs("\033[1;33m", stderr);
261 /* print content without overflowing terminal width */
262 for (i = 0; i < strlen(content) && n < cols; n++, i++) {
263 fputc(content[i], stderr);
266 /* print spaces without overflowing terminal width */
267 for (i = n; i <= 40 && n < cols; n++, i++) {
268 fputc(' ', stderr);
271 /* comments in grey */
272 fputs("\033[1;30m", stderr);
274 /* print comment without overflowing terminal width */
275 for (i = 0; i < strlen(comment) && n < cols; n++, i++) {
276 fputc(comment[i], stderr);
279 fputs("\033[0m\n", stderr);
281 free(content);
282 free(comment);
286 * Print a header title.
288 void
289 print_header()
294 * Print all the lines from an array of pointer to lines.
296 * The total number oflines printed shall not excess 'count'.
298 void
299 print_lines(Buffer *buffer, int count, int offset, int cols)
301 Line *line = buffer->current;
302 int i = 0;
303 int j = 0;
305 /* seek back from current line to the first line to print */
306 while (line && i < count - offset) {
307 i = line->matches ? i + 1 : i;
308 line = line->prev;
310 line = line ? line : buffer->first;
312 /* print up to count lines that match the input */
313 while (line && j < count) {
314 if (line->matches) {
315 print_line(line, line == buffer->current, cols);
316 j++;
319 line = line->next;
322 /* continue up to the end of the screen clearing it */
323 for (; j < count; j++) {
324 fputs("\r\033[K\n", stderr);
329 * Update the screen interface and print all candidates.
331 * This also has to clear the previous lines.
333 void
334 update_screen(Buffer *buffer, int count, int offset, int tty_fd)
336 struct winsize w;
337 ioctl(tty_fd, TIOCGWINSZ, &w);
338 count = MIN(count, w.ws_row - 2);
340 fputs("\n", stderr);
341 print_lines(buffer, count, offset, w.ws_col);
343 /* go up to the prompt position and update it */
344 fprintf(stderr, "\033[%dA", count + 1);
345 print_prompt(buffer, w.ws_col);
348 void clear_screen(int count)
350 int i;
351 for (i = 0; i < count + 1; i++) {
352 fputs("\r\033[K\n", stderr);
355 fprintf(stderr, "\033[%dA", count + 1);
359 * Set terminal to send one char at a time for interactive mode, and return the
360 * last terminal state.
362 struct termios
363 set_terminal(int tty_fd)
365 struct termios termio_old;
366 struct termios termio_new;
368 /* set the terminal to send one key at a time. */
370 /* get the terminal's state */
371 if (tcgetattr(tty_fd, &termio_old) < 0) {
372 die("Can not get terminal attributes with tcgetattr().");
375 /* create a new modified state by switching the binary flags */
376 termio_new = termio_old;
377 termio_new.c_lflag &= ~(ICANON | ECHO | IGNBRK);
379 /* apply this state to current terminal now (TCSANOW) */
380 tcsetattr(tty_fd, TCSANOW, &termio_new);
382 return termio_old;
386 * Listen for the user input and call the appropriate functions.
388 void
389 get_input(Buffer *buffer, int count, int offset, int tty_fd)
391 FILE *tty_fp = fopen("/dev/tty", "r");
393 /* receive one character at a time from the terminal */
394 struct termios termio_old = set_terminal(tty_fd);
396 /* get input char by char from the keyboard. */
397 while (do_key(fgetc(tty_fp), buffer)) {
398 update_screen(buffer, count, offset, tty_fd);
401 /* resets the terminal to the previous state. */
402 tcsetattr(tty_fd, TCSANOW, &termio_old);
404 fclose(tty_fp);
408 * Perform action associated with key
411 do_key(char key, Buffer *buffer)
413 if (key == opt_validate_key) {
414 do_print_selection(buffer);
415 return FALSE;
418 switch (key) {
420 case CONTROL('C'):
421 return FALSE;
423 case CONTROL('U'):
424 buffer->input[0] = '\0';
425 buffer->current = buffer->first;
426 filter_lines(buffer);
427 break;
429 case CONTROL('W'):
430 do_remove_word_input(buffer);
431 filter_lines(buffer);
432 break;
434 case 127:
435 case CONTROL('H'): /* backspace */
436 buffer->input[strlen(buffer->input) - 1] = '\0';
437 filter_lines(buffer);
439 if (!buffer->current->matches) {
440 do_jump(buffer, BOTH);
442 break;
444 case CONTROL('N'):
445 do_jump(buffer, NEXT);
446 break;
448 case CONTROL('P'):
449 do_jump(buffer, PREV);
450 break;
452 case CONTROL('I'): /* tab */
453 strcpy(buffer->input, buffer->current->content);
454 filter_lines(buffer);
455 break;
457 case CONTROL('M'):
458 case CONTROL('J'): /* enter */
459 do_print_selection(buffer);
460 return FALSE;
462 default:
463 do_add_character(buffer, key);
466 return TRUE;
470 * Set the current line to the next/previous/closest matching line.
472 void
473 do_jump(Buffer *buffer, int direction)
475 Line * line = buffer->current;
477 if (direction == BOTH) {
478 line = matching_next(buffer->current);
479 line = (line) ? line : matching_prev(buffer->current);
480 } else if (direction == NEXT) {
481 line = matching_next(line);
482 } else if (direction == PREV) {
483 line = matching_prev(line);
486 buffer->current = (line) ? line : buffer->current;
488 if (opt_print_numbers)
489 do_print_selection(buffer);
493 * Send the selection to stdout.
495 void
496 do_print_selection(Buffer *buffer)
498 fputs("\r\033[K", stderr);
500 if (opt_print_numbers) {
501 printf("%d\n", buffer->current->number);
502 } else {
503 if (opt_complete_mode) {
504 puts(buffer->input);
505 } else {
506 puts(buffer->current->content);
512 * Remove the last word from the buffer's input
514 void
515 do_remove_word_input(Buffer *buffer)
517 size_t length = strlen(buffer->input) - 1;
518 int i;
520 for (i = length; i >= 0 && isspace(buffer->input[i]); i--) {
521 buffer->input[i] = '\0';
524 length = strlen(buffer->input) - 1;
525 for (i = length; i >= 0 && !isspace(buffer->input[i]); i--) {
526 buffer->input[i] = '\0';
531 * Add a character to the buffer input and filter lines again.
533 void
534 do_add_character(Buffer *buffer, char key)
536 size_t length = strlen(buffer->input);
538 if (isprint(key)) {
539 buffer->input[length] = key;
540 buffer->input[length + 1] = '\0';
543 filter_lines(buffer);
545 if (!buffer->current->matches) {
546 do_jump(buffer, BOTH);
549 if (!buffer->current->matches) {
550 buffer->current = buffer->empty;
555 * Print the prompt, before the input, with the number of candidates that
556 * match.
558 void
559 print_prompt(Buffer *buffer, int cols)
561 size_t i;
562 int matching = buffer->matching;
563 int total = buffer->total;
564 char *input = expand_tabs(buffer->input);
565 char *suggest = expand_tabs(buffer->current->content);
567 /* for the '/' separator between the numbers */
568 cols--;
570 /* number of digits */
571 for (i = matching; i; i /= 10, cols--)
573 for (i = total; i; i /= 10, cols--)
576 /* 0 also has one digit*/
577 cols -= !matching ? 1 : 0;
579 /* actual prompt */
580 fprintf(stderr, "\r%s\033[K> ", opt_prompt);
581 cols -= 2 + strlen(opt_prompt);
583 /* input without overflowing terminal width */
584 for (i = 0; i < strlen(input) && cols > 0; cols--, i++) {
585 fputc(input[i], stderr);
588 /* save the cursor position at the end of the input */
589 fputs("\033[s", stderr);
591 /* grey */
592 fputs("\033[1;30m", stderr);
594 /* suggest without overflowing terminal width */
595 if (opt_complete_mode) {
596 for (; i < strlen(suggest) && cols > 0; cols--, i++) {
597 fputc(suggest[i], stderr);
601 /* go to the end of the line */
602 for (i = 0; cols > 0; cols--, i++) {
603 fputc(' ', stderr);
606 /* total match and line count at the end of the line */
607 fprintf(stderr, "%d/%d", matching, total);
609 /* restore cursor position at the end of the input */
610 fputs("\033[0m\033[u", stderr);
612 free(input);
613 free(suggest);
617 * Reset the terminal state and exit with error.
619 void
620 die(const char *s)
622 /* tcsetattr(STDIN_FILENO, TCSANOW, &termio_old); */
623 fprintf(stderr, "%s\n", s);
624 exit(EXIT_FAILURE);
628 main(int argc, char *argv[])
630 int i;
631 Buffer *buffer = NULL;
632 int offset = 3;
633 int tty_fd = open("/dev/tty", O_RDWR);
635 /* command line arguments */
636 for (i = 0; i < argc; i++) {
637 if (argv[i][0] == '-') {
638 switch (argv[i][1]) {
639 case 'n':
640 opt_line_numbers = TRUE;
641 break;
642 case 'c':
643 opt_complete_mode = TRUE;
644 break;
645 case 'N':
646 opt_print_numbers = TRUE;
647 opt_line_numbers = TRUE;
648 break;
649 case 'k':
650 opt_validate_key = (argv[i++][0] == '^') ?
651 CONTROL(argv[i][1]):
652 argv[i][0];
653 break;
654 case 's':
655 opt_separator = argv[++i];
656 break;
657 case 'l':
658 if (sscanf(argv[++i], "%d", &opt_lines) <= 0)
659 die("Wrong number format after -l.");
660 break;
661 case 'p':
662 opt_prompt = argv[++i];
663 break;
668 /* command line arguments */
669 buffer = fill_buffer(opt_separator);
671 /* set the interface */
672 filter_lines(buffer);
673 update_screen(buffer, opt_lines, offset, tty_fd);
675 /* listen and interact to input */
676 get_input(buffer, opt_lines, offset, tty_fd);
678 clear_screen(opt_lines);
680 /* close files descriptors and pointers, and free memory */
681 close(tty_fd);
683 return 0;