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 20d3ddaa2305581b4ca8b6c76455232e8ee8d24f

Add <help> :*map argument for help messages
It enables providing description for the mapping with the same curly
braces syntax as used by :file[x]type.
Author: xaizek
Author date (UTC): 2025-06-15 16:17
Committer name: xaizek
Committer date (UTC): 2025-06-15 16:18
Parent(s): 2baf12a7cf14ae13eaa5077237cffd97b2e03ce7
Signing key: 99DC5E4DB05F6BE2
Tree: 43d868688f8f09c2aeb89cb5a23aeabbb2ec4bef
File Lines added Lines deleted
ChangeLog 3 0
data/man/vifm.1 15 2
data/vim/doc/app/vifm-app.txt 10 2
data/vim/syntax/vifm.vim 2 2
src/cmd_handlers.c 32 6
src/engine/keys.c 11 0
src/engine/keys.h 6 0
src/event_loop.c 1 1
src/menus/map_menu.c 11 1
src/tags.c 1 0
tests/commands/map.c 28 0
tests/menus/map.c 20 0
tests/misc/event_loop.c 2 2
tests/test-data/syntax-highlight/syntax.vifm 2 0
File ChangeLog changed (mode: 100644) (index 7188243b5..c481dd7f9)
7 7 inside of a directory, instead of to indicate directories. Thanks to inside of a directory, instead of to indicate directories. Thanks to
8 8 CaptainFantastic. CaptainFantastic.
9 9
10 Added <help> :*map argument that enables providing description for the
11 mapping with the same curly braces syntax as used by :file[x]type.
12
10 13 Updated utf8proc to v2.10.0. Updated utf8proc to v2.10.0.
11 14
12 15 Made documentation on which :commands can have comments a bit more Made documentation on which :commands can have comments a bit more
File data/man/vifm.1 changed (mode: 100644) (index a468f35a5..e9b76845e)
1 .TH VIFM 1 "7 June 2025" "vifm 0.15"
1 .TH VIFM 1 "15 June 2025" "vifm 0.15"
2 2 .\" --------------------------------------------------------------------------- .\" ---------------------------------------------------------------------------
3 3 .SH NAME .SH NAME
4 4 .\" --------------------------------------------------------------------------- .\" ---------------------------------------------------------------------------
 
... ... map lhs key sequence to rhs in normal and visual modes.
3358 3358 .BI ":map! lhs rhs" .BI ":map! lhs rhs"
3359 3359 map lhs key sequence to rhs in command line mode. map lhs key sequence to rhs in command line mode.
3360 3360
3361 Generic syntax of mapping commands:
3362 .EX
3363
3364 mapcommand [<map-arguments>] lhs rhs
3365 mapcommand [<map-arguments>] <help> lhs {help text} rhs
3366
3367 .EE
3368 See "Mappings" section for more details.
3369
3361 3370 .TP .TP
3362 3371 .BI " :amap :cmap :dmap :mmap :nmap :qmap :vmap" .BI " :amap :cmap :dmap :mmap :nmap :qmap :vmap"
3363 3372 .TP .TP
 
... ... the mapping was started and can span multiple modes.
5662 5671 .B Map arguments .B Map arguments
5663 5672
5664 5673 LHS of mappings can be preceded by arguments which take the form of special LHS of mappings can be preceded by arguments which take the form of special
5665 sequences:
5674 sequences. They can appear in any order relative to each other.
5675 .TP
5676 .BI <help>
5677 Indicates that the RHS is optionally preceded by a description enclosed in
5678 curly braces.
5666 5679 .TP .TP
5667 5680 .BI <silent> .BI <silent>
5668 5681 Postpone UI updates until RHS is completely processed. Postpone UI updates until RHS is completely processed.
File data/vim/doc/app/vifm-app.txt changed (mode: 100644) (index 5f888116f..bfc29f5ee)
1 *vifm-app.txt* For Vifm version 0.15 Last change: 2025 June 7
1 *vifm-app.txt* For Vifm version 0.15 Last change: 2025 June 15
2 2
3 3 Email for bugs and suggestions: <xaizek@posteo.net> Email for bugs and suggestions: <xaizek@posteo.net>
4 4
 
