Coverage Report

Created: 2024-02-11 06:06

/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-2023 Richard Russon <rich@flatcap.org>
7
 * Copyright (C) 2022 Gerrit Rüsing <gerrit@macclub-os.de>
8
 *
9
 * @copyright
10
 * This program is free software: you can redistribute it and/or modify it under
11
 * the terms of the GNU General Public License as published by the Free Software
12
 * Foundation, either version 2 of the License, or (at your option) any later
13
 * version.
14
 *
15
 * This program is distributed in the hope that it will be useful, but WITHOUT
16
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
18
 * details.
19
 *
20
 * You should have received a copy of the GNU General Public License along with
21
 * this program.  If not, see <http://www.gnu.org/licenses/>.
22
 */
23
24
/**
25
 * @page question_question Ask the user a question
26
 *
27
 * Ask the user a question
28
 */
29
30
#include "config.h"
31
#include <assert.h>
32
#include <ctype.h>
33
#include <langinfo.h>
34
#include <limits.h>
35
#include <stdbool.h>
36
#include <stdint.h>
37
#include <stdio.h>
38
#include <string.h>
39
#include "mutt/lib.h"
40
#include "config/lib.h"
41
#include "gui/lib.h"
42
#include "color/lib.h"
43
#include "key/lib.h"
44
#ifdef HAVE_SYS_PARAM_H
45
#include <sys/param.h> // IWYU pragma: keep
46
#endif
47
48
/**
49
 * mw_multi_choice - Offer the user a multiple choice question - @ingroup gui_mw
50
 * @param prompt  Message prompt
51
 * @param letters Allowable selection keys
52
 * @retval >=1 1-based user selection
53
 * @retval  -1 Selection aborted
54
 *
55
 * This function uses a message window.
56
 *
57
 * Ask the user a multiple-choice question, using shortcut letters, e.g.
58
 * `PGP (e)ncrypt, (s)ign, sign (a)s, (b)oth, s/(m)ime or (c)lear?`
59
 *
60
 * Colours:
61
 * - Question:  `color prompt`
62
 * - Shortcuts: `color options`
63
 */
