Automatic commit
[selector.git] / selector.cc
1 /*
2  *  selector is a simple shell command for selection of strings with a
3  *  dynamic pattern-matching.
4  *
5  *  Copyright (c) 2009 Francois Fleuret
6  *  Written by Francois Fleuret <francois.fleuret@idiap.ch>
7  *
8  *  This file is part of selector.
9  *
10  *  selector is free software: you can redistribute it and/or modify
11  *  it under the terms of the GNU General Public License version 3 as
12  *  published by the Free Software Foundation.
13  *
14  *  selector is distributed in the hope that it will be useful, but
15  *  WITHOUT ANY WARRANTY; without even the implied warranty of
16  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17  *  General Public License for more details.
18  *
19  *  You should have received a copy of the GNU General Public License
20  *  along with selector.  If not, see <http://www.gnu.org/licenses/>.
21  *
22  */
23
24 /*
25
26   Here is the magical shell script for a smart bash-history. Note that
27   the line remains in /tmp/selector.out, which may be a security
28   concern.
29
30   ./selector -f ~/.bash_history
31   OLD_SETTINGS=`stty -g`
32   stty -echo raw
33   writevt `tty` "`cat /tmp/selector.out`"
34   stty ${OLD_SETTINGS}
35
36 */
37
38 #include <stdio.h>
39 #include <ncurses.h>
40 #include <iostream>
41 #include <fstream>
42 #include <string.h>
43 #include <stdlib.h>
44 #include <sys/types.h>
45 #include <sys/stat.h>
46 #include <fcntl.h>
47
48 using namespace std;
49
50 int buffer_size = 1024;
51 int nb_lines_max = 1000;
52 char pattern_separator = ';';
53 int output_to_vt_buffer = 0;
54
55 int match(char *string, int nb_patterns, char **patterns) {
56   for(int n = 0; n < nb_patterns; n++) {
57     if(strstr(string, patterns[n]) == 0) return 0;
58   }
59   return 1;
60 }
61
62 void check_opt(int argc, char **argv, int n_opt, int n, const char *help) {
63   if(n_opt + n >= argc) {
64     cerr << "Missing argument for " << argv[n_opt] << "."
65          << " "
66          << "Expecting " << help << "."
67          << endl;
68     exit(1);
69   }
70 }
71
72 int previous_visible(int current_line, int nb_lines, char **lines, int nb_patterns, char **patterns) {
73   int line = current_line - 1;
74   while(line >= 0 && !match(lines[line], nb_patterns, patterns)) line--;
75   return line;
76 }
77
78 int next_visible(int current_line, int nb_lines, char **lines, int nb_patterns, char **patterns) {
79   int line = current_line + 1;
80   while(line < nb_lines && !match(lines[line], nb_patterns, patterns)) line++;
81
82   if(line < nb_lines)
83     return line;
84   else
85     return -1;
86 }
87
88 void update_screen(int *current_line, int *temporary_line, int motion,
89                    int nb_lines, char **lines,
90                    char *pattern_list,
91                    int no_blink) {
92
93   char buffer[buffer_size];
94
95   // We split the pattern list into individual patterns
96
97   int nb_patterns = 1;
98
99   for(char *s = pattern_list; *s; s++) {
100     if(*s == pattern_separator) {
101       nb_patterns++;
102     }
103   }
104
105   char splitted_patterns[strlen(pattern_list) + 1];
106   char *patterns[nb_patterns];
107
108   strcpy(splitted_patterns, pattern_list);
109
110   int n = 0;
111   char *last_pattern_start = splitted_patterns;
112   for(char *s = splitted_patterns; n < nb_patterns; s++) {
113     if(*s == pattern_separator || *s == '\0') {
114       *s = '\0';
115       patterns[n++] = last_pattern_start;
116       last_pattern_start = s + 1;
117     }
118   }
119
120   // We now take care of printing the lines per se
121
122   int console_width = getmaxx(stdscr);
123   int console_height = getmaxy(stdscr);
124
125   int nb_printed_lines = 1;
126
127   // First, we find a visible line. In priority: The current, or the
128   // first visible after it, or the first visible before it.
129
130   int new_line;
131   if(match(lines[*current_line], nb_patterns, patterns)) {
132     new_line = *current_line;
133   } else {
134     new_line = next_visible(*current_line, nb_lines, lines, nb_patterns, patterns);
135     if(new_line < 0) {
136       new_line = previous_visible(*current_line, nb_lines, lines, nb_patterns, patterns);
137     }
138   }
139
140   // If we found a visible line and we should move, let's move
141
142   if(new_line >= 0 && motion != 0) {
143     int l = new_line;
144     if(motion > 0) {
145       // We want to go down, let's find the first visible line below
146       for(int m = 0; l >= 0 && m < motion; m++) {
147         l = next_visible(l, nb_lines, lines, nb_patterns, patterns);
148         if(l >= 0) {
149           new_line = l;
150         }
151       }
152     } else {
153       // We want to go up, let's find the first visible line above
154       for(int m = 0; l >= 0 && m < -motion; m++) {
155         l = previous_visible(l, nb_lines, lines, nb_patterns, patterns);
156         if(l >= 0) {
157           new_line = l;
158         }
159       }
160     }
161   }
162
163   if(!no_blink) {
164     clear();
165   }
166
167   use_default_colors();
168
169   addstr("\n");
170
171   // Here new_line is either a line number matching the patterns, or -1
172
173   if(new_line >= 0) {
174
175     int first_line = new_line, last_line = new_line, nb_match = 1;
176
177     // We find the first and last line to show, so that the total of
178     // visible lines between them (them include) is console_height - 1
179
180     while(nb_match < console_height-1 && (first_line > 0 || last_line < nb_lines - 1)) {
181
182       if(first_line > 0) {
183         first_line--;
184         while(first_line > 0 && !match(lines[first_line], nb_patterns, patterns)) {
185           first_line--;
186         }
187         if(match(lines[first_line], nb_patterns, patterns)) {
188           nb_match++;
189         }
190       }
191
192       if(last_line < nb_lines - 1) {
193         last_line++;
194         while(last_line < nb_lines - 1 && !match(lines[last_line], nb_patterns, patterns)) {
195           last_line++;
196         }
197
198         if(match(lines[last_line], nb_patterns, patterns)) {
199           nb_match++;
200         }
201       }
202     }
203
204     // Now we display them
205
206     for(int l = first_line; l <= last_line; l++) {
207       if(match(lines[l], nb_patterns, patterns)) {
208         int k = 0;
209
210         while(lines[l][k] && k < buffer_size - 2 && k < console_width - 2) {
211           buffer[k] = lines[l][k];
212           k++;
213         }
214
215         if(no_blink || l == new_line) {
216           while(k < console_width) {
217             buffer[k++] = ' ';
218           }
219         }
220
221         buffer[k++] = '\n';
222         buffer[k++] = '\0';
223
224         if(l == new_line) {
225           attron(COLOR_PAIR(2));
226           addnstr(buffer, console_width);
227           attroff(COLOR_PAIR(2));
228         } else {
229           addnstr(buffer, console_width);
230         }
231
232         nb_printed_lines++;
233       }
234     }
235
236     *temporary_line = new_line;
237     if(motion != 0) {
238       *current_line = new_line;
239     }
240   }
241
242   // if(nb_printed_lines == 1) {
243   // addnstr("[no selection]\n", console_width);
244   // nb_printed_lines++;
245   // }
246
247   if(no_blink) { // Erase the rest of the window. That's slightly ugly.
248     int k = 0;
249     while(k < console_width) {
250       buffer[k++] = ' ';
251     }
252     buffer[k++] = '\n';
253     buffer[k++] = '\0';
254     for(int l = nb_printed_lines; l < console_height; l++) {
255       addnstr(buffer, console_width);
256     }
257   }
258
259   // Draw the modeline
260
261   sprintf(buffer, "%d/%d pattern: %s",
262           nb_printed_lines - 1,
263           nb_lines,
264           pattern_list);
265
266   for(int k = strlen(buffer); k < console_width; k++) buffer[k] = ' ';
267   buffer[console_width] = '\0';
268
269   move(0, 0);
270   attron(COLOR_PAIR(1));
271   addnstr(buffer, console_width);
272   attroff(COLOR_PAIR(1));
273
274   refresh();       // After doing something on the display, we refresh it
275 }
276
277 //////////////////////////////////////////////////////////////////////
278
279 int main(int argc, char **argv) {
280   char buffer[buffer_size];
281   char *lines[nb_lines_max];
282   int no_blink = 0;
283
284   char input_filename[buffer_size], output_filename[buffer_size];
285   strcpy(input_filename, "/dev/stdin");
286   strcpy(output_filename, "/tmp/selector.out");
287
288   int i = 1;
289   while(i < argc) {
290     if(strcmp(argv[i], "-o") == 0) {
291       check_opt(argc, argv, i, 1, "<output filename>");
292       strncpy(output_filename, argv[i+1], buffer_size);
293       i += 2;
294     }
295
296     else if(strcmp(argv[i], "-s") == 0) {
297       check_opt(argc, argv, i, 1, "<pattern separator>");
298       pattern_separator = argv[i+1][0];
299       i += 2;
300     }
301
302     else if(strcmp(argv[i], "-v") == 0) {
303       output_to_vt_buffer = 1;
304       i++;
305     }
306
307     else if(strcmp(argv[i], "-f") == 0) {
308       check_opt(argc, argv, i, 1, "<input filename>");
309       strncpy(input_filename, argv[i+1], buffer_size);
310       i += 2;
311     }
312
313     else if(strcmp(argv[i], "-b") == 0) {
314       no_blink = 1;
315       i++;
316     }
317
318     else if(strcmp(argv[i], "-l") == 0) {
319       check_opt(argc, argv, i, 1, "<maximum number of lines>");
320       nb_lines_max = atoi(argv[i+1]);
321       i += 2;
322     }
323
324     else {
325       cerr << argv[0] << " [-h] [-o <output filename>] [-b] [-l <max number of lines>] [-s <pattern separator>] [-v]" << endl;
326       if(strcmp(argv[i], "-h") == 0) {
327         exit(0);
328       } else {
329         exit(1);
330       }
331     }
332   }
333
334   ifstream file(input_filename);
335
336   if(file.fail()) {
337     cerr << "Can not open \"" << input_filename << "\"" << endl;
338     return 1;
339   }
340
341   int nb_lines = 0;
342   while(nb_lines < nb_lines_max && !file.eof()) {
343     file.getline(buffer, buffer_size);
344     lines[nb_lines] = new char[strlen(buffer) + 1];
345     strcpy(lines[nb_lines], buffer);
346     nb_lines++;
347   }
348
349   char patterns[buffer_size];
350   patterns[0] = '\0';
351   int patterns_point;
352   patterns_point = 0;
353
354   initscr();
355
356   if(!has_colors()) {
357     cerr << "No colors." << endl;
358     return 1;
359   }
360
361   noecho();
362   curs_set(0);
363   keypad(stdscr, TRUE);
364
365   start_color();
366   // init_pair(1, COLOR_WHITE, COLOR_BLACK);
367   init_pair(1, COLOR_WHITE, COLOR_GREEN);
368   init_pair(2, COLOR_BLACK, COLOR_YELLOW);
369
370   int key;
371
372   int current_line = 0, temporary_line = 0;
373
374   update_screen(&current_line, &temporary_line, 0, nb_lines, lines, patterns, no_blink);
375
376   do {
377
378     key = getch();
379
380     int motion = 0;
381
382     if(key >= ' ' && key <= 'z') {
383       patterns[patterns_point++] = key;
384       patterns[patterns_point] = '\0';
385     }
386
387     else if(key == KEY_BACKSPACE || key == KEY_DC || key == '\b') {
388       if(patterns_point > 0) {
389         patterns_point--;
390         patterns[patterns_point] = '\0';
391       }
392     }
393
394     else if(key == KEY_HOME) {
395       current_line = 0;
396     }
397
398     else if(key == KEY_END) {
399       current_line = nb_lines - 1;
400     }
401
402     else if(key == KEY_NPAGE) {
403       motion = 10;
404     }
405
406     else if(key == KEY_PPAGE) {
407       motion = -10;
408     }
409
410     else if(key == KEY_UP || key == '\10') {
411       motion = -1;
412     }
413
414     else if(key == KEY_DOWN || key == '\ e') {
415       motion = 1;
416     }
417
418     update_screen(&current_line, &temporary_line, motion,
419                   nb_lines, lines, patterns, no_blink);
420   } while(key != '\n' && key != KEY_ENTER && key != '\a');
421
422   echo();
423   curs_set(1);
424   endwin();
425
426   if(output_to_vt_buffer) {
427     if((key == KEY_ENTER || key == '\n') && temporary_line >= 0 && temporary_line < nb_lines) {
428       char *tty = ttyname (STDIN_FILENO);
429       int fd = open(tty, O_WRONLY);
430       write(fd, lines[temporary_line], strlen(lines[temporary_line]));
431       close(fd);
432     }
433   } else {
434
435     ofstream out(output_filename);
436     if(out.fail()) {
437       cerr << "Can not open " << output_filename << " for writing." << endl;
438       exit(1);
439     } else {
440       if((key == KEY_ENTER || key == '\n') && temporary_line >= 0 && temporary_line < nb_lines) {
441         out << lines[temporary_line] << endl;
442       } else {
443         out << endl;
444       }
445       out.flush();
446     }
447   }
448
449   for(int l = 0; l < nb_lines; l++) {
450     delete[] lines[l];
451   }
452
453   exit(0);
454 }