Imported Upstream version 2.1.2
[platform/upstream/fdupes.git] / ncurses-commands.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 "ncurses-status.h"
24 #include "ncurses-commands.h"
25 #include "wcs.h"
26 #include "mbstowcs_escape_invalid.h"
27 #include "log.h"
28 #include <wchar.h>
29 #include <pcre2.h>
30
31 void set_file_action(struct groupfile *file, int new_action, size_t *deletion_tally);
32
33 struct command_map command_list[] = {
34   {L"sel", COMMAND_SELECT_CONTAINING},
35   {L"selb", COMMAND_SELECT_BEGINNING},
36   {L"sele", COMMAND_SELECT_ENDING},
37   {L"selm", COMMAND_SELECT_MATCHING},
38   {L"selr", COMMAND_SELECT_REGEX},
39   {L"dsel", COMMAND_CLEAR_SELECTIONS_CONTAINING},
40   {L"dselb", COMMAND_CLEAR_SELECTIONS_BEGINNING},
41   {L"dsele", COMMAND_CLEAR_SELECTIONS_ENDING},
42   {L"dselm", COMMAND_CLEAR_SELECTIONS_MATCHING},
43   {L"dselr", COMMAND_CLEAR_SELECTIONS_REGEX},
44   {L"csel", COMMAND_CLEAR_ALL_SELECTIONS},
45   {L"isel", COMMAND_INVERT_GROUP_SELECTIONS},
46   {L"ks", COMMAND_KEEP_SELECTED},
47   {L"ds", COMMAND_DELETE_SELECTED},
48   {L"rs", COMMAND_RESET_SELECTED},
49   {L"rg", COMMAND_RESET_GROUP},
50   {L"all", COMMAND_PRESERVE_ALL},
51   {L"goto", COMMAND_GOTO_SET},
52   {L"prune", COMMAND_PRUNE},
53   {L"exit", COMMAND_EXIT},
54   {L"quit", COMMAND_EXIT},
55   {L"help", COMMAND_HELP},
56   {0, COMMAND_UNDEFINED}
57 };
58
59 struct command_map confirmation_keyword_list[] = {
60   {L"yes", COMMAND_YES},
61   {L"no", COMMAND_NO},
62   {0, COMMAND_UNDEFINED}
63 };
64
65 /* select files containing string */
66 int cmd_select_containing(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
67 {
68   int g;
69   int f;
70   int selectedgroupcount = 0;
71   int selectedfilecount = 0;
72   int groupselected;
73
74   if (wcscmp(commandarguments, L"") != 0)
75   {
76     for (g = 0; g < groupcount; ++g)
77     {
78       groupselected = 0;
79
80       for (f = 0; f < groups[g].filecount; ++f)
81       {
82         if (wcsinmbcs(groups[g].files[f].file->d_name, commandarguments))
83         {
84           groups[g].selected = 1;
85           groups[g].files[f].selected = 1;
86
87           groupselected = 1;
88           ++selectedfilecount;
89         }
90       }
91
92       if (groupselected)
93         ++selectedgroupcount;
94     }
95   }
96
97   format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
98
99   return 1;
100 }
101
102 /* select files beginning with string */
103 int cmd_select_beginning(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
104 {
105   int g;
106   int f;
107   int selectedgroupcount = 0;
108   int selectedfilecount = 0;
109   int groupselected;
110
111   if (wcscmp(commandarguments, L"") != 0)
112   {
113     for (g = 0; g < groupcount; ++g)
114     {
115       groupselected = 0;
116
117       for (f = 0; f < groups[g].filecount; ++f)
118       {
119         if (wcsbeginmbcs(groups[g].files[f].file->d_name, commandarguments))
120         {
121           groups[g].selected = 1;
122           groups[g].files[f].selected = 1;
123
124           groupselected = 1;
125           ++selectedfilecount;
126         }
127       }
128
129       if (groupselected)
130         ++selectedgroupcount;
131     }
132   }
133
134   format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
135
136   return 1;
137 }
138
139 /* select files ending with string */
140 int cmd_select_ending(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
141 {
142   int g;
143   int f;
144   int selectedgroupcount = 0;
145   int selectedfilecount = 0;
146   int groupselected;
147
148   if (wcscmp(commandarguments, L"") != 0)
149   {
150     for (g = 0; g < groupcount; ++g)
151     {
152       groupselected = 0;
153
154       for (f = 0; f < groups[g].filecount; ++f)
155       {
156         if (wcsendsmbcs(groups[g].files[f].file->d_name, commandarguments))
157         {
158           groups[g].selected = 1;
159           groups[g].files[f].selected = 1;
160
161           groupselected = 1;
162           ++selectedfilecount;
163         }
164       }
165
166       if (groupselected)
167         ++selectedgroupcount;
168     }
169   }
170
171   format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
172
173   return 1;
174 }
175
176 /* select files matching string */
177 int cmd_select_matching(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
178 {
179   int g;
180   int f;
181   int selectedgroupcount = 0;
182   int selectedfilecount = 0;
183   int groupselected;
184
185   if (wcscmp(commandarguments, L"") != 0)
186   {
187     for (g = 0; g < groupcount; ++g)
188     {
189       groupselected = 0;
190
191       for (f = 0; f < groups[g].filecount; ++f)
192       {
193         if (wcsmbcscmp(commandarguments, groups[g].files[f].file->d_name) == 0)
194         {
195           groups[g].selected = 1;
196           groups[g].files[f].selected = 1;
197
198           groupselected = 1;
199           ++selectedfilecount;
200         }
201       }
202
203       if (groupselected)
204         ++selectedgroupcount;
205     }
206   }
207
208   format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
209
210   return 1;
211 }
212
213 /* select files matching pattern */
214 int cmd_select_regex(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
215 {
216   size_t size;
217   wchar_t *wcsfilename;
218   size_t needed;
219   int errorcode;
220   PCRE2_SIZE erroroffset;
221   pcre2_code *code;
222   pcre2_match_data *md;
223   int matches;
224   int g;
225   int f;
226   int selectedgroupcount = 0;
227   int selectedfilecount = 0;
228   int groupselected;
229
230   code = pcre2_compile((PCRE2_SPTR)commandarguments, PCRE2_ZERO_TERMINATED, PCRE2_UTF | PCRE2_UCP, &errorcode, &erroroffset, 0);
231
232   if (code == 0)
233     return -1;
234
235   pcre2_jit_compile(code, PCRE2_JIT_COMPLETE);
236
237   md = pcre2_match_data_create(1, 0);
238   if (md == 0)
239     return -1;
240
241   for (g = 0; g < groupcount; ++g)
242   {
243     groupselected = 0;
244
245     for (f = 0; f < groups[g].filecount; ++f)
246     {
247       needed = mbstowcs_escape_invalid(0, groups[g].files[f].file->d_name, 0);
248
249       wcsfilename = (wchar_t*) malloc(needed * sizeof(wchar_t));
250       if (wcsfilename == 0)
251         continue;
252
253       mbstowcs_escape_invalid(wcsfilename, groups[g].files[f].file->d_name, needed);
254
255       matches = pcre2_match(code, (PCRE2_SPTR)wcsfilename, PCRE2_ZERO_TERMINATED, 0, 0, md, 0);
256
257       free(wcsfilename);
258
259       if (matches > 0)
260       {
261         groups[g].selected = 1;
262         groups[g].files[f].selected = 1;
263
264         groupselected = 1;
265         ++selectedfilecount;
266       }
267     }
268
269     if (groupselected)
270       ++selectedgroupcount;
271   }
272
273   format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
274
275   pcre2_code_free(code);
276
277   return 1;
278 }
279
280 /* clear selections containing string */
281 int cmd_clear_selections_containing(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
282 {
283   int g;
284   int f;
285   int matchedgroupcount = 0;
286   int matchedfilecount = 0;
287   int groupmatched;
288   int filedeselected;
289   int selectionsremaining;
290
291   if (wcscmp(commandarguments, L"") != 0)
292   {
293     for (g = 0; g < groupcount; ++g)
294     {
295       groupmatched = 0;
296       filedeselected = 0;
297       selectionsremaining = 0;
298
299       for (f = 0; f < groups[g].filecount; ++f)
300       {
301         if (wcsinmbcs(groups[g].files[f].file->d_name, commandarguments))
302         {
303           if (groups[g].files[f].selected)
304           {
305             groups[g].files[f].selected = 0;
306             filedeselected = 1;
307           }
308
309           groupmatched = 1;
310           ++matchedfilecount;
311         }
312
313         if (groups[g].files[f].selected)
314           selectionsremaining = 1;
315       }
316
317       if (filedeselected && !selectionsremaining)
318         groups[g].selected = 0;
319
320       if (groupmatched)
321         ++matchedgroupcount;
322     }
323   }
324
325   format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
326
327   return 1;
328 }
329
330 /* clear selections beginning with string */
331 int cmd_clear_selections_beginning(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
332 {
333   int g;
334   int f;
335   int matchedgroupcount = 0;
336   int matchedfilecount = 0;
337   int groupmatched;
338   int filedeselected;
339   int selectionsremaining;
340
341   if (wcscmp(commandarguments, L"") != 0)
342   {
343     for (g = 0; g < groupcount; ++g)
344     {
345       groupmatched = 0;
346       filedeselected = 0;
347       selectionsremaining = 0;
348
349       for (f = 0; f < groups[g].filecount; ++f)
350       {
351         if (wcsbeginmbcs(groups[g].files[f].file->d_name, commandarguments))
352         {
353           if (groups[g].files[f].selected)
354           {
355             groups[g].files[f].selected = 0;
356             filedeselected = 1;
357           }
358
359           groupmatched = 1;
360           ++matchedfilecount;
361         }
362
363         if (groups[g].files[f].selected)
364           selectionsremaining = 1;
365       }
366
367       if (filedeselected && !selectionsremaining)
368         groups[g].selected = 0;
369
370       if (groupmatched)
371         ++matchedgroupcount;
372     }
373   }
374
375   format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
376
377   return 1;
378 }
379
380 /* clear selections ending with string */
381 int cmd_clear_selections_ending(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
382 {
383   int g;
384   int f;
385   int matchedgroupcount = 0;
386   int matchedfilecount = 0;
387   int groupmatched;
388   int filedeselected;
389   int selectionsremaining;
390
391   if (wcscmp(commandarguments, L"") != 0)
392   {
393     for (g = 0; g < groupcount; ++g)
394     {
395       groupmatched = 0;
396       filedeselected = 0;
397       selectionsremaining = 0;
398
399       for (f = 0; f < groups[g].filecount; ++f)
400       {
401         if (wcsendsmbcs(groups[g].files[f].file->d_name, commandarguments))
402         {
403           if (groups[g].files[f].selected)
404           {
405             groups[g].files[f].selected = 0;
406             filedeselected = 1;
407           }
408
409           groupmatched = 1;
410           ++matchedfilecount;
411         }
412
413         if (groups[g].files[f].selected)
414           selectionsremaining = 1;
415       }
416
417       if (filedeselected && !selectionsremaining)
418         groups[g].selected = 0;
419
420       if (groupmatched)
421         ++matchedgroupcount;
422     }
423   }
424
425   format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
426
427   return 1;
428 }
429
430 /* clear selections matching string */
431 int cmd_clear_selections_matching(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
432 {
433   int g;
434   int f;
435   int matchedgroupcount = 0;
436   int matchedfilecount = 0;
437   int groupmatched;
438   int filedeselected;
439   int selectionsremaining;
440
441   if (wcscmp(commandarguments, L"") != 0)
442   {
443     for (g = 0; g < groupcount; ++g)
444     {
445       groupmatched = 0;
446       filedeselected = 0;
447       selectionsremaining = 0;
448
449       for (f = 0; f < groups[g].filecount; ++f)
450       {
451         if (wcsmbcscmp(commandarguments, groups[g].files[f].file->d_name) == 0)
452         {
453           if (groups[g].files[f].selected)
454           {
455             groups[g].files[f].selected = 0;
456             filedeselected = 1;
457           }
458
459           groupmatched = 1;
460           ++matchedfilecount;
461         }
462
463         if (groups[g].files[f].selected)
464           selectionsremaining = 1;
465       }
466
467       if (filedeselected && !selectionsremaining)
468         groups[g].selected = 0;
469
470       if (groupmatched)
471         ++matchedgroupcount;
472     }
473   }
474
475   format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
476
477   return 1;
478 }
479
480 /* clear selection matching pattern */
481 int cmd_clear_selections_regex(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
482 {
483   size_t size;
484   wchar_t *wcsfilename;
485   size_t needed;
486   int errorcode;
487   PCRE2_SIZE erroroffset;
488   pcre2_code *code;
489   pcre2_match_data *md;
490   int matches;
491   int g;
492   int f;
493   int matchedgroupcount = 0;
494   int matchedfilecount = 0;
495   int groupmatched;
496   int filedeselected;
497   int selectionsremaining;
498
499   code = pcre2_compile((PCRE2_SPTR)commandarguments, PCRE2_ZERO_TERMINATED, PCRE2_UTF | PCRE2_UCP, &errorcode, &erroroffset, 0);
500
501   if (code == 0)
502     return -1;
503
504   pcre2_jit_compile(code, PCRE2_JIT_COMPLETE);
505
506   md = pcre2_match_data_create(1, 0);
507   if (md == 0)
508     return -1;
509
510   for (g = 0; g < groupcount; ++g)
511   {
512     groupmatched = 0;
513     filedeselected = 0;
514     selectionsremaining = 0;
515
516     for (f = 0; f < groups[g].filecount; ++f)
517     {
518       needed = mbstowcs_escape_invalid(0, groups[g].files[f].file->d_name, 0);
519
520       wcsfilename = (wchar_t*) malloc(needed * sizeof(wchar_t));
521       if (wcsfilename == 0)
522         continue;
523
524       mbstowcs_escape_invalid(wcsfilename, groups[g].files[f].file->d_name, needed);
525
526       matches = pcre2_match(code, (PCRE2_SPTR)wcsfilename, PCRE2_ZERO_TERMINATED, 0, 0, md, 0);
527
528       free(wcsfilename);
529
530       if (matches > 0)
531       {
532         if (groups[g].files[f].selected)
533         {
534           groups[g].files[f].selected = 0;
535           filedeselected = 1;
536         }
537
538         groupmatched = 1;
539         ++matchedfilecount;
540       }
541
542       if (groups[g].files[f].selected)
543         selectionsremaining = 1;
544     }
545
546     if (filedeselected && !selectionsremaining)
547       groups[g].selected = 0;
548
549     if (groupmatched)
550       ++matchedgroupcount;
551   }
552
553   format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
554
555   pcre2_code_free(code);
556
557   return 1;
558 }
559
560 /* clear all selections and selected groups */
561 int cmd_clear_all_selections(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
562 {
563   int g;
564   int f;
565
566   for (g = 0; g < groupcount; ++g)
567   {
568     for (f = 0; f < groups[g].filecount; ++f)
569       groups[g].files[f].selected = 0;
570
571     groups[g].selected = 0;
572   }
573
574   if (status)
575     format_status_left(status, L"Cleared all selections.");
576
577   return 1;
578 }
579
580 /* invert selections within selected groups */
581 int cmd_invert_group_selections(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
582 {
583   int g;
584   int f;
585   int selectedcount = 0;
586   int deselectedcount = 0;
587
588   for (g = 0; g < groupcount; ++g)
589   {
590     if (groups[g].selected)
591     {
592       for (f = 0; f < groups[g].filecount; ++f)
593       {
594         groups[g].files[f].selected = !groups[g].files[f].selected;
595
596         if (groups[g].files[f].selected)
597           ++selectedcount;
598         else
599           ++deselectedcount;
600       }
601     }
602   }
603
604   format_status_left(status, L"Selected %d files. Deselected %d files.", selectedcount, deselectedcount);
605
606   return 1;
607 }
608
609 /* mark selected files for preservation */
610 int cmd_keep_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status)
611 {
612   int g;
613   int f;
614   int keepfilecount = 0;
615
616   for (g = 0; g < groupcount; ++g)
617   {
618     for (f = 0; f < groups[g].filecount; ++f)
619     {
620       if (groups[g].files[f].selected)
621       {
622         set_file_action(&groups[g].files[f], 1, deletiontally);
623         ++keepfilecount;
624       }
625     }
626   }
627
628   format_status_left(status, L"Marked %d files for preservation.", keepfilecount);
629
630   return 1;
631 }
632
633 /* mark selected files for deletion */
634 int cmd_delete_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status)
635 {
636   int g;
637   int f;
638   int deletefilecount = 0;
639
640   for (g = 0; g < groupcount; ++g)
641   {
642     for (f = 0; f < groups[g].filecount; ++f)
643     {
644       if (groups[g].files[f].selected)
645       {
646         set_file_action(&groups[g].files[f], -1, deletiontally);
647         ++deletefilecount;
648       }
649     }
650   }
651
652   format_status_left(status, L"Marked %d files for deletion.", deletefilecount);
653
654   return 1;
655 }
656
657 /* mark selected files as unresolved */
658 int cmd_reset_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status)
659 {
660   int g;
661   int f;
662   int resetfilecount = 0;
663
664   for (g = 0; g < groupcount; ++g)
665   {
666     for (f = 0; f < groups[g].filecount; ++f)
667     {
668       if (groups[g].files[f].selected)
669       {
670         set_file_action(&groups[g].files[f], 0, deletiontally);
671         ++resetfilecount;
672       }
673     }
674   }
675
676   format_status_left(status, L"Unmarked %d files.", resetfilecount);
677
678   return 1;
679 }
680
681 int filerowcount(file_t *file, const int columns, int group_file_count);
682
683 /* delete files tagged for deletion, delist sets with no untagged files */
684 int cmd_prune(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, int *totalgroups, int *cursorgroup, int *cursorfile, int *topline, char *logfile, WINDOW *filewin, struct status_text *status)
685 {
686   int deletecount;
687   int preservecount;
688   int unresolvedcount;
689   int totaldeleted = 0;
690   double deletedbytes = 0;
691   struct log_info *loginfo;
692   int g;
693   int f;
694   int to;
695   int adjusttopline;
696   int toplineoffset;
697   int groupfirstline;
698
699   if (logfile != 0)
700     loginfo = log_open(logfile, 0);
701   else
702     loginfo = 0;
703
704   for (g = 0; g < *totalgroups; ++g)
705   {
706     preservecount = 0;
707     deletecount = 0;
708     unresolvedcount = 0;
709
710     for (f = 0; f < groups[g].filecount; ++f)
711     {
712       switch (groups[g].files[f].action)
713       {
714         case -1:
715           ++deletecount;
716           break;
717         case 0:
718           ++unresolvedcount;
719           break;
720         case 1:
721           ++preservecount;
722           break;
723       }
724     }
725
726     if (loginfo)
727       log_begin_set(loginfo);
728
729     /* delete files marked for deletion unless no files left undeleted */
730     if (deletecount < groups[g].filecount)
731     {
732       for (f = 0; f < groups[g].filecount; ++f)
733       {
734         if (groups[g].files[f].action == -1)
735         {
736           if (remove(groups[g].files[f].file->d_name) == 0)
737           {
738             set_file_action(&groups[g].files[f], -2, deletiontally);
739
740             deletedbytes += groups[g].files[f].file->size;
741             ++totaldeleted;
742
743             if (loginfo)
744               log_file_deleted(loginfo, groups[g].files[f].file->d_name);
745           }
746         }
747       }
748
749       if (loginfo)
750       {
751         for (f = 0; f < groups[g].filecount; ++f)
752         {
753           if (groups[g].files[f].action >= 0)
754             log_file_remaining(loginfo, groups[g].files[f].file->d_name);
755         }
756       }
757
758       deletecount = 0;
759     }
760
761     if (loginfo)
762       log_end_set(loginfo);
763
764     /* if no files left unresolved, mark preserved files for delisting */
765     if (unresolvedcount == 0)
766     {
767       for (f = 0; f < groups[g].filecount; ++f)
768         if (groups[g].files[f].action == 1)
769           set_file_action(&groups[g].files[f], -2, deletiontally);
770
771       preservecount = 0;
772     }
773     /* if only one file left unresolved, mark it for delesting */
774     else if (unresolvedcount == 1 && preservecount + deletecount == 0)
775     {
776       for (f = 0; f < groups[g].filecount; ++f)
777         if (groups[g].files[f].action == 0)
778           set_file_action(&groups[g].files[f], -2, deletiontally);
779     }
780
781     /* delist any files marked for delisting */
782     to = 0;
783     for (f = 0; f < groups[g].filecount; ++f)
784       if (groups[g].files[f].action != -2)
785         groups[g].files[to++] = groups[g].files[f];
786
787     groups[g].filecount = to;
788
789     /* reposition cursor, if necessary */
790     if (*cursorgroup == g && *cursorfile > 0 && *cursorfile >= groups[g].filecount)
791       *cursorfile = groups[g].filecount - 1;
792   }
793
794   if (loginfo != 0)
795     log_close(loginfo);
796
797   if (deletedbytes < 1000.0)
798     format_status_left(status, L"Deleted %ld files (occupying %.0f bytes).", totaldeleted, deletedbytes);
799   else if (deletedbytes <= (1000.0 * 1000.0))
800     format_status_left(status, L"Deleted %ld files (occupying %.1f KB).", totaldeleted, deletedbytes / 1000.0);
801   else if (deletedbytes <= (1000.0 * 1000.0 * 1000.0))
802     format_status_left(status, L"Deleted %ld files (occupying %.1f MB).", totaldeleted, deletedbytes / (1000.0 * 1000.0));
803   else
804     format_status_left(status, L"Deleted %ld files (occupying %.1f GB).", totaldeleted, deletedbytes / (1000.0 * 1000.0 * 1000.0));
805
806   /* delist empty groups */
807   to = 0;
808   for (g = 0; g < *totalgroups; ++g)
809   {
810     if (groups[g].filecount > 0)
811     {
812       groups[to] = groups[g];
813
814       /* reposition cursor, if necessary */
815       if (to == *cursorgroup && to != g)
816         *cursorfile = 0;
817
818       ++to;
819     }
820     else
821     {
822       free(groups[g].files);
823     }
824   }
825
826   *totalgroups = to;
827
828   /* reposition cursor, if necessary */
829   if (*cursorgroup >= *totalgroups)
830   {
831     *cursorgroup = *totalgroups - 1;
832     *cursorfile = 0;
833   }
834
835   /* recalculate line boundaries */
836   adjusttopline = 1;
837   toplineoffset = 0;
838   groupfirstline = 0;
839
840   for (g = 0; g < *totalgroups; ++g)
841   {
842     if (adjusttopline && groups[g].endline >= *topline)
843       toplineoffset = groups[g].endline - *topline;
844
845     groups[g].startline = groupfirstline;
846     groups[g].endline = groupfirstline + 2;
847
848     for (f = 0; f < groups[g].filecount; ++f)
849       groups[g].endline += filerowcount(groups[g].files[f].file, COLS, groups[g].filecount);
850
851     if (adjusttopline && toplineoffset > 0)
852     {
853       *topline = groups[g].endline - toplineoffset;
854
855       if (*topline < 0)
856         *topline = 0;
857
858       adjusttopline = 0;
859     }
860
861     groupfirstline = groups[g].endline + 1;
862   }
863
864   if (*totalgroups > 0 && groups[*totalgroups-1].endline <= *topline)
865   {
866     *topline = groups[*totalgroups-1].endline - getmaxy(filewin) + 1;
867
868     if (*topline < 0)
869       *topline = 0;
870   }
871
872   cmd_clear_all_selections(groups, *totalgroups, commandarguments, 0);
873 }