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 27745820edd3a1802f2191f3dbb5f7e4daa4a013

Add `:view next|prev` to switch previewers
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 22:05
Committer name: xaizek
Committer date (UTC): 2025-09-02 21:33
Parent(s): 49f429201137190dd157b8324fdca8e98949bb11
Signing key: 99DC5E4DB05F6BE2
Tree: 02620d3c1cf2c1e3289639bd5b4eaee6a996bce2
File Lines added Lines deleted
ChangeLog 3 0
data/man/vifm.1 9 1
data/vim/doc/app/vifm-app.txt 7 1
src/cmd_completion.c 19 0
src/cmd_completion.h 1 0
src/cmd_handlers.c 75 6
src/filetype.c 161 0
src/filetype.h 6 0
tests/commands/completion.c 10 0
tests/commands/misc.c 30 0
tests/filetype/viewers.c 231 0
File ChangeLog changed (mode: 100644) (index 97aea0db9..e95060a26)
24 24 Added P view mode key to make choice of a viewer via a and A keys Added P view mode key to make choice of a viewer via a and A keys
25 25 persistent for the session. Thanks to j-xella, durcheinandr and vuenn. persistent for the session. Thanks to j-xella, durcheinandr and vuenn.
26 26
27 Added optional "next" and "prev" actions to :view command to switch
28 default previewer for the current and similar files.
29
27 30 Updated utf8proc to v2.10.0. Updated utf8proc to v2.10.0.
28 31
29 32 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 cb147f5e0..82ccdca97)
1 .TH VIFM 1 "1 September 2025" "vifm 0.15"
1 .TH VIFM 1 "2 September 2025" "vifm 0.15"
2 2 .\" --------------------------------------------------------------------------- .\" ---------------------------------------------------------------------------
3 3 .SH NAME .SH NAME
4 4 .\" --------------------------------------------------------------------------- .\" ---------------------------------------------------------------------------
 
... ... also 'quickview' option.
3276 3276 .BI :vie[w]! .BI :vie[w]!
3277 3277 turn on quick file view if it's off. turn on quick file view if it's off.
3278 3278 .TP .TP
3279 .BI :vie[w] next
3280 move the default viewer for the current and similar files to the bottom of
3281 the list of viewers, making the next avabile one the new default.
3282 .TP
3283 .BI :vie[w] prev
3284 move the last viewer for the current and similar files to the top of
3285 the list of viewers.
3286 .TP
3279 3287 .BI " :volumes" .BI " :volumes"
3280 3288 .TP .TP
3281 3289 .BI :volumes .BI :volumes
File data/vim/doc/app/vifm-app.txt changed (mode: 100644) (index 5299e0d9f..ced1d1a84)
1 *vifm-app.txt* For Vifm version 0.15 Last change: 2025 September 1
1 *vifm-app.txt* For Vifm version 0.15 Last change: 2025 September 2
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.
2746 2746 See also |vifm-'quickview'| option and |vifm-commands-and-selection|. See also |vifm-'quickview'| option and |vifm-commands-and-selection|.
2747 2747 :vie[w]! :vie[w]!
2748 2748 turn on quick file view if it's off. turn on quick file view if it's off.
2749 :vie[w] next
2750 move the default viewer for the current and similar files to the bottom of
2751 the list of viewers, making the next avabile one the new default.
2752 :vie[w] prev
2753 move the last viewer for the current and similar files to the top of
2754 the list of viewers.
2749 2755
2750 2756 *vifm-:volumes* *vifm-:volumes*
2751 2757 :volumes {only for MS-Windows} :volumes {only for MS-Windows}
File src/cmd_completion.c changed (mode: 100644) (index 9eda1c771..9b0c01a60)
... ... static void complete_wingo(const char str[]);
119 119 static int wingo_sorter(const char a[], const char b[]); static int wingo_sorter(const char a[], const char b[]);
120 120 static const char * skip_number(const char str[]); static const char * skip_number(const char str[]);
121 121 static void complete_winrun(const char str[]); static void complete_winrun(const char str[]);
122 static void complete_view(const char str[]);
122 123 static void complete_from_string_list(const char str[], const char *items[][2], static void complete_from_string_list(const char str[], const char *items[][2],
123 124 size_t item_count, int ignore_case); size_t item_count, int ignore_case);
124 125 static void complete_command_name(const char beginning[]); static void complete_command_name(const char beginning[]);
 
