Can now be somewhat useful.
[iomenu.git] / iomenu.c
blob491eceb15cdfaa97ff8678ff8c12189884e1d22b
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 /* preprocessor macros */
17 #define LENGTH(x) (sizeof(x) / sizeof(*x))
18 #define CONTROL(char) (char ^ 0x40)
19 #define MIN(X, Y) (((X) < (Y)) ? (X) : (Y))
20 #define MAX(X, Y) (((X) > (Y)) ? (X) : (Y))
23 * Fill the buffer apropriately with the lines and headers
25 Buffer *
26 fill_buffer(char *separator)
28 /* fill buffer with string */
29 char string[LINE_SIZE];
30 Line *last = NULL;
31 Buffer *buffer = malloc(sizeof(Buffer));
32 FILE *fp = fopen("/dev/stdin", "r");
34 if (!fp) {
35 die("Can not open file for reading.");
38 /* read the file into a doubly linked list of lines */
39 while (fgets(string, LINE_SIZE, fp)) {
40 buffer->total++;
41 last = add_line(buffer, buffer->total, string, separator, last);
44 /* set the buffer stats */
45 buffer->current = buffer->first;
47 /* empty line */
48 buffer->empty = malloc(sizeof(Line));
49 buffer->empty->content = "";
50 buffer->empty->comment = "";
51 buffer->empty->number = 0;
52 buffer->empty->matches = 0;
53 buffer->empty->next = buffer->first;
54 buffer->empty->prev = buffer->first;
56 return buffer;
60 * Parse the line content to determine if it is a header and identify the
61 * separator if any.
63 Line *
64 parse_line(char *s, char *separator)
66 Line *line = malloc(sizeof(Line));
67 char *sep = strstr(s, separator);
68 int pos = !sep ? (int) strlen(s) - 1 : (int) (sep - s);
70 /* strip trailing newline */
71 s[strlen(s) - 1] = '\0';
73 /* fill line->content */
74 line->content = malloc((pos + 1) * sizeof(char));
75 strncpy(line->content, s, pos);
77 /* fill line->comment */
78 if (sep) {
79 line->comment = malloc((strlen(s) - pos) * sizeof(char));
80 strcpy(line->comment, s + pos);
81 } else {
82 line->comment = "";
85 return line;
89 * Add a line to the end of the current buffer.
91 * This requires to create a new line with a link to the previous line
92 * and to NULL as the next line.
94 * The previous line's 'next' should be relinked to this new line.
96 * The header's last line have to point to this last line
98 Line *
99 add_line(Buffer *buffer, int number, char *string, char *separator, Line *prev)
101 /* allocate new line */
102 Line *line = malloc(sizeof(Line));
103 line = parse_line(string, separator);
104 line->next = NULL;
105 line->prev = NULL;
106 buffer->last = line;
107 line->number = number;
109 /* interlink with previous line if exists */
110 if (number == 1) {
111 buffer->first = line;
112 } else {
113 prev->next = line;
114 line->prev = prev;
117 return line;
121 * Set buffer->candidates to an array of lines that match and update
122 * buffer->matching to number of matching candidates.
124 void
125 filter_lines(Buffer *buffer)
127 Line * line = buffer->first;
129 buffer->matching = 0;
131 while (line) {
132 if (line_match_input(line, buffer->input)) {
133 line->matches = TRUE;
134 buffer->matching++;
135 } else {
136 line->matches = FALSE;
139 line = line->next;
144 * Check if line matches and return TRUE if so
147 line_match_input(Line *line, char *input)
149 size_t /*i, j,*/ len_input/*, len_content*/;
150 len_input = strlen(input);
151 /*len_content = strlen(line->content);*/
153 if (!strncmp(input, line->content, len_input)) {
154 return TRUE;
155 } else {
156 return FALSE;
160 for (i = 0; i < len_content - len_input; i++) {
161 for (j = 0;
162 j < len_input && i + j < len_content
163 && tolower(input[j]) == tolower(line->content[i + j]);
164 j++)
167 if (j == len_input) {
168 return TRUE;
173 return FALSE;
177 * Replace tab as a multiple of 8 spaces in a line.
179 char *
180 expand_tabs(char *line)
182 size_t i, n;
183 char *converted = malloc(sizeof(char) * (strlen(line) * 8 + 1));
185 for (i = 0, n = 0; i < strlen(line); i++, n++) {
186 if (line[i] == '\t') {
187 converted[n] = ' ';
188 n++;
190 for (; (n) % 8 != 0; n++) {
191 converted[n] = ' ';
194 n--;
195 } else {
196 converted[n] = line[i];
200 converted[n] = '\0';
202 return converted;
206 * Print a line to stderr.
208 void
209 print_line(Line *line, int current, int cols)
211 size_t i;
212 int n = 0;
213 char *content = expand_tabs(line->content);
214 char *comment = expand_tabs(line->comment);
216 /* clean the line in case it was not empty */
217 fputs("\033[K", stderr);
219 /* line number if option set */
220 if (TRUE) {
221 if (current) {
222 fputs("\033[1m", stderr);
223 } else {
224 fputs("\033[1;30m", stderr);
227 fprintf(stderr, "%7d\033[0m ", line->number);
230 n += 8;
233 /* highlight current line */
234 if (current) {
235 fputs("\033[1;33m", stderr);
238 /* print content without overflowing terminal width */
239 for (i = 0; i < strlen(content) && n < cols; n++, i++) {
240 fputc(content[i], stderr);
243 /* print spaces without overflowing terminal width */
244 for (i = n; i <= 40 && n < cols; n++, i++) {
245 fputc(' ', stderr);
248 /* comments in grey */
249 fputs("\033[1;30m", stderr);
251 /* print comment without overflowing terminal width */
252 for (i = 0; i < strlen(comment) && n < cols; n++, i++) {
253 fputc(comment[i], stderr);
256 fputs("\033[0m\n", stderr);
258 free(content);
259 free(comment);
263 * Print a header title.
265 void
266 print_header()
271 * Print all the lines from an array of pointer to lines.
273 * The total number oflines printed shall not excess 'count'.
275 void
276 print_lines(Buffer *buffer, int count, int offset, int cols)
278 Line *line = buffer->current;
279 int i = 0;
280 int j = 0;
282 /* seek back from current line to the first line to print */
283 while (line && i < count - offset) {
284 i = line->matches ? i + 1 : i;
285 line = line->prev;
287 line = line ? line : buffer->first;
289 /* print up to count lines that match the input */
290 while (line && j < count) {
291 if (line->matches) {
292 print_line(line, line == buffer->current, cols);
293 j++;
296 line = line->next;
299 /* continue up to the end of the screen clearing it */
300 for (; j < count; j++) {
301 fputs("\r\033[K\n", stderr);
306 * Update the screen interface and print all candidates.
308 * This also has to clear the previous lines.
310 void
311 update_screen(Buffer *buffer, int count, int offset, int tty)
313 struct winsize w;
314 ioctl(tty, TIOCGWINSZ, &w);
316 fputs("\n", stderr);
317 print_lines(buffer, count, offset, w.ws_col);
319 /* go up to the prompt position and update it */
320 fprintf(stderr, "\033[%dA", count + 1);
321 print_prompt(buffer, w.ws_col);
324 void clear_screen(int count)
326 int i;
327 for (i = 0; i < count + 1; i++) {
328 fputs("\r\033[K\n", stderr);
331 fprintf(stderr, "\033[%dA", count + 1);
335 * Set terminal to send one char at a time for interactive mode, and return the
336 * last terminal state.
338 struct termios
339 terminal_set(int tty)
341 struct termios termio_old;
342 struct termios termio_new;
344 /* set the terminal to send one key at a time. */
346 /* get the terminal's state */
347 if (tcgetattr(tty, &termio_old) < 0) {
348 die("Can not get terminal attributes with tcgetattr().");
351 /* create a new modified state by switching the binary flags */
352 termio_new = termio_old;
353 termio_new.c_lflag &= ~(ICANON | ECHO | IGNBRK);
355 /* apply this state to current terminal now (TCSANOW) */
356 tcsetattr(tty, TCSANOW, &termio_new);
358 return termio_old;
362 * Listen for the user input and call the appropriate functions.
364 void
365 get_input(Buffer *buffer, int count, int offset, int tty)
367 FILE *tty_fd = fopen("/dev/tty", "r");
369 /* receive one character at a time from the terminal */
370 struct termios termio_old = terminal_set(tty);
372 /* get input char by char from the keyboard. */
373 while (do_key(fgetc(tty_fd), buffer)) {
374 update_screen(buffer, count, offset, tty);
377 /* resets the terminal to the previous state. */
378 tcsetattr(tty, TCSANOW, &termio_old);
380 fclose(tty_fd);
384 * Perform action associated with key
387 do_key(char key, Buffer *buffer)
389 size_t length;
390 int i;
392 switch (key) {
394 case CONTROL('C'):
395 return FALSE;
397 case CONTROL('U'):
398 buffer->input[0] = '\0';
399 buffer->current = buffer->first;
400 filter_lines(buffer);
401 break;
403 case CONTROL('W'):
404 length = strlen(buffer->input) - 1;
406 for (i = length; i >= 0 && isspace(buffer->input[i]); i--) {
407 buffer->input[i] = '\0';
410 length = strlen(buffer->input) - 1;
411 for (i = length; i >= 0 && !isspace(buffer->input[i]); i--) {
412 buffer->input[i] = '\0';
415 filter_lines(buffer);
417 break;
419 case 127:
420 case CONTROL('H'): /* backspace */
421 buffer->input[strlen(buffer->input) - 1] = '\0';
422 filter_lines(buffer);
424 if (!buffer->current->matches) {
425 do_next_line(buffer);
426 do_prev_line(buffer);
428 break;
430 case CONTROL('N'):
431 do_next_line(buffer);
432 break;
434 case CONTROL('P'):
435 do_prev_line(buffer);
436 break;
438 case CONTROL('I'): /* tab */
439 strcpy(buffer->input, buffer->current->content);
441 filter_lines(buffer);
442 break;
444 case CONTROL('M'):
445 case CONTROL('J'): /* enter */
446 fputs("\r\033[K", stderr);
447 puts(buffer->current->content);
448 return FALSE;
450 default:
451 if (isprint(key)) {
452 length = strlen(buffer->input);
453 buffer->input[length] = key;
454 buffer->input[length + 1] = '\0';
457 filter_lines(buffer);
459 if (!buffer->current->matches) {
460 do_next_line(buffer);
461 do_prev_line(buffer);
464 if (!buffer->current->matches) {
465 buffer->current = buffer->empty;
469 return TRUE;
473 * Set the current line to the next matching line, if any.
475 void
476 do_prev_line(Buffer *buffer)
478 Line * line = buffer->current;
480 while (line->prev) {
481 line = line->prev;
483 if (line->matches) {
484 buffer->current = line;
485 break;
492 * Set the current line to the next matching line, if any.
494 void
495 do_next_line(Buffer *buffer)
497 Line * line = buffer->current;
499 while (line->next) {
500 line = line->next;
502 if (line->matches) {
503 buffer->current = line;
504 break;
511 * Print the prompt, before the input, with the number of candidates that
512 * match.
514 void
515 print_prompt(Buffer *buffer, int cols)
517 size_t i;
518 int digits = 0;
519 int matching = buffer->matching;
520 int total = buffer->total;
521 char *input = expand_tabs(buffer->input);
522 char *suggest = expand_tabs(buffer->current->content);
524 /* for the '/' separator between the numbers */
525 cols--;
527 /* count the number of digits */
528 for (i = matching; i; i /= 10, digits++);
529 for (i = total; i; i /= 10, digits++);
531 /* actual prompt */
532 fputs("\r\033[K> ", stderr);
533 cols -= 2;
535 /* input without overflowing terminal width */
536 for (i = 0; i < strlen(input) && cols > digits; cols--, i++) {
537 fputc(input[i], stderr);
540 /* save the cursor position at the end of the input */
541 fputs("\033[s", stderr);
543 /* grey */
544 fputs("\033[1;30m", stderr);
546 /* suggest without overflowing terminal width */
547 for (; i < strlen(suggest) && cols > digits; cols--, i++) {
548 fputc(suggest[i], stderr);
551 /* go to the end of the line */
552 for (i = 0; cols > digits; cols--, i++) {
553 fputc(' ', stderr);
556 /* total match and line count at the end of the line */
557 fprintf(stderr, "%d/%d", matching, total);
559 /* restore cursor position at the end of the input */
560 fputs("\033[u", stderr);
562 free(input);
563 free(suggest);
567 * Reset the terminal state and exit with error.
569 void die(const char *s)
571 /* tcsetattr(STDIN_FILENO, TCSANOW, &termio_old); */
572 fprintf(stderr, "%s\n", s);
573 exit(EXIT_FAILURE);
578 main(int argc, char *argv[])
580 int i;
581 Buffer *buffer = NULL;
582 char *separator = "";
583 int count = 30;
584 int offset = 3;
585 int tty = open("/dev/tty", O_RDWR);
587 /* command line arguments */
588 for (i = 0; i <= argc; i++) {
589 if (argv[i][1] == '-') {
594 /* command line arguments */
595 buffer = fill_buffer(separator);
597 /* set the interface */
598 filter_lines(buffer);
599 update_screen(buffer, count, offset, tty);
601 /* listen and interact to input */
602 get_input(buffer, count, offset, tty);
604 clear_screen(count);
606 return 0;