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