1 /*
2 Internal file viewer for the Midnight Commander
3 Callback function for some actions (hotkeys, menu)
4
5 Copyright (C) 1994-2021
6 Free Software Foundation, Inc.
7
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, 2013
18 Ilia Maslakov <il.smind@gmail.com>, 2009
19
20 This file is part of the Midnight Commander.
21
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.
26
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.
31
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/>.
34 */
35
36 /*
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".
43 */
44
45 #include <config.h>
46
47 #include <errno.h>
48 #include <stdlib.h>
49
50 #include "lib/global.h"
51
52 #include "lib/tty/tty.h"
53 #include "lib/tty/key.h" /* is_idle() */
54 #include "lib/lock.h" /* lock_file() */
55 #include "lib/util.h"
56 #include "lib/widget.h"
57 #ifdef HAVE_CHARSET
58 #include "lib/charsets.h"
59 #endif
60 #include "lib/event.h" /* mc_event_raise() */
61 #include "lib/mcconfig.h" /* mc_config_history_get() */
62
63 #include "src/filemanager/layout.h"
64 #include "src/filemanager/filemanager.h" /* current_panel */
65 #include "src/filemanager/ext.h" /* regex_command_for() */
66
67 #include "src/history.h"
68 #include "src/file_history.h" /* show_file_history() */
69 #include "src/execute.h"
70 #include "src/keymap.h"
71
72 #include "internal.h"
73
74 /*** global variables ****************************************************************************/
75
76 /*** file scope macro definitions ****************************************************************/
77
78 /*** file scope type declarations ****************************************************************/
79
80 /*** file scope variables ************************************************************************/
81
82 /* --------------------------------------------------------------------------------------------- */
83 /*** file scope functions ************************************************************************/
84 /* --------------------------------------------------------------------------------------------- */
85
86 static void
mcview_remove_ext_script(WView * view)87 mcview_remove_ext_script (WView * view)
88 {
89 if (view->ext_script != NULL)
90 {
91 mc_unlink (view->ext_script);
92 vfs_path_free (view->ext_script, TRUE);
93 view->ext_script = NULL;
94 }
95 }
96
97 /* --------------------------------------------------------------------------------------------- */
98
99 /* Both views */
100 static void
mcview_search(WView * view,gboolean start_search)101 mcview_search (WView * view, gboolean start_search)
102 {
103 off_t want_search_start = view->search_start;
104
105 if (start_search)
106 {
107 if (mcview_dialog_search (view))
108 {
109 if (view->mode_flags.hex)
110 want_search_start = view->hex_cursor;
111
112 mcview_do_search (view, want_search_start);
113 }
114 }
115 else
116 {
117 if (view->mode_flags.hex)
118 {
119 if (!mcview_search_options.backwards)
120 want_search_start = view->hex_cursor + 1;
121 else if (view->hex_cursor > 0)
122 want_search_start = view->hex_cursor - 1;
123 else
124 want_search_start = 0;
125 }
126
127 mcview_do_search (view, want_search_start);
128 }
129 }
130
131 /* --------------------------------------------------------------------------------------------- */
132
133 static void
mcview_continue_search_cmd(WView * view)134 mcview_continue_search_cmd (WView * view)
135 {
136 if (view->last_search_string != NULL)
137 mcview_search (view, FALSE);
138 else
139 {
140 /* find last search string in history */
141 GList *history;
142
143 history = mc_config_history_get (MC_HISTORY_SHARED_SEARCH);
144 if (history != NULL)
145 {
146 /* FIXME: is it possible that history->data == NULL? */
147 view->last_search_string = (gchar *) history->data;
148 history->data = NULL;
149 history = g_list_first (history);
150 g_list_free_full (history, g_free);
151
152 if (mcview_search_init (view))
153 {
154 mcview_search (view, FALSE);
155 return;
156 }
157
158 /* found, but cannot init search */
159 MC_PTR_FREE (view->last_search_string);
160 }
161
162 /* if not... then ask for an expression */
163 mcview_search (view, TRUE);
164 }
165 }
166
167 /* --------------------------------------------------------------------------------------------- */
168
169 static void
mcview_hook(void * v)170 mcview_hook (void *v)
171 {
172 WView *view = (WView *) v;
173 WPanel *panel;
174
175 /* If the user is busy typing, wait until he finishes to update the
176 screen */
177 if (!is_idle ())
178 {
179 if (!hook_present (idle_hook, mcview_hook))
180 add_hook (&idle_hook, mcview_hook, v);
181 return;
182 }
183
184 delete_hook (&idle_hook, mcview_hook);
185
186 if (get_current_type () == view_listing)
187 panel = current_panel;
188 else if (get_other_type () == view_listing)
189 panel = other_panel;
190 else
191 return;
192
193 mcview_done (view);
194 mcview_init (view);
195 mcview_load (view, 0, panel->dir.list[panel->selected].fname->str, 0, 0, 0);
196 mcview_display (view);
197 }
198
199 /* --------------------------------------------------------------------------------------------- */
200
201 static cb_ret_t
mcview_handle_editkey(WView * view,int key)202 mcview_handle_editkey (WView * view, int key)
203 {
204 struct hexedit_change_node *node;
205 int byte_val = -1;
206
207 /* Has there been a change at this position? */
208 node = view->change_list;
209 while ((node != NULL) && (node->offset != view->hex_cursor))
210 node = node->next;
211
212 if (!view->hexview_in_text)
213 {
214 /* Hex editing */
215 unsigned int hexvalue = 0;
216
217 if (key >= '0' && key <= '9')
218 hexvalue = 0 + (key - '0');
219 else if (key >= 'A' && key <= 'F')
220 hexvalue = 10 + (key - 'A');
221 else if (key >= 'a' && key <= 'f')
222 hexvalue = 10 + (key - 'a');
223 else
224 return MSG_NOT_HANDLED;
225
226 if (node != NULL)
227 byte_val = node->value;
228 else
229 mcview_get_byte (view, view->hex_cursor, &byte_val);
230
231 if (view->hexedit_lownibble)
232 byte_val = (byte_val & 0xf0) | (hexvalue);
233 else
234 byte_val = (byte_val & 0x0f) | (hexvalue << 4);
235 }
236 else
237 {
238 /* Text editing */
239 if (key < 256 && key != '\t')
240 byte_val = key;
241 else
242 return MSG_NOT_HANDLED;
243 }
244
245 if ((view->filename_vpath != NULL)
246 && (*(vfs_path_get_last_path_str (view->filename_vpath)) != '\0')
247 && (view->change_list == NULL))
248 view->locked = lock_file (view->filename_vpath);
249
250 if (node == NULL)
251 {
252 node = g_new (struct hexedit_change_node, 1);
253 node->offset = view->hex_cursor;
254 node->value = byte_val;
255 mcview_enqueue_change (&view->change_list, node);
256 }
257 else
258 node->value = byte_val;
259
260 view->dirty++;
261 mcview_move_right (view, 1);
262
263 return MSG_HANDLED;
264 }
265
266 /* --------------------------------------------------------------------------------------------- */
267
268 static void
mcview_load_next_prev_init(WView * view)269 mcview_load_next_prev_init (WView * view)
270 {
271 if (mc_global.mc_run_mode != MC_RUN_VIEWER)
272 {
273 /* get file list from current panel. Update it each time */
274 view->dir = ¤t_panel->dir;
275 view->dir_idx = ¤t_panel->selected;
276 }
277 else if (view->dir == NULL)
278 {
279 /* Run from command line */
280 /* Run 1st time. Load/get directory */
281
282 /* TODO: check mtime of directory to reload it */
283
284 dir_sort_options_t sort_op = { FALSE, TRUE, FALSE };
285
286 /* load directory where requested file is */
287 view->dir = g_new0 (dir_list, 1);
288 view->dir_idx = g_new (int, 1);
289
290 if (dir_list_load
291 (view->dir, view->workdir_vpath, (GCompareFunc) sort_name, &sort_op, NULL))
292 {
293 const char *fname;
294 size_t fname_len;
295 int i;
296
297 fname = x_basename (vfs_path_as_str (view->filename_vpath));
298 fname_len = strlen (fname);
299
300 /* search current file in the list */
301 for (i = 0; i != view->dir->len; i++)
302 {
303 const file_entry_t *fe = &view->dir->list[i];
304
305 if (fname_len == fe->fname->len && strncmp (fname, fe->fname->str, fname_len) == 0)
306 break;
307 }
308
309 *view->dir_idx = i;
310 }
311 else
312 {
313 message (D_ERROR, MSG_ERROR, _("Cannot read directory contents"));
314 MC_PTR_FREE (view->dir);
315 MC_PTR_FREE (view->dir_idx);
316 }
317 }
318 }
319
320 /* --------------------------------------------------------------------------------------------- */
321
322 static void
mcview_scan_for_file(WView * view,int direction)323 mcview_scan_for_file (WView * view, int direction)
324 {
325 int i;
326
327 for (i = *view->dir_idx + direction; i != *view->dir_idx; i += direction)
328 {
329 if (i < 0)
330 i = view->dir->len - 1;
331 if (i == view->dir->len)
332 i = 0;
333 if (!S_ISDIR (view->dir->list[i].st.st_mode))
334 break;
335 }
336
337 *view->dir_idx = i;
338 }
339
340 /* --------------------------------------------------------------------------------------------- */
341
342 static void
mcview_load_next_prev(WView * view,int direction)343 mcview_load_next_prev (WView * view, int direction)
344 {
345 dir_list *dir;
346 int *dir_idx;
347 vfs_path_t *vfile;
348 vfs_path_t *ext_script = NULL;
349
350 mcview_load_next_prev_init (view);
351 mcview_scan_for_file (view, direction);
352
353 /* reinit view */
354 dir = view->dir;
355 dir_idx = view->dir_idx;
356 view->dir = NULL;
357 view->dir_idx = NULL;
358 vfile =
359 vfs_path_append_new (view->workdir_vpath, dir->list[*dir_idx].fname->str, (char *) NULL);
360 mcview_done (view);
361 mcview_remove_ext_script (view);
362 mcview_init (view);
363 if (regex_command_for (view, vfile, "View", &ext_script) == 0)
364 mcview_load (view, NULL, vfs_path_as_str (vfile), 0, 0, 0);
365 vfs_path_free (vfile, TRUE);
366 view->dir = dir;
367 view->dir_idx = dir_idx;
368 view->ext_script = ext_script;
369
370 view->dpy_bbar_dirty = FALSE; /* FIXME */
371 view->dirty++;
372 }
373
374 /* --------------------------------------------------------------------------------------------- */
375
376 static void
mcview_load_file_from_history(WView * view)377 mcview_load_file_from_history (WView * view)
378 {
379 char *filename;
380 int action;
381
382 filename = show_file_history (CONST_WIDGET (view), &action);
383
384 if (filename != NULL && (action == CK_View || action == CK_Enter))
385 {
386 mcview_done (view);
387 mcview_init (view);
388
389 mcview_load (view, NULL, filename, 0, 0, 0);
390
391 view->dpy_bbar_dirty = FALSE; /* FIXME */
392 view->dirty++;
393 }
394
395 g_free (filename);
396 }
397
398 /* --------------------------------------------------------------------------------------------- */
399
400 static cb_ret_t
mcview_execute_cmd(WView * view,long command)401 mcview_execute_cmd (WView * view, long command)
402 {
403 int res = MSG_HANDLED;
404
405 switch (command)
406 {
407 case CK_Help:
408 {
409 ev_help_t event_data = { NULL, "[Internal File Viewer]" };
410 mc_event_raise (MCEVENT_GROUP_CORE, "help", &event_data);
411 }
412 break;
413 case CK_HexMode:
414 /* Toggle between hex view and text view */
415 mcview_toggle_hex_mode (view);
416 break;
417 case CK_HexEditMode:
418 /* Toggle between hexview and hexedit mode */
419 mcview_toggle_hexedit_mode (view);
420 break;
421 case CK_ToggleNavigation:
422 view->hexview_in_text = !view->hexview_in_text;
423 view->dirty++;
424 break;
425 case CK_LeftQuick:
426 if (!view->mode_flags.hex)
427 mcview_move_left (view, 10);
428 break;
429 case CK_RightQuick:
430 if (!view->mode_flags.hex)
431 mcview_move_right (view, 10);
432 break;
433 case CK_Goto:
434 {
435 off_t addr;
436
437 if (mcview_dialog_goto (view, &addr))
438 {
439 if (addr >= 0)
440 mcview_moveto_offset (view, addr);
441 else
442 {
443 message (D_ERROR, _("Warning"), "%s", _("Invalid value"));
444 view->dirty++;
445 }
446 }
447 break;
448 }
449 case CK_Save:
450 mcview_hexedit_save_changes (view);
451 break;
452 case CK_Search:
453 mcview_search (view, TRUE);
454 break;
455 case CK_SearchContinue:
456 mcview_continue_search_cmd (view);
457 break;
458 case CK_SearchForward:
459 mcview_search_options.backwards = FALSE;
460 mcview_search (view, TRUE);
461 break;
462 case CK_SearchForwardContinue:
463 mcview_search_options.backwards = FALSE;
464 mcview_continue_search_cmd (view);
465 break;
466 case CK_SearchBackward:
467 mcview_search_options.backwards = TRUE;
468 mcview_search (view, TRUE);
469 break;
470 case CK_SearchBackwardContinue:
471 mcview_search_options.backwards = TRUE;
472 mcview_continue_search_cmd (view);
473 break;
474 case CK_SearchOppositeContinue:
475 {
476 gboolean direction;
477
478 direction = mcview_search_options.backwards;
479 mcview_search_options.backwards = !direction;
480 mcview_continue_search_cmd (view);
481 mcview_search_options.backwards = direction;
482 }
483 break;
484 case CK_WrapMode:
485 /* Toggle between wrapped and unwrapped view */
486 mcview_toggle_wrap_mode (view);
487 break;
488 case CK_MagicMode:
489 mcview_toggle_magic_mode (view);
490 break;
491 case CK_NroffMode:
492 mcview_toggle_nroff_mode (view);
493 break;
494 case CK_Home:
495 mcview_moveto_bol (view);
496 break;
497 case CK_End:
498 mcview_moveto_eol (view);
499 break;
500 case CK_Left:
501 mcview_move_left (view, 1);
502 break;
503 case CK_Right:
504 mcview_move_right (view, 1);
505 break;
506 case CK_Up:
507 mcview_move_up (view, 1);
508 break;
509 case CK_Down:
510 mcview_move_down (view, 1);
511 break;
512 case CK_HalfPageUp:
513 mcview_move_up (view, (view->data_area.height + 1) / 2);
514 break;
515 case CK_HalfPageDown:
516 mcview_move_down (view, (view->data_area.height + 1) / 2);
517 break;
518 case CK_PageUp:
519 mcview_move_up (view, view->data_area.height);
520 break;
521 case CK_PageDown:
522 mcview_move_down (view, view->data_area.height);
523 break;
524 case CK_Top:
525 mcview_moveto_top (view);
526 break;
527 case CK_Bottom:
528 mcview_moveto_bottom (view);
529 break;
530 case CK_Shell:
531 toggle_subshell ();
532 break;
533 case CK_Ruler:
534 mcview_display_toggle_ruler (view);
535 break;
536 case CK_Bookmark:
537 view->dpy_start = view->marks[view->marker];
538 view->dpy_paragraph_skip_lines = 0; /* TODO: remember this value in the marker? */
539 view->dpy_wrap_dirty = TRUE;
540 view->dirty++;
541 break;
542 case CK_BookmarkGoto:
543 view->marks[view->marker] = view->dpy_start;
544 break;
545 #ifdef HAVE_CHARSET
546 case CK_SelectCodepage:
547 mcview_select_encoding (view);
548 view->dirty++;
549 break;
550 #endif
551 case CK_FileNext:
552 case CK_FilePrev:
553 /* Does not work in panel mode */
554 if (!mcview_is_in_panel (view))
555 mcview_load_next_prev (view, command == CK_FileNext ? 1 : -1);
556 break;
557 case CK_History:
558 mcview_load_file_from_history (view);
559 break;
560 case CK_Quit:
561 if (!mcview_is_in_panel (view))
562 dlg_stop (DIALOG (WIDGET (view)->owner));
563 break;
564 case CK_Cancel:
565 /* don't close viewer due to SIGINT */
566 break;
567 default:
568 res = MSG_NOT_HANDLED;
569 }
570 return res;
571 }
572
573 /* --------------------------------------------------------------------------------------------- */
574
575 static long
mcview_lookup_key(WView * view,int key)576 mcview_lookup_key (WView * view, int key)
577 {
578 if (view->mode_flags.hex)
579 return keybind_lookup_keymap_command (view->hex_keymap, key);
580
581 return widget_lookup_key (WIDGET (view), key);
582 }
583
584 /* --------------------------------------------------------------------------------------------- */
585 /** Both views */
586 static cb_ret_t
mcview_handle_key(WView * view,int key)587 mcview_handle_key (WView * view, int key)
588 {
589 long command;
590
591 #ifdef HAVE_CHARSET
592 key = convert_from_input_c (key);
593 #endif
594
595 if (view->hexedit_mode && view->mode_flags.hex
596 && mcview_handle_editkey (view, key) == MSG_HANDLED)
597 return MSG_HANDLED;
598
599 command = mcview_lookup_key (view, key);
600 if (command != CK_IgnoreKey && mcview_execute_cmd (view, command) == MSG_HANDLED)
601 return MSG_HANDLED;
602
603 #ifdef MC_ENABLE_DEBUGGING_CODE
604 if (c == 't')
605 { /* mnemonic: "test" */
606 mcview_ccache_dump (view);
607 return MSG_HANDLED;
608 }
609 #endif
610 if (key >= '0' && key <= '9')
611 view->marker = key - '0';
612
613 /* Key not used */
614 return MSG_NOT_HANDLED;
615 }
616
617
618 /* --------------------------------------------------------------------------------------------- */
619
620 static inline void
mcview_resize(WView * view)621 mcview_resize (WView * view)
622 {
623 view->dpy_wrap_dirty = TRUE;
624 mcview_compute_areas (view);
625 mcview_update_bytes_per_line (view);
626 }
627
628 /* --------------------------------------------------------------------------------------------- */
629
630 static gboolean
mcview_ok_to_quit(WView * view)631 mcview_ok_to_quit (WView * view)
632 {
633 int r;
634
635 if (view->change_list == NULL)
636 return TRUE;
637
638 if (!mc_global.midnight_shutdown)
639 {
640 query_set_sel (2);
641 r = query_dialog (_("Quit"),
642 _("File was modified. Save with exit?"), D_NORMAL, 3,
643 _("&Yes"), _("&No"), _("&Cancel quit"));
644 }
645 else
646 {
647 r = query_dialog (_("Quit"),
648 _("Midnight Commander is being shut down.\nSave modified file?"),
649 D_NORMAL, 2, _("&Yes"), _("&No"));
650 /* Esc is No */
651 if (r == -1)
652 r = 1;
653 }
654
655 switch (r)
656 {
657 case 0: /* Yes */
658 return mcview_hexedit_save_changes (view) || mc_global.midnight_shutdown;
659 case 1: /* No */
660 mcview_hexedit_free_change_list (view);
661 return TRUE;
662 default:
663 return FALSE;
664 }
665 }
666
667 /* --------------------------------------------------------------------------------------------- */
668 /*** public functions ****************************************************************************/
669 /* --------------------------------------------------------------------------------------------- */
670
671 cb_ret_t
mcview_callback(Widget * w,Widget * sender,widget_msg_t msg,int parm,void * data)672 mcview_callback (Widget * w, Widget * sender, widget_msg_t msg, int parm, void *data)
673 {
674 WView *view = (WView *) w;
675 cb_ret_t i;
676
677 mcview_compute_areas (view);
678 mcview_update_bytes_per_line (view);
679
680 switch (msg)
681 {
682 case MSG_INIT:
683 if (mcview_is_in_panel (view))
684 add_hook (&select_file_hook, mcview_hook, view);
685 else
686 view->dpy_bbar_dirty = TRUE;
687 return MSG_HANDLED;
688
689 case MSG_DRAW:
690 mcview_display (view);
691 return MSG_HANDLED;
692
693 case MSG_CURSOR:
694 if (view->mode_flags.hex)
695 mcview_place_cursor (view);
696 return MSG_HANDLED;
697
698 case MSG_KEY:
699 i = mcview_handle_key (view, parm);
700 mcview_update (view);
701 return i;
702
703 case MSG_ACTION:
704 i = mcview_execute_cmd (view, parm);
705 mcview_update (view);
706 return i;
707
708 case MSG_FOCUS:
709 view->dpy_bbar_dirty = TRUE;
710 /* TODO: get rid of draw here before MSG_DRAW */
711 mcview_update (view);
712 return MSG_HANDLED;
713
714 case MSG_RESIZE:
715 widget_default_callback (w, NULL, MSG_RESIZE, 0, data);
716 mcview_resize (view);
717 return MSG_HANDLED;
718
719 case MSG_DESTROY:
720 if (mcview_is_in_panel (view))
721 {
722 delete_hook (&select_file_hook, mcview_hook);
723
724 /*
725 * In some cases when mc startup is very slow and one panel is in quick vew mode,
726 * @view is registered in two hook lists at the same time:
727 * mcview_callback (MSG_INIT) -> add_hook (&select_file_hook)
728 * mcview_hook () -> add_hook (&idle_hook).
729 * If initialization of file manager is not completed yet, but user switches
730 * panel mode from qick view to another one (by pressing C-x q), the following
731 * occurs:
732 * view hook is deleted from select_file_hook list via following call chain:
733 * create_panel (view_listing) -> widget_replace () ->
734 * send_message (MSG_DESTROY) -> mcview_callback (MSG_DESTROY) ->
735 * delete_hook (&select_file_hook);
736 * @view object is free'd:
737 * create_panel (view_listing) -> g_free (old_widget);
738 * but @view still is in idle_hook list and tried to be executed:
739 * frontend_dlg_run () -> execute_hooks (idle_hook).
740 * Thus here we have access to free'd @view object. To prevent this, remove view hook
741 * from idle_hook list.
742 */
743 delete_hook (&idle_hook, mcview_hook);
744
745 if (mc_global.midnight_shutdown)
746 mcview_ok_to_quit (view);
747 }
748 mcview_done (view);
749 mcview_remove_ext_script (view);
750 return MSG_HANDLED;
751
752 default:
753 return widget_default_callback (w, sender, msg, parm, data);
754 }
755 }
756
757 /* --------------------------------------------------------------------------------------------- */
758
759 cb_ret_t
mcview_dialog_callback(Widget * w,Widget * sender,widget_msg_t msg,int parm,void * data)760 mcview_dialog_callback (Widget * w, Widget * sender, widget_msg_t msg, int parm, void *data)
761 {
762 WDialog *h = DIALOG (w);
763 WView *view;
764
765 switch (msg)
766 {
767 case MSG_ACTION:
768 /* Handle shortcuts. */
769
770 /* Note: the buttonbar sends messages directly to the the WView, not to
771 * here, which is why we can pass NULL in the following call. */
772 return mcview_execute_cmd (NULL, parm);
773
774 case MSG_VALIDATE:
775 view = (WView *) widget_find_by_type (w, mcview_callback);
776 /* don't stop the dialog before final decision */
777 widget_set_state (w, WST_ACTIVE, TRUE);
778 if (mcview_ok_to_quit (view))
779 dlg_stop (h);
780 else
781 mcview_update (view);
782 return MSG_HANDLED;
783
784 default:
785 return dlg_default_callback (w, sender, msg, parm, data);
786 }
787 }
788
789 /* --------------------------------------------------------------------------------------------- */
790