Coverage Report

Created: 2023-09-25 07:17

/src/neomutt/help.c
Line
Count
Source (jump to first uncovered line)
1
/**
2
 * @file
3
 * Generate the help-page and GUI display it
4
 *
5
 * @authors
6
 * Copyright (C) 1996-2000,2009 Michael R. Elkins <me@mutt.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 neo_help Generate the help-page and GUI display it
25
 *
26
 * Generate and help-page and GUI display it
27
 */
28
29
#include "config.h"
30
#include <stddef.h>
31
#include <limits.h>
32
#include <stdbool.h>
33
#include <stdio.h>
34
#include <string.h>
35
#include <wchar.h>
36
#include "mutt/lib.h"
37
#include "config/lib.h"
38
#include "core/lib.h"
39
#include "gui/lib.h"
40
#include "key/lib.h"
41
#include "menu/lib.h"
42
#include "pager/lib.h"
43
#include "hdrline.h"
44
#include "protos.h"
45
46
/**
47
 * help_lookup_function - Find a keybinding for an operation
48
 * @param op   Operation, e.g. OP_DELETE
49
 * @param menu Current Menu, e.g. #MENU_PAGER
50
 * @retval ptr  Key binding
51
 * @retval NULL No key binding found
52
 */
53
static const struct MenuFuncOp *help_lookup_function(int op, enum MenuType menu)
54
0
{
55
0
  if (menu != MENU_PAGER && (menu != MENU_GENERIC))
56
0
  {
57
    /* first look in the generic map for the function */
58
0
    for (int i = 0; OpGeneric[i].name; i++)
59
0
      if (OpGeneric[i].op == op)
60
0
        return &OpGeneric[i];
61
0
  }
62
63
0
  const struct MenuFuncOp *funcs = km_get_table(menu);
64
0
  if (funcs)
65
0
  {
66
0
    for (int i = 0; funcs[i].name; i++)
67
0
      if (funcs[i].op == op)
68
0
        return &funcs[i];
69
0
  }
70
71
0
  return NULL;
72
0
}
73
74
/**
75
 * print_macro - Print a macro string to a file
76
 * @param[in]  fp       File to write to
77
 * @param[in]  maxwidth Maximum width in screen columns
78
 * @param[out] macro    Macro string
79
 * @retval num Number of screen columns used
80
 *
81
 * The `macro` pointer is move past the string we've printed
82
 */
83
static int print_macro(FILE *fp, int maxwidth, const char **macro)
84
0
{
85
0
  int n = maxwidth;
86
0
  wchar_t wc = 0;
87
0
  size_t k;
88
0
  size_t len = mutt_str_len(*macro);
89
0
  mbstate_t mbstate1 = { 0 };
90
0
  mbstate_t mbstate2 = { 0 };
91
92
0
  for (; len && (k = mbrtowc(&wc, *macro, len, &mbstate1)); *macro += k, len -= k)
93
0
  {
94
0
    if ((k == ICONV_ILLEGAL_SEQ) || (k == ICONV_BUF_TOO_SMALL))
95
0
    {
96
0
      if (k == ICONV_ILLEGAL_SEQ)
97
0
        memset(&mbstate1, 0, sizeof(mbstate1));
98
0
      k = (k == ICONV_ILLEGAL_SEQ) ? 1 : len;
99
0
      wc = ReplacementChar;
100
0
    }
101
    /* glibc-2.1.3's wcwidth() returns 1 for unprintable chars! */
102
0
    const int w = wcwidth(wc);
103
0
    if (IsWPrint(wc) && (w >= 0))
104
0
    {
105
0
      if (w > n)
106
0
        break;
107
0
      n -= w;
108
0
      {
109
0
        char buf[MB_LEN_MAX * 2];
110
0
        size_t n1, n2;
111
0
        if (((n1 = wcrtomb(buf, wc, &mbstate2)) != ICONV_ILLEGAL_SEQ) &&
112
0
            ((n2 = wcrtomb(buf + n1, 0, &mbstate2)) != ICONV_ILLEGAL_SEQ))
113
0
        {
114
0
          fputs(buf, fp);
115
0
        }
116
0
      }
117
0
    }
118
0
    else if ((wc < 0x20) || (wc == 0x7f))
119
0
    {
120
0
      if (n < 2)
121
0
        break;
122
0
      n -= 2;
123
0
      if (wc == '\033') // Escape
124
0
        fprintf(fp, "\\e");
125
0
      else if (wc == '\n')
126
0
        fprintf(fp, "\\n");
127
0
      else if (wc == '\r')
128
0
        fprintf(fp, "\\r");
129
0
      else if (wc == '\t')
130
0
        fprintf(fp, "\\t");
131
0
      else
132
0
        fprintf(fp, "^%c", (char) ((wc + '@') & 0x7f));
133
0
    }
134
0
    else
135
0
    {
136
0
      if (n < 1)
137
0
        break;
138
0
      n -= 1;
139
0
      fprintf(fp, "?");
140
0
    }
141
0
  }
142
0
  return maxwidth - n;
143
0
}
144
145
/**
146
 * get_wrapped_width - Wrap a string at a sensible place
147
 * @param t   String to wrap
148
 * @param wid Maximum width
149
 * @retval num Break after this many characters
150
 *
151
 * If the string's too long, look for some whitespace to break at.
152
 */
