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 / engine / autocmds.c (e046395e4694164cf2a0a2769aa635c56c7220e5) (7,786B) (mode 100644) [raw]
/* vifm
 * Copyright (C) 2015 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 "autocmds.h"

#include <regex.h> /* regex_t regcomp() regexec() regfree() */

#include <stddef.h> /* size_t */
#include <stdlib.h> /* free() */
#include <string.h> /* strcasecmp() strchr() strdup() */

#include "../compat/fs_limits.h"
#include "../compat/reallocarray.h"
#include "../utils/darray.h"
#include "../utils/globs.h"
#include "../utils/path.h"
#include "../utils/str.h"
#include "../utils/string_array.h"

/* Describes single registered autocommand. */
typedef struct
{
	char *event;               /* Name of the event (case is ignored). */
	char *pattern;             /* Pattern for the path. */
	regex_t regex;             /* The pattern in compiled form. */
	char *action;              /* Action to perform via handler. */
	vle_aucmd_handler handler; /* Handler to invoke on event firing. */
	int negated;               /* Whether pattern is negated. */
}
aucmd_info_t;

static int add_aucmd(const char event[], const char pattern[], int negated,
		const char action[], vle_aucmd_handler handler);
static int is_pattern_match(const aucmd_info_t *autocmd, const char path[]);
static void free_autocmd_data(aucmd_info_t *autocmd);
static char ** get_patterns(const char patterns[], int *len);

/* List of registered autocommands. */
static aucmd_info_t *autocmds;
/* Declarations to enable use of DA_* on autocmds. */
static DA_INSTANCE(autocmds);

/* Pattern expansion hook. */
static vle_aucmd_expand_hook expand_hook = &strdup;

void
vle_aucmd_set_expand_hook(vle_aucmd_expand_hook hook)
{
	expand_hook = hook;
}

int
vle_aucmd_on_execute(const char event[], const char patterns[],
		const char action[], vle_aucmd_handler handler)
{
	int err = 0;

	char *free_this = strdup(patterns);

	char *pat = free_this, *state = NULL;
	while((pat = split_and_get_dc(pat, &state)) != NULL)
	{
		const int negated = (*pat == '!');
		char *const expanded_pat = expand_hook(negated ? (pat + 1) : pat);
		if(expanded_pat == NULL)
		{
			err = 1;
			continue;
		}

		err += (add_aucmd(event, expanded_pat, negated, action, handler) != 0);
		free(expanded_pat);
	}

	free(free_this);
	return err;
}

/* Registers action handler for a particular combination of event and path
 * pattern.  Event name is case insensitive.  Returns zero on successful
 * registration or non-zero on error. */
static int
add_aucmd(const char event[], const char pattern[], int negated,
		const char action[], vle_aucmd_handler handler)
{
	char canonic_path[PATH_MAX + 1];
	aucmd_info_t *autocmd;
	char *regexp;

	autocmd = DA_EXTEND(autocmds);
	if(autocmd == NULL)
	{
		return 1;
	}

	if(strchr(pattern, '/') != NULL)
	{
		canonicalize_path(pattern, canonic_path, sizeof(canonic_path));
		if(!is_root_dir(canonic_path))
		{
			chosp(canonic_path);
		}
		pattern = canonic_path;
	}

	regexp = glob_to_regex(pattern, 1);
	if(regexp == NULL)
	{
		return 1;
	}

	if(regcomp(&autocmd->regex, regexp, REG_EXTENDED | REG_ICASE) != 0)
	{
		free(regexp);
		return 1;
	}
	free(regexp);

	autocmd->event = strdup(event);
	autocmd->pattern = strdup(pattern);
	autocmd->negated = negated;
	autocmd->action = strdup(action);
	autocmd->handler = handler;
	if(autocmd->event == NULL || autocmd->pattern == NULL ||
			autocmd->action == NULL)
	{
		free_autocmd_data(autocmd);
		return 1;
	}

	DA_COMMIT(autocmds);
	/* TODO: sort by event name (case insensitive) and then by pattern? */
	return 0;
}