64
int mw_multi_choice(const char *prompt, const char *letters)
65
0
{
66
0
  struct MuttWindow *win = msgwin_new(true);
67
0
  if (!win)
68
0
    return -1;
69
70
0
  int choice = 0;
71
72
0
  const struct AttrColor *ac_opts = NULL;
73
0
  if (simple_color_is_set(MT_COLOR_OPTIONS))
74
0
  {
75
0
    const struct AttrColor *ac_base = simple_color_get(MT_COLOR_NORMAL);
76
0
    ac_base = merged_color_overlay(ac_base, simple_color_get(MT_COLOR_PROMPT));
77
78
0
    ac_opts = simple_color_get(MT_COLOR_OPTIONS);
79
0
    ac_opts = merged_color_overlay(ac_base, ac_opts);
80
0
  }
81
82
0
  const struct AttrColor *ac_normal = simple_color_get(MT_COLOR_NORMAL);
83
0
  const struct AttrColor *ac_prompt = simple_color_get(MT_COLOR_PROMPT);
84
85
0
  if (ac_opts)
86
0
  {
87
0
    char *cur = NULL;
88
89
0
    while ((cur = strchr(prompt, '(')))
90
0
    {
91
      // write the part between prompt and cur using MT_COLOR_PROMPT
92
0
      msgwin_add_text_n(win, prompt, cur - prompt, ac_prompt);
93
94
0
      if (isalnum(cur[1]) && (cur[2] == ')'))
95
0
      {
96
        // we have a single letter within parentheses - MT_COLOR_OPTIONS
97
0
        msgwin_add_text_n(win, cur + 1, 1, ac_opts);
98
0
        prompt = cur + 3;
99
0
      }
100
0
      else
101
0
      {
102
        // we have a parenthesis followed by something else
103
0
        msgwin_add_text_n(win, cur, 1, ac_prompt);
104
0
        prompt = cur + 1;
105
0
      }
106
0
    }
107
0
  }
108
109
0
  msgwin_add_text(win, prompt, ac_prompt);
110
0
  msgwin_add_text(win, " ", ac_normal);
111
112
0
  msgcont_push_window(win);
113
0
  struct MuttWindow *old_focus = window_set_focus(win);
114
0
  window_redraw(win);
115
116
  // ---------------------------------------------------------------------------
117
  // Event Loop
118
0
  struct KeyEvent event = { 0, OP_NULL };
119
0
  while (true)
120
0
  {
121
0
    event = mutt_getch(GETCH_NO_FLAGS);
122
0
    mutt_debug(LL_DEBUG1, "mw_multi_choice: EVENT(%d,%d)\n", event.ch, event.op);
123
124
0
    if (event.op == OP_REPAINT)
125
0
      window_redraw(NULL);
126
127
0
    if ((event.op == OP_TIMEOUT) || (event.op == OP_REPAINT))
128
0
      continue;
129
130
0
    if ((event.op == OP_ABORT) || key_is_return(event.ch))
131
0
    {
132
0
      choice = -1;
133
0
      break;
134
0
    }
135
136
0
    char *p = strchr(letters, event.ch);
137
0
    if (p)
138
0
    {
139
0
      choice = p - letters + 1;
140
0
      break;
141
0
    }
142
143
0
    if ((event.ch > '0') && (event.ch <= '9'))
144
0
    {
145
0
      choice = event.ch - '0';
146
0
      if (choice <= mutt_str_len(letters))
147
0
        break;
148
0
    }
149
0
  }
150
  // ---------------------------------------------------------------------------
151
152
0
  win = msgcont_pop_window();
153
0
  window_set_focus(old_focus);
154
0
  mutt_window_free(&win);
155
156
0
  return choice;
157
0
}
158
159
/**
160
 * mw_yesorno - Ask the user a Yes/No question offering help - @ingroup gui_mw
161
 * @param prompt Prompt
162
 * @param def    Default answer, e.g. #MUTT_YES
163
 * @param cdef   Config definition for help
164
 * @retval enum #QuadOption, Selection made
165
 *
166
 * This function uses a message window.
167
 *
168
 * Ask the user a yes/no question, using shortcut letters, e.g.
169
 * `Quit NeoMutt? ([yes]/no):`
170
 *
171
 * This question can be answered using locale-dependent letters, e.g.
172
 * - English, `[+1yY]` or `[-0nN]`
173
 * - Serbian, `[+1yYdDДд]` or `[-0nNНн]`
174
 *
175
 * If a config variable (cdef) is given, then help is offered.
176
 * The options change to: `([yes]/no/?)`
177
 *
178
 * Pressing '?' will show the name and one-line description of the config variable.
179
 * Additionally, if `$help` is set, a link to the config's documentation is shown.
180
 */
181
static enum QuadOption mw_yesorno(const char *prompt, enum QuadOption def,
182
                                  struct ConfigDef *cdef)
