1 /*
2 ** 1999-09-11 - A fun Rename command, with support for both simplistic replacement in filenames
3 ** as well as powerful regular expression replacement thingie. The former is handy
4 ** for changing the extension in all selected names from ".jpg" to ".jpeg", for
5 ** example. The latter RE mode can be used to do rather complex mappings and trans-
6 ** forms on the filenames. For example, consider having a set of 100 image files,
7 ** named "frame0.gif", "frame1.gif", ..., "frame99.gif". You want to rename these
8 ** to include a movie index (11), and also (of course) change the extension to ".png".
9 ** No problem. Just enter "^frame([0-9]+)\.gif$" in the 'From' text entry field
10 ** on the Reg Exp page, and "frame-11-$1.png" in the 'To' entry, and bam! The point
11 ** here is that you can use up to 9 parenthesized (?) subexpressions, and then access
12 ** the text that matched each with $n (n is a digit, 1 to 9) in the 'To' string.
13 ** If you've programmed any Perl, you might be familiar with the concept.
14 */
15
16 #include "gentoo.h"
17
18 #include <ctype.h>
19
20 #include "cmd_delete.h"
21 #include "dialog.h"
22 #include "dirpane.h"
23 #include "errors.h"
24 #include "guiutil.h"
25 #include "overwrite.h"
26 #include "strutil.h"
27
28 #include "cmd_renamere.h"
29
30 #define CMD_ID "renamere"
31
32 /* ----------------------------------------------------------------------------------------- */
33
34 typedef enum {
35 LOWER, UPPER, INITIAL, HEADLINE
36 } CaseMode;
37
38 typedef struct {
39 Dialog *dlg;
40
41 GtkWidget *nbook;
42
43 /* Simple replace mode. */
44 GtkWidget *r_vbox;
45 GtkWidget *r_from;
46 GtkWidget *r_to;
47 GtkWidget *r_cnocase;
48 GtkWidget *r_cglobal;
49
50 /* Full RE matching mode. */
51 GtkWidget *re_vbox;
52 GtkWidget *re_from;
53 GtkWidget *re_to;
54 GtkWidget *re_cnocase;
55
56 /* Mapping mode (for character substitutions). */
57 GtkWidget *map_vbox;
58 GtkWidget *map_from;
59 GtkWidget *map_to;
60 GtkWidget *map_remove;
61
62 /* Case-conversion mode. */
63 GtkWidget *case_vbox;
64 GtkWidget *case_lower;
65 GtkWidget *case_upper;
66 GtkWidget *case_initial;
67
68 MainInfo *min;
69 DirPane *src;
70 gint page;
71 GSList *selection;
72 } RenREInfo;
73
74 /* ----------------------------------------------------------------------------------------- */
75
do_rename(MainInfo * min,DirPane * dp,const DirRow2 * row,const gchar * newname,GError ** error)76 static gboolean do_rename(MainInfo *min, DirPane *dp, const DirRow2 *row, const gchar *newname, GError **error)
77 {
78 OvwRes ores;
79 GFile *file;
80 gboolean ok, doit = TRUE;
81
82 if(strcmp(dp_row_get_name_display(dp_get_tree_model(dp), row), newname) == 0)
83 {
84 dp_unselect(dp, row);
85 return TRUE;
86 }
87
88 /* Get imaginary destination file. */
89 if((file = dp_get_file_from_name(dp, newname)) == NULL)
90 return FALSE;
91 ores = ovw_overwrite_unary_file(dp, file);
92 if(ores == OVW_SKIP)
93 ok = !(doit = FALSE); /* H4xX0r. */
94 else if(ores == OVW_CANCEL)
95 ok = doit = FALSE;
96 else if(ores == OVW_PROCEED_FILE || ores == OVW_PROCEED_DIR)
97 ok = del_delete_gfile(min, file, FALSE, error);
98 else
99 ok = TRUE;
100
101 if(ok && doit)
102 {
103 GFile *dfile;
104
105 if((dfile = dp_get_file_from_row(dp, row)) != NULL)
106 {
107 GFile *nfile;
108
109 if((nfile = g_file_set_display_name(dfile, newname, NULL, error)) != NULL)
110 g_object_unref(nfile);
111 g_object_unref(dfile);
112 ok = nfile != NULL;
113 }
114 else
115 ok = FALSE;
116 }
117 if(!ok && error != NULL && *error != NULL)
118 err_set_gerror(min, error, CMD_ID, file);
119 g_object_unref(file);
120
121 if(ok)
122 dp_unselect(dp, row);
123
124 return ok;
125 }
126
127 /* ----------------------------------------------------------------------------------------- */
128
rename_simple(RenREInfo * ri,GError ** err)129 static gboolean rename_simple(RenREInfo *ri, GError **err)
130 {
131 const gchar *fromd, *tod;
132 GString *nn;
133 GSList *iter;
134 gboolean nocase, global, ok = TRUE;
135
136 if((fromd = gtk_entry_get_text(GTK_ENTRY(ri->r_from))) == NULL)
137 return 0;
138 tod = gtk_entry_get_text(GTK_ENTRY(ri->r_to));
139
140 nocase = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(ri->r_cnocase));
141 global = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(ri->r_cglobal));
142
143 ovw_overwrite_begin(ri->min, _("\"%s\" Already Exists - Proceed With Rename?"), 0U);
144 nn = g_string_new("");
145 for(iter = ri->selection; ok && (iter != NULL); iter = g_slist_next(iter))
146 {
147 if(stu_replace_simple(nn, dp_row_get_name_display(dp_get_tree_model(ri->src), iter->data), fromd, tod, global, nocase) > 0)
148 ok = do_rename(ri->min, ri->src, iter->data, nn->str, err);
149 }
150 g_string_free(nn, TRUE);
151 ovw_overwrite_end(ri->min);
152
153 return ok;
154 }
155
156 /* ----------------------------------------------------------------------------------------- */
157
158 /* 1999-09-11 - Go through <def>, copying characters to <str>. If the sequence $n is found,
159 ** where n is a digit 1..9, replace that with the n:th piece of <src>, as described
160 ** by <match>. To get a single dollar in the output, write $$ in <def>.
161 ** Returns TRUE if the entire <def> was successfully interpolated, FALSE on error.
162 */
interpolate(GString * str,const gchar * def,const GMatchInfo * match)163 static gboolean interpolate(GString *str, const gchar *def, const GMatchInfo *match)
164 {
165 const gchar *ptr;
166
167 g_string_truncate(str, 0);
168 for(ptr = def; *ptr; ptr = g_utf8_next_char(ptr))
169 {
170 gunichar here;
171
172 here = g_utf8_get_char(ptr);
173 if(here == '$')
174 {
175 ptr = g_utf8_next_char(ptr);
176 here = g_utf8_get_char(ptr);
177 if(g_unichar_isdigit(here))
178 {
179 gint slot;
180
181 slot = g_unichar_digit_value(here);
182 if(slot >= 0 && slot <= 9)
183 {
184 gchar *ms = g_match_info_fetch(match, slot);
185
186 if(ms != NULL)
187 {
188 g_string_append(str, ms);
189 g_free(ms);
190 }
191 else
192 return FALSE;
193 }
194 else
195 return FALSE;
196 }
197 else if(*ptr == '$')
198 g_string_append_c(str, '$'), ptr++;
199 else
200 return FALSE;
201 }
202 else
203 g_string_append_unichar(str, here);
204 }
205 return TRUE;
206 }
207
rename_regexp(RenREInfo * ri,GError ** err)208 static gboolean rename_regexp(RenREInfo *ri, GError **err)
209 {
210 const gchar *fromdefd, *tod;
211 guint reflags;
212 GRegex *fromre = NULL;
213 gboolean ok = TRUE;
214
215 if((fromdefd = gtk_entry_get_text(GTK_ENTRY(ri->re_from))) == NULL)
216 return FALSE;
217 if((tod = gtk_entry_get_text(GTK_ENTRY(ri->re_to))) == NULL)
218 return FALSE;
219
220 reflags = G_REGEX_EXTENDED | (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(ri->re_cnocase)) ? G_REGEX_CASELESS : 0);
221 if((fromre = g_regex_new(fromdefd, reflags, G_REGEX_MATCH_NOTEMPTY, err)) != NULL)
222 {
223 GSList *iter;
224 GString *nn;
225
226 ovw_overwrite_begin(ri->min, _("\"%s\" Already Exists - Proceed With Rename?"), 0U);
227 nn = g_string_new("");
228 for(iter = ri->selection; ok && (iter != NULL); iter = g_slist_next(iter))
229 {
230 GMatchInfo *mi = NULL;
231
232 if(g_regex_match(fromre, dp_row_get_name_display(dp_get_tree_model(ri->src), iter->data), G_REGEX_MATCH_NOTEMPTY, &mi))
233 {
234 if(interpolate(nn, tod, mi) && g_utf8_strchr(nn->str, G_DIR_SEPARATOR, -1) == NULL)
235 ok = do_rename(ri->min, ri->src, iter->data, nn->str, err);
236 g_match_info_free(mi);
237 }
238 }
239 g_string_free(nn, TRUE);
240 ovw_overwrite_end(ri->min);
241 }
242 return ok;
243 }
244
245 /* ----------------------------------------------------------------------------------------- */
246
rename_map(RenREInfo * ri,GError ** err)247 static gboolean rename_map(RenREInfo *ri, GError **err)
248 {
249 const gchar *fromdef, *todef, *remdef;
250 GSList *iter;
251 GString *nn, *nr;
252 gsize fl, rl, i, nl;
253 gboolean ok = TRUE;
254
255 if((fromdef = gtk_entry_get_text(GTK_ENTRY(ri->map_from))) == NULL)
256 return FALSE;
257 if((todef = gtk_entry_get_text(GTK_ENTRY(ri->map_to))) == NULL)
258 return FALSE;
259 if(g_utf8_strchr(todef, -1, G_DIR_SEPARATOR) != NULL) /* As always, prevent rogue moves. */
260 return FALSE;
261 if((remdef = gtk_entry_get_text(GTK_ENTRY(ri->map_remove))) == NULL)
262 return FALSE;
263
264 /* Totally important: some lengths are in chars (for iteration), some in bytes for g_utf8_strchr(). */
265 fl = strlen(fromdef);
266 rl = strlen(remdef);
267
268 ovw_overwrite_begin(ri->min, _("\"%s\" Already Exists - Proceed With Rename?"), 0U);
269 nn = g_string_new("");
270 nr = g_string_new("");
271 for(iter = ri->selection; iter != NULL; iter = g_slist_next(iter))
272 {
273 const gchar *ptr, *map;
274
275 /* Step 1: clear the dynamic string. */
276 g_string_truncate(nn, 0);
277 ptr = dp_row_get_name_display(dp_get_tree_model(ri->src), iter->data);
278 nl = g_utf8_strlen(ptr, -1);
279 /* Step 2: go through and do the mapping. */
280 for(i = 0; i < nl; i++, ptr = g_utf8_next_char(ptr))
281 {
282 gunichar ch = g_utf8_get_char(ptr);
283
284 if((map = g_utf8_strchr(fromdef, fl, ch)) != NULL)
285 {
286 glong index = g_utf8_pointer_to_offset(fromdef, map);
287 const gchar *tptr = g_utf8_offset_to_pointer(todef, index);
288
289 ch = g_utf8_get_char(tptr); /* Map. */
290 }
291 g_string_append_unichar(nn, ch);
292 }
293 /* Step 3: remove any characters found in the 'remove' string. */
294 ptr = nn->str;
295 nl = g_utf8_strlen(ptr, -1);
296 g_string_truncate(nr, 0);
297 for(i = 0; i < nl; i++, ptr = g_utf8_next_char(ptr))
298 {
299 gunichar ch = g_utf8_get_char(ptr);
300
301 if(g_utf8_strchr(remdef, rl, ch) == NULL)
302 g_string_append_unichar(nr, ch);
303 }
304 /* Step 4: do the rename, using the now-built "display name". */
305 ok = do_rename(ri->min, ri->src, iter->data, nr->str, err);
306 }
307 g_string_free(nr, TRUE);
308 g_string_free(nn, TRUE);
309 ovw_overwrite_end(ri->min);
310
311 return ok;
312 }
313
314 /* ----------------------------------------------------------------------------------------- */
315
316 /* 2008-07-19 - Converts the given string to have an upper-case initial character, while the
317 * remainder is lower-case. This is a very western idea of a useful case
318 * conversion, but ... I'll include it anyway.
319 */
utf8_str_initial(const gchar * str,gssize len)320 static gchar * utf8_str_initial(const gchar *str, gssize len)
321 {
322 GString *tmp;
323 gchar *buf;
324 gunichar here;
325
326 tmp = g_string_sized_new(len);
327
328 here = g_utf8_get_char(str);
329 g_string_append_unichar(tmp, g_unichar_toupper(here));
330
331 while(--len)
332 {
333 str = g_utf8_next_char(str);
334 here = g_utf8_get_char(str);
335 g_string_append_unichar(tmp, g_unichar_tolower(here));
336 }
337 buf = tmp->str;
338 g_string_free(tmp, FALSE);
339
340 return buf;
341 }
342
343 /* 2008-04-20 - Minor touch-ups due to GTK+ 2.0 and UTF-8 in strings. We need to make a decision:
344 * Is the map operation happening in file-system or display space? The obvious
345 * choice seems to be file-system, which is after all what the filenames are for.
346 * However, we don't have a general way of downcasing if the local file system is
347 * using UTF-8 ... So we use the display versions of the names, downcase in UTF-8,
348 * and then convert back.
349 */
rename_case(RenREInfo * ri,GError ** err)350 static gboolean rename_case(RenREInfo *ri, GError **err)
351 {
352 GString *nn;
353 GSList *iter;
354 gchar * (*func)(const gchar *, gssize), *cc;
355 gboolean ok = TRUE;
356
357 /* Select the case-conversion function, once. */
358 if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(ri->case_lower)))
359 func = g_utf8_strdown;
360 else if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(ri->case_upper)))
361 func = g_utf8_strup;
362 else if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(ri->case_initial)))
363 func = utf8_str_initial;
364 else
365 return FALSE;
366
367 ovw_overwrite_begin(ri->min, _("\"%s\" Already Exists - Proceed With Rename?"), 0U);
368 nn = g_string_new("");
369 for(iter = ri->selection; ok && (iter != NULL); iter = g_slist_next(iter))
370 {
371 const gchar *dn = dp_row_get_name_display(dp_get_tree_model(ri->src), iter->data);
372
373 /* Step 1: set the dynamic string to the input filename. */
374 g_string_assign(nn, dn);
375 /* Step 2: perform case conversion. */
376 cc = func(nn->str, nn->len);
377 /* Step 3: if resulting name is different, rename on disk. */
378 if(strcmp(dn, cc) != 0)
379 {
380 ok = do_rename(ri->min, ri->src, iter->data, cc, err);
381 }
382 g_free(cc);
383 }
384 g_string_free(nn, TRUE);
385 ovw_overwrite_end(ri->min);
386
387 return ok;
388 }
389
390 /* ----------------------------------------------------------------------------------------- */
391
evt_nbook_switchpage(GtkWidget * wid,gpointer page,gint page_num,gpointer user)392 static void evt_nbook_switchpage(GtkWidget *wid, gpointer page, gint page_num, gpointer user)
393 {
394 RenREInfo *ri = user;
395
396 ri->page = page_num;
397 gtk_widget_grab_focus(ri->page ? ri->re_from : ri->r_from);
398 }
399
cmd_renamere(MainInfo * min,DirPane * src,DirPane * dst,const CmdArg * ca)400 gint cmd_renamere(MainInfo *min, DirPane *src, DirPane *dst, const CmdArg *ca)
401 {
402 GtkWidget *label, *grid;
403 static RenREInfo ri = { NULL };
404 gboolean ok = TRUE;
405
406 ri.min = min;
407 ri.src = src;
408
409 if((ri.selection = dp_get_selection(ri.src)) == NULL)
410 return 1;
411
412 if(ri.dlg == NULL)
413 {
414 ri.nbook = gtk_notebook_new();
415 g_signal_connect(G_OBJECT(ri.nbook), "switch-page", G_CALLBACK(evt_nbook_switchpage), &ri);
416
417 /* Build the 'Simple' mode's GUI. */
418 ri.r_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
419 label = gtk_label_new(_("Look for substring in all filenames, and replace\nit with another string."));
420 gtk_box_pack_start(GTK_BOX(ri.r_vbox), label, FALSE, FALSE, 0);
421 grid = gtk_grid_new();
422 label = gtk_label_new(_("Replace"));
423 gtk_grid_attach(GTK_GRID(grid), label, 0, 0, 1, 1);
424 ri.r_from = gui_dialog_entry_new();
425 gtk_widget_set_hexpand(ri.r_from, TRUE);
426 gtk_widget_set_halign(ri.r_from, GTK_ALIGN_FILL);
427 gtk_grid_attach(GTK_GRID(grid), ri.r_from, 1, 0, 1, 1);
428 label = gtk_label_new(_("With"));
429 gtk_grid_attach(GTK_GRID(grid), label, 0, 1, 1, 1);
430 ri.r_to = gui_dialog_entry_new();
431 gtk_grid_attach(GTK_GRID(grid), ri.r_to, 1, 1, 1, 1);
432 gtk_box_pack_start(GTK_BOX(ri.r_vbox), grid, FALSE, FALSE, 0);
433 grid = gtk_grid_new();
434 ri.r_cnocase = gtk_check_button_new_with_label(_("Ignore Case?"));
435 gtk_widget_set_hexpand(ri.r_cnocase, TRUE);
436 gtk_widget_set_halign(ri.r_cnocase, GTK_ALIGN_FILL);
437 gtk_grid_attach(GTK_GRID(grid), ri.r_cnocase, 0, 0, 1, 1);
438 ri.r_cglobal = gtk_check_button_new_with_label(_("Replace All?"));
439 gtk_grid_attach(GTK_GRID(grid), ri.r_cglobal, 1, 0, 1, 1);
440 gtk_box_pack_start(GTK_BOX(ri.r_vbox), grid, FALSE, FALSE, 0);
441
442 gtk_notebook_append_page(GTK_NOTEBOOK(ri.nbook), ri.r_vbox, gtk_label_new(_("Simple")));
443
444 /* Build the 'Reg Exp' mode's GUI. */
445 ri.re_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
446 label = gtk_label_new( _("Execute the 'From' RE on each filename, storing\n"
447 "parenthesised subexpression matches. Then replace\n"
448 "any occurance of $n in 'To', where n is the index\n"
449 "(counting from 1) of a subexpression, with the text\n"
450 "that matched, and use the result as a new filename."));
451 gtk_box_pack_start(GTK_BOX(ri.re_vbox), label, FALSE, FALSE, 0);
452 grid = gtk_grid_new();
453 label = gtk_label_new(_("From"));
454 gtk_grid_attach(GTK_GRID(grid), label, 0, 0, 1, 1);
455 ri.re_from = gui_dialog_entry_new();
456 gtk_widget_set_hexpand(ri.re_from, TRUE);
457 gtk_widget_set_halign(ri.re_from, GTK_ALIGN_FILL);
458 gtk_grid_attach(GTK_GRID(grid), ri.re_from, 1, 0, 1, 1);
459 label = gtk_label_new(_("To"));
460 gtk_grid_attach(GTK_GRID(grid), label, 0, 1, 1, 1);
461 ri.re_to = gui_dialog_entry_new();
462 gtk_grid_attach(GTK_GRID(grid), ri.re_to, 1, 1, 1, 1);
463 gtk_box_pack_start(GTK_BOX(ri.re_vbox), grid, FALSE, FALSE, 0);
464 ri.re_cnocase = gtk_check_button_new_with_label(_("Ignore Case?"));
465 gtk_box_pack_start(GTK_BOX(ri.re_vbox), ri.re_cnocase, FALSE, FALSE, 0);
466
467 gtk_notebook_append_page(GTK_NOTEBOOK(ri.nbook), ri.re_vbox, gtk_label_new(_("Reg Exp")));
468 gtk_widget_show_all(ri.re_vbox);
469
470 /* Build the 'Map' mode's GUI. */
471 ri.map_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
472 label = gtk_label_new(_("Look for each character in the 'From' string, and replace\n"
473 "any hits with the corresponding character in the 'To' string.\n"
474 "Then, any characters in the 'Remove' string are removed from\n"
475 "the filename, and the result used as the new name for each file."));
476 gtk_box_pack_start(GTK_BOX(ri.map_vbox), label, FALSE, FALSE, 0);
477 grid = gtk_grid_new();
478 label = gtk_label_new(_("From"));
479 gtk_grid_attach(GTK_GRID(grid), label, 0, 0, 1, 1);
480 ri.map_from = gui_dialog_entry_new();
481 gtk_widget_set_hexpand(ri.map_from, TRUE);
482 gtk_widget_set_halign(ri.map_from, GTK_ALIGN_FILL);
483 gtk_grid_attach(GTK_GRID(grid), ri.map_from, 1, 0, 1, 1);
484 label = gtk_label_new(_("To"));
485 gtk_grid_attach(GTK_GRID(grid), label, 0, 1, 1, 1);
486 ri.map_to = gui_dialog_entry_new();
487 gtk_grid_attach(GTK_GRID(grid), ri.map_to, 1, 1, 1, 1);
488 label = gtk_label_new(_("Remove"));
489 gtk_grid_attach(GTK_GRID(grid), label, 0, 2, 1, 1);
490 ri.map_remove = gui_dialog_entry_new();
491 gtk_grid_attach(GTK_GRID(grid), ri.map_remove, 1, 2, 1, 1);
492 gtk_box_pack_start(GTK_BOX(ri.map_vbox), grid, FALSE, FALSE, 0);
493 gtk_notebook_append_page(GTK_NOTEBOOK(ri.nbook), ri.map_vbox, gtk_label_new(_("Map")));
494
495 /* Build the 'Case' mode's GUI. */
496 ri.case_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
497 label = gtk_label_new(_("Changes the case (upper/lower) of the characters\nin the selected filename(s)."));
498 gtk_box_pack_start(GTK_BOX(ri.case_vbox), label, FALSE, FALSE, 0);
499 ri.case_lower = gtk_radio_button_new_with_label(NULL, _("Lower Case?"));
500 gtk_box_pack_start(GTK_BOX(ri.case_vbox), ri.case_lower, FALSE, FALSE, 0);
501 ri.case_upper = gtk_radio_button_new_with_label(gtk_radio_button_get_group(GTK_RADIO_BUTTON(ri.case_lower)), _("Upper Case?"));
502 gtk_box_pack_start(GTK_BOX(ri.case_vbox), ri.case_upper, FALSE, FALSE, 0);
503 ri.case_initial = gtk_radio_button_new_with_label(gtk_radio_button_get_group(GTK_RADIO_BUTTON(ri.case_lower)), _("Upper Case Initial?"));
504 gtk_box_pack_start(GTK_BOX(ri.case_vbox), ri.case_initial, FALSE, FALSE, 0);
505 gtk_widget_show_all(ri.map_vbox);
506 gtk_notebook_append_page(GTK_NOTEBOOK(ri.nbook), ri.case_vbox, gtk_label_new(_("Case")));
507
508 ri.dlg = dlg_dialog_sync_new(ri.nbook, _("RenameRE"), NULL);
509
510 ri.page = 0;
511 }
512 gtk_notebook_set_current_page(GTK_NOTEBOOK(ri.nbook), ri.page);
513 gtk_widget_grab_focus(ri.page ? ri.re_from : ri.r_from);
514
515 if(dlg_dialog_sync_wait(ri.dlg) == DLG_POSITIVE)
516 {
517 GError *err = NULL;
518
519 if(ri.page == 0)
520 ok = rename_simple(&ri, &err);
521 else if(ri.page == 1)
522 ok = rename_regexp(&ri, &err);
523 else if(ri.page == 2)
524 ok = rename_map(&ri, &err);
525 else
526 ok = rename_case(&ri, &err);
527 if(ok)
528 dp_rescan_post_cmd(ri.src);
529 }
530 dp_free_selection(ri.selection);
531 ri.selection = NULL;
532
533 return ok;
534 }
535