3 * selector is a simple command line utility for selection of strings
4 * with a dynamic pattern-matching.
6 * Copyright (c) 2009 Francois Fleuret
7 * Written by Francois Fleuret <francois@fleuret.org>
9 * This file is part of selector.
11 * selector is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License version 3 as
13 * published by the Free Software Foundation.
15 * selector is distributed in the hope that it will be useful, but
16 * WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 * General Public License for more details.
20 * You should have received a copy of the GNU General Public License
21 * along with selector. If not, see <http://www.gnu.org/licenses/>.
25 /* To use it as a super-history-search for bash:
26 selector -q -b -i -d -v -w -l ${HISTSIZE} <(history)
37 #include <sys/ioctl.h>
44 const int buffer_size = 4096;
46 /* Yeah, global variables! */
48 int nb_lines_max = 1000;
49 char pattern_separator = ';';
50 char label_separator = '\0';
51 int output_to_vt_buffer = 0;
52 int add_control_qs = 0;
56 int inverse_order = 0;
57 int remove_duplicates = 0;
59 int case_sensitive = 0;
63 int attr_modeline, attr_focus_line, attr_error;
65 /*********************************************************************/
67 void inject_into_tty_buffer(char *string) {
68 struct termios oldtio, newtio;
70 tcgetattr(STDIN_FILENO, &oldtio);
71 memset(&newtio, 0, sizeof(newtio));
72 /* Set input mode (non-canonical, *no echo*,...) */
73 tcsetattr(STDIN_FILENO, TCSANOW, &newtio);
74 const char control_q = '\021';
75 /* Put the selected string in the tty input buffer */
76 for(k = string; *k; k++) {
77 if(add_control_qs && !(*k >= ' ' && *k <= '~')) {
78 /* Add ^Q to quote control characters */
79 ioctl(STDIN_FILENO, TIOCSTI, &control_q);
81 ioctl(STDIN_FILENO, TIOCSTI, k);
83 /* Restore the old settings */
84 tcsetattr(STDIN_FILENO, TCSANOW, &oldtio);
87 /*********************************************************************/
89 void check_opt(int argc, char **argv, int n_opt, int n, const char *help) {
90 if(n_opt + n >= argc) {
91 fprintf(stderr, "Missing argument for %s, expecting %s.\n",
97 int string_to_positive_integer(char *string) {
103 for(s = string; *s; s++) {
104 if(*s >= '0' && *s <= '9') {
105 result = result * 10 + (int) (*s - '0');
111 fprintf(stderr, "Value `%s' is not a positive integer.\n", string);
118 void error_feedback() {
126 /*********************************************************************/
127 /* A quick and dirty hash table
129 The table itself stores indexes of the strings taken in a char
130 **table. When a string is added, if it was already in the table,
131 the new index replaces the previous one. */
133 int *new_hash_table(int hash_table_size) {
135 result = (int *) malloc(hash_table_size * sizeof(int));
136 for(k = 0; k < hash_table_size; k++) {
142 /* Adds new_string in the table, associated to new_index. If this
143 string was not already in the table, returns -1. Otherwise, returns
144 the previous index it had. */
146 int test_and_add(char *new_string, int new_index,
148 int *hash_table, int hash_table_size) {
150 unsigned int code = 0;
153 /* This is my recipe. I checked, it seems to work (as long as
154 hash_table_size is not a multiple of 387433 that should be okay) */
156 for(k = 0; new_string[k]; k++) {
157 code = code * 387433 + (unsigned int) (new_string[k]);
160 code = code % hash_table_size;
162 while(hash_table[code] >= 0) {
163 /* There is a string with that code */
164 if(strcmp(new_string, strings[hash_table[code]]) == 0) {
165 /* It is the same string, we keep a copy of the stored index */
166 int result = hash_table[code];
167 /* Put the new one */
168 hash_table[code] = new_index;
169 /* And return the previous one */
172 /* This collision was not the same string, let's move to the next
174 code = (code + 1) % hash_table_size;
177 /* This string was not already in there, store the index in the
178 table and return -1 */
180 hash_table[code] = new_index;
184 /*********************************************************************/
185 /* A matcher matches either with a collection of substrings, or with a
193 char *splitted_patterns, **patterns;
196 int match(char *string, matcher_t *matcher) {
198 if(matcher->nb_patterns >= 0) {
199 if(matcher->case_sensitive) {
200 for(n = 0; n < matcher->nb_patterns; n++) {
201 if(strstr(string, matcher->patterns[n]) == 0) return 0;
204 for(n = 0; n < matcher->nb_patterns; n++) {
205 if(strcasestr(string, matcher->patterns[n]) == 0) return 0;
210 return regexec(&matcher->preg, string, 0, 0, 0) == 0;
214 void free_matcher(matcher_t *matcher) {
215 if(matcher->nb_patterns < 0) {
216 if(!matcher->regexp_error) regfree(&matcher->preg);
218 free(matcher->splitted_patterns);
219 free(matcher->patterns);
223 void initialize_matcher(int use_regexp, int case_sensitive,
224 matcher_t *matcher, const char *pattern) {
229 matcher->nb_patterns = -1;
230 matcher->regexp_error = regcomp(&matcher->preg, pattern, case_sensitive ? 0 : REG_ICASE);
232 matcher->regexp_error = 0;
233 matcher->nb_patterns = 1;
234 matcher->case_sensitive = case_sensitive;
236 for(s = pattern; *s; s++) {
237 if(*s == pattern_separator) {
238 matcher->nb_patterns++;
242 matcher->splitted_patterns = (char *) malloc((strlen(pattern) + 1) * sizeof(char));
243 matcher->patterns = (char **) malloc(matcher->nb_patterns * sizeof(char *));
245 strcpy(matcher->splitted_patterns, pattern);
248 char *last_pattern_start = matcher->splitted_patterns;
249 for(t = matcher->splitted_patterns; n < matcher->nb_patterns; t++) {
250 if(*t == pattern_separator || *t == '\0') {
252 matcher->patterns[n++] = last_pattern_start;
253 last_pattern_start = t + 1;
259 /*********************************************************************/
262 void delete_char(char *buffer, int *position) {
263 if(buffer[*position]) {
265 while(c < buffer_size && buffer[c]) {
266 buffer[c] = buffer[c+1];
269 } else error_feedback();
272 void backspace_char(char *buffer, int *position) {
274 if(buffer[*position]) {
275 int c = *position - 1;
277 buffer[c] = buffer[c+1];
281 buffer[*position - 1] = '\0';
285 } else error_feedback();
288 void insert_char(char *buffer, int *position, char character) {
289 if(strlen(buffer) < buffer_size - 1) {
291 char t = buffer[c], u;
300 buffer[(*position)++] = character;
301 } else error_feedback();
304 void kill_before_cursor(char *buffer, int *position) {
306 while(buffer[*position + s]) {
307 buffer[s] = buffer[*position + s];
314 void kill_after_cursor(char *buffer, int *position) {
315 buffer[*position] = '\0';
318 /*********************************************************************/
320 int previous_visible(int current_line, int nb_lines, char **lines, matcher_t *matcher) {
321 int line = current_line - 1;
322 while(line >= 0 && !match(lines[line], matcher)) line--;
326 int next_visible(int current_line, int nb_lines, char **lines, matcher_t *matcher) {
327 int line = current_line + 1;
328 while(line < nb_lines && !match(lines[line], matcher)) line++;
336 /*********************************************************************/
338 /* The value passed to this routine in current_focus_line is the index
339 of the line we should have highlighted if there was no motion and if
340 it matched the matcher. So, the line actually highlighted is the
341 first one matching the matcher in that order: (1)
342 current_focus_line after motion, (2) the first with a greater
343 index, (3) the first with a lesser index.
345 The index of the line actually shown highlighted is written in
346 displayed_focus_line (it can be -1)
348 If there is a motion and a line is actually shown highlighted, its
349 value is written in current_focus_line. */
351 void update_screen(int *current_focus_line, int *displayed_focus_line,
353 int nb_lines, char **lines,
357 char buffer[buffer_size];
361 initialize_matcher(use_regexp, case_sensitive, &matcher, pattern);
363 int console_width = getmaxx(stdscr);
364 int console_height = getmaxy(stdscr);
366 /* First, we find a visible line. */
368 int nb_printed_lines = 0;
370 use_default_colors();
374 if(matcher.regexp_error) {
376 addnstr("Regexp syntax error", console_width);
378 } else if(nb_lines > 0) {
380 if(match(lines[*current_focus_line], &matcher)) {
381 new_focus_line = *current_focus_line;
383 new_focus_line = next_visible(*current_focus_line, nb_lines, lines, &matcher);
384 if(new_focus_line < 0) {
385 new_focus_line = previous_visible(*current_focus_line, nb_lines, lines, &matcher);
389 /* If we found a visible line and we should move, let's move */
391 if(new_focus_line >= 0 && motion != 0) {
392 int l = new_focus_line;
394 /* We want to go down, let's find the first visible line below */
395 for(m = 0; l >= 0 && m < motion; m++) {
396 l = next_visible(l, nb_lines, lines, &matcher);
402 /* We want to go up, let's find the first visible line above */
403 for(m = 0; l >= 0 && m < -motion; m++) {
404 l = previous_visible(l, nb_lines, lines, &matcher);
412 /* Here new_focus_line is either a line number matching the pattern, or -1 */
414 if(new_focus_line >= 0) {
416 int first_line = new_focus_line, last_line = new_focus_line, nb_match = 1;
418 /* We find the first and last line to show, so that the total of
419 visible lines between them (them included) is
422 while(nb_match < console_height-1 && (first_line > 0 || last_line < nb_lines - 1)) {
426 while(first_line > 0 && !match(lines[first_line], &matcher)) {
429 if(match(lines[first_line], &matcher)) {
434 if(nb_match < console_height - 1 && last_line < nb_lines - 1) {
436 while(last_line < nb_lines - 1 && !match(lines[last_line], &matcher)) {
440 if(match(lines[last_line], &matcher)) {
446 /* Now we display them */
448 for(l = first_line; l <= last_line; l++) {
449 if(match(lines[l], &matcher)) {
452 while(lines[l][k] && k < buffer_size - 2 && k < console_width - 2) {
453 buffer[k] = lines[l][k];
457 /* We fill the rest of the line with blanks if this is the
460 if(l == new_focus_line) {
461 while(k < console_width) {
471 /* Highlight the highlighted line ... */
473 if(l == new_focus_line) {
474 attron(attr_focus_line);
475 addnstr(buffer, console_width);
476 attroff(attr_focus_line);
478 addnstr(buffer, console_width);
485 /* If we are on a focused line and we moved, this become the new
489 *current_focus_line = new_focus_line;
493 *displayed_focus_line = new_focus_line;
495 if(nb_printed_lines == 0) {
497 addnstr("No selection", console_width);
502 addnstr("Empty choice", console_width);
508 /* Draw the modeline */
512 attron(attr_modeline);
514 for(k = 0; k < console_width; k++) buffer[k] = ' ';
515 buffer[console_width] = '\0';
516 addnstr(buffer, console_width);
520 /* There must be a more elegant way of moving the cursor at a
521 location met during display */
528 cursor_x += strlen(title) + 1;
531 sprintf(buffer, "%d/%d ", nb_printed_lines, nb_lines);
533 cursor_x += strlen(buffer);
535 addnstr(pattern, cursor_position);
536 cursor_x += cursor_position;
538 if(pattern[cursor_position]) {
539 addstr(pattern + cursor_position);
544 if(use_regexp || case_sensitive) {
561 attroff(attr_modeline);
566 free_matcher(&matcher);
569 /*********************************************************************/
571 void read_file(const char *input_filename,
572 int nb_lines_max, int *nb_lines, char **lines,
573 int hash_table_size, int *hash_table) {
575 char raw_line[buffer_size];
577 FILE *file = fopen(input_filename, "r");
580 fprintf(stderr, "Can not open `%s'.\n", input_filename);
584 int start = 0, end = 0, k;
586 while(*nb_lines < nb_lines_max && (end > start || !feof(file))) {
588 while(eol < end && raw_line[eol] != '\n') eol++;
591 for(k = 0; k < end - start; k++) {
592 raw_line[k] = raw_line[k + start];
597 end += fread(raw_line + end, sizeof(char), buffer_size - end, file);
598 while(eol < end && raw_line[eol] != '\n') eol++;
601 if(eol == buffer_size) {
602 raw_line[buffer_size - 1] = '\0';
603 fprintf(stderr, "Line too long:\n");
604 fprintf(stderr, raw_line);
605 fprintf(stderr, "\n");
609 raw_line[eol] = '\0';
611 char *t = raw_line + start;
613 /* Remove the zsh history prefix */
615 if(zsh_history && *t == ':') {
616 while(*t && *t != ';') t++;
620 /* Remove the bash history prefix */
623 while(*t == ' ') t++;
624 while(*t >= '0' && *t <= '9') t++;
625 while(*t == ' ') t++;
628 /* Check for duplicates with the hash table and insert the line in
629 the list if necessary */
634 dup = test_and_add(t, *nb_lines, lines, hash_table, hash_table_size);
640 lines[*nb_lines] = (char *) malloc((strlen(t) + 1) * sizeof(char));
641 strcpy(lines[*nb_lines], t);
643 /* The string was already in there, so we do not allocate a new
644 string but use the pointer to the first occurence of it */
645 lines[*nb_lines] = lines[dup];
655 /*********************************************************************/
657 int main(int argc, char **argv) {
659 if(!ttyname(STDIN_FILENO)) {
660 fprintf(stderr, "The standard input is not a tty.\n");
664 char input_filename[buffer_size], output_filename[buffer_size];
666 int error = 0, show_help = 0;
667 int rest_are_files = 0;
669 int color_fg_modeline, color_bg_modeline;
670 int color_fg_highlight, color_bg_highlight;
672 color_fg_modeline = COLOR_WHITE;
673 color_bg_modeline = COLOR_BLACK;
674 color_fg_highlight = COLOR_BLACK;
675 color_bg_highlight = COLOR_YELLOW;
677 setlocale(LC_ALL, "");
679 strcpy(input_filename, "");
680 strcpy(output_filename, "");
683 while(!error && !show_help && i < argc && argv[i][0] == '-' && !rest_are_files) {
685 if(strcmp(argv[i], "-o") == 0) {
686 check_opt(argc, argv, i, 1, "<output filename>");
687 strncpy(output_filename, argv[i+1], buffer_size);
691 else if(strcmp(argv[i], "-s") == 0) {
692 check_opt(argc, argv, i, 1, "<pattern separator>");
693 pattern_separator = argv[i+1][0];
697 else if(strcmp(argv[i], "-x") == 0) {
698 check_opt(argc, argv, i, 1, "<label separator>");
699 label_separator = argv[i+1][0];
703 else if(strcmp(argv[i], "-v") == 0) {
704 output_to_vt_buffer = 1;
708 else if(strcmp(argv[i], "-w") == 0) {
713 else if(strcmp(argv[i], "-m") == 0) {
718 else if(strcmp(argv[i], "-q") == 0) {
723 else if(strcmp(argv[i], "-f") == 0) {
724 check_opt(argc, argv, i, 1, "<input filename>");
725 strncpy(input_filename, argv[i+1], buffer_size);
729 else if(strcmp(argv[i], "-i") == 0) {
734 else if(strcmp(argv[i], "-b") == 0) {
739 else if(strcmp(argv[i], "-z") == 0) {
744 else if(strcmp(argv[i], "-d") == 0) {
745 remove_duplicates = 1;
749 else if(strcmp(argv[i], "-e") == 0) {
754 else if(strcmp(argv[i], "-a") == 0) {
759 else if(strcmp(argv[i], "-t") == 0) {
760 check_opt(argc, argv, i, 1, "<title>");
762 title = (char *) malloc((strlen(argv[i+1]) + 1) * sizeof(char));
763 strcpy(title, argv[i+1]);
767 else if(strcmp(argv[i], "-l") == 0) {
768 check_opt(argc, argv, i, 1, "<maximum number of lines>");
769 nb_lines_max = string_to_positive_integer(argv[i+1]);
773 else if(strcmp(argv[i], "-c") == 0) {
774 check_opt(argc, argv, i, 4, "<fg modeline> <bg modeline> <fg highlight> <bg highlight>");
775 color_fg_modeline = string_to_positive_integer(argv[i + 1]);
776 color_bg_modeline = string_to_positive_integer(argv[i + 2]);
777 color_fg_highlight = string_to_positive_integer(argv[i + 3]);
778 color_bg_highlight = string_to_positive_integer(argv[i + 4]);
782 else if(strcmp(argv[i], "--") == 0) {
787 else if(strcmp(argv[i], "-h") == 0) {
793 fprintf(stderr, "Unknown option %s.\n", argv[i]);
798 if(show_help || error) {
799 fprintf(stderr, "Selector version %s-R%s\n", VERSION, REVISION_NUMBER);
800 fprintf(stderr, "Written by Francois Fleuret <francois@fleuret.org>.\n");
801 fprintf(stderr, "Usage: %s [options] [<filename1> [<filename2> ...]]\n", argv[0]);
802 fprintf(stderr, "\n");
803 fprintf(stderr, " -h show this help\n");
804 fprintf(stderr, " -v inject the selected line in the tty\n");
805 fprintf(stderr, " -w quote control characters with ^Qs when using -v\n");
806 fprintf(stderr, " -d remove duplicated lines\n");
807 fprintf(stderr, " -b remove the bash history line prefix\n");
808 fprintf(stderr, " -z remove the zsh history line prefix\n");
809 fprintf(stderr, " -i invert the order of lines\n");
810 fprintf(stderr, " -e start in regexp mode\n");
811 fprintf(stderr, " -a start in case sensitive mode\n");
812 fprintf(stderr, " -m monochrome mode\n");
813 fprintf(stderr, " -q make a flash instead of a beep on an edition error\n");
814 fprintf(stderr, " -- all following arguments are filenames\n");
815 fprintf(stderr, " -t <title>\n");
816 fprintf(stderr, " add a title in the modeline\n");
817 fprintf(stderr, " -c <fg modeline> <bg modeline> <fg highlight> <bg highlight>\n");
818 fprintf(stderr, " set the display colors\n");
819 fprintf(stderr, " -o <output filename>\n");
820 fprintf(stderr, " set a file to write the selected line to\n");
821 fprintf(stderr, " -s <pattern separator>\n");
822 fprintf(stderr, " set the symbol to separate substrings in the pattern\n");
823 fprintf(stderr, " -x <label separator>\n");
824 fprintf(stderr, " set the symbol to terminate the label\n");
825 fprintf(stderr, " -l <max number of lines>\n");
826 fprintf(stderr, " set the maximum number of lines to take into account\n");
827 fprintf(stderr, "\n");
831 char **lines = (char **) malloc(nb_lines_max * sizeof(char *));
834 int hash_table_size = nb_lines_max * 10;
837 if(remove_duplicates) {
838 hash_table = new_hash_table(hash_table_size);
841 if(input_filename[0]) {
842 read_file(input_filename,
843 nb_lines_max, &nb_lines, lines,
844 hash_table_size, hash_table);
849 nb_lines_max, &nb_lines, lines,
850 hash_table_size, hash_table);
856 /* Now remove the null strings */
859 for(k = 0; k < nb_lines; k++) {
861 lines[n++] = lines[k];
868 for(i = 0; i < nb_lines / 2; i++) {
869 char *s = lines[nb_lines - 1 - i];
870 lines[nb_lines - 1 - i] = lines[i];
875 /* Build the labels from the strings, take only the part before the
876 label_separator and transform control characters to printable
879 char **labels = (char **) malloc(nb_lines * sizeof(char *));
880 for(l = 0; l < nb_lines; l++) {
885 while(*t && *t != label_separator) {
889 labels[l] = (char *) malloc((e + 1) * sizeof(char));
892 while(*t && *t != label_separator) {
894 while(*u) { *s++ = *u++; }
899 char pattern[buffer_size];
905 /* Here we start to display with curse */
911 intrflush(stdscr, FALSE);
913 /* So that the arrow keys work */
914 keypad(stdscr, TRUE);
916 attr_error = A_STANDOUT;
917 attr_modeline = A_REVERSE;
918 attr_focus_line = A_STANDOUT;
920 if(with_colors && has_colors()) {
924 if(color_fg_modeline < 0 || color_fg_modeline >= COLORS ||
925 color_bg_modeline < 0 || color_bg_modeline >= COLORS ||
926 color_fg_highlight < 0 || color_bg_highlight >= COLORS ||
927 color_bg_highlight < 0 || color_bg_highlight >= COLORS) {
930 fprintf(stderr, "Color numbers have to be between 0 and %d.\n", COLORS - 1);
934 init_pair(1, color_fg_modeline, color_bg_modeline);
935 attr_modeline = COLOR_PAIR(1);
937 init_pair(2, color_fg_highlight, color_bg_highlight);
938 attr_focus_line = COLOR_PAIR(2);
940 init_pair(3, COLOR_WHITE, COLOR_RED);
941 attr_error = COLOR_PAIR(3);
946 int current_focus_line = 0, displayed_focus_line = 0;
948 update_screen(¤t_focus_line, &displayed_focus_line,
950 nb_lines, labels, cursor_position, pattern);
958 if(key >= ' ' && key <= '~') { /* Insert character */
959 insert_char(pattern, &cursor_position, key);
962 else if(key == KEY_BACKSPACE ||
963 key == '\010' || /* ^H */
964 key == '\177') { /* ^? */
965 backspace_char(pattern, &cursor_position);
968 else if(key == KEY_DC ||
969 key == '\004') { /* ^D */
970 delete_char(pattern, &cursor_position);
973 else if(key == KEY_HOME) {
974 current_focus_line = 0;
977 else if(key == KEY_END) {
978 current_focus_line = nb_lines - 1;
981 else if(key == KEY_NPAGE) {
985 else if(key == KEY_PPAGE) {
989 else if(key == KEY_DOWN ||
990 key == '\016') { /* ^N */
994 else if(key == KEY_UP ||
995 key == '\020') { /* ^P */
999 else if(key == KEY_LEFT ||
1000 key == '\002') { /* ^B */
1001 if(cursor_position > 0) cursor_position--;
1002 else error_feedback();
1005 else if(key == KEY_RIGHT ||
1006 key == '\006') { /* ^F */
1007 if(pattern[cursor_position]) cursor_position++;
1008 else error_feedback();
1011 else if(key == '\001') { /* ^A */
1012 cursor_position = 0;
1015 else if(key == '\005') { /* ^E */
1016 cursor_position = strlen(pattern);
1019 else if(key == '\022') { /* ^R */
1020 use_regexp = !use_regexp;
1023 else if(key == '\011') { /* ^I */
1024 case_sensitive = !case_sensitive;
1027 else if(key == '\025') { /* ^U */
1028 kill_before_cursor(pattern, &cursor_position);
1031 else if(key == '\013') { /* ^K */
1032 kill_after_cursor(pattern, &cursor_position);
1035 else if(key == '\014') { /* ^L */
1036 /* I suspect that we may sometime mess up the display */
1040 update_screen(¤t_focus_line, &displayed_focus_line,
1042 nb_lines, labels, cursor_position, pattern);
1044 } while(key != '\007' && /* ^G */
1045 key != '\033' && /* ^[ (escape) */
1052 /* Here we come back to standard display */
1054 if((key == KEY_ENTER || key == '\n')) {
1058 if(displayed_focus_line >= 0 && displayed_focus_line < nb_lines) {
1059 t = lines[displayed_focus_line];
1060 if(label_separator) {
1061 while(*t && *t != label_separator) t++;
1068 if(output_to_vt_buffer && t) {
1069 inject_into_tty_buffer(t);
1072 if(output_filename[0]) {
1073 FILE *out = fopen(output_filename, "w");
1080 fprintf(stderr, "Can not open %s for writing.\n", output_filename);
1087 printf("Aborted.\n");
1090 for(l = 0; l < nb_lines; l++) {