/src/neomutt/question/question.c
Line | Count | Source (jump to first uncovered line) |
1 | | /** |
2 | | * @file |
3 | | * Ask the user a question |
4 | | * |
5 | | * @authors |
6 | | * Copyright (C) 2021 Richard Russon <rich@flatcap.org> |
7 | | * |
8 | | * @copyright |
9 | | * This program is free software: you can redistribute it and/or modify it under |
10 | | * the terms of the GNU General Public License as published by the Free Software |
11 | | * Foundation, either version 2 of the License, or (at your option) any later |
12 | | * version. |
13 | | * |
14 | | * This program is distributed in the hope that it will be useful, but WITHOUT |
15 | | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
16 | | * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
17 | | * details. |
18 | | * |
19 | | * You should have received a copy of the GNU General Public License along with |
20 | | * this program. If not, see <http://www.gnu.org/licenses/>. |
21 | | */ |
22 | | |
23 | | /** |
24 | | * @page question_question Ask the user a question |
25 | | * |
26 | | * Ask the user a question |
27 | | */ |
28 | | |
29 | | #include "config.h" |
30 | | #include <ctype.h> |
31 | | #include <langinfo.h> |
32 | | #include <stdbool.h> |
33 | | #include <stdio.h> |
34 | | #include <string.h> |
35 | | #include "mutt/lib.h" |
36 | | #include "config/lib.h" |
37 | | #include "gui/lib.h" |
38 | | #include "color/lib.h" |
39 | | #include "globals.h" |
40 | | #include "keymap.h" |
41 | | #include "mutt_logging.h" |
42 | | #include "opcodes.h" |
43 | | #ifdef HAVE_SYS_PARAM_H |
44 | | #include <sys/param.h> |
45 | | #endif |
46 | | |
47 | | /** |
48 | | * mutt_multi_choice - Offer the user a multiple choice question |
49 | | * @param prompt Message prompt |
50 | | * @param letters Allowable selection keys |
51 | | * @retval >=1 1-based user selection |
52 | | * @retval -1 Selection aborted |
53 | | */ |
54 | | int mutt_multi_choice(const char *prompt, const char *letters) |
55 | 0 | { |
56 | 0 | struct MuttWindow *win = msgwin_get_window(); |
57 | 0 | if (!win) |
58 | 0 | return -1; |
59 | | |
60 | 0 | struct KeyEvent ch = { OP_NULL, OP_NULL }; |
61 | 0 | int choice; |
62 | 0 | bool redraw = true; |
63 | 0 | int prompt_lines = 1; |
64 | |
|
65 | 0 | struct AttrColor *ac_opts = NULL; |
66 | 0 | if (simple_color_is_set(MT_COLOR_OPTIONS)) |
67 | 0 | { |
68 | 0 | struct AttrColor *ac_base = simple_color_get(MT_COLOR_NORMAL); |
69 | 0 | ac_base = merged_color_overlay(ac_base, simple_color_get(MT_COLOR_PROMPT)); |
70 | |
|
71 | 0 | ac_opts = simple_color_get(MT_COLOR_OPTIONS); |
72 | 0 | ac_opts = merged_color_overlay(ac_base, ac_opts); |
73 | 0 | } |
74 | |
|
75 | 0 | struct MuttWindow *old_focus = window_set_focus(win); |
76 | 0 | enum MuttCursorState cursor = mutt_curses_set_cursor(MUTT_CURSOR_VISIBLE); |
77 | 0 | window_redraw(NULL); |
78 | 0 | while (true) |
79 | 0 | { |
80 | 0 | if (redraw || SigWinch) |
81 | 0 | { |
82 | 0 | redraw = false; |
83 | 0 | if (SigWinch) |
84 | 0 | { |
85 | 0 | SigWinch = false; |
86 | 0 | mutt_resize_screen(); |
87 | 0 | clearok(stdscr, true); |
88 | 0 | window_redraw(NULL); |
89 | 0 | } |
90 | 0 | if (win->state.cols) |
91 | 0 | { |
92 | 0 | int width = mutt_strwidth(prompt) + 2; // + '?' + space |
93 | | /* If we're going to colour the options, |
94 | | * make an assumption about the modified prompt size. */ |
95 | 0 | if (ac_opts) |
96 | 0 | width -= 2 * mutt_str_len(letters); |
97 | |
|
98 | 0 | prompt_lines = (width + win->state.cols - 1) / win->state.cols; |
99 | 0 | prompt_lines = MAX(1, MIN(3, prompt_lines)); |
100 | 0 | } |
101 | 0 | if (prompt_lines != win->state.rows) |
102 | 0 | { |
103 | 0 | msgwin_set_height(prompt_lines); |
104 | 0 | window_redraw(NULL); |
105 | 0 | } |
106 | |
|
107 | 0 | mutt_window_move(win, 0, 0); |
108 | |
|
109 | 0 | if (ac_opts) |
110 | 0 | { |
111 | 0 | char *cur = NULL; |
112 | |
|
113 | 0 | while ((cur = strchr(prompt, '('))) |
114 | 0 | { |
115 | | // write the part between prompt and cur using MT_COLOR_PROMPT |
116 | 0 | mutt_curses_set_normal_backed_color_by_id(MT_COLOR_PROMPT); |
117 | 0 | mutt_window_addnstr(win, prompt, cur - prompt); |
118 | |
|
119 | 0 | if (isalnum(cur[1]) && (cur[2] == ')')) |
120 | 0 | { |
121 | | // we have a single letter within parentheses |
122 | 0 | mutt_curses_set_color(ac_opts); |
123 | 0 | mutt_window_addch(win, cur[1]); |
124 | 0 | prompt = cur + 3; |
125 | 0 | } |
126 | 0 | else |
127 | 0 | { |
128 | | // we have a parenthesis followed by something else |
129 | 0 | mutt_window_addch(win, cur[0]); |
130 | 0 | prompt = cur + 1; |
131 | 0 | } |
132 | 0 | } |
133 | 0 | } |
134 | |
|
135 | 0 | mutt_curses_set_normal_backed_color_by_id(MT_COLOR_PROMPT); |
136 | 0 | mutt_window_addstr(win, prompt); |
137 | 0 | mutt_curses_set_color_by_id(MT_COLOR_NORMAL); |
138 | |
|
139 | 0 | mutt_window_addch(win, ' '); |
140 | 0 | mutt_window_clrtoeol(win); |
141 | 0 | } |
142 | |
|
143 | 0 | mutt_refresh(); |
144 | | /* SigWinch is not processed unless timeout is set */ |
145 | 0 | ch = mutt_getch_timeout(30 * 1000); |
146 | 0 | if (ch.op == OP_TIMEOUT) |
147 | 0 | continue; |
148 | 0 | if (ch.op == OP_ABORT || key_is_return(ch.ch)) |
149 | 0 | { |
150 | 0 | choice = -1; |
151 | 0 | break; |
152 | 0 | } |
153 | 0 | else |
154 | 0 | { |
155 | 0 | char *p = strchr(letters, ch.ch); |
156 | 0 | if (p) |
157 | 0 | { |
158 | 0 | choice = p - letters + 1; |
159 | 0 | break; |
160 | 0 | } |
161 | 0 | else if ((ch.ch <= '9') && (ch.ch > '0')) |
162 | 0 | { |
163 | 0 | choice = ch.ch - '0'; |
164 | 0 | if (choice <= mutt_str_len(letters)) |
165 | 0 | break; |
166 | 0 | } |
167 | 0 | } |
168 | 0 | mutt_beep(false); |
169 | 0 | } |
170 | |
|
171 | 0 | if (win->state.rows == 1) |
172 | 0 | { |
173 | 0 | mutt_window_clearline(win, 0); |
174 | 0 | } |
175 | 0 | else |
176 | 0 | { |
177 | 0 | msgwin_set_height(1); |
178 | 0 | window_redraw(NULL); |
179 | 0 | } |
180 | |
|
181 | 0 | mutt_curses_set_color_by_id(MT_COLOR_NORMAL); |
182 | 0 | window_set_focus(old_focus); |
183 | 0 | mutt_curses_set_cursor(cursor); |
184 | 0 | mutt_refresh(); |
185 | 0 | return choice; |
186 | 0 | } |
187 | | |
188 | | /** |
189 | | * mutt_yesorno - Ask the user a Yes/No question |
190 | | * @param msg Prompt |
191 | | * @param def Default answer, #MUTT_YES or #MUTT_NO (see #QuadOption) |
192 | | * @retval enum #QuadOption, Selection made |
193 | | */ |
194 | | enum QuadOption mutt_yesorno(const char *msg, enum QuadOption def) |
195 | 0 | { |
196 | 0 | struct MuttWindow *win = msgwin_get_window(); |
197 | 0 | if (!win) |
198 | 0 | return MUTT_ABORT; |
199 | | |
200 | 0 | struct KeyEvent ch = { OP_NULL, OP_NULL }; |
201 | 0 | char *answer_string = NULL; |
202 | 0 | int answer_string_wid, msg_wid; |
203 | 0 | size_t trunc_msg_len; |
204 | 0 | bool redraw = true; |
205 | 0 | int prompt_lines = 1; |
206 | 0 | char answer[2] = { 0 }; |
207 | |
|
208 | 0 | char *yes = N_("yes"); |
209 | 0 | char *no = N_("no"); |
210 | 0 | char *trans_yes = _(yes); |
211 | 0 | char *trans_no = _(no); |
212 | |
|
213 | 0 | regex_t reyes = { 0 }; |
214 | 0 | regex_t reno = { 0 }; |
215 | |
|
216 | 0 | bool reyes_ok = false; |
217 | 0 | bool reno_ok = false; |
218 | |
|
219 | | #ifdef OpenBSD |
220 | | /* OpenBSD only supports locale C and UTF-8 |
221 | | * so there is no suitable base system's locale identification |
222 | | * Remove this code immediately if this situation changes! */ |
223 | | char rexyes[16] = "^[+1YyYy]"; |
224 | | rexyes[6] = toupper(trans_yes[0]); |
225 | | rexyes[7] = tolower(trans_yes[0]); |
226 | | |
227 | | char rexno[16] = "^[-0NnNn]"; |
228 | | rexno[6] = toupper(trans_no[0]); |
229 | | rexno[7] = tolower(trans_no[0]); |
230 | | |
231 | | if (REG_COMP(&reyes, rexyes, REG_NOSUB) == 0) |
232 | | reyes_ok = true; |
233 | | |
234 | | if (REG_COMP(&reno, rexno, REG_NOSUB) == 0) |
235 | | reno_ok = true; |
236 | | |
237 | | #else |
238 | 0 | char *expr = NULL; |
239 | 0 | reyes_ok = (expr = nl_langinfo(YESEXPR)) && (expr[0] == '^') && |
240 | 0 | (REG_COMP(&reyes, expr, REG_NOSUB) == 0); |
241 | 0 | reno_ok = (expr = nl_langinfo(NOEXPR)) && (expr[0] == '^') && |
242 | 0 | (REG_COMP(&reno, expr, REG_NOSUB) == 0); |
243 | 0 | #endif |
244 | |
|
245 | 0 | if ((yes != trans_yes) && (no != trans_no) && reyes_ok && reno_ok) |
246 | 0 | { |
247 | | // If all parts of the translation succeeded... |
248 | 0 | yes = trans_yes; |
249 | 0 | no = trans_no; |
250 | 0 | } |
251 | 0 | else |
252 | 0 | { |
253 | | // otherwise, fallback to English |
254 | 0 | if (reyes_ok) |
255 | 0 | { |
256 | 0 | regfree(&reyes); |
257 | 0 | reyes_ok = false; |
258 | 0 | } |
259 | 0 | if (reno_ok) |
260 | 0 | { |
261 | 0 | regfree(&reno); |
262 | 0 | reno_ok = false; |
263 | 0 | } |
264 | 0 | } |
265 | | |
266 | | /* In order to prevent the default answer to the question to wrapped |
267 | | * around the screen in the event the question is wider than the screen, |
268 | | * ensure there is enough room for the answer and truncate the question |
269 | | * to fit. */ |
270 | 0 | mutt_str_asprintf(&answer_string, " ([%s]/%s): ", (def == MUTT_YES) ? yes : no, |
271 | 0 | (def == MUTT_YES) ? no : yes); |
272 | 0 | answer_string_wid = mutt_strwidth(answer_string); |
273 | 0 | msg_wid = mutt_strwidth(msg); |
274 | |
|
275 | 0 | struct MuttWindow *old_focus = window_set_focus(win); |
276 | |
|
277 | 0 | enum MuttCursorState cursor = mutt_curses_set_cursor(MUTT_CURSOR_VISIBLE); |
278 | 0 | window_redraw(NULL); |
279 | 0 | while (true) |
280 | 0 | { |
281 | 0 | if (redraw || SigWinch) |
282 | 0 | { |
283 | 0 | redraw = false; |
284 | 0 | if (SigWinch) |
285 | 0 | { |
286 | 0 | SigWinch = false; |
287 | 0 | mutt_resize_screen(); |
288 | 0 | clearok(stdscr, true); |
289 | 0 | window_redraw(NULL); |
290 | 0 | } |
291 | 0 | if (win->state.cols) |
292 | 0 | { |
293 | 0 | prompt_lines = (msg_wid + answer_string_wid + win->state.cols - 1) / |
294 | 0 | win->state.cols; |
295 | 0 | prompt_lines = MAX(1, MIN(3, prompt_lines)); |
296 | 0 | } |
297 | 0 | if (prompt_lines != win->state.rows) |
298 | 0 | { |
299 | 0 | msgwin_set_height(prompt_lines); |
300 | 0 | window_redraw(NULL); |
301 | 0 | } |
302 | | |
303 | | /* maxlen here is sort of arbitrary, so pick a reasonable upper bound */ |
304 | 0 | trunc_msg_len = mutt_wstr_trunc(msg, |
305 | 0 | (size_t) 4 * prompt_lines * win->state.cols, |
306 | 0 | ((size_t) prompt_lines * win->state.cols) - answer_string_wid, |
307 | 0 | NULL); |
308 | |
|
309 | 0 | mutt_window_move(win, 0, 0); |
310 | 0 | mutt_curses_set_normal_backed_color_by_id(MT_COLOR_PROMPT); |
311 | 0 | mutt_window_addnstr(win, msg, trunc_msg_len); |
312 | 0 | mutt_window_addstr(win, answer_string); |
313 | 0 | mutt_curses_set_color_by_id(MT_COLOR_NORMAL); |
314 | 0 | mutt_window_clrtoeol(win); |
315 | 0 | } |
316 | |
|
317 | 0 | mutt_refresh(); |
318 | | /* SigWinch is not processed unless timeout is set */ |
319 | 0 | ch = mutt_getch_timeout(30 * 1000); |
320 | 0 | if (ch.op == OP_TIMEOUT) |
321 | 0 | continue; |
322 | 0 | if (key_is_return(ch.ch)) |
323 | 0 | break; |
324 | 0 | if (ch.op == OP_ABORT) |
325 | 0 | { |
326 | 0 | def = MUTT_ABORT; |
327 | 0 | break; |
328 | 0 | } |
329 | | |
330 | 0 | answer[0] = ch.ch; |
331 | 0 | if (reyes_ok ? (regexec(&reyes, answer, 0, 0, 0) == 0) : (tolower(ch.ch) == 'y')) |
332 | 0 | { |
333 | 0 | def = MUTT_YES; |
334 | 0 | break; |
335 | 0 | } |
336 | 0 | else if (reno_ok ? (regexec(&reno, answer, 0, 0, 0) == 0) : (tolower(ch.ch) == 'n')) |
337 | 0 | { |
338 | 0 | def = MUTT_NO; |
339 | 0 | break; |
340 | 0 | } |
341 | 0 | else |
342 | 0 | { |
343 | 0 | mutt_beep(false); |
344 | 0 | } |
345 | 0 | } |
346 | 0 | window_set_focus(old_focus); |
347 | 0 | mutt_curses_set_cursor(cursor); |
348 | |
|
349 | 0 | FREE(&answer_string); |
350 | |
|
351 | 0 | if (reyes_ok) |
352 | 0 | regfree(&reyes); |
353 | 0 | if (reno_ok) |
354 | 0 | regfree(&reno); |
355 | |
|
356 | 0 | if (win->state.rows == 1) |
357 | 0 | { |
358 | 0 | mutt_window_clearline(win, 0); |
359 | 0 | } |
360 | 0 | else |
361 | 0 | { |
362 | 0 | msgwin_set_height(1); |
363 | 0 | window_redraw(NULL); |
364 | 0 | } |
365 | |
|
366 | 0 | if (def == MUTT_ABORT) |
367 | 0 | { |
368 | | /* when the users cancels with ^G, clear the message stored with |
369 | | * mutt_message() so it isn't displayed when the screen is refreshed. */ |
370 | 0 | mutt_clear_error(); |
371 | 0 | } |
372 | 0 | else |
373 | 0 | { |
374 | 0 | mutt_window_addstr(win, (char *) ((def == MUTT_YES) ? yes : no)); |
375 | 0 | mutt_refresh(); |
376 | 0 | } |
377 | 0 | return def; |
378 | 0 | } |
379 | | |
380 | | /** |
381 | | * query_quadoption - Ask the user a quad-question |
382 | | * @param opt Option to use |
383 | | * @param prompt Message to show to the user |
384 | | * @retval #QuadOption Result, e.g. #MUTT_NO |
385 | | */ |
386 | | enum QuadOption query_quadoption(enum QuadOption opt, const char *prompt) |
387 | 0 | { |
388 | 0 | switch (opt) |
389 | 0 | { |
390 | 0 | case MUTT_YES: |
391 | 0 | case MUTT_NO: |
392 | 0 | return opt; |
393 | | |
394 | 0 | default: |
395 | 0 | opt = mutt_yesorno(prompt, (opt == MUTT_ASKYES) ? MUTT_YES : MUTT_NO); |
396 | 0 | msgwin_clear_text(); |
397 | 0 | return opt; |
398 | 0 | } |
399 | | |
400 | | /* not reached */ |
401 | 0 | } |