... ... yet supported) and might be changed in future releases, or get an alias.
2806 2806 :map! lhs rhs :map! lhs rhs
2807 2807 map lhs key sequence to rhs in command line mode. map lhs key sequence to rhs in command line mode.
2808 2808
2809 Generic syntax of mapping commands: >
2810 mapcommand [<map-arguments>] lhs rhs
2811 mapcommand [<map-arguments>] <help> lhs {help text} rhs
2812 See |vifm-mappings| for more details.
2813
2809 2814 *vifm-:amap* *vifm-:amap*
2810 2815 *vifm-:cmap* *vifm-:cm* *vifm-:cmap* *vifm-:cm*
2811 2816 *vifm-:dmap* *vifm-:dm* *vifm-:dmap* *vifm-:dm*
 
... ... the mapping was started and can span multiple modes.
4789 4794 Map arguments~ Map arguments~
4790 4795
4791 4796 LHS of mappings can be preceded by arguments which take the form of special LHS of mappings can be preceded by arguments which take the form of special
4792 sequences:
4797 sequences. They can appear in any order relative to each other.
4793 4798
4799 <help> *vifm-:map-<help>*
4800 Indicates that the RHS is optionally preceded by a description enclosed in
4801 curly braces.
4794 4802 <silent> *vifm-:map-<silent>* <silent> *vifm-:map-<silent>*
4795 4803 Postpone UI updates until RHS is completely processed. Postpone UI updates until RHS is completely processed.
4796 4804 <wait> *vifm-:map-<wait>* <wait> *vifm-:map-<wait>*
File data/vim/syntax/vifm.vim changed (mode: 100644) (index 79fb9165a..0c2f5a2b1)
1 1 " vifm syntax file " vifm syntax file
2 2 " Maintainer: xaizek <xaizek@posteo.net> " Maintainer: xaizek <xaizek@posteo.net>
3 " Last Change: January 18, 2025
3 " Last Change: June 15, 2025
4 4 " Inspired By: Vim syntax file by Dr. Charles E. Campbell, Jr. " Inspired By: Vim syntax file by Dr. Charles E. Campbell, Jr.
5 5
6 6 if exists('b:current_syntax') if exists('b:current_syntax')
 
... ... syntax match vifmNumber contained /\d\+/
448 448 syntax match vifmHexColor contained /#[0-9a-fA-F]\{6}/ syntax match vifmHexColor contained /#[0-9a-fA-F]\{6}/
449 449
450 450 " Optional map arguments right after command name " Optional map arguments right after command name
451 syntax match vifmMapArgList '\(<\(silent\|wait\)>\s*\)*' contained
451 syntax match vifmMapArgList '\(<\(help\|silent\|wait\)>\s*\)*' contained
452 452 \ nextgroup=vifmMapLhs \ nextgroup=vifmMapLhs
453 453
454 454 " Ange-bracket notation " Ange-bracket notation
File src/cmd_handlers.c changed (mode: 100644) (index f945afbee..70dfac96e)
... ... static int vsplit_cmd(const cmd_info_t *cmd_info);
332 332 static int do_split(const cmd_info_t *cmd_info, SPLIT orientation); static int do_split(const cmd_info_t *cmd_info, SPLIT orientation);
333 333 static int do_map(const cmd_info_t *cmd_info, const char map_type[], int mode, static int do_map(const cmd_info_t *cmd_info, const char map_type[], int mode,
334 334 int no_remap); int no_remap);
335 static int parse_map_args(const char **args);
335 static int parse_map_args(const char **args, int *with_help);
336 336 static int vunmap_cmd(const cmd_info_t *cmd_info); static int vunmap_cmd(const cmd_info_t *cmd_info);
337 337 static int do_unmap(const char *keys, int mode); static int do_unmap(const char *keys, int mode);
338 338 static int wincmd_cmd(const cmd_info_t *cmd_info); static int wincmd_cmd(const cmd_info_t *cmd_info);
 
