Imported Upstream version 2.0.0
[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 *fmtmtime(char *filename);
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   int totaldeleted;
380   size_t globaldeletiontally = 0;
381   double deletedbytes;
382   int row;
383   int x;
384   int g;
385   wint_t wch;
386   int keyresult;
387   int cy;
388   int f;
389   int to;
390   wchar_t *commandbuffer;
391   size_t commandbuffersize;
392   wchar_t *commandarguments;
393   struct command_identifier_node *commandidentifier;
394   struct command_identifier_node *confirmationkeywordidentifier;
395   int doprune;
396   wchar_t *token;
397   wchar_t *wcsptr;
398   wchar_t *wcstolcheck;
399   long int number;
400   struct status_text *status;
401   struct prompt_info *prompt;
402   int dupesfound;
403   int intresult;
404   int adjusttopline;
405   int toplineoffset = 0;
406   int resumecommandinput = 0;
407   int index_width;
408   int timestamp_width;
409   struct log_info *loginfo;
410
411   noecho();
412   cbreak();
413   halfdelay(5);
414
415   filewin = newwin(LINES - 2, COLS, 0, 0);
416   statuswin = newwin(1, COLS, LINES - 1, 0);
417   promptwin = newwin(1, COLS, LINES - 2, 0);
418
419   scrollok(filewin, FALSE);
420   scrollok(statuswin, FALSE);
421   scrollok(promptwin, FALSE);
422
423   wattron(statuswin, A_REVERSE);
424
425   keypad(promptwin, 1);
426
427   commandbuffersize = 80;
428   commandbuffer = malloc(commandbuffersize * sizeof(wchar_t));
429   if (commandbuffer == 0)
430   {
431     endwin();
432     errormsg("out of memory\n");
433     exit(1);
434   }
435
436   allocatedgroups = 1024;
437   groups = malloc(sizeof(struct filegroup) * allocatedgroups);
438   if (groups == 0)
439   {
440     free(commandbuffer);
441
442     endwin();
443     errormsg("out of memory\n");
444     exit(1);
445   }
446
447   commandidentifier = build_command_identifier_tree(command_list);
448   if (commandidentifier == 0)
449   {
450     free(groups);
451     free(commandbuffer);
452
453     endwin();
454     errormsg("out of memory\n");
455     exit(1);
456   }
457
458   confirmationkeywordidentifier = build_command_identifier_tree(confirmation_keyword_list);
459   if (confirmationkeywordidentifier == 0)
460   {
461     free(groups);
462     free(commandbuffer);
463     free_command_identifier_tree(commandidentifier);
464
465     endwin();
466     errormsg("out of memory\n");
467     exit(1);
468   }
469
470   register_sigint_handler();
471
472   curfile = files;
473   while (curfile)
474   {
475     if (!curfile->hasdupes)
476     {
477       curfile = curfile->next;
478       continue;
479     }
480
481     if (totalgroups + 1 > allocatedgroups)
482     {
483       allocatedgroups *= 2;
484
485       reallocgroups = realloc(groups, sizeof(struct filegroup) * allocatedgroups);
486       if (reallocgroups == 0)
487       {
488         for (g = 0; g < totalgroups; ++g)
489           free(groups[g].files);
490
491         free(groups);
492         free(commandbuffer);
493         free_command_identifier_tree(commandidentifier);
494         free_command_identifier_tree(confirmationkeywordidentifier);
495
496         endwin();
497         errormsg("out of memory\n");
498         exit(1);
499       }
500
501       groups = reallocgroups;
502     }
503
504     groups[totalgroups].startline = groupfirstline;
505     groups[totalgroups].endline = groupfirstline + 2;
506     groups[totalgroups].selected = 0;
507
508     groupfilecount = 0;
509
510     dupefile = curfile;
511     do
512     {
513       ++groupfilecount;
514
515       dupefile = dupefile->duplicates;
516     } while(dupefile);
517
518     dupefile = curfile;
519     do
520     {
521       groups[totalgroups].endline += filerowcount(dupefile, COLS, groupfilecount);
522
523       dupefile = dupefile->duplicates;
524     } while (dupefile);
525
526     groups[totalgroups].files = malloc(sizeof(struct groupfile) * groupfilecount);
527     if (groups[totalgroups].files == 0)
528     {
529       for (g = 0; g < totalgroups; ++g)
530         free(groups[g].files);
531
532       free(groups);
533       free(commandbuffer);
534       free_command_identifier_tree(commandidentifier);
535       free_command_identifier_tree(confirmationkeywordidentifier);
536
537       endwin();
538       errormsg("out of memory\n");
539       exit(1);
540     }
541
542     groupfilecount = 0;
543
544     dupefile = curfile;
545     do
546     {
547       groups[totalgroups].files[groupfilecount].file = dupefile;
548       groups[totalgroups].files[groupfilecount].action = 0;
549       groups[totalgroups].files[groupfilecount].selected = 0;
550       ++groupfilecount;
551
552       dupefile = dupefile->duplicates;
553     } while (dupefile);
554
555     groups[totalgroups].filecount = groupfilecount;
556
557     groupfirstline = groups[totalgroups].endline + 1;
558
559     ++totalgroups;
560
561     curfile = curfile->next;
562   }
563
564   dupesfound = totalgroups > 0;
565
566   status = status_text_alloc(0, COLS);
567   if (status == 0)
568   {
569     for (g = 0; g < totalgroups; ++g)
570       free(groups[g].files);
571
572     free(groups);
573     free(commandbuffer);
574     free_command_identifier_tree(commandidentifier);
575     free_command_identifier_tree(confirmationkeywordidentifier);
576
577     endwin();
578     errormsg("out of memory\n");
579     exit(1);
580   }
581
582   format_status_left(status, L"Ready");
583
584   prompt = prompt_info_alloc(80);
585   if (prompt == 0)
586   {
587     free_status_text(status);
588
589     for (g = 0; g < totalgroups; ++g)
590       free(groups[g].files);
591
592     free(groups);
593     free(commandbuffer);
594     free_command_identifier_tree(commandidentifier);
595     free_command_identifier_tree(confirmationkeywordidentifier);
596
597     endwin();
598     errormsg("out of memory\n");
599     exit(1);
600   }
601
602   doprune = 1;
603   do
604   {
605     wmove(filewin, 0, 0);
606     werase(filewin);
607
608     if (totalgroups > 0)
609       totallines = groups[totalgroups-1].endline;
610     else
611       totallines = 0;
612
613     for (x = topline; x < topline + getmaxy(filewin); ++x)
614     {
615       if (x >= totallines)
616       {
617         wclrtoeol(filewin);
618         continue;
619       }
620
621       groupindex = getgroupindex(groups, totalgroups, groupindex, x);
622
623       index_width = get_num_digits(groups[groupindex].filecount);
624
625       if (index_width < FILE_INDEX_MIN_WIDTH)
626         index_width = FILE_INDEX_MIN_WIDTH;
627
628       timestamp_width = ISFLAG(flags, F_SHOWTIME) ? 19 : 0;
629
630       linestyle = getlinestyle(groups + groupindex, x);
631       
632       if (linestyle == linestyle_groupheader)
633       {
634         wattron(filewin, A_BOLD);
635         if (groups[groupindex].selected)
636           wattron(filewin, A_REVERSE);
637         wprintw(filewin, "Set %d of %d:\n", groupindex + 1, totalgroups);
638         if (groups[groupindex].selected)
639           wattroff(filewin, A_REVERSE);
640         wattroff(filewin, A_BOLD);
641       }
642       else if (linestyle == linestyle_groupheaderspacing)
643       {
644         wprintw(filewin, "\n");
645       }
646       else if (linestyle == linestyle_filename)
647       {
648         f = getgroupfileindex(&row, groups + groupindex, x, COLS);
649
650         if (cursorgroup != groupindex)
651         {
652           if (row == 0)
653           {
654             print_spaces(filewin, index_width);
655
656             wprintw(filewin, " [%c] ", groups[groupindex].files[f].action > 0 ? '+' : groups[groupindex].files[f].action < 0 ? '-' : ' ');
657
658             if (ISFLAG(flags, F_SHOWTIME))
659               wprintw(filewin, "[%s] ", fmtmtime(groups[groupindex].files[f].file->d_name));
660           }
661
662           cy = getcury(filewin);
663
664           if (groups[groupindex].files[f].selected)
665             wattron(filewin, A_REVERSE);
666           putline(filewin, groups[groupindex].files[f].file->d_name, row, COLS, index_width + timestamp_width + FILENAME_INDENT_EXTRA);
667           if (groups[groupindex].files[f].selected)
668             wattroff(filewin, A_REVERSE);
669
670           wclrtoeol(filewin);
671           wmove(filewin, cy+1, 0);
672         }
673         else
674         {
675           if (row == 0)
676           {
677             print_right_justified_int(filewin, f+1, index_width);
678             wprintw(filewin, " ");
679
680             if (cursorgroup == groupindex && cursorfile == f)
681               wattron(filewin, A_REVERSE);
682             wprintw(filewin, "[%c]", groups[groupindex].files[f].action > 0 ? '+' : groups[groupindex].files[f].action < 0 ? '-' : ' ');
683             if (cursorgroup == groupindex && cursorfile == f)
684               wattroff(filewin, A_REVERSE);
685             wprintw(filewin, " ");
686
687             if (ISFLAG(flags, F_SHOWTIME))
688               wprintw(filewin, "[%s] ", fmtmtime(groups[groupindex].files[f].file->d_name));
689           }
690
691           cy = getcury(filewin);
692
693           if (groups[groupindex].files[f].selected)
694             wattron(filewin, A_REVERSE);
695           putline(filewin, groups[groupindex].files[f].file->d_name, row, COLS, index_width + timestamp_width + FILENAME_INDENT_EXTRA);
696           if (groups[groupindex].files[f].selected)
697             wattroff(filewin, A_REVERSE);
698
699           wclrtoeol(filewin);
700           wmove(filewin, cy+1, 0);
701         }
702       }
703       else if (linestyle == linestyle_groupfooterspacing)
704       {
705         wprintw(filewin, "\n");
706       }
707     }
708
709     if (totalgroups > 0)
710       format_status_right(status, L"Set %d of %d", cursorgroup+1, totalgroups);
711     else
712       format_status_right(status, L"Finished");
713
714     print_status(statuswin, status);
715
716     if (totalgroups > 0)
717       format_prompt(prompt, L"( Preserve files [1 - %d, all, help] )", groups[cursorgroup].filecount);
718     else if (dupesfound)
719       format_prompt(prompt, L"( No duplicates remaining; type 'exit' to exit program )");
720     else
721       format_prompt(prompt, L"( No duplicates found; type 'exit' to exit program )");
722
723     print_prompt(promptwin, prompt, L"");
724
725     /* refresh windows (using wrefresh instead of wnoutrefresh to avoid bug in gnome-terminal) */
726     wrefresh(filewin);
727     wrefresh(statuswin);
728     wrefresh(promptwin);
729
730     /* wait for user input */
731     if (!resumecommandinput)
732     {
733       do
734       {
735         keyresult = wget_wch(promptwin, &wch);
736
737         if (got_sigint)
738         {
739           getyx(promptwin, cursor_y, cursor_x);
740
741           format_status_left(status, L"Type 'exit' to exit fdupes.");
742           print_status(statuswin, status);
743
744           wmove(promptwin, cursor_y, cursor_x);
745
746           got_sigint = 0;
747
748           wrefresh(statuswin);
749         }
750       } while (keyresult == ERR);
751
752       if (keyresult == OK && iswprint(wch))
753       {
754         commandbuffer[0] = wch;
755         commandbuffer[1] = '\0';
756       }
757       else
758       {
759         commandbuffer[0] = '\0';
760       }
761     }
762
763     if (resumecommandinput || (keyresult == OK && iswprint(wch) && ((wch != '\t' && wch != '\n' && wch != '?'))))
764     {
765       resumecommandinput = 0;
766
767       switch (get_command_text(&commandbuffer, &commandbuffersize, promptwin, prompt, 1, 1))
768       {
769         case GET_COMMAND_OK:
770           get_command_arguments(&commandarguments, commandbuffer);
771
772           switch (identify_command(commandidentifier, commandbuffer, 0))
773           {
774             case COMMAND_SELECT_CONTAINING:
775               cmd_select_containing(groups, totalgroups, commandarguments, status);
776               break;
777
778             case COMMAND_SELECT_BEGINNING:
779               cmd_select_beginning(groups, totalgroups, commandarguments, status);
780               break;
781
782             case COMMAND_SELECT_ENDING:
783               cmd_select_ending(groups, totalgroups, commandarguments, status);
784               break;
785
786             case COMMAND_SELECT_MATCHING:
787               cmd_select_matching(groups, totalgroups, commandarguments, status);
788               break;
789
790             case COMMAND_SELECT_REGEX:
791               cmd_select_regex(groups, totalgroups, commandarguments, status);
792               break;
793
794             case COMMAND_CLEAR_SELECTIONS_CONTAINING:
795               cmd_clear_selections_containing(groups, totalgroups, commandarguments, status);
796               break;
797
798             case COMMAND_CLEAR_SELECTIONS_BEGINNING:
799               cmd_clear_selections_beginning(groups, totalgroups, commandarguments, status);
800               break;
801
802             case COMMAND_CLEAR_SELECTIONS_ENDING:
803               cmd_clear_selections_ending(groups, totalgroups, commandarguments, status);
804               break;
805
806             case COMMAND_CLEAR_SELECTIONS_MATCHING:
807               cmd_clear_selections_matching(groups, totalgroups, commandarguments, status);
808               break;
809
810             case COMMAND_CLEAR_SELECTIONS_REGEX:
811               cmd_clear_selections_regex(groups, totalgroups, commandarguments, status);
812               break;
813
814             case COMMAND_CLEAR_ALL_SELECTIONS:
815               cmd_clear_all_selections(groups, totalgroups, commandarguments, status);
816               break;
817
818             case COMMAND_INVERT_GROUP_SELECTIONS:
819               cmd_invert_group_selections(groups, totalgroups, commandarguments, status);
820               break;
821
822             case COMMAND_KEEP_SELECTED:
823               cmd_keep_selected(groups, totalgroups, commandarguments, &globaldeletiontally, status);
824               break;
825
826             case COMMAND_DELETE_SELECTED:
827               cmd_delete_selected(groups, totalgroups, commandarguments, &globaldeletiontally, status);
828               break;
829
830             case COMMAND_RESET_SELECTED:
831               cmd_reset_selected(groups, totalgroups, commandarguments, &globaldeletiontally, status);
832               break;
833
834             case COMMAND_RESET_GROUP:
835               for (x = 0; x < groups[cursorgroup].filecount; ++x)
836                 set_file_action(&groups[cursorgroup].files[x], 0, &globaldeletiontally);
837
838               format_status_left(status, L"Reset all files in current group.");
839
840               break;
841
842             case COMMAND_PRESERVE_ALL:
843               /* mark all files for preservation */
844               for (x = 0; x < groups[cursorgroup].filecount; ++x)
845                 set_file_action(&groups[cursorgroup].files[x], 1, &globaldeletiontally);
846
847               format_status_left(status, L"%d files marked for preservation", groups[cursorgroup].filecount);
848
849               if (cursorgroup < totalgroups - 1)
850                 move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
851
852               break;
853
854             case COMMAND_GOTO_SET:
855               number = wcstol(commandarguments, &wcstolcheck, 10);
856               if (wcstolcheck != commandarguments && *wcstolcheck == '\0')
857               {
858                 if (number >= 1 && number <= totalgroups)
859                 {
860                   scroll_to_group(&topline, number - 1, 0, groups, filewin);
861
862                   cursorgroup = number - 1;
863                   cursorfile = 0;
864                 }
865                 else
866                 {
867                   format_status_left(status, L"Group index out of range.");
868                 }
869               }
870               else
871               {
872                 format_status_left(status, L"Invalid group index.");
873               }
874
875               break;
876
877             case COMMAND_HELP:
878               endwin();
879
880               if (system(HELP_COMMAND_STRING) == -1)
881                 format_status_left(status, L"Could not display help text.");
882
883               refresh();
884
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         totaldeleted = 0;
1235         deletedbytes = 0;
1236
1237         if (logfile != 0)
1238           loginfo = log_open(logfile, 0);
1239         else
1240           loginfo = 0;
1241
1242         for (g = 0; g < totalgroups; ++g)
1243         {
1244           preservecount = 0;
1245           deletecount = 0;
1246           unresolvedcount = 0;
1247
1248           for (f = 0; f < groups[g].filecount; ++f)
1249           {
1250             switch (groups[g].files[f].action)
1251             {
1252               case -1:
1253                 ++deletecount;
1254                 break;
1255               case 0:
1256                 ++unresolvedcount;
1257                 break;
1258               case 1:
1259                 ++preservecount;
1260                 break;
1261             }
1262           }
1263
1264           if (loginfo)
1265             log_begin_set(loginfo);
1266
1267           /* delete files marked for deletion unless no files left undeleted */
1268           if (deletecount < groups[g].filecount)
1269           {
1270             for (f = 0; f < groups[g].filecount; ++f)
1271             {
1272               if (groups[g].files[f].action == -1)
1273               {
1274                 if (remove(groups[g].files[f].file->d_name) == 0)
1275                 {
1276                   set_file_action(&groups[g].files[f], -2, &globaldeletiontally);
1277
1278                   deletedbytes += groups[g].files[f].file->size;
1279                   ++totaldeleted;
1280
1281                   if (loginfo)
1282                     log_file_deleted(loginfo, groups[g].files[f].file->d_name);
1283                 }
1284               }
1285             }
1286
1287             if (loginfo)
1288             {
1289               for (f = 0; f < groups[g].filecount; ++f)
1290               {
1291                 if (groups[g].files[f].action >= 0)
1292                   log_file_remaining(loginfo, groups[g].files[f].file->d_name);
1293               }
1294             }
1295
1296             deletecount = 0;
1297           }
1298
1299           if (loginfo)
1300             log_end_set(loginfo);
1301
1302           /* if no files left unresolved, mark preserved files for delisting */
1303           if (unresolvedcount == 0)
1304           {
1305             for (f = 0; f < groups[g].filecount; ++f)
1306               if (groups[g].files[f].action == 1)
1307                 set_file_action(&groups[g].files[f], -2, &globaldeletiontally);
1308
1309             preservecount = 0;
1310           }
1311           /* if only one file left unresolved, mark it for delesting */
1312           else if (unresolvedcount == 1 && preservecount + deletecount == 0)
1313           {
1314             for (f = 0; f < groups[g].filecount; ++f)
1315               if (groups[g].files[f].action == 0)
1316                 set_file_action(&groups[g].files[f], -2, &globaldeletiontally);
1317           }
1318
1319           /* delist any files marked for delisting */
1320           to = 0;
1321           for (f = 0; f < groups[g].filecount; ++f)
1322             if (groups[g].files[f].action != -2)
1323               groups[g].files[to++] = groups[g].files[f];
1324
1325           groups[g].filecount = to;
1326
1327           /* reposition cursor, if necessary */
1328           if (cursorgroup == g && cursorfile > 0 && cursorfile >= groups[g].filecount)
1329             cursorfile = groups[g].filecount - 1;
1330         }
1331
1332         if (loginfo != 0)
1333           log_close(loginfo);
1334
1335         if (deletedbytes < 1000.0)
1336           format_status_left(status, L"Deleted %ld files (occupying %.0f bytes).", totaldeleted, deletedbytes);
1337         else if (deletedbytes <= (1000.0 * 1000.0))
1338           format_status_left(status, L"Deleted %ld files (occupying %.1f KB).", totaldeleted, deletedbytes / 1000.0);
1339         else if (deletedbytes <= (1000.0 * 1000.0 * 1000.0))
1340           format_status_left(status, L"Deleted %ld files (occupying %.1f MB).", totaldeleted, deletedbytes / (1000.0 * 1000.0));
1341         else
1342           format_status_left(status, L"Deleted %ld files (occupying %.1f GB).", totaldeleted, deletedbytes / (1000.0 * 1000.0 * 1000.0));
1343
1344         /* delist empty groups */
1345         to = 0;
1346         for (g = 0; g < totalgroups; ++g)
1347         {
1348           if (groups[g].filecount > 0)
1349           {
1350             groups[to] = groups[g];
1351
1352             /* reposition cursor, if necessary */
1353             if (to == cursorgroup && to != g)
1354               cursorfile = 0;
1355
1356             ++to;
1357           }
1358           else
1359           {
1360             free(groups[g].files);
1361           }
1362         }
1363
1364         totalgroups = to;
1365
1366         /* reposition cursor, if necessary */
1367         if (cursorgroup >= totalgroups)
1368         {
1369           cursorgroup = totalgroups - 1;
1370           cursorfile = 0;
1371         }
1372
1373         /* recalculate line boundaries */
1374         adjusttopline = 1;
1375         toplineoffset = 0;
1376         groupfirstline = 0;
1377
1378         for (g = 0; g < totalgroups; ++g)
1379         {
1380           if (adjusttopline && groups[g].endline >= topline)
1381             toplineoffset = groups[g].endline - topline;
1382
1383           groups[g].startline = groupfirstline;
1384           groups[g].endline = groupfirstline + 2;
1385
1386           for (f = 0; f < groups[g].filecount; ++f)
1387             groups[g].endline += filerowcount(groups[g].files[f].file, COLS, groups[g].filecount);
1388
1389           if (adjusttopline && toplineoffset > 0)
1390           {
1391             topline = groups[g].endline - toplineoffset;
1392
1393             if (topline < 0)
1394               topline = 0;
1395
1396             adjusttopline = 0;
1397           }
1398
1399           groupfirstline = groups[g].endline + 1;
1400         }
1401
1402         if (totalgroups > 0 && groups[totalgroups-1].endline <= topline)
1403         {
1404           topline = groups[totalgroups-1].endline - getmaxy(filewin) + 1;
1405
1406           if (topline < 0)
1407             topline = 0;
1408         }
1409
1410         break;
1411
1412       case KEY_RESIZE:
1413         /* resize windows */
1414         wresize(filewin, LINES - 2, COLS);
1415
1416         wresize(statuswin, 1, COLS);
1417         wresize(promptwin, 1, COLS);
1418         mvwin(statuswin, LINES - 1, 0);
1419         mvwin(promptwin, LINES - 2, 0);
1420
1421         status_text_alloc(status, COLS);
1422
1423         /* recalculate line boundaries */
1424         groupfirstline = 0;
1425
1426         for (g = 0; g < totalgroups; ++g)
1427         {
1428           groups[g].startline = groupfirstline;
1429           groups[g].endline = groupfirstline + 2;
1430
1431           for (f = 0; f < groups[g].filecount; ++f)
1432             groups[g].endline += filerowcount(groups[g].files[f].file, COLS, groups[g].filecount);
1433
1434           groupfirstline = groups[g].endline + 1;
1435         }
1436
1437         break;
1438       }
1439     }
1440     else if (keyresult == OK)
1441     {
1442       switch (wch)
1443       {
1444       case '?':
1445         if (groups[cursorgroup].files[cursorfile].action == 0)
1446           break;
1447
1448         set_file_action(&groups[cursorgroup].files[cursorfile], 0, &globaldeletiontally);
1449
1450         if (cursorfile < groups[cursorgroup].filecount - 1)
1451           move_to_next_file(&topline, &cursorgroup, &cursorfile, groups, filewin);
1452         else if (cursorgroup < totalgroups - 1)
1453           move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
1454
1455         break;
1456
1457       case '\n':
1458         deletecount = 0;
1459         preservecount = 0;
1460
1461         for (x = 0; x < groups[cursorgroup].filecount; ++x)
1462         {
1463           if (groups[cursorgroup].files[x].action == 1)
1464             ++preservecount;
1465         }
1466
1467         if (preservecount == 0)
1468           break;
1469
1470         for (x = 0; x < groups[cursorgroup].filecount; ++x)
1471         {
1472           if (groups[cursorgroup].files[x].action == 0)
1473             set_file_action(&groups[cursorgroup].files[x], -1, &globaldeletiontally);
1474
1475           if (groups[cursorgroup].files[x].action == -1)
1476             ++deletecount;
1477         }
1478
1479         if (cursorgroup < totalgroups - 1 && deletecount < groups[cursorgroup].filecount)
1480           move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
1481
1482         break;
1483
1484       case '\t':
1485         if (cursorgroup < totalgroups - 1)
1486           move_to_next_group(&topline, &cursorgroup, &cursorfile, groups, filewin);
1487
1488         break;
1489       }
1490     }
1491   } while (doprune);
1492
1493   endwin();
1494
1495   free(commandbuffer);
1496
1497   free_prompt_info(prompt);
1498
1499   free_status_text(status);
1500
1501   free_command_identifier_tree(commandidentifier);
1502   free_command_identifier_tree(confirmationkeywordidentifier);
1503
1504   for (g = 0; g < totalgroups; ++g)
1505     free(groups[g].files);
1506
1507   free(groups);
1508 }