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 49f429201137190dd157b8324fdca8e98949bb11

Add P view mode key to persist current viewer
For the duration of the session.

Thanks to j-xella, durcheinandr and vuenn.

See #330 on GitHub.

Also see
https://q2a.vifm.info/2109/is-it-possible-to-rember-last-used-fileviewer
Author: xaizek
Author date (UTC): 2025-09-01 18:24
Committer name: xaizek
Committer date (UTC): 2025-09-02 21:32
Parent(s): d4b8262466beeed9cf4ef21d68d78f342668744e
Signing key: 99DC5E4DB05F6BE2
Tree: 51f95a4f4efdce22aa3d7385e83e96dbc6648133
File Lines added Lines deleted
ChangeLog 3 0
data/man/vifm.1 5 1
data/vim/doc/app/vifm-app.txt 4 1
src/filetype.c 217 0
src/filetype.h 4 0
src/modes/view.c 21 0
src/tags.c 1 0
src/utils/mem.c 9 0
src/utils/mem.h 3 0
tests/misc/view_mode.c 44 0
File ChangeLog changed (mode: 100644) (index bf179fb51..97aea0db9)
21 21 Added `:messages clear` to clear the history of the most recent status bar Added `:messages clear` to clear the history of the most recent status bar
22 22 messages. Thanks to qadzek. messages. Thanks to qadzek.
23 23
24 Added P view mode key to make choice of a viewer via a and A keys
25 persistent for the session. Thanks to j-xella, durcheinandr and vuenn.
26
24 27 Updated utf8proc to v2.10.0. Updated utf8proc to v2.10.0.
25 28
26 29 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 2ab76dd3f..cb147f5e0)
1 .TH VIFM 1 "18 August 2025" "vifm 0.15"
1 .TH VIFM 1 "1 September 2025" "vifm 0.15"
2 2 .\" --------------------------------------------------------------------------- .\" ---------------------------------------------------------------------------
3 3 .SH NAME .SH NAME
4 4 .\" --------------------------------------------------------------------------- .\" ---------------------------------------------------------------------------
 
... ... switch to the next viewer. Does nothing for preview constructed via %q macro.
1058 1058 switch to the previous viewer. Does nothing for preview constructed via switch to the previous viewer. Does nothing for preview constructed via
1059 1059 %q macro. %q macro.
1060 1060 .TP .TP
1061 .BI P
1062 persist currently selected viewer as the default one for the current and
1063 similar files.
1064 .TP
1061 1065 .BI i .BI i
1062 1066 toggle raw mode (ignoring of defined viewers). Does nothing for preview toggle raw mode (ignoring of defined viewers). Does nothing for preview
1063 1067 constructed via %q macro. constructed via %q macro.
File data/vim/doc/app/vifm-app.txt changed (mode: 100644) (index 52647c597..5299e0d9f)
1 *vifm-app.txt* For Vifm version 0.15 Last change: 2025 August 21
1 *vifm-app.txt* For Vifm version 0.15 Last change: 2025 September 1
2 2
3 3 Email for bugs and suggestions: <xaizek@posteo.net> Email for bugs and suggestions: <xaizek@posteo.net>
4 4
 
