• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

neomutt / neomutt / 23727705929

28 Mar 2026 01:30PM UTC coverage: 42.543% (+0.009%) from 42.534%
23727705929

push

github

flatcap
menu: fix half-down when data fits in one page

When the menu has fewer entries than the window size (page_len),
menu_move_view_relative() could get confused by view-scrolling logic
(particularly the menu_move_off clamp) and compute the same top/index
pair on repeated half-down presses, causing no movement.

Short-circuit to menu_move_selection() when all entries fit on screen,
since there's no view to scroll -- only the cursor needs to move.

12204 of 28686 relevant lines covered (42.54%)

2834.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.3
/expando/node_expando.c
1
/**
2
 * @file
3
 * Expando Node for an Expando
4
 *
5
 * @authors
6
 * Copyright (C) 2023-2024 Tóth János <gomba007@gmail.com>
7
 * Copyright (C) 2023-2025 Richard Russon <rich@flatcap.org>
8
 * Copyright (C) 2025 Thomas Klausner <wiz@gatalith.at>
9
 *
10
 * @copyright
11
 * This program is free software: you can redistribute it and/or modify it under
12
 * the terms of the GNU General Public License as published by the Free Software
13
 * Foundation, either version 2 of the License, or (at your option) any later
14
 * version.
15
 *
16
 * This program is distributed in the hope that it will be useful, but WITHOUT
17
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
19
 * details.
20
 *
21
 * You should have received a copy of the GNU General Public License along with
22
 * this program.  If not, see <http://www.gnu.org/licenses/>.
23
 */
24

25
/**
26
 * @page expando_node_expando Expando Node
27
 *
28
 * Expando Node for an Expando
29
 */
30

31
#include "config.h"
32
#include <limits.h>
33
#include <stdio.h>
34
#include "mutt/lib.h"
35
#include "gui/lib.h"
36
#include "node_expando.h"
37
#include "color/lib.h"
38
#include "definition.h"
39
#include "format.h"
40
#include "helpers.h"
41
#include "node.h"
42
#include "parse.h"
43
#include "render.h"
44

45
/**
46
 * node_expando_private_new - Create new Expando private data
47
 * @retval ptr New Expando private data
48
 */
49
struct NodeExpandoPrivate *node_expando_private_new(void)
1✔
50
{
51
  struct NodeExpandoPrivate *priv = MUTT_MEM_CALLOC(1, struct NodeExpandoPrivate);
1✔
52

53
  // NOTE(g0mb4): Expando definition should contain this
54
  priv->color = -1;
63,894✔
55

56
  return priv;
1✔
57
}
58

59
/**
60
 * node_expando_private_free - Free Expando private data - Implements ExpandoNode::ndata_free()
61
 * @param ptr Data to free
62
 */
63
void node_expando_private_free(void **ptr)
63,828✔
64
{
65
  if (!ptr || !*ptr)
63,828✔
66
    return;
67

68
  FREE(ptr);
63,827✔
69
}
70

71
/**
72
 * node_expando_new - Create a new Expando ExpandoNode
73
 * @param fmt    Formatting data
74
 * @param did    Domain ID
75
 * @param uid    Unique ID
76
 * @retval ptr New Expando ExpandoNode
77
 */
78
struct ExpandoNode *node_expando_new(struct ExpandoFormat *fmt, int did, int uid)
63,893✔
79
{
80
  struct ExpandoNode *node = node_new();
63,893✔
81

82
  node->type = ENT_EXPANDO;
63,893✔
83
  node->did = did;
63,893✔
84
  node->uid = uid;
63,893✔
85
  node->render = node_expando_render;
63,893✔
86

87
  node->format = fmt;
63,893✔
88

89
  node->ndata = node_expando_private_new();
63,893✔
90
  node->ndata_free = node_expando_private_free;
63,893✔
91

92
  return node;
63,893✔
93
}
94

95
/**
96
 * node_expando_set_color - Set the colour for an Expando
97
 * @param node Node to alter
98
 * @param cid  Colour
99
 */
100
void node_expando_set_color(const struct ExpandoNode *node, int cid)
4✔
101
{
102
  if (!node || (node->type != ENT_EXPANDO) || !node->ndata)
4✔
103
    return;
104

105
  struct NodeExpandoPrivate *priv = node->ndata;
106

107
  priv->color = cid;
3✔
108
}
109

110
/**
111
 * node_expando_set_has_tree - Set the has_tree flag for an Expando
112
 * @param node     Node to alter
113
 * @param has_tree Flag to set
114
 */