153
static int get_wrapped_width(const char *t, size_t wid)
154
0
{
155
0
  wchar_t wc = 0;
156
0
  size_t k;
157
0
  size_t m, n;
158
0
  size_t len = mutt_str_len(t);
159
0
  const char *s = t;
160
0
  mbstate_t mbstate = { 0 };
161
162
0
  for (m = wid, n = 0; len && (k = mbrtowc(&wc, s, len, &mbstate)) && (n <= wid);
163
0
       s += k, len -= k)
164
0
  {
165
0
    if (*s == ' ')
166
0
      m = n;
167
0
    if ((k == ICONV_ILLEGAL_SEQ) || (k == ICONV_BUF_TOO_SMALL))
168
0
    {
169
0
      if (k == ICONV_ILLEGAL_SEQ)
170
0
        memset(&mbstate, 0, sizeof(mbstate));
171
0
      k = (k == ICONV_ILLEGAL_SEQ) ? 1 : len;
172
0
      wc = ReplacementChar;
173
0
    }
174
0
    if (!IsWPrint(wc))
175
0
      wc = '?';
176
0
    n += wcwidth(wc);
177
0
  }
178
0
  if (n > wid)
179
0
    n = m;
180
0
  else
181
0
    n = wid;
182
0
  return n;
183
0
}
184
185
/**
186
 * pad - Write some padding to a file
187
 * @param fp  File to write to
188
 * @param col Current screen column
189
 * @param i   Screen column to pad until
190
 * @retval num
191
 * - `i` - Padding was added
192
 * - `col` - Content was already wider than col
193
 */
194
static int pad(FILE *fp, int col, int i)
195
0
{
196
0
  if (col < i)
197
0
  {
198
0
    char fmt[32] = { 0 };
199
0
    snprintf(fmt, sizeof(fmt), "%%-%ds", i - col);
200
0
    fprintf(fp, fmt, "");
201
0
    return i;
202
0
  }
203
0
  fputc(' ', fp);
204
0
  return col + 1;
205
0
}
206
207
/**
208
 * format_line - Write a formatted line to a file
209
 * @param fp      File to write to
210
 * @param ismacro Layout mode, see below
211
 * @param t1      Text part 1
212
 * @param t2      Text part 2
213
 * @param t3      Text part 3
214
 * @param wraplen Width to wrap to
215
 *
216
 * Assemble the three columns of text.
217
 *
218
 * `ismacro` can be:
219
 * *  1 : Macro with a description
220
 * *  0 : Non-macro
221
 * * -1 : Macro with no description
222
 */
223
static void format_line(FILE *fp, int ismacro, const char *t1, const char *t2,
224
                        const char *t3, int wraplen)
