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.
Commit 66ce2c106af7e3146723f046b685ed6ef718f3fe

Allow implementing custom selectors in Lua
Like "a" and "A" in "da", "dA".
Author: xaizek
Author date (UTC): 2022-03-06 23:01
Committer name: xaizek
Committer date (UTC): 2022-03-06 23:08
Parent(s): 530c7445d4ea3e60e52fe4d994ee445278a2941a
Signing key: 99DC5E4DB05F6BE2
Tree: 1058e058ee47d1d0e8c87bbb28aaddc69257e225
File Lines added Lines deleted
data/vim/doc/app/vifm-lua.txt 22 5
src/lua/vifm_keys.c 141 13
tests/lua/api_keys.c 156 3
File data/vim/doc/app/vifm-lua.txt changed (mode: 100644) (index 2f7453aee..95039099b)
1 *vifm-lua.txt* For Vifm version 1.0 Last change: 2022 Feb 6
1 *vifm-lua.txt* For Vifm version 1.0 Last change: 2022 Mar 7
2 2
3 3 Email for bugs and suggestions: <xaizek@posteo.net> Email for bugs and suggestions: <xaizek@posteo.net>
4 4
 
... ... This is what one can do by using Lua at the moment:
226 226 * :filetype/:filextype/:fileviewer/%q handlers in Lua (|vifm-lua-handlers|) * :filetype/:filextype/:fileviewer/%q handlers in Lua (|vifm-lua-handlers|)
227 227 * 'statusline' generator in Lua (|vifm-lua-handlers|) * 'statusline' generator in Lua (|vifm-lua-handlers|)
228 228 * starting background jobs that appear on a job bar (|vifm-l_vifm.startjob()|) * starting background jobs that appear on a job bar (|vifm-l_vifm.startjob()|)
229 * defining custom keys and selectors (|vifm-l_vifm.keys.add()|)
229 230
230 231 -------------------------------------------------------------------------------- --------------------------------------------------------------------------------
231 232 *vifm-lua-api* *vifm-lua-api*
 
... ... Possible fields of {column}:
291 292 Whether this column is highlighted with file color and search match Whether this column is highlighted with file color and search match
292 293 is highlighted as well. is highlighted as well.
293 294
295 {column}.handler is executed in a safe environment and can't call API marked
296 as {unsafe}.
297
294 298 Fields of {info} argument for {column}.handler: Fields of {info} argument for {column}.handler:
295 299 - "entry" (table) - "entry" (table)
296 300 Information about a file list entry as an instance of |vifm-l_VifmEntry|. Information about a file list entry as an instance of |vifm-l_VifmEntry|.
 
... ... Return:~
551 555 This global `vifm.keys` table provides means of managing key bindings. This global `vifm.keys` table provides means of managing key bindings.
552 556
553 557 vifm.keys.add({key}) *vifm-l_vifm.keys.add()* vifm.keys.add({key}) *vifm-l_vifm.keys.add()*
554 Registers a new user-defined key in one or several modes or replaces an
555 existing one.
558 Registers in one or several modes:
559 * a new user-defined key which might replace an existing one
560 * a new selector which must not conflict with an existing one
556 561
557 562 Possible fields of {key}: Possible fields of {key}:
558 563 - "shortcut" (string) - "shortcut" (string)
 