... ... a *vifm-q_a*
937 937 A *vifm-q_A* A *vifm-q_A*
938 938 switch to the previous viewer. Does nothing for preview constructed switch to the previous viewer. Does nothing for preview constructed
939 939 via |vifm-%q| macro. via |vifm-%q| macro.
940 P *vifm-q_P*
941 persist currently selected viewer as the default one for the current and
942 similar files.
940 943 i *vifm-q_i* i *vifm-q_i*
941 944 toggle raw mode (ignoring of defined viewers). Does nothing for preview toggle raw mode (ignoring of defined viewers). Does nothing for preview
942 945 constructed via `%q` macro. constructed via `%q` macro.
File src/filetype.c changed (mode: 100644) (index 98f0661cb..13d6bf38b)
28 28 #include "compat/fs_limits.h" #include "compat/fs_limits.h"
29 29 #include "compat/reallocarray.h" #include "compat/reallocarray.h"
30 30 #include "modes/dialogs/msg_dialog.h" #include "modes/dialogs/msg_dialog.h"
31 #include "utils/darray.h"
31 32 #include "utils/matchers.h" #include "utils/matchers.h"
33 #include "utils/mem.h"
32 34 #include "utils/str.h" #include "utils/str.h"
33 35 #include "utils/string_array.h" #include "utils/string_array.h"
34 36 #include "utils/path.h" #include "utils/path.h"
35 37 #include "utils/utils.h" #include "utils/utils.h"
36 38
39 /*
40 * Temporary state used while reordering viewers.
41 *
42 * We're always collecting a prefix because regardless whether the pivot is
43 * moved to the top or the bottom of a matching subset, we group the subset
44 * around bottom of the list of viewers where catch-all viewers are likely to
45 * appear. Moving items down rather than up isolates the subset more from the
46 * rest of the viewers and does not mess up matching by putting a catch-all
47 * viewer above entries which did not participate in a rotation.
48 */
49 typedef struct
50 {
51 assoc_t *list; /* Storage for the new list. */
52
53 /* Matching associations that precede the pivot in fileviewers. */
54 assoc_t *prefix;
55 DA_INSTANCE_FIELD(prefix);
56
57 int i; /* Index of an association in fileviewers used as a "pivot". */
58 int j; /* Number of associations copied to .list array. */
59 int k; /* Matching record number within fileviewers.list[i].records[]. */
60 }
61 reordering_data_t;
62
37 63 static const char * find_existing_cmd(const assoc_list_t *record_list, static const char * find_existing_cmd(const assoc_list_t *record_list,
38 64 const char file[]); const char file[]);
39 65 static assoc_record_t find_existing_cmd_record(const assoc_records_t *records); static assoc_record_t find_existing_cmd_record(const assoc_records_t *records);
66 static void make_pivot_first(reordering_data_t *d);
67 static int pick_out_until_first(const char file[], const char viewer[],
68 reordering_data_t *d);
69 static int split_assoc(assoc_t *assoc, int at_record_idx, assoc_t *out_prefix);
70 static int mg_clone(const matchers_group_t *from, matchers_group_t *to);
40 71 static assoc_records_t parse_command_list(const char cmds[]); static assoc_records_t parse_command_list(const char cmds[]);
41 72 static assoc_records_t clone_all_matching_records(const char file[], static assoc_records_t clone_all_matching_records(const char file[],
42 73 const assoc_list_t *record_list); const assoc_list_t *record_list);
 