225
0
{
226
0
  int col;
227
0
  int col_b;
228
229
0
  fputs(t1, fp);
230
231
  /* don't try to press string into one line with less than 40 characters. */
232
0
  bool split = (wraplen < 40);
233
0
  if (split)
234
0
  {
235
0
    col = 0;
236
0
    col_b = 1024;
237
0
    fputc('\n', fp);
238
0
  }
239
0
  else
240
0
  {
241
0
    const int col_a = (wraplen > 83) ? (wraplen - 32) >> 2 : 12;
242
0
    col_b = (wraplen > 49) ? (wraplen - 10) >> 1 : 19;
243
0
    col = pad(fp, mutt_strwidth(t1), col_a);
244
0
  }
245
246
0
  const char *const c_pager = pager_get_pager(NeoMutt->sub);
247
0
  if (ismacro > 0)
248
0
  {
249
0
    if (!c_pager)
250
0
      fputs("_\010", fp); // Ctrl-H (backspace)
251
0
    fputs("M ", fp);
252
0
    col += 2;
253
254
0
    if (!split)
255
0
    {
256
0
      col += print_macro(fp, col_b - col - 4, &t2);
257
0
      if (mutt_strwidth(t2) > col_b - col)
258
0
        t2 = "...";
259
0
    }
260
0
  }
261
262
0
  col += print_macro(fp, col_b - col - 1, &t2);
263
0
  if (split)
264
0
    fputc('\n', fp);
265
0
  else
266
0
    col = pad(fp, col, col_b);
267
268
0
  if (split)
269
0
  {
270
0
    print_macro(fp, 1024, &t3);
271
0
    fputc('\n', fp);
272
0
  }
273
0
  else
274
0
  {
275
0
    while (*t3)
276
0
    {
277
0
      int n = wraplen - col;
278
279
0
      if (ismacro >= 0)
280
0
      {
281
0
        SKIPWS(t3);
282
0
        n = get_wrapped_width(t3, n);
283
0
      }
284
285
0
      n = print_macro(fp, n, &t3);
286
287
0
      if (*t3)
288
0
      {
289
0
        if (c_pager)
290
0
        {
291
0
          fputc('\n', fp);
292
0
          n = 0;
293
0
        }
294
0
        else
295
0
        {
296
0
          n += col - wraplen;
297
0
          const bool c_markers = cs_subset_bool(NeoMutt->sub, "markers");
298
0
          if (c_markers)
299
0
            n++;
300
0
        }
301
0
        col = pad(fp, n, col_b);
302
0
      }
303
0
    }
304
0
  }
305
306
0
  fputc('\n', fp);
307
0
}
308
309
/**
310
 * dump_menu - Write all the key bindings to a file
311
 * @param fp      File to write to
312
 * @param menu    Current Menu, e.g. #MENU_PAGER
313
 * @param wraplen Width to wrap to
314
 */
315
static void dump_menu(FILE *fp, enum MenuType menu, int wraplen)
316
0
{
317
0
  struct Keymap *map = NULL;
318
0
  char buf[128] = { 0 };
319
320
0
  STAILQ_FOREACH(map, &Keymaps[menu], entries)
321
0
  {
322
0
    if (map->op != OP_NULL)
323
0
    {
324
0
      km_expand_key(buf, sizeof(buf), map);
325
326
0
      if (map->op == OP_MACRO)
327
0
      {
328
0
        if (map->desc)
329
0
          format_line(fp, 1, buf, map->macro, map->desc, wraplen);
330
0
        else
331
0
          format_line(fp, -1, buf, "macro", map->macro, wraplen);
332
0
      }
333
0
      else
334
0
      {
335
0
        const struct MenuFuncOp *funcs = help_lookup_function(map->op, menu);
336
0
        format_line(fp, 0, buf, funcs ? funcs->name : "UNKNOWN",
337
0
                    funcs ? _(opcodes_get_description(funcs->op)) :
338
0
                            _("ERROR: please report this bug"),
339
0
                    wraplen);
340
0
      }
341
0
    }
342
0
  }
343
0
}
344
345
/**
346
 * is_bound - Does a function have a keybinding?
347
 * @param km_list Keymap to examine
348
 * @param op      Operation, e.g. OP_DELETE
349
 * @retval true A key is bound to that operation
350
 */
351
static bool is_bound(struct KeymapList *km_list, int op)
352
0
{
353
0
  struct Keymap *map = NULL;
354
0
  STAILQ_FOREACH(map, km_list, entries)
355
0
  {
356
0
    if (map->op == op)
357
0
      return true;
358
0
  }
359
0
  return false;
360
0
}
361
362
/**
363
 * dump_unbound - Write out all the operations with no key bindings
364
 * @param fp      File to write to
365
 * @param funcs   All the bindings for the current menu
366
 * @param km_list First key map to consider
367
 * @param aux     Second key map to consider
368
 * @param wraplen Width to wrap to
369
 */
370
static void dump_unbound(FILE *fp, const struct MenuFuncOp *funcs,
371
                         struct KeymapList *km_list, struct KeymapList *aux, int wraplen)