... ... Possible fields of {key}:
563 568 List of modes to register the key in. Supported values: cmdline, normal, List of modes to register the key in. Supported values: cmdline, normal,
564 569 visual, menus, view and dialogs (sort, attributes, change and file info). visual, menus, view and dialogs (sort, attributes, change and file info).
565 570 Unsupported values are ignored. Unsupported values are ignored.
571 - "isselector" (boolean) (default: false)
572 Whether this handler defines a selector rather than a regular key.
566 573 - "handler" (function) - "handler" (function)
567 Handler which accepts {info}.
574 Handler which accepts {info} and returns a table for selector handlers.
575 See below.
568 576
569 Fields of {info} argument for "handler":
577 {key}.handler for selectors is executed in a safe environment and can't call
578 API marked as {unsafe}.
579
580 Fields of {info} argument for {key}.handler:
570 581 - "count" (integer) - "count" (integer)
571 582 Count preceding the key or nil if none. Count preceding the key or nil if none.
572 583 - "register" (string) - "register" (string)
573 584 Register name or nil if none was specified. Register name or nil if none was specified.
574 585
586 Fields of table returned by {key}.handler for selectors:
587 - "indexes" (table)
588 Table of indexes of entries of the current view (|vifm-l_vifm.currview()|)
589 that were selected for the operation. Out of range values and duplicates
590 are silently ignored. Indexes are sorted before processing.
591
575 592 Parameters:~ Parameters:~
576 593 {cmd} Table with information about a key. {cmd} Table with information about a key.
577 594
File src/lua/vifm_keys.c changed (mode: 100644) (index 160931f3a..1c59fc4cb)
20 20
21 21 #include <wchar.h> #include <wchar.h>
22 22
23 #include "../compat/reallocarray.h"
23 24 #include "../engine/keys.h" #include "../engine/keys.h"
24 25 #include "../modes/modes.h" #include "../modes/modes.h"
25 26 #include "../ui/statusbar.h" #include "../ui/statusbar.h"
27 #include "../ui/ui.h"
26 28 #include "../utils/macros.h" #include "../utils/macros.h"
27 29 #include "../utils/str.h" #include "../utils/str.h"
30 #include "../utils/utils.h"
28 31 #include "../bracket_notation.h" #include "../bracket_notation.h"
29 32 #include "../status.h" #include "../status.h"
30 33 #include "lua/lauxlib.h" #include "lua/lauxlib.h"
 
... ... VLUA_DECLARE_SAFE(keys_add);
39 42
40 43 static void parse_modes(vlua_t *vlua, char modes[MODES_COUNT]); static void parse_modes(vlua_t *vlua, char modes[MODES_COUNT]);
41 44 static void lua_key_handler(key_info_t key_info, keys_info_t *keys_info); static void lua_key_handler(key_info_t key_info, keys_info_t *keys_info);
45 static void build_handler_args(lua_State *lua, key_info_t key_info);
46 static int extract_indexes(lua_State *lua, keys_info_t *keys_info);
47 static int deduplicate_ints(int array[], int count);
48 static int int_sorter(const void *first, const void *second);
42 49
43 50 /* Functions of `vifm.keys` table. */ /* Functions of `vifm.keys` table. */
44 51 static const luaL_Reg vifm_keys_methods[] = { static const luaL_Reg vifm_keys_methods[] = {
 
... ... VLUA_API(keys_add)(lua_State *lua)
81 88 descr = state_store_string(vlua, lua_tostring(lua, -1)); descr = state_store_string(vlua, lua_tostring(lua, -1));
82 89 } }
83 90
91 int is_selector = 0;
92 if(check_opt_field(lua, 1, "isselector", LUA_TBOOLEAN))
93 {
94 is_selector = lua_toboolean(lua, -1);
95 }
96
84 97 char modes[MODES_COUNT] = { }; char modes[MODES_COUNT] = { };
85 98 check_field(lua, 1, "modes", LUA_TTABLE); check_field(lua, 1, "modes", LUA_TTABLE);
86 99 parse_modes(vlua, modes); parse_modes(vlua, modes);
 