... ... find_existing_cmd_record(const assoc_records_t *records)
185 216 return empty_record; return empty_record;
186 217 } }
187 218
219 void
220 ft_move_viewer_to_top(const char file[], const char viewer[])
221 {
222 reordering_data_t d = {};
223
224 /*
225 * Overall approach:
226 * 1. Find an entry with a matching program record.
227 * 2. Move it and all predecessors matching file after the last matching
228 * entry.
229 * 3. Special case: if the record is not the last one, cut the entry after
230 * the record and move only this prefix.
231 */
232 if(pick_out_until_first(file, viewer, &d) == 0)
233 {
234 make_pivot_first(&d);
235 }
236
237 free(d.list);
238 DA_REMOVE_ALL(d.prefix);
239 }
240
241 /* Makes a "pivot" element the first one among subset of matching file viewers.
242 * This is done by putting the element, the tail of the original list and all
243 * subset elements in front of the pivot in the right order. */
244 static void
245 make_pivot_first(reordering_data_t *d)
246 {
247 assoc_t split_prefix;
248 int need_split = (d->k != 0);
249 if(need_split)
250 {
251 /* The viewer is not the first in the list, so split the association at
252 * the viewer. The code below ensures the viewer will be at the new top
253 * for the subset matching this file and then the prefix will be
254 * appended. */
255 if(split_assoc(&fileviewers.list[d->i], d->k, &split_prefix) != 0)
256 {
257 return;
258 }
259 }
260
261 mem_cpy(&d->list[d->j], &fileviewers.list[d->i], fileviewers.count - d->i,
262 sizeof(d->list[0]));
263 d->j += fileviewers.count - d->i;
264
265 mem_cpy(&d->list[d->j], &d->prefix[0], DA_SIZE(d->prefix),
266 sizeof(d->list[0]));
267 d->j += DA_SIZE(d->prefix);
268
269 assert(d->j == fileviewers.count);
270
271 free(fileviewers.list);
272 fileviewers.list = d->list;
273 d->list = NULL;
274
275 if(need_split)
276 {
277 fileviewers.list[fileviewers.count++] = split_prefix;
278 }
279 }
280
281 /* Finds a matching entry in the list of viewers either by checking for its
282 * existence (viewer is NULL) or by matching it against a passed in string
283 * (viewer is not NULL). Collects all entries matching file in d->prefix array
284 * while walking the list. Returns zero on successfully finding a viewer. */
285 static int
286 pick_out_until_first(const char file[], const char viewer[],
287 reordering_data_t *d)
288 {
289 /* The new list needs to have room for an extra element in case a matching
290 * entry needs to be split in two. */
291 d->list = malloc((fileviewers.count + 1)*sizeof(*d->list));
292 if(d->list == NULL)
293 {
294 return 1;
295 }
296
297 for(d->i = 0, d->j = 0; d->i < fileviewers.count; ++d->i)
298 {
299 assoc_t *assoc = &fileviewers.list[d->i];
300
301 if(!mg_match(&assoc->mg, file))
302 {
303 d->list[d->j++] = *assoc;
304 continue;
305 }
306
307 for(d->k = 0; d->k < assoc->records.count; ++d->k)
308 {
309 if(viewer != NULL)
310 {
311 if(strcmp(assoc->records.list[d->k].command, viewer) == 0)
312 {
313 return 0;
314 }
315 }
316 else
317 {
318 if(ft_exists(assoc->records.list[d->k].command))
319 {
320 return 0;
321 }
322 }
323 }
324
325 assoc_t *another = DA_EXTEND(d->prefix);
326 if(another == NULL)
327 {
328 return 1;
329 }
330
331 *another = *assoc;
332 DA_COMMIT(d->prefix);
333 }
334
335 /* No matching viewer was found. */
336 return 1;
337 }
338
339 /* Leaves assoc->records[0..at_record_idx] in *assoc and creates *out_prefix
340 * with assoc->records[at_record_idx+1..]. Returns zero on success. */
341 static int
342 split_assoc(assoc_t *assoc, int at_record_idx, assoc_t *out_prefix)
343 {
344 assoc_t prefix;
345 if(mg_clone(&assoc->mg, &prefix.mg) != 0)
346 {
347 return 1;
348 }
349
350 prefix.records.list =
351 reallocarray(NULL, at_record_idx, sizeof(*prefix.records.list));
352 if(prefix.records.list == NULL)
353 {
354 mg_free(&prefix.mg);
355 return 1;
356 }
357
358 mem_cpy(prefix.records.list, assoc->records.list, at_record_idx,
359 sizeof(*prefix.records.list));
360 mem_shl(assoc->records.list, assoc->records.count,
361 sizeof(*assoc->records.list), at_record_idx);
362
363 prefix.records.count = at_record_idx;
364 assoc->records.count -= at_record_idx;
365
366 *out_prefix = prefix;
367 return 0;
368 }
369
370 /* Clones a group of matchers. Returns zero on success. */
371 static int
372 mg_clone(const matchers_group_t *from, matchers_group_t *to)
373 {
374 if(from->count == 0)
375 {
376 to->list = NULL;
377 to->count = 0;
378 return 0;
379 }
380
381 matchers_group_t result = {
382 .list = reallocarray(NULL, from->count, sizeof(*result.list)),
383 .count = 0,
384 };
385 if(result.list == NULL)
386 {
387 return 1;
388 }
389
390 int i;
391 for(i = 0; i < from->count; ++i, ++result.count)
392 {
393 result.list[i] = matchers_clone(from->list[i]);
394 if(result.list[i] == NULL)
395 {
396 mg_free(&result);
397 return 1;
398 }
399 }
400
401 *to = result;
402 return 0;
403 }
404
188 405 assoc_records_t assoc_records_t
189 406 ft_get_all_programs(const char file[]) ft_get_all_programs(const char file[])
190 407 { {
File src/filetype.h changed (mode: 100644) (index ab542ee04..24a00d8f6)
... ... struct strlist_t;
136 136 /* Gets all existing viewers for file. Returns the list, which can be empty. */ /* Gets all existing viewers for file. Returns the list, which can be empty. */
137 137 struct strlist_t ft_get_viewers(const char file[]); struct strlist_t ft_get_viewers(const char file[]);
138 138
139 /* Moves the specified viewer to the top by rotating the subset of viewers that
140 * match the specified files. */
141 void ft_move_viewer_to_top(const char file[], const char viewer[]);
142
139 143 /* Gets list of programs associated with specified file name. Returns the list. /* Gets list of programs associated with specified file name. Returns the list.
140 144 * Caller should free the result by calling ft_assoc_records_free() on it. */ * Caller should free the result by calling ft_assoc_records_free() on it. */
141 145 assoc_records_t ft_get_all_viewers(const char file[]); assoc_records_t ft_get_all_viewers(const char file[]);
File src/modes/view.c changed (mode: 100644) (index efa912162..f41ca6a9a)
... ... static void cmd_A(key_info_t key_info, keys_info_t *keys_info);
165 165 static void cmd_F(key_info_t key_info, keys_info_t *keys_info); static void cmd_F(key_info_t key_info, keys_info_t *keys_info);
166 166 static void cmd_G(key_info_t key_info, keys_info_t *keys_info); static void cmd_G(key_info_t key_info, keys_info_t *keys_info);
167 167 static void cmd_N(key_info_t key_info, keys_info_t *keys_info); static void cmd_N(key_info_t key_info, keys_info_t *keys_info);
168 static void cmd_P(key_info_t key_info, keys_info_t *keys_info);
168 169 static void cmd_R(key_info_t key_info, keys_info_t *keys_info); static void cmd_R(key_info_t key_info, keys_info_t *keys_info);
169 170 static int load_view_data(modview_info_t *vi, const char action[], static int load_view_data(modview_info_t *vi, const char action[],
170 171 const char file_to_view[], int silent); const char file_to_view[], int silent);
 
... ... static keys_add_info_t builtin_cmds[] = {
279 280 {WK_G, {{&cmd_G}, .descr = "scroll to the end"}}, {WK_G, {{&cmd_G}, .descr = "scroll to the end"}},
280 281 {WK_N, {{&cmd_N}, .descr = "go to previous search match"}}, {WK_N, {{&cmd_N}, .descr = "go to previous search match"}},
281 282 {WK_Q, {{&cmd_q}, .descr = "leave view mode"}}, {WK_Q, {{&cmd_q}, .descr = "leave view mode"}},
283 {WK_P, {{&cmd_P}, .descr = "preserve the current viewer choice"}},
282 284 {WK_R, {{&cmd_R}, .descr = "reload view contents"}}, {WK_R, {{&cmd_R}, .descr = "reload view contents"}},
283 285 {WK_Z WK_Q, {{&cmd_q}, .descr = "leave view mode"}}, {WK_Z WK_Q, {{&cmd_q}, .descr = "leave view mode"}},
284 286 {WK_Z WK_Z, {{&cmd_q}, .descr = "leave view mode"}}, {WK_Z WK_Z, {{&cmd_q}, .descr = "leave view mode"}},
 
... ... cmd_N(key_info_t key_info, keys_info_t *keys_info)
1063 1065 goto_search_result(key_info.count, 1); goto_search_result(key_info.count, 1);
1064 1066 } }
1065 1067
1068 /* Preserves current previewer for the duration of the session. */
1069 static void
1070 cmd_P(key_info_t key_info, keys_info_t *keys_info)
1071 {
1072 if(vi->curr_viewer == vi->ext_viewer)
1073 {
1074 display_error("Can't persist an external viewer.");
1075 return;
1076 }
1077
1078 if(vi->raw)
1079 {
1080 display_error("No viewer to persist, raw previewing is active.");
1081 return;
1082 }
1083
1084 ft_move_viewer_to_top(vi->filename, vi->curr_viewer);
1085 }
1086
1066 1087 /* Handles view data reloading key. */ /* Handles view data reloading key. */
1067 1088 static void static void
1068 1089 cmd_R(key_info_t key_info, keys_info_t *keys_info) cmd_R(key_info_t key_info, keys_info_t *keys_info)
File src/tags.c changed (mode: 100644) (index cf7ef930e..ad219091d)
... ... const char *tags[] = {
938 938 "vifm-q_F", "vifm-q_F",
939 939 "vifm-q_G", "vifm-q_G",
940 940 "vifm-q_N", "vifm-q_N",
941 "vifm-q_P",
941 942 "vifm-q_Q", "vifm-q_Q",
942 943 "vifm-q_R", "vifm-q_R",
943 944 "vifm-q_SHIFT-Tab", "vifm-q_SHIFT-Tab",
File src/utils/mem.c changed (mode: 100644) (index b81f46d5a..1d61b576b)
... ... mem_shr(void *ptr, size_t count, size_t item_len, int offset)
57 57 memmove(p + item_len*offset, p, (count - offset)*item_len); memmove(p + item_len*offset, p, (count - offset)*item_len);
58 58 } }
59 59
60 void
61 mem_cpy(void *dst, void *src, size_t count, size_t item_len)
62 {
63 if(count != 0)
64 {
65 memcpy(dst, src, count*item_len);
66 }
67 }
68
60 69 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */ /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
61 70 /* vim: set cinoptions+=t0 filetype=c : */ /* vim: set cinoptions+=t0 filetype=c : */
File src/utils/mem.h changed (mode: 100644) (index a9d8d19a2..e95cc3c5d)
... ... void mem_shl(void *ptr, size_t count, size_t item_len, int offset);
32 32 * 0 -> 1, 1 -> 2, ... */ * 0 -> 1, 1 -> 2, ... */
33 33 void mem_shr(void *ptr, size_t count, size_t item_len, int offset); void mem_shr(void *ptr, size_t count, size_t item_len, int offset);
34 34
35 /* A super thin wrapper over memcpy() for copying arrays. */
36 void mem_cpy(void *dst, void *src, size_t count, size_t item_len);
37
35 38 #endif /* VIFM__UTILS__MEM_H__ */ #endif /* VIFM__UTILS__MEM_H__ */
36 39
37 40 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */ /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
File tests/misc/view_mode.c changed (mode: 100644) (index 7d6b62f08..4516e5877)
... ... TEST(switching_between_viewers)
127 127 (void)vle_keys_exec_timed_out(WK_A); (void)vle_keys_exec_timed_out(WK_A);
128 128 assert_string_equal("echo 3", modview_current_viewer(lwin.vi)); assert_string_equal("echo 3", modview_current_viewer(lwin.vi));
129 129
130 /* Switching viewers is local to the view mode. */
131 assert_string_equal("echo 1", ft_get_viewer("read"));
132
130 133 (void)vle_keys_exec_timed_out(WK_i); (void)vle_keys_exec_timed_out(WK_i);
131 134 assert_true(modview_is_raw(lwin.vi)); assert_true(modview_is_raw(lwin.vi));
132 135 (void)vle_keys_exec_timed_out(WK_a); (void)vle_keys_exec_timed_out(WK_a);
 