372
0
{
373
0
  for (int i = 0; funcs[i].name; i++)
374
0
  {
375
0
    if (!is_bound(km_list, funcs[i].op) && (!aux || !is_bound(aux, funcs[i].op)))
376
0
      format_line(fp, 0, funcs[i].name, "", _(opcodes_get_description(funcs[i].op)), wraplen);
377
0
  }
378
0
}
379
380
/**
381
 * show_flag_if_present - Write out a message flag if exists
382
 * @param fp              File to write to
383
 * @param wraplen         Width to wrap to
384
 * @param table           Table containing the flag characters
385
 * @param index           Index of flag character int the table
386
 * @param description     Description of flag
387
 */
388
static void show_flag_if_present(FILE *fp, int wraplen, const struct MbTable *table,
389
                                 int index, char *description)
390
0
{
391
0
  const char *flag = mbtable_get_nth_wchar(table, index);
392
0
  if ((strlen(flag) < 1) || (*flag == ' '))
393
0
  {
394
0
    return;
395
0
  }
396
397
0
  format_line(fp, 0, flag, "", description, wraplen);
398
0
}
399
400
/**
401
 * dump_message_flags - Write out all the message flags
402
 * @param fp            File to write to
403
 * @param wraplen       Width to wrap to
404
 */
405
static void dump_message_flags(FILE *fp, int wraplen)
406
0
{
407
0
  const struct MbTable *c_flag_chars = cs_subset_mbtable(NeoMutt->sub, "flag_chars");
408
0
  const struct MbTable *c_crypt_chars = cs_subset_mbtable(NeoMutt->sub, "crypt_chars");
409
0
  const struct MbTable *c_to_chars = cs_subset_mbtable(NeoMutt->sub, "to_chars");
410
411
0
  format_line(fp, 0, "$flag_chars:", "", "", wraplen);
412
0
  show_flag_if_present(fp, wraplen, c_flag_chars, FLAG_CHAR_TAGGED, _("message is tagged"));
413
0
  show_flag_if_present(fp, wraplen, c_flag_chars, FLAG_CHAR_IMPORTANT,
414
0
                       _("message is flagged"));
415
0
  show_flag_if_present(fp, wraplen, c_flag_chars, FLAG_CHAR_DELETED, _("message is deleted"));
416
0
  show_flag_if_present(fp, wraplen, c_flag_chars, FLAG_CHAR_DELETED_ATTACH,
417
0
                       _("attachment is deleted"));
418
0
  show_flag_if_present(fp, wraplen, c_flag_chars, FLAG_CHAR_REPLIED,
419
0
                       _("message has been replied to"));
420
0
  show_flag_if_present(fp, wraplen, c_flag_chars, FLAG_CHAR_OLD, _("message has been read"));
421
0
  show_flag_if_present(fp, wraplen, c_flag_chars, FLAG_CHAR_NEW, _("message is new"));
422
0
  show_flag_if_present(fp, wraplen, c_flag_chars, FLAG_CHAR_OLD_THREAD,
423
0
                       _("thread has been read"));
424
0
  show_flag_if_present(fp, wraplen, c_flag_chars, FLAG_CHAR_NEW_THREAD,
425
0
                       _("thread has at least one new message"));
426
0
  show_flag_if_present(fp, wraplen, c_flag_chars, FLAG_CHAR_SEMPTY,
427
0
                       _("message has been read (%S expando)"));
428
0
  show_flag_if_present(fp, wraplen, c_flag_chars, FLAG_CHAR_ZEMPTY,
429
0
                       _("message has been read (%Z expando)"));
430
431
0
  format_line(fp, 0, "\n$crypt_chars:", "", "", wraplen);
432
0
  show_flag_if_present(fp, wraplen, c_crypt_chars, FLAG_CHAR_CRYPT_GOOD_SIGN,
433
0
                       _("message signed with a verified key"));
434
0
  show_flag_if_present(fp, wraplen, c_crypt_chars, FLAG_CHAR_CRYPT_ENCRYPTED,
435
0
                       _("message is PGP-encrypted"));
436
0
  show_flag_if_present(fp, wraplen, c_crypt_chars, FLAG_CHAR_CRYPT_SIGNED,
437
0
                       _("message is signed"));
438
0
  show_flag_if_present(fp, wraplen, c_crypt_chars, FLAG_CHAR_CRYPT_CONTAINS_KEY,
439
0
                       _("message contains a PGP key"));
440
0
  show_flag_if_present(fp, wraplen, c_crypt_chars, FLAG_CHAR_CRYPT_NO_CRYPTO,
441
0
                       _("message has no cryptography information"));
442
443
0
  format_line(fp, 0, "\n$to_chars:", "", "", wraplen);
444
0
  show_flag_if_present(fp, wraplen, c_to_chars, FLAG_CHAR_TO_NOT_IN_THE_LIST,
445
0
                       _("message is not To: you"));
446
0
  show_flag_if_present(fp, wraplen, c_to_chars, FLAG_CHAR_TO_UNIQUE,
447
0
                       _("message is To: you and only you"));
448
0
  show_flag_if_present(fp, wraplen, c_to_chars, FLAG_CHAR_TO_TO, _("message is To: you"));
449
0
  show_flag_if_present(fp, wraplen, c_to_chars, FLAG_CHAR_TO_CC, _("message is Cc: to you"));
450
0
  show_flag_if_present(fp, wraplen, c_to_chars, FLAG_CHAR_TO_ORIGINATOR,
451
0
                       _("message is From: you"));
452
0
  show_flag_if_present(fp, wraplen, c_to_chars, FLAG_CHAR_TO_SUBSCRIBED_LIST,
453
0
                       _("message is sent to a subscribed mailing list"));
454
0
  show_flag_if_present(fp, wraplen, c_to_chars, FLAG_CHAR_TO_REPLY_TO,
455
0
                       _("you are in the Reply-To: list"));
456
0
}
457
458
/**
459
 * mutt_help - Display the help menu
460
 * @param menu    Current Menu
461
 */
