File copy/move: make ETA accurate.
[midnight-commander.git] / src / viewer / actions_cmd.c
blob265224695ebb4cf042232e3a6cf52b436d2584c7
1 /*
2 Internal file viewer for the Midnight Commander
3 Callback function for some actions (hotkeys, menu)
5 Copyright (C) 1994-2024
6 Free Software Foundation, Inc.
8 Written by:
9 Miguel de Icaza, 1994, 1995, 1998
10 Janne Kukonlehto, 1994, 1995
11 Jakub Jelinek, 1995
12 Joseph M. Hinkle, 1996
13 Norbert Warmuth, 1997
14 Pavel Machek, 1998
15 Roland Illig <roland.illig@gmx.de>, 2004, 2005
16 Slava Zanko <slavazanko@google.com>, 2009, 2013
17 Andrew Borodin <aborodin@vmail.ru>, 2009-2022
18 Ilia Maslakov <il.smind@gmail.com>, 2009
20 This file is part of the Midnight Commander.
22 The Midnight Commander is free software: you can redistribute it
23 and/or modify it under the terms of the GNU General Public License as
24 published by the Free Software Foundation, either version 3 of the License,
25 or (at your option) any later version.
27 The Midnight Commander is distributed in the hope that it will be useful,
28 but WITHOUT ANY WARRANTY; without even the implied warranty of
29 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 GNU General Public License for more details.
32 You should have received a copy of the GNU General Public License
33 along with this program. If not, see <http://www.gnu.org/licenses/>.
37 The functions in this section can be bound to hotkeys. They are all
38 of the same type (taking a pointer to WView as parameter and
39 returning void). TODO: In the not-too-distant future, these commands
40 will become fully configurable, like they already are in the
41 internal editor. By convention, all the function names end in
42 "_cmd".
45 #include <config.h>
47 #include <stdlib.h>
49 #include "lib/global.h"
51 #include "lib/tty/tty.h"
52 #include "lib/tty/key.h" /* is_idle() */
53 #include "lib/lock.h" /* lock_file() */
54 #include "lib/file-entry.h"
55 #include "lib/widget.h"
56 #ifdef HAVE_CHARSET
57 #include "lib/charsets.h"
58 #endif
59 #include "lib/event.h" /* mc_event_raise() */
60 #include "lib/mcconfig.h" /* mc_config_history_get_recent_item() */
62 #include "src/filemanager/layout.h"
63 #include "src/filemanager/filemanager.h" /* current_panel */
64 #include "src/filemanager/ext.h" /* regex_command_for() */
66 #include "src/history.h" /* MC_HISTORY_SHARED_SEARCH */
67 #include "src/file_history.h" /* show_file_history() */
68 #include "src/execute.h"
69 #include "src/keymap.h"
71 #include "internal.h"
73 /*** global variables ****************************************************************************/
75 /*** file scope macro definitions ****************************************************************/
77 /*** file scope type declarations ****************************************************************/
79 /*** forward declarations (file scope functions) *************************************************/
81 /*** file scope variables ************************************************************************/
83 /* --------------------------------------------------------------------------------------------- */
84 /*** file scope functions ************************************************************************/
85 /* --------------------------------------------------------------------------------------------- */
87 static void
88 mcview_remove_ext_script (WView *view)
90 if (view->ext_script != NULL)
92 mc_unlink (view->ext_script);
93 vfs_path_free (view->ext_script, TRUE);
94 view->ext_script = NULL;
98 /* --------------------------------------------------------------------------------------------- */
100 /* Both views */
101 static void
102 mcview_search (WView *view, gboolean start_search)
104 off_t want_search_start = view->search_start;
106 if (start_search)
108 if (mcview_dialog_search (view))
110 if (view->mode_flags.hex)
111 want_search_start = view->hex_cursor;
113 mcview_do_search (view, want_search_start);
116 else
118 if (view->mode_flags.hex)
120 if (!mcview_search_options.backwards)
121 want_search_start = view->hex_cursor + 1;
122 else if (view->hex_cursor > 0)
123 want_search_start = view->hex_cursor - 1;
124 else
125 want_search_start = 0;
128 mcview_do_search (view, want_search_start);
132 /* --------------------------------------------------------------------------------------------- */
134 static void
135 mcview_continue_search_cmd (WView *view)
137 if (view->last_search_string != NULL)
138 mcview_search (view, FALSE);
139 else
141 /* find last search string in history */
142 char *s;
144 s = mc_config_history_get_recent_item (MC_HISTORY_SHARED_SEARCH);
145 if (s != NULL)
147 view->last_search_string = s;
149 if (mcview_search_init (view))
151 mcview_search (view, FALSE);
152 return;
155 /* found, but cannot init search */
156 MC_PTR_FREE (view->last_search_string);
159 /* if not... then ask for an expression */
160 mcview_search (view, TRUE);
164 /* --------------------------------------------------------------------------------------------- */
166 static void
167 mcview_hook (void *v)
169 WView *view = (WView *) v;
170 WPanel *panel;
171 const file_entry_t *fe;
173 /* If the user is busy typing, wait until he finishes to update the
174 screen */
175 if (!is_idle ())
177 if (!hook_present (idle_hook, mcview_hook))
178 add_hook (&idle_hook, mcview_hook, v);
179 return;
182 delete_hook (&idle_hook, mcview_hook);
184 if (get_current_type () == view_listing)
185 panel = current_panel;
186 else if (get_other_type () == view_listing)
187 panel = other_panel;
188 else
189 return;
191 fe = panel_current_entry (panel);
192 if (fe == NULL)
193 return;
195 mcview_done (view);
196 mcview_init (view);
197 mcview_load (view, 0, fe->fname->str, 0, 0, 0);
198 mcview_display (view);
201 /* --------------------------------------------------------------------------------------------- */
203 static cb_ret_t
204 mcview_handle_editkey (WView *view, int key)
206 struct hexedit_change_node *node;
207 int byte_val = -1;
209 /* Has there been a change at this position? */
210 node = view->change_list;
211 while ((node != NULL) && (node->offset != view->hex_cursor))
212 node = node->next;
214 if (!view->hexview_in_text)
216 /* Hex editing */
217 unsigned int hexvalue = 0;
219 if (key >= '0' && key <= '9')
220 hexvalue = 0 + (key - '0');
221 else if (key >= 'A' && key <= 'F')
222 hexvalue = 10 + (key - 'A');
223 else if (key >= 'a' && key <= 'f')
224 hexvalue = 10 + (key - 'a');
225 else
226 return MSG_NOT_HANDLED;
228 if (node != NULL)
229 byte_val = node->value;
230 else
231 mcview_get_byte (view, view->hex_cursor, &byte_val);
233 if (view->hexedit_lownibble)
234 byte_val = (byte_val & 0xf0) | (hexvalue);
235 else
236 byte_val = (byte_val & 0x0f) | (hexvalue << 4);
238 else
240 /* Text editing */
241 if (key < 256 && key != '\t')
242 byte_val = key;
243 else
244 return MSG_NOT_HANDLED;
247 if ((view->filename_vpath != NULL)
248 && (*(vfs_path_get_last_path_str (view->filename_vpath)) != '\0')
249 && (view->change_list == NULL))
250 view->locked = lock_file (view->filename_vpath);
252 if (node == NULL)
254 node = g_new (struct hexedit_change_node, 1);
255 node->offset = view->hex_cursor;
256 node->value = byte_val;
257 mcview_enqueue_change (&view->change_list, node);
259 else
260 node->value = byte_val;
262 view->dirty++;
263 mcview_move_right (view, 1);
265 return MSG_HANDLED;
268 /* --------------------------------------------------------------------------------------------- */
270 static void
271 mcview_load_next_prev_init (WView *view)
273 if (mc_global.mc_run_mode != MC_RUN_VIEWER)
275 /* get file list from current panel. Update it each time */
276 view->dir = &current_panel->dir;
277 view->dir_idx = &current_panel->current;
279 else if (view->dir == NULL)
281 /* Run from command line */
282 /* Run 1st time. Load/get directory */
284 /* TODO: check mtime of directory to reload it */
286 dir_sort_options_t sort_op = { FALSE, TRUE, FALSE };
288 /* load directory where requested file is */
289 view->dir = g_new0 (dir_list, 1);
290 view->dir_idx = g_new (int, 1);
292 if (dir_list_load
293 (view->dir, view->workdir_vpath, (GCompareFunc) sort_name, &sort_op, NULL))
295 const char *fname;
296 size_t fname_len;
297 int i;
299 fname = x_basename (vfs_path_as_str (view->filename_vpath));
300 fname_len = strlen (fname);
302 /* search current file in the list */
303 for (i = 0; i != view->dir->len; i++)
305 const file_entry_t *fe = &view->dir->list[i];
307 if (fname_len == fe->fname->len && strncmp (fname, fe->fname->str, fname_len) == 0)
308 break;
311 *view->dir_idx = i;
313 else
315 message (D_ERROR, MSG_ERROR, _("Cannot read directory contents"));
316 MC_PTR_FREE (view->dir);
317 MC_PTR_FREE (view->dir_idx);
322 /* --------------------------------------------------------------------------------------------- */
324 static void
325 mcview_scan_for_file (WView *view, int direction)
327 int i;
329 for (i = *view->dir_idx + direction; i != *view->dir_idx; i += direction)
331 if (i < 0)
332 i = view->dir->len - 1;
333 if (i == view->dir->len)
334 i = 0;
335 if (!S_ISDIR (view->dir->list[i].st.st_mode))
336 break;
339 *view->dir_idx = i;
342 /* --------------------------------------------------------------------------------------------- */
344 static void
345 mcview_load_next_prev (WView *view, int direction)
347 dir_list *dir;
348 int *dir_idx;
349 vfs_path_t *vfile;
350 vfs_path_t *ext_script = NULL;
352 mcview_load_next_prev_init (view);
353 mcview_scan_for_file (view, direction);
355 /* reinit view */
356 dir = view->dir;
357 dir_idx = view->dir_idx;
358 view->dir = NULL;
359 view->dir_idx = NULL;
360 vfile =
361 vfs_path_append_new (view->workdir_vpath, dir->list[*dir_idx].fname->str, (char *) NULL);
362 mcview_done (view);
363 mcview_remove_ext_script (view);
364 mcview_init (view);
365 if (regex_command_for (view, vfile, "View", &ext_script) == 0)
366 mcview_load (view, NULL, vfs_path_as_str (vfile), 0, 0, 0);
367 vfs_path_free (vfile, TRUE);
368 view->dir = dir;
369 view->dir_idx = dir_idx;
370 view->ext_script = ext_script;
372 view->dpy_bbar_dirty = FALSE; /* FIXME */
373 view->dirty++;
376 /* --------------------------------------------------------------------------------------------- */
378 static void
379 mcview_load_file_from_history (WView *view)
381 char *filename;
382 int action;
384 filename = show_file_history (CONST_WIDGET (view), &action);
386 if (filename != NULL && (action == CK_View || action == CK_Enter))
388 mcview_done (view);
389 mcview_init (view);
391 mcview_load (view, NULL, filename, 0, 0, 0);
393 view->dpy_bbar_dirty = FALSE; /* FIXME */
394 view->dirty++;
397 g_free (filename);
400 /* --------------------------------------------------------------------------------------------- */
402 static cb_ret_t
403 mcview_execute_cmd (WView *view, long command)
405 int res = MSG_HANDLED;
407 switch (command)
409 case CK_HexMode:
410 /* Toggle between hex view and text view */
411 mcview_toggle_hex_mode (view);
412 break;
413 case CK_HexEditMode:
414 /* Toggle between hexview and hexedit mode */
415 mcview_toggle_hexedit_mode (view);
416 break;
417 case CK_ToggleNavigation:
418 view->hexview_in_text = !view->hexview_in_text;
419 view->dirty++;
420 break;
421 case CK_LeftQuick:
422 if (!view->mode_flags.hex)
423 mcview_move_left (view, 10);
424 break;
425 case CK_RightQuick:
426 if (!view->mode_flags.hex)
427 mcview_move_right (view, 10);
428 break;
429 case CK_Goto:
431 off_t addr;
433 if (mcview_dialog_goto (view, &addr))
435 if (addr >= 0)
436 mcview_moveto_offset (view, addr);
437 else
439 message (D_ERROR, _("Warning"), "%s", _("Invalid value"));
440 view->dirty++;
443 break;
445 case CK_Save:
446 mcview_hexedit_save_changes (view);
447 break;
448 case CK_Search:
449 mcview_search (view, TRUE);
450 break;
451 case CK_SearchContinue:
452 mcview_continue_search_cmd (view);
453 break;
454 case CK_SearchForward:
455 mcview_search_options.backwards = FALSE;
456 mcview_search (view, TRUE);
457 break;
458 case CK_SearchForwardContinue:
459 mcview_search_options.backwards = FALSE;
460 mcview_continue_search_cmd (view);
461 break;
462 case CK_SearchBackward:
463 mcview_search_options.backwards = TRUE;
464 mcview_search (view, TRUE);
465 break;
466 case CK_SearchBackwardContinue:
467 mcview_search_options.backwards = TRUE;
468 mcview_continue_search_cmd (view);
469 break;
470 case CK_SearchOppositeContinue:
472 gboolean direction;
474 direction = mcview_search_options.backwards;
475 mcview_search_options.backwards = !direction;
476 mcview_continue_search_cmd (view);
477 mcview_search_options.backwards = direction;
479 break;
480 case CK_WrapMode:
481 /* Toggle between wrapped and unwrapped view */
482 mcview_toggle_wrap_mode (view);
483 break;
484 case CK_MagicMode:
485 mcview_toggle_magic_mode (view);
486 break;
487 case CK_NroffMode:
488 mcview_toggle_nroff_mode (view);
489 break;
490 case CK_Home:
491 mcview_moveto_bol (view);
492 break;
493 case CK_End:
494 mcview_moveto_eol (view);
495 break;
496 case CK_Left:
497 mcview_move_left (view, 1);
498 break;
499 case CK_Right:
500 mcview_move_right (view, 1);
501 break;
502 case CK_Up:
503 mcview_move_up (view, 1);
504 break;
505 case CK_Down:
506 mcview_move_down (view, 1);
507 break;
508 case CK_HalfPageUp:
509 mcview_move_up (view, (view->data_area.lines + 1) / 2);
510 break;
511 case CK_HalfPageDown:
512 mcview_move_down (view, (view->data_area.lines + 1) / 2);
513 break;
514 case CK_PageUp:
515 mcview_move_up (view, view->data_area.lines);
516 break;
517 case CK_PageDown:
518 mcview_move_down (view, view->data_area.lines);
519 break;
520 case CK_Top:
521 mcview_moveto_top (view);
522 break;
523 case CK_Bottom:
524 mcview_moveto_bottom (view);
525 break;
526 case CK_Shell:
527 toggle_subshell ();
528 break;
529 case CK_Ruler:
530 mcview_display_toggle_ruler (view);
531 break;
532 case CK_Bookmark:
533 view->dpy_start = view->marks[view->marker];
534 view->dpy_paragraph_skip_lines = 0; /* TODO: remember this value in the marker? */
535 view->dpy_wrap_dirty = TRUE;
536 view->dirty++;
537 break;
538 case CK_BookmarkGoto:
539 view->marks[view->marker] = view->dpy_start;
540 break;
541 #ifdef HAVE_CHARSET
542 case CK_SelectCodepage:
543 mcview_select_encoding (view);
544 view->dirty++;
545 break;
546 #endif
547 case CK_FileNext:
548 case CK_FilePrev:
549 /* Does not work in panel mode */
550 if (!mcview_is_in_panel (view))
551 mcview_load_next_prev (view, command == CK_FileNext ? 1 : -1);
552 break;
553 case CK_History:
554 mcview_load_file_from_history (view);
555 break;
556 case CK_Quit:
557 if (!mcview_is_in_panel (view))
558 dlg_close (DIALOG (WIDGET (view)->owner));
559 break;
560 case CK_Cancel:
561 /* don't close viewer due to SIGINT */
562 break;
563 default:
564 res = MSG_NOT_HANDLED;
566 return res;
569 /* --------------------------------------------------------------------------------------------- */
571 static long
572 mcview_lookup_key (WView *view, int key)
574 if (view->mode_flags.hex)
575 return keybind_lookup_keymap_command (view->hex_keymap, key);
577 return widget_lookup_key (WIDGET (view), key);
580 /* --------------------------------------------------------------------------------------------- */
581 /** Both views */
582 static cb_ret_t
583 mcview_handle_key (WView *view, int key)
585 long command;
587 #ifdef HAVE_CHARSET
588 key = convert_from_input_c (key);
589 #endif
591 if (view->hexedit_mode && view->mode_flags.hex
592 && mcview_handle_editkey (view, key) == MSG_HANDLED)
593 return MSG_HANDLED;
595 command = mcview_lookup_key (view, key);
596 if (command != CK_IgnoreKey && mcview_execute_cmd (view, command) == MSG_HANDLED)
597 return MSG_HANDLED;
599 #ifdef MC_ENABLE_DEBUGGING_CODE
600 if (key == 't')
601 { /* mnemonic: "test" */
602 mcview_ccache_dump (view);
603 return MSG_HANDLED;
605 #endif
606 if (key >= '0' && key <= '9')
607 view->marker = key - '0';
609 /* Key not used */
610 return MSG_NOT_HANDLED;
614 /* --------------------------------------------------------------------------------------------- */
616 static inline void
617 mcview_resize (WView *view)
619 view->dpy_wrap_dirty = TRUE;
620 mcview_compute_areas (view);
621 mcview_update_bytes_per_line (view);
624 /* --------------------------------------------------------------------------------------------- */
626 static gboolean
627 mcview_ok_to_quit (WView *view)
629 int r;
631 if (view->change_list == NULL)
632 return TRUE;
634 if (!mc_global.midnight_shutdown)
636 query_set_sel (2);
637 r = query_dialog (_("Quit"),
638 _("File was modified. Save with exit?"), D_NORMAL, 3,
639 _("&Yes"), _("&No"), _("&Cancel quit"));
641 else
643 r = query_dialog (_("Quit"),
644 _("Midnight Commander is being shut down.\nSave modified file?"),
645 D_NORMAL, 2, _("&Yes"), _("&No"));
646 /* Esc is No */
647 if (r == -1)
648 r = 1;
651 switch (r)
653 case 0: /* Yes */
654 return mcview_hexedit_save_changes (view) || mc_global.midnight_shutdown;
655 case 1: /* No */
656 mcview_hexedit_free_change_list (view);
657 return TRUE;
658 default:
659 return FALSE;
663 /* --------------------------------------------------------------------------------------------- */
664 /*** public functions ****************************************************************************/
665 /* --------------------------------------------------------------------------------------------- */
667 cb_ret_t
668 mcview_callback (Widget *w, Widget *sender, widget_msg_t msg, int parm, void *data)
670 WView *view = (WView *) w;
671 cb_ret_t i;
673 mcview_compute_areas (view);
674 mcview_update_bytes_per_line (view);
676 switch (msg)
678 case MSG_INIT:
679 if (mcview_is_in_panel (view))
680 add_hook (&select_file_hook, mcview_hook, view);
681 else
682 view->dpy_bbar_dirty = TRUE;
683 return MSG_HANDLED;
685 case MSG_DRAW:
686 mcview_display (view);
687 return MSG_HANDLED;
689 case MSG_CURSOR:
690 if (view->mode_flags.hex)
691 mcview_place_cursor (view);
692 return MSG_HANDLED;
694 case MSG_KEY:
695 i = mcview_handle_key (view, parm);
696 mcview_update (view);
697 return i;
699 case MSG_ACTION:
700 i = mcview_execute_cmd (view, parm);
701 mcview_update (view);
702 return i;
704 case MSG_FOCUS:
705 view->dpy_bbar_dirty = TRUE;
706 /* TODO: get rid of draw here before MSG_DRAW */
707 mcview_update (view);
708 return MSG_HANDLED;
710 case MSG_RESIZE:
711 widget_default_callback (w, NULL, MSG_RESIZE, 0, data);
712 mcview_resize (view);
713 return MSG_HANDLED;
715 case MSG_DESTROY:
716 if (mcview_is_in_panel (view))
718 delete_hook (&select_file_hook, mcview_hook);
721 * In some cases when mc startup is very slow and one panel is in quick view mode,
722 * @view is registered in two hook lists at the same time:
723 * mcview_callback (MSG_INIT) -> add_hook (&select_file_hook)
724 * mcview_hook () -> add_hook (&idle_hook).
725 * If initialization of file manager is not completed yet, but user switches
726 * panel mode from qick view to another one (by pressing C-x q), the following
727 * occurs:
728 * view hook is deleted from select_file_hook list via following call chain:
729 * create_panel (view_listing) -> widget_replace () ->
730 * send_message (MSG_DESTROY) -> mcview_callback (MSG_DESTROY) ->
731 * delete_hook (&select_file_hook);
732 * @view object is free'd:
733 * create_panel (view_listing) -> g_free (old_widget);
734 * but @view still is in idle_hook list and tried to be executed:
735 * frontend_dlg_run () -> execute_hooks (idle_hook).
736 * Thus here we have access to free'd @view object. To prevent this, remove view hook
737 * from idle_hook list.
739 delete_hook (&idle_hook, mcview_hook);
741 if (mc_global.midnight_shutdown)
742 mcview_ok_to_quit (view);
744 mcview_done (view);
745 mcview_remove_ext_script (view);
746 return MSG_HANDLED;
748 default:
749 return widget_default_callback (w, sender, msg, parm, data);
753 /* --------------------------------------------------------------------------------------------- */
755 cb_ret_t
756 mcview_dialog_callback (Widget *w, Widget *sender, widget_msg_t msg, int parm, void *data)
758 WDialog *h = DIALOG (w);
759 WView *view;
761 switch (msg)
763 case MSG_ACTION:
764 /* Handle shortcuts. */
766 /* Note: the buttonbar sends messages directly to the the WView, not to
767 * here, which is why we can pass NULL in the following call. */
768 return mcview_execute_cmd (NULL, parm);
770 case MSG_VALIDATE:
771 view = (WView *) widget_find_by_type (w, mcview_callback);
772 /* don't stop the dialog before final decision */
773 widget_set_state (w, WST_ACTIVE, TRUE);
774 if (mcview_ok_to_quit (view))
775 dlg_close (h);
776 else
777 mcview_update (view);
778 return MSG_HANDLED;
780 default:
781 return dlg_default_callback (w, sender, msg, parm, data);
785 /* --------------------------------------------------------------------------------------------- */