... ... TEST(switching_between_viewers)
135 138 assert_string_equal("echo 3", modview_current_viewer(lwin.vi)); assert_string_equal("echo 3", modview_current_viewer(lwin.vi));
136 139 } }
137 140
141 TEST(no_persisting_for_external_viewer)
142 {
143 opt_handlers_setup();
144 curr_stats.number_of_windows = 2;
145 make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH, "read",
146 NULL);
147 populate_dir_list(&lwin, 0);
148
149 int save_msg;
150 rn_ext(&lwin, "echo 1", "title", MF_PREVIEW_OUTPUT | MF_NO_CACHE, /*pause=*/0,
151 /*bg=*/0, &save_msg);
152 (void)vle_keys_exec_timed_out(WK_C_w WK_w);
153 assert_true(vle_mode_is(VIEW_MODE));
154
155 ui_sb_msg("");
156 (void)vle_keys_exec_timed_out(WK_P);
157 assert_string_equal("Can't persist an external viewer.", ui_sb_last());
158
159 qv_hide();
160 opt_handlers_teardown();
161 }
162
163 TEST(persisting_a_viewer_choice)
164 {
165 assert_true(start_view_mode("bin*", "echo 1, echo 2, echo 3", TEST_DATA_PATH,
166 "read"));
167 assert_string_equal("echo 1", ft_get_viewer("binary-data"));
168
169 ui_sb_msg("");
170 (void)vle_keys_exec_timed_out(WK_i);
171 (void)vle_keys_exec_timed_out(WK_P);
172 assert_string_equal("No viewer to persist, raw previewing is active.",
173 ui_sb_last());
174 (void)vle_keys_exec_timed_out(WK_i);
175
176 (void)vle_keys_exec_timed_out(WK_a);
177 assert_string_equal("echo 2", modview_current_viewer(lwin.vi));
178 (void)vle_keys_exec_timed_out(WK_P);
179 assert_string_equal("echo 2", ft_get_viewer("binary-data"));
180 }
181
138 182 TEST(directories_are_matched_separately) TEST(directories_are_matched_separately)
139 183 { {
140 184 assert_true(start_view_mode("*[^/]", "echo 1, echo 2, echo 3", TEST_DATA_PATH, assert_true(start_view_mode("*[^/]", "echo 1, echo 2, echo 3", TEST_DATA_PATH,
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