... ... VLUA_API(keys_add)(lua_State *lua)
88 101 lua_newtable(lua); lua_newtable(lua);
89 102 check_field(lua, 1, "handler", LUA_TFUNCTION); check_field(lua, 1, "handler", LUA_TFUNCTION);
90 103 lua_setfield(lua, -2, "handler"); lua_setfield(lua, -2, "handler");
104 lua_pushboolean(lua, is_selector);
105 lua_setfield(lua, -2, "isselector");
91 106 void *handler = to_pointer(lua); void *handler = to_pointer(lua);
92 107
93 108 key_conf_t key = { key_conf_t key = {
 
... ... VLUA_API(keys_add)(lua_State *lua)
103 118
104 119 int success = 1; int success = 1;
105 120
106 int i;
107 for(i = 0; i < MODES_COUNT; ++i)
121 int mode;
122 for(mode = 0; mode < MODES_COUNT; ++mode)
108 123 { {
109 if(modes[i])
124 if(modes[mode])
110 125 { {
111 success &= (vle_keys_foreign_add(lhs, &key, /*is_selector=*/0, i) == 0);
126 success &= (vle_keys_foreign_add(lhs, &key, is_selector, mode) == 0);
112 127 } }
113 128 } }
114 129
 
... ... lua_key_handler(key_info_t key_info, keys_info_t *keys_info)
166 181 lua_State *lua = p->vlua->lua; lua_State *lua = p->vlua->lua;
167 182
168 183 from_pointer(lua, p->ptr); from_pointer(lua, p->ptr);
169 lua_getfield(lua, -1, "handler");
184 lua_getfield(lua, -1, "isselector");
185 int is_selector = lua_toboolean(lua, -1);
186 lua_getfield(lua, -2, "handler");
187
188 build_handler_args(lua, key_info);
170 189
190 curr_stats.save_msg = 0;
191
192 if(is_selector)
193 {
194 vlua_state_safe_mode_set(lua, 1);
195 }
196
197 int result = lua_pcall(lua, 1, 1, 0);
198
199 if(is_selector)
200 {
201 vlua_state_safe_mode_set(lua, 0);
202 }
203
204 if(result != LUA_OK)
205 {
206 ui_sb_err(lua_tostring(lua, -1));
207 lua_pop(lua, 3);
208 curr_stats.save_msg = 1;
209 return;
210 }
211
212 if(is_selector && extract_indexes(lua, keys_info) == 0)
213 {
214 keys_info->count = deduplicate_ints(keys_info->indexes, keys_info->count);
215 }
216
217 lua_pop(lua, 3);
218 }
219
220 /* Builds table passed to key handler, leaves it at the top of the stack. */
221 static void
222 build_handler_args(lua_State *lua, key_info_t key_info)
223 {
171 224 lua_newtable(lua); lua_newtable(lua);
172 225
173 226 if(key_info.count == NO_COUNT_GIVEN) if(key_info.count == NO_COUNT_GIVEN)
 
... ... lua_key_handler(key_info_t key_info, keys_info_t *keys_info)
190 243 lua_pushstring(lua, reg_name); lua_pushstring(lua, reg_name);
191 244 } }
192 245 lua_setfield(lua, -2, "register"); lua_setfield(lua, -2, "register");
246 }
193 247
194 curr_stats.save_msg = 0;
248 /* Extracts selected indexes from "indexes" field of the table at the top of
249 * Lua stack. Returns zero on success and non-zero on error. */
250 static int
251 extract_indexes(lua_State *lua, keys_info_t *keys_info)
252 {
253 if(!lua_istable(lua, -1))
254 {
255 return 1;
256 }
195 257
196 if(lua_pcall(lua, 1, 0, 0) != LUA_OK)
258 if(lua_getfield(lua, -1, "indexes") != LUA_TTABLE)
197 259 { {
198 const char *error = lua_tostring(lua, -1);
199 ui_sb_err(error);
260 lua_pop(lua, 1);
261 return 1;
262 }
263
264 lua_len(lua, -1);
265 keys_info->count = lua_tointeger(lua, -1);
266
267 keys_info->indexes = reallocarray(NULL, keys_info->count,
268 sizeof(keys_info->indexes[0]));
269 if(keys_info->indexes == NULL)
270 {
271 keys_info->count = 0;
200 272 lua_pop(lua, 2); lua_pop(lua, 2);
201 curr_stats.save_msg = 1;
202 return;
273 return 1;
274 }
275
276 int i = 0;
277 lua_pushnil(lua);
278 while(lua_next(lua, -3) != 0)
279 {
280 int idx = lua_tointeger(lua, -1) - 1;
281 if(idx >= 0 && idx < curr_view->list_rows)
282 {
283 keys_info->indexes[i++] = idx;
284 }
285 lua_pop(lua, 1);
286 }
287 keys_info->count = i;
288
289 if(keys_info->count == 0)
290 {
291 free(keys_info->indexes);
292 keys_info->indexes = NULL;
293 }
294
295 lua_pop(lua, 2);
296 return 0;
297 }
298
299 /* Removes duplicates from array of ints while sorting it. Returns new array
300 * size. */
301 static int
302 deduplicate_ints(int array[], int count)
303 {
304 if(count == 0)
305 {
306 return 0;
307 }
308
309 /* Sort list of indexes to simplify finding duplicates. */
310 safe_qsort(array, count, sizeof(array[0]), &int_sorter);
311
312 /* Drop duplicates from the list of indexes. */
313 int i;
314 int j = 1;
315 for(i = 1; i < count; ++i)
316 {
317 if(array[i] != array[j - 1])
318 {
319 array[j++] = array[i];
320 }
203 321 } }
204 322
205 lua_pop(lua, 1);
206 return;
323 return j;
324 }
325
326 /* qsort() comparer that sorts ints. Returns standard -1, 0, 1 for
327 * comparisons. */
328 static int
329 int_sorter(const void *first, const void *second)
330 {
331 const int *a = first;
332 const int *b = second;
333
334 return (*a - *b);
207 335 } }
208 336
209 337 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */ /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
File tests/lua/api_keys.c changed (mode: 100644) (index 91d5eb8c9..f52219d2f)
1 1 #include <stic.h> #include <stic.h>
2 2
3 #include <string.h>
4
3 5 #include "../../src/engine/keys.h" #include "../../src/engine/keys.h"
4 6 #include "../../src/lua/vlua.h" #include "../../src/lua/vlua.h"
5 7 #include "../../src/ui/statusbar.h" #include "../../src/ui/statusbar.h"
6 8 #include "../../src/ui/ui.h" #include "../../src/ui/ui.h"
9 #include "../../src/utils/dynarray.h"
7 10 #include "../../src/utils/str.h" #include "../../src/utils/str.h"
8 11 #include "../../src/bracket_notation.h" #include "../../src/bracket_notation.h"
9 #include "../../src/status.h"
10
11 12 #include "../../src/modes/modes.h" #include "../../src/modes/modes.h"
12 13 #include "../../src/modes/wk.h" #include "../../src/modes/wk.h"
14 #include "../../src/registers.h"
15 #include "../../src/status.h"
16
17 #include <test-utils.h>
13 18
14 19 static vlua_t *vlua; static vlua_t *vlua;
15 20
16 21 SETUP_ONCE() SETUP_ONCE()
17 22 { {
18 23 init_bracket_notation(); init_bracket_notation();
24 stub_colmgr();
19 25 } }
20 26
21 27 SETUP() SETUP()
 
