It seems to work now
[iomenu.git] / iomenu.c
bloba18e69cad4c98b3e93bc7752abd6f0504cd5d119
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
19 /* preprocessor macros */
20 #define LENGTH(x) (sizeof(x) / sizeof(*x))
21 #define CONTROL(char) (char ^ 0x40)
22 #define MIN(X, Y) (((X) < (Y)) ? (X) : (Y))
23 #define MAX(X, Y) (((X) > (Y)) ? (X) : (Y))
26 * Fill the buffer apropriately with the lines and headers
28 Buffer *
29 fill_buffer(char *separator)
31 /* fill buffer with string */
32 char string[LINE_SIZE];
33 Line *last = NULL;
34 Buffer *buffer = malloc(sizeof(Buffer));
35 FILE *fp = fopen("/dev/stdin", "r");
37 if (!fp) {
38 die("Can not open file for reading.");
41 /* read the file into a doubly linked list of lines */
42 while (fgets(string, LINE_SIZE, fp)) {
43 buffer->total++;
44 last = add_line(buffer, buffer->total, string, separator, last);
47 /* set the buffer stats */
48 buffer->current = buffer->first;
50 /* empty line */
51 buffer->empty = malloc(sizeof(Line));
52 buffer->empty->content = "";
53 buffer->empty->comment = "";
54 buffer->empty->number = 0;
55 buffer->empty->matches = 0;
56 buffer->empty->next = buffer->first;
57 buffer->empty->prev = buffer->first;
59 return buffer;
63 * Parse the line content to determine if it is a header and identify the
64 * separator if any.
66 Line *
67 parse_line(char *s, char *separator)
69 Line *line = malloc(sizeof(Line));
70 char *sep = strstr(s, separator);
71 int pos = !sep ? (int) strlen(s) - 1 : (int) (sep - s);
73 /* strip trailing newline */
74 s[strlen(s) - 1] = '\0';
76 /* fill line->content */
77 line->content = malloc((pos + 1) * sizeof(char));
78 strncpy(line->content, s, pos);
80 /* fill line->comment */
81 if (sep) {
82 line->comment = malloc((strlen(s) - pos) * sizeof(char));
83 strcpy(line->comment, s + pos);
84 } else {
85 line->comment = "";
88 return line;
92 * Add a line to the end of the current buffer.
94 * This requires to create a new line with a link to the previous line
95 * and to NULL as the next line.
97 * The previous line's 'next' should be relinked to this new line.
99 * The header's last line have to point to this last line
101 Line *
102 add_line(Buffer *buffer, int number, char *string, char *separator, Line *prev)
104 /* allocate new line */
105 Line *line = malloc(sizeof(Line));
106 line = parse_line(string, separator);
107 line->next = NULL;
108 line->prev = NULL;
109 buffer->last = line;
110 line->number = number;
112 /* interlink with previous line if exists */
113 if (number == 1) {
114 buffer->first = line;
115 } else {
116 prev->next = line;
117 line->prev = prev;
120 return line;
124 * Set buffer->candidates to an array of lines that match and update
125 * buffer->matching to number of matching candidates.
127 void
128 filter_lines(Buffer *buffer)
130 Line * line = buffer->first;
131 buffer->matching = 0;
133 while (line) {
134 line->matches = line_match_input(line, buffer->input);
135 buffer->matching += line->matches;
137 line = line->next;
142 * Check if line matches and return TRUE if so
145 line_match_input(Line *line, char *input)
147 if (COMPLETE_MODE) {
148 if (!strncmp(input, line->content, strlen(input)))
149 return TRUE;
150 } else {
151 if (strstr(line->content, input))
152 return TRUE;
155 return FALSE;
159 * Replace tab as a multiple of 8 spaces in a line.
161 char *
162 expand_tabs(char *line)
164 size_t i, n;
165 char *converted = malloc(sizeof(char) * (strlen(line) * 8 + 1));
167 for (i = 0, n = 0; i < strlen(line); i++, n++) {
168 if (line[i] == '\t') {
169 converted[n] = ' ';
170 n++;
172 for (; (n) % 8 != 0; n++) {
173 converted[n] = ' ';
176 n--;
177 } else {
178 converted[n] = line[i];
182 converted[n] = '\0';
184 return converted;
188 * Print a line to stderr.
190 void
191 print_line(Line *line, int current, int cols)
193 size_t i;
194 int n = 0;
195 char *content = expand_tabs(line->content);
196 char *comment = expand_tabs(line->comment);
198 /* clean the line in case it was not empty */
199 fputs("\033[K", stderr);
201 /* line number if option set */
202 if (TRUE) {
203 if (current) {
204 fputs("\033[1m", stderr);
205 } else {
206 fputs("\033[1;30m", stderr);
209 fprintf(stderr, "%7d\033[0m ", line->number);
212 n += 8;
215 /* highlight current line */
216 if (current) {
217 fputs("\033[1;33m", stderr);
220 /* print content without overflowing terminal width */
221 for (i = 0; i < strlen(content) && n < cols; n++, i++) {
222 fputc(content[i], stderr);
225 /* print spaces without overflowing terminal width */
226 for (i = n; i <= 40 && n < cols; n++, i++) {
227 fputc(' ', stderr);
230 /* comments in grey */
231 fputs("\033[1;30m", stderr);
233 /* print comment without overflowing terminal width */
234 for (i = 0; i < strlen(comment) && n < cols; n++, i++) {
235 fputc(comment[i], stderr);
238 fputs("\033[0m\n", stderr);
240 free(content);
241 free(comment);
245 * Print a header title.
247 void
248 print_header()
253 * Print all the lines from an array of pointer to lines.
255 * The total number oflines printed shall not excess 'count'.
257 void
258 print_lines(Buffer *buffer, int count, int offset, int cols)
260 Line *line = buffer->current;
261 int i = 0;
262 int j = 0;
264 /* seek back from current line to the first line to print */
265 while (line && i < count - offset) {
266 i = line->matches ? i + 1 : i;
267 line = line->prev;
269 line = line ? line : buffer->first;
271 /* print up to count lines that match the input */
272 while (line && j < count) {
273 if (line->matches) {
274 print_line(line, line == buffer->current, cols);
275 j++;
278 line = line->next;
281 /* continue up to the end of the screen clearing it */
282 for (; j < count; j++) {
283 fputs("\r\033[K\n", stderr);
288 * Update the screen interface and print all candidates.
290 * This also has to clear the previous lines.
292 void
293 update_screen(Buffer *buffer, int count, int offset, int tty)
295 struct winsize w;
296 ioctl(tty, TIOCGWINSZ, &w);
298 fputs("\n", stderr);
299 print_lines(buffer, count, offset, w.ws_col);
301 /* go up to the prompt position and update it */
302 fprintf(stderr, "\033[%dA", count + 1);
303 print_prompt(buffer, w.ws_col);
306 void clear_screen(int count)
308 int i;
309 for (i = 0; i < count + 1; i++) {
310 fputs("\r\033[K\n", stderr);
313 fprintf(stderr, "\033[%dA", count + 1);
317 * Set terminal to send one char at a time for interactive mode, and return the
318 * last terminal state.
320 struct termios
321 terminal_set(int tty)
323 struct termios termio_old;
324 struct termios termio_new;
326 /* set the terminal to send one key at a time. */
328 /* get the terminal's state */
329 if (tcgetattr(tty, &termio_old) < 0) {
330 die("Can not get terminal attributes with tcgetattr().");
333 /* create a new modified state by switching the binary flags */
334 termio_new = termio_old;
335 termio_new.c_lflag &= ~(ICANON | ECHO | IGNBRK);
337 /* apply this state to current terminal now (TCSANOW) */
338 tcsetattr(tty, TCSANOW, &termio_new);
340 return termio_old;
344 * Listen for the user input and call the appropriate functions.
346 void
347 get_input(Buffer *buffer, int count, int offset, int tty)
349 FILE *tty_fd = fopen("/dev/tty", "r");
351 /* receive one character at a time from the terminal */
352 struct termios termio_old = terminal_set(tty);
354 /* get input char by char from the keyboard. */
355 while (do_key(fgetc(tty_fd), buffer)) {
356 update_screen(buffer, count, offset, tty);
359 /* resets the terminal to the previous state. */
360 tcsetattr(tty, TCSANOW, &termio_old);
362 fclose(tty_fd);
366 * Perform action associated with key
369 do_key(char key, Buffer *buffer)
371 size_t length;
372 int i;
374 switch (key) {
376 case CONTROL('C'):
377 return FALSE;
379 case CONTROL('U'):
380 buffer->input[0] = '\0';
381 buffer->current = buffer->first;
382 filter_lines(buffer);
383 break;
385 case CONTROL('W'):
386 length = strlen(buffer->input) - 1;
388 for (i = length; i >= 0 && isspace(buffer->input[i]); i--) {
389 buffer->input[i] = '\0';
392 length = strlen(buffer->input) - 1;
393 for (i = length; i >= 0 && !isspace(buffer->input[i]); i--) {
394 buffer->input[i] = '\0';
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_next_line(buffer);
408 do_prev_line(buffer);
410 break;
412 case CONTROL('N'):
413 do_next_line(buffer);
414 break;
416 case CONTROL('P'):
417 do_prev_line(buffer);
418 break;
420 case CONTROL('I'): /* tab */
421 if (COMPLETE_MODE) {
422 strcpy(buffer->input, buffer->current->content);
423 filter_lines(buffer);
424 } else {
425 do_next_line(buffer);
427 break;
429 case CONTROL('M'):
430 case CONTROL('J'): /* enter */
431 fputs("\r\033[K", stderr);
433 if (COMPLETE_MODE) {
434 puts(buffer->input);
435 } else {
436 puts(buffer->current->content);
438 return FALSE;
440 default:
441 if (isprint(key)) {
442 length = strlen(buffer->input);
443 buffer->input[length] = key;
444 buffer->input[length + 1] = '\0';
447 filter_lines(buffer);
449 if (!buffer->current->matches) {
450 do_next_line(buffer);
451 do_prev_line(buffer);
454 if (!buffer->current->matches) {
455 buffer->current = buffer->empty;
459 return TRUE;
463 * Set the current line to the next matching line, if any.
465 void
466 do_prev_line(Buffer *buffer)
468 Line * line = buffer->current;
470 while (line->prev) {
471 line = line->prev;
473 if (line->matches) {
474 buffer->current = line;
475 break;
482 * Set the current line to the next matching line, if any.
484 void
485 do_next_line(Buffer *buffer)
487 Line * line = buffer->current;
489 while (line->next) {
490 line = line->next;
492 if (line->matches) {
493 buffer->current = line;
494 break;
501 * Print the prompt, before the input, with the number of candidates that
502 * match.
504 void
505 print_prompt(Buffer *buffer, int cols)
507 size_t i;
508 int digits = 0;
509 int matching = buffer->matching;
510 int total = buffer->total;
511 char *input = expand_tabs(buffer->input);
512 char *suggest = expand_tabs(buffer->current->content);
514 /* for the '/' separator between the numbers */
515 cols--;
517 /* count the number of digits */
518 for (i = matching; i; i /= 10, digits++);
519 for (i = total; i; i /= 10, digits++);
521 /* actual prompt */
522 fputs("\r\033[K> ", stderr);
523 cols -= 2;
525 /* input without overflowing terminal width */
526 for (i = 0; i < strlen(input) && cols > digits; cols--, i++) {
527 fputc(input[i], stderr);
530 /* save the cursor position at the end of the input */
531 fputs("\033[s", stderr);
533 /* grey */
534 fputs("\033[1;30m", stderr);
536 /* suggest without overflowing terminal width */
537 if (COMPLETE_MODE) {
538 for (; i < strlen(suggest) && cols > digits; cols--, i++) {
539 fputc(suggest[i], stderr);
543 /* go to the end of the line */
544 for (i = 0; cols > digits; cols--, i++) {
545 fputc(' ', stderr);
548 /* total match and line count at the end of the line */
549 fprintf(stderr, "%d/%d", matching, total);
551 /* restore cursor position at the end of the input */
552 fputs("\033[u", stderr);
554 free(input);
555 free(suggest);
559 * Reset the terminal state and exit with error.
561 void die(const char *s)
563 /* tcsetattr(STDIN_FILENO, TCSANOW, &termio_old); */
564 fprintf(stderr, "%s\n", s);
565 exit(EXIT_FAILURE);
570 main(int argc, char *argv[])
572 int i;
573 Buffer *buffer = NULL;
574 char *separator = "separator";
575 int count = 30;
576 int offset = 3;
577 int tty = open("/dev/tty", O_RDWR);
579 /* command line arguments */
580 for (i = 0; i <= argc; i++) {
581 if (argv[i][1] == '-') {
586 /* command line arguments */
587 buffer = fill_buffer(separator);
589 /* set the interface */
590 filter_lines(buffer);
591 update_screen(buffer, count, offset, tty);
593 /* listen and interact to input */
594 get_input(buffer, count, offset, tty);
596 clear_screen(count);
598 return 0;