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