115
void node_expando_set_has_tree(const struct ExpandoNode *node, bool has_tree)
2✔
116
{
117
  if (!node || (node->type != ENT_EXPANDO) || !node->ndata)
2✔
118
    return;
119

120
  struct NodeExpandoPrivate *priv = node->ndata;
121

122
  priv->has_tree = has_tree;
1✔
123
}
124

125
/**
126
 * parse_format - Parse a format string
127
 * @param[in]  str          String to parse
128
 * @param[out] parsed_until First character after the parsed string
129
 * @param[out] err          Buffer for errors
130
 * @retval ptr New ExpandoFormat object
131
 *
132
 * Parse a printf()-style format, e.g. '-15.20x'
133
 *
134
 * @note A trailing `_` (underscore) means lowercase the string
135
 */
136
struct ExpandoFormat *parse_format(const char *str, const char **parsed_until,
158,763✔
137
                                   struct ExpandoParseError *err)
138
{
139
  if (!str || !parsed_until || !err)
158,763✔
140
    return NULL;
141

142
  const char *start = str;
143

144
  struct ExpandoFormat *fmt = MUTT_MEM_CALLOC(1, struct ExpandoFormat);
158,762✔
145

146
  fmt->leader = ' ';
158,762✔
147
  fmt->justification = JUSTIFY_RIGHT;
158,762✔
148
  fmt->min_cols = 0;
158,762✔
149
  fmt->max_cols = -1;
158,762✔
150

151
  if (*str == '-')
158,762✔
152
  {
153
    fmt->justification = JUSTIFY_LEFT;
9,979✔
154
    str++;
9,979✔
155
  }
156
  else if (*str == '=')
148,783✔
157
  {
158
    fmt->justification = JUSTIFY_CENTER;
12✔
159
    str++;
12✔
160
  }
161

162
  if (*str == '0')
158,762✔
163
  {
164
    // Ignore '0' with left-justification
165
    if (fmt->justification != JUSTIFY_LEFT)
81✔
166
      fmt->leader = '0';
45✔
167
    str++;
81✔
168
  }
169

170
  // Parse the width (min_cols)
171
  if (mutt_isdigit(*str))
158,762✔
172
  {
173
    unsigned short number = 0;
19,992✔
174
    const char *end_ptr = mutt_str_atous(str, &number);
19,992✔
175

176
    if (!end_ptr || (number == USHRT_MAX))
19,992✔
177
    {
178
      err->position = str;
11✔
179
      snprintf(err->message, sizeof(err->message), _("Invalid number: %s"), str);
11✔
180
      FREE(&fmt);
11✔
181
      return NULL;
11✔
182
    }
183

184
    fmt->min_cols = number;
19,981✔
185
    str = end_ptr;
186
  }
187

188
  // Parse the precision (max_cols)
189
  if (*str == '.')
158,751✔
190
  {
191
    str++;
2,160✔
192

193
    unsigned short number = 1;
2,160✔
194

195
    if (mutt_isdigit(*str))
2,160✔
196
    {
197
      const char *end_ptr = mutt_str_atous(str, &number);
2,130✔
198

199
      if (!end_ptr || (number == USHRT_MAX))
2,130✔
200
      {
201
        err->position = str;
5✔
202
        snprintf(err->message, sizeof(err->message), _("Invalid number: %s"), str);
5✔
203
        FREE(&fmt);
5✔
204
        return NULL;
5✔
205
      }
206

207
      str = end_ptr;
208
    }
209
    else
210
    {
211
      number = 0;
30✔
212
    }
213

214
    fmt->leader = (number == 0) ? ' ' : '0';
2,155✔
215
    fmt->max_cols = number;
2,155✔
216
  }
217

218
  // A modifier of '_' before the letter means force lower case
219
  if (*str == '_')
158,746✔
220
  {
221
    fmt->lower = true;
11✔
222
    str++;
11✔
223
  }
224

225
  if (str == start) // Failed to parse anything
158,746✔
226
    FREE(&fmt);
138,675✔
227

228
  if (fmt && (fmt->min_cols == 0) && (fmt->max_cols == -1) && !fmt->lower)
158,746✔
229
    FREE(&fmt);
12✔
230

231
  *parsed_until = str;
158,746✔
232
  return fmt;
158,746✔
233
}
234

235
/**
236
 * parse_short_name - Create an expando by its short name
237
 * @param[in]  str          String to parse
238
 * @param[in]  defs         Expando definitions
239
 * @param[in]  flags        Flag for conditional expandos
240
 * @param[in]  fmt          Formatting info
241
 * @param[out] parsed_until First character after parsed string
242
 * @param[out] err          Buffer for errors
243
 * @retval ptr New ExpandoNode
244
 */