... ... SETUP()
26 32 curr_view = &lwin; curr_view = &lwin;
27 33 other_view = &rwin; other_view = &rwin;
28 34
35 view_setup(&lwin);
36 strcpy(lwin.curr_dir, "/lwin");
37 lwin.list_rows = 2;
38 lwin.list_pos = 1;
39 lwin.dir_entry = dynarray_cextend(NULL,
40 lwin.list_rows*sizeof(*lwin.dir_entry));
41 lwin.dir_entry[0].name = strdup("file0");
42 lwin.dir_entry[0].origin = &lwin.curr_dir[0];
43 lwin.dir_entry[1].name = strdup("file1");
44 lwin.dir_entry[1].origin = &lwin.curr_dir[0];
45
29 46 init_modes(); init_modes();
47 regs_init();
30 48 } }
31 49
32 50 TEARDOWN() TEARDOWN()
33 51 { {
52 view_teardown(&lwin);
53
34 54 vlua_finish(vlua); vlua_finish(vlua);
35 55 curr_stats.vlua = NULL; curr_stats.vlua = NULL;
36 56
37 57 vle_keys_reset(); vle_keys_reset();
58 regs_reset();
38 59 } }
39 60
40 61 TEST(keys_add_errors) TEST(keys_add_errors)
 