... ... do_map(const cmd_info_t *cmd_info, const char map_type[], int mode,
5407 5407 } }
5408 5408
5409 5409 const char *args = cmd_info->args; const char *args = cmd_info->args;
5410 int with_help;
5410 5411 const int flags = (no_remap ? KEYS_FLAG_NOREMAP : KEYS_FLAG_NONE) const int flags = (no_remap ? KEYS_FLAG_NOREMAP : KEYS_FLAG_NONE)
5411 | parse_map_args(&args);
5412 | parse_map_args(&args, &with_help);
5412 5413
5413 5414 char *raw_rhs = vle_cmds_past_arg(args); char *raw_rhs = vle_cmds_past_arg(args);
5414 5415 *raw_rhs = '\0'; *raw_rhs = '\0';
5415 5416
5416 5417 char *rhs = vle_cmds_at_arg(raw_rhs + 1); char *rhs = vle_cmds_at_arg(raw_rhs + 1);
5418
5419 const char *descr = NULL;
5420 if(with_help && *rhs == '{')
5421 {
5422 char *p = strchr(rhs + 1, '}');
5423 if(p != NULL)
5424 {
5425 *p = '\0';
5426 descr = rhs + 1;
5427 rhs = vle_cmds_at_arg(p + 1);
5428 }
5429
5430 if(*rhs == '\0')
5431 {
5432 ui_sb_err("<nop> is required to map to nothing");
5433 return CMDS_ERR_CUSTOM;
5434 }
5435 }
5436
5417 5437 wchar_t *keys = substitute_specs(args); wchar_t *keys = substitute_specs(args);
5418 5438 wchar_t *mapping = substitute_specs(rhs); wchar_t *mapping = substitute_specs(rhs);
5419 5439 if(keys != NULL && mapping != NULL) if(keys != NULL && mapping != NULL)
5420 5440 { {
5421 if(vle_keys_user_add(keys, mapping, /*descr=*/NULL, mode, flags) != 0)
5441 if(vle_keys_user_add(keys, mapping, descr, mode, flags) != 0)
5422 5442 { {
5423 5443 show_error_msg("Mapping Error", "Unable to allocate enough memory"); show_error_msg("Mapping Error", "Unable to allocate enough memory");
5424 5444 } }
 
... ... do_map(const cmd_info_t *cmd_info, const char map_type[], int mode,
5434 5454 return 0; return 0;
5435 5455 } }
5436 5456
5437 /* Parses <*> :*map arguments removing them from the line. Returns flags
5438 * collected. */
5457 /* Parses <*> :*map arguments removing them from the line. Returns a
5458 * combination of KEYS_FLAG_* flags and sets *with_help. */
5439 5459 static int static int
5440 parse_map_args(const char **args)
5460 parse_map_args(const char **args, int *with_help)
5441 5461 { {
5462 *with_help = 0;
5463
5442 5464 int flags = 0; int flags = 0;
5443 5465 do do
5444 5466 { {
 
... ... parse_map_args(const char **args)
5450 5472 { {
5451 5473 flags |= KEYS_FLAG_WAIT; flags |= KEYS_FLAG_WAIT;
5452 5474 } }
5475 else if(skip_prefix(args, "<help>"))
5476 {
5477 *with_help = 1;
5478 }
5453 5479 else else
5454 5480 { {
5455 5481 break; break;
File src/engine/keys.c changed (mode: 100644) (index 70276be75..806ef8aea)
37 37
38 38 #include "../utils/macros.h" #include "../utils/macros.h"
39 39 #include "../utils/str.h" #include "../utils/str.h"
40 #include "../utils/test_helpers.h"
40 41 #include "mode.h" #include "mode.h"
41 42
42 43 /* Type of key chunk. */ /* Type of key chunk. */
 
... ... static const wchar_t * get_reg(const wchar_t *keys, int *reg);
141 142 static const wchar_t * get_count(const wchar_t keys[], int *count); static const wchar_t * get_count(const wchar_t keys[], int *count);
142 143 static int is_at_count(const wchar_t keys[]); static int is_at_count(const wchar_t keys[]);
143 144 static int combine_counts(int count_a, int count_b); static int combine_counts(int count_a, int count_b);
145 TSTATIC const key_conf_t * vle_keys_get_user_key(const wchar_t lhs[], int mode);
144 146 static key_chunk_t * find_keys(key_chunk_t *root, const wchar_t keys[]); static key_chunk_t * find_keys(key_chunk_t *root, const wchar_t keys[]);
145 147 static void remove_chunk(key_chunk_t *chunk); static void remove_chunk(key_chunk_t *chunk);
146 148 static int add_list_of_keys(key_chunk_t *root, keys_add_info_t cmds[], static int add_list_of_keys(key_chunk_t *root, keys_add_info_t cmds[],
 
... ... vle_keys_user_exists(const wchar_t keys[], int mode)
1088 1090 return find_keys(&user_cmds_root[mode], keys) != NULL; return find_keys(&user_cmds_root[mode], keys) != NULL;
1089 1091 } }
1090 1092
1093 /* Retrieves configuration of a user key. Returns a pointer to it or NULL if
1094 * the mapping doesn't exist. */
1095 TSTATIC const key_conf_t *
1096 vle_keys_get_user_key(const wchar_t lhs[], int mode)
1097 {
1098 key_chunk_t *chunk = find_keys(&user_cmds_root[mode], lhs);
1099 return (chunk == NULL ? NULL : &chunk->conf);
1100 }
1101
1091 1102 int int
1092 1103 vle_keys_user_remove(const wchar_t keys[], int mode) vle_keys_user_remove(const wchar_t keys[], int mode)
1093 1104 { {
File src/engine/keys.h changed (mode: 100644) (index 8c3533932..eac61d450)
21 21
22 22 #include <stddef.h> /* size_t wchar_t */ #include <stddef.h> /* size_t wchar_t */
23 23
24 #include "../utils/test_helpers.h"
25
24 26 enum enum
25 27 { {
26 28 MAX_LHS_LEN = 4 MAX_LHS_LEN = 4
 
... ... int vle_keys_mapping_state(void);
221 223 void vle_keys_suggest(const wchar_t keys[], vle_keys_list_cb cb, void vle_keys_suggest(const wchar_t keys[], vle_keys_list_cb cb,
222 224 int custom_only, int fold_subkeys); int custom_only, int fold_subkeys);
223 225
226 TSTATIC_DEFS(
227 const key_conf_t * vle_keys_get_user_key(const wchar_t lhs[], int mode);
228 )
229
224 230 #endif /* VIFM__ENGINE__KEYS_H__ */ #endif /* VIFM__ENGINE__KEYS_H__ */
225 231
226 232 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0: */ /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0: */
File src/event_loop.c changed (mode: 100644) (index a1c87a355..8c9fe47e6)
... ... process_suggestion(const wchar_t lhs[], const wchar_t rhs[], const char descr[])
703 703 if(rhs[0] != '\0') if(rhs[0] != '\0')
704 704 { {
705 705 char *const mb_rhs = wstr_to_spec(rhs); char *const mb_rhs = wstr_to_spec(rhs);
706 vle_compl_put_match(wstr_to_spec(lhs), mb_rhs);
706 vle_compl_put_match(wstr_to_spec(lhs), *descr != '\0' ? descr : mb_rhs);
707 707 free(mb_rhs); free(mb_rhs);
708 708 } }
709 709 else else
File src/menus/map_menu.c changed (mode: 100644) (index 7cfe8afaf..7a65f91e7)
... ... add_mapping_item(const wchar_t lhs[], const wchar_t rhs[], const char descr[])
119 119 } }
120 120 else if(rhs[0] == L'\0') else if(rhs[0] == L'\0')
121 121 { {
122 /* RHS is empty, but description isn't. This is a builtin key. */
122 123 m.items[m.len++] = format_str("%-*s %s", MAP_WIDTH, mb_lhs, descr); m.items[m.len++] = format_str("%-*s %s", MAP_WIDTH, mb_lhs, descr);
123 124 } }
124 125 else else
125 126 { {
127 /* RHS isn't empty. This is a user key with optional description. */
126 128 char *const mb_rhs = wstr_to_spec(rhs); char *const mb_rhs = wstr_to_spec(rhs);
127 m.items[m.len++] = format_str("%-*s %s", MAP_WIDTH, mb_lhs, mb_rhs);
129 if(descr[0] == '\0')
130 {
131 m.items[m.len++] = format_str("%-*s %s", MAP_WIDTH, mb_lhs, mb_rhs);
132 }
133 else
134 {
135 m.items[m.len++] =
136 format_str("%-*s {%s} %s", MAP_WIDTH, mb_lhs, descr, mb_rhs);
137 }
128 138 free(mb_rhs); free(mb_rhs);
129 139 } }
130 140
File src/tags.c changed (mode: 100644) (index 76a906497..ed9a46058)
... ... const char *tags[] = {
309 309 "vifm-:m_chi", "vifm-:m_chi",
310 310 "vifm-:ma", "vifm-:ma",
311 311 "vifm-:map", "vifm-:map",
312 "vifm-:map-<help>",
312 313 "vifm-:map-<silent>", "vifm-:map-<silent>",
313 314 "vifm-:map-<wait>", "vifm-:map-<wait>",
314 315 "vifm-:mark", "vifm-:mark",
File tests/commands/map.c changed (mode: 100644) (index 684b25933..f3c7ecadd)
3 3 #include "../../src/engine/keys.h" #include "../../src/engine/keys.h"
4 4 #include "../../src/modes/modes.h" #include "../../src/modes/modes.h"
5 5 #include "../../src/modes/wk.h" #include "../../src/modes/wk.h"
6 #include "../../src/ui/statusbar.h"
6 7 #include "../../src/ui/ui.h" #include "../../src/ui/ui.h"
7 8 #include "../../src/cmd_core.h" #include "../../src/cmd_core.h"
8 9 #include "../../src/status.h" #include "../../src/status.h"
 
... ... TEST(map_parses_args)
86 87 /* <wait> */ /* <wait> */
87 88 assert_success(cmds_dispatch("map <wait>xj j", &lwin, CIT_COMMAND)); assert_success(cmds_dispatch("map <wait>xj j", &lwin, CIT_COMMAND));
88 89 assert_int_equal(KEYS_WAIT, vle_keys_exec(L"x")); assert_int_equal(KEYS_WAIT, vle_keys_exec(L"x"));
90
91 /* <help> */
92 ui_sb_msg("");
93 /* Help text must be followed by something. */
94 assert_failure(cmds_dispatch("map <help> Xj {j}", &lwin, CIT_COMMAND));
95 assert_string_equal("<nop> is required to map to nothing", ui_sb_last());
96 assert_failure(cmds_dispatch("map <help> Xj {j} ", &lwin, CIT_COMMAND));
97 assert_string_equal("<nop> is required to map to nothing", ui_sb_last());
98 /* Help text is parsed. */
99 assert_success(cmds_dispatch("map <help> Xj {help}j", &lwin, CIT_COMMAND));
100 assert_wstring_equal(L"j",
101 vle_keys_get_user_key(L"Xj", NORMAL_MODE)->data.cmd);
102 assert_string_equal("help", vle_keys_get_user_key(L"Xj", NORMAL_MODE)->descr);
103 /* Help text is optional. */
104 assert_success(cmds_dispatch("map <help> Xj {j", &lwin, CIT_COMMAND));
105 assert_wstring_equal(L"{j",
106 vle_keys_get_user_key(L"Xj", NORMAL_MODE)->data.cmd);
107 assert_string_equal(NULL, vle_keys_get_user_key(L"Xj", NORMAL_MODE)->descr);
108 assert_success(cmds_dispatch("map <help> Xj j", &lwin, CIT_COMMAND));
109 assert_wstring_equal(L"j",
110 vle_keys_get_user_key(L"Xj", NORMAL_MODE)->data.cmd);
111 assert_string_equal(NULL, vle_keys_get_user_key(L"Xj", NORMAL_MODE)->descr);
112 /* No help text by default. */
113 assert_success(cmds_dispatch("map Xj j", &lwin, CIT_COMMAND));
114 assert_wstring_equal(L"j",
115 vle_keys_get_user_key(L"Xj", NORMAL_MODE)->data.cmd);
116 assert_string_equal(NULL, vle_keys_get_user_key(L"Xj", NORMAL_MODE)->descr);
89 117 } }
90 118
91 119 TEST(dialogs_exit_silent_mode) TEST(dialogs_exit_silent_mode)
File tests/menus/map.c changed (mode: 100644) (index 507a635d1..f76b0975b)
... ... TEST(builtin_key_description_is_displayed)
143 143 modes_abort_menu_like(); modes_abort_menu_like();
144 144 } }
145 145
146 TEST(user_key_description_is_displayed)
147 {
148 assert_success(cmds_dispatch("nmap helpno {nohelp} rhs", &lwin,
149 CIT_COMMAND));
150 assert_success(cmds_dispatch("nmap <help> help {help} rhs", &lwin,
151 CIT_COMMAND));
152
153 assert_success(cmds_dispatch("nmap help", &lwin, CIT_COMMAND));
154
155 assert_int_equal(5, menu_get_current()->len);
156 assert_string_equal("User mappings:", menu_get_current()->items[0]);
157 assert_string_equal("help {help} rhs", menu_get_current()->items[1]);
158 assert_string_equal("helpno {nohelp} rhs",
159 menu_get_current()->items[2]);
160 assert_string_equal("", menu_get_current()->items[3]);
161 assert_string_equal("Builtin mappings:", menu_get_current()->items[4]);
162
163 modes_abort_menu_like();
164 }
165
146 166 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */ /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
147 167 /* vim: set cinoptions+=t0 : */ /* vim: set cinoptions+=t0 : */
File tests/misc/event_loop.c changed (mode: 100644) (index dd43b8b4e..47763c5e4)
... ... TEST(key_suggestions)
179 179 const vle_compl_t *items = vle_compl_get_items(); const vle_compl_t *items = vle_compl_get_items();
180 180 assert_int_equal(2, vle_compl_get_count()); assert_int_equal(2, vle_compl_get_count());
181 181 assert_string_equal("key: j", items[0].text); assert_string_equal("key: j", items[0].text);
182 assert_string_equal("j", items[0].descr);
182 assert_string_equal("Xj help", items[0].descr);
183 183 assert_string_equal("key: k", items[1].text); assert_string_equal("key: k", items[1].text);
184 assert_string_equal("k", items[1].descr);
184 assert_string_equal("Xk help", items[1].descr);
185 185
186 186 cfg.sug.flags = 0; cfg.sug.flags = 0;
187 187 cfg.min_timeout_len = 0; cfg.min_timeout_len = 0;
File tests/test-data/syntax-highlight/syntax.vifm changed (mode: 100644) (index 1ee5aab6c..fd05c67bb)
... ... nnoremap ,b :set viewcolumns="-{name}..,6{}."<cr>
37 37 " "<left>" and "<cr>" should be highlighted as angle bracket notation " "<left>" and "<cr>" should be highlighted as angle bracket notation
38 38 nnoremap s :let $a <left>= 'a'<cr> nnoremap s :let $a <left>= 'a'<cr>
39 39
40 " "<help>" should be highlighted as angle bracket notation
40 41 " "<silent>" should be highlighted as angle bracket notation " "<silent>" should be highlighted as angle bracket notation
41 42 " "<wait>" should be highlighted as angle bracket notation " "<wait>" should be highlighted as angle bracket notation
43 map <help><silent> xy {help text} 123
42 44 map <silent> ,b :set viewcolumns="-{name}..,6{}."<cr> map <silent> ,b :set viewcolumns="-{name}..,6{}."<cr>
43 45 map <silent>,b :set viewcolumns="-{name}..,6{}."<cr> map <silent>,b :set viewcolumns="-{name}..,6{}."<cr>
44 46 map <silent><silent>,b :set viewcolumns="-{name}..,6{}."<cr> map <silent><silent>,b :set viewcolumns="-{name}..,6{}."<cr>
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