245
struct ExpandoNode *parse_short_name(const char *str, const struct ExpandoDefinition *defs,
64,732✔
246
                                     ExpandoParserFlags flags,
247
                                     struct ExpandoFormat *fmt, const char **parsed_until,
248
                                     struct ExpandoParseError *err)
249
{
250
  if (!str || !defs)
64,732✔
251
    return NULL;
252

253
  const struct ExpandoDefinition *def = defs;
254
  for (; def && (def->short_name || def->long_name); def++)
892,353✔
255
  {
256
    size_t len = mutt_str_len(def->short_name);
892,342✔
257
    if (len == 0)
892,342✔
258
      continue;
×
259

260
    if (mutt_strn_equal(def->short_name, str, len))
892,342✔
261
    {
262
      if (def->parse)
64,719✔
263
      {
264
        return def->parse(str, fmt, def->did, def->uid, flags, parsed_until, err);
5,452✔
265
      }
266
      else
267
      {
268
        *parsed_until = str + len;
59,267✔
269
        return node_expando_new(fmt, def->did, def->uid);
59,267✔
270
      }
271
    }
272
  }
273

274
  return NULL;
275
}
276

277
/**
278
 * parse_long_name - Create an expando by its long name
279
 * @param[in]  str          String to parse
280
 * @param[in]  defs         Expando definitions
281
 * @param[in]  flags        Flag for conditional expandos
282
 * @param[in]  fmt          Formatting info
283
 * @param[out] parsed_until First character after parsed string
284
 * @param[out] err          Buffer for errors
285
 * @retval ptr New ExpandoNode
286
 */
287
struct ExpandoNode *parse_long_name(const char *str, const struct ExpandoDefinition *defs,
658✔
288
                                    ExpandoParserFlags flags,
289
                                    struct ExpandoFormat *fmt, const char **parsed_until,
290
                                    struct ExpandoParseError *err)
291
{
292
  if (!str || !defs)
658✔
293
    return NULL;
294

295
  const struct ExpandoDefinition *def = defs;
296
  for (; def && (def->short_name || def->long_name); def++)
36,058✔
297
  {
298
    if (!def->long_name)
35,400✔
299
      continue;
2,626✔
300

301
    size_t len = mutt_str_len(def->long_name);
32,774✔
302

303
    if (mutt_strn_equal(def->long_name, str, len))
32,774✔
304
    {
305
      *parsed_until = str + len;
×
306
      if (def->parse)
×
307
      {
308
        struct ExpandoNode *node = def->parse(str, fmt, def->did, def->uid,
×
309
                                              flags, parsed_until, err);
310
        if (node || (err->message[0] != '\0'))
×
311
          return node;
×
312
      }
313
      else
314
      {
315
        if (str[len] != '}') // Not an exact match
×
316
          continue;
×
317

318
        return node_expando_new(fmt, def->did, def->uid);
×
319
      }
320
    }
321
  }
322

323
  return NULL;
324
}
325

326
/**
327
 * node_expando_parse - Parse an Expando format string
328
 * @param[in]  str          String to parse
329
 * @param[in]  defs         Expando definitions
330
 * @param[in]  flags        Flag for conditional expandos
331
 * @param[out] parsed_until First character after parsed string
332
 * @param[out] err          Buffer for errors
333
 * @retval ptr New ExpandoNode
334
 */
335
struct ExpandoNode *node_expando_parse(const char *str, const struct ExpandoDefinition *defs,
46,988✔
336
                                       ExpandoParserFlags flags, const char **parsed_until,
337
                                       struct ExpandoParseError *err)
338
{
339
  ASSERT(str[0] == '%');
46,988✔
340
  str++;
46,988✔
341

342
  struct ExpandoFormat *fmt = parse_format(str, parsed_until, err);
46,988✔
343
  if (err->position)
46,988✔
344
  {
345
    FREE(&fmt);
15✔
346
    return NULL;
15✔
347
  }
348

349
  str = *parsed_until;
46,973✔
350

351
  struct ExpandoNode *node = parse_short_name(str, defs, flags, fmt, parsed_until, err);
46,973✔
352
  if (node)
46,973✔
353
    return node;
354

355
  if (!err->position)
11✔
356
  {
357
    err->position = *parsed_until;
9✔
358
    // L10N: e.g. "Unknown expando: %Q"
359
    snprintf(err->message, sizeof(err->message), _("Unknown expando: %%%.1s"), *parsed_until);
9✔
360
  }
361

362
  FREE(&fmt);
11✔
363
  return NULL;
11✔
364
}
365