462
void mutt_help(enum MenuType menu)
463
0
{
464
0
  char banner[128] = { 0 };
465
0
  FILE *fp = NULL;
466
467
  /* We don't use the buffer pool because of the extended lifetime of t */
468
0
  struct Buffer t = buf_make(PATH_MAX);
469
0
  buf_mktemp(&t);
470
471
0
  const struct MenuFuncOp *funcs = km_get_table(menu);
472
0
  const char *desc = mutt_map_get_name(menu, MenuNames);
473
0
  if (!desc)
474
0
    desc = _("<UNKNOWN>");
475
476
0
  struct PagerData pdata = { 0 };
477
0
  struct PagerView pview = { &pdata };
478
479
0
  pview.mode = PAGER_MODE_HELP;
480
0
  pview.flags = MUTT_PAGER_RETWINCH | MUTT_PAGER_MARKER | MUTT_PAGER_NSKIP |
481
0
                MUTT_PAGER_NOWRAP | MUTT_PAGER_STRIPES;
482
483
0
  do
484
0
  {
485
0
    fp = mutt_file_fopen(buf_string(&t), "w");
486
0
    if (!fp)
487
0
    {
488
0
      mutt_perror("%s", buf_string(&t));
489
0
      goto cleanup;
490
0
    }
491
492
0
    const int wraplen = AllDialogsWindow->state.cols;
493
0
    dump_menu(fp, menu, wraplen);
494
0
    if ((menu != MENU_EDITOR) && (menu != MENU_PAGER) && (menu != MENU_GENERIC))
495
0
    {
496
0
      fprintf(fp, "\n%s\n\n", _("Generic bindings:"));
497
0
      dump_menu(fp, MENU_GENERIC, wraplen);
498
0
    }
499
500
0
    fprintf(fp, "\n%s\n\n", _("Unbound functions:"));
501
0
    if (funcs)
502
0
      dump_unbound(fp, funcs, &Keymaps[menu], NULL, wraplen);
503
0
    if ((menu != MENU_EDITOR) && (menu != MENU_PAGER) && (menu != MENU_GENERIC))
504
0
      dump_unbound(fp, OpGeneric, &Keymaps[MENU_GENERIC], &Keymaps[menu], wraplen);
505
506
0
    if (menu == MENU_INDEX)
507
0
    {
508
0
      fprintf(fp, "\n%s\n\n", _("Message flags:"));
509
0
      dump_message_flags(fp, wraplen);
510
0
    }
511
512
0
    mutt_file_fclose(&fp);
513
514
0
    snprintf(banner, sizeof(banner), _("Help for %s"), desc);
515
0
    pdata.fname = buf_string(&t);
516
0
    pview.banner = banner;
517
0
  } while (mutt_do_pager(&pview, NULL) == OP_REFORMAT_WINCH);
518
519
0
cleanup:
520
0
  buf_dealloc(&t);
521
0
}