... ... non_path_completion(completion_data_t *data)
322 323 }; };
323 324 complete_from_string_list(arg, lines, ARRAY_LEN(lines), /*ignore_case=*/0); complete_from_string_list(arg, lines, ARRAY_LEN(lines), /*ignore_case=*/0);
324 325 } }
326 else if(id == COM_VIEW)
327 {
328 if(!emark)
329 {
330 complete_view(arg);
331 }
332 }
325 333 else else
326 334 { {
327 335 return -1; return -1;
 
... ... complete_winrun(const char str[])
1124 1132 complete_from_string_list(str, win_marks, ARRAY_LEN(win_marks), 0); complete_from_string_list(str, win_marks, ARRAY_LEN(win_marks), 0);
1125 1133 } }
1126 1134
1135 /* Completes the first argument of :view. */
1136 static void
1137 complete_view(const char str[])
1138 {
1139 static const char *lines[][2] = {
1140 { "next", "make the next previewer the default one" },
1141 { "prev", "make the last previewer the default one" },
1142 };
1143 complete_from_string_list(str, lines, ARRAY_LEN(lines), /*ignore_case=*/0);
1144 }
1145
1127 1146 /* Performs str completion using items in the list of length item_count. */ /* Performs str completion using items in the list of length item_count. */
1128 1147 static void static void
1129 1148 complete_from_string_list(const char str[], const char *items[][2], complete_from_string_list(const char str[], const char *items[][2],
File src/cmd_completion.h changed (mode: 100644) (index 4436d1c81..4dc2542ff)
... ... enum
75 75 COM_TOUCH, COM_TOUCH,
76 76 COM_TREE, COM_TREE,
77 77 COM_UNLET, COM_UNLET,
78 COM_VIEW,
78 79 COM_VSPLIT, COM_VSPLIT,
79 80 COM_WINCMD, COM_WINCMD,
80 81 COM_WINDO, COM_WINDO,
File src/cmd_handlers.c changed (mode: 100644) (index 5a21128f5..b99cd3c8a)
... ... const cmd_add_t cmds_list[] = {
936 936 .descr = "display version information", .descr = "display version information",
937 937 .flags = HAS_COMMENT, .flags = HAS_COMMENT,
938 938 .handler = &vifm_cmd, .min_args = 0, .max_args = 0, }, .handler = &vifm_cmd, .min_args = 0, .max_args = 0, },
939 { .name = "view", .abbr = "vie", .id = -1,
939 { .name = "view", .abbr = "vie", .id = COM_VIEW,
940 940 .descr = "control visibility of preview", .descr = "control visibility of preview",
941 941 .flags = HAS_EMARK | HAS_COMMENT, .flags = HAS_EMARK | HAS_COMMENT,
942 .handler = &view_cmd, .min_args = 0, .max_args = 0, },
942 .handler = &view_cmd, .min_args = 0, .max_args = 1, },
943 943 { .name = "vifm", .abbr = NULL, .id = -1, { .name = "vifm", .abbr = NULL, .id = -1,
944 944 .descr = "display version information", .descr = "display version information",
945 945 .flags = HAS_COMMENT, .flags = HAS_COMMENT,
 
... ... unselect_cmd(const cmd_info_t *cmd_info)
5355 5355 return (error ? CMDS_ERR_CUSTOM : 0); return (error ? CMDS_ERR_CUSTOM : 0);
5356 5356 } }
5357 5357
5358 /* Enables/disables/toggles quickview and switches viewers. */
5358 5359 static int static int
5359 5360 view_cmd(const cmd_info_t *cmd_info) view_cmd(const cmd_info_t *cmd_info)
5360 5361 { {
5361 5362 cmds_preserve_selection(); cmds_preserve_selection();
5362 5363
5363 if((!curr_stats.preview.on || cmd_info->emark) && !qv_can_show())
5364 if(cmd_info->emark && cmd_info->argc != 0)
5364 5365 { {
5366 ui_sb_err("No arguments are allowed if you use \"!\"");
5365 5367 return CMDS_ERR_CUSTOM; return CMDS_ERR_CUSTOM;
5366 5368 } }
5367 if(curr_stats.preview.on && cmd_info->emark)
5369
5370 if(cmd_info->argc == 0)
5368 5371 { {
5372 if((!curr_stats.preview.on || cmd_info->emark) && !qv_can_show())
5373 {
5374 return CMDS_ERR_CUSTOM;
5375 }
5376 if(curr_stats.preview.on && cmd_info->emark)
5377 {
5378 return 0;
5379 }
5380 qv_toggle();
5369 5381 return 0; return 0;
5370 5382 } }
5371 qv_toggle();
5372 return 0;
5383
5384 if(strcmp(cmd_info->argv[0], "prev") != 0 &&
5385 strcmp(cmd_info->argv[0], "next") != 0)
5386 {
5387 ui_sb_errf("Unexpected action: %s", cmd_info->argv[0]);
5388 return CMDS_ERR_CUSTOM;
5389 }
5390
5391 char *typed_fpath = get_typed_entry_fpath(get_current_entry(curr_view));
5392 if(typed_fpath == NULL)
5393 {
5394 show_error_msg("Memory Error", "Unable to allocate enough memory");
5395 return 0;
5396 }
5397
5398 assoc_records_t viewers = ft_get_all_viewers(typed_fpath);
5399 if(viewers.count == 0)
5400 {
5401 free(typed_fpath);
5402 ft_assoc_records_free(&viewers);
5403 ui_sb_msg("No viewers for the current file");
5404 return 1;
5405 }
5406
5407 if(viewers.count != 1)
5408 {
5409 if(strcmp(cmd_info->argv[0], "next") == 0)
5410 {
5411 ft_move_viewer_cycle_next(typed_fpath);
5412 }
5413 else
5414 {
5415 ft_move_viewer_cycle_prev(typed_fpath);
5416 }
5417
5418 ft_assoc_records_free(&viewers);
5419 viewers = ft_get_all_viewers(typed_fpath);
5420
5421 stats_redraw_later();
5422
5423 if(viewers.count == 0)
5424 {
5425 free(typed_fpath);
5426 ft_assoc_records_free(&viewers);
5427 show_error_msg("Memory Error", "Unable to allocate enough memory");
5428 return 0;
5429 }
5430 }
5431
5432 const char *text = viewers.list[0].description;
5433 if(is_null_or_empty(text))
5434 {
5435 text = viewers.list[0].command;
5436 }
5437 ui_sb_msgf("Top viewer (out of %d): %s", viewers.count, text);
5438
5439 ft_assoc_records_free(&viewers);
5440 free(typed_fpath);
5441 return 1;
5373 5442 } }
5374 5443
5375 5444 static int static int
File src/filetype.c changed (mode: 100644) (index 13d6bf38b..2c8d98982)
... ... reordering_data_t;
63 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,
64 64 const char file[]); const char file[]);
65 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 int pick_out_until_last(const char file[], reordering_data_t *d);
66 67 static void make_pivot_first(reordering_data_t *d); static void make_pivot_first(reordering_data_t *d);
68 static void make_pivot_last(reordering_data_t *d);
67 69 static int pick_out_until_first(const char file[], const char viewer[], static int pick_out_until_first(const char file[], const char viewer[],
68 70 reordering_data_t *d); reordering_data_t *d);
69 71 static int split_assoc(assoc_t *assoc, int at_record_idx, assoc_t *out_prefix); static int split_assoc(assoc_t *assoc, int at_record_idx, assoc_t *out_prefix);
 
... ... ft_move_viewer_to_top(const char file[], const char viewer[])
238 240 DA_REMOVE_ALL(d.prefix); DA_REMOVE_ALL(d.prefix);
239 241 } }
240 242
243 void
244 ft_move_viewer_cycle_prev(const char file[])
245 {
246 reordering_data_t d = {};
247
248 /*
249 * Overall approach:
250 * 1. Find the last entry with an existing program record.
251 * 2. Move all of its matching predecessors to the end of the list.
252 * 3. Special case: if that record is not the first one, cut the entry at it
253 * and make it last.
254 */
255 if(pick_out_until_last(file, &d) == 0)
256 {
257 make_pivot_first(&d);
258 }
259
260 free(d.list);
261 DA_REMOVE_ALL(d.prefix);
262 }
263
264 /* Finds the last matching entry in the list of viewers that's also present.
265 * Collects all entries that match file and precede the found entry in
266 * d->prefix. Returns zero on successfully finding a viewer. */
267 static int
268 pick_out_until_last(const char file[], reordering_data_t *d)
269 {
270 int l;
271 for(l = fileviewers.count - 1; l >= 0; --l)
272 {
273 assoc_t *assoc = &fileviewers.list[l];
274
275 if(!mg_match(&assoc->mg, file))
276 {
277 continue;
278 }
279
280 for(d->k = assoc->records.count - 1; d->k >= 0; --d->k)
281 {
282 if(ft_exists(assoc->records.list[d->k].command))
283 {
284 break;
285 }
286 }
287
288 if(d->k >= 0)
289 {
290 break;
291 }
292 }
293
294 if(l < 0)
295 {
296 /* No present viewers for the file. */
297 return 1;
298 }
299
300 /* The new list needs to have room for an extra element in case a matching
301 * entry needs to be split in two. */
302 d->list = malloc((fileviewers.count + 1)*sizeof(*d->list));
303 if(d->list == NULL)
304 {
305 return 1;
306 }
307
308 /* Make the last entry the top one by moving all matching predecessors
309 * directly below it (rotate matching viewers around the last of them). */
310 for(d->i = 0, d->j = 0; d->i < l; ++d->i)
311 {
312 assoc_t *assoc = &fileviewers.list[d->i];
313
314 if(!mg_match(&assoc->mg, file))
315 {
316 d->list[d->j++] = *assoc;
317 continue;
318 }
319
320 assoc_t *another = DA_EXTEND(d->prefix);
321 if(another == NULL)
322 {
323 return 1;
324 }
325
326 *another = *assoc;
327 DA_COMMIT(d->prefix);
328 }
329
330 return 0;
331 }
332
241 333 /* Makes a "pivot" element the first one among subset of matching file viewers. /* Makes a "pivot" element the first one among subset of matching file viewers.
242 334 * This is done by putting the element, the tail of the original list and all * This is done by putting the element, the tail of the original list and all
243 335 * subset elements in front of the pivot in the right order. */ * subset elements in front of the pivot in the right order. */
 