366
/**
367
 * node_expando_parse_name - Parse an Expando format string
368
 * @param[in]  str          String to parse
369
 * @param[in]  defs         Expando definitions
370
 * @param[in]  flags        Flag for conditional expandos
371
 * @param[out] parsed_until First character after parsed string
372
 * @param[out] err          Buffer for errors
373
 * @retval ptr New ExpandoNode
374
 */
375
struct ExpandoNode *node_expando_parse_name(const char *str,
46,980✔
376
                                            const struct ExpandoDefinition *defs,
377
                                            ExpandoParserFlags flags, const char **parsed_until,
378
                                            struct ExpandoParseError *err)
379
{
380
  ASSERT(str[0] == '%');
46,980✔
381
  str++;
46,980✔
382

383
  struct ExpandoFormat *fmt = parse_format(str, parsed_until, err);
46,980✔
384
  if (err->position)
46,980✔
385
    goto fail;
12✔
386

387
  str = *parsed_until;
46,968✔
388

389
  if (str[0] != '{')
46,968✔
390
    goto fail;
46,310✔
391

392
  str++;
658✔
393

394
  struct ExpandoNode *node = parse_long_name(str, defs, flags, fmt, parsed_until, err);
658✔
395
  if (!node)
658✔
396
  {
397
    if (!err->position)
658✔
398
    {
399
      const char *end = str + strspn(str, "abcdefghijklmnopqrstuvwxyz0123456789-");
658✔
400

401
      // Only report an error if the content looks like a long name (i.e. starts
402
      // with at least one valid character).  If it doesn't, the '{' is probably
403
      // a short-name expando like %{%b %d} — let node_expando_parse() handle it.
404
      if (end != str)
658✔
405
      {
406
        err->position = str;
2✔
407
        if (*end != '}')
2✔
408
        {
409
          snprintf(err->message, sizeof(err->message), _("Expando is missing closing '}'"));
1✔
410
        }
411
        else
412
        {
413
          // L10N: e.g. "Unknown expando: %{bad}"
414
          snprintf(err->message, sizeof(err->message),
1✔
415
                   _("Unknown expando: %%{%.*s}"), (int) (end - str), str);
1✔
416
        }
417
      }
418
    }
419
    goto fail;
658✔
420
  }
421

422
  fmt = NULL; // owned by the node, now
×
423

424
  if ((*parsed_until)[0] == '}')
×
425
  {
426
    (*parsed_until)++;
×
427
    return node;
×
428
  }
429

430
  node_free(&node);
×
431

432
fail:
46,980✔
433
  FREE(&fmt);
46,980✔
434
  return NULL;
46,980✔
435
}
436

437
/**
438
 * skip_until_ch - Search a string for a terminator character
439
 * @param str        Start of string
440
 * @param terminator Character to find
441
 * @retval ptr Position of terminator character, or end-of-string
442
 */
443
const char *skip_until_ch(const char *str, char terminator)
675✔
444
{
445
  while (str[0] != '\0')
4,068✔
446
  {
447
    if (*str == terminator)
4,065✔
448
      break;
449

450
    if (str[0] == '\\') // Literal character
3,394✔
451
    {
452
      if (str[1] == '\0')
2✔
453
        return str + 1;
1✔
454

455
      str++;
1✔
456
    }
457

458
    str++;
3,393✔
459
  }
460

461
  return str;
462
}
463

464
/**
465
 * node_expando_parse_enclosure - Parse an enclosed Expando
466
 * @param[in]  str          String to parse
467
 * @param[in]  did          Domain ID
468
 * @param[in]  uid          Unique ID
469
 * @param[in]  terminator   Terminating character
470
 * @param[in]  fmt          Formatting info
471
 * @param[out] parsed_until First character after the parsed string
472
 * @param[out] err          Buffer for errors
473
 * @retval ptr New ExpandoNode
474
 */
475
struct ExpandoNode *node_expando_parse_enclosure(const char *str, int did,
672✔
476
                                                 int uid, char terminator,
477
                                                 struct ExpandoFormat *fmt,
478
                                                 const char **parsed_until,
479
                                                 struct ExpandoParseError *err)
480