... ... TEST(keys_add_errors)
64 85 "}")); "}"));
65 86 assert_true(ends_with(ui_sb_last(), assert_true(ends_with(ui_sb_last(),
66 87 ": Shortcut can't be empty or longer than 15")); ": Shortcut can't be empty or longer than 15"));
88
89 assert_failure(vlua_run_string(vlua, "vifm.keys.add {"
90 " shortcut = 'X',"
91 " isselector = 10,"
92 "}"));
93 assert_true(ends_with(ui_sb_last(),
94 ": `isselector` value must be a boolean"));
67 95 } }
68 96
69 TEST(keys_bad_handler)
97 TEST(keys_bad_key_handler)
70 98 { {
71 99 assert_success(vlua_run_string(vlua, "function badhandler()\n" assert_success(vlua_run_string(vlua, "function badhandler()\n"
72 100 " adsf()\n" " adsf()\n"
 
... ... TEST(keys_bad_handler)
84 112 ": global 'adsf' is not callable (a nil value)")); ": global 'adsf' is not callable (a nil value)"));
85 113 } }
86 114
115 TEST(keys_bad_selector_handler)
116 {
117 assert_success(vlua_run_string(vlua, "function badhandler()\n"
118 " adsf()\n"
119 "end"));
120
121 assert_success(vlua_run_string(vlua, "print(vifm.keys.add {"
122 " shortcut = 'X',"
123 " modes = { 'normal' },"
124 " isselector = true,"
125 " handler = badhandler,"
126 "})"));
127 assert_string_equal("true", ui_sb_last());
128
129 (void)vle_keys_exec_timed_out(L"yX");
130 assert_true(ends_with(ui_sb_last(),
131 ": global 'adsf' is not callable (a nil value)"));
132 }
133
134 TEST(keys_bad_selector_return)
135 {
136 assert_success(vlua_run_string(vlua, "function badhandler()\n"
137 " return 1\n"
138 "end"));
139
140 assert_success(vlua_run_string(vlua, "print(vifm.keys.add {"
141 " shortcut = 'X',"
142 " modes = { 'normal' },"
143 " isselector = true,"
144 " handler = badhandler,"
145 "})"));
146 assert_string_equal("true", ui_sb_last());
147
148 ui_sb_msg("");
149 (void)vle_keys_exec_timed_out(L"yX");
150 assert_string_equal("", ui_sb_last());
151 }
152
153 TEST(keys_bad_selector_return_table)
154 {
155 assert_success(vlua_run_string(vlua, "function badhandler()\n"
156 " return {}\n"
157 "end"));
158
159 assert_success(vlua_run_string(vlua, "print(vifm.keys.add {"
160 " shortcut = 'X',"
161 " modes = { 'normal' },"
162 " isselector = true,"
163 " handler = badhandler,"
164 "})"));
165 assert_string_equal("true", ui_sb_last());
166
167 ui_sb_msg("");
168 (void)vle_keys_exec_timed_out(L"yX");
169 assert_string_equal("", ui_sb_last());
170 }
171
172 TEST(keys_bad_selector_index)
173 {
174 assert_success(vlua_run_string(vlua, "function badhandler()\n"
175 " return { indexes = { 0 } }\n"
176 "end"));
177
178 assert_success(vlua_run_string(vlua, "print(vifm.keys.add {"
179 " shortcut = 'X',"
180 " modes = { 'normal' },"
181 " isselector = true,"
182 " handler = badhandler,"
183 "})"));
184 assert_string_equal("true", ui_sb_last());
185
186 ui_sb_msg("");
187 (void)vle_keys_exec_timed_out(L"yX");
188 assert_string_equal("", ui_sb_last());
189 }
190
191 TEST(keys_selector_duplicated_indexes)
192 {
193 assert_success(vlua_run_string(vlua, "function badhandler()\n"
194 " return { indexes = { 1, 1 } }\n"
195 "end"));
196
197 assert_success(vlua_run_string(vlua, "print(vifm.keys.add {"
198 " shortcut = 'X',"
199 " modes = { 'normal' },"
200 " isselector = true,"
201 " handler = badhandler,"
202 "})"));
203 assert_string_equal("true", ui_sb_last());
204
205 (void)vle_keys_exec_timed_out(L"yX");
206 assert_int_equal(1, curr_stats.save_msg);
207 assert_string_equal("1 file yanked", ui_sb_last());
208 }
209
87 210 TEST(keys_add) TEST(keys_add)
88 211 { {
89 212 ui_sb_msg(""); ui_sb_msg("");
 
... ... TEST(keys_add)
128 251 assert_string_equal("22\"", ui_sb_last()); assert_string_equal("22\"", ui_sb_last());
129 252 } }
130 253
254 TEST(keys_add_selector)
255 {
256 ui_sb_msg("");
257
258 assert_success(vlua_run_string(vlua, "function handler(info)"
259 " print('in handler')"
260 " return { indexes = { 1, 2 } }"
261 "end"));
262 assert_string_equal("", ui_sb_last());
263
264 assert_success(vlua_run_string(vlua, "print(vifm.keys.add {"
265 " shortcut = 'X',"
266 " modes = { 'cmdline', 'normal' },"
267 " description = 'print a message',"
268 " isselector = true,"
269 " handler = handler,"
270 "})"));
271 assert_string_equal("true", ui_sb_last());
272
273 (void)vle_keys_exec_timed_out(L"yX");
274 assert_int_equal(1, curr_stats.save_msg);
275 assert_string_equal("2 files yanked", ui_sb_last());
276
277 reg_t *reg = regs_find(DEFAULT_REG_NAME);
278 assert_non_null(reg);
279 assert_int_equal(2, reg->nfiles);
280 assert_string_equal("/lwin/file0", reg->files[0]);
281 assert_string_equal("/lwin/file1", reg->files[1]);
282 }
283
131 284 TEST(keys_add_modes) TEST(keys_add_modes)
132 285 { {
133 286 ui_sb_msg(""); ui_sb_msg("");
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