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 / lua / vifm_cmds.c (da97537de51e173795156a1456c7a1a56456e1a8) (7,601B) (mode 100644) [raw]
/* vifm
 * Copyright (C) 2021 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 "vifm_cmds.h"

#include <limits.h> /* INT_MAX */

#include "../engine/cmds.h"
#include "../engine/completion.h"
#include "../ui/statusbar.h"
#include "../utils/str.h"
#include "../cmd_completion.h"
#include "../status.h"
#include "lua/lauxlib.h"
#include "lua/lua.h"
#include "api.h"
#include "common.h"
#include "vlua_state.h"

static int VLUA_API(cmds_add)(lua_State *lua);
static void parse_cmd_params(vlua_t *vlua, cmd_add_t *cmd);
static int VLUA_API(cmds_command)(lua_State *lua);
static int VLUA_API(cmds_delcommand)(lua_State *lua);
static int lua_cmd_handler(const cmd_info_t *cmd_info);
static int apply_completion(lua_State *lua, const char str[]);

VLUA_DECLARE_SAFE(cmds_add);
VLUA_DECLARE_SAFE(cmds_command);
VLUA_DECLARE_SAFE(cmds_delcommand);

/* Functions of `vifm.cmds` table. */
static const luaL_Reg vifm_cmds_methods[] = {
	{ "add",        VLUA_REF(cmds_add)        },
	{ "command",    VLUA_REF(cmds_command)    },
	{ "delcommand", VLUA_REF(cmds_delcommand) },
	{ NULL,         NULL                      }
};

void
vifm_cmds_init(lua_State *lua)
{
	luaL_newlib(lua, vifm_cmds_methods);
}

/* Member of `vifm.cmds` that registers a new :command or raises an error.
 * Returns boolean, which is true on success. */
static int
VLUA_API(cmds_add)(lua_State *lua)
{
	vlua_t *vlua = vlua_state_get(lua);

	luaL_checktype(lua, 1, LUA_TTABLE);

	vlua_cmn_check_field(lua, 1, "name", LUA_TSTRING);
	const char *name = lua_tostring(lua, -1);

	int id = -1;

	lua_createtable(lua, /*narr=*/0, /*nrec=*/1);
	vlua_cmn_check_field(lua, 1, "handler", LUA_TFUNCTION);
	lua_setfield(lua, -2, "handler");
	if(vlua_cmn_check_opt_field(lua, 1, "complete", LUA_TFUNCTION))
	{
		lua_setfield(lua, -2, "complete");
		id = COM_FOREIGN;
	}
	void *handler = vlua_cmn_to_pointer(lua);

	cmd_add_t cmd = {
	  .name = name,
	  .abbr = NULL,
	  .id = id,
	  .descr = "",
	  .flags = 0,
	  .handler = &lua_cmd_handler,
	  .user_data = NULL,
	  .min_args = 0,
	  .max_args = 0,
	};

	parse_cmd_params(vlua, &cmd);

	cmd.user_data = vlua_state_store_pointer(vlua, handler);
	if(cmd.user_data == NULL)
	{
		return luaL_error(lua, "%s", "Failed to store handler data");
	}

	lua_pushboolean(lua, vle_cmds_add_foreign(&cmd) == 0);
	return 1;
}

/* Initializes fields of cmd_add_t from parameter for `vifm.cmds.add`. */
static void
parse_cmd_params(vlua_t *vlua, cmd_add_t *cmd)
{
	if(vlua_cmn_check_opt_field(vlua->lua, 1, "description", LUA_TSTRING))
	{
		cmd->descr = vlua_state_store_string(vlua, lua_tostring(vlua->lua, -1));
	}

	if(vlua_cmn_check_opt_field(vlua->lua, 1, "minargs", LUA_TNUMBER))
	{
		cmd->min_args = lua_tointeger(vlua->lua, -1);
	}
	if(vlua_cmn_check_opt_field(vlua->lua, 1, "maxargs", LUA_TNUMBER))
	{
		cmd->max_args = lua_tointeger(vlua->lua, -1);
		if(cmd->max_args < 0)
		{
			cmd->max_args = NOT_DEF;
		}
	}
	else
	{
		cmd->max_args = cmd->min_args;
	}
}

/* Member of `vifm.command` that registers a new user-defined :command or raises
 * an error.  Returns boolean, which is true on success. */