481
{
482
  str++; // skip opening char
672✔
483

484
  const char *expando_end = skip_until_ch(str, terminator);
672✔
485

486
  if (*expando_end != terminator)
672✔
487
  {
488
    err->position = expando_end;
2✔
489
    snprintf(err->message, sizeof(err->message),
2✔
490
             // L10N: Expando is missing a terminator character
491
             //       e.g. "%[..." is missing the final ']'
492
             _("Expando is missing terminator: '%c'"), terminator);
2✔
493
    return NULL;
2✔
494
  }
495

496
  *parsed_until = expando_end + 1;
670✔
497

498
  struct ExpandoNode *node = node_expando_new(fmt, did, uid);
670✔
499

500
  struct Buffer *buf = buf_pool_get();
670✔
501
  for (; str < expando_end; str++)
4,046✔
502
  {
503
    if (str[0] == '\\')
3,376✔
504
      continue;
1✔
505
    buf_addch(buf, str[0]);
3,375✔
506
  }
507

508
  node->text = buf_strdup(buf);
670✔
509
  buf_pool_release(&buf);
670✔
510

511
  return node;
670✔
512
}
513

514
/**
515
 * add_color - Add a colour code to a buffer
516
 * @param buf Buffer for colour code
517
 * @param cid Colour
518
 */
519
void add_color(struct Buffer *buf, enum ColorId cid)
25✔
520
{
521
  ASSERT(cid < MT_COLOR_MAX);
25✔
522

523
  buf_addch(buf, MUTT_SPECIAL_INDEX);
25✔
524
  buf_addch(buf, cid);
25✔
525
}
25✔
526

527
/**
528
 * node_expando_render - Render an Expando Node - Implements ExpandoNode::render() - @ingroup expando_render
529
 */
530
int node_expando_render(const struct ExpandoNode *node,
307✔
531
                        const struct ExpandoRenderCallback *erc, struct Buffer *buf,
532
                        int max_cols, void *data, MuttFormatFlags flags)
533
{
534
  ASSERT(node->type == ENT_EXPANDO);
307✔
535

536
  struct Buffer *buf_expando = buf_pool_get();
307✔
537
  struct Buffer *buf_format = buf_pool_get();
307✔
538

539
  const struct ExpandoFormat *fmt = node->format;
307✔
540
  const struct NodeExpandoPrivate *priv = node->ndata;
307✔
541

542
  // ---------------------------------------------------------------------------
543
  // Numbers and strings get treated slightly differently. We prefer strings.
544
  // This allows dates to be stored as 1729850182, but displayed as "2024-10-25".
545

546
  const struct ExpandoRenderCallback *erc_match = find_get_string(erc, node->did, node->uid);
307✔
547
  if (erc_match)
307✔
548
  {
549
    erc_match->get_string(node, data, flags, buf_expando);
188✔
550

551
    if (fmt && fmt->lower)
188✔
552
      buf_lower_special(buf_expando);
1✔
553
  }
554
  else
555
  {
556
    erc_match = find_get_number(erc, node->did, node->uid);
119✔
557
    ASSERT(erc_match && "Unknown UID");
119✔
558

559
    const long num = erc_match->get_number(node, data, flags);
119✔
560

561
    int precision = 1;
562

563
    if (fmt)
119✔
564
    {
565
      precision = fmt->max_cols;
106✔
566
      if ((precision < 0) && (fmt->leader == '0'))
106✔
567
        precision = fmt->min_cols;
6✔
568
    }
569

570
    if (num < 0)
119✔
571
      precision--; // Allow space for the '-' sign
39✔
572

573
    buf_printf(buf_expando, "%.*ld", precision, num);
119✔
574
  }
575

576
  // ---------------------------------------------------------------------------
577

578
  int max = max_cols;
579
  int min = 0;
580

581
  if (fmt)
137✔
582
  {
583
    min = fmt->min_cols;
124✔
584
    if (fmt->max_cols > 0)
124✔
585
      max = MIN(max_cols, fmt->max_cols);
49✔
586
  }
587

588
  const enum FormatJustify just = fmt ? fmt->justification : JUSTIFY_LEFT;
307✔
589

590
  int total_cols = format_string(buf_format, min, max, just, ' ', buf_string(buf_expando),
614✔
591
                                 buf_len(buf_expando), priv->has_tree);
307✔
592

593
  if (!buf_is_empty(buf_format))
307✔
594
  {
595
    if (priv->color > -1)
299✔
596
      add_color(buf, priv->color);
12✔
597

598
    buf_addstr(buf, buf_string(buf_format));
598✔
599

600
    if (priv->color > -1)
299✔
601
      add_color(buf, MT_COLOR_INDEX);
12✔
602
  }
603

604
  buf_pool_release(&buf_format);
307✔
605
  buf_pool_release(&buf_expando);
307✔
606

607
  return total_cols;
307✔
608
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc