Pulled content from README to manpage
[iomenu.git] / iomenu.c
blobb9ef931c9b7c98fd7e050bde14db4aff6d932d67
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 if (strncmp(input, line->content, strlen(input)) == 0) {
150 return TRUE;
151 } else {
152 return FALSE;
157 * Replace tab as a multiple of 8 spaces in a line.
159 char *
160 expand_tabs(char *line)
162 size_t i, n;
163 char *converted = malloc(sizeof(char) * (strlen(line) * 8 + 1));
165 for (i = 0, n = 0; i < strlen(line); i++, n++) {
166 if (line[i] == '\t') {
167 converted[n] = ' ';
168 n++;
170 for (; (n) % 8 != 0; n++) {
171 converted[n] = ' ';
174 n--;
175 } else {
176 converted[n] = line[i];
180 converted[n] = '\0';
182 return converted;
186 * Print a line to stderr.
188 void
189 print_line(Line *line, int current, int cols)
191 size_t i;
192 int n = 0;
193 char *content = expand_tabs(line->content);
194 char *comment = expand_tabs(line->comment);
196 /* clean the line in case it was not empty */
197 fputs("\033[K", stderr);
199 /* line number if option set */
200 if (TRUE) {
201 if (current) {
202 fputs("\033[1m", stderr);
203 } else {
204 fputs("\033[1;30m", stderr);
207 fprintf(stderr, "%7d\033[0m ", line->number);
210 n += 8;
213 /* highlight current line */
214 if (current) {
215 fputs("\033[1;33m", stderr);
218 /* print content without overflowing terminal width */
219 for (i = 0; i < strlen(content) && n < cols; n++, i++) {
220 fputc(content[i], stderr);
223 /* print spaces without overflowing terminal width */
224 for (i = n; i <= 40 && n < cols; n++, i++) {
225 fputc(' ', stderr);
228 /* comments in grey */
229 fputs("\033[1;30m", stderr);
231 /* print comment without overflowing terminal width */
232 for (i = 0; i < strlen(comment) && n < cols; n++, i++) {
233 fputc(comment[i], stderr);
236 fputs("\033[0m\n", stderr);
238 free(content);
239 free(comment);
243 * Print a header title.
245 void
246 print_header()
251 * Print all the lines from an array of pointer to lines.
253 * The total number oflines printed shall not excess 'count'.
255 void
256 print_lines(Buffer *buffer, int count, int offset, int cols)
258 Line *line = buffer->current;
259 int i = 0;
260 int j = 0;
262 /* seek back from current line to the first line to print */
263 while (line && i < count - offset) {
264 i = line->matches ? i + 1 : i;
265 line = line->prev;
267 line = line ? line : buffer->first;
269 /* print up to count lines that match the input */
270 while (line && j < count) {
271 if (line->matches) {
272 print_line(line, line == buffer->current, cols);
273 j++;
276 line = line->next;
279 /* continue up to the end of the screen clearing it */
280 for (; j < count; j++) {
281 fputs("\r\033[K\n", stderr);
286 * Update the screen interface and print all candidates.
288 * This also has to clear the previous lines.
290 void
291 update_screen(Buffer *buffer, int count, int offset, int tty)
293 struct winsize w;
294 ioctl(tty, TIOCGWINSZ, &w);
296 fputs("\n", stderr);
297 print_lines(buffer, count, offset, w.ws_col);
299 /* go up to the prompt position and update it */
300 fprintf(stderr, "\033[%dA", count + 1);
301 print_prompt(buffer, w.ws_col);
304 void clear_screen(int count)
306 int i;
307 for (i = 0; i < count + 1; i++) {
308 fputs("\r\033[K\n", stderr);
311 fprintf(stderr, "\033[%dA", count + 1);
315 * Set terminal to send one char at a time for interactive mode, and return the
316 * last terminal state.
318 struct termios
319 terminal_set(int tty)
321 struct termios termio_old;
322 struct termios termio_new;
324 /* set the terminal to send one key at a time. */
326 /* get the terminal's state */
327 if (tcgetattr(tty, &termio_old) < 0) {
328 die("Can not get terminal attributes with tcgetattr().");
331 /* create a new modified state by switching the binary flags */
332 termio_new = termio_old;
333 termio_new.c_lflag &= ~(ICANON | ECHO | IGNBRK);
335 /* apply this state to current terminal now (TCSANOW) */
336 tcsetattr(tty, TCSANOW, &termio_new);
338 return termio_old;
342 * Listen for the user input and call the appropriate functions.
344 void
345 get_input(Buffer *buffer, int count, int offset, int tty)
347 FILE *tty_fd = fopen("/dev/tty", "r");
349 /* receive one character at a time from the terminal */
350 struct termios termio_old = terminal_set(tty);
352 /* get input char by char from the keyboard. */
353 while (do_key(fgetc(tty_fd), buffer)) {
354 update_screen(buffer, count, offset, tty);
357 /* resets the terminal to the previous state. */
358 tcsetattr(tty, TCSANOW, &termio_old);
360 fclose(tty_fd);
364 * Perform action associated with key
367 do_key(char key, Buffer *buffer)
369 size_t length;
370 int i;
372 switch (key) {
374 case CONTROL('C'):
375 return FALSE;
377 case CONTROL('U'):
378 buffer->input[0] = '\0';
379 filter_lines(buffer);
380 break;
382 case CONTROL('W'):
383 length = strlen(buffer->input) - 1;
385 for (i = length; i >= 0 && isspace(buffer->input[i]); i--) {
386 buffer->input[i] = '\0';
389 length = strlen(buffer->input) - 1;
390 for (i = length; i >= 0 && !isspace(buffer->input[i]); i--) {
391 buffer->input[i] = '\0';
394 filter_lines(buffer);
396 break;
398 case CONTROL('H'):
399 case 127: /* backspace */
400 buffer->input[strlen(buffer->input) - 1] = '\0';
401 filter_lines(buffer);
402 break;
404 case CONTROL('N'):
405 do_next_line(buffer);
406 break;
408 case CONTROL('P'):
409 do_prev_line(buffer);
410 break;
412 case CONTROL('I'):
413 /* do_prev_line(buffer); */
414 /* strcpy(buffer->input, buffer->current->content); */
415 filter_lines(buffer);
416 break;
418 case CONTROL('M'):
419 case CONTROL('J'): /* enter */
420 fputs("\r\033[K", stderr);
421 puts(buffer->current->content);
422 return FALSE;
424 default:
425 if (isprint(key)) {
426 length = strlen(buffer->input);
427 buffer->input[length] = key;
428 buffer->input[length + 1] = '\0';
431 filter_lines(buffer);
433 if (!buffer->current->matches) {
434 do_next_line(buffer);
435 do_prev_line(buffer);
438 if (!buffer->current->matches) {
439 buffer->current = buffer->empty;
443 return TRUE;
447 * Set the current line to the next matching line, if any.
449 void
450 do_prev_line(Buffer *buffer)
452 Line * line = buffer->current;
454 while (line->prev) {
455 line = line->prev;
457 if (line->matches) {
458 buffer->current = line;
459 break;
466 * Set the current line to the next matching line, if any.
468 void
469 do_next_line(Buffer *buffer)
471 Line * line = buffer->current;
473 while (line->next) {
474 line = line->next;
476 if (line->matches) {
477 buffer->current = line;
478 break;
485 * Print the prompt, before the input, with the number of candidates that
486 * match.
488 void
489 print_prompt(Buffer *buffer, int cols)
491 size_t i;
492 int digits = 0;
493 int matching = buffer->matching;
494 int total = buffer->total;
495 char *input = expand_tabs(buffer->input);
496 char *suggest = expand_tabs(buffer->current->content);
498 /* for the '/' separator between the numbers */
499 cols--;
501 /* count the number of digits */
502 for (i = matching; i; i /= 10, digits++);
503 for (i = total; i; i /= 10, digits++);
505 /* actual prompt */
506 fputs("\r\033[K> ", stderr);
507 cols -= 2;
509 /* input without overflowing terminal width */
510 for (i = 0; i < strlen(input) && cols > digits; cols--, i++) {
511 fputc(input[i], stderr);
514 /* save the cursor position at the end of the input */
515 fputs("\033[s", stderr);
517 /* grey */
518 fputs("\033[1;30m", stderr);
520 /* suggest without overflowing terminal width */
521 for (; i < strlen(suggest) && cols > digits; cols--, i++) {
522 fputc(suggest[i], stderr);
525 /* go to the end of the line */
526 for (i = 0; cols > digits; cols--, i++) {
527 fputc(' ', stderr);
530 /* total match and line count at the end of the line */
531 fprintf(stderr, "%d/%d", matching, total);
533 /* restore cursor position at the end of the input */
534 fputs("\033[u", stderr);
536 free(input);
537 free(suggest);
541 * Reset the terminal state and exit with error.
543 void die(const char *s)
545 /* tcsetattr(STDIN_FILENO, TCSANOW, &termio_old); */
546 fprintf(stderr, "%s\n", s);
547 exit(EXIT_FAILURE);
552 main(int argc, char *argv[])
554 int i;
555 Buffer *buffer = NULL;
556 char *separator = "* ";
557 int count = 30;
558 int offset = 3;
559 int tty = open("/dev/tty", O_RDWR);
561 /* command line arguments */
562 for (i = 0; i <= argc; i++) {
563 if (argv[i][1] == '-') {
568 /* command line arguments */
569 buffer = fill_buffer(separator);
571 /* set the interface */
572 filter_lines(buffer);
573 update_screen(buffer, count, offset, tty);
575 /* listen and interact to input */
576 get_input(buffer, count, offset, tty);
578 clear_screen(count);
580 return 0;