Cleaned a bit
[iomenu.git] / main.c
blob9d1f3b183b62255f29fa5b8c361b261f357e3232
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, MATCH = 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");
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);
123 } else {
124 line->comment = "";
127 return line;
131 * Set buffer->candidates to an array of lines that match and update
132 * buffer->matching to number of matching candidates.
134 void
135 filter_lines(Buffer *buffer)
137 Line * line = buffer->first;
138 buffer->matching = 0;
140 while (line) {
141 line->matches = line_match_input(line, buffer->input);
142 buffer->matching += line->matches;
144 line = line->next;
149 * Check if line matches and return TRUE if so
152 line_match_input(Line *line, char *input)
154 if (opt_complete_mode) {
155 if (!strncmp(input, line->content, strlen(input)))
156 return TRUE;
157 } else {
158 if (strstr(line->content, input))
159 return TRUE;
162 return FALSE;
166 * Replace tab as a multiple of 8 spaces in a line.
168 char *
169 expand_tabs(char *line)
171 size_t i, n;
172 char *converted = malloc(sizeof(char) * (strlen(line) * 8 + 1));
174 for (i = 0, n = 0; i < strlen(line); i++, n++) {
175 if (line[i] == '\t') {
176 converted[n] = ' ';
177 n++;
179 for (; (n) % 8 != 0; n++) {
180 converted[n] = ' ';
183 n--;
184 } else {
185 converted[n] = line[i];
189 converted[n] = '\0';
191 return converted;
195 * Print a line to stderr.
197 void
198 print_line(Line *line, int current, int cols)
200 size_t i;
201 int n = 0;
202 char *content = expand_tabs(line->content);
203 char *comment = expand_tabs(line->comment);
205 /* clean the line in case it was not empty */
206 fputs("\033[K", stderr);
208 /* line number if option set */
209 if (opt_line_numbers) {
210 if (current) {
211 fputs("\033[1m", stderr);
212 } else {
213 fputs("\033[1;30m", stderr);
216 fprintf(stderr, "%7d\033[0m ", line->number);
219 n += 8;
222 /* highlight current line */
223 if (current) {
224 fputs("\033[1;33m", stderr);
227 /* print content without overflowing terminal width */
228 for (i = 0; i < strlen(content) && n < cols; n++, i++) {
229 fputc(content[i], stderr);
232 /* print spaces without overflowing terminal width */
233 for (i = n; i <= 40 && n < cols; n++, i++) {
234 fputc(' ', stderr);
237 /* comments in grey */
238 fputs("\033[1;30m", stderr);
240 /* print comment without overflowing terminal width */
241 for (i = 0; i < strlen(comment) && n < cols; n++, i++) {
242 fputc(comment[i], stderr);
245 fputs("\033[0m\n", stderr);
247 free(content);
248 free(comment);
252 * Print a header title.
254 void
255 print_header()
260 * Print all the lines from an array of pointer to lines.
262 * The total number oflines printed shall not excess 'count'.
264 void
265 print_lines(Buffer *buffer, int count, int offset, int cols)
267 Line *line = buffer->current;
268 int i = 0;
269 int j = 0;
271 /* seek back from current line to the first line to print */
272 while (line && i < count - offset) {
273 i = line->matches ? i + 1 : i;
274 line = line->prev;
276 line = line ? line : buffer->first;
278 /* print up to count lines that match the input */
279 while (line && j < count) {
280 if (line->matches) {
281 print_line(line, line == buffer->current, cols);
282 j++;
285 line = line->next;
288 /* continue up to the end of the screen clearing it */
289 for (; j < count; j++) {
290 fputs("\r\033[K\n", stderr);
295 * Update the screen interface and print all candidates.
297 * This also has to clear the previous lines.
299 void
300 update_screen(Buffer *buffer, int count, int offset, int tty)
302 struct winsize w;
303 ioctl(tty, TIOCGWINSZ, &w);
305 fputs("\n", stderr);
306 print_lines(buffer, count, offset, w.ws_col);
308 /* go up to the prompt position and update it */
309 fprintf(stderr, "\033[%dA", count + 1);
310 print_prompt(buffer, w.ws_col);
313 void clear_screen(int count)
315 int i;
316 for (i = 0; i < count + 1; i++) {
317 fputs("\r\033[K\n", stderr);
320 fprintf(stderr, "\033[%dA", count + 1);
324 * Set terminal to send one char at a time for interactive mode, and return the
325 * last terminal state.
327 struct termios
328 terminal_set(int tty)
330 struct termios termio_old;
331 struct termios termio_new;
333 /* set the terminal to send one key at a time. */
335 /* get the terminal's state */
336 if (tcgetattr(tty, &termio_old) < 0) {
337 die("Can not get terminal attributes with tcgetattr().");
340 /* create a new modified state by switching the binary flags */
341 termio_new = termio_old;
342 termio_new.c_lflag &= ~(ICANON | ECHO | IGNBRK);
344 /* apply this state to current terminal now (TCSANOW) */
345 tcsetattr(tty, TCSANOW, &termio_new);
347 return termio_old;
351 * Listen for the user input and call the appropriate functions.
353 void
354 get_input(Buffer *buffer, int count, int offset, int tty)
356 FILE *tty_fd = fopen("/dev/tty", "r");
358 /* receive one character at a time from the terminal */
359 struct termios termio_old = terminal_set(tty);
361 /* get input char by char from the keyboard. */
362 while (do_key(fgetc(tty_fd), buffer)) {
363 update_screen(buffer, count, offset, tty);
366 /* resets the terminal to the previous state. */
367 tcsetattr(tty, TCSANOW, &termio_old);
369 fclose(tty_fd);
373 * Perform action associated with key
376 do_key(char key, Buffer *buffer)
378 if (key == opt_validate_key) {
379 do_print_selection(buffer);
380 return FALSE;
383 switch (key) {
385 case CONTROL('C'):
386 return FALSE;
388 case CONTROL('U'):
389 buffer->input[0] = '\0';
390 buffer->current = buffer->first;
391 filter_lines(buffer);
392 break;
394 case CONTROL('W'):
395 do_remove_word_input(buffer);
397 filter_lines(buffer);
399 break;
401 case 127:
402 case CONTROL('H'): /* backspace */
403 buffer->input[strlen(buffer->input) - 1] = '\0';
404 filter_lines(buffer);
406 if (!buffer->current->matches) {
407 do_go_to(buffer, MATCH);
409 break;
411 case CONTROL('N'):
412 do_go_to(buffer, NEXT);
413 break;
415 case CONTROL('P'):
416 do_go_to(buffer, PREV);
417 break;
419 case CONTROL('I'): /* tab */
420 if (opt_complete_mode) {
421 strcpy(buffer->input, buffer->current->content);
422 filter_lines(buffer);
423 } else {
424 do_go_to(buffer, NEXT);
426 break;
428 case CONTROL('M'):
429 case CONTROL('J'): /* enter */
430 do_print_selection(buffer);
431 return FALSE;
433 default:
434 do_add_character(buffer, key);
437 return TRUE;
441 * Set the current line to the next/previous/closest matching line.
443 void
444 do_go_to(Buffer *buffer, int to)
446 Line * line = buffer->current;
448 if (to == MATCH) {
449 do_go_to(buffer, NEXT);
450 do_go_to(buffer, PREV);
451 } else {
452 while ((line = (to == PREV ) ? line->prev : line->next)) {
453 if (line->matches) {
454 buffer->current = line;
455 break;
460 if (opt_print_numbers)
461 do_print_selection(buffer);
465 * Send the selection to stdout.
467 void
468 do_print_selection(Buffer *buffer)
470 fputs("\r\033[K", stderr);
472 if (opt_print_numbers) {
473 printf("%d\n", buffer->current->number);
474 } else {
475 if (opt_complete_mode) {
476 puts(buffer->input);
477 } else {
478 puts(buffer->current->content);
484 * Remove the last word from the buffer's input
486 void
487 do_remove_word_input(Buffer *buffer)
489 size_t length = strlen(buffer->input) - 1;
490 int i;
492 for (i = length; i >= 0 && isspace(buffer->input[i]); i--) {
493 buffer->input[i] = '\0';
496 length = strlen(buffer->input) - 1;
497 for (i = length; i >= 0 && !isspace(buffer->input[i]); i--) {
498 buffer->input[i] = '\0';
503 * Add a character to the buffer input and filter lines again.
505 void
506 do_add_character(Buffer *buffer, char key)
508 size_t length = strlen(buffer->input);
510 if (isprint(key)) {
511 buffer->input[length] = key;
512 buffer->input[length + 1] = '\0';
515 filter_lines(buffer);
517 if (!buffer->current->matches) {
518 do_go_to(buffer, MATCH);
521 if (!buffer->current->matches) {
522 buffer->current = buffer->empty;
527 * Print the prompt, before the input, with the number of candidates that
528 * match.
530 void
531 print_prompt(Buffer *buffer, int cols)
533 size_t i;
534 int digits = 0;
535 int matching = buffer->matching;
536 int total = buffer->total;
537 char *input = expand_tabs(buffer->input);
538 char *suggest = expand_tabs(buffer->current->content);
540 /* for the '/' separator between the numbers */
541 cols--;
543 /* count the number of digits */
544 for (i = matching; i; i /= 10, digits++);
545 for (i = total; i; i /= 10, digits++);
547 /* actual prompt */
548 fprintf(stderr, "\r%s\033[K> ", opt_prompt);
549 cols -= 2 + strlen(opt_prompt);
551 /* input without overflowing terminal width */
552 for (i = 0; i < strlen(input) && cols > digits; cols--, i++) {
553 fputc(input[i], stderr);
556 /* save the cursor position at the end of the input */
557 fputs("\033[s", stderr);
559 /* grey */
560 fputs("\033[1;30m", stderr);
562 /* suggest without overflowing terminal width */
563 if (opt_complete_mode) {
564 for (; i < strlen(suggest) && cols > digits; cols--, i++) {
565 fputc(suggest[i], stderr);
569 /* go to the end of the line */
570 for (i = 0; cols > digits; cols--, i++) {
571 fputc(' ', stderr);
574 /* total match and line count at the end of the line */
575 fprintf(stderr, "%d/%d", matching, total);
577 /* restore cursor position at the end of the input */
578 fputs("\033[u", stderr);
580 free(input);
581 free(suggest);
585 * Reset the terminal state and exit with error.
587 void die(const char *s)
589 /* tcsetattr(STDIN_FILENO, TCSANOW, &termio_old); */
590 fprintf(stderr, "%s\n", s);
591 exit(EXIT_FAILURE);
596 main(int argc, char *argv[])
598 int i;
599 Buffer *buffer = NULL;
600 int offset = 3;
601 int tty = open("/dev/tty", O_RDWR);
603 /* command line arguments */
604 for (i = 0; i < argc; i++) {
605 if (argv[i][0] == '-') {
606 switch (argv[i][1]) {
607 case 'n':
608 opt_line_numbers = TRUE;
609 break;
610 case 'c':
611 opt_complete_mode = TRUE;
612 break;
613 case 'N':
614 opt_print_numbers = TRUE;
615 opt_line_numbers = TRUE;
616 break;
617 case 'k':
618 opt_validate_key = (argv[i++][0] == '^') ?
619 CONTROL(argv[i][1]):
620 argv[i][0];
621 break;
622 case 's':
623 opt_separator = argv[++i];
624 break;
625 case 'l':
626 if (sscanf(argv[++i], "%d", &opt_lines) <= 0)
627 die("Wrong number format after -l.");
628 break;
629 case 'p':
630 opt_prompt = argv[++i];
631 break;
636 /* command line arguments */
637 buffer = fill_buffer(opt_separator);
639 /* set the interface */
640 filter_lines(buffer);
641 update_screen(buffer, opt_lines, offset, tty);
643 /* listen and interact to input */
644 get_input(buffer, opt_lines, offset, tty);
646 clear_screen(opt_lines);
648 return 0;