1 /* vifm
2 * Copyright (C) 2001 Ken Steen.
3 * Copyright (C) 2011 xaizek.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
18 */
19
20 #include "running.h"
21
22 #include <curses.h> /* FALSE curs_set() */
23
24 #include <sys/stat.h> /* stat */
25 #ifndef _WIN32
26 #include <sys/wait.h> /* WEXITSTATUS() */
27 #else
28 #define WIN32_LEAN_AND_MEAN
29 #include <windows.h>
30 #include <shellapi.h>
31 #ifndef ERROR_ELEVATION_REQUIRED /* Windows Vista and later. */
32 #define ERROR_ELEVATION_REQUIRED 740L
33 #endif
34 #endif
35 #include <unistd.h> /* pid_t */
36
37 #include <assert.h> /* assert() */
38 #include <errno.h> /* errno */
39 #include <stddef.h> /* NULL size_t */
40 #include <stdio.h> /* snprintf() */
41 #include <stdlib.h> /* EXIT_FAILURE EXIT_SUCCESS free() realloc() */
42 #include <string.h> /* strcmp() strerror() strrchr() strcat() strstr() strlen()
43 strchr() strdup() strncmp() */
44
45 #include "cfg/config.h"
46 #include "cfg/info.h"
47 #include "compat/fs_limits.h"
48 #include "compat/os.h"
49 #include "int/file_magic.h"
50 #include "int/fuse.h"
51 #include "int/path_env.h"
52 #include "int/vim.h"
53 #include "menus/users_menu.h"
54 #include "modes/dialogs/msg_dialog.h"
55 #include "modes/view.h"
56 #include "ui/statusbar.h"
57 #include "ui/quickview.h"
58 #include "ui/ui.h"
59 #include "utils/env.h"
60 #include "utils/fs.h"
61 #include "utils/log.h"
62 #include "utils/path.h"
63 #include "utils/str.h"
64 #include "utils/string_array.h"
65 #include "utils/utils.h"
66 #include "utils/utf8.h"
67 #include "background.h"
68 #include "filelist.h"
69 #include "filetype.h"
70 #include "flist_hist.h"
71 #include "flist_pos.h"
72 #include "flist_sel.h"
73 #include "macros.h"
74 #include "opt_handlers.h"
75 #include "status.h"
76 #include "types.h"
77 #include "vifm.h"
78
79 /* Kinds of symbolic link file treatment on file handling. */
80 typedef enum
81 {
82 FHL_NO_FOLLOW, /* Don't follow (navigate to instead of navigation inside). */
83 FHL_FOLLOW, /* Follow (end up on the link target, not inside it). */
84 FHL_FOLLOW_ALL, /* Follow all the way (end up on final link's target). */
85 }
86 FileHandleLink;
87
88 static void handle_file(view_t *view, FileHandleExec exec,
89 FileHandleLink follow);
90 static int is_runnable(view_t *view, const char full_path[], int type,
91 int force_follow);
92 static int is_executable(const char full_path[], const dir_entry_t *curr,
93 int dont_execute, int runnable);
94 #ifdef _WIN32
95 static void run_win_executable(char full_path[], int elevate);
96 static int run_win_executable_as_evaluated(const char full_path[]);
97 #endif
98 static int selection_is_consistent(view_t *view);
99 static void execute_file(const char full_path[], int elevate);
100 static void run_selection(view_t *view, int dont_execute);
101 static void run_with_defaults(view_t *view);
102 static void run_selection_separately(view_t *view, int dont_execute);
103 static int is_multi_run_compat(view_t *view, const char prog_cmd[]);
104 static void run_explicit_prog(const char prog_spec[], int pause, int force_bg);
105 static void run_implicit_prog(view_t *view, const char prog_spec[], int pause,
106 int force_bg);
107 static void view_current_file(const view_t *view);
108 static void follow_link(view_t *view, int follow_dirs, int ultimate);
109 static void enter_dir(struct view_t *view);
110 static int cd_to_parent_dir(view_t *view);
111 static void extract_last_path_component(const char path[], char buf[]);
112 static void setup_shellout_env(void);
113 static void cleanup_shellout_env(void);
114 static char * gen_shell_cmd(const char cmd[], int pause,
115 int use_term_multiplexer, ShellRequester *by);
116 static char * gen_term_multiplexer_cmd(const char cmd[], int pause,
117 ShellRequester by);
118 static char * gen_term_multiplexer_title_arg(const char cmd[]);
119 static char * gen_normal_cmd(const char cmd[], int pause);
120 static void set_pwd_in_screen(const char path[]);
121 static int try_run_with_filetype(view_t *view, const assoc_records_t assocs,
122 const char start[], int background);
123 static void output_to_statusbar(const char cmd[]);
124 static int output_to_preview(const char cmd[]);
125 static void output_to_nowhere(const char cmd[]);
126 static void run_in_split(const view_t *view, const char cmd[]);
127 static void path_handler(const char line[], void *arg);
128 static void line_handler(const char line[], void *arg);
129
130 /* Name of environment variable used to communicate path to file used to
131 * initiate FUSE mounting of directory we're in. */
132 static const char *const FUSE_FILE_ENVVAR = "VIFM_FUSE_FILE";
133
134 void
rn_open(view_t * view,FileHandleExec exec)135 rn_open(view_t *view, FileHandleExec exec)
136 {
137 handle_file(view, exec, FHL_NO_FOLLOW);
138 }
139
140 void
rn_follow(view_t * view,int ultimate)141 rn_follow(view_t *view, int ultimate)
142 {
143 if(flist_custom_active(view))
144 {
145 const dir_entry_t *const curr = get_current_entry(view);
146 if(!fentry_is_fake(curr))
147 {
148 /* Entry might be freed on navigation, so make sure name and origin will
149 * remain available for the call. */
150 char *const name = strdup(curr->name);
151 char *const origin = strdup(curr->origin);
152 navigate_to_file(view, origin, name, 0);
153 free(origin);
154 free(name);
155 }
156 return;
157 }
158
159 handle_file(view, FHE_RUN, ultimate ? FHL_FOLLOW_ALL : FHL_FOLLOW);
160 }
161
162 static void
handle_file(view_t * view,FileHandleExec exec,FileHandleLink follow)163 handle_file(view_t *view, FileHandleExec exec, FileHandleLink follow)
164 {
165 char full_path[PATH_MAX + 1];
166 const dir_entry_t *const curr = get_current_entry(view);
167
168 int user_selection = !view->pending_marking;
169 flist_set_marking(view, 1);
170
171 if(fentry_is_fake(curr))
172 {
173 return;
174 }
175
176 get_full_path_of(curr, sizeof(full_path), full_path);
177
178 int could_enter_entry = (curr->type != FT_LINK || follow == FHL_NO_FOLLOW);
179 int selected_entry = (curr->marked && (!user_selection || curr->selected));
180 if(!selected_entry && could_enter_entry)
181 {
182 int dir_like_entry = (is_dir(full_path) || is_unc_root(view->curr_dir));
183 if(dir_like_entry)
184 {
185 enter_dir(view);
186 return;
187 }
188 }
189
190 int runnable = is_runnable(view, full_path, curr->type,
191 follow != FHL_NO_FOLLOW);
192 int executable = is_executable(full_path, curr, exec == FHE_NO_RUN, runnable);
193
194 if(stats_file_choose_action_set() && (executable || runnable))
195 {
196 /* Reuse marking second time. */
197 view->pending_marking = 1;
198 /* The call below does not return. */
199 vifm_choose_files(view, 0, NULL);
200 }
201
202 if(executable && !fentry_is_dir(curr))
203 {
204 execute_file(full_path, exec == FHE_ELEVATE_AND_RUN);
205 }
206 else if(runnable)
207 {
208 run_selection(view, exec == FHE_NO_RUN);
209 }
210 else if(curr->type == FT_LINK || is_shortcut(curr->name))
211 {
212 follow_link(view, follow != FHL_NO_FOLLOW, follow == FHL_FOLLOW_ALL);
213 }
214 }
215
216 /* Returns non-zero if file can be executed or it's a link to a directory (it
217 * can be entered), otherwise zero is returned. */
218 static int
is_runnable(view_t * view,const char full_path[],int type,int force_follow)219 is_runnable(view_t *view, const char full_path[], int type, int force_follow)
220 {
221 int count = 0;
222 dir_entry_t *entry = NULL;
223 while(iter_marked_entries(view, &entry))
224 {
225 if(++count > 1)
226 {
227 return 1;
228 }
229 }
230
231 if(!force_follow && !cfg.follow_links && type == FT_LINK &&
232 get_symlink_type(full_path) != SLT_DIR)
233 {
234 return 1;
235 }
236
237 if(type == FT_REG)
238 {
239 return (!force_follow || !is_shortcut(full_path));
240 }
241
242 return (type == FT_EXEC || type == FT_DIR);
243 }
244
245 /* Returns non-zero if file can be executed, otherwise zero is returned. */
246 static int
is_executable(const char full_path[],const dir_entry_t * curr,int dont_execute,int runnable)247 is_executable(const char full_path[], const dir_entry_t *curr, int dont_execute,
248 int runnable)
249 {
250 int executable;
251 #ifndef _WIN32
252 executable = curr->type == FT_EXEC ||
253 (runnable && os_access(full_path, X_OK) == 0 && S_ISEXE(curr->mode));
254 #else
255 executable = curr->type == FT_EXEC;
256 #endif
257 return executable && !dont_execute && cfg.auto_execute;
258 }
259
260 #ifdef _WIN32
261
262 /* Runs a Windows executable handling errors and rights elevation. */
263 static void
run_win_executable(char full_path[],int elevate)264 run_win_executable(char full_path[], int elevate)
265 {
266 int running_error = 0;
267 int running_error_code = NO_ERROR;
268 if(elevate && is_vista_and_above())
269 {
270 running_error = run_win_executable_as_evaluated(full_path);
271 }
272 else
273 {
274 int returned_exit_code;
275 const int error = win_exec_cmd(full_path, &returned_exit_code);
276 if(error != 0 && !returned_exit_code)
277 {
278 if(error == ERROR_ELEVATION_REQUIRED && is_vista_and_above())
279 {
280 const int user_response = prompt_msg("Program running error",
281 "Executable requires rights elevation. Run with elevated rights?");
282 if(user_response != 0)
283 {
284 running_error = run_win_executable_as_evaluated(full_path);
285 }
286 }
287 else
288 {
289 running_error = 1;
290 running_error_code = error;
291 }
292 }
293 update_screen(UT_FULL);
294 }
295 if(running_error)
296 {
297 char err_msg[512];
298 err_msg[0] = '\0';
299 if(running_error_code != NO_ERROR && FormatMessageA(
300 FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_SYSTEM, NULL,
301 running_error_code, 0, err_msg, sizeof(err_msg), NULL) == 0)
302 {
303 LOG_WERROR(GetLastError());
304 }
305
306 show_error_msgf("Program running error", "Can't run an executable%s%s",
307 (err_msg[0] == '\0') ? "." : ": ", err_msg);
308 }
309 }
310
311 /* Returns non-zero on error, otherwise zero is returned. */
312 static int
run_win_executable_as_evaluated(const char full_path[])313 run_win_executable_as_evaluated(const char full_path[])
314 {
315 wchar_t *utf16_path;
316 SHELLEXECUTEINFOW sei;
317
318 utf16_path = utf8_to_utf16(full_path);
319
320 memset(&sei, 0, sizeof(sei));
321 sei.cbSize = sizeof(sei);
322 sei.fMask = SEE_MASK_FLAG_NO_UI | SEE_MASK_NOCLOSEPROCESS;
323 sei.lpVerb = L"runas";
324 sei.lpFile = utf16_path;
325 sei.lpParameters = NULL;
326 sei.nShow = SW_SHOWNORMAL;
327
328 if(!ShellExecuteExW(&sei))
329 {
330 const DWORD last_error = GetLastError();
331 free(utf16_path);
332 LOG_WERROR(last_error);
333 return last_error != ERROR_CANCELLED;
334 }
335
336 free(utf16_path);
337 CloseHandle(sei.hProcess);
338 return 0;
339 }
340
341 #endif /* _WIN32 */
342
343 /* Returns non-zero if selection doesn't mix files and directories, otherwise
344 * zero is returned. */
345 static int
selection_is_consistent(view_t * view)346 selection_is_consistent(view_t *view)
347 {
348 int files = 0, dirs = 0;
349 dir_entry_t *entry = NULL;
350 while(iter_marked_entries(view, &entry))
351 {
352 if(fentry_is_dir(entry))
353 {
354 ++dirs;
355 }
356 else
357 {
358 ++files;
359 }
360 }
361 return (dirs == 0 || files == 0);
362 }
363
364 /* Executes file, specified by the full_path. Changes type of slashes on
365 * Windows. */
366 static void
execute_file(const char full_path[],int elevate)367 execute_file(const char full_path[], int elevate)
368 {
369 #ifndef _WIN32
370 char *const escaped = shell_like_escape(full_path, 0);
371 rn_shell(escaped, PAUSE_ALWAYS, 1, SHELL_BY_APP);
372 free(escaped);
373 #else
374 char *const dquoted_full_path = strdup(enclose_in_dquotes(full_path));
375
376 internal_to_system_slashes(dquoted_full_path);
377 run_win_executable(dquoted_full_path, elevate);
378
379 free(dquoted_full_path);
380 #endif
381 }
382
383 /* Tries to run selection displaying error message on file type
384 * inconsistency. */
385 static void
run_selection(view_t * view,int dont_execute)386 run_selection(view_t *view, int dont_execute)
387 {
388 if(!selection_is_consistent(view))
389 {
390 show_error_msg("Selection error",
391 "Selection cannot contain files and directories at the same time");
392 return;
393 }
394
395 char *typed_fname = get_typed_entry_fpath(get_current_entry(view));
396 const char *common_prog_cmd = ft_get_program(typed_fname);
397 free(typed_fname);
398
399 int can_multi_run = (is_multi_run_compat(view, common_prog_cmd) != 0);
400 int files_without_handler = (common_prog_cmd == NULL);
401 int identical_handlers = 1;
402 int nentries = 0;
403
404 dir_entry_t *entry = NULL;
405 while(iter_marked_entries(view, &entry))
406 {
407 ++nentries;
408
409 if(!path_exists_at(entry->origin, entry->name, DEREF))
410 {
411 show_error_msgf("Broken Link", "Destination of \"%s\" link doesn't exist",
412 entry->name);
413 return;
414 }
415
416 char *typed_fname = get_typed_entry_fpath(entry);
417 const char *entry_prog_cmd = ft_get_program(typed_fname);
418 free(typed_fname);
419
420 if(entry_prog_cmd == NULL)
421 {
422 ++files_without_handler;
423 continue;
424 }
425
426 can_multi_run &= (is_multi_run_compat(view, entry_prog_cmd) != 0);
427 if(common_prog_cmd == NULL)
428 {
429 common_prog_cmd = entry_prog_cmd;
430 }
431 else if(strcmp(entry_prog_cmd, common_prog_cmd) != 0)
432 {
433 identical_handlers = 0;
434 }
435 }
436
437 can_multi_run &= (nentries > 1);
438
439 if(files_without_handler > 0)
440 {
441 run_with_defaults(view);
442 }
443 else if(can_multi_run)
444 {
445 run_selection_separately(view, dont_execute);
446 }
447 else if(identical_handlers)
448 {
449 rn_open_with(view, common_prog_cmd, dont_execute, 0);
450 }
451 else
452 {
453 show_error_msg("Run error", "Handlers of selected files are "
454 "incompatible.");
455 }
456 }
457
458 /* Runs current file entry of the view in a generic way (entering directories
459 * and opening files in editors). */
460 static void
run_with_defaults(view_t * view)461 run_with_defaults(view_t *view)
462 {
463 if(get_current_entry(view)->type == FT_DIR)
464 {
465 enter_dir(view);
466 return;
467 }
468
469 /* Reuse marking second time. */
470 view->pending_marking = 1;
471 if(vim_edit_marking() != 0)
472 {
473 show_error_msg("Running error", "Can't edit selection");
474 }
475 }
476
477 /* Runs each of selected file entries of the view individually. */
478 static void
run_selection_separately(view_t * view,int dont_execute)479 run_selection_separately(view_t *view, int dont_execute)
480 {
481 const int pos = view->list_pos;
482
483 dir_entry_t *entry = NULL;
484 while(iter_marked_entries(view, &entry))
485 {
486 char *typed_fname;
487 const char *entry_prog_cmd;
488
489 typed_fname = get_typed_entry_fpath(entry);
490 entry_prog_cmd = ft_get_program(typed_fname);
491 free(typed_fname);
492
493 view->list_pos = entry_to_pos(view, entry);
494 rn_open_with(view, entry_prog_cmd, dont_execute, 0);
495 }
496
497 view->list_pos = pos;
498 }
499
500 /* Checks whether command is compatible with firing multiple file handlers for a
501 * set of selected files. Returns non-zero if so, otherwise zero is
502 * returned. */
503 static int
is_multi_run_compat(view_t * view,const char prog_cmd[])504 is_multi_run_compat(view_t *view, const char prog_cmd[])
505 {
506 size_t len;
507 if(prog_cmd == NULL)
508 return 0;
509 if((len = strlen(prog_cmd)) == 0)
510 return 0;
511 /* XXX: should the check be for " &" and not just "&"? */
512 if(prog_cmd[len - 1] != '&')
513 return 0;
514 if(strstr(prog_cmd, "%f") != NULL || strstr(prog_cmd, "%F") != NULL)
515 return 0;
516 if(strstr(prog_cmd, "%c") == NULL && strstr(prog_cmd, "%C") == NULL)
517 return 0;
518 return 1;
519 }
520
521 void
rn_open_with(view_t * view,const char prog_spec[],int dont_execute,int force_bg)522 rn_open_with(view_t *view, const char prog_spec[], int dont_execute,
523 int force_bg)
524 {
525 const dir_entry_t *const curr = get_current_entry(view);
526 const int pause = skip_prefix(&prog_spec, "!!");
527
528 if(!path_exists_at(curr->origin, curr->name, DEREF))
529 {
530 show_error_msg("Access Error", "File doesn't exist.");
531 return;
532 }
533
534 if(fuse_is_mount_string(prog_spec))
535 {
536 if(dont_execute)
537 {
538 view_current_file(view);
539 }
540 else
541 {
542 fuse_try_mount(view, prog_spec);
543 }
544 }
545 else if(strcmp(prog_spec, VIFM_PSEUDO_CMD) == 0)
546 {
547 enter_dir(view);
548 }
549 else if(strchr(prog_spec, '%') != NULL)
550 {
551 run_explicit_prog(prog_spec, pause, force_bg);
552 }
553 else
554 {
555 run_implicit_prog(view, prog_spec, pause, force_bg);
556 }
557 }
558
559 /* Executes current file of the current view by program specification that
560 * includes at least one macro. */
561 static void
run_explicit_prog(const char prog_spec[],int pause,int force_bg)562 run_explicit_prog(const char prog_spec[], int pause, int force_bg)
563 {
564 int bg;
565 MacroFlags flags;
566 int save_msg;
567 char *const cmd = ma_expand(prog_spec, NULL, &flags, 1);
568
569 bg = cut_suffix(cmd, " &");
570 bg = !pause && (bg || force_bg);
571
572 save_msg = 0;
573 if(rn_ext(cmd, prog_spec, flags, bg, &save_msg) != 0)
574 {
575 if(save_msg)
576 {
577 curr_stats.save_msg = 1;
578 }
579 }
580 else if(bg)
581 {
582 assert(flags != MF_IGNORE && "This case is for rn_ext()");
583 (void)bg_run_external(cmd, flags == MF_IGNORE, SHELL_BY_USER);
584 }
585 else
586 {
587 (void)rn_shell(cmd, pause ? PAUSE_ALWAYS : PAUSE_ON_ERROR,
588 flags != MF_NO_TERM_MUX, SHELL_BY_USER);
589 }
590
591 free(cmd);
592 }
593
594 /* Executes current file of the view by program specification that does not
595 * include any macros (hence file name is appended implicitly. */
596 static void
run_implicit_prog(view_t * view,const char prog_spec[],int pause,int force_bg)597 run_implicit_prog(view_t *view, const char prog_spec[], int pause, int force_bg)
598 {
599 int bg;
600 char cmd[NAME_MAX + 1 + NAME_MAX + 1];
601 const char *name_macro;
602 char *file_name;
603 char spec[strlen(prog_spec) + 1U];
604
605 strcpy(spec, prog_spec);
606 bg = cut_suffix(spec, " &") || force_bg;
607
608 if(curr_stats.shell_type == ST_CMD)
609 {
610 name_macro = (view == curr_view) ? "%\"c" : "%\"C";
611 }
612 else
613 {
614 name_macro = (view == curr_view) ? "%c" : "%C";
615 }
616
617 file_name = ma_expand(name_macro, NULL, NULL, 1);
618 snprintf(cmd, sizeof(cmd), "%s %s", spec, file_name);
619 free(file_name);
620
621 if(bg)
622 {
623 (void)bg_run_external(cmd, 0, SHELL_BY_USER);
624 }
625 else
626 {
627 (void)rn_shell(cmd, pause ? PAUSE_ALWAYS : PAUSE_ON_ERROR, 1,
628 SHELL_BY_USER);
629 }
630 }
631
632 /* Opens file under the cursor in the viewer. */
633 static void
view_current_file(const view_t * view)634 view_current_file(const view_t *view)
635 {
636 char full_path[PATH_MAX + 1];
637 get_current_full_path(view, sizeof(full_path), full_path);
638 (void)vim_view_file(full_path, -1, -1, 1);
639 }
640
641 /* Resolves link target and either navigates inside directory the link points to
642 * or navigates to directory where target is located placing cursor at it (the
643 * follow_dirs flag controls the behaviour). */
644 static void
follow_link(view_t * view,int follow_dirs,int ultimate)645 follow_link(view_t *view, int follow_dirs, int ultimate)
646 {
647 char *dir, *file;
648 char linkto[PATH_MAX + NAME_MAX + 1];
649 const dir_entry_t *const curr = get_current_entry(view);
650
651 char full_path[PATH_MAX + 1];
652 get_full_path_of(curr, sizeof(full_path), full_path);
653
654 int resolve_one_level = !ultimate;
655
656 if(ultimate)
657 {
658 if(os_realpath(full_path, linkto) != linkto)
659 {
660 show_error_msg("Error", "Can't resolve the link.");
661 return;
662 }
663
664 #ifdef _WIN32
665 /* This might be a Windows shortcut which aren't resolved by
666 * os_realpath(). */
667 resolve_one_level = paths_are_equal(full_path, linkto);
668 #endif
669 }
670
671 if(resolve_one_level)
672 {
673 /* We resolve origin to a real path because relative symbolic links are
674 * relative to real location of the link. Can't simply do realpath() on
675 * full path as it would resolve symbolic links recursively, while we want
676 * to follow only one level deep. */
677
678 char origin_real[PATH_MAX + 1];
679 if(os_realpath(curr->origin, origin_real) != origin_real)
680 {
681 show_error_msg("Error", "Can't resolve origin of the link.");
682 return;
683 }
684
685 if(get_link_target_abs(full_path, origin_real, linkto, sizeof(linkto)) != 0)
686 {
687 show_error_msg("Error", "Can't read link.");
688 return;
689 }
690 }
691
692 if(!path_exists(linkto, NODEREF))
693 {
694 show_error_msg("Broken Link",
695 "Can't access link destination. It might be broken.");
696 return;
697 }
698
699 chosp(linkto);
700
701 if(is_dir(linkto) && !follow_dirs)
702 {
703 dir = strdup(curr->name);
704 file = NULL;
705 }
706 else
707 {
708 dir = strdup(linkto);
709 remove_last_path_component(dir);
710
711 file = get_last_path_component(linkto);
712 }
713
714 if(dir[0] != '\0' && file != NULL)
715 {
716 navigate_to_file(view, dir, file, 1);
717 }
718 else if(dir[0] != '\0')
719 {
720 navigate_to(view, dir);
721 }
722 else if(file != NULL)
723 {
724 fpos_ensure_selected(view, file);
725 }
726
727 free(dir);
728 }
729
730 /* Handles opening of current entry of the view as a directory. */
731 static void
enter_dir(view_t * view)732 enter_dir(view_t *view)
733 {
734 dir_entry_t *const curr = get_current_entry(view);
735
736 if(is_parent_dir(curr->name) && curr->origin == view->curr_dir)
737 {
738 rn_leave(view, 1);
739 return;
740 }
741
742 char full_path[PATH_MAX + 1];
743 get_full_path_of(curr, sizeof(full_path), full_path);
744
745 if(cd_is_possible(full_path))
746 {
747 curr_stats.ch_pos = (cfg_ch_pos_on(CHPOS_ENTER) ? 1 : 0);
748 navigate_to(view, full_path);
749 curr_stats.ch_pos = 1;
750 }
751 }
752
753 void
rn_leave(view_t * view,int levels)754 rn_leave(view_t *view, int levels)
755 {
756 /* Do not save intermediate directories in directory history. */
757 curr_stats.drop_new_dir_hist = 1;
758
759 while(levels-- > 0)
760 {
761 if(cd_to_parent_dir(view) != 0)
762 {
763 break;
764 }
765 }
766
767 curr_stats.drop_new_dir_hist = 0;
768 flist_hist_save(view);
769 }
770
771 /* Goes one directory up from current location. Returns zero unless it won't
772 * make sense to continue going up (like on error or reaching root). */
773 static int
cd_to_parent_dir(view_t * view)774 cd_to_parent_dir(view_t *view)
775 {
776 char dir_name[strlen(view->curr_dir) + 1];
777 int ret;
778
779 /* Return to original directory from custom view. */
780 if(flist_custom_active(view))
781 {
782 navigate_to(view, view->custom.orig_dir);
783 return 0;
784 }
785
786 /* Do nothing in root. */
787 if(is_root_dir(view->curr_dir))
788 {
789 return 1;
790 }
791
792 dir_name[0] = '\0';
793 extract_last_path_component(view->curr_dir, dir_name);
794
795 ret = change_directory(view, "../");
796 if(ret == -1)
797 {
798 return 1;
799 }
800
801 if(ret == 0)
802 {
803 /* XXX: we put cursor at one position and then move it. Ideally it would
804 * be set where it should be right away. */
805 load_dir_list(view, 0);
806 fpos_set_pos(view, fpos_find_by_name(view, dir_name));
807 }
808 return 0;
809 }
810
811 /* Extracts last part of the path into buf. Assumes that size of the buf is
812 * large enough. */
813 static void
extract_last_path_component(const char path[],char buf[])814 extract_last_path_component(const char path[], char buf[])
815 {
816 const char *const last = get_last_path_component(path);
817 copy_str(buf, until_first(last, '/') - last + 1, last);
818 }
819
820 int
rn_shell(const char command[],ShellPause pause,int use_term_multiplexer,ShellRequester by)821 rn_shell(const char command[], ShellPause pause, int use_term_multiplexer,
822 ShellRequester by)
823 {
824 char *cmd;
825 int result;
826 int ec;
827
828 /* Shutdown UI at this point, where $PATH isn't cleared. */
829 ui_shutdown();
830
831 int shellout = (command == NULL);
832 if(shellout)
833 {
834 command = env_get_def("SHELL", cfg.shell);
835
836 /* Run shell with clean $PATH environment variable. */
837 load_clean_path_env();
838 }
839
840 if(pause == PAUSE_ALWAYS && command != NULL && ends_with(command, "&"))
841 {
842 pause = PAUSE_ON_ERROR;
843 }
844
845 setup_shellout_env();
846
847 cmd = gen_shell_cmd(command, pause == PAUSE_ALWAYS, use_term_multiplexer,
848 &by);
849
850 ec = vifm_system(cmd, by);
851 /* No WIFEXITED(ec) check here, since vifm_system(...) shouldn't return until
852 * subprocess exited. */
853 result = WEXITSTATUS(ec);
854
855 cleanup_shellout_env();
856
857 if(result != 0 && pause == PAUSE_ON_ERROR)
858 {
859 LOG_ERROR_MSG("Subprocess (%s) exit code: %d (0x%x); status = 0x%x", cmd,
860 result, result, ec);
861 pause_shell();
862 }
863
864 free(cmd);
865
866 /* Force updates of views that don't have associated watchers. */
867 if(flist_custom_active(&lwin) && lwin.custom.type != CV_TREE)
868 {
869 ui_view_schedule_reload(&lwin);
870 }
871 if(flist_custom_active(&rwin) && rwin.custom.type != CV_TREE)
872 {
873 ui_view_schedule_reload(&rwin);
874 }
875
876 recover_after_shellout();
877
878 if(!curr_stats.skip_shellout_redraw)
879 {
880 /* Redraw to handle resizing of terminal that we could have missed. */
881 stats_redraw_later();
882 }
883
884 if(curr_stats.load_stage > 0)
885 {
886 curs_set(0);
887 }
888
889 if(shellout)
890 {
891 load_real_path_env();
892 }
893
894 return result;
895 }
896
897 /* Configures environment variables before shellout. Should be used in pair
898 * with cleanup_shellout_env(). */
899 static void
setup_shellout_env(void)900 setup_shellout_env(void)
901 {
902 const char *mount_file;
903 const char *term_multiplexer_fmt;
904 char *escaped_path;
905 char *cmd;
906
907 /* Need to use internal value instead of getcwd() for a symlink directory. */
908 env_set("PWD", curr_view->curr_dir);
909
910 mount_file = fuse_get_mount_file(curr_view->curr_dir);
911 if(mount_file == NULL)
912 {
913 env_remove(FUSE_FILE_ENVVAR);
914 return;
915 }
916
917 env_set(FUSE_FILE_ENVVAR, mount_file);
918
919 switch(curr_stats.term_multiplexer)
920 {
921 case TM_TMUX: term_multiplexer_fmt = "tmux set-environment %s %s"; break;
922 case TM_SCREEN: term_multiplexer_fmt = "screen -X setenv %s %s"; break;
923
924 default:
925 return;
926 }
927
928 escaped_path = shell_like_escape(mount_file, 0);
929 cmd = format_str(term_multiplexer_fmt, FUSE_FILE_ENVVAR, escaped_path);
930 (void)vifm_system(cmd, SHELL_BY_APP);
931 free(cmd);
932 free(escaped_path);
933 }
934
935 /* Cleans up some environment changes made by setup_shellout_env() after command
936 * execution ends. */
937 static void
cleanup_shellout_env(void)938 cleanup_shellout_env(void)
939 {
940 const char *term_multiplexer_fmt;
941 char *cmd;
942
943 switch(curr_stats.term_multiplexer)
944 {
945 case TM_TMUX: term_multiplexer_fmt = "tmux set-environment -u %s"; break;
946 case TM_SCREEN: term_multiplexer_fmt = "screen -X unsetenv %s"; break;
947
948 default:
949 return;
950 }
951
952 cmd = format_str(term_multiplexer_fmt, FUSE_FILE_ENVVAR);
953 (void)vifm_system(cmd, SHELL_BY_APP);
954 free(cmd);
955 }
956
957 /* Composes shell command to run based on parameters for execution. Returns a
958 * newly allocated string, which should be freed by the caller. */
959 static char *
gen_shell_cmd(const char cmd[],int pause,int use_term_multiplexer,ShellRequester * by)960 gen_shell_cmd(const char cmd[], int pause, int use_term_multiplexer,
961 ShellRequester *by)
962 {
963 char *shell_cmd;
964
965 if(use_term_multiplexer && curr_stats.term_multiplexer != TM_NONE)
966 {
967 shell_cmd = gen_term_multiplexer_cmd(cmd, pause, *by);
968 /* User shell settings were taken into account in command for multiplexer,
969 * don't use them to invoke the multiplexer itself. */
970 *by = SHELL_BY_APP;
971 }
972 else
973 {
974 shell_cmd = gen_normal_cmd(cmd, pause);
975 }
976
977 return shell_cmd;
978 }
979
980 /* Composes command to be run using terminal multiplexer. Returns newly
981 * allocated string that should be freed by the caller. */
982 static char *
gen_term_multiplexer_cmd(const char cmd[],int pause,ShellRequester by)983 gen_term_multiplexer_cmd(const char cmd[], int pause, ShellRequester by)
984 {
985 char *title_arg;
986 char *raw_shell_cmd;
987 char *escaped_shell_cmd;
988 char *shell_cmd = NULL;
989
990 if(curr_stats.term_multiplexer != TM_TMUX &&
991 curr_stats.term_multiplexer != TM_SCREEN)
992 {
993 assert(0 && "Unexpected active terminal multiplexer value.");
994 return NULL;
995 }
996
997 title_arg = gen_term_multiplexer_title_arg(cmd);
998
999 raw_shell_cmd = format_str("%s%s", cmd, pause ? PAUSE_STR : "");
1000 escaped_shell_cmd = shell_like_escape(raw_shell_cmd, 0);
1001
1002 const char *sh_flag = (by == SHELL_BY_USER ? cfg.shell_cmd_flag : "-c");
1003
1004 if(curr_stats.term_multiplexer == TM_TMUX)
1005 {
1006 char *const arg = format_str("%s %s %s", cfg.shell, sh_flag,
1007 escaped_shell_cmd);
1008 char *const escaped_arg = shell_like_escape(arg, 0);
1009
1010 shell_cmd = format_str("tmux new-window %s %s", title_arg, escaped_arg);
1011
1012 free(escaped_arg);
1013 free(arg);
1014 }
1015 else if(curr_stats.term_multiplexer == TM_SCREEN)
1016 {
1017 set_pwd_in_screen(flist_get_dir(curr_view));
1018
1019 shell_cmd = format_str("screen %s %s %s %s", title_arg, cfg.shell, sh_flag,
1020 escaped_shell_cmd);
1021 }
1022 else
1023 {
1024 assert(0 && "Unsupported terminal multiplexer type.");
1025 }
1026
1027 free(escaped_shell_cmd);
1028 free(raw_shell_cmd);
1029 free(title_arg);
1030
1031 return shell_cmd;
1032 }
1033
1034 /* Composes title for window of a terminal multiplexer from a command. Returns
1035 * newly allocated string that should be freed by the caller. */
1036 static char *
gen_term_multiplexer_title_arg(const char cmd[])1037 gen_term_multiplexer_title_arg(const char cmd[])
1038 {
1039 int bg;
1040 const char *const vicmd = cfg_get_vicmd(&bg);
1041 const char *const visubcmd = strstr(cmd, vicmd);
1042 char *command_name = NULL;
1043 const char *title;
1044 char *title_arg;
1045
1046 if(visubcmd != NULL)
1047 {
1048 title = skip_whitespace(visubcmd + strlen(vicmd) + 1);
1049 if(cfg.short_term_mux_titles)
1050 {
1051 title = get_last_path_component(title);
1052 }
1053 }
1054 else
1055 {
1056 char *const separator = strchr(cmd, ' ');
1057 if(separator != NULL)
1058 {
1059 *separator = '\0';
1060 command_name = strdup(cmd);
1061 *separator = ' ';
1062 }
1063 title = command_name;
1064 }
1065
1066 if(is_null_or_empty(title))
1067 {
1068 title_arg = strdup("");
1069 }
1070 else
1071 {
1072 const char opt_c = (curr_stats.term_multiplexer == TM_SCREEN) ? 't' : 'n';
1073 char *const escaped_title = shell_like_escape(title, 0);
1074 title_arg = format_str("-%c %s", opt_c, escaped_title);
1075 free(escaped_title);
1076 }
1077
1078 free(command_name);
1079
1080 return title_arg;
1081 }
1082
1083 /* Composes command to be run without terminal multiplexer. Returns a newly
1084 * allocated string, which should be freed by the caller. */
1085 static char *
gen_normal_cmd(const char cmd[],int pause)1086 gen_normal_cmd(const char cmd[], int pause)
1087 {
1088 if(pause)
1089 {
1090 const char *cmd_with_pause_fmt;
1091
1092 if(curr_stats.shell_type == ST_CMD)
1093 {
1094 cmd_with_pause_fmt = "%s" PAUSE_STR;
1095 }
1096 else if(curr_stats.shell_type == ST_PS)
1097 {
1098 /* TODO: make pausing work. */
1099 cmd_with_pause_fmt = "%s";
1100 }
1101 else
1102 {
1103 cmd_with_pause_fmt = "%s; " PAUSE_CMD;
1104 }
1105
1106 return format_str(cmd_with_pause_fmt, cmd);
1107 }
1108 else
1109 {
1110 return strdup(cmd);
1111 }
1112 }
1113
1114 /* Changes $PWD in running GNU/screen session to the specified path. Needed for
1115 * symlink directories and sshfs mounts. */
1116 static void
set_pwd_in_screen(const char path[])1117 set_pwd_in_screen(const char path[])
1118 {
1119 char *const escaped_dir = shell_like_escape(path, 0);
1120 char *const set_pwd = format_str("screen -X setenv PWD %s", escaped_dir);
1121
1122 (void)vifm_system(set_pwd, SHELL_BY_APP);
1123
1124 free(set_pwd);
1125 free(escaped_dir);
1126 }
1127
1128 int
rn_open_with_match(view_t * view,const char beginning[],int background)1129 rn_open_with_match(view_t *view, const char beginning[], int background)
1130 {
1131 dir_entry_t *const curr = get_current_entry(view);
1132 assoc_records_t ft, magic;
1133 char *typed_fname;
1134
1135 if(fentry_is_fake(curr))
1136 {
1137 return 1;
1138 }
1139
1140 typed_fname = get_typed_entry_fpath(curr);
1141 ft = ft_get_all_programs(typed_fname);
1142 magic = get_magic_handlers(typed_fname);
1143 free(typed_fname);
1144
1145 if(try_run_with_filetype(view, ft, beginning, background))
1146 {
1147 ft_assoc_records_free(&ft);
1148 return 0;
1149 }
1150
1151 ft_assoc_records_free(&ft);
1152
1153 return !try_run_with_filetype(view, magic, beginning, background);
1154 }
1155
1156 /* Returns non-zero on successful running. */
1157 static int
try_run_with_filetype(view_t * view,const assoc_records_t assocs,const char start[],int background)1158 try_run_with_filetype(view_t *view, const assoc_records_t assocs,
1159 const char start[], int background)
1160 {
1161 const size_t len = strlen(start);
1162 int i;
1163 for(i = 0; i < assocs.count; i++)
1164 {
1165 if(strncmp(assocs.list[i].command, start, len) == 0)
1166 {
1167 rn_open_with(view, assocs.list[i].command, 0, background);
1168 return 1;
1169 }
1170 }
1171 return 0;
1172 }
1173
1174 int
rn_ext(const char cmd[],const char title[],MacroFlags flags,int bg,int * save_msg)1175 rn_ext(const char cmd[], const char title[], MacroFlags flags, int bg,
1176 int *save_msg)
1177 {
1178 if(bg && flags != MF_NONE && flags != MF_NO_TERM_MUX && flags != MF_IGNORE)
1179 {
1180 ui_sb_errf("\"%s\" macro can't be combined with \" &\"",
1181 ma_flags_to_str(flags));
1182 *save_msg = 1;
1183 return -1;
1184 }
1185
1186 if(flags == MF_STATUSBAR_OUTPUT)
1187 {
1188 output_to_statusbar(cmd);
1189 *save_msg = 1;
1190 return -1;
1191 }
1192 else if(flags == MF_PREVIEW_OUTPUT)
1193 {
1194 *save_msg = output_to_preview(cmd);
1195 return -1;
1196 }
1197 else if(flags == MF_IGNORE)
1198 {
1199 *save_msg = 0;
1200 if(bg)
1201 {
1202 int error;
1203
1204 setup_shellout_env();
1205 error = (bg_run_external(cmd, 1, SHELL_BY_USER) != 0);
1206 cleanup_shellout_env();
1207
1208 if(error)
1209 {
1210 ui_sb_errf("Failed to start in bg: %s", cmd);
1211 *save_msg = 1;
1212 }
1213 }
1214 else
1215 {
1216 output_to_nowhere(cmd);
1217 }
1218 return -1;
1219 }
1220 else if(flags == MF_MENU_OUTPUT || flags == MF_MENU_NAV_OUTPUT)
1221 {
1222 const int navigate = flags == MF_MENU_NAV_OUTPUT;
1223 setup_shellout_env();
1224 *save_msg = show_user_menu(curr_view, cmd, title, navigate) != 0;
1225 cleanup_shellout_env();
1226 }
1227 else if(flags == MF_SPLIT && curr_stats.term_multiplexer != TM_NONE)
1228 {
1229 run_in_split(curr_view, cmd);
1230 }
1231 else if(ONE_OF(flags, MF_CUSTOMVIEW_OUTPUT, MF_VERYCUSTOMVIEW_OUTPUT,
1232 MF_CUSTOMVIEW_IOUTPUT, MF_VERYCUSTOMVIEW_IOUTPUT))
1233 {
1234 const int very =
1235 ONE_OF(flags, MF_VERYCUSTOMVIEW_OUTPUT, MF_VERYCUSTOMVIEW_IOUTPUT);
1236 const int interactive =
1237 ONE_OF(flags, MF_CUSTOMVIEW_IOUTPUT, MF_VERYCUSTOMVIEW_IOUTPUT);
1238 rn_for_flist(curr_view, cmd, title, very, interactive);
1239 }
1240 else
1241 {
1242 return 0;
1243 }
1244 return 1;
1245 }
1246
1247 /* Executes the cmd and displays its output on the status bar. */
1248 static void
output_to_statusbar(const char cmd[])1249 output_to_statusbar(const char cmd[])
1250 {
1251 FILE *file, *err;
1252 char buf[2048];
1253 char *lines;
1254 size_t len;
1255 int error;
1256
1257 setup_shellout_env();
1258 error = (bg_run_and_capture((char *)cmd, 1, &file, &err) == (pid_t)-1);
1259 cleanup_shellout_env();
1260 if(error)
1261 {
1262 show_error_msgf("Trouble running command", "Unable to run: %s", cmd);
1263 return;
1264 }
1265
1266 lines = NULL;
1267 len = 0;
1268 while(fgets(buf, sizeof(buf), file) == buf)
1269 {
1270 char *p;
1271
1272 chomp(buf);
1273 p = realloc(lines, len + 1 + strlen(buf) + 1);
1274 if(p != NULL)
1275 {
1276 lines = p;
1277 len += sprintf(lines + len, "%s%s", (len == 0) ? "": "\n", buf);
1278 }
1279 }
1280
1281 fclose(file);
1282 fclose(err);
1283
1284 ui_sb_msg((lines == NULL) ? "" : lines);
1285 free(lines);
1286 }
1287
1288 /* Runs the command and captures its output into detached preview. Returns new
1289 * value for the save_msg flag. */
1290 static int
output_to_preview(const char cmd[])1291 output_to_preview(const char cmd[])
1292 {
1293 if(qv_ensure_is_shown() != 0)
1294 {
1295 return 1;
1296 }
1297 modview_detached_make(other_view, cmd);
1298 return 0;
1299 }
1300
1301 /* Executes the cmd ignoring its output. */
1302 static void
output_to_nowhere(const char cmd[])1303 output_to_nowhere(const char cmd[])
1304 {
1305 FILE *file, *err;
1306 int error;
1307
1308 setup_shellout_env();
1309 error = (bg_run_and_capture((char *)cmd, 1, &file, &err) == (pid_t)-1);
1310 cleanup_shellout_env();
1311 if(error)
1312 {
1313 show_error_msgf("Trouble running command", "Unable to run: %s", cmd);
1314 return;
1315 }
1316
1317 /* FIXME: better way of doing this would be to redirect these streams to
1318 * /dev/null rather than closing them, but not sure about Windows (NUL
1319 * device might work). */
1320 fclose(file);
1321 fclose(err);
1322 }
1323
1324 /* Runs the cmd in a split window of terminal multiplexer. Runs shell, if cmd
1325 * is NULL. */
1326 static void
run_in_split(const view_t * view,const char cmd[])1327 run_in_split(const view_t *view, const char cmd[])
1328 {
1329 char *const escaped_cmd = (cmd == NULL)
1330 ? strdup(cfg.shell)
1331 : shell_like_escape(cmd, 0);
1332
1333 setup_shellout_env();
1334
1335 if(curr_stats.term_multiplexer == TM_TMUX)
1336 {
1337 char cmd[1024];
1338 snprintf(cmd, sizeof(cmd), "tmux split-window %s", escaped_cmd);
1339 (void)vifm_system(cmd, SHELL_BY_APP);
1340 }
1341 else if(curr_stats.term_multiplexer == TM_SCREEN)
1342 {
1343 char cmd[1024];
1344
1345 /* "eval" executes each argument as a separate argument, but escaping rules
1346 * are not exactly like in shell, so last command is run separately. */
1347 char *const escaped_dir = shell_like_escape(flist_get_dir(view), 0);
1348 snprintf(cmd, sizeof(cmd), "screen -X eval chdir\\ %s 'focus bottom' "
1349 "split 'focus bottom'", escaped_dir);
1350 free(escaped_dir);
1351 (void)vifm_system(cmd, SHELL_BY_APP);
1352
1353 snprintf(cmd, sizeof(cmd), "screen -X screen vifm-screen-split %s",
1354 escaped_cmd);
1355 (void)vifm_system(cmd, SHELL_BY_APP);
1356 }
1357 else
1358 {
1359 assert(0 && "Unexpected active terminal multiplexer value.");
1360 }
1361
1362 cleanup_shellout_env();
1363
1364 free(escaped_cmd);
1365 }
1366
1367 int
rn_for_flist(struct view_t * view,const char cmd[],const char title[],int very,int interactive)1368 rn_for_flist(struct view_t *view, const char cmd[], const char title[],
1369 int very, int interactive)
1370 {
1371 enum { MAX_TITLE_WIDTH = 80 };
1372
1373 /* It makes sense to do escaping before adding ellipses to get a predictable
1374 * result. */
1375 char *escaped_title = escape_unreadable(title);
1376 char *final_title = right_ellipsis(escaped_title, MAX_TITLE_WIDTH,
1377 curr_stats.ellipsis);
1378 free(escaped_title);
1379
1380 flist_custom_start(view, final_title);
1381 free(final_title);
1382
1383 if(interactive && curr_stats.load_stage != 0)
1384 {
1385 ui_shutdown();
1386 }
1387
1388 setup_shellout_env();
1389 int error = (process_cmd_output("Loading custom view", cmd, 1, interactive,
1390 &path_handler, view) != 0);
1391 cleanup_shellout_env();
1392
1393 if(error)
1394 {
1395 show_error_msgf("Trouble running command", "Unable to run: %s", cmd);
1396 return 1;
1397 }
1398
1399 flist_custom_end(view, very);
1400 return 0;
1401 }
1402
1403 /* Implements process_cmd_output() callback that loads paths into custom
1404 * view. */
1405 static void
path_handler(const char line[],void * arg)1406 path_handler(const char line[], void *arg)
1407 {
1408 view_t *view = arg;
1409 flist_custom_add_spec(view, line);
1410 }
1411
1412 int
rn_for_lines(const char cmd[],char *** lines,int * nlines)1413 rn_for_lines(const char cmd[], char ***lines, int *nlines)
1414 {
1415 int error;
1416 strlist_t list = {};
1417
1418 setup_shellout_env();
1419 error = (process_cmd_output("Loading list", cmd, 1, 0, &line_handler,
1420 &list) != 0);
1421 cleanup_shellout_env();
1422
1423 if(error)
1424 {
1425 free_string_array(list.items, list.nitems);
1426 return 1;
1427 }
1428
1429 *lines = list.items;
1430 *nlines = list.nitems;
1431 return 0;
1432 }
1433
1434 /* Implements process_cmd_output() callback that collects lines into a list. */
1435 static void
line_handler(const char line[],void * arg)1436 line_handler(const char line[], void *arg)
1437 {
1438 strlist_t *const list = arg;
1439 list->nitems = add_to_string_array(&list->items, list->nitems, line);
1440 }
1441
1442 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
1443 /* vim: set cinoptions+=t0 filetype=c : */
1444