183
0
{
184
0
  struct MuttWindow *win = msgwin_new(true);
185
0
  if (!win)
186
0
    return MUTT_ABORT;
187
188
0
  char *yes = N_("yes");
189
0
  char *no = N_("no");
190
0
  char *trans_yes = _(yes);
191
0
  char *trans_no = _(no);
192
193
0
  regex_t reyes = { 0 };
194
0
  regex_t reno = { 0 };
195
196
0
  bool reyes_ok = false;
197
0
  bool reno_ok = false;
198
199
#ifdef OpenBSD
200
  /* OpenBSD only supports locale C and UTF-8
201
   * so there is no suitable base system's locale identification
202
   * Remove this code immediately if this situation changes! */
203
  char rexyes[16] = "^[+1YyYy]";
204
  rexyes[6] = toupper(trans_yes[0]);
205
  rexyes[7] = tolower(trans_yes[0]);
206
207
  char rexno[16] = "^[-0NnNn]";
208
  rexno[6] = toupper(trans_no[0]);
209
  rexno[7] = tolower(trans_no[0]);
210
211
  if (REG_COMP(&reyes, rexyes, REG_NOSUB) == 0)
212
    reyes_ok = true;
213
214
  if (REG_COMP(&reno, rexno, REG_NOSUB) == 0)
215
    reno_ok = true;
216
217
#else
218
0
  char *expr = NULL;
219
0
  reyes_ok = (expr = nl_langinfo(YESEXPR)) && (expr[0] == '^') &&
220
0
             (REG_COMP(&reyes, expr, REG_NOSUB) == 0);
221
0
  reno_ok = (expr = nl_langinfo(NOEXPR)) && (expr[0] == '^') &&
222
0
            (REG_COMP(&reno, expr, REG_NOSUB) == 0);
223
0
#endif
224
225
0
  if ((yes != trans_yes) && (no != trans_no) && reyes_ok && reno_ok)
226
0
  {
227
    // If all parts of the translation succeeded...
228
0
    yes = trans_yes;
229
0
    no = trans_no;
230
0
  }
231
0
  else
232
0
  {
233
    // otherwise, fallback to English
234
0
    if (reyes_ok)
235
0
    {
236
0
      regfree(&reyes);
237
0
      reyes_ok = false;
238
0
    }
239
0
    if (reno_ok)
240
0
    {
241
0
      regfree(&reno);
242
0
      reno_ok = false;
243
0
    }
244
0
  }
245
246
0
  bool show_help_prompt = cdef;
247
248
0
  struct Buffer *text = buf_pool_get();
249
0
  buf_printf(text, "%s ([%s]/%s%s): ", prompt, (def == MUTT_YES) ? yes : no,
250
0
             (def == MUTT_YES) ? no : yes, show_help_prompt ? "/?" : "");
251
252
0
  msgwin_set_text(win, buf_string(text), MT_COLOR_PROMPT);
253
0
  msgcont_push_window(win);
254
0
  struct MuttWindow *old_focus = window_set_focus(win);
255
256
0
  struct KeyEvent event = { 0, OP_NULL };
257
0
  window_redraw(NULL);
258
0
  while (true)
259
0
  {
260
0
    event = mutt_getch(GETCH_NO_FLAGS);
261
0
    if ((event.op == OP_TIMEOUT) || (event.op == OP_REPAINT))
262
0
    {
263
0
      window_redraw(NULL);
264
0
      mutt_refresh();
265
0
      continue;
266
0
    }
267
268
0
    if (key_is_return(event.ch))
269
0
      break; // Do nothing, use default
270
271
0
    if (event.op == OP_ABORT)
272
0
    {
273
0
      def = MUTT_ABORT;
274
0
      break;
275
0
    }
276
277
0
    char answer[4] = { 0 };
278
0
    answer[0] = event.ch;
279
0
    if (reyes_ok ? (regexec(&reyes, answer, 0, 0, 0) == 0) : (tolower(event.ch) == 'y'))
280
0
    {
281
0
      def = MUTT_YES;
282
0
      break;
283
0
    }
284
0
    if (reno_ok ? (regexec(&reno, answer, 0, 0, 0) == 0) : (tolower(event.ch) == 'n'))
285
0
    {
286
0
      def = MUTT_NO;
287
0
      break;
288
0
    }
289
0
    if (show_help_prompt && (event.ch == '?'))
290
0
    {
291
0
      show_help_prompt = false;
292
0
      msgwin_clear_text(win);
293
0
      buf_printf(text, "$%s - %s\n", cdef->name, cdef->docs);
294
295
0
      char hyphen[128] = { 0 };
296
0
      mutt_str_hyphenate(hyphen, sizeof(hyphen), cdef->name);
297
0
      buf_add_printf(text, "https://neomutt.org/guide/reference#%s\n", hyphen);
298
299
0
      msgwin_add_text(win, buf_string(text), simple_color_get(MT_COLOR_NORMAL));
300
301
0
      buf_printf(text, "%s ([%s]/%s): ", prompt, (def == MUTT_YES) ? yes : no,
302
0
                 (def == MUTT_YES) ? no : yes);
303
0
      msgwin_add_text(win, buf_string(text), simple_color_get(MT_COLOR_PROMPT));
304
0
      msgwin_add_text(win, NULL, NULL);
305
306
0
      window_redraw(NULL);
307
0
      mutt_refresh();
308
0
    }
309
310
0
    mutt_beep(false);
311
0
  }
312
313
0
  win = msgcont_pop_window();
314
0
  window_set_focus(old_focus);
315
0
  mutt_window_free(&win);
316
317
0
  if (reyes_ok)
318
0
    regfree(&reyes);
319
0
  if (reno_ok)
320
0
    regfree(&reno);
321
322
0
  buf_pool_release(&text);
323
0
  return def;
324
0
}
325
326
/**
327
 * query_yesorno - Ask the user a Yes/No question
328
 * @param prompt Prompt
329
 * @param def Default answer, e.g. #MUTT_YES
330
 * @retval enum #QuadOption, Selection made
331
 *
332
 * Wrapper for mw_yesorno().
333
 */
