1 /* vifm
2 * Copyright (C) 2001 Ken Steen.
3 * Copyright (C) 2011 xaizek.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
18 */
19
20 #include "menus.h"
21
22 #include <curses.h>
23
24 #include <assert.h> /* assert() */
25 #include <stddef.h> /* NULL size_t */
26 #include <stdio.h> /* FILE */
27 #include <stdlib.h> /* free() malloc() */
28 #include <string.h> /* memmove() memset() strdup() strcat() strncat() strchr()
29 strlen() strrchr() */
30 #include <wchar.h> /* wchar_t wcscmp() */
31
32 #include "../cfg/config.h"
33 #include "../compat/fs_limits.h"
34 #include "../compat/os.h"
35 #include "../compat/reallocarray.h"
36 #include "../int/term_title.h"
37 #include "../int/vim.h"
38 #include "../modes/dialogs/msg_dialog.h"
39 #include "../modes/cmdline.h"
40 #include "../modes/menu.h"
41 #include "../modes/modes.h"
42 #include "../ui/cancellation.h"
43 #include "../ui/color_manager.h"
44 #include "../ui/color_scheme.h"
45 #include "../ui/colors.h"
46 #include "../ui/statusbar.h"
47 #include "../ui/ui.h"
48 #include "../utils/fs.h"
49 #include "../utils/log.h"
50 #include "../utils/macros.h"
51 #include "../utils/path.h"
52 #include "../utils/regexp.h"
53 #include "../utils/str.h"
54 #include "../utils/string_array.h"
55 #include "../utils/utf8.h"
56 #include "../utils/utils.h"
57 #include "../background.h"
58 #include "../filelist.h"
59 #include "../flist_hist.h"
60 #include "../flist_pos.h"
61 #include "../macros.h"
62 #include "../marks.h"
63 #include "../running.h"
64 #include "../search.h"
65 #include "../status.h"
66
67 static void reset_menu_state(menu_state_t *ms);
68 static void show_position_in_menu(const menu_data_t *m);
69 static void open_selected_file(const char path[], int line_num);
70 static void navigate_to_selected_file(view_t *view, const char path[]);
71 static void draw_menu_item(menu_state_t *ms, int pos, int line, int clear);
72 static void draw_search_match(char str[], int start, int end, int line,
73 int width, const cchar_t *attrs);
74 static void normalize_top(menu_state_t *m);
75 static void draw_menu_frame(const menu_state_t *m);
76 static void output_handler(const char line[], void *arg);
77 static void append_to_string(char **str, const char suffix[]);
78 static char * expand_tabulation_a(const char line[], size_t tab_stops);
79 static void init_menu_state(menu_state_t *ms, view_t *view);
80 static const char * get_relative_path_base(const menu_data_t *m,
81 const view_t *view);
82 static int menu_and_view_are_in_sync(const menu_data_t *m, const view_t *view);
83 static int search_menu(menu_state_t *ms, int start_pos, int print_errors);
84 static int search_menu_forwards(menu_state_t *m, int start_pos);
85 static int search_menu_backwards(menu_state_t *m, int start_pos);
86 static int navigate_to_match(menu_state_t *m, int pos);
87 static int get_match_index(const menu_state_t *m);
88
89 struct menu_state_t
90 {
91 menu_data_t *d;
92 int current; /* Cursor position on the menu_win. */
93 int win_rows;
94 int backward_search; /* Search direction. */
95 /* Number of menu entries that actually match the regexp. */
96 int matching_entries;
97 /* Whether search highlight matches are currently highlighted. */
98 int search_highlight;
99 /* Start and end positions of search match. If there is no match, values are
100 * equal to -1. */
101 short int (*matches)[2];
102 char *regexp;
103 /* Number of times to repeat search. */
104 int search_repeat;
105 /* View associated with the menu (e.g. to navigate to a file in it). */
106 view_t *view;
107 }
108 menu_state;
109
110 /* Temporary storage for data of the last stashable menu. */
111 static menu_data_t menu_data_stash;
112
113 void
menus_remove_current(menu_state_t * ms)114 menus_remove_current(menu_state_t *ms)
115 {
116 menu_data_t *const m = ms->d;
117 menus_erase_current(ms);
118
119 remove_from_string_array(m->items, m->len, m->pos);
120
121 if(m->data != NULL)
122 {
123 remove_from_string_array(m->data, m->len, m->pos);
124 }
125
126 if(m->void_data != NULL)
127 {
128 memmove(m->void_data + m->pos, m->void_data + m->pos + 1,
129 sizeof(*m->void_data)*((m->len - 1) - m->pos));
130 }
131
132 if(ms->matches != NULL)
133 {
134 if(ms->matches[m->pos][0] >= 0)
135 {
136 --ms->matching_entries;
137 }
138 memmove(ms->matches + m->pos, ms->matches + m->pos + 1,
139 sizeof(*ms->matches)*((m->len - 1) - m->pos));
140 }
141
142 --m->len;
143 menus_partial_redraw(ms);
144
145 menus_set_pos(ms, m->pos);
146 }
147
148 void
menus_erase_current(menu_state_t * m)149 menus_erase_current(menu_state_t *m)
150 {
151 draw_menu_item(m, m->d->pos, m->current, 1);
152 }
153
154 void
menus_init_data(menu_data_t * m,view_t * view,char title[],char empty_msg[])155 menus_init_data(menu_data_t *m, view_t *view, char title[], char empty_msg[])
156 {
157 if(m->initialized)
158 {
159 menus_reset_data(m);
160 }
161
162 if(menu_state.d != NULL)
163 {
164 menu_state.d->state = NULL;
165 }
166 menu_state.d = m;
167
168 m->title = escape_unreadable(title);
169 free(title);
170
171 m->top = 0;
172 m->len = 0;
173 m->pos = 0;
174 m->hor_pos = 0;
175 m->items = NULL;
176 m->data = NULL;
177 m->void_data = NULL;
178 m->key_handler = NULL;
179 m->extra_data = 0;
180 m->execute_handler = NULL;
181 m->empty_msg = empty_msg;
182 m->cwd = strdup(flist_get_dir(view));
183 m->state = &menu_state;
184 m->initialized = 1;
185 }
186
187 void
menus_reset_data(menu_data_t * m)188 menus_reset_data(menu_data_t *m)
189 {
190 if(!m->initialized)
191 {
192 return;
193 }
194
195 /* On releasing of non-empty stashable menu, but not the stash. */
196 if(m->stashable && m->len > 0 && m != &menu_data_stash)
197 {
198 /* Release previously stashed menu, if any. */
199 if(menu_data_stash.initialized)
200 {
201 menu_data_stash.state = NULL;
202 menus_reset_data(&menu_data_stash);
203 }
204
205 menu_data_stash = *m;
206 m->initialized = 0;
207 reset_menu_state(m->state);
208 return;
209 }
210
211 /* Menu elements don't always have data associated with them, but len isn't
212 * zero. That's why we need this check. */
213 if(m->data != NULL)
214 {
215 free_string_array(m->data, m->len);
216 m->data = NULL;
217 }
218 free_string_array(m->items, m->len);
219 free(m->void_data);
220 free(m->title);
221 free(m->empty_msg);
222 free(m->cwd);
223 m->initialized = 0;
224
225 reset_menu_state(m->state);
226 }
227
228 /* Frees resources associated with menu mode. ms can be NULL. */
229 static void
reset_menu_state(menu_state_t * ms)230 reset_menu_state(menu_state_t *ms)
231 {
232 if(ms == NULL)
233 {
234 return;
235 }
236
237 update_string(&ms->regexp, NULL);
238 free(ms->matches);
239 ms->matches = NULL;
240
241 if(menu_state.d != NULL)
242 {
243 menu_state.d->state = NULL;
244 }
245 menu_state.d = NULL;
246
247 ms->view = NULL;
248 }
249
250 void
menus_set_pos(menu_state_t * ms,int pos)251 menus_set_pos(menu_state_t *ms, int pos)
252 {
253 menu_data_t *const m = ms->d;
254 int redraw;
255
256 pos = MIN(m->len - 1, MAX(0, pos));
257 if(pos < 0)
258 {
259 return;
260 }
261
262 normalize_top(ms);
263
264 redraw = 0;
265 if(pos > modmenu_last_line(m))
266 {
267 m->top = pos - (ms->win_rows - 2 - 1);
268 redraw = 1;
269 }
270 else if(pos < m->top)
271 {
272 m->top = pos;
273 redraw = 1;
274 }
275
276 if(cfg.scroll_off > 0)
277 {
278 int s = MIN(DIV_ROUND_UP(ms->win_rows - 2, 2), cfg.scroll_off);
279 if(pos - m->top < s && m->top > 0)
280 {
281 m->top -= s - (pos - m->top);
282 normalize_top(ms);
283 redraw = 1;
284 }
285 if(pos > modmenu_last_line(m) - s)
286 {
287 m->top += s - (modmenu_last_line(m) - pos);
288 normalize_top(ms);
289 redraw = 1;
290 }
291 }
292
293 ms->current = 1 + (pos - m->top);
294 m->pos = pos;
295
296 if(redraw)
297 {
298 menus_partial_redraw(ms);
299 }
300 else
301 {
302 draw_menu_item(ms, m->pos, ms->current, 0);
303 }
304 checked_wmove(menu_win, ms->current, 2);
305
306 show_position_in_menu(m);
307 }
308
309 /* Displays current menu position on a ruler. */
310 static void
show_position_in_menu(const menu_data_t * m)311 show_position_in_menu(const menu_data_t *m)
312 {
313 char pos_buf[32];
314 snprintf(pos_buf, sizeof(pos_buf), " %d-%d ", m->pos + 1, m->len);
315
316 ui_ruler_set(pos_buf);
317 }
318
319 void
menus_full_redraw(menu_state_t * m)320 menus_full_redraw(menu_state_t *m)
321 {
322 if(resize_for_menu_like() != 0)
323 {
324 return;
325 }
326
327 m->win_rows = getmaxy(menu_win);
328
329 menus_partial_redraw(m);
330 menus_set_pos(m, m->d->pos);
331 ui_refresh_win(menu_win);
332 }
333
334 int
menus_goto_file(menu_data_t * m,view_t * view,const char spec[],int try_open)335 menus_goto_file(menu_data_t *m, view_t *view, const char spec[], int try_open)
336 {
337 char *path_buf;
338 int line_num;
339
340 path_buf = parse_file_spec(spec, &line_num, get_relative_path_base(m, view));
341 if(path_buf == NULL)
342 {
343 show_error_msg("Memory Error", "Unable to allocate enough memory");
344 return 1;
345 }
346
347 if(!path_exists(path_buf, NODEREF))
348 {
349 show_error_msgf("Missing file", "File \"%s\" doesn't exist", path_buf);
350 free(path_buf);
351 return 1;
352 }
353
354 if(try_open)
355 {
356 open_selected_file(path_buf, line_num);
357 }
358 else
359 {
360 navigate_to_selected_file(view, path_buf);
361 }
362
363 free(path_buf);
364 return 0;
365 }
366
367 /* Opens file specified by its path on the given line number. */
368 static void
open_selected_file(const char path[],int line_num)369 open_selected_file(const char path[], int line_num)
370 {
371 if(os_access(path, R_OK) == 0)
372 {
373 (void)vim_view_file(path, line_num, -1, 1);
374 }
375 else
376 {
377 show_error_msgf("Can't read file", "File \"%s\" is not readable", path);
378 }
379 }
380
381 /* Navigates the view to a given dir/file combination specified by the path. */
382 static void
navigate_to_selected_file(view_t * view,const char path[])383 navigate_to_selected_file(view_t *view, const char path[])
384 {
385 char name[NAME_MAX + 1];
386 char *dir = strdup(path);
387 char *const last_slash = find_slashr(dir);
388
389 if(last_slash == NULL)
390 {
391 copy_str(name, sizeof(name), dir);
392 }
393 else
394 {
395 *last_slash = '\0';
396 copy_str(name, sizeof(name), last_slash + 1);
397 }
398
399 if(change_directory(view, dir) >= 0)
400 {
401 ui_sb_quick_msgf("%s", "Finding the correct directory...");
402
403 load_dir_list(view, 0);
404
405 (void)fpos_ensure_selected(view, name);
406 }
407 else
408 {
409 show_error_msgf("Invalid path", "Cannot change dir to \"%s\"", dir);
410 }
411
412 free(dir);
413 }
414
415 void
menus_goto_dir(view_t * view,const char path[])416 menus_goto_dir(view_t *view, const char path[])
417 {
418 if(!cfg.auto_ch_pos)
419 {
420 flist_hist_clear(view);
421 curr_stats.ch_pos = 0;
422 }
423 navigate_to(view, path);
424 if(!cfg.auto_ch_pos)
425 {
426 curr_stats.ch_pos = 1;
427 }
428 }
429
430 void
menus_partial_redraw(menu_state_t * m)431 menus_partial_redraw(menu_state_t *m)
432 {
433 int i, pos;
434 const int y = getmaxy(menu_win);
435
436 normalize_top(m);
437
438 werase(menu_win);
439 draw_menu_frame(m);
440
441 for(i = 0, pos = m->d->top; i < y - 2 && pos < m->d->len; ++i, ++pos)
442 {
443 draw_menu_item(m, pos, i + 1, 0);
444 }
445 }
446
447 /* Draws single menu item at position specified by line argument. Non-zero
448 * clear argument suppresses drawing current items in different color. */
449 static void
draw_menu_item(menu_state_t * ms,int pos,int line,int clear)450 draw_menu_item(menu_state_t *ms, int pos, int line, int clear)
451 {
452 menu_data_t *const m = ms->d;
453 int i;
454 int off;
455 char *item_tail;
456 const int width = (curr_stats.load_stage == 0) ? 100 : getmaxx(menu_win) - 2;
457
458 /* Calculate color for the line. */
459 col_attr_t col = cfg.cs.color[WIN_COLOR];
460 if(cfg.hl_search && ms->search_highlight &&
461 ms->matches != NULL && ms->matches[pos][0] >= 0)
462 {
463 cs_mix_colors(&col, &cfg.cs.color[SELECTED_COLOR]);
464 }
465 if(!clear && pos == m->pos)
466 {
467 cs_mix_colors(&col, &cfg.cs.color[CURR_LINE_COLOR]);
468 }
469 int color_pair = colmgr_get_pair(col.fg, col.bg);
470
471 /* Calculate offset of m->hor_pos's character in item text. */
472 off = 0;
473 i = m->hor_pos;
474 while(i-- > 0 && m->items[pos][off] != '\0')
475 {
476 off += utf8_chrw(m->items[pos] + off);
477 }
478
479 item_tail = strdup(m->items[pos] + off);
480 replace_char(item_tail, '\t', ' ');
481
482 ui_set_attr(menu_win, &col, color_pair);
483
484 /* Clear the area. */
485 checked_wmove(menu_win, line, 1);
486 if(curr_stats.load_stage > 0)
487 {
488 wprintw(menu_win, "%*s", width, "");
489 }
490
491 /* Truncate the item to fit the screen if needed. */
492 if(utf8_strsw(item_tail) > (size_t)(width - 2))
493 {
494 char *ellipsed = right_ellipsis(item_tail, width - 2, curr_stats.ellipsis);
495 free(item_tail);
496 item_tail = ellipsed;
497 }
498 else
499 {
500 const size_t len = utf8_nstrsnlen(item_tail, width - 2 + 1);
501 item_tail[len] = '\0';
502 }
503
504 checked_wmove(menu_win, line, 2);
505 wprint(menu_win, item_tail);
506
507 if(ms->search_highlight && ms->matches != NULL && ms->matches[pos][0] >= 0)
508 {
509 cchar_t cch;
510 setcchar(&cch, L" ", col.attr, color_pair, NULL);
511 draw_search_match(item_tail, ms->matches[pos][0] - m->hor_pos,
512 ms->matches[pos][1] - m->hor_pos, line, width, &cch);
513 }
514
515 free(item_tail);
516 }
517
518 /* Draws search match highlight on the element. */
519 static void
draw_search_match(char str[],int start,int end,int line,int width,const cchar_t * attrs)520 draw_search_match(char str[], int start, int end, int line, int width,
521 const cchar_t *attrs)
522 {
523 const int len = strlen(str);
524
525 if(end <= 0)
526 {
527 /* Match is completely at the left. */
528
529 checked_wmove(menu_win, line, 2);
530 wprinta(menu_win, "<<<", attrs, A_REVERSE);
531 }
532 else if(start >= len)
533 {
534 /* Match is completely at the right. */
535
536 checked_wmove(menu_win, line, width - 3);
537 wprinta(menu_win, ">>>", attrs, A_REVERSE);
538 }
539 else
540 {
541 /* Match is at least partially visible. */
542
543 char c;
544 int match_start;
545
546 if(start < 0)
547 {
548 start = 0;
549 }
550 if(end < len)
551 {
552 str[end] = '\0';
553 }
554
555 /* Calculate number of screen characters before the match. */
556 c = str[start];
557 str[start] = '\0';
558 match_start = utf8_strsw(str);
559 str[start] = c;
560
561 checked_wmove(menu_win, line, 2 + match_start);
562 wprinta(menu_win, str + start, attrs, A_REVERSE | A_UNDERLINE);
563 }
564 }
565
566 /* Ensures that value of m->top lies in a correct range. */
567 static void
normalize_top(menu_state_t * m)568 normalize_top(menu_state_t *m)
569 {
570 m->d->top = MAX(0, MIN(m->d->len - (m->win_rows - 2), m->d->top));
571 }
572
573 /* Draws box and title of the menu. */
574 static void
draw_menu_frame(const menu_state_t * m)575 draw_menu_frame(const menu_state_t *m)
576 {
577 const size_t title_len = getmaxx(menu_win) - 2*4;
578 const char *const suffix = menu_and_view_are_in_sync(m->d, m->view)
579 ? ""
580 : replace_home_part(m->d->cwd);
581 const char *const at = (suffix[0] == '\0' ? "" : " @ ");
582 char *const title = format_str("%s%s%s", m->d->title, at, suffix);
583 char *const ellipsed = right_ellipsis(title, title_len, curr_stats.ellipsis);
584 free(title);
585
586 ui_set_attr(menu_win, &cfg.cs.color[WIN_COLOR], cfg.cs.pair[WIN_COLOR]);
587
588 box(menu_win, 0, 0);
589 wattron(menu_win, A_BOLD);
590 checked_wmove(menu_win, 0, 3);
591 wprint(menu_win, " ");
592 wprint(menu_win, ellipsed);
593 wprint(menu_win, " ");
594 wattroff(menu_win, A_BOLD);
595
596 free(ellipsed);
597 }
598
599 /* Implements process_cmd_output() callback that loads lines to a menu. */
600 static void
output_handler(const char line[],void * arg)601 output_handler(const char line[], void *arg)
602 {
603 menu_data_t *const m = arg;
604 char *expanded_line;
605
606 m->items = reallocarray(m->items, m->len + 1, sizeof(char *));
607 expanded_line = expand_tabulation_a(line, cfg.tab_stop);
608 if(expanded_line != NULL)
609 {
610 m->items[m->len++] = expanded_line;
611 }
612 }
613
614 /* Replaces *str with a copy of the with string extended by the suffix. *str
615 * can be NULL in which case it's treated as empty string, equal to the with
616 * (then function does nothing). Returns non-zero if memory allocation
617 * failed. */
618 static void
append_to_string(char ** str,const char suffix[])619 append_to_string(char **str, const char suffix[])
620 {
621 const char *const non_null_str = (*str == NULL) ? "" : *str;
622 char *const appended_str = format_str("%s%s", non_null_str, suffix);
623 if(appended_str != NULL)
624 {
625 free(*str);
626 *str = appended_str;
627 }
628 }
629
630 /* Clones the line replacing all occurrences of horizontal tabulation character
631 * with appropriate number of spaces. The tab_stops parameter shows how many
632 * character position are taken by one tabulation. Returns newly allocated
633 * string. */
634 static char *
expand_tabulation_a(const char line[],size_t tab_stops)635 expand_tabulation_a(const char line[], size_t tab_stops)
636 {
637 const size_t tab_count = chars_in_str(line, '\t');
638 const size_t extra_line_len = tab_count*tab_stops;
639 const size_t expanded_line_len = (strlen(line) - tab_count) + extra_line_len;
640 char *const expanded_line = malloc(expanded_line_len + 1);
641
642 if(expanded_line != NULL)
643 {
644 const char *const end = expand_tabulation(line, (size_t)-1, tab_stops,
645 expanded_line);
646 assert(*end == '\0' && "The line should be processed till the end");
647 (void)end;
648 }
649
650 return expanded_line;
651 }
652
653 int
menus_enter(menu_state_t * m,view_t * view)654 menus_enter(menu_state_t *m, view_t *view)
655 {
656 if(m->d->len < 1)
657 {
658 ui_sb_msg(m->d->empty_msg);
659 menus_reset_data(m->d);
660 return 1;
661 }
662
663 init_menu_state(m, view);
664
665 ui_setup_for_menu_like();
666 term_title_update(m->d->title);
667 menus_partial_redraw(m);
668 menus_set_pos(m, m->d->pos);
669 modmenu_enter(m->d, view);
670 return 0;
671 }
672
673 /* Initializes menu state structure with default/initial value. */
674 static void
init_menu_state(menu_state_t * ms,view_t * view)675 init_menu_state(menu_state_t *ms, view_t *view)
676 {
677 ms->current = 1;
678 ms->win_rows = getmaxy(menu_win);
679 ms->backward_search = 0;
680 ms->matching_entries = 0;
681 ms->search_highlight = 1;
682 ms->matches = NULL;
683 ms->regexp = NULL;
684 ms->search_repeat = 0;
685 ms->view = view;
686 }
687
688 char *
menus_get_targets(view_t * view)689 menus_get_targets(view_t *view)
690 {
691 if(view->selected_files > 0 ||
692 (view->pending_marking && flist_count_marked(view) > 0))
693 {
694 return ma_expand("%f", NULL, NULL, 1);
695 }
696
697 if(!flist_custom_active(view))
698 {
699 return strdup(".");
700 }
701
702 return (vifm_chdir(flist_get_dir(view)) == 0) ? strdup(".") : NULL;
703 }
704
705 int
menus_unstash(view_t * view)706 menus_unstash(view_t *view)
707 {
708 static menu_data_t menu_data_storage;
709
710 if(!menu_data_stash.initialized)
711 {
712 ui_sb_msg("No saved menu to display");
713 return 1;
714 }
715
716 menus_reset_data(&menu_data_storage);
717 menu_data_storage = menu_data_stash;
718 menu_data_stash.initialized = 0;
719 menu_state.d = &menu_data_storage;
720
721 return menus_enter(menu_data_storage.state, view);
722 }
723
724 KHandlerResponse
menus_def_khandler(view_t * view,menu_data_t * m,const wchar_t keys[])725 menus_def_khandler(view_t *view, menu_data_t *m, const wchar_t keys[])
726 {
727 if(wcscmp(keys, L"gf") == 0)
728 {
729 (void)menus_goto_file(m, curr_view, m->items[m->pos], 0);
730 return KHR_CLOSE_MENU;
731 }
732 else if(wcscmp(keys, L"e") == 0)
733 {
734 (void)menus_goto_file(m, curr_view, m->items[m->pos], 1);
735 return KHR_REFRESH_WINDOW;
736 }
737 else if(wcscmp(keys, L"c") == 0)
738 {
739 /* Insert just file name. */
740 int line_num;
741 const char *const rel_base = get_relative_path_base(m, view);
742 char *const path = parse_file_spec(m->items[m->pos], &line_num, rel_base);
743 if(path == NULL)
744 {
745 show_error_msg("Command insertion", "No valid filename found");
746 return KHR_REFRESH_WINDOW;
747 }
748 modmenu_morph_into_cline(CLS_COMMAND, path, 1);
749 free(path);
750 return KHR_MORPHED_MENU;
751 }
752
753 return KHR_UNHANDLED;
754 }
755
756 int
menus_to_custom_view(menu_state_t * m,view_t * view,int very)757 menus_to_custom_view(menu_state_t *m, view_t *view, int very)
758 {
759 int i;
760 char *current = NULL;
761 const char *const rel_base = get_relative_path_base(m->d, view);
762
763 flist_custom_start(view, m->d->title);
764
765 for(i = 0; i < m->d->len; ++i)
766 {
767 char *path;
768 int line_num;
769
770 /* Skip empty lines. */
771 if(skip_whitespace(m->d->items[i])[0] == '\0')
772 {
773 continue;
774 }
775
776 path = parse_file_spec(m->d->items[i], &line_num, rel_base);
777 if(path == NULL)
778 {
779 continue;
780 }
781
782 flist_custom_add(view, path);
783
784 /* Use either exact position or the next path. */
785 if(i == m->d->pos || (current == NULL && i > m->d->pos))
786 {
787 current = path;
788 continue;
789 }
790
791 free(path);
792 }
793
794 /* If current line and none of the lines below didn't contain valid path, try
795 * to use file above cursor position. */
796 if(current == NULL && view->custom.entry_count != 0)
797 {
798 char full_path[PATH_MAX + 1];
799 get_full_path_of(&view->custom.entries[view->custom.entry_count - 1],
800 sizeof(full_path), full_path);
801
802 current = strdup(full_path);
803 }
804
805 if(flist_custom_finish(view, very ? CV_VERY : CV_REGULAR, 0) != 0)
806 {
807 free(current);
808 return 1;
809 }
810
811 if(current != NULL)
812 {
813 flist_goto_by_path(view, current);
814 free(current);
815 }
816
817 return 0;
818 }
819
820 /* Gets base for relative paths navigated to from the menu. Returns the
821 * path. The purpose is to omit "././" or "..././..." in paths shown to a
822 * user. */
823 static const char *
get_relative_path_base(const menu_data_t * m,const view_t * view)824 get_relative_path_base(const menu_data_t *m, const view_t *view)
825 {
826 if(menu_and_view_are_in_sync(m, view))
827 {
828 return ".";
829 }
830 return m->cwd;
831 }
832
833 /* Checks whether menu working directory and current directory of the view are
834 * in sync. Returns non-zero if so, otherwise zero is returned. */
835 static int
menu_and_view_are_in_sync(const menu_data_t * m,const view_t * view)836 menu_and_view_are_in_sync(const menu_data_t *m, const view_t *view)
837 {
838 /* NULL check is for tests. */
839 return (view == NULL || paths_are_same(m->cwd, flist_get_dir(view)));
840 }
841
842 int
menus_capture(view_t * view,const char cmd[],int user_sh,menu_data_t * m,int custom_view,int very_custom_view)843 menus_capture(view_t *view, const char cmd[], int user_sh, menu_data_t *m,
844 int custom_view, int very_custom_view)
845 {
846 if(custom_view || very_custom_view)
847 {
848 rn_for_flist(view, cmd, m->title, very_custom_view, 0);
849 menus_reset_data(m);
850 return 0;
851 }
852
853 if(process_cmd_output("Loading menu", cmd, user_sh, 0, &output_handler,
854 m) != 0)
855 {
856 show_error_msgf("Trouble running command", "Unable to run: %s", cmd);
857 return 0;
858 }
859
860 if(ui_cancellation_requested())
861 {
862 append_to_string(&m->title, "(cancelled)");
863 append_to_string(&m->empty_msg, " (cancelled)");
864 }
865
866 return menus_enter(m->state, view);
867 }
868
869 void
menus_search_repeat(menu_state_t * m,int backward)870 menus_search_repeat(menu_state_t *m, int backward)
871 {
872 if(is_null_or_empty(m->regexp))
873 {
874 ui_sb_err("No search pattern set");
875 curr_stats.save_msg = 1;
876 return;
877 }
878
879 m->backward_search = backward;
880 (void)menus_search(NULL, m->d, 1);
881 ui_refresh_win(menu_win);
882
883 if(m->matching_entries > 0)
884 {
885 ui_sb_msgf("(%d of %d) %c%s", get_match_index(m), m->matching_entries,
886 backward ? '?' : '/', m->regexp);
887 }
888
889 curr_stats.save_msg = 1;
890 }
891
892 int
menus_search(const char pattern[],menu_data_t * m,int print_errors)893 menus_search(const char pattern[], menu_data_t *m, int print_errors)
894 {
895 menu_state_t *const ms = m->state;
896 const int do_search = (pattern != NULL || ms->matches == NULL);
897 int save = 0;
898 int i;
899
900 if(pattern != NULL)
901 {
902 replace_string(&ms->regexp, pattern);
903 }
904
905 if(do_search)
906 {
907 /* Reactivate match highlighting on search. */
908 ms->search_highlight = 1;
909 if(search_menu(ms, m->pos, print_errors) != 0)
910 {
911 menus_partial_redraw(ms);
912 menus_set_pos(ms, m->pos);
913 return -1;
914 }
915 menus_partial_redraw(ms);
916 }
917
918 for(i = 0; i < ms->search_repeat; ++i)
919 {
920 if(ms->backward_search)
921 {
922 save = search_menu_backwards(ms, m->pos - 1);
923 }
924 else
925 {
926 save = search_menu_forwards(ms, m->pos + 1);
927 }
928 }
929 return save;
930 }
931
932 /* Goes through all menu items and marks those that match search pattern.
933 * Returns non-zero on error. */
934 static int
search_menu(menu_state_t * ms,int start_pos,int print_errors)935 search_menu(menu_state_t *ms, int start_pos, int print_errors)
936 {
937 menu_data_t *const m = ms->d;
938 int cflags;
939 regex_t re;
940 int err;
941 int i;
942
943 if(ms->matches == NULL)
944 {
945 ms->matches = reallocarray(NULL, m->len, sizeof(*ms->matches));
946 }
947
948 memset(ms->matches, -1, 2*sizeof(**ms->matches)*m->len);
949 ms->matching_entries = 0;
950
951 if(ms->regexp[0] == '\0')
952 {
953 return 0;
954 }
955
956 cflags = get_regexp_cflags(ms->regexp);
957 err = regcomp(&re, ms->regexp, cflags);
958 if(err != 0)
959 {
960 if(print_errors)
961 {
962 ui_sb_errf("Regexp error: %s", get_regexp_error(err, &re));
963 }
964 regfree(&re);
965 return -1;
966 }
967
968 for(i = 0; i < m->len; ++i)
969 {
970 regmatch_t matches[1];
971 if(regexec(&re, m->items[i], 1, matches, 0) == 0)
972 {
973 ms->matches[i][0] = matches[0].rm_so;
974 ms->matches[i][1] = matches[0].rm_eo;
975
976 ++ms->matching_entries;
977 }
978 }
979 regfree(&re);
980 return 0;
981 }
982
983 /* Looks for next matching element in forward direction from current position.
984 * Returns new value for save_msg flag. */
985 static int
search_menu_forwards(menu_state_t * m,int start_pos)986 search_menu_forwards(menu_state_t *m, int start_pos)
987 {
988 int match_up = -1;
989 int match_down = -1;
990 int i;
991
992 for(i = 0; i < m->d->len; ++i)
993 {
994 if(m->matches[i][0] < 0)
995 {
996 continue;
997 }
998
999 if(match_up < 0 && i < start_pos)
1000 {
1001 match_up = i;
1002 }
1003 if(match_down < 0 && i >= start_pos)
1004 {
1005 match_down = i;
1006 }
1007 }
1008
1009 if(!cfg.wrap_scan && match_down <= -1)
1010 {
1011 ui_sb_errf("Search hit BOTTOM without match for: %s", m->regexp);
1012 return 1;
1013 }
1014
1015 return navigate_to_match(m, (match_down > -1) ? match_down : match_up);
1016 }
1017
1018 /* Looks for next matching element in backward direction from current position.
1019 * Returns new value for save_msg flag. */
1020 static int
search_menu_backwards(menu_state_t * m,int start_pos)1021 search_menu_backwards(menu_state_t *m, int start_pos)
1022 {
1023 int match_up = -1;
1024 int match_down = -1;
1025 int i;
1026
1027 for(i = m->d->len - 1; i > -1; --i)
1028 {
1029 if(m->matches[i][0] < 0)
1030 {
1031 continue;
1032 }
1033
1034 if(match_up < 0 && i <= start_pos)
1035 {
1036 match_up = i;
1037 }
1038 if(match_down < 0 && i > start_pos)
1039 {
1040 match_down = i;
1041 }
1042 }
1043
1044 if(!cfg.wrap_scan && match_up <= -1)
1045 {
1046 ui_sb_errf("Search hit TOP without match for: %s", m->regexp);
1047 return 1;
1048 }
1049
1050 return navigate_to_match(m, (match_up > -1) ? match_up : match_down);
1051 }
1052
1053 /* Tries to navigate to menu search match specified via pos argument. If pos is
1054 * negative, match wasn't found and the message is printed. Returns new value
1055 * for save_msg flag. */
1056 static int
navigate_to_match(menu_state_t * m,int pos)1057 navigate_to_match(menu_state_t *m, int pos)
1058 {
1059 if(pos > -1)
1060 {
1061 if(!m->search_highlight)
1062 {
1063 /* Might need to highlight other items, so redraw whole menu. */
1064 m->search_highlight = 1;
1065 m->d->pos = pos;
1066 menus_partial_redraw(m);
1067 }
1068 else
1069 {
1070 menus_erase_current(m);
1071 menus_set_pos(m, pos);
1072 }
1073 menus_search_print_msg(m);
1074 }
1075 else
1076 {
1077 menus_set_pos(m, m->d->pos);
1078 if(cfg.wrap_scan)
1079 {
1080 menus_search_print_msg(m);
1081 }
1082 }
1083 return 1;
1084 }
1085
1086 void
menus_search_print_msg(const menu_state_t * m)1087 menus_search_print_msg(const menu_state_t *m)
1088 {
1089 int cflags;
1090 regex_t re;
1091 int err;
1092
1093 /* Can be NULL after regex compilation failure. */
1094 if(m->regexp == NULL)
1095 {
1096 return;
1097 }
1098
1099 cflags = get_regexp_cflags(m->regexp);
1100 err = regcomp(&re, m->regexp, cflags);
1101
1102 if(err != 0)
1103 {
1104 ui_sb_errf("Regexp (%s) error: %s", m->regexp, get_regexp_error(err, &re));
1105 regfree(&re);
1106 return;
1107 }
1108
1109 regfree(&re);
1110
1111 if(m->matching_entries > 0)
1112 {
1113 ui_sb_msgf("%d of %d %s", get_match_index(m), m->matching_entries,
1114 (m->matching_entries == 1) ? "match" : "matches");
1115 }
1116 else
1117 {
1118 ui_sb_errf("No matches for: %s", m->regexp);
1119 }
1120 }
1121
1122 /* Calculates the index of the current match from the list of matches. Returns
1123 * the index. */
1124 static int
get_match_index(const menu_state_t * m)1125 get_match_index(const menu_state_t *m)
1126 {
1127 int n, i;
1128
1129 n = (m->matches[0][0] >= 0 ? 1 : 0);
1130 i = 0;
1131 while(i++ < m->d->pos)
1132 {
1133 if(m->matches[i][0] >= 0)
1134 {
1135 ++n;
1136 }
1137 }
1138
1139 return n;
1140 }
1141
1142 void
menus_search_reset_hilight(menu_state_t * m)1143 menus_search_reset_hilight(menu_state_t *m)
1144 {
1145 m->search_highlight = 0;
1146 menus_full_redraw(m);
1147 }
1148
1149 int
menus_search_matched(menu_state_t * m)1150 menus_search_matched(menu_state_t *m)
1151 {
1152 return m->matching_entries;
1153 }
1154
1155 void
menus_search_reset(menu_state_t * m,int backward,int new_repeat_count)1156 menus_search_reset(menu_state_t *m, int backward, int new_repeat_count)
1157 {
1158 m->search_repeat = new_repeat_count;
1159 m->backward_search = backward;
1160 update_string(&m->regexp, NULL);
1161 }
1162
1163 void
menus_replace_data(menu_data_t * m)1164 menus_replace_data(menu_data_t *m)
1165 {
1166 menu_state.current = 1;
1167 menu_state.matching_entries = 0;
1168 free(menu_state.matches);
1169 menu_state.matches = NULL;
1170
1171 if(menu_state.d != NULL)
1172 {
1173 menu_state.d->state = NULL;
1174 }
1175 menu_state.d = m;
1176 m->state = &menu_state;
1177 }
1178
1179 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
1180 /* vim: set cinoptions+=t0 filetype=c : */
1181