xaizek / vifm (License: GPLv2+) (since 2018-12-07)
Vifm is a file manager with curses interface, which provides Vi[m]-like environment for managing objects within file systems, extended with some useful ideas from mutt.
<root> / src / int / vim.c (2034fc6907335bb5b1778cc94e60c8c544826c1f) (9,153B) (mode 100644) [raw]
/* vifm
 * Copyright (C) 2014 xaizek.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include "vim.h"

#include <ctype.h> /* isspace() */
#include <errno.h> /* errno */
#include <stdio.h> /* FILE fclose() fprintf() fputs() snprintf() */
#include <stdlib.h> /* EXIT_SUCCESS free() */
#include <string.h> /* strcmp() strlen() strrchr() strstr() */

#include "../cfg/config.h"
#include "../compat/fs_limits.h"
#include "../compat/os.h"
#include "../lua/vlua.h"
#include "../modes/dialogs/msg_dialog.h"
#include "../ui/ui.h"
#include "../utils/fs.h"
#include "../utils/log.h"
#include "../utils/path.h"
#include "../utils/str.h"
#include "../utils/string_array.h"
#include "../utils/test_helpers.h"
#include "../utils/utils.h"
#include "../background.h"
#include "../filelist.h"
#include "../flist_sel.h"
#include "../running.h"
#include "../status.h"
#include "../vifm.h"

/* File name known to Vim-plugin. */
#define LIST_FILE "vimfiles"

static int run_vim(const char cmd[], int bg, int use_term_multiplexer);
TSTATIC void trim_right(char text[]);
static void dump_filenames(view_t *view, FILE *fp, int nfiles, char *files[]);

int
vim_format_help_cmd(const char topic[], char cmd[], size_t cmd_size)
{
	char *escaped_data = posix_like_escape(get_installed_data_dir(), /*type=*/0);
	char *set_rtp = format_str("+set runtimepath+=%s/vim-doc", escaped_data);
	free(escaped_data);

	char *escaped_set_rtp = shell_arg_escape(set_rtp, curr_stats.shell_type);
	free(set_rtp);

	char *help = format_str("+help %s", topic);
	char *escaped_help = shell_arg_escape(help, curr_stats.shell_type);
	free(help);

	int bg;
	snprintf(cmd, cmd_size, "%s %s %s -c only", cfg_get_vicmd(&bg),
			escaped_set_rtp, escaped_help);

	free(escaped_set_rtp);
	free(escaped_help);
	return bg;
}

int
vim_edit_files(int nfiles, char *files[])
{
	int i;
	int bg;
	int error;

	const char *vi_cmd = cfg_get_vicmd(&bg);

	if(vlua_handler_cmd(curr_stats.vlua, vi_cmd))
	{
		if(vlua_edit_many(curr_stats.vlua, vi_cmd, files, nfiles) != 0)
		{
			show_error_msg("File View", "Failed to view files via handler");
			return 1;
		}
		return 0;
	}

	char *cmd = NULL;
	size_t len = 0U;
	(void)strappend(&cmd, &len, vi_cmd);

	for(i = 0; i < nfiles; ++i)
	{
		char *const expanded_path = expand_tilde(files[i]);
		char *const escaped =
			shell_arg_escape(expanded_path, curr_stats.shell_type);
		(void)strappendch(&cmd, &len, ' ');
		(void)strappend(&cmd, &len, escaped);
		free(escaped);
		free(expanded_path);
	}

	error = (cmd == NULL || run_vim(cmd, bg, 1));
	free(cmd);

	return error;
}

int
vim_edit_marking(view_t *view)
{
	const int cv = flist_custom_active(view);

	char **marked = NULL;
	int nmarked = 0;

	dir_entry_t *entry = NULL;
	while(iter_marked_entries(view, &entry))
	{
		if(!cv)
		{
			nmarked = add_to_string_array(&marked, nmarked, entry->name);
			continue;
		}

		char path[PATH_MAX + 1];
		get_short_path_of(view, entry, NF_NONE, 0, sizeof(path), path);
		nmarked = add_to_string_array(&marked, nmarked, path);
	}

	int result = vim_edit_files(nmarked, marked);

	free_string_array(marked, nmarked);
	return result;
}

int
vim_view_file(const char filename[], int line, int column, int allow_forking)
{
	char vicmd[PATH_MAX + 1];
	char cmd[2*PATH_MAX + 5];
	/* Use `-f` and not `--nofork` here to make it for with nvim (and possibly
	 * other flavours of vi). */
	const char *fork_str = allow_forking ? "" : "-f";
	char *escaped;
	int bg;
	int result;

	cmd[0] = '\0';

	if(!path_exists(filename, DEREF))
	{
		if(path_exists(filename, NODEREF))
		{
			show_error_msg("Broken Link", "Link destination doesn't exist");
		}
		else
		{
			show_error_msg("Wrong Path", "File doesn't exist");
		}
		return 1;
	}

	copy_str(vicmd, sizeof(vicmd), cfg_get_vicmd(&bg));

	if(vlua_handler_cmd(curr_stats.vlua, vicmd))
	{
		if(vlua_edit_one(curr_stats.vlua, vicmd, filename, line, column,
					!allow_forking) != 0)
		{
			show_error_msg("File View", "Failed to view file via handler");
			return 1;
		}
		return 0;
	}

	trim_right(vicmd);
	if(!allow_forking)
	{
		char *p = strrchr(vicmd, ' ');
		if(p != NULL && strstr(p, "remote"))
		{
			*p = '\0';
		}
	}

	escaped = shell_arg_escape(filename, curr_stats.shell_type);

	if(line < 0 && column < 0)
		snprintf(cmd, sizeof(cmd), "%s %s %s", vicmd, fork_str, escaped);
	else if(column < 0)
		snprintf(cmd, sizeof(cmd), "%s %s +%d %s", vicmd, fork_str, line, escaped);
	else
		snprintf(cmd, sizeof(cmd), "%s %s \"+call cursor(%d, %d)\" %s", vicmd,
				fork_str, line, column, escaped);

	free(escaped);

	result = run_vim(cmd, bg && allow_forking, allow_forking);

	ui_set_cursor(/*visibility=*/0);

	return result;
}