void
vle_aucmd_execute(const char event[], const char path[], void *arg)
{
	size_t i;
	char canonic_path[PATH_MAX + 1];

	canonicalize_path(path, canonic_path, sizeof(canonic_path));
	if(!is_root_dir(canonic_path))
	{
		chosp(canonic_path);
	}

	for(i = 0U; i < DA_SIZE(autocmds); ++i)
	{
		if(strcasecmp(event, autocmds[i].event) == 0 &&
				is_pattern_match(&autocmds[i], canonic_path))
		{
			autocmds[i].handler(autocmds[i].action, arg);
		}
	}
}

/* Checks whether path matches pattern in the autocommand.  Returns non-zero if
 * so, otherwise zero is returned. */
static int
is_pattern_match(const aucmd_info_t *autocmd, const char path[])
{
	const char *const part = (strchr(autocmd->pattern, '/') == NULL)
	                       ? get_last_path_component(path)
	                       : path;

	/* Leading start shouldn't match dot at the first character.  Can't be
	 * handled by globs->regex translation. */
	if(autocmd->pattern[0] == '*' && part[0] == '.')
	{
		return 0;
	}

	return (regexec(&autocmd->regex, part, 0, NULL, 0) == 0)^autocmd->negated;
}

void
vle_aucmd_remove(const char event[], const char patterns[])
{
	int i;
	int len;
	char **pats = get_patterns(patterns, &len);

	for(i = (int)DA_SIZE(autocmds) - 1; i >= 0; --i)
	{
		char pat[1U + strlen(autocmds[i].pattern) + 1U];

		copy_str(&pat[1], sizeof(pat) - 1U, autocmds[i].pattern);
		pat[0] = autocmds[i].negated ? '!' : '=';

		if(event != NULL && strcasecmp(event, autocmds[i].event) != 0)
		{
			continue;
		}
		if(patterns != NULL && !is_in_string_array(pats, len, pat))
		{
			continue;
		}

		free_autocmd_data(&autocmds[i]);
		DA_REMOVE(autocmds, &autocmds[i]);
	}

	free_string_array(pats, len);
}

/* Frees data allocated for the autocommand. */
static void
free_autocmd_data(aucmd_info_t *autocmd)
{
	free(autocmd->event);
	free(autocmd->pattern);
	free(autocmd->action);
	regfree(&autocmd->regex);
}

void
vle_aucmd_list(const char event[], const char patterns[], vle_aucmd_list_cb cb,
		void *arg)
{
	size_t i;
	int len;
	char **pats = get_patterns(patterns, &len);

	for(i = 0U; i < DA_SIZE(autocmds); ++i)
	{
		char pat[1U + strlen(autocmds[i].pattern) + 1U];

		copy_str(&pat[1], sizeof(pat) - 1U, autocmds[i].pattern);
		pat[0] = autocmds[i].negated ? '!' : '=';

		if(event != NULL && strcasecmp(event, autocmds[i].event) != 0)
		{
			continue;
		}
		if(patterns != NULL && !is_in_string_array(pats, len, pat))
		{
			continue;
		}

		cb(autocmds[i].event, autocmds[i].pattern, autocmds[i].negated,
				autocmds[i].action, arg);
	}

	free_string_array(pats, len);
}

/* Parses single pattern string into list of patterns.  Returns the list and
 * writes its length into *len.  Each pattern in the list is prepended with
 * either "!" or "=" to indicate negation. */
static char **
get_patterns(const char patterns[], int *len)
{
	char **pats = NULL;
	*len = 0;

	if(patterns != NULL)
	{
		char *free_this = strdup(patterns);

		char *pat = free_this, *state = NULL;
		while((pat = split_and_get_dc(pat, &state)) != NULL)
		{
			const int negated = (*pat == '!');
			char canonic_path[PATH_MAX + 1];
			char *path = &canonic_path[1];

			char *const expanded_pat = expand_hook(negated ? (pat + 1) : pat);
			if(expanded_pat == NULL)
			{
				continue;
			}

			canonicalize_path(expanded_pat, path, sizeof(canonic_path) - 1);
			if(!is_root_dir(path))
			{
				chosp(path);
			}

			*--path = negated ? '!' : '=';
			*len = add_to_string_array(&pats, *len, path);
			free(expanded_pat);
		}

		free(free_this);
	}

	return pats;
}

/* 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