1 /* FDUPES Copyright (c) 2018 Adrian Lopez
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:
11 The above copyright notice and this permission notice shall be
12 included in all copies or substantial portions of the Software.
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. */
23 #include "ncurses-status.h"
24 #include "ncurses-commands.h"
26 #include "mbstowcs_escape_invalid.h"
31 void set_file_action(struct groupfile *file, int new_action, size_t *deletion_tally);
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}
59 struct command_map confirmation_keyword_list[] = {
60 {L"yes", COMMAND_YES},
62 {0, COMMAND_UNDEFINED}
65 /* select files containing string */
66 int cmd_select_containing(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
70 int selectedgroupcount = 0;
71 int selectedfilecount = 0;
74 if (wcscmp(commandarguments, L"") != 0)
76 for (g = 0; g < groupcount; ++g)
80 for (f = 0; f < groups[g].filecount; ++f)
82 if (wcsinmbcs(groups[g].files[f].file->d_name, commandarguments))
84 groups[g].selected = 1;
85 groups[g].files[f].selected = 1;
97 format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
102 /* select files beginning with string */
103 int cmd_select_beginning(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
107 int selectedgroupcount = 0;
108 int selectedfilecount = 0;
111 if (wcscmp(commandarguments, L"") != 0)
113 for (g = 0; g < groupcount; ++g)
117 for (f = 0; f < groups[g].filecount; ++f)
119 if (wcsbeginmbcs(groups[g].files[f].file->d_name, commandarguments))
121 groups[g].selected = 1;
122 groups[g].files[f].selected = 1;
130 ++selectedgroupcount;
134 format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
139 /* select files ending with string */
140 int cmd_select_ending(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
144 int selectedgroupcount = 0;
145 int selectedfilecount = 0;
148 if (wcscmp(commandarguments, L"") != 0)
150 for (g = 0; g < groupcount; ++g)
154 for (f = 0; f < groups[g].filecount; ++f)
156 if (wcsendsmbcs(groups[g].files[f].file->d_name, commandarguments))
158 groups[g].selected = 1;
159 groups[g].files[f].selected = 1;
167 ++selectedgroupcount;
171 format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
176 /* select files matching string */
177 int cmd_select_matching(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
181 int selectedgroupcount = 0;
182 int selectedfilecount = 0;
185 if (wcscmp(commandarguments, L"") != 0)
187 for (g = 0; g < groupcount; ++g)
191 for (f = 0; f < groups[g].filecount; ++f)
193 if (wcsmbcscmp(commandarguments, groups[g].files[f].file->d_name) == 0)
195 groups[g].selected = 1;
196 groups[g].files[f].selected = 1;
204 ++selectedgroupcount;
208 format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
213 /* select files matching pattern */
214 int cmd_select_regex(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
217 wchar_t *wcsfilename;
220 PCRE2_SIZE erroroffset;
222 pcre2_match_data *md;
226 int selectedgroupcount = 0;
227 int selectedfilecount = 0;
230 code = pcre2_compile((PCRE2_SPTR)commandarguments, PCRE2_ZERO_TERMINATED, PCRE2_UTF | PCRE2_UCP, &errorcode, &erroroffset, 0);
235 pcre2_jit_compile(code, PCRE2_JIT_COMPLETE);
237 md = pcre2_match_data_create(1, 0);
241 for (g = 0; g < groupcount; ++g)
245 for (f = 0; f < groups[g].filecount; ++f)
247 needed = mbstowcs_escape_invalid(0, groups[g].files[f].file->d_name, 0);
249 wcsfilename = (wchar_t*) malloc(needed * sizeof(wchar_t));
250 if (wcsfilename == 0)
253 mbstowcs_escape_invalid(wcsfilename, groups[g].files[f].file->d_name, needed);
255 matches = pcre2_match(code, (PCRE2_SPTR)wcsfilename, PCRE2_ZERO_TERMINATED, 0, 0, md, 0);
261 groups[g].selected = 1;
262 groups[g].files[f].selected = 1;
270 ++selectedgroupcount;
273 format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
275 pcre2_code_free(code);
280 /* clear selections containing string */
281 int cmd_clear_selections_containing(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
285 int matchedgroupcount = 0;
286 int matchedfilecount = 0;
289 int selectionsremaining;
291 if (wcscmp(commandarguments, L"") != 0)
293 for (g = 0; g < groupcount; ++g)
297 selectionsremaining = 0;
299 for (f = 0; f < groups[g].filecount; ++f)
301 if (wcsinmbcs(groups[g].files[f].file->d_name, commandarguments))
303 if (groups[g].files[f].selected)
305 groups[g].files[f].selected = 0;
313 if (groups[g].files[f].selected)
314 selectionsremaining = 1;
317 if (filedeselected && !selectionsremaining)
318 groups[g].selected = 0;
325 format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
330 /* clear selections beginning with string */
331 int cmd_clear_selections_beginning(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
335 int matchedgroupcount = 0;
336 int matchedfilecount = 0;
339 int selectionsremaining;
341 if (wcscmp(commandarguments, L"") != 0)
343 for (g = 0; g < groupcount; ++g)
347 selectionsremaining = 0;
349 for (f = 0; f < groups[g].filecount; ++f)
351 if (wcsbeginmbcs(groups[g].files[f].file->d_name, commandarguments))
353 if (groups[g].files[f].selected)
355 groups[g].files[f].selected = 0;
363 if (groups[g].files[f].selected)
364 selectionsremaining = 1;
367 if (filedeselected && !selectionsremaining)
368 groups[g].selected = 0;
375 format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
380 /* clear selections ending with string */
381 int cmd_clear_selections_ending(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
385 int matchedgroupcount = 0;
386 int matchedfilecount = 0;
389 int selectionsremaining;
391 if (wcscmp(commandarguments, L"") != 0)
393 for (g = 0; g < groupcount; ++g)
397 selectionsremaining = 0;
399 for (f = 0; f < groups[g].filecount; ++f)
401 if (wcsendsmbcs(groups[g].files[f].file->d_name, commandarguments))
403 if (groups[g].files[f].selected)
405 groups[g].files[f].selected = 0;
413 if (groups[g].files[f].selected)
414 selectionsremaining = 1;
417 if (filedeselected && !selectionsremaining)
418 groups[g].selected = 0;
425 format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
430 /* clear selections matching string */
431 int cmd_clear_selections_matching(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
435 int matchedgroupcount = 0;
436 int matchedfilecount = 0;
439 int selectionsremaining;
441 if (wcscmp(commandarguments, L"") != 0)
443 for (g = 0; g < groupcount; ++g)
447 selectionsremaining = 0;
449 for (f = 0; f < groups[g].filecount; ++f)
451 if (wcsmbcscmp(commandarguments, groups[g].files[f].file->d_name) == 0)
453 if (groups[g].files[f].selected)
455 groups[g].files[f].selected = 0;
463 if (groups[g].files[f].selected)
464 selectionsremaining = 1;
467 if (filedeselected && !selectionsremaining)
468 groups[g].selected = 0;
475 format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
480 /* clear selection matching pattern */
481 int cmd_clear_selections_regex(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
484 wchar_t *wcsfilename;
487 PCRE2_SIZE erroroffset;
489 pcre2_match_data *md;
493 int matchedgroupcount = 0;
494 int matchedfilecount = 0;
497 int selectionsremaining;
499 code = pcre2_compile((PCRE2_SPTR)commandarguments, PCRE2_ZERO_TERMINATED, PCRE2_UTF | PCRE2_UCP, &errorcode, &erroroffset, 0);
504 pcre2_jit_compile(code, PCRE2_JIT_COMPLETE);
506 md = pcre2_match_data_create(1, 0);
510 for (g = 0; g < groupcount; ++g)
514 selectionsremaining = 0;
516 for (f = 0; f < groups[g].filecount; ++f)
518 needed = mbstowcs_escape_invalid(0, groups[g].files[f].file->d_name, 0);
520 wcsfilename = (wchar_t*) malloc(needed * sizeof(wchar_t));
521 if (wcsfilename == 0)
524 mbstowcs_escape_invalid(wcsfilename, groups[g].files[f].file->d_name, needed);
526 matches = pcre2_match(code, (PCRE2_SPTR)wcsfilename, PCRE2_ZERO_TERMINATED, 0, 0, md, 0);
532 if (groups[g].files[f].selected)
534 groups[g].files[f].selected = 0;
542 if (groups[g].files[f].selected)
543 selectionsremaining = 1;
546 if (filedeselected && !selectionsremaining)
547 groups[g].selected = 0;
553 format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
555 pcre2_code_free(code);
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)
566 for (g = 0; g < groupcount; ++g)
568 for (f = 0; f < groups[g].filecount; ++f)
569 groups[g].files[f].selected = 0;
571 groups[g].selected = 0;
575 format_status_left(status, L"Cleared all selections.");
580 /* invert selections within selected groups */
581 int cmd_invert_group_selections(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
585 int selectedcount = 0;
586 int deselectedcount = 0;
588 for (g = 0; g < groupcount; ++g)
590 if (groups[g].selected)
592 for (f = 0; f < groups[g].filecount; ++f)
594 groups[g].files[f].selected = !groups[g].files[f].selected;
596 if (groups[g].files[f].selected)
604 format_status_left(status, L"Selected %d files. Deselected %d files.", selectedcount, deselectedcount);
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)
614 int keepfilecount = 0;
616 for (g = 0; g < groupcount; ++g)
618 for (f = 0; f < groups[g].filecount; ++f)
620 if (groups[g].files[f].selected)
622 set_file_action(&groups[g].files[f], 1, deletiontally);
628 format_status_left(status, L"Marked %d files for preservation.", keepfilecount);
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)
638 int deletefilecount = 0;
640 for (g = 0; g < groupcount; ++g)
642 for (f = 0; f < groups[g].filecount; ++f)
644 if (groups[g].files[f].selected)
646 set_file_action(&groups[g].files[f], -1, deletiontally);
652 format_status_left(status, L"Marked %d files for deletion.", deletefilecount);
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)
662 int resetfilecount = 0;
664 for (g = 0; g < groupcount; ++g)
666 for (f = 0; f < groups[g].filecount; ++f)
668 if (groups[g].files[f].selected)
670 set_file_action(&groups[g].files[f], 0, deletiontally);
676 format_status_left(status, L"Unmarked %d files.", resetfilecount);
681 int filerowcount(file_t *file, const int columns, int group_file_count);
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)
689 int totaldeleted = 0;
690 double deletedbytes = 0;
691 struct log_info *loginfo;
700 loginfo = log_open(logfile, 0);
704 for (g = 0; g < *totalgroups; ++g)
710 for (f = 0; f < groups[g].filecount; ++f)
712 switch (groups[g].files[f].action)
727 log_begin_set(loginfo);
729 /* delete files marked for deletion unless no files left undeleted */
730 if (deletecount < groups[g].filecount)
732 for (f = 0; f < groups[g].filecount; ++f)
734 if (groups[g].files[f].action == -1)
736 if (remove(groups[g].files[f].file->d_name) == 0)
738 set_file_action(&groups[g].files[f], -2, deletiontally);
740 deletedbytes += groups[g].files[f].file->size;
744 log_file_deleted(loginfo, groups[g].files[f].file->d_name);
751 for (f = 0; f < groups[g].filecount; ++f)
753 if (groups[g].files[f].action >= 0)
754 log_file_remaining(loginfo, groups[g].files[f].file->d_name);
762 log_end_set(loginfo);
764 /* if no files left unresolved, mark preserved files for delisting */
765 if (unresolvedcount == 0)
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);
773 /* if only one file left unresolved, mark it for delesting */
774 else if (unresolvedcount == 1 && preservecount + deletecount == 0)
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);
781 /* delist any files marked for delisting */
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];
787 groups[g].filecount = to;
789 /* reposition cursor, if necessary */
790 if (*cursorgroup == g && *cursorfile > 0 && *cursorfile >= groups[g].filecount)
791 *cursorfile = groups[g].filecount - 1;
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));
804 format_status_left(status, L"Deleted %ld files (occupying %.1f GB).", totaldeleted, deletedbytes / (1000.0 * 1000.0 * 1000.0));
806 /* delist empty groups */
808 for (g = 0; g < *totalgroups; ++g)
810 if (groups[g].filecount > 0)
812 groups[to] = groups[g];
814 /* reposition cursor, if necessary */
815 if (to == *cursorgroup && to != g)
822 free(groups[g].files);
828 /* reposition cursor, if necessary */
829 if (*cursorgroup >= *totalgroups)
831 *cursorgroup = *totalgroups - 1;
835 /* recalculate line boundaries */
840 for (g = 0; g < *totalgroups; ++g)
842 if (adjusttopline && groups[g].endline >= *topline)
843 toplineoffset = groups[g].endline - *topline;
845 groups[g].startline = groupfirstline;
846 groups[g].endline = groupfirstline + 2;
848 for (f = 0; f < groups[g].filecount; ++f)
849 groups[g].endline += filerowcount(groups[g].files[f].file, COLS, groups[g].filecount);
851 if (adjusttopline && toplineoffset > 0)
853 *topline = groups[g].endline - toplineoffset;
861 groupfirstline = groups[g].endline + 1;
864 if (*totalgroups > 0 && groups[*totalgroups-1].endline <= *topline)
866 *topline = groups[*totalgroups-1].endline - getmaxy(filewin) + 1;
872 cmd_clear_all_selections(groups, *totalgroups, commandarguments, 0);