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_viewcolumns.c (b734d5cb984297c38c9d661d1995acae90c723bf) (7,350B) (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_viewcolumns.h"

#include <stdlib.h> /* free() */
#include <string.h> /* strdup() */

#include "../compat/fs_limits.h"
#include "../ui/column_view.h"
#include "../ui/fileview.h"
#include "../ui/statusbar.h"
#include "../ui/ui.h"
#include "../utils/str.h"
#include "../filelist.h"
#include "../types.h"
#include "lua/lauxlib.h"
#include "lua/lua.h"
#include "api.h"
#include "common.h"
#include "vifmentry.h"
#include "vlua_state.h"

static int check_viewcolumn_name(vlua_t *vlua, const char name[]);
static void lua_viewcolumn_handler(void *data, size_t buf_len, char buf[],
		const format_info_t *info);

/* Minimal ID for columns added by this view. */
enum { FIRST_LUA_COLUMN_ID = SK_TOTAL };

/* Address of this variable serves as a key in Lua table.  The associated table
 * is doubly keyed: by column name and by corresponding ID. */
static char viewcolumns_key;
/* Next id for a view column. */
static int viewcolumn_next_id = FIRST_LUA_COLUMN_ID;

void
vifm_viewcolumns_init(vlua_t *vlua)
{
	vifmentry_init(vlua->lua);

	vlua_state_make_table(vlua, &viewcolumns_key);
}

int
vifm_viewcolumns_next_id(vlua_t *vlua)
{
	return viewcolumn_next_id;
}

int
vifm_viewcolumns_map(vlua_t *vlua, const char name[])
{
	/* Don't need lua_pcall() to handle errors, because no one should be able to
	 * mess with internal tables. */
	vlua_state_get_table(vlua, &viewcolumns_key);
	if(lua_getfield(vlua->lua, -1, name) != LUA_TTABLE)
	{
		lua_pop(vlua->lua, 2);
		return -1;
	}

	lua_getfield(vlua->lua, -1, "id");
	int id = lua_tointeger(vlua->lua, -1);
	lua_pop(vlua->lua, 3);
	return id;
}

char *
vifm_viewcolumns_map_back(vlua_t *vlua, int id)
{
	/* Don't need lua_pcall() to handle errors, because no one should be able to
	 * mess with internal tables. */
	vlua_state_get_table(vlua, &viewcolumns_key);
	if(lua_geti(vlua->lua, -1, id) != LUA_TTABLE)
	{
		lua_pop(vlua->lua, 2);
		return NULL;
	}

	lua_getfield(vlua->lua, -1, "name");
	char *name = strdup(lua_tostring(vlua->lua, -1));
	lua_pop(vlua->lua, 3);
	return name;
}

int
vifm_viewcolumns_is_primary(vlua_t *vlua, int column_id)
{
	if(column_id < FIRST_LUA_COLUMN_ID)
	{
		return 0;
	}

	/* Don't need lua_pcall() to handle errors, because no one should be able to
	 * mess with internal tables. */
	vlua_state_get_table(vlua, &viewcolumns_key);
	if(lua_geti(vlua->lua, -1, column_id) != LUA_TTABLE)
	{
		lua_pop(vlua->lua, 2);
		return -1;
	}

	lua_getfield(vlua->lua, -1, "isprimary");
	int is_primary = lua_toboolean(vlua->lua, -1);
	lua_pop(vlua->lua, 3);
	return is_primary;
}

int
VLUA_API(vifm_addcolumntype)(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);
	check_viewcolumn_name(vlua, name);

	vlua_cmn_check_field(lua, 1, "handler", LUA_TFUNCTION);
	void *handler = vlua_cmn_to_pointer(lua);

	int is_primary = 0;
	if(vlua_cmn_check_opt_field(lua, 1, "isprimary", LUA_TBOOLEAN))
	{
		is_primary = lua_toboolean(vlua->lua, -1);
	}

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

	int column_id = viewcolumn_next_id++;
	vlua_state_get_table(vlua, &viewcolumns_key); /* viewcolumns table */
	lua_createtable(lua, /*narr=*/0, /*nrec=*/2); /* viewcolumn table */
	lua_pushinteger(lua, column_id);
	lua_setfield(lua, -2, "id");
	lua_pushstring(lua, name);
	lua_setfield(lua, -2, "name");
	lua_pushboolean(lua, is_primary);
	lua_setfield(lua, -2, "isprimary");
	lua_pushvalue(lua, -1);                       /* viewcolumn table */
	lua_setfield(lua, -3, name);                  /* viewcolumns[name] */
	lua_seti(lua, -2, column_id);                 /* viewcolumns[id] */

	int error = columns_add_column_desc(column_id, &lua_viewcolumn_handler, data);
	if(error)
	{
		vlua_cmn_drop_pointer(lua, handler);
	}
	lua_pushboolean(lua, !error);
	return 1;
}

