1 /* vifm
2  * Copyright (C) 2001 Ken Steen.
3  * Copyright (C) 2015 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 "msg_dialog.h"
21 
22 #include <curses.h>
23 
24 #include <assert.h> /* assert() */
25 #include <limits.h> /* CHAR_MAX CHAR_MIN */
26 #include <stdarg.h> /* va_list va_start() va_end() vsnprintf() */
27 #include <stddef.h> /* NULL */
28 #include <string.h> /* strchr() strlen() */
29 
30 #include "../../cfg/config.h"
31 #include "../../engine/keys.h"
32 #include "../../engine/mode.h"
33 #include "../../modes/modes.h"
34 #include "../../ui/ui.h"
35 #include "../../utils/macros.h"
36 #include "../../utils/str.h"
37 #include "../../utils/utf8.h"
38 #include "../../event_loop.h"
39 #include "../../status.h"
40 #include "../wk.h"
41 
42 /* Kinds of dialogs. */
43 typedef enum
44 {
45 	D_ERROR,              /* Error message. */
46 	D_QUERY_WITHOUT_LIST, /* User query with all lines centered. */
47 	D_QUERY_WITH_LIST,    /* User query with left aligned list and one line
48 	                         centered at the top. */
49 }
50 Dialog;
51 
52 /* Kinds of dialog results. */
53 typedef enum
54 {
55 	R_OK,     /* Agreed. */
56 	R_CANCEL, /* Cancelled. */
57 	R_YES,    /* Confirmed. */
58 	R_NO,     /* Denied. */
59 	R_CUSTOM, /* One of user-specified keys. */
60 }
61 Result;
62 
63 static int def_handler(wchar_t key);
64 static void cmd_ctrl_c(key_info_t key_info, keys_info_t *keys_info);
65 static void cmd_ctrl_l(key_info_t key_info, keys_info_t *keys_info);
66 static void cmd_ctrl_m(key_info_t key_info, keys_info_t *keys_info);
67 static void cmd_n(key_info_t key_info, keys_info_t *keys_info);
68 static void cmd_y(key_info_t key_info, keys_info_t *keys_info);
69 static void handle_response(Result r);
70 static void leave(Result r);
71 static int prompt_error_msg_internalv(const char title[], const char format[],
72 		int prompt_skip, va_list pa);
73 static int prompt_error_msg_internal(const char title[], const char message[],
74 		int prompt_skip);
75 static void prompt_msg_internal(const char title[], const char message[],
76 		const response_variant variants[], int with_list);
77 static void enter(int result_mask);
78 static void redraw_error_msg(const char title_arg[], const char message_arg[],
79 		int prompt_skip, int lazy);
80 static const char * get_control_msg(Dialog msg_kind, int global_skip);
81 static const char * get_custom_control_msg(const response_variant responses[]);
82 static void draw_msg(const char title[], const char msg[],
83 		const char ctrl_msg[], int lines_to_center, int recommended_width);
84 static size_t measure_sub_lines(const char msg[], size_t *max_len);
85 static size_t determine_width(const char msg[]);
86 
87 /* List of builtin key bindings. */
88 static keys_add_info_t builtin_cmds[] = {
89 	{WK_C_c, {{&cmd_ctrl_c}, .descr = "cancel"}},
90 	{WK_C_l, {{&cmd_ctrl_l}, .descr = "redraw"}},
91 	{WK_C_m, {{&cmd_ctrl_m}, .descr = "agree to the query"}},
92 	{WK_ESC, {{&cmd_ctrl_c}, .descr = "cancel"}},
93 	{WK_n,   {{&cmd_n},      .descr = "deny the query"}},
94 	{WK_y,   {{&cmd_y},      .descr = "confirm the query"}},
95 };
96 
97 /* Main loop quit flag. */
98 static int quit;
99 /* Dialog result. */
100 static Result result;
101 /* Bit mask of R_* kinds of results that are allowed. */
102 static int accept_mask;
103 /* Type of active dialog message. */
104 static Dialog msg_kind;
105 
106 /* Possible responses for custom prompt message. */
107 static const response_variant *responses;
108 /* One of user-defined input keys. */
109 static char custom_result;
110 
111 void
init_msg_dialog_mode(void)112 init_msg_dialog_mode(void)
113 {
114 	int ret_code;
115 
116 	vle_keys_set_def_handler(MSG_MODE, def_handler);
117 
118 	ret_code = vle_keys_add(builtin_cmds, ARRAY_LEN(builtin_cmds), MSG_MODE);
119 	assert(ret_code == 0 && "Failed to register msg dialog keys.");
120 
121 	(void)ret_code;
122 }
123 
124 /* Handles all keys uncaught by shortcuts.  Returns zero on success and non-zero
125  * on error. */
126 static int
def_handler(wchar_t key)127 def_handler(wchar_t key)
128 {
129 	if(key < CHAR_MIN || key > CHAR_MAX)
130 	{
131 		return 0;
132 	}
133 
134 	const response_variant *response = responses;
135 	while(response != NULL && response->key != '\0')
136 	{
137 		if(response->key == (char)key)
138 		{
139 			custom_result = key;
140 			leave(R_CUSTOM);
141 			break;
142 		}
143 		++response;
144 	}
145 	return 0;
146 }
147 
148 /* Cancels the query. */
149 static void
cmd_ctrl_c(key_info_t key_info,keys_info_t * keys_info)150 cmd_ctrl_c(key_info_t key_info, keys_info_t *keys_info)
151 {
152 	handle_response(R_CANCEL);
153 }
154 
155 /* Redraws the screen. */
156 static void
cmd_ctrl_l(key_info_t key_info,keys_info_t * keys_info)157 cmd_ctrl_l(key_info_t key_info, keys_info_t *keys_info)
158 {
159 	stats_redraw_later();
160 }
161 
162 /* Agrees to the query. */
163 static void
cmd_ctrl_m(key_info_t key_info,keys_info_t * keys_info)164 cmd_ctrl_m(key_info_t key_info, keys_info_t *keys_info)
165 {
166 	handle_response(R_OK);
167 }
168 
169 /* Denies the query. */
170 static void
cmd_n(key_info_t key_info,keys_info_t * keys_info)171 cmd_n(key_info_t key_info, keys_info_t *keys_info)
172 {
173 	handle_response(R_NO);
174 }
175 
176 /* Confirms the query. */
177 static void
cmd_y(key_info_t key_info,keys_info_t * keys_info)178 cmd_y(key_info_t key_info, keys_info_t *keys_info)
179 {
180 	handle_response(R_YES);
181 }
182 
183 /* Processes users choice.  Leaves the mode if the result is found among the
184  * list of expected results. */
185 static void
handle_response(Result r)186 handle_response(Result r)
187 {
188 	/* Map result to corresponding input key to omit branching per handler. */
189 	static const char r_to_c[] = {
190 		[R_OK]     = '\r',
191 		[R_CANCEL] = NC_C_c,
192 		[R_YES]    = 'y',
193 		[R_NO]     = 'n',
194 	};
195 
196 	(void)def_handler(r_to_c[r]);
197 	/* Default handler might have already requested quitting. */
198 	if(!quit)
199 	{
200 		if(accept_mask & MASK(r))
201 		{
202 			leave(r);
203 		}
204 	}
205 }
206 
207 /* Leaves the mode with given result. */
208 static void
leave(Result r)209 leave(Result r)
210 {
211 	result = r;
212 	quit = 1;
213 }
214 
215 void
redraw_msg_dialog(int lazy)216 redraw_msg_dialog(int lazy)
217 {
218 	redraw_error_msg(NULL, NULL, 0, lazy);
219 }
220 
221 void
show_error_msg(const char title[],const char message[])222 show_error_msg(const char title[], const char message[])
223 {
224 	(void)prompt_error_msg_internal(title, message, 0);
225 }
226 
227 void
show_error_msgf(const char title[],const char format[],...)228 show_error_msgf(const char title[], const char format[], ...)
229 {
230 	va_list pa;
231 
232 	va_start(pa, format);
233 	(void)prompt_error_msg_internalv(title, format, 0, pa);
234 	va_end(pa);
235 }
236 
237 int
prompt_error_msg(const char title[],const char message[])238 prompt_error_msg(const char title[], const char message[])
239 {
240 	char portion[1024];
241 	do
242 	{
243 		copy_str(portion, sizeof(portion), message);
244 		message += strlen(portion);
245 		if(prompt_error_msg_internal(title, portion, 1))
246 		{
247 			return 1;
248 		}
249 	}
250 	while(*message != '\0');
251 	return 0;
252 }
253 
254 int
prompt_error_msgf(const char title[],const char format[],...)255 prompt_error_msgf(const char title[], const char format[], ...)
256 {
257 	int result;
258 	va_list pa;
259 
260 	va_start(pa, format);
261 	result = prompt_error_msg_internalv(title, format, 1, pa);
262 	va_end(pa);
263 
264 	return result;
265 }
266 
267 /* Just a varargs wrapper over prompt_error_msg_internal(). */
268 static int
prompt_error_msg_internalv(const char title[],const char format[],int prompt_skip,va_list pa)269 prompt_error_msg_internalv(const char title[], const char format[],
270 		int prompt_skip, va_list pa)
271 {
272 	char msg[2048];
273 	vsnprintf(msg, sizeof(msg), format, pa);
274 	return prompt_error_msg_internal(title, msg, prompt_skip);
275 }
276 
277 /* Internal function for displaying error messages to a user.  Automatically
278  * skips whitespace in front of the message and does nothing for empty messages
279  * (due to skipping whitespace-only are counted as empty). When the prompt_skip
280  * isn't zero, asks user about successive messages.  Returns non-zero if all
281  * successive messages should be skipped, otherwise zero is returned. */
282 static int
prompt_error_msg_internal(const char title[],const char message[],int prompt_skip)283 prompt_error_msg_internal(const char title[], const char message[],
284 		int prompt_skip)
285 {
286 	static int skip_until_started;
287 
288 	if(curr_stats.load_stage == 0)
289 		return 1;
290 	if(curr_stats.load_stage < 2 && skip_until_started)
291 		return 1;
292 
293 	message = skip_whitespace(message);
294 	if(*message == '\0')
295 	{
296 		return 0;
297 	}
298 
299 	msg_kind = D_ERROR;
300 
301 	redraw_error_msg(title, message, prompt_skip, 0);
302 
303 	enter(MASK(R_OK) | (prompt_skip ? MASK(R_CANCEL) : 0));
304 
305 	if(curr_stats.load_stage < 2)
306 		skip_until_started = (result == R_CANCEL);
307 
308 	modes_redraw();
309 
310 	return result == R_CANCEL;
311 }
312 
313 int
prompt_msg(const char title[],const char message[])314 prompt_msg(const char title[], const char message[])
315 {
316 	prompt_msg_internal(title, message, NULL, 0);
317 	return result == R_YES;
318 }
319 
320 char
prompt_msg_custom(const char title[],const char message[],const response_variant variants[])321 prompt_msg_custom(const char title[], const char message[],
322 		const response_variant variants[])
323 {
324 	assert(variants[0].key != '\0' && "Variants should have at least one item.");
325 	prompt_msg_internal(title, message, variants, 0);
326 	return custom_result;
327 }
328 
329 /* Common implementation of prompt message.  The variants can be NULL. */
330 static void
prompt_msg_internal(const char title[],const char message[],const response_variant variants[],int with_list)331 prompt_msg_internal(const char title[], const char message[],
332 		const response_variant variants[], int with_list)
333 {
334 	responses = variants;
335 	msg_kind = (with_list ? D_QUERY_WITH_LIST : D_QUERY_WITHOUT_LIST);
336 
337 	redraw_error_msg(title, message, 0, 0);
338 
339 	enter(variants == NULL ? MASK(R_YES, R_NO) : 0);
340 
341 	modes_redraw();
342 }
343 
344 /* Enters the mode, which won't be left until one of expected results specified
345  * by the mask is picked by the user. */
346 static void
enter(int result_mask)347 enter(int result_mask)
348 {
349 	const int prev_use_input_bar = curr_stats.use_input_bar;
350 	const vle_mode_t prev_mode = vle_mode_get();
351 
352 	/* Message must be displayed to the user for him to close it.  It won't happen
353 	 * without user interaction as new event loop is out of scope of a mapping
354 	 * that started it. */
355 	stats_unsilence_ui();
356 
357 	accept_mask = result_mask;
358 	curr_stats.use_input_bar = 0;
359 	vle_mode_set(MSG_MODE, VMT_SECONDARY);
360 
361 	quit = 0;
362 	/* Avoid starting nested loop in tests. */
363 	if(curr_stats.load_stage > 0)
364 	{
365 		event_loop(&quit);
366 	}
367 
368 	vle_mode_set(prev_mode, VMT_SECONDARY);
369 	curr_stats.use_input_bar = prev_use_input_bar;
370 }
371 
372 /* Draws error message on the screen or redraws the last message when both
373  * title_arg and message_arg are NULL. */
374 static void
redraw_error_msg(const char title_arg[],const char message_arg[],int prompt_skip,int lazy)375 redraw_error_msg(const char title_arg[], const char message_arg[],
376 		int prompt_skip, int lazy)
377 {
378 	/* TODO: refactor this function redraw_error_msg() */
379 
380 	static const char *title;
381 	static const char *message;
382 	static int ctrl_c;
383 
384 	const char *ctrl_msg;
385 	const int lines_to_center = msg_kind == D_QUERY_WITHOUT_LIST ? INT_MAX
386 	                          : msg_kind == D_QUERY_WITH_LIST ? 1 : 0;
387 
388 	if(title_arg != NULL && message_arg != NULL)
389 	{
390 		title = title_arg;
391 		message = message_arg;
392 		ctrl_c = prompt_skip;
393 	}
394 
395 	if(title == NULL || message == NULL)
396 	{
397 		assert(title != NULL && "Asked to redraw dialog, but no title is set.");
398 		assert(message != NULL && "Asked to redraw dialog, but no message is set.");
399 		return;
400 	}
401 
402 	ctrl_msg = get_control_msg(msg_kind, ctrl_c);
403 	draw_msg(title, message, ctrl_msg, lines_to_center, 0);
404 
405 	if(lazy)
406 	{
407 		wnoutrefresh(error_win);
408 	}
409 	else
410 	{
411 		ui_refresh_win(error_win);
412 	}
413 }
414 
415 /* Picks control message (information on available actions) basing on dialog
416  * kind and previous actions.  Returns the message. */
417 static const char *
get_control_msg(Dialog msg_kind,int global_skip)418 get_control_msg(Dialog msg_kind, int global_skip)
419 {
420 	if(msg_kind == D_QUERY_WITHOUT_LIST || msg_kind == D_QUERY_WITH_LIST)
421 	{
422 		if(responses == NULL)
423 		{
424 			return "Enter [y]es or [n]o";
425 		}
426 
427 		return get_custom_control_msg(responses);
428 	}
429 	else if(global_skip)
430 	{
431 		return "Press Return to continue or "
432 		       "Ctrl-C to skip its future error messages";
433 	}
434 
435 	return "Press Return to continue";
436 }
437 
438 /* Formats dialog control message for custom set of responses.  Returns pointer
439  * to statically allocated buffer. */
440 static const char *
get_custom_control_msg(const response_variant responses[])441 get_custom_control_msg(const response_variant responses[])
442 {
443 	static char msg_buf[256];
444 
445 	const response_variant *response = responses;
446 	size_t len = 0U;
447 	msg_buf[0] = '\0';
448 	while(response != NULL && response->key != '\0')
449 	{
450 		if(response->descr[0] != '\0')
451 		{
452 			(void)sstrappend(msg_buf, &len, sizeof(msg_buf), response->descr);
453 			if(response[1].key != '\0')
454 			{
455 				(void)sstrappendch(msg_buf, &len, sizeof(msg_buf), '/');
456 			}
457 		}
458 
459 		++response;
460 	}
461 
462 	return msg_buf;
463 }
464 
465 void
draw_msgf(const char title[],const char ctrl_msg[],int recommended_width,const char format[],...)466 draw_msgf(const char title[], const char ctrl_msg[], int recommended_width,
467 		const char format[], ...)
468 {
469 	char msg[8192];
470 	va_list pa;
471 
472 	va_start(pa, format);
473 	vsnprintf(msg, sizeof(msg), format, pa);
474 	va_end(pa);
475 
476 	draw_msg(title, msg, ctrl_msg, 0, recommended_width);
477 	touch_all_windows();
478 	ui_refresh_win(error_win);
479 }
480 
481 /* Draws formatted message with lines_to_center top lines centered with
482  * specified title and control message on error_win. */
483 static void
draw_msg(const char title[],const char msg[],const char ctrl_msg[],int lines_to_center,int recommended_width)484 draw_msg(const char title[], const char msg[], const char ctrl_msg[],
485 		int lines_to_center, int recommended_width)
486 {
487 	enum { margin = 1 };
488 
489 	int sw, sh;
490 	int w, h;
491 	int max_h;
492 	int len;
493 	size_t ctrl_msg_n;
494 	size_t wctrl_msg;
495 	int first_line_x = 1;
496 	const int first_line_y = 2;
497 
498 	if(curr_stats.load_stage < 1)
499 	{
500 		return;
501 	}
502 
503 	curs_set(0);
504 
505 	getmaxyx(stdscr, sh, sw);
506 
507 	ctrl_msg_n = MAX(measure_sub_lines(ctrl_msg, &wctrl_msg), 1U);
508 
509 	max_h = sh - 2 - ctrl_msg_n + !cfg.display_statusline;
510 	h = max_h;
511 	/* The outermost condition is for VLA below (to calm static analyzers). */
512 	w = MAX(2 + 2*margin, MIN(sw - 2,
513 	        MAX(MAX(recommended_width, sw/3),
514 	            (int)MAX(wctrl_msg, determine_width(msg)) + 4)));
515 	wresize(error_win, h, w);
516 
517 	werase(error_win);
518 
519 	len = strlen(msg);
520 	if(len <= w - 2 && strchr(msg, '\n') == NULL)
521 	{
522 		first_line_x = (w - len)/2;
523 		h = 5 + ctrl_msg_n;
524 		wresize(error_win, h, w);
525 		mvwin(error_win, (sh - h)/2, (sw - w)/2);
526 		checked_wmove(error_win, first_line_y, first_line_x);
527 		wprint(error_win, msg);
528 	}
529 	else
530 	{
531 		int i = 0;
532 		int cy = first_line_y;
533 		while(i < len)
534 		{
535 			int j;
536 			char buf[w - 2 - 2*margin + 1];
537 			int cx;
538 
539 			copy_str(buf, sizeof(buf), msg + i);
540 
541 			for(j = 0; buf[j] != '\0'; j++)
542 				if(buf[j] == '\n')
543 					break;
544 
545 			if(buf[j] != '\0')
546 				i++;
547 			buf[j] = '\0';
548 			i += j;
549 
550 			if(buf[0] == '\0')
551 				continue;
552 
553 			if(cy >= max_h - (int)ctrl_msg_n - 3)
554 			{
555 				/* Skip trailing part of the message if it's too long, just print how
556 				 * many lines we're omitting. */
557 				size_t max_len;
558 				const int more_lines = 1U + measure_sub_lines(msg + i, &max_len);
559 				snprintf(buf, sizeof(buf), "<<%d more line%s not shown>>", more_lines,
560 						(more_lines == 1) ? "" : "s");
561 				/* Make sure this is the last iteration of the loop. */
562 				i = len;
563 			}
564 
565 			h = 1 + cy + 1 + ctrl_msg_n + 1;
566 			wresize(error_win, h, w);
567 			mvwin(error_win, (sh - h)/2, (sw - w)/2);
568 
569 			cx = lines_to_center-- > 0 ? (w - utf8_strsw(buf))/2 : (1 + margin);
570 			if(cy == first_line_y)
571 			{
572 				first_line_x = cx;
573 			}
574 			checked_wmove(error_win, cy++, cx);
575 			wprint(error_win, buf);
576 		}
577 	}
578 
579 	box(error_win, 0, 0);
580 	if(title[0] != '\0')
581 	{
582 		mvwprintw(error_win, 0, (w - strlen(title) - 2)/2, " %s ", title);
583 	}
584 
585 	/* Print control message line by line. */
586 	size_t i = ctrl_msg_n;
587 	while(1)
588 	{
589 		const size_t len = strcspn(ctrl_msg, "\n");
590 		mvwaddnstr(error_win, h - i - 1, MAX(0, (w - (int)len)/2), ctrl_msg, len);
591 
592 		if(--i == 0)
593 		{
594 			break;
595 		}
596 
597 		ctrl_msg = skip_char(ctrl_msg + len + 1U, '/');
598 	}
599 
600 	checked_wmove(error_win, first_line_y, first_line_x);
601 }
602 
603 /* Counts number of sub-lines (separated by new-line character in the msg.  Sets
604  * *max_len to the length of the longest sub-line.  Returns total number of
605  * sub-lines, which can be zero is msg is an empty line. */
606 static size_t
measure_sub_lines(const char msg[],size_t * max_len)607 measure_sub_lines(const char msg[], size_t *max_len)
608 {
609 	size_t nlines = 0U;
610 	*max_len = 0U;
611 	while(*msg != '\0')
612 	{
613 		const size_t len = strcspn(msg, "\n");
614 		if(len > *max_len)
615 		{
616 			*max_len = len;
617 		}
618 		++nlines;
619 		msg += len + (msg[len] == '\n' ? 1U : 0U);
620 	}
621 	return nlines;
622 }
623 
624 /* Determines maximum width of line in the message.  Returns the width. */
625 static size_t
determine_width(const char msg[])626 determine_width(const char msg[])
627 {
628 	size_t max_width = 0U;
629 
630 	while(*msg != '\0')
631 	{
632 		size_t width = 0U;
633 		while(*msg != '\n' && *msg != '\0')
634 		{
635 			++width;
636 			++msg;
637 		}
638 
639 		if(width > max_width)
640 		{
641 			max_width = width;
642 		}
643 
644 		if(*msg == '\n')
645 		{
646 			++msg;
647 		}
648 	}
649 
650 	return max_width;
651 }
652 
653 int
confirm_deletion(char * files[],int nfiles,int use_trash)654 confirm_deletion(char *files[], int nfiles, int use_trash)
655 {
656 	if(nfiles == 0)
657 	{
658 		return 0;
659 	}
660 
661 	curr_stats.confirmed = 0;
662 	if(!cfg_confirm_delete(use_trash))
663 	{
664 		return 1;
665 	}
666 
667 	const char *const title = use_trash ? "Deletion" : "Permanent deletion";
668 	char *msg;
669 
670 	/* Trailing space is needed to prevent line dropping. */
671 	if(nfiles == 1)
672 	{
673 		msg = format_str("Are you sure you want to delete \"%s\"?", files[0]);
674 	}
675 	else
676 	{
677 		msg = format_str("Are you sure you want to delete %d files?\n ", nfiles);
678 
679 		size_t msg_len = strlen(msg);
680 		int i;
681 		for(i = 0; i < nfiles; ++i)
682 		{
683 			if(strappend(&msg, &msg_len, "\n* ") != 0 ||
684 					strappend(&msg, &msg_len, files[i]) != 0)
685 			{
686 				break;
687 			}
688 		}
689 	}
690 
691 	prompt_msg_internal(title, msg, NULL, 1);
692 	free(msg);
693 
694 	if(result != R_YES)
695 	{
696 		return 0;
697 	}
698 
699 	curr_stats.confirmed = 1;
700 	return 1;
701 }
702 
703 void
show_errors_from_file(FILE * ef,const char title[])704 show_errors_from_file(FILE *ef, const char title[])
705 {
706 	char linebuf[160];
707 	char buf[sizeof(linebuf)*5];
708 
709 	if(ef == NULL)
710 		return;
711 
712 	buf[0] = '\0';
713 	while(fgets(linebuf, sizeof(linebuf), ef) == linebuf)
714 	{
715 		if(linebuf[0] == '\n')
716 			continue;
717 		if(strlen(buf) + strlen(linebuf) + 1 >= sizeof(buf))
718 		{
719 			int skip = (prompt_error_msg(title, buf) != 0);
720 			buf[0] = '\0';
721 			if(skip)
722 				break;
723 		}
724 		strcat(buf, linebuf);
725 	}
726 
727 	if(buf[0] != '\0')
728 	{
729 		show_error_msg(title, buf);
730 	}
731 
732 	fclose(ef);
733 }
734 
735 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
736 /* vim: set cinoptions+=t0 filetype=c : */
737