static int
VLUA_API(cmds_command)(lua_State *lua)
{
	vlua_t *vlua = vlua_state_get(lua);

	luaL_checktype(lua, 1, LUA_TTABLE);

	vlua_cmn_check_field(lua, 1, "name", LUA_TSTRING);
	const char *name = lua_tostring(lua, -1);

	vlua_cmn_check_field(lua, 1, "action", LUA_TSTRING);
	const char *action = skip_whitespace(lua_tostring(lua, -1));
	if(action[0] == '\0')
	{
		return luaL_error(lua, "%s", "Action can't be empty");
	}

	const char *descr = NULL;
	if(vlua_cmn_check_opt_field(lua, 1, "description", LUA_TSTRING))
	{
		descr = vlua_state_store_string(vlua, lua_tostring(lua, -1));
	}

	int overwrite = 0;
	if(vlua_cmn_check_opt_field(lua, 1, "overwrite", LUA_TBOOLEAN))
	{
		overwrite = lua_toboolean(lua, -1);
	}

	int success = (vle_cmds_add_user(name, action, descr, overwrite) == 0);
	lua_pushboolean(lua, success);
	return 1;
}

/* Member of `vifm.command` that unregisters a user-defined :command.  Returns
 * boolean, which is true on success. */
static int
VLUA_API(cmds_delcommand)(lua_State *lua)
{
	const char *name = luaL_checkstring(lua, 1);

	int success = (vle_cmds_del_user(name) == 0);
	lua_pushboolean(lua, success);
	return 1;
}

/* Handler of all foreign :commands registered from Lua. */
static int
lua_cmd_handler(const cmd_info_t *cmd_info)
{
	state_ptr_t *p = cmd_info->user_data;
	lua_State *lua = p->vlua->lua;

	vlua_cmn_from_pointer(lua, p->ptr);
	lua_getfield(lua, -1, "handler");

	lua_createtable(lua, /*narr=*/0, /*nrec=*/2);
	lua_pushstring(lua, cmd_info->args);
	lua_setfield(lua, -2, "args");
	vlua_cmn_push_str_array(lua, cmd_info->argv, cmd_info->argc);
	lua_setfield(lua, -2, "argv");

	curr_stats.save_msg = 0;

	if(lua_pcall(lua, 1, 0, 0) != LUA_OK)
	{
		const char *error = lua_tostring(lua, -1);
		ui_sb_err(error);
		lua_pop(lua, 2);
		return CMDS_ERR_CUSTOM;
	}

	lua_pop(lua, 1);
	return curr_stats.save_msg;
}

int
vifm_cmds_complete(lua_State *lua, const cmd_info_t *cmd_info, int arg_pos)
{
	state_ptr_t *p = cmd_info->user_data;

	vlua_cmn_from_pointer(lua, p->ptr);
	if(lua_getfield(lua, -1, "complete") == LUA_TNIL)
	{
		return 0;
	}

	lua_createtable(lua, /*narr=*/0, /*nrec=*/3);
	lua_pushstring(lua, cmd_info->args);
	lua_setfield(lua, -2, "args");
	vlua_cmn_push_str_array(lua, cmd_info->argv, cmd_info->argc);
	lua_setfield(lua, -2, "argv");
	lua_pushstring(lua, cmd_info->args + arg_pos);
	lua_setfield(lua, -2, "arg");

	if(lua_pcall(lua, 1, 1, 0) != LUA_OK)
	{
		lua_pop(lua, 2);
		return 0;
	}

	if(lua_type(lua, -1) != LUA_TTABLE)
	{
		return 0;
	}

	return apply_completion(lua, cmd_info->args + arg_pos);
}

/* Does stack cleanup for the caller (-2).  Returns offset of completion
 * matches. */
static int
apply_completion(lua_State *lua, const char str[])
{
	int offset = 0;
	if(lua_getfield(lua, -1, "offset") == LUA_TNUMBER)
	{
		LUA_INTEGER n = lua_tointeger(lua, -1);
		if(n > 0 && n <= INT_MAX)
		{
			offset = n;
		}
	}
	lua_pop(lua, 1);

	if(lua_getfield(lua, -1, "matches") == LUA_TTABLE)
	{
		lua_pushnil(lua);
		while(lua_next(lua, -2) != 0)
		{
			const char *match = NULL;
			const char *descr = NULL;

			if(lua_type(lua, -1) == LUA_TSTRING)
			{
				match = lua_tostring(lua, -1);
			}
			else if(lua_type(lua, -1) == LUA_TTABLE)
			{
				(void)lua_getfield(lua, -1, "match");
				match = lua_tostring(lua, -1);

				if(match != NULL)
				{
					(void)lua_getfield(lua, -2, "description");
					descr = lua_tostring(lua, -1);
					lua_pop(lua, 1);
				}

				lua_pop(lua, 1);
			}

			if(match != NULL)
			{
				vle_compl_add_match(match, descr == NULL ? "" : descr);
			}

			lua_pop(lua, 1);
		}

		vle_compl_finish_group();
		vle_compl_add_last_match(str);
	}
	lua_pop(lua, 1);

	lua_pop(lua, 2);
	return offset;
}

/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
/* vim: set cinoptions+=t0 : */
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