/* Verifies validity of a user-defined view column name.  Return value has no
 * particular meaning. */
static int
check_viewcolumn_name(vlua_t *vlua, const char name[])
{
	if(vifm_viewcolumns_map(vlua, name) != -1)
	{
		return luaL_error(vlua->lua,
				"View column with such name already exists: %s", name);
	}

	if(name[0] == '\0')
	{
		return luaL_error(vlua->lua, "%s", "View column name can't be empty");
	}
	if(name[0] >= 'a' && name[0] <= 'z')
	{
		return luaL_error(vlua->lua, "%s",
				"View column name must not start with a lower case Latin letter");
	}

	while(*name)
	{
		char c = *name++;
		if(c >= 'a' && c <= 'z') continue;
		if(c >= 'A' && c <= 'Z') continue;
		return luaL_error(vlua->lua, "%s",
				"View column name must not contain non-Latin characters");
	}

	return 0;
}

/* Handler of all user-defined view columns registered from Lua. */
static void
lua_viewcolumn_handler(void *data, size_t buf_len, char buf[],
		const format_info_t *info)
{
	state_ptr_t *p = data;
	lua_State *lua = p->vlua->lua;

	vlua_cmn_from_pointer(lua, p->ptr);

	lua_createtable(lua, /*narr=*/0, /*nrec=*/2);

	lua_pushinteger(lua, info->width);
	lua_setfield(lua, -2, "width");

	column_data_t *cdt = info->data;
	dir_entry_t *entry = cdt->entry;
	vifmentry_new(lua, entry);
	lua_setfield(lua, -2, "entry");

	/* No match highlighting by default. */
	cdt->custom_match = 1;
	cdt->match_from = 0;
	cdt->match_to = 0;

	const int sm_cookie = vlua_state_safe_mode_on(lua);
	if(lua_pcall(lua, 1, 1, 0) != LUA_OK)
	{
		vlua_state_safe_mode_off(lua, sm_cookie);

		const char *error = lua_tostring(lua, -1);
		ui_sb_err(error);
		copy_str(buf, buf_len, "ERROR");
		lua_pop(lua, 1);
		return;
	}

	vlua_state_safe_mode_off(lua, sm_cookie);

	if(!lua_istable(lua, -1))
	{
		copy_str(buf, buf_len, "NOVALUE");
		lua_pop(lua, 1);
		return;
	}

	if(lua_getfield(lua, -1, "text") == LUA_TNIL)
	{
		copy_str(buf, buf_len, "NOVALUE");
		lua_pop(lua, 2);
		return;
	}

	const char *text = lua_tostring(lua, -1);
	copy_str(buf, buf_len, (text == NULL ? "NOVALUE" : text));
	lua_pop(lua, 1);

	int has_start = (lua_getfield(lua, -1, "matchstart") == LUA_TNUMBER);
	int has_end = (lua_getfield(lua, -2, "matchend") == LUA_TNUMBER);
	if(has_start && has_end)
	{
		int start = lua_tointegerx(lua, -2, &has_start) - 1;
		int end = lua_tointegerx(lua, -1, &has_end) - 1;

		if(has_start && has_end && start >= 0 && end >= 0 && start <= end)
		{
			cdt->match_from = start;
			cdt->match_to = end;
		}
	}

	lua_pop(lua, 3);
}

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