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, BOTH
= 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
;
30 char* opt_prompt
= "";
33 * Fill the buffer apropriately with the lines and headers.
36 fill_buffer(char *separator
)
38 /* fill buffer with string */
39 char string
[LINE_SIZE
];
41 Buffer
*buffer
= malloc(sizeof(Buffer
));
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
)) {
51 last
= add_line(buffer
, buffer
->total
, string
, separator
, last
);
54 /* set the buffer stats */
55 buffer
->current
= buffer
->first
;
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
;
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
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
);
88 line
->number
= number
;
90 /* interlink with previous line if exists */
102 * Parse the line content to determine if it is a header and identify the
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 */
121 line
->comment
= malloc((strlen(s
) - pos
) * sizeof(char));
122 strcpy(line
->comment
, s
+ pos
+ strlen(separator
));
127 /* strip trailing whitespaces from line->content */
128 for (pos
--; pos
> 0 && isspace(line
->content
[pos
]); pos
--) {
129 line
->content
[pos
] = '\0';
132 /* strip leading whitespaces from line->comment */
133 for (pos
= 0; isspace(line
->comment
[pos
]); pos
++)
135 line
->comment
+= pos
;
141 * Set buffer->candidates to an array of lines that match and update
142 * buffer->matching to number of matching candidates.
145 filter_lines(Buffer
*buffer
)
147 Line
* line
= buffer
->first
;
148 buffer
->matching
= 0;
151 line
->matches
= line_match_input(line
, buffer
->input
);
152 buffer
->matching
+= line
->matches
;
159 * Check if line matches and return TRUE or FALSE
162 line_match_input(Line
*line
, char *input
)
164 if (opt_complete_mode
) {
165 if (!strncmp(input
, line
->content
, strlen(input
))) {
169 if (strstr(line
->content
, input
)) {
178 * Seek the previous matching line, or NULL if none matches.
181 matching_prev(Line
*line
)
183 while ((line
= line
->prev
) && !line
->matches
)
189 * Seek the next matching line, or NULL if none matches.
192 matching_next(Line
*line
)
194 while ((line
= line
->next
) && !line
->matches
)
200 * Replace tab as a multiple of 8 spaces in a line.
203 expand_tabs(char *line
)
206 char *converted
= malloc(sizeof(char) * (strlen(line
) * 8 + 1));
208 for (i
= 0, n
= 0; i
< strlen(line
); i
++, n
++) {
209 if (line
[i
] == '\t') {
213 for (; (n
) % 8 != 0; n
++) {
219 converted
[n
] = line
[i
];
229 * Print a line to stderr.
232 print_line(Line
*line
, int current
, int cols
)
236 char *content
= expand_tabs(line
->content
);
237 char *comment
= expand_tabs(line
->comment
);
239 /* clean the line in case it was not empty */
240 fputs("\033[K", stderr
);
242 /* line number if option set */
243 if (opt_line_numbers
) {
245 fputs("\033[1m", stderr
);
247 fputs("\033[1;30m", stderr
);
250 fprintf(stderr
, "%7d\033[0m ", line
->number
);
256 /* highlight current line */
258 fputs("\033[1;33m", stderr
);
261 /* print content without overflowing terminal width */
262 for (i
= 0; i
< strlen(content
) && n
< cols
; n
++, i
++) {
263 fputc(content
[i
], stderr
);
266 /* print spaces without overflowing terminal width */
267 for (i
= n
; i
<= 40 && n
< cols
; n
++, i
++) {
271 /* comments in grey */
272 fputs("\033[1;30m", stderr
);
274 /* print comment without overflowing terminal width */
275 for (i
= 0; i
< strlen(comment
) && n
< cols
; n
++, i
++) {
276 fputc(comment
[i
], stderr
);
279 fputs("\033[0m\n", stderr
);
286 * Print a header title.
294 * Print all the lines from an array of pointer to lines.
296 * The total number oflines printed shall not excess 'count'.
299 print_lines(Buffer
*buffer
, int count
, int offset
, int cols
)
301 Line
*line
= buffer
->current
;
305 /* seek back from current line to the first line to print */
306 while (line
&& i
< count
- offset
) {
307 i
= line
->matches
? i
+ 1 : i
;
310 line
= line
? line
: buffer
->first
;
312 /* print up to count lines that match the input */
313 while (line
&& j
< count
) {
315 print_line(line
, line
== buffer
->current
, cols
);
322 /* continue up to the end of the screen clearing it */
323 for (; j
< count
; j
++) {
324 fputs("\r\033[K\n", stderr
);
329 * Update the screen interface and print all candidates.
331 * This also has to clear the previous lines.
334 update_screen(Buffer
*buffer
, int count
, int offset
, int tty_fd
)
337 ioctl(tty_fd
, TIOCGWINSZ
, &w
);
338 count
= MIN(count
, w
.ws_row
- 2);
341 print_lines(buffer
, count
, offset
, w
.ws_col
);
343 /* go up to the prompt position and update it */
344 fprintf(stderr
, "\033[%dA", count
+ 1);
345 print_prompt(buffer
, w
.ws_col
);
348 void clear_screen(int count
)
351 for (i
= 0; i
< count
+ 1; i
++) {
352 fputs("\r\033[K\n", stderr
);
355 fprintf(stderr
, "\033[%dA", count
+ 1);
359 * Set terminal to send one char at a time for interactive mode, and return the
360 * last terminal state.
363 set_terminal(int tty_fd
)
365 struct termios termio_old
;
366 struct termios termio_new
;
368 /* set the terminal to send one key at a time. */
370 /* get the terminal's state */
371 if (tcgetattr(tty_fd
, &termio_old
) < 0) {
372 die("Can not get terminal attributes with tcgetattr().");
375 /* create a new modified state by switching the binary flags */
376 termio_new
= termio_old
;
377 termio_new
.c_lflag
&= ~(ICANON
| ECHO
| IGNBRK
);
379 /* apply this state to current terminal now (TCSANOW) */
380 tcsetattr(tty_fd
, TCSANOW
, &termio_new
);
386 * Listen for the user input and call the appropriate functions.
389 get_input(Buffer
*buffer
, int count
, int offset
, int tty_fd
)
391 FILE *tty_fp
= fopen("/dev/tty", "r");
393 /* receive one character at a time from the terminal */
394 struct termios termio_old
= set_terminal(tty_fd
);
396 /* get input char by char from the keyboard. */
397 while (do_key(fgetc(tty_fp
), buffer
)) {
398 update_screen(buffer
, count
, offset
, tty_fd
);
401 /* resets the terminal to the previous state. */
402 tcsetattr(tty_fd
, TCSANOW
, &termio_old
);
408 * Perform action associated with key
411 do_key(char key
, Buffer
*buffer
)
413 if (key
== opt_validate_key
) {
414 do_print_selection(buffer
);
424 buffer
->input
[0] = '\0';
425 buffer
->current
= buffer
->first
;
426 filter_lines(buffer
);
430 do_remove_word_input(buffer
);
431 filter_lines(buffer
);
435 case CONTROL('H'): /* backspace */
436 buffer
->input
[strlen(buffer
->input
) - 1] = '\0';
437 filter_lines(buffer
);
439 if (!buffer
->current
->matches
) {
440 do_jump(buffer
, BOTH
);
445 do_jump(buffer
, NEXT
);
449 do_jump(buffer
, PREV
);
452 case CONTROL('I'): /* tab */
453 strcpy(buffer
->input
, buffer
->current
->content
);
454 filter_lines(buffer
);
458 case CONTROL('J'): /* enter */
459 do_print_selection(buffer
);
463 do_add_character(buffer
, key
);
470 * Set the current line to the next/previous/closest matching line.
473 do_jump(Buffer
*buffer
, int direction
)
475 Line
* line
= buffer
->current
;
477 if (direction
== BOTH
) {
478 line
= matching_next(buffer
->current
);
479 line
= (line
) ? line
: matching_prev(buffer
->current
);
480 } else if (direction
== NEXT
) {
481 line
= matching_next(line
);
482 } else if (direction
== PREV
) {
483 line
= matching_prev(line
);
486 buffer
->current
= (line
) ? line
: buffer
->current
;
488 if (opt_print_numbers
)
489 do_print_selection(buffer
);
493 * Send the selection to stdout.
496 do_print_selection(Buffer
*buffer
)
498 fputs("\r\033[K", stderr
);
500 if (opt_print_numbers
) {
501 printf("%d\n", buffer
->current
->number
);
503 if (opt_complete_mode
) {
506 puts(buffer
->current
->content
);
512 * Remove the last word from the buffer's input
515 do_remove_word_input(Buffer
*buffer
)
517 size_t length
= strlen(buffer
->input
) - 1;
520 for (i
= length
; i
>= 0 && isspace(buffer
->input
[i
]); i
--) {
521 buffer
->input
[i
] = '\0';
524 length
= strlen(buffer
->input
) - 1;
525 for (i
= length
; i
>= 0 && !isspace(buffer
->input
[i
]); i
--) {
526 buffer
->input
[i
] = '\0';
531 * Add a character to the buffer input and filter lines again.
534 do_add_character(Buffer
*buffer
, char key
)
536 size_t length
= strlen(buffer
->input
);
539 buffer
->input
[length
] = key
;
540 buffer
->input
[length
+ 1] = '\0';
543 filter_lines(buffer
);
545 if (!buffer
->current
->matches
) {
546 do_jump(buffer
, BOTH
);
549 if (!buffer
->current
->matches
) {
550 buffer
->current
= buffer
->empty
;
555 * Print the prompt, before the input, with the number of candidates that
559 print_prompt(Buffer
*buffer
, int cols
)
562 int matching
= buffer
->matching
;
563 int total
= buffer
->total
;
564 char *input
= expand_tabs(buffer
->input
);
565 char *suggest
= expand_tabs(buffer
->current
->content
);
567 /* for the '/' separator between the numbers */
570 /* number of digits */
571 for (i
= matching
; i
; i
/= 10, cols
--)
573 for (i
= total
; i
; i
/= 10, cols
--)
576 /* 0 also has one digit*/
577 cols
-= !matching
? 1 : 0;
580 fprintf(stderr
, "\r%s\033[K> ", opt_prompt
);
581 cols
-= 2 + strlen(opt_prompt
);
583 /* input without overflowing terminal width */
584 for (i
= 0; i
< strlen(input
) && cols
> 0; cols
--, i
++) {
585 fputc(input
[i
], stderr
);
588 /* save the cursor position at the end of the input */
589 fputs("\033[s", stderr
);
592 fputs("\033[1;30m", stderr
);
594 /* suggest without overflowing terminal width */
595 if (opt_complete_mode
) {
596 for (; i
< strlen(suggest
) && cols
> 0; cols
--, i
++) {
597 fputc(suggest
[i
], stderr
);
601 /* go to the end of the line */
602 for (i
= 0; cols
> 0; cols
--, i
++) {
606 /* total match and line count at the end of the line */
607 fprintf(stderr
, "%d/%d", matching
, total
);
609 /* restore cursor position at the end of the input */
610 fputs("\033[0m\033[u", stderr
);
617 * Reset the terminal state and exit with error.
622 /* tcsetattr(STDIN_FILENO, TCSANOW, &termio_old); */
623 fprintf(stderr
, "%s\n", s
);
628 main(int argc
, char *argv
[])
631 Buffer
*buffer
= NULL
;
633 int tty_fd
= open("/dev/tty", O_RDWR
);
635 /* command line arguments */
636 for (i
= 0; i
< argc
; i
++) {
637 if (argv
[i
][0] == '-') {
638 switch (argv
[i
][1]) {
640 opt_line_numbers
= TRUE
;
643 opt_complete_mode
= TRUE
;
646 opt_print_numbers
= TRUE
;
647 opt_line_numbers
= TRUE
;
650 opt_validate_key
= (argv
[i
++][0] == '^') ?
655 opt_separator
= argv
[++i
];
658 if (sscanf(argv
[++i
], "%d", &opt_lines
) <= 0)
659 die("Wrong number format after -l.");
662 opt_prompt
= argv
[++i
];
668 /* command line arguments */
669 buffer
= fill_buffer(opt_separator
);
671 /* set the interface */
672 filter_lines(buffer
);
673 update_screen(buffer
, opt_lines
, offset
, tty_fd
);
675 /* listen and interact to input */
676 get_input(buffer
, opt_lines
, offset
, tty_fd
);
678 clear_screen(opt_lines
);
680 /* close files descriptors and pointers, and free memory */