Imported Upstream version 2.0.0
[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 <wchar.h>
28 #include <pcre2.h>
29
30 void set_file_action(struct groupfile *file, int new_action, size_t *deletion_tally);
31
32 struct command_map command_list[] = {
33   {L"sel", COMMAND_SELECT_CONTAINING},
34   {L"selb", COMMAND_SELECT_BEGINNING},
35   {L"sele", COMMAND_SELECT_ENDING},
36   {L"selm", COMMAND_SELECT_MATCHING},
37   {L"selr", COMMAND_SELECT_REGEX},
38   {L"dsel", COMMAND_CLEAR_SELECTIONS_CONTAINING},
39   {L"dselb", COMMAND_CLEAR_SELECTIONS_BEGINNING},
40   {L"dsele", COMMAND_CLEAR_SELECTIONS_ENDING},
41   {L"dselm", COMMAND_CLEAR_SELECTIONS_MATCHING},
42   {L"dselr", COMMAND_CLEAR_SELECTIONS_REGEX},
43   {L"cs", COMMAND_CLEAR_ALL_SELECTIONS},
44   {L"igs", COMMAND_INVERT_GROUP_SELECTIONS},
45   {L"ks", COMMAND_KEEP_SELECTED},
46   {L"ds", COMMAND_DELETE_SELECTED},
47   {L"rs", COMMAND_RESET_SELECTED},
48   {L"rg", COMMAND_RESET_GROUP},
49   {L"all", COMMAND_PRESERVE_ALL},
50   {L"goto", COMMAND_GOTO_SET},
51   {L"exit", COMMAND_EXIT},
52   {L"quit", COMMAND_EXIT},
53   {L"help", COMMAND_HELP},
54   {0, COMMAND_UNDEFINED}
55 };
56
57 struct command_map confirmation_keyword_list[] = {
58   {L"yes", COMMAND_YES},
59   {L"no", COMMAND_NO},
60   {0, COMMAND_UNDEFINED}
61 };
62
63 /* select files containing string */
64 int cmd_select_containing(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
65 {
66   int g;
67   int f;
68   int selectedgroupcount = 0;
69   int selectedfilecount = 0;
70   int groupselected;
71
72   if (wcscmp(commandarguments, L"") != 0)
73   {
74     for (g = 0; g < groupcount; ++g)
75     {
76       groupselected = 0;
77
78       for (f = 0; f < groups[g].filecount; ++f)
79       {
80         if (wcsinmbcs(groups[g].files[f].file->d_name, commandarguments))
81         {
82           groups[g].selected = 1;
83           groups[g].files[f].selected = 1;
84
85           groupselected = 1;
86           ++selectedfilecount;
87         }
88       }
89
90       if (groupselected)
91         ++selectedgroupcount;
92     }
93   }
94
95   format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
96
97   return 1;
98 }
99
100 /* select files beginning with string */
101 int cmd_select_beginning(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
102 {
103   int g;
104   int f;
105   int selectedgroupcount = 0;
106   int selectedfilecount = 0;
107   int groupselected;
108
109   if (wcscmp(commandarguments, L"") != 0)
110   {
111     for (g = 0; g < groupcount; ++g)
112     {
113       groupselected = 0;
114
115       for (f = 0; f < groups[g].filecount; ++f)
116       {
117         if (wcsbeginmbcs(groups[g].files[f].file->d_name, commandarguments))
118         {
119           groups[g].selected = 1;
120           groups[g].files[f].selected = 1;
121
122           groupselected = 1;
123           ++selectedfilecount;
124         }
125       }
126
127       if (groupselected)
128         ++selectedgroupcount;
129     }
130   }
131
132   format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
133
134   return 1;
135 }
136
137 /* select files ending with string */
138 int cmd_select_ending(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
139 {
140   int g;
141   int f;
142   int selectedgroupcount = 0;
143   int selectedfilecount = 0;
144   int groupselected;
145
146   if (wcscmp(commandarguments, L"") != 0)
147   {
148     for (g = 0; g < groupcount; ++g)
149     {
150       groupselected = 0;
151
152       for (f = 0; f < groups[g].filecount; ++f)
153       {
154         if (wcsendsmbcs(groups[g].files[f].file->d_name, commandarguments))
155         {
156           groups[g].selected = 1;
157           groups[g].files[f].selected = 1;
158
159           groupselected = 1;
160           ++selectedfilecount;
161         }
162       }
163
164       if (groupselected)
165         ++selectedgroupcount;
166     }
167   }
168
169   format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
170
171   return 1;
172 }
173
174 /* select files matching string */
175 int cmd_select_matching(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
176 {
177   int g;
178   int f;
179   int selectedgroupcount = 0;
180   int selectedfilecount = 0;
181   int groupselected;
182
183   if (wcscmp(commandarguments, L"") != 0)
184   {
185     for (g = 0; g < groupcount; ++g)
186     {
187       groupselected = 0;
188
189       for (f = 0; f < groups[g].filecount; ++f)
190       {
191         if (wcsmbcscmp(commandarguments, groups[g].files[f].file->d_name) == 0)
192         {
193           groups[g].selected = 1;
194           groups[g].files[f].selected = 1;
195
196           groupselected = 1;
197           ++selectedfilecount;
198         }
199       }
200
201       if (groupselected)
202         ++selectedgroupcount;
203     }
204   }
205
206   format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
207
208   return 1;
209 }
210
211 /* select files matching pattern */
212 int cmd_select_regex(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
213 {
214   size_t size;
215   wchar_t *wcsfilename;
216   size_t needed;
217   int errorcode;
218   PCRE2_SIZE erroroffset;
219   pcre2_code *code;
220   pcre2_match_data *md;
221   int matches;
222   int g;
223   int f;
224   int selectedgroupcount = 0;
225   int selectedfilecount = 0;
226   int groupselected;
227
228   code = pcre2_compile((PCRE2_SPTR)commandarguments, PCRE2_ZERO_TERMINATED, PCRE2_UTF | PCRE2_UCP, &errorcode, &erroroffset, 0);
229
230   if (code == 0)
231     return -1;
232
233   pcre2_jit_compile(code, PCRE2_JIT_COMPLETE);
234
235   md = pcre2_match_data_create(1, 0);
236   if (md == 0)
237     return -1;
238
239   for (g = 0; g < groupcount; ++g)
240   {
241     groupselected = 0;
242
243     for (f = 0; f < groups[g].filecount; ++f)
244     {
245       needed = mbstowcs_escape_invalid(0, groups[g].files[f].file->d_name, 0);
246
247       wcsfilename = (wchar_t*) malloc(needed * sizeof(wchar_t));
248       if (wcsfilename == 0)
249         continue;
250
251       mbstowcs_escape_invalid(wcsfilename, groups[g].files[f].file->d_name, needed);
252
253       matches = pcre2_match(code, (PCRE2_SPTR)wcsfilename, PCRE2_ZERO_TERMINATED, 0, 0, md, 0);
254
255       free(wcsfilename);
256
257       if (matches > 0)
258       {
259         groups[g].selected = 1;
260         groups[g].files[f].selected = 1;
261
262         groupselected = 1;
263         ++selectedfilecount;
264       }
265     }
266
267     if (groupselected)
268       ++selectedgroupcount;
269   }
270
271   format_status_left(status, L"Matched %d files in %d groups.", selectedfilecount, selectedgroupcount);
272
273   pcre2_code_free(code);
274
275   return 1;
276 }
277
278 /* clear selections containing string */
279 int cmd_clear_selections_containing(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
280 {
281   int g;
282   int f;
283   int matchedgroupcount = 0;
284   int matchedfilecount = 0;
285   int groupmatched;
286   int filedeselected;
287   int selectionsremaining;
288
289   if (wcscmp(commandarguments, L"") != 0)
290   {
291     for (g = 0; g < groupcount; ++g)
292     {
293       groupmatched = 0;
294       filedeselected = 0;
295       selectionsremaining = 0;
296
297       for (f = 0; f < groups[g].filecount; ++f)
298       {
299         if (wcsinmbcs(groups[g].files[f].file->d_name, commandarguments))
300         {
301           if (groups[g].files[f].selected)
302           {
303             groups[g].files[f].selected = 0;
304             filedeselected = 1;
305           }
306
307           groupmatched = 1;
308           ++matchedfilecount;
309         }
310
311         if (groups[g].files[f].selected)
312           selectionsremaining = 1;
313       }
314
315       if (filedeselected && !selectionsremaining)
316         groups[g].selected = 0;
317
318       if (groupmatched)
319         ++matchedgroupcount;
320     }
321   }
322
323   format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
324
325   return 1;
326 }
327
328 /* clear selections beginning with string */
329 int cmd_clear_selections_beginning(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
330 {
331   int g;
332   int f;
333   int matchedgroupcount = 0;
334   int matchedfilecount = 0;
335   int groupmatched;
336   int filedeselected;
337   int selectionsremaining;
338
339   if (wcscmp(commandarguments, L"") != 0)
340   {
341     for (g = 0; g < groupcount; ++g)
342     {
343       groupmatched = 0;
344       filedeselected = 0;
345       selectionsremaining = 0;
346
347       for (f = 0; f < groups[g].filecount; ++f)
348       {
349         if (wcsbeginmbcs(groups[g].files[f].file->d_name, commandarguments))
350         {
351           if (groups[g].files[f].selected)
352           {
353             groups[g].files[f].selected = 0;
354             filedeselected = 1;
355           }
356
357           groupmatched = 1;
358           ++matchedfilecount;
359         }
360
361         if (groups[g].files[f].selected)
362           selectionsremaining = 1;
363       }
364
365       if (filedeselected && !selectionsremaining)
366         groups[g].selected = 0;
367
368       if (groupmatched)
369         ++matchedgroupcount;
370     }
371   }
372
373   format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
374
375   return 1;
376 }
377
378 /* clear selections ending with string */
379 int cmd_clear_selections_ending(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
380 {
381   int g;
382   int f;
383   int matchedgroupcount = 0;
384   int matchedfilecount = 0;
385   int groupmatched;
386   int filedeselected;
387   int selectionsremaining;
388
389   if (wcscmp(commandarguments, L"") != 0)
390   {
391     for (g = 0; g < groupcount; ++g)
392     {
393       groupmatched = 0;
394       filedeselected = 0;
395       selectionsremaining = 0;
396
397       for (f = 0; f < groups[g].filecount; ++f)
398       {
399         if (wcsendsmbcs(groups[g].files[f].file->d_name, commandarguments))
400         {
401           if (groups[g].files[f].selected)
402           {
403             groups[g].files[f].selected = 0;
404             filedeselected = 1;
405           }
406
407           groupmatched = 1;
408           ++matchedfilecount;
409         }
410
411         if (groups[g].files[f].selected)
412           selectionsremaining = 1;
413       }
414
415       if (filedeselected && !selectionsremaining)
416         groups[g].selected = 0;
417
418       if (groupmatched)
419         ++matchedgroupcount;
420     }
421   }
422
423   format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
424
425   return 1;
426 }
427
428 /* clear selections matching string */
429 int cmd_clear_selections_matching(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
430 {
431   int g;
432   int f;
433   int matchedgroupcount = 0;
434   int matchedfilecount = 0;
435   int groupmatched;
436   int filedeselected;
437   int selectionsremaining;
438
439   if (wcscmp(commandarguments, L"") != 0)
440   {
441     for (g = 0; g < groupcount; ++g)
442     {
443       groupmatched = 0;
444       filedeselected = 0;
445       selectionsremaining = 0;
446
447       for (f = 0; f < groups[g].filecount; ++f)
448       {
449         if (wcsmbcscmp(commandarguments, groups[g].files[f].file->d_name) == 0)
450         {
451           if (groups[g].files[f].selected)
452           {
453             groups[g].files[f].selected = 0;
454             filedeselected = 1;
455           }
456
457           groupmatched = 1;
458           ++matchedfilecount;
459         }
460
461         if (groups[g].files[f].selected)
462           selectionsremaining = 1;
463       }
464
465       if (filedeselected && !selectionsremaining)
466         groups[g].selected = 0;
467
468       if (groupmatched)
469         ++matchedgroupcount;
470     }
471   }
472
473   format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
474
475   return 1;
476 }
477
478 /* clear selection matching pattern */
479 int cmd_clear_selections_regex(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
480 {
481   size_t size;
482   wchar_t *wcsfilename;
483   size_t needed;
484   int errorcode;
485   PCRE2_SIZE erroroffset;
486   pcre2_code *code;
487   pcre2_match_data *md;
488   int matches;
489   int g;
490   int f;
491   int matchedgroupcount = 0;
492   int matchedfilecount = 0;
493   int groupmatched;
494   int filedeselected;
495   int selectionsremaining;
496
497   code = pcre2_compile((PCRE2_SPTR)commandarguments, PCRE2_ZERO_TERMINATED, PCRE2_UTF | PCRE2_UCP, &errorcode, &erroroffset, 0);
498
499   if (code == 0)
500     return -1;
501
502   pcre2_jit_compile(code, PCRE2_JIT_COMPLETE);
503
504   md = pcre2_match_data_create(1, 0);
505   if (md == 0)
506     return -1;
507
508   for (g = 0; g < groupcount; ++g)
509   {
510     groupmatched = 0;
511     filedeselected = 0;
512     selectionsremaining = 0;
513
514     for (f = 0; f < groups[g].filecount; ++f)
515     {
516       needed = mbstowcs_escape_invalid(0, groups[g].files[f].file->d_name, 0);
517
518       wcsfilename = (wchar_t*) malloc(needed * sizeof(wchar_t));
519       if (wcsfilename == 0)
520         continue;
521
522       mbstowcs_escape_invalid(wcsfilename, groups[g].files[f].file->d_name, needed);
523
524       matches = pcre2_match(code, (PCRE2_SPTR)wcsfilename, PCRE2_ZERO_TERMINATED, 0, 0, md, 0);
525
526       free(wcsfilename);
527
528       if (matches > 0)
529       {
530         if (groups[g].files[f].selected)
531         {
532           groups[g].files[f].selected = 0;
533           filedeselected = 1;
534         }
535
536         groupmatched = 1;
537         ++matchedfilecount;
538       }
539
540       if (groups[g].files[f].selected)
541         selectionsremaining = 1;
542     }
543
544     if (filedeselected && !selectionsremaining)
545       groups[g].selected = 0;
546
547     if (groupmatched)
548       ++matchedgroupcount;
549   }
550
551   format_status_left(status, L"Matched %d files in %d groups.", matchedfilecount, matchedgroupcount);
552
553   pcre2_code_free(code);
554
555   return 1;
556 }
557
558 /* clear all selections and selected groups */
559 int cmd_clear_all_selections(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
560 {
561   int g;
562   int f;
563
564   for (g = 0; g < groupcount; ++g)
565   {
566     for (f = 0; f < groups[g].filecount; ++f)
567       groups[g].files[f].selected = 0;
568
569     groups[g].selected = 0;
570   }
571
572   format_status_left(status, L"Cleared all selections.");
573
574   return 1;
575 }
576
577 /* invert selections within selected groups */
578 int cmd_invert_group_selections(struct filegroup *groups, int groupcount, wchar_t *commandarguments, struct status_text *status)
579 {
580   int g;
581   int f;
582   int selectedcount = 0;
583   int deselectedcount = 0;
584
585   for (g = 0; g < groupcount; ++g)
586   {
587     if (groups[g].selected)
588     {
589       for (f = 0; f < groups[g].filecount; ++f)
590       {
591         groups[g].files[f].selected = !groups[g].files[f].selected;
592
593         if (groups[g].files[f].selected)
594           ++selectedcount;
595         else
596           ++deselectedcount;
597       }
598     }
599   }
600
601   format_status_left(status, L"Selected %d files. Deselected %d files.", selectedcount, deselectedcount);
602
603   return 1;
604 }
605
606 /* mark selected files for preservation */
607 int cmd_keep_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status)
608 {
609   int g;
610   int f;
611   int keepfilecount = 0;
612
613   for (g = 0; g < groupcount; ++g)
614   {
615     for (f = 0; f < groups[g].filecount; ++f)
616     {
617       if (groups[g].files[f].selected)
618       {
619         set_file_action(&groups[g].files[f], 1, deletiontally);
620         ++keepfilecount;
621       }
622     }
623   }
624
625   format_status_left(status, L"Marked %d files for preservation.", keepfilecount);
626
627   return 1;
628 }
629
630 /* mark selected files for deletion */
631 int cmd_delete_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status)
632 {
633   int g;
634   int f;
635   int deletefilecount = 0;
636
637   for (g = 0; g < groupcount; ++g)
638   {
639     for (f = 0; f < groups[g].filecount; ++f)
640     {
641       if (groups[g].files[f].selected)
642       {
643         set_file_action(&groups[g].files[f], -1, deletiontally);
644         ++deletefilecount;
645       }
646     }
647   }
648
649   format_status_left(status, L"Marked %d files for deletion.", deletefilecount);
650
651   return 1;
652 }
653
654 /* mark selected files as unresolved */
655 int cmd_reset_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status)
656 {
657   int g;
658   int f;
659   int resetfilecount = 0;
660
661   for (g = 0; g < groupcount; ++g)
662   {
663     for (f = 0; f < groups[g].filecount; ++f)
664     {
665       if (groups[g].files[f].selected)
666       {
667         set_file_action(&groups[g].files[f], 0, deletiontally);
668         ++resetfilecount;
669       }
670     }
671   }
672
673   format_status_left(status, L"Unmarked %d files.", resetfilecount);
674
675   return 1;
676 }