"Fossies" - the Fresh Open Source Software Archive

Member "fdupes-2.1.2/ncurses-interface.c" (12 Aug 2020, 37471 Bytes) of package /linux/privat/fdupes-2.1.2.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "ncurses-interface.c" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 2.1.0_vs_2.1.1.

    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 }