/* Removes all trailing whitespace. */
TSTATIC void
trim_right(char str[])
{
	size_t len = strlen(str);
	while(len > 0U && isspace(str[len - 1U]))
	{
		str[--len] = '\0';
	}
}

/* Runs command with specified settings.  Returns exit code of the command. */
static int
run_vim(const char cmd[], int bg, int use_term_multiplexer)
{
	if(bg)
	{
		return bg_run_external(cmd, 0, SHELL_BY_APP, NULL);
	}

	return rn_shell(cmd, PAUSE_ON_ERROR, use_term_multiplexer, SHELL_BY_APP);
}

int
vim_write_file_list(view_t *view, int nfiles, char *files[])
{
	FILE *fp;
	const char *const files_out = curr_stats.chosen_files_out;

	if(is_null_or_empty(files_out))
	{
		return 0;
	}

	if(strcmp(files_out, "-") == 0)
	{
		dump_filenames(view, curr_stats.original_stdout, nfiles, files);
		return 0;
	}

	fp = os_fopen(files_out, "w");
	if(fp == NULL)
	{
		LOG_SERROR_MSG(errno, "Can't open file for writing: \"%s\"", files_out);
		return 1;
	}

	dump_filenames(view, fp, nfiles, files);
	fclose(fp);
	return 0;
}

/* Writes list of full paths to files into the file pointed to by fp.  files and
 * nfiles parameters can be used to supply list of file names in the current
 * directory of the view.  Otherwise current selection is used if current files
 * is selected, if current file is not selected it's the only one that is
 * stored. */
static void
dump_filenames(view_t *view, FILE *fp, int nfiles, char *files[])
{
	/* Break delimiter in it's first character and the rest to be able to insert
	 * null character via "%c%s" format string. */
	const char delim_c = curr_stats.output_delimiter[0];
	const char *const delim_str = curr_stats.output_delimiter[0] == '\0'
	                            ? ""
	                            : curr_stats.output_delimiter + 1;

	if(nfiles == 0)
	{
		dir_entry_t *entry = NULL;
		while(iter_marked_entries(view, &entry))
		{
			const char *const sep = (ends_with_slash(entry->origin) ? "" : "/");
			fprintf(fp, "%s%s%s%c%s", entry->origin, sep, entry->name, delim_c,
					delim_str);
		}
	}
	else
	{
		int i;
		for(i = 0; i < nfiles; ++i)
		{
			if(is_path_absolute(files[i]))
			{
				fprintf(fp, "%s%c%s", files[i], delim_c, delim_str);
			}
			else
			{
				const char *const dir = flist_get_dir(view);
				const char *const sep = (ends_with_slash(dir) ? "" : "/");
				fprintf(fp, "%s%s%s%c%s", dir, sep, files[i], delim_c, delim_str);
			}
		}
	}
}

void
vim_write_empty_file_list(void)
{
	FILE *fp;
	const char *const files_out = curr_stats.chosen_files_out;

	if(is_null_or_empty(files_out) || strcmp(files_out, "-") == 0)
	{
		return;
	}

	fp = os_fopen(files_out, "w");
	if(fp != NULL)
	{
		fclose(fp);
	}
	else
	{
		LOG_SERROR_MSG(errno, "Can't truncate file: \"%s\"", files_out);
	}
}

void
vim_write_dir(const char path[])
{
	/* TODO: move this and other non-Vim related code to extern.c unit. */

	FILE *fp;
	const char *const dir_out = curr_stats.chosen_dir_out;

	if(is_null_or_empty(dir_out))
	{
		return;
	}

	if(strcmp(dir_out, "-") == 0)
	{
		fputs(path, curr_stats.original_stdout);
		putc('\n', curr_stats.original_stdout);
		return;
	}

	fp = os_fopen(dir_out, "w");
	if(fp == NULL)
	{
		LOG_SERROR_MSG(errno, "Can't open file for writing: \"%s\"", dir_out);
		return;
	}

	fputs(path, fp);
	putc('\n', fp);
	fclose(fp);
}

int
vim_run_choose_cmd(view_t *view)
{
	char *expanded_cmd;

	if(is_null_or_empty(curr_stats.on_choose))
	{
		return 0;
	}

	expanded_cmd = ma_expand(curr_stats.on_choose, NULL, NULL, MER_SHELL_OP);
	if(vifm_system(expanded_cmd, SHELL_BY_USER) != EXIT_SUCCESS)
	{
		free(expanded_cmd);
		return 1;
	}

	return 0;
}

void
vim_get_list_file_path(char buf[], size_t buf_size)
{
	snprintf(buf, buf_size, "%s/" LIST_FILE, cfg.config_dir);
}

/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
/* vim: set cinoptions+=t0 filetype=c : */
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/vifm

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/vifm

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master