Imported Upstream version 2.1.2
[platform/upstream/fdupes.git] / ncurses-interface.c
1 /* FDUPES Copyright (c) 2018 Adrian Lopez
2
3    Permission is hereby granted, free of charge, to any person
4    obtaining a copy of this software and associated documentation files
5    (the "Software"), to deal in the Software without restriction,
6    including without limitation the rights to use, copy, modify, merge,
7    publish, distribute, sublicense, and/or sell copies of the Software,
8    and to permit persons to whom the Software is furnished to do so,
9    subject to the following conditions:
10
11    The above copyright notice and this permission notice shall be
12    included in all copies or substantial portions of the Software.
13
14    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
15    OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
16    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
17    IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
18    CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
19    TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
20    SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
21
22 #include "config.h"
23 #include <stdlib.h>
24 #include <string.h>
25 #include <wchar.h>
26 #ifdef HAVE_NCURSESW_CURSES_H
27   #include <ncursesw/curses.h>
28 #else
29   #include <curses.h>
30 #endif
31 #include "ncurses-interface.h"
32 #include "ncurses-getcommand.h"
33 #include "ncurses-commands.h"
34 #include "ncurses-prompt.h"
35 #include "ncurses-status.h"
36 #include "ncurses-print.h"
37 #include "mbstowcs_escape_invalid.h"
38 #include "positive_wcwidth.h"
39 #include "commandidentifier.h"
40 #include "filegroup.h"
41 #include "errormsg.h"
42 #include "log.h"
43 #include "sigint.h"
44 #include "flags.h"
45
46 char *fmttime(time_t t);
47
48 enum linestyle
49 {
50   linestyle_groupheader = 0,
51   linestyle_groupheaderspacing,
52   linestyle_filename,
53   linestyle_groupfooterspacing
54 };
55
56 enum linestyle getlinestyle(struct filegroup *group, int line)
57 {
58   if (line <= group->startline)
59     return linestyle_groupheader;
60   else if (line == group->startline + 1)
61     return linestyle_groupheaderspacing;
62   else if (line >= group->endline)
63     return linestyle_groupfooterspacing;
64   else
65     return linestyle_filename;
66 }
67
68 #define FILENAME_INDENT_EXTRA 5
69 #define FILE_INDEX_MIN_WIDTH 3
70
71 int filerowcount(file_t *file, const int columns, int group_file_count)
72 {
73   int lines;
74   int line_remaining;
75   size_t x = 0;
76   size_t read;
77   size_t filename_bytes;
78   wchar_t ch;
79   mbstate_t mbstate;
80   int index_width;
81   int timestamp_width;
82   size_t needed;
83   wchar_t *wcfilename;
84
85   memset(&mbstate, 0, sizeof(mbstate));
86
87   needed = mbstowcs_escape_invalid(0, file->d_name, 0);
88
89   wcfilename = (wchar_t*)malloc(sizeof(wchar_t) * needed);
90   if (wcfilename == 0)
91     return 0;
92
93   mbstowcs_escape_invalid(wcfilename, file->d_name, needed);
94
95   index_width = get_num_digits(group_file_count);
96   if (index_width < FILE_INDEX_MIN_WIDTH)
97     index_width = FILE_INDEX_MIN_WIDTH;
98
99   timestamp_width = ISFLAG(flags, F_SHOWTIME) ? 19 : 0;
100
101   lines = (index_width + timestamp_width + FILENAME_INDENT_EXTRA) / columns + 1;
102
103   line_remaining = columns - (index_width + timestamp_width + FILENAME_INDENT_EXTRA) % columns;
104
105   while (wcfilename[x] != L'\0')
106   {
107     if (positive_wcwidth(wcfilename[x]) <= line_remaining)
108     {
109       line_remaining -= positive_wcwidth(wcfilename[x]);
110     }
111     else
112     {
113       line_remaining = columns - positive_wcwidth(wcfilename[x]);
114       ++lines;
115     }
116
117     ++x;
118   }
119
120   free(wcfilename);
121
122   return lines;
123 }
124
125 int getgroupindex(struct filegroup *groups, int group_count, int group_hint, int line)
126 {
127   int group = group_hint;
128
129   while (group > 0 && line < groups[group].startline)
130     --group;
131
132   while (group < group_count && line > groups[group].endline)
133     ++group;
134
135   return group;
136 }
137
138 int getgroupfileindex(int *row, struct filegroup *group, int line, int columns)
139 {
140   int l;
141   int f = 0;
142   int rowcount;
143
144   l = group->startline + 2;
145
146   while (f < group->filecount)
147   {
148     rowcount = filerowcount(group->files[f].file, columns, group->filecount);
149
150     if (line <= l + rowcount - 1)
151     {
152       *row = line - l;
153       return f;
154     }
155
156     l += rowcount;
157     ++f;
158   }
159
160   return -1;
161 }
162
163 int getgroupfileline(struct filegroup *group, int fileindex, int columns)
164 {
165   int l;
166   int f = 0;
167   int rowcount;
168
169   l = group->startline + 2;
170
171   while (f < fileindex && f < group->filecount)
172   {
173     rowcount = filerowcount(group->files[f].file, columns, group->filecount);
174     l += rowcount;
175     ++f;
176   }
177
178   return l;
179 }
180
181 void set_file_action(struct groupfile *file, int new_action, size_t *deletion_tally)
182 {
183   switch (file->action)
184   {
185     case -1:
186       if (new_action != -1)
187         --*deletion_tally;
188       break;
189
190     default:
191       if (new_action == -1)
192         ++*deletion_tally;
193       break;
194   }
195
196   file->action = new_action;
197 }
198
199 void scroll_to_group(int *topline, int group, int tail, struct filegroup *groups, WINDOW *filewin)
200 {
201   if (*topline < groups[group].startline)
202   {
203     if (groups[group].endline >= *topline + getmaxy(filewin))
204     {
205       if (groups[group].endline - groups[group].startline < getmaxy(filewin))
206         *topline = groups[group].endline - getmaxy(filewin) + 1;
207       else
208         *topline = groups[group].startline;
209     }
210   }
211   else
212   {
213     if (groups[group].endline - groups[group].startline < getmaxy(filewin) || !tail)
214       *topline = groups[group].startline;
215     else
216       *topline = groups[group].endline - getmaxy(filewin);
217   }
218 }
219
220 void move_to_next_group(int *topline, int *cursorgroup, int *cursorfile, struct filegroup *groups, WINDOW *filewin)
221 {
222   *cursorgroup += 1;
223
224   *cursorfile = 0;
225
226   scroll_to_group(topline, *cursorgroup, 0, groups, filewin);
227 }
228
229 int move_to_next_selected_group(int *topline, int *cursorgroup, int *cursorfile, struct filegroup *groups, int totalgroups, WINDOW *filewin)
230 {
231   size_t g;
232
233   for (g = *cursorgroup + 1; g < totalgroups; ++g)
234   {
235     if (groups[g].selected)
236     {
237       *cursorgroup = g;
238       *cursorfile = 0;
239
240       scroll_to_group(topline, *cursorgroup, 0, groups, filewin);
241
242       return 1;
243     }
244   }
245
246   return 0;
247 }
248
249 void move_to_next_file(int *topline, int *cursorgroup, int *cursorfile, struct filegroup *groups, WINDOW *filewin)
250 {
251   *cursorfile += 1;
252
253   if (getgroupfileline(&groups[*cursorgroup], *cursorfile, COLS) >= *topline + getmaxy(filewin))
254   {
255       if (groups[*cursorgroup].endline - getgroupfileline(&groups[*cursorgroup], *cursorfile, COLS) < getmaxy(filewin))
256         *topline = groups[*cursorgroup].endline - getmaxy(filewin) + 1;
257       else
258         *topline = getgroupfileline(&groups[*cursorgroup], *cursorfile, COLS);
259   }
260 }
261
262 void move_to_previous_group(int *topline, int *cursorgroup, int *cursorfile, struct filegroup *groups, WINDOW *filewin)
263 {
264   *cursorgroup -= 1;
265
266   *cursorfile = groups[*cursorgroup].filecount - 1;
267
268   scroll_to_group(topline, *cursorgroup, 1, groups, filewin);
269 }
270
271 int move_to_previous_selected_group(int *topline, int *cursorgroup, int *cursorfile, struct filegroup *groups, int totalgroups, WINDOW *filewin)
272 {
273   size_t g;
274
275   for (g = *cursorgroup; g > 0; --g)
276   {
277     if (groups[g - 1].selected)
278     {
279       *cursorgroup = g - 1;
280       *cursorfile = 0;
281
282       scroll_to_group(topline, *cursorgroup, 0, groups, filewin);
283
284       return 1;
285     }
286   }
287
288   return 0;
289 }
290
291 void move_to_previous_file(int *topline, int *cursorgroup, int *cursorfile, struct filegroup *groups, WINDOW *filewin)
292 {
293   *cursorfile -= 1;
294
295   if (getgroupfileline(&groups[*cursorgroup], *cursorfile, COLS) < *topline)
296   {
297       if (getgroupfileline(&groups[*cursorgroup], *cursorfile, COLS) - groups[*cursorgroup].startline < getmaxy(filewin))
298         *topline -= getgroupfileline(&groups[*cursorgroup], *cursorfile, COLS) - groups[*cursorgroup].startline + 1;
299       else
300         *topline -= getmaxy(filewin);
301   }
302 }
303
304 #define FILE_LIST_OK 1
305 #define FILE_LIST_ERROR_INDEX_OUT_OF_RANGE -1
306 #define FILE_LIST_ERROR_LIST_CONTAINS_INVALID_INDEX -2
307 #define FILE_LIST_ERROR_UNKNOWN_COMMAND -3
308 #define FILE_LIST_ERROR_OUT_OF_MEMORY -4
309
310 int validate_file_list(struct filegroup *currentgroup, wchar_t *commandbuffer_in)
311 {
312   wchar_t *commandbuffer;
313   wchar_t *token;
314   wchar_t *wcsptr;
315   wchar_t *wcstolcheck;
316   long int number;
317   int parts = 0;
318   int parse_error = 0;
319   int out_of_bounds_error = 0;
320
321   commandbuffer = malloc(sizeof(wchar_t) * (wcslen(commandbuffer_in)+1));
322   if (commandbuffer == 0)
323     return FILE_LIST_ERROR_OUT_OF_MEMORY;
324
325   wcscpy(commandbuffer, commandbuffer_in);
326
327   token = wcstok(commandbuffer, L",", &wcsptr);
328
329   while (token != NULL)
330   {
331     ++parts;
332
333     number = wcstol(token, &wcstolcheck, 10);
334     if (wcstolcheck == token || *wcstolcheck != '\0')
335       parse_error = 1;
336
337     if (number > currentgroup->filecount || number < 1)
338       out_of_bounds_error = 1;
339
340     token = wcstok(NULL, L",", &wcsptr);
341   }
342
343   free(commandbuffer);
344
345   if (parts == 1 && parse_error)
346     return FILE_LIST_ERROR_UNKNOWN_COMMAND;
347   else if (parse_error)
348     return FILE_LIST_ERROR_LIST_CONTAINS_INVALID_INDEX;
349   else if (out_of_bounds_error)
350     return FILE_LIST_ERROR_INDEX_OUT_OF_RANGE;
351
352   return FILE_LIST_OK;
353 }
354
355 void deletefiles_ncurses(file_t *files, char *logfile)
356 {
357   WINDOW *filewin;
358   WINDOW *promptwin;
359   WINDOW *statuswin;
360   file_t *curfile;
361   file_t *dupefile;
362   struct filegroup *groups;
363   struct filegroup *reallocgroups;
364   size_t groupfilecount;
365   int topline = 0;
366   int cursorgroup = 0;
367   int cursorfile = 0;
368   int cursor_x;
369   int cursor_y;
370   int groupfirstline = 0;
371   int totallines = 0;
372   int allocatedgroups = 0;
373   int totalgroups = 0;
374   size_t groupindex = 0;
375   enum linestyle linestyle;
376   int preservecount;
377   int deletecount;
378   int unresolvedcount;
379   size_t globaldeletiontally = 0;
380   int row;
381   int x;
382   int g;
383   wint_t wch;
384   int keyresult;
385   int cy;
386   int f;
387   wchar_t *commandbuffer;
388   size_t commandbuffersize;
389   wchar_t *commandarguments;
390   struct command_identifier_node *commandidentifier;
391   struct command_identifier_node *confirmationkeywordidentifier;
392   int doprune;
393   wchar_t *token;
394   wchar_t *wcsptr;
395   wchar_t *wcstolcheck;
396   long int number;
397   struct status_text *status;
398   struct prompt_info *prompt;
399   int dupesfound;
400   int intresult;
401   int resumecommandinput = 0;
402   int index_width;
403   int timestamp_width;
404
405   noecho();
406   cbreak();
407   halfdelay(5);
408
409   filewin = newwin(LINES - 2, COLS, 0, 0);
410   statuswin = newwin(1, COLS, LINES - 1, 0);
411   promptwin = newwin(1, COLS, LINES - 2, 0);
412
413   scrollok(filewin, FALSE);
414   scrollok(statuswin, FALSE);
415   scrollok(promptwin, FALSE);
416
417   wattron(statuswin, A_REVERSE);
418
419   keypad(promptwin, 1);
420
421   commandbuffersize = 80;
422   commandbuffer = malloc(commandbuffersize * sizeof(wchar_t));
423   if (commandbuffer == 0)
424   {
425     endwin();
426     errormsg("out of memory\n");
427     exit(1);
428   }
429
430   allocatedgroups = 1024;
431   groups = malloc(sizeof(struct filegroup) * allocatedgroups);
432   if (groups == 0)
433   {
434     free(commandbuffer);
435
436     endwin();
437     errormsg("out of memory\n");
438     exit(1);
439   }
440
441   commandidentifier = build_command_identifier_tree(command_list);
442   if (commandidentifier == 0)
443   {
444     free(groups);
445     free(commandbuffer);
446
447     endwin();
448     errormsg("out of memory\n");
449     exit(1);
450   }
451
452   confirmationkeywordidentifier = build_command_identifier_tree(confirmation_keyword_list);
453   if (confirmationkeywordidentifier == 0)
454   {
455     free(groups);
456     free(commandbuffer);
457     free_command_identifier_tree(commandidentifier);
458
459     endwin();
460     errormsg("out of memory\n");
461     exit(1);
462   }
463
464   register_sigint_handler();
465
466   curfile = files;
467   while (curfile)
468   {
469     if (!curfile->hasdupes)
470     {
471       curfile = curfile->next;
472       continue;
473     }
474
475     if (totalgroups + 1 > allocatedgroups)
476     {
477       allocatedgroups *= 2;
478
479       reallocgroups = realloc(groups, sizeof(struct filegroup) * allocatedgroups);
480       if (reallocgroups == 0)
481       {
482         for (g = 0; g < totalgroups; ++g)
483           free(groups[g].files);
484
485         free(groups);
486         free(commandbuffer);
487         free_command_identifier_tree(commandidentifier);
488         free_command_identifier_tree(confirmationkeywordidentifier);
489
490         endwin();
491         errormsg("out of memory\n");
492         exit(1);
493       }
494
495       groups = reallocgroups;
496     }
497
498     groups[totalgroups].startline = groupfirstline;
499     groups[totalgroups].endline = groupfirstline + 2;
500     groups[totalgroups].selected = 0;
501
502     groupfilecount = 0;
503
504     dupefile = curfile;
505     do
506     {
507       ++groupfilecount;
508
509       dupefile = dupefile->duplicates;
510     } while(dupefile);
511
512     dupefile = curfile;
513     do
514     {
515       groups[totalgroups].endline += filerowcount(dupefile, COLS, groupfilecount);
516
517       dupefile = dupefile->duplicates;
518     } while (dupefile);
519
520     groups[totalgroups].files = malloc(sizeof(struct groupfile) * groupfilecount);
521     if (groups[totalgroups].files == 0)
522     {
523       for (g = 0; g < totalgroups; ++g)
524         free(groups[g].files);
525
526       free(groups);
527       free(commandbuffer);
528       free_command_identifier_tree(commandidentifier);
529       free_command_identifier_tree(confirmationkeywordidentifier);
530
531       endwin();
532       errormsg("out of memory\n");
533       exit(1);
534     }
535
536     groupfilecount = 0;
537
538     dupefile = curfile;
539     do
540     {
541       groups[totalgroups].files[groupfilecount].file = dupefile;
542       groups[totalgroups].files[groupfilecount].action = 0;
543       groups[totalgroups].files[groupfilecount].selected = 0;
544       ++groupfilecount;
545
546       dupefile = dupefile->duplicates;
547     } while (dupefile);
548
549     groups[totalgroups].filecount = groupfilecount;
550
551     groupfirstline = groups[totalgroups].endline + 1;
552
553     ++totalgroups;
554
555     curfile = curfile->next;
556   }
557
558   dupesfound = totalgroups > 0;
559
560   status = status_text_alloc(0, COLS);
561   if (status == 0)
562   {
563     for (g = 0; g < totalgroups; ++g)
564       free(groups[g].files);
565
566     free(groups);
567     free(commandbuffer);
568     free_command_identifier_tree(commandidentifier);
569     free_command_identifier_tree(confirmationkeywordidentifier);
570
571     endwin();
572     errormsg("out of memory\n");
573     exit(1);
574   }
575
576   format_status_left(status, L"Ready");
577
578   prompt = prompt_info_alloc(80);
579   if (prompt == 0)
580   {
581     free_status_text(status);
582
583     for (g = 0; g < totalgroups; ++g)
584       free(groups[g].files);
585
586     free(groups);
587     free(commandbuffer);
588     free_command_identifier_tree(commandidentifier);
589     free_command_identifier_tree(confirmationkeywordidentifier);
590
591     endwin();
592     errormsg("out of memory\n");
593     exit(1);
594   }
595
596   doprune = 1;
597   do
598   {
599     wmove(filewin, 0, 0);
600     werase(filewin);
601
602     if (totalgroups > 0)
603       totallines = groups[totalgroups-1].endline;
604     else
605       totallines = 0;
606
607     for (x = topline; x < topline + getmaxy(filewin); ++x)
608     {
609       if (x >= totallines)
610       {
611         wclrtoeol(filewin);
612         continue;
613       }
614
615       groupindex = getgroupindex(groups, totalgroups, groupindex, x);
616
617       index_width = get_num_digits(groups[groupindex].filecount);
618
619       if (index_width < FILE_INDEX_MIN_WIDTH)
620         index_width = FILE_INDEX_MIN_WIDTH;
621
622       timestamp_width = ISFLAG(flags, F_SHOWTIME) ? 19 : 0;
623
624       linestyle = getlinestyle(groups + groupindex, x);
625       
626       if (linestyle == linestyle_groupheader)
627       {
628         wattron(filewin, A_BOLD);
629         if (groups[groupindex].selected)
630           wattron(filewin, A_REVERSE);
631         wprintw(filewin, "Set %d of %d:\n", groupindex + 1, totalgroups);
632         if (groups[groupindex].selected)
633           wattroff(filewin, A_REVERSE);
634         wattroff(filewin, A_BOLD);
635       }
636       else if (linestyle == linestyle_groupheaderspacing)
637       {
638         wprintw(filewin, "\n");
639       }
640       else if (linestyle == linestyle_filename)
641       {
642         f = getgroupfileindex(&row, groups + groupindex, x, COLS);
643
644         if (cursorgroup != groupindex)
645         {
646           if (row == 0)
647           {
648             print_spaces(filewin, index_width);
649
650             wprintw(filewin, " [%c] ", groups[groupindex].files[f].action > 0 ? '+' : groups[groupindex].files[f].action < 0 ? '-' : ' ');
651
652             if (ISFLAG(flags, F_SHOWTIME))
653               wprintw(filewin, "[%s] ", fmttime(groups[groupindex].files[f].file->mtime));
654           }
655
656           cy = getcury(filewin);
657
658           if (groups[groupindex].files[f].selected)
659             wattron(filewin, A_REVERSE);
660           putline(filewin, groups[groupindex].files[f].file->d_name, row, COLS, index_width + timestamp_width + FILENAME_INDENT_EXTRA);
661           if (groups[groupindex].files[f].selected)
662             wattroff(filewin, A_REVERSE);
663
664           wclrtoeol(filewin);
665           wmove(filewin, cy+1, 0);
666         }
667         else
668         {
669           if (row == 0)
670           {
671             print_right_justified_int(filewin, f+1, index_width);
672             wprintw(filewin, " ");
673
674             if (cursorgroup == groupindex && cursorfile == f)
675               wattron(filewin, A_REVERSE);
676             wprintw(filewin, "[%c]", groups[groupindex].files[f].action > 0 ? '+' : groups[groupindex].files[f].action < 0 ? '-' : ' ');
677             if (cursorgroup == groupindex && cursorfile == f)
678               wattroff(filewin, A_REVERSE);
679             wprintw(filewin, " ");
680
681             if (ISFLAG(flags, F_SHOWTIME))
682               wprintw(filewin, "[%s] ", fmttime(groups[groupindex].files[f].file->mtime));
683           }
684
685           cy = getcury(filewin);
686
687           if (groups[groupindex].files[f].selected)
688             wattron(filewin, A_REVERSE);
689           putline(filewin, groups[groupindex].files[f].file->d_name, row, COLS, index_width + timestamp_width + FILENAME_INDENT_EXTRA);
690           if (groups[groupindex].files[f].selected)
691             wattroff(filewin, A_REVERSE);
692
693           wclrtoeol(filewin);
694           wmove(filewin, cy+1, 0);
695         }
696       }
697       else if (linestyle == linestyle_groupfooterspacing)
698       {
699         wprintw(filewin, "\n");
700       }
701     }
702
703     if (totalgroups > 0)
704       format_status_right(status, L"Set %d of %d", cursorgroup+1, totalgroups);
705     else
706       format_status_right(status, L"Finished");
707
708     print_status(statuswin, status);
709
710     if (totalgroups > 0)
711       format_prompt(prompt, L"( Preserve files [1 - %d, all, help] )", groups[cursorgroup].filecount);
712     else if (dupesfound)
713       format_prompt(prompt, L"( No duplicates remaining; type 'exit' to exit program )");
714     else
715       format_prompt(prompt, L"( No duplicates found; type 'exit' to exit program )");
716
717     print_prompt(promptwin, prompt, L"");
718
719     /* refresh windows (using wrefresh instead of wnoutrefresh to avoid bug in gnome-terminal) */
720     wrefresh(filewin);
721     wrefresh(statuswin);
722     wrefresh(promptwin);
723
724     /* wait for user input */
725     if (!resumecommandinput)
726     {
727       do
728       {
729         keyresult = wget_wch(promptwin, &wch);
730
731         if (got_sigint)
732         {
733           getyx(promptwin, cursor_y, cursor_x);
734
735           format_status_left(status, L"Type 'exit' to exit fdupes.");
736           print_status(statuswin, status);
737
738           wmove(promptwin, cursor_y, cursor_x);
739
740           got_sigint = 0;
741
742           wrefresh(statuswin);
743         }
744       } while (keyresult == ERR);
745
746       if (keyresult == OK && iswprint(wch))
747       {
748         commandbuffer[0] = wch;
749         commandbuffer[1] = '\0';
750       }
751       else
752       {
753         commandbuffer[0] = '\0';
754       }
755     }
756
757     if (resumecommandinput || (keyresult == OK && iswprint(wch) && ((wch != '\t' && wch != '\n' && wch != '?'))))
758     {
759       resumecommandinput = 0;
760
761       switch (get_command_text(&commandbuffer, &commandbuffersize, promptwin, prompt, 1, 1))
762       {
763         case GET_COMMAND_OK:
764           format_status_left(status, L"Ready");
765
766           get_command_arguments(&commandarguments, commandbuffer);
767
768           switch (identify_command(commandidentifier, commandbuffer, 0))
769           {
770             case COMMAND_SELECT_CONTAINING:
771               cmd_select_containing(groups, totalgroups, commandarguments, status);
772               break;
773
774             case COMMAND_SELECT_BEGINNING:
775               cmd_select_beginning(groups, totalgroups, commandarguments, status);
776               break;
777
778             case COMMAND_SELECT_ENDING:
779               cmd_select_ending(groups, totalgroups, commandarguments, status);
780               break;
781
782             case COMMAND_SELECT_MATCHING:
783               cmd_select_matching(groups, totalgroups, commandarguments, status);
784               break;
785
786             case COMMAND_SELECT_REGEX:
787               cmd_select_regex(groups, totalgroups, commandarguments, status);
788               break;
789
790             case COMMAND_CLEAR_SELECTIONS_CONTAINING:
791               cmd_clear_selections_containing(groups, totalgroups, commandarguments, status);
792               break;
793
794             case COMMAND_CLEAR_SELECTIONS_BEGINNING:
795               cmd_clear_selections_beginning(groups, totalgroups, commandarguments, status);
796               break;
797
798             case COMMAND_CLEAR_SELECTIONS_ENDING:
799               cmd_clear_selections_ending(groups, totalgroups, commandarguments, status);
800               break;
801
802             case COMMAND_CLEAR_SELECTIONS_MATCHING:
803               cmd_clear_selections_matching(groups, totalgroups, commandarguments, status);
804               break;
805
806             case COMMAND_CLEAR_SELECTIONS_REGEX:
807               cmd_clear_selections_regex(groups, totalgroups, commandarguments, status);
808               break;
809
810             case COMMAND_CLEAR_ALL_SELECTIONS:
811               cmd_clear_all_selections(groups, totalgroups, commandarguments, status);
812               break;
813
814             case COMMAND_INVERT_GROUP_SELECTIONS:
815               cmd_invert_group_selections(groups, totalgroups, commandarguments, status);
816               break;
817
818             case COMMAND_KEEP_SELECTED:
819               cmd_keep_selected(groups, totalgroups, commandarguments, &globaldeletiontally, status);
820               break;
821
822             case COMMAND_DELETE_SELECTED:
823               cmd_delete_selected(groups, totalgroups, commandarguments, &globaldeletiontally, status);
824               break;
825
826             case COMMAND_RESET_SELECTED:
827               cmd_reset_selected(groups, totalgroups, commandarguments, &globaldeletiontally, status);
828               break;
829
830             case COMMAND_RESET_GROUP:
831               for (x = 0; x < groups[cursorgroup].filecount; ++x)
832                 set_file_action(&groups[cursorgroup].files[x], 0, &globaldeletiontally);
833
834               format_status_left(status, L"Reset all files in current group.");
835
836               break;
837
838             case COMMAND_PRESERVE_ALL:
839               /* mark all files for preservation */
840               for (x = 0; x < groups[cursorgroup].filecount; ++x)
841                 set_file_action(&groups[cursorgroup].files[x], 1, &globaldeletiontally);
842
843               format_status_left(status, L"%d files marked for preservation", groups[cursorgroup].filecount);
844
845               if (cursorgroup < totalgroups - 1)
846                 move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
847
848               break;
849
850             case COMMAND_GOTO_SET:
851               number = wcstol(commandarguments, &wcstolcheck, 10);
852               if (wcstolcheck != commandarguments && *wcstolcheck == '\0')
853               {
854                 if (number >= 1 && number <= totalgroups)
855                 {
856                   scroll_to_group(&topline, number - 1, 0, groups, filewin);
857
858                   cursorgroup = number - 1;
859                   cursorfile = 0;
860                 }
861                 else
862                 {
863                   format_status_left(status, L"Group index out of range.");
864                 }
865               }
866               else
867               {
868                 format_status_left(status, L"Invalid group index.");
869               }
870
871               break;
872
873             case COMMAND_HELP:
874               endwin();
875
876               if (system(HELP_COMMAND_STRING) == -1)
877                 format_status_left(status, L"Could not display help text.");
878
879               refresh();
880
881               break;
882
883             case COMMAND_PRUNE:
884               cmd_prune(groups, totalgroups, commandarguments, &globaldeletiontally, &totalgroups, &cursorgroup, &cursorfile, &topline, logfile, filewin, status);
885               break;
886
887             case COMMAND_EXIT: /* exit program */
888               if (totalgroups == 0)
889               {
890                 doprune = 0;
891                 continue;
892               }
893               else
894               {
895                 if (globaldeletiontally != 0)
896                   format_prompt(prompt, L"( There are files marked for deletion. Exit without deleting? )");
897                 else
898                   format_prompt(prompt, L"( There are duplicates remaining. Exit anyway? )");
899
900                 print_prompt(promptwin, prompt, L"");
901
902                 wrefresh(promptwin);
903
904                 switch (get_command_text(&commandbuffer, &commandbuffersize, promptwin, prompt, 0, 0))
905                 {
906                   case GET_COMMAND_OK:
907                     switch (identify_command(confirmationkeywordidentifier, commandbuffer, 0))
908                     {
909                       case COMMAND_YES:
910                         doprune = 0;
911                         continue;
912
913                       case COMMAND_NO:
914                       case COMMAND_UNDEFINED:
915                         commandbuffer[0] = '\0';
916                         continue;
917                     }
918                     break;
919
920                   case GET_COMMAND_CANCELED:
921                     commandbuffer[0] = '\0';
922                     continue;
923
924                   case GET_COMMAND_RESIZE_REQUESTED:
925                     /* resize windows */
926                     wresize(filewin, LINES - 2, COLS);
927
928                     wresize(statuswin, 1, COLS);
929                     wresize(promptwin, 1, COLS);
930                     mvwin(statuswin, LINES - 1, 0);
931                     mvwin(promptwin, LINES - 2, 0);
932
933                     status_text_alloc(status, COLS);
934
935                     /* recalculate line boundaries */
936                     groupfirstline = 0;
937
938                     for (g = 0; g < totalgroups; ++g)
939                     {
940                       groups[g].startline = groupfirstline;
941                       groups[g].endline = groupfirstline + 2;
942
943                       for (f = 0; f < groups[g].filecount; ++f)
944                         groups[g].endline += filerowcount(groups[g].files[f].file, COLS, groups[g].filecount);
945
946                       groupfirstline = groups[g].endline + 1;
947                     }
948
949                     commandbuffer[0] = '\0';
950
951                     break;
952
953                   case GET_COMMAND_ERROR_OUT_OF_MEMORY:
954                     for (g = 0; g < totalgroups; ++g)
955                       free(groups[g].files);
956
957                     free(groups);
958                     free(commandbuffer);
959                     free_command_identifier_tree(commandidentifier);
960                     free_command_identifier_tree(confirmationkeywordidentifier);
961
962                     endwin();
963                     errormsg("out of memory\n");
964                     exit(1);
965                     break;
966                 }
967               }
968               break;
969
970             default: /* parse list of files to preserve and mark for preservation */
971               intresult = validate_file_list(groups + cursorgroup, commandbuffer);
972               if (intresult != FILE_LIST_OK)
973               {
974                 if (intresult == FILE_LIST_ERROR_UNKNOWN_COMMAND)
975                 {
976                   format_status_left(status, L"Unrecognized command");
977                   break;
978                 }
979                 else if (intresult == FILE_LIST_ERROR_INDEX_OUT_OF_RANGE)
980                 {
981                   format_status_left(status, L"Index out of range (1 - %d).", groups[cursorgroup].filecount);
982                   break;
983                 }
984                 else if (intresult == FILE_LIST_ERROR_LIST_CONTAINS_INVALID_INDEX)
985                 {
986                   format_status_left(status, L"Invalid index");
987                   break;
988                 }
989                 else if (intresult == FILE_LIST_ERROR_OUT_OF_MEMORY)
990                 {
991                   free(commandbuffer);
992
993                   free_command_identifier_tree(commandidentifier);
994
995                   for (g = 0; g < totalgroups; ++g)
996                     free(groups[g].files);
997
998                   free(groups);
999
1000                   endwin();
1001                   errormsg("out of memory\n");
1002                   exit(1);
1003                 }
1004                 else
1005                 {
1006                   format_status_left(status, L"Could not interpret command");
1007                   break;
1008                 }
1009               }
1010
1011               token = wcstok(commandbuffer, L",", &wcsptr);
1012
1013               while (token != NULL)
1014               {
1015                 number = wcstol(token, &wcstolcheck, 10);
1016                 if (wcstolcheck != token && *wcstolcheck == '\0')
1017                 {
1018                   if (number > 0 && number <= groups[cursorgroup].filecount)
1019                     set_file_action(&groups[cursorgroup].files[number - 1], 1, &globaldeletiontally);
1020                 }
1021
1022                 token = wcstok(NULL, L",", &wcsptr);
1023               }
1024
1025               /* mark remaining files for deletion */
1026               preservecount = 0;
1027               deletecount = 0;
1028
1029               for (x = 0; x < groups[cursorgroup].filecount; ++x)
1030               {
1031                 if (groups[cursorgroup].files[x].action == 1)
1032                   ++preservecount;
1033                 if (groups[cursorgroup].files[x].action == -1)
1034                   ++deletecount;
1035               }
1036
1037               if (preservecount > 0)
1038               {
1039                 for (x = 0; x < groups[cursorgroup].filecount; ++x)
1040                 {
1041                   if (groups[cursorgroup].files[x].action == 0)
1042                   {
1043                     set_file_action(&groups[cursorgroup].files[x], -1, &globaldeletiontally);
1044                     ++deletecount;
1045                   }
1046                 }
1047               }
1048
1049               format_status_left(status, L"%d files marked for preservation, %d for deletion", preservecount, deletecount);
1050
1051               if (cursorgroup < totalgroups - 1 && preservecount > 0)
1052                 move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
1053
1054               break;
1055           }
1056
1057           break;
1058
1059         case GET_COMMAND_KEY_SF:
1060           ++topline;
1061
1062           resumecommandinput = 1;
1063
1064           continue;
1065
1066         case GET_COMMAND_KEY_SR:
1067           if (topline > 0)
1068             --topline;
1069
1070           resumecommandinput = 1;
1071
1072           continue;
1073
1074         case GET_COMMAND_KEY_NPAGE:
1075           topline += getmaxy(filewin);
1076
1077           resumecommandinput = 1;
1078
1079           continue;
1080
1081         case GET_COMMAND_KEY_PPAGE:
1082           topline -= getmaxy(filewin);
1083
1084           if (topline < 0)
1085             topline = 0;
1086
1087           resumecommandinput = 1;
1088
1089           continue;
1090
1091         case GET_COMMAND_CANCELED:
1092           break;
1093
1094         case GET_COMMAND_RESIZE_REQUESTED:
1095           /* resize windows */
1096           wresize(filewin, LINES - 2, COLS);
1097
1098           wresize(statuswin, 1, COLS);
1099           wresize(promptwin, 1, COLS);
1100           mvwin(statuswin, LINES - 1, 0);
1101           mvwin(promptwin, LINES - 2, 0);
1102
1103           status_text_alloc(status, COLS);
1104
1105           /* recalculate line boundaries */
1106           groupfirstline = 0;
1107
1108           for (g = 0; g < totalgroups; ++g)
1109           {
1110             groups[g].startline = groupfirstline;
1111             groups[g].endline = groupfirstline + 2;
1112
1113             for (f = 0; f < groups[g].filecount; ++f)
1114               groups[g].endline += filerowcount(groups[g].files[f].file, COLS, groups[g].filecount);
1115
1116             groupfirstline = groups[g].endline + 1;
1117           }
1118
1119           commandbuffer[0] = '\0';
1120
1121           break;
1122
1123         case GET_COMMAND_ERROR_OUT_OF_MEMORY:
1124           for (g = 0; g < totalgroups; ++g)
1125             free(groups[g].files);
1126
1127           free(groups);
1128           free(commandbuffer);
1129           free_command_identifier_tree(commandidentifier);
1130           free_command_identifier_tree(confirmationkeywordidentifier);
1131
1132           endwin();
1133           errormsg("out of memory\n");
1134           exit(1);
1135
1136           break;
1137       }
1138
1139       commandbuffer[0] = '\0';
1140     }
1141     else if (keyresult == KEY_CODE_YES)
1142     {
1143       switch (wch)
1144       {
1145       case KEY_DOWN:
1146         if (cursorfile < groups[cursorgroup].filecount - 1)
1147           move_to_next_file(&topline, &cursorgroup, &cursorfile, groups, filewin);
1148         else if (cursorgroup < totalgroups - 1)
1149           move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
1150
1151         break;
1152
1153       case KEY_UP:
1154         if (cursorfile > 0)
1155           move_to_previous_file(&topline, &cursorgroup, &cursorfile, groups, filewin);
1156         else if (cursorgroup > 0)
1157           move_to_previous_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
1158
1159         break;
1160
1161       case KEY_SF:
1162         ++topline;
1163         break;
1164
1165       case KEY_SR:
1166         if (topline > 0)
1167           --topline;
1168         break;
1169
1170       case KEY_NPAGE:
1171         topline += getmaxy(filewin);
1172         break;
1173
1174       case KEY_PPAGE:
1175         topline -= getmaxy(filewin);
1176
1177         if (topline < 0)
1178           topline = 0;
1179
1180         break;
1181
1182       case KEY_SRIGHT:
1183         set_file_action(&groups[cursorgroup].files[cursorfile], 1, &globaldeletiontally);
1184
1185         format_status_left(status, L"1 file marked for preservation.");
1186
1187         if (cursorfile < groups[cursorgroup].filecount - 1)
1188           move_to_next_file(&topline, &cursorgroup, &cursorfile, groups, filewin);
1189         else if (cursorgroup < totalgroups - 1)
1190           move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
1191
1192         break;
1193
1194       case KEY_SLEFT:
1195         deletecount = 0;
1196
1197         set_file_action(&groups[cursorgroup].files[cursorfile], -1, &globaldeletiontally);
1198
1199         format_status_left(status, L"1 file marked for deletion.");
1200
1201         for (x = 0; x < groups[cursorgroup].filecount; ++x)
1202           if (groups[cursorgroup].files[x].action == -1)
1203             ++deletecount;
1204
1205         if (deletecount < groups[cursorgroup].filecount)
1206         {
1207           if (cursorfile < groups[cursorgroup].filecount - 1)
1208             move_to_next_file(&topline, &cursorgroup, &cursorfile, groups, filewin);
1209           else if (cursorgroup < totalgroups - 1)
1210             move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
1211         }
1212
1213         break;
1214
1215       case KEY_BACKSPACE:
1216         if (cursorgroup > 0)
1217           --cursorgroup;
1218
1219         cursorfile = 0;
1220
1221         scroll_to_group(&topline, cursorgroup, 0, groups, filewin);
1222
1223         break;
1224
1225       case KEY_F(3):
1226         move_to_next_selected_group(&topline, &cursorgroup, &cursorfile, groups, totalgroups, filewin);
1227         break;
1228
1229       case KEY_F(2):
1230         move_to_previous_selected_group(&topline, &cursorgroup, &cursorfile, groups, totalgroups, filewin);
1231         break;
1232
1233       case KEY_DC:
1234         cmd_prune(groups, totalgroups, commandarguments, &globaldeletiontally, &totalgroups, &cursorgroup, &cursorfile, &topline, logfile, filewin, status);
1235         break;
1236
1237       case KEY_RESIZE:
1238         /* resize windows */
1239         wresize(filewin, LINES - 2, COLS);
1240
1241         wresize(statuswin, 1, COLS);
1242         wresize(promptwin, 1, COLS);
1243         mvwin(statuswin, LINES - 1, 0);
1244         mvwin(promptwin, LINES - 2, 0);
1245
1246         status_text_alloc(status, COLS);
1247
1248         /* recalculate line boundaries */
1249         groupfirstline = 0;
1250
1251         for (g = 0; g < totalgroups; ++g)
1252         {
1253           groups[g].startline = groupfirstline;
1254           groups[g].endline = groupfirstline + 2;
1255
1256           for (f = 0; f < groups[g].filecount; ++f)
1257             groups[g].endline += filerowcount(groups[g].files[f].file, COLS, groups[g].filecount);
1258
1259           groupfirstline = groups[g].endline + 1;
1260         }
1261
1262         break;
1263       }
1264     }
1265     else if (keyresult == OK)
1266     {
1267       switch (wch)
1268       {
1269       case '?':
1270         if (groups[cursorgroup].files[cursorfile].action == 0)
1271           break;
1272
1273         set_file_action(&groups[cursorgroup].files[cursorfile], 0, &globaldeletiontally);
1274
1275         if (cursorfile < groups[cursorgroup].filecount - 1)
1276           move_to_next_file(&topline, &cursorgroup, &cursorfile, groups, filewin);
1277         else if (cursorgroup < totalgroups - 1)
1278           move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
1279
1280         break;
1281
1282       case '\n':
1283         deletecount = 0;
1284         preservecount = 0;
1285
1286         for (x = 0; x < groups[cursorgroup].filecount; ++x)
1287         {
1288           if (groups[cursorgroup].files[x].action == 1)
1289             ++preservecount;
1290         }
1291
1292         if (preservecount == 0)
1293           break;
1294
1295         for (x = 0; x < groups[cursorgroup].filecount; ++x)
1296         {
1297           if (groups[cursorgroup].files[x].action == 0)
1298             set_file_action(&groups[cursorgroup].files[x], -1, &globaldeletiontally);
1299
1300           if (groups[cursorgroup].files[x].action == -1)
1301             ++deletecount;
1302         }
1303
1304         if (cursorgroup < totalgroups - 1 && deletecount < groups[cursorgroup].filecount)
1305           move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
1306
1307         break;
1308
1309       case '\t':
1310         if (cursorgroup < totalgroups - 1)
1311           move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
1312
1313         break;
1314       }
1315     }
1316   } while (doprune);
1317
1318   endwin();
1319
1320   free(commandbuffer);
1321
1322   free_prompt_info(prompt);
1323
1324   free_status_text(status);
1325
1326   free_command_identifier_tree(commandidentifier);
1327   free_command_identifier_tree(confirmationkeywordidentifier);
1328
1329   for (g = 0; g < totalgroups; ++g)
1330     free(groups[g].files);
1331
1332   free(groups);
1333 }