334
enum QuadOption query_yesorno(const char *prompt, enum QuadOption def)
335
0
{
336
0
  return mw_yesorno(prompt, def, NULL);
337
0
}
338
339
/**
340
 * query_yesorno_help - Ask the user a Yes/No question offering help
341
 * @param prompt Prompt
342
 * @param def    Default answer, e.g. #MUTT_YES
343
 * @param sub    Config Subset
344
 * @param name   Name of controlling config variable
345
 * @retval enum #QuadOption, Selection made
346
 *
347
 * Wrapper for mw_yesorno().
348
 */
349
enum QuadOption query_yesorno_help(const char *prompt, enum QuadOption def,
350
                                   struct ConfigSubset *sub, const char *name)
351
0
{
352
0
  struct HashElem *he = cs_subset_create_inheritance(sub, name);
353
0
  struct HashElem *he_base = cs_get_base(he);
354
0
  assert(DTYPE(he_base->type) == DT_BOOL);
355
356
0
  intptr_t value = cs_subset_he_native_get(sub, he, NULL);
357
0
  assert(value != INT_MIN);
358
359
0
  struct ConfigDef *cdef = he_base->data;
360
0
  return mw_yesorno(prompt, def, cdef);
361
0
}
362
363
/**
364
 * query_quadoption - Ask the user a quad-question
365
 * @param prompt Message to show to the user
366
 * @param sub    Config Subset
367
 * @param name   Name of controlling config variable
368
 * @retval #QuadOption Result, e.g. #MUTT_NO
369
 *
370
 * If the config variable is set to 'yes' or 'no', the function returns immediately.
371
 * Otherwise, the job is delegated to mw_yesorno().
372
 */
373
enum QuadOption query_quadoption(const char *prompt, struct ConfigSubset *sub, const char *name)
374
0
{
375
0
  struct HashElem *he = cs_subset_create_inheritance(sub, name);
376
0
  struct HashElem *he_base = cs_get_base(he);
377
0
  assert(DTYPE(he_base->type) == DT_QUAD);
378
379
0
  intptr_t value = cs_subset_he_native_get(sub, he, NULL);
380
0
  assert(value != INT_MIN);
381
382
0
  if ((value == MUTT_YES) || (value == MUTT_NO))
383
0
    return value;
384
385
0
  struct ConfigDef *cdef = he_base->data;
386
0
  enum QuadOption def = (value == MUTT_ASKYES) ? MUTT_YES : MUTT_NO;
387
0
  return mw_yesorno(prompt, def, cdef);
388
0
}