Added ghetto make install rule to Makefile
[iomenu.git] / iomenu.c
blob0db244be024e03f5814d51d543329e8f689b44ad
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 "iomenu.h"
11 #include "config.h"
13 /* add abstraction at no cost, but may add more complexity as well? */
14 enum { FALSE = 0, TRUE = 1 };
16 /* debug switches */
17 #define COMPLETE_MODE 0
18 #define LINE_NUMBERS 1
20 /* preprocessor macros */
21 #define LENGTH(x) (sizeof(x) / sizeof(*x))
22 #define CONTROL(char) (char ^ 0x40)
23 #define MIN(X, Y) (((X) < (Y)) ? (X) : (Y))
24 #define MAX(X, Y) (((X) > (Y)) ? (X) : (Y))
27 * Fill the buffer apropriately with the lines and headers
29 Buffer *
30 fill_buffer(char *separator)
32 /* fill buffer with string */
33 char string[LINE_SIZE];
34 Line *last = NULL;
35 Buffer *buffer = malloc(sizeof(Buffer));
36 FILE *fp = fopen("/dev/stdin", "r");
38 if (!fp) {
39 die("Can not open file for reading.");
42 /* read the file into a doubly linked list of lines */
43 while (fgets(string, LINE_SIZE, fp)) {
44 buffer->total++;
45 last = add_line(buffer, buffer->total, string, separator, last);
48 /* set the buffer stats */
49 buffer->current = buffer->first;
51 /* empty line */
52 buffer->empty = malloc(sizeof(Line));
53 buffer->empty->content = "";
54 buffer->empty->comment = "";
55 buffer->empty->number = 0;
56 buffer->empty->matches = 0;
57 buffer->empty->next = buffer->first;
58 buffer->empty->prev = buffer->first;
60 return buffer;
64 * Parse the line content to determine if it is a header and identify the
65 * separator if any.
67 Line *
68 parse_line(char *s, char *separator)
70 Line *line = malloc(sizeof(Line));
71 char *sep = strstr(s, separator);
72 int pos = !sep ? (int) strlen(s) - 1 : (int) (sep - s);
74 /* strip trailing newline */
75 s[strlen(s) - 1] = '\0';
77 /* fill line->content */
78 line->content = malloc((pos + 1) * sizeof(char));
79 strncpy(line->content, s, pos);
81 /* fill line->comment */
82 if (sep) {
83 line->comment = malloc((strlen(s) - pos) * sizeof(char));
84 strcpy(line->comment, s + pos);
85 } else {
86 line->comment = "";
89 return line;
93 * Add a line to the end of the current buffer.
95 * This requires to create a new line with a link to the previous line
96 * and to NULL as the next line.
98 * The previous line's 'next' should be relinked to this new line.
100 * The header's last line have to point to this last line
102 Line *
103 add_line(Buffer *buffer, int number, char *string, char *separator, Line *prev)
105 /* allocate new line */
106 Line *line = malloc(sizeof(Line));
107 line = parse_line(string, separator);
108 line->next = NULL;
109 line->prev = NULL;
110 buffer->last = line;
111 line->number = number;
113 /* interlink with previous line if exists */
114 if (number == 1) {
115 buffer->first = line;
116 } else {
117 prev->next = line;
118 line->prev = prev;
121 return line;
125 * Set buffer->candidates to an array of lines that match and update
126 * buffer->matching to number of matching candidates.
128 void
129 filter_lines(Buffer *buffer)
131 Line * line = buffer->first;
132 buffer->matching = 0;
134 while (line) {
135 line->matches = line_match_input(line, buffer->input);
136 buffer->matching += line->matches;
138 line = line->next;
143 * Check if line matches and return TRUE if so
146 line_match_input(Line *line, char *input)
148 if (COMPLETE_MODE) {
149 if (!strncmp(input, line->content, strlen(input)))
150 return TRUE;
151 } else {
152 if (strstr(line->content, input))
153 return TRUE;
156 return FALSE;
160 * Replace tab as a multiple of 8 spaces in a line.
162 char *
163 expand_tabs(char *line)
165 size_t i, n;
166 char *converted = malloc(sizeof(char) * (strlen(line) * 8 + 1));
168 for (i = 0, n = 0; i < strlen(line); i++, n++) {
169 if (line[i] == '\t') {
170 converted[n] = ' ';
171 n++;
173 for (; (n) % 8 != 0; n++) {
174 converted[n] = ' ';
177 n--;
178 } else {
179 converted[n] = line[i];
183 converted[n] = '\0';
185 return converted;
189 * Print a line to stderr.
191 void
192 print_line(Line *line, int current, int cols)
194 size_t i;
195 int n = 0;
196 char *content = expand_tabs(line->content);
197 char *comment = expand_tabs(line->comment);
199 /* clean the line in case it was not empty */
200 fputs("\033[K", stderr);
202 /* line number if option set */
203 if (LINE_NUMBERS) {
204 if (current) {
205 fputs("\033[1m", stderr);
206 } else {
207 fputs("\033[1;30m", stderr);
210 fprintf(stderr, "%7d\033[0m ", line->number);
213 n += 8;
216 /* highlight current line */
217 if (current) {
218 fputs("\033[1;33m", stderr);
221 /* print content without overflowing terminal width */
222 for (i = 0; i < strlen(content) && n < cols; n++, i++) {
223 fputc(content[i], stderr);
226 /* print spaces without overflowing terminal width */
227 for (i = n; i <= 40 && n < cols; n++, i++) {
228 fputc(' ', stderr);
231 /* comments in grey */
232 fputs("\033[1;30m", stderr);
234 /* print comment without overflowing terminal width */
235 for (i = 0; i < strlen(comment) && n < cols; n++, i++) {
236 fputc(comment[i], stderr);
239 fputs("\033[0m\n", stderr);
241 free(content);
242 free(comment);
246 * Print a header title.
248 void
249 print_header()
254 * Print all the lines from an array of pointer to lines.
256 * The total number oflines printed shall not excess 'count'.
258 void
259 print_lines(Buffer *buffer, int count, int offset, int cols)
261 Line *line = buffer->current;
262 int i = 0;
263 int j = 0;
265 /* seek back from current line to the first line to print */
266 while (line && i < count - offset) {
267 i = line->matches ? i + 1 : i;
268 line = line->prev;
270 line = line ? line : buffer->first;
272 /* print up to count lines that match the input */
273 while (line && j < count) {
274 if (line->matches) {
275 print_line(line, line == buffer->current, cols);
276 j++;
279 line = line->next;
282 /* continue up to the end of the screen clearing it */
283 for (; j < count; j++) {
284 fputs("\r\033[K\n", stderr);
289 * Update the screen interface and print all candidates.
291 * This also has to clear the previous lines.
293 void
294 update_screen(Buffer *buffer, int count, int offset, int tty)
296 struct winsize w;
297 ioctl(tty, TIOCGWINSZ, &w);
299 fputs("\n", stderr);
300 print_lines(buffer, count, offset, w.ws_col);
302 /* go up to the prompt position and update it */
303 fprintf(stderr, "\033[%dA", count + 1);
304 print_prompt(buffer, w.ws_col);
307 void clear_screen(int count)
309 int i;
310 for (i = 0; i < count + 1; i++) {
311 fputs("\r\033[K\n", stderr);
314 fprintf(stderr, "\033[%dA", count + 1);
318 * Set terminal to send one char at a time for interactive mode, and return the
319 * last terminal state.
321 struct termios
322 terminal_set(int tty)
324 struct termios termio_old;
325 struct termios termio_new;
327 /* set the terminal to send one key at a time. */
329 /* get the terminal's state */
330 if (tcgetattr(tty, &termio_old) < 0) {
331 die("Can not get terminal attributes with tcgetattr().");
334 /* create a new modified state by switching the binary flags */
335 termio_new = termio_old;
336 termio_new.c_lflag &= ~(ICANON | ECHO | IGNBRK);
338 /* apply this state to current terminal now (TCSANOW) */
339 tcsetattr(tty, TCSANOW, &termio_new);
341 return termio_old;
345 * Listen for the user input and call the appropriate functions.
347 void
348 get_input(Buffer *buffer, int count, int offset, int tty)
350 FILE *tty_fd = fopen("/dev/tty", "r");
352 /* receive one character at a time from the terminal */
353 struct termios termio_old = terminal_set(tty);
355 /* get input char by char from the keyboard. */
356 while (do_key(fgetc(tty_fd), buffer)) {
357 update_screen(buffer, count, offset, tty);
360 /* resets the terminal to the previous state. */
361 tcsetattr(tty, TCSANOW, &termio_old);
363 fclose(tty_fd);
367 * Perform action associated with key
370 do_key(char key, Buffer *buffer)
372 size_t length;
373 int i;
375 switch (key) {
377 case CONTROL('C'):
378 return FALSE;
380 case CONTROL('U'):
381 buffer->input[0] = '\0';
382 buffer->current = buffer->first;
383 filter_lines(buffer);
384 break;
386 case CONTROL('W'):
387 length = strlen(buffer->input) - 1;
389 for (i = length; i >= 0 && isspace(buffer->input[i]); i--) {
390 buffer->input[i] = '\0';
393 length = strlen(buffer->input) - 1;
394 for (i = length; i >= 0 && !isspace(buffer->input[i]); i--) {
395 buffer->input[i] = '\0';
398 filter_lines(buffer);
400 break;
402 case 127:
403 case CONTROL('H'): /* backspace */
404 buffer->input[strlen(buffer->input) - 1] = '\0';
405 filter_lines(buffer);
407 if (!buffer->current->matches) {
408 do_next_line(buffer);
409 do_prev_line(buffer);
411 break;
413 case CONTROL('N'):
414 do_next_line(buffer);
415 break;
417 case CONTROL('P'):
418 do_prev_line(buffer);
419 break;
421 case CONTROL('I'): /* tab */
422 if (COMPLETE_MODE) {
423 strcpy(buffer->input, buffer->current->content);
424 filter_lines(buffer);
425 } else {
426 do_next_line(buffer);
428 break;
430 case CONTROL('M'):
431 case CONTROL('J'): /* enter */
432 fputs("\r\033[K", stderr);
434 if (COMPLETE_MODE) {
435 puts(buffer->input);
436 } else {
437 puts(buffer->current->content);
439 return FALSE;
441 default:
442 if (isprint(key)) {
443 length = strlen(buffer->input);
444 buffer->input[length] = key;
445 buffer->input[length + 1] = '\0';
448 filter_lines(buffer);
450 if (!buffer->current->matches) {
451 do_next_line(buffer);
452 do_prev_line(buffer);
455 if (!buffer->current->matches) {
456 buffer->current = buffer->empty;
460 return TRUE;
464 * Set the current line to the next matching line, if any.
466 void
467 do_prev_line(Buffer *buffer)
469 Line * line = buffer->current;
471 while (line->prev) {
472 line = line->prev;
474 if (line->matches) {
475 buffer->current = line;
476 break;
483 * Set the current line to the next matching line, if any.
485 void
486 do_next_line(Buffer *buffer)
488 Line * line = buffer->current;
490 while (line->next) {
491 line = line->next;
493 if (line->matches) {
494 buffer->current = line;
495 break;
502 * Print the prompt, before the input, with the number of candidates that
503 * match.
505 void
506 print_prompt(Buffer *buffer, int cols)
508 size_t i;
509 int digits = 0;
510 int matching = buffer->matching;
511 int total = buffer->total;
512 char *input = expand_tabs(buffer->input);
513 char *suggest = expand_tabs(buffer->current->content);
515 /* for the '/' separator between the numbers */
516 cols--;
518 /* count the number of digits */
519 for (i = matching; i; i /= 10, digits++);
520 for (i = total; i; i /= 10, digits++);
522 /* actual prompt */
523 fputs("\r\033[K> ", stderr);
524 cols -= 2;
526 /* input without overflowing terminal width */
527 for (i = 0; i < strlen(input) && cols > digits; cols--, i++) {
528 fputc(input[i], stderr);
531 /* save the cursor position at the end of the input */
532 fputs("\033[s", stderr);
534 /* grey */
535 fputs("\033[1;30m", stderr);
537 /* suggest without overflowing terminal width */
538 if (COMPLETE_MODE) {
539 for (; i < strlen(suggest) && cols > digits; cols--, i++) {
540 fputc(suggest[i], stderr);
544 /* go to the end of the line */
545 for (i = 0; cols > digits; cols--, i++) {
546 fputc(' ', stderr);
549 /* total match and line count at the end of the line */
550 fprintf(stderr, "%d/%d", matching, total);
552 /* restore cursor position at the end of the input */
553 fputs("\033[u", stderr);
555 free(input);
556 free(suggest);
560 * Reset the terminal state and exit with error.
562 void die(const char *s)
564 /* tcsetattr(STDIN_FILENO, TCSANOW, &termio_old); */
565 fprintf(stderr, "%s\n", s);
566 exit(EXIT_FAILURE);
571 main(int argc, char *argv[])
573 int i;
574 Buffer *buffer = NULL;
575 char *separator = "separator";
576 int count = 30;
577 int offset = 3;
578 int tty = open("/dev/tty", O_RDWR);
580 /* command line arguments */
581 for (i = 0; i <= argc; i++) {
582 if (argv[i][1] == '-') {
587 /* command line arguments */
588 buffer = fill_buffer(separator);
590 /* set the interface */
591 filter_lines(buffer);
592 update_screen(buffer, count, offset, tty);
594 /* listen and interact to input */
595 get_input(buffer, count, offset, tty);
597 clear_screen(count);
599 return 0;