... ... make_pivot_first(reordering_data_t *d)
278 370 } }
279 371 } }
280 372
373 void
374 ft_move_viewer_cycle_next(const char file[])
375 {
376 reordering_data_t d = {};
377
378 /*
379 * Overall approach:
380 * 1. Find the first entry with an existing program record.
381 * 2. Move it after the last matching entry.
382 * 3. Special case: if the record is not the last one, cut the entry after
383 * the record and move only this prefix.
384 */
385 if(pick_out_until_first(file, /*viewer=*/NULL, &d) == 0)
386 {
387 make_pivot_last(&d);
388 }
389
390 free(d.list);
391 DA_REMOVE_ALL(d.prefix);
392 }
393
394 /* Makes a "pivot" element the last one among subset of matching file viewers.
395 * This is done by putting the element, the tail of the original list and all
396 * subset elements in front of the pivot in the right order. */
397 static void
398 make_pivot_last(reordering_data_t *d)
399 {
400 assoc_t last_item;
401 int need_split = (d->k != fileviewers.list[d->i].records.count - 1);
402 if(need_split)
403 {
404 /* The viewer is not the last in the list, so split the association at
405 * the viewer. The code below ensures the viewer will be at the new top
406 * for the subset matching this file and then the prefix will be
407 * appended-> */
408 if(split_assoc(&fileviewers.list[d->i], d->k + 1, &last_item) != 0)
409 {
410 return;
411 }
412 }
413 else
414 {
415 last_item = fileviewers.list[d->i++];
416 }
417
418 mem_cpy(&d->list[d->j], &fileviewers.list[d->i], fileviewers.count - d->i,
419 sizeof(d->list[0]));
420 d->j += fileviewers.count - d->i;
421
422 mem_cpy(&d->list[d->j], &d->prefix[0], DA_SIZE(d->prefix),
423 sizeof(d->prefix[0]));
424 d->j += DA_SIZE(d->prefix);
425
426 free(fileviewers.list);
427 fileviewers.list = d->list;
428 d->list = NULL;
429
430 if(need_split)
431 {
432 assert(d->j == fileviewers.count);
433 fileviewers.list[fileviewers.count++] = last_item;
434 }
435 else
436 {
437 assert(d->j == fileviewers.count - 1);
438 fileviewers.list[d->j] = last_item;
439 }
440 }
441
281 442 /* Finds a matching entry in the list of viewers either by checking for its /* Finds a matching entry in the list of viewers either by checking for its
282 443 * existence (viewer is NULL) or by matching it against a passed in string * existence (viewer is NULL) or by matching it against a passed in string
283 444 * (viewer is not NULL). Collects all entries matching file in d->prefix array * (viewer is not NULL). Collects all entries matching file in d->prefix array
File src/filetype.h changed (mode: 100644) (index 24a00d8f6..03d488a6c)
... ... struct strlist_t ft_get_viewers(const char file[]);
140 140 * match the specified files. */ * match the specified files. */
141 141 void ft_move_viewer_to_top(const char file[], const char viewer[]); void ft_move_viewer_to_top(const char file[], const char viewer[]);
142 142
143 /* Moves the bottom viewer to the top, top to the second place, etc. */
144 void ft_move_viewer_cycle_prev(const char file[]);
145
146 /* Moves the current top viewer to the bottom, second place to the top, etc. */
147 void ft_move_viewer_cycle_next(const char file[]);
148
143 149 /* Gets list of programs associated with specified file name. Returns the list. /* Gets list of programs associated with specified file name. Returns the list.
144 150 * 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. */
145 151 assoc_records_t ft_get_all_viewers(const char file[]); assoc_records_t ft_get_all_viewers(const char file[]);
File tests/commands/completion.c changed (mode: 100644) (index 101f7c1e6..eb06c4dc3)
... ... TEST(messages_is_completed)
679 679 ASSERT_COMPLETION(L"messages c c", L"messages c c"); ASSERT_COMPLETION(L"messages c c", L"messages c c");
680 680 } }
681 681
682 TEST(view_is_completed)
683 {
684 ASSERT_NO_COMPLETION(L"view! n");
685
686 ASSERT_COMPLETION(L"view ", L"view next");
687 ASSERT_NEXT_MATCH("prev");
688 ASSERT_NEXT_MATCH("");
689 ASSERT_NEXT_MATCH("next");
690 }
691
682 692 static void static void
683 693 prepare_for_line_completion(const wchar_t str[]) prepare_for_line_completion(const wchar_t str[])
684 694 { {
File tests/commands/misc.c changed (mode: 100644) (index 1d589b0ed..1c2ff31b9)
... ... TEST(view_command)
506 506 assert_success(cmds_dispatch("view", &lwin, CIT_COMMAND)); assert_success(cmds_dispatch("view", &lwin, CIT_COMMAND));
507 507 assert_false(curr_stats.preview.on); assert_false(curr_stats.preview.on);
508 508
509 /* Reordering viewers. */
510
511 ui_sb_msg("");
512 assert_failure(cmds_dispatch("view! next", &lwin, CIT_COMMAND));
513 assert_string_equal("No arguments are allowed if you use \"!\"",
514 ui_sb_last());
515
516 assert_failure(cmds_dispatch("view n", &lwin, CIT_COMMAND));
517 assert_string_equal("Unexpected action: n", ui_sb_last());
518
519 append_view_entry(&lwin, "file.zip");
520
521 assert_failure(cmds_dispatch("view next", &lwin, CIT_COMMAND));
522 assert_string_equal("No viewers for the current file", ui_sb_last());
523
524 assoc_viewers("*.zip", "archiver1, {custom name}archiver2");
525 assert_string_equal("archiver1", ft_get_viewer("file.zip"));
526
527 assert_failure(cmds_dispatch("view next", &lwin, CIT_COMMAND));
528 assert_string_equal("archiver2", ft_get_viewer("file.zip"));
529 assert_string_equal("Top viewer (out of 2): custom name", ui_sb_last());
530
531 assert_failure(cmds_dispatch("view next", &lwin, CIT_COMMAND));
532 assert_string_equal("archiver1", ft_get_viewer("file.zip"));
533 assert_string_equal("Top viewer (out of 2): archiver1", ui_sb_last());
534
535 assert_failure(cmds_dispatch("view prev", &lwin, CIT_COMMAND));
536 assert_string_equal("archiver2", ft_get_viewer("file.zip"));
537 assert_string_equal("Top viewer (out of 2): custom name", ui_sb_last());
538
509 539 opt_handlers_teardown(); opt_handlers_teardown();
510 540 } }
511 541
File tests/filetype/viewers.c changed (mode: 100644) (index 73f3107ed..8262a752e)
12 12 #include "../../src/utils/string_array.h" #include "../../src/utils/string_array.h"
13 13 #include "test.h" #include "test.h"
14 14
15 static int name_based_exists(const char name[]);
16
15 17 static int static int
16 18 prog1_available(const char name[]) prog1_available(const char name[])
17 19 { {
 
... ... TEST(viewer_kind)
145 147 assert_int_equal(VK_GRAPHICAL, ft_viewer_kind("echo %px %py %pw %ph %%pd")); assert_int_equal(VK_GRAPHICAL, ft_viewer_kind("echo %px %py %pw %ph %%pd"));
146 148 } }
147 149
150 TEST(viewer_topping_1file_2separate_viewers)
151 {
152 assoc_viewers("*.tar.bz2", "prog1");
153 assoc_viewers("*.tar.bz2", "prog2");
154
155 assert_string_equal("prog1", ft_get_viewer("archive.tar.bz2"));
156
157 /* Already the top one. */
158 ft_move_viewer_to_top("archive.tar.bz2", "prog1");
159 assert_string_equal("prog1", ft_get_viewer("archive.tar.bz2"));
160
161 /* Swap the order. */
162 ft_move_viewer_to_top("archive.tar.bz2", "prog2");
163 assert_string_equal("prog2", ft_get_viewer("archive.tar.bz2"));
164
165 /* Swap the order again. */
166 ft_move_viewer_to_top("archive.tar.bz2", "prog2");
167 assert_string_equal("prog2", ft_get_viewer("archive.tar.bz2"));
168 }
169
170 TEST(viewer_topping_1file_2joined_viewers)
171 {
172 assoc_viewers("*.tar.bz2", "prog1, prog2");
173
174 assert_string_equal("prog1", ft_get_viewer("archive.tar.bz2"));
175
176 /* Already the top one. */
177 ft_move_viewer_to_top("archive.tar.bz2", "prog1");
178 assert_string_equal("prog1", ft_get_viewer("archive.tar.bz2"));
179
180 /* Swap the order. */
181 ft_move_viewer_to_top("archive.tar.bz2", "prog2");
182 assert_string_equal("prog2", ft_get_viewer("archive.tar.bz2"));
183
184 /* Swap the order again. */
185 ft_move_viewer_to_top("archive.tar.bz2", "prog2");
186 assert_string_equal("prog2", ft_get_viewer("archive.tar.bz2"));
187 }
188
189 TEST(viewer_topping_2files_2joined_viewers)
190 {
191 assoc_viewers("*.tar.bz2,*.zip", "prog1, prog2");
192
193 assert_string_equal("prog1", ft_get_viewer("archive.tar.bz2"));
194 assert_string_equal("prog1", ft_get_viewer("archive.zip"));
195
196 /* Already the top one. */
197 ft_move_viewer_to_top("archive.tar.bz2", "prog1");
198 assert_string_equal("prog1", ft_get_viewer("archive.tar.bz2"));
199 assert_string_equal("prog1", ft_get_viewer("archive.zip"));
200
201 /* Swap the order. */
202 ft_move_viewer_to_top("archive.tar.bz2", "prog2");
203 assert_string_equal("prog2", ft_get_viewer("archive.tar.bz2"));
204 assert_string_equal("prog2", ft_get_viewer("archive.zip"));
205
206 /* Swap the order again. */
207 ft_move_viewer_to_top("archive.tar.bz2", "prog2");
208 assert_string_equal("prog2", ft_get_viewer("archive.tar.bz2"));
209 assert_string_equal("prog2", ft_get_viewer("archive.zip"));
210 }
211
212 TEST(viewer_topping_2files_mixed_viewers)
213 {
214 assoc_viewers("*.tar.bz2,*.zip", "prog1, prog2");
215 assoc_viewers("*.zip", "prog4");
216 assoc_viewers("*.tar.bz2", "prog3");
217 assoc_viewers("*", "defprog");
218
219 assert_string_equal("prog1", ft_get_viewer("archive.tar.bz2"));
220 assert_string_equal("prog1", ft_get_viewer("archive.zip"));
221
222 /* Already the top one. */
223 ft_move_viewer_to_top("archive.tar.bz2", "prog1");
224 assert_string_equal("prog1", ft_get_viewer("archive.tar.bz2"));
225 assert_string_equal("prog1", ft_get_viewer("archive.zip"));
226
227 /* Swap the order. */
228 ft_move_viewer_to_top("archive.tar.bz2", "prog3");
229 assert_string_equal("prog3", ft_get_viewer("archive.tar.bz2"));
230 assert_string_equal("prog4", ft_get_viewer("archive.zip"));
231
232 /* Swap the order again. */
233 ft_move_viewer_to_top("archive.tar.bz2", "prog2");
234 assert_string_equal("prog2", ft_get_viewer("archive.tar.bz2"));
235 assert_string_equal("prog4", ft_get_viewer("archive.zip"));
236 }
237
238 TEST(viewer_cycling_1file_overlap_next)
239 {
240 assoc_viewers("{*.png}", "unrelated");
241 assoc_viewers("{*.tar.bz2},{*.tar.*},{*.bz2}", "prog,prog2");
242 assoc_viewers("{*}", "defprog");
243
244 assert_string_equal("unrelated", ft_get_viewer("file.png"));
245 assert_string_equal("prog", ft_get_viewer("archive.tar.bz2"));
246
247 ft_move_viewer_cycle_next("archive.tar.bz2");
248 assert_string_equal("prog2", ft_get_viewer("archive.tar.bz2"));
249 ft_move_viewer_cycle_next("archive.tar.bz2");
250 assert_string_equal("defprog", ft_get_viewer("archive.tar.bz2"));
251 ft_move_viewer_cycle_next("archive.tar.bz2");
252 assert_string_equal("prog", ft_get_viewer("archive.tar.bz2"));
253 ft_move_viewer_cycle_next("archive.tar.bz2");
254 assert_string_equal("prog2", ft_get_viewer("archive.tar.bz2"));
255
256 /* Verifying that nothing unexpected has happened to this file. */
257 assert_string_equal("unrelated", ft_get_viewer("file.png"));
258 }
259
260 TEST(viewer_cycling_1file_overlap_prev)
261 {
262 assoc_viewers("{*.png}", "unrelated");
263 assoc_viewers("{*.tar.bz2},{*.tar.*},{*.bz2}", "prog");
264 assoc_viewers("{*}", "defprog");
265
266 assert_string_equal("unrelated", ft_get_viewer("file.png"));
267 assert_string_equal("prog", ft_get_viewer("archive.tar.bz2"));
268
269 ft_move_viewer_cycle_prev("archive.tar.bz2");
270 assert_string_equal("defprog", ft_get_viewer("archive.tar.bz2"));
271 ft_move_viewer_cycle_prev("archive.tar.bz2");
272 assert_string_equal("prog", ft_get_viewer("archive.tar.bz2"));
273 ft_move_viewer_cycle_prev("archive.tar.bz2");
274 assert_string_equal("defprog", ft_get_viewer("archive.tar.bz2"));
275
276 /* Verifying that nothing unexpected has happened to this file. */
277 assert_string_equal("unrelated", ft_get_viewer("file.png"));
278 }
279
280 TEST(viewer_cycling_1file_overlap)
281 {
282 assoc_viewers("{*.mp3}", "prog1");
283 assoc_viewers("{*.mp3}", "prog2");
284 assoc_viewers("{*}", "defprog");
285
286 assert_string_equal("prog1", ft_get_viewer("file.mp3"));
287
288 /* Forwards. */
289 ft_move_viewer_cycle_next("file.mp3");
290 assert_string_equal("prog2", ft_get_viewer("file.mp3"));
291 ft_move_viewer_cycle_next("file.mp3");
292 assert_string_equal("defprog", ft_get_viewer("file.mp3"));
293 ft_move_viewer_cycle_next("file.mp3");
294 assert_string_equal("prog1", ft_get_viewer("file.mp3"));
295
296 /* Backwards. */
297 ft_move_viewer_cycle_prev("file.mp3");
298 assert_string_equal("defprog", ft_get_viewer("file.mp3"));
299 ft_move_viewer_cycle_prev("file.mp3");
300 assert_string_equal("prog2", ft_get_viewer("file.mp3"));
301 ft_move_viewer_cycle_prev("file.mp3");
302 assert_string_equal("prog1", ft_get_viewer("file.mp3"));
303 }
304
305 TEST(viewer_reordering_1file_missing_viewers)
306 {
307 ft_init(&name_based_exists);
308
309 assoc_viewers("{*.mp3}", "m1");
310 assoc_viewers("{*.mp3}", "p2");
311 assoc_viewers("{*.mp3}", "p3");
312
313 assert_string_equal("p2", ft_get_viewer("file.mp3"));
314
315 /* Forwards. */
316 ft_move_viewer_cycle_next("file.mp3");
317 assert_string_equal("p3", ft_get_viewer("file.mp3"));
318 ft_move_viewer_cycle_next("file.mp3");
319 assert_string_equal("p2", ft_get_viewer("file.mp3"));
320 ft_move_viewer_cycle_next("file.mp3");
321 assert_string_equal("p3", ft_get_viewer("file.mp3"));
322 ft_init(NULL);
323 ft_move_viewer_cycle_next("file.mp3");
324 assert_string_equal("m1", ft_get_viewer("file.mp3"));
325 ft_move_viewer_cycle_next("file.mp3");
326 assert_string_equal("p2", ft_get_viewer("file.mp3"));
327
328 ft_init(&name_based_exists);
329
330 /* Backwards. */
331 ft_move_viewer_cycle_prev("file.mp3");
332 assert_string_equal("p3", ft_get_viewer("file.mp3"));
333 ft_move_viewer_cycle_prev("file.mp3");
334 assert_string_equal("p2", ft_get_viewer("file.mp3"));
335 ft_init(NULL);
336 ft_move_viewer_cycle_prev("file.mp3");
337 assert_string_equal("m1", ft_get_viewer("file.mp3"));
338 ft_move_viewer_cycle_prev("file.mp3");
339 assert_string_equal("p3", ft_get_viewer("file.mp3"));
340 ft_move_viewer_cycle_prev("file.mp3");
341 assert_string_equal("p2", ft_get_viewer("file.mp3"));
342
343 ft_init(&name_based_exists);
344
345 /* To the top. */
346 ft_move_viewer_to_top("file.mp3", "p3");
347 assert_string_equal("p3", ft_get_viewer("file.mp3"));
348 ft_init(NULL);
349 ft_move_viewer_cycle_next("file.mp3");
350 assert_string_equal("m1", ft_get_viewer("file.mp3"));
351 ft_init(&name_based_exists);
352 assert_string_equal("p2", ft_get_viewer("file.mp3"));
353 }
354
355 TEST(viewer_reordering_file_with_no_viewers)
356 {
357 assoc_viewers("{*.png}", "unrelated");
358
359 assert_string_equal("unrelated", ft_get_viewer("file.png"));
360
361 ft_move_viewer_cycle_prev("archive.tar.bz2");
362 assert_string_equal(NULL, ft_get_viewer("archive.tar.bz2"));
363 ft_move_viewer_cycle_next("archive.tar.bz2");
364 assert_string_equal(NULL, ft_get_viewer("archive.tar.bz2"));
365 ft_move_viewer_to_top("archive.tar.bz2", "nope");
366 assert_string_equal(NULL, ft_get_viewer("archive.tar.bz2"));
367
368 /* Verifying that nothing unexpected has happened to this file. */
369 assert_string_equal("unrelated", ft_get_viewer("file.png"));
370 }
371
372 static int
373 name_based_exists(const char name[])
374 {
375 /* 'p' for "present". */
376 return (name[0] == 'p');
377 }
378
148 379 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */ /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
149 380 /* vim: set cinoptions+=t0 filetype=c : */ /* vim: set cinoptions+=t0 filetype=c : */
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