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

neomutt / neomutt / 20704611763

04 Jan 2026 11:45AM UTC coverage: 45.271% (+1.1%) from 44.194%
20704611763

push

github

flatcap
notmuch: restore virtual-mailboxes

`virtual-mailboxes` has been dropped from the docs.

The preferred commands are:

- `mailboxes -label LABEL MAILBOX-PATH`
- `named-mailboxes LABEL MAILBOX-PATH`

11367 of 25109 relevant lines covered (45.27%)

291.36 hits per line

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

93.47
/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 "node_expando.h"
36
#include "color/lib.h"
37
#include "definition.h"
38
#include "format.h"
39
#include "helpers.h"
40
#include "mutt_thread.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;
547✔
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)
544✔
64
{
65
  if (!ptr || !*ptr)
544✔
66
    return;
67

68
  FREE(ptr);
543✔
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)
546✔
79
{
80
  struct ExpandoNode *node = node_new();
546✔
81

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

87
  node->format = fmt;
546✔
88

89
  node->ndata = node_expando_private_new();
546✔
90
  node->ndata_free = node_expando_private_free;
546✔
91

92
  return node;
546✔
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,
2,013✔
137
                                   struct ExpandoParseError *err)
138
{
139
  if (!str || !parsed_until || !err)
2,013✔
140
    return NULL;
141

142
  const char *start = str;
143

144
  struct ExpandoFormat *fmt = MUTT_MEM_CALLOC(1, struct ExpandoFormat);
2,012✔
145

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

151
  if (*str == '-')
2,012✔
152
  {
153
    fmt->justification = JUSTIFY_LEFT;
184✔
154
    str++;
184✔
155
  }
156
  else if (*str == '=')
1,828✔
157
  {
158
    fmt->justification = JUSTIFY_CENTER;
12✔
159
    str++;
12✔
160
  }
161

162
  if (*str == '0')
2,012✔
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))
2,012✔
172
  {
173
    unsigned short number = 0;
402✔
174
    const char *end_ptr = mutt_str_atous(str, &number);
402✔
175

176
    if (!end_ptr || (number == USHRT_MAX))
402✔
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;
391✔
185
    str = end_ptr;
186
  }
187

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

193
    unsigned short number = 1;
201✔
194

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

199
      if (!end_ptr || (number == USHRT_MAX))
171✔
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';
196✔
215
    fmt->max_cols = number;
196✔
216
  }
217

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

225
  if (str == start) // Failed to parse anything
1,996✔
226
    FREE(&fmt);
1,515✔
227

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

231
  *parsed_until = str;
1,996✔
232
  return fmt;
1,996✔
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,
730✔
246
                                     ExpandoParserFlags flags,
247
                                     struct ExpandoFormat *fmt, const char **parsed_until,
248
                                     struct ExpandoParseError *err)
249
{
250
  if (!str || !defs)
730✔
251
    return NULL;
252

253
  const struct ExpandoDefinition *def = defs;
254
  for (; def && (def->short_name || def->long_name); def++)
4,821✔
255
  {
256
    size_t len = mutt_str_len(def->short_name);
4,810✔
257

258
    if (mutt_strn_equal(def->short_name, str, len))
4,810✔
259
    {
260
      if (def->parse)
717✔
261
      {
262
        return def->parse(str, fmt, def->did, def->uid, flags, parsed_until, err);
225✔
263
      }
264
      else
265
      {
266
        *parsed_until = str + len;
492✔
267
        return node_expando_new(fmt, def->did, def->uid);
492✔
268
      }
269
    }
270
  }
271

272
  return NULL;
273
}
274

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

293
  const struct ExpandoDefinition *def = defs;
294
  for (; def && (def->short_name || def->long_name); def++)
110✔
295
  {
296
    if (!def->long_name)
108✔
297
      continue;
8✔
298

299
    size_t len = mutt_str_len(def->long_name);
100✔
300

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

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

321
  return NULL;
322
}
323

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

340
  struct ExpandoFormat *fmt = parse_format(str, parsed_until, err);
615✔
341
  if (err->position)
615✔
342
  {
343
    FREE(&fmt);
13✔
344
    return NULL;
13✔
345
  }
346

347
  str = *parsed_until;
602✔
348

349
  struct ExpandoNode *node = parse_short_name(str, defs, flags, fmt, parsed_until, err);
602✔
350
  if (node)
602✔
351
    return node;
352

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

360
  FREE(&fmt);
11✔
361
  return NULL;
11✔
362
}
363

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

381
  struct ExpandoFormat *fmt = parse_format(str, parsed_until, err);
607✔
382
  if (err->position)
607✔
383
    goto fail;
12✔
384

385
  str = *parsed_until;
595✔
386

387
  if (str[0] != '{')
595✔
388
    goto fail;
593✔
389

390
  str++;
2✔
391

392
  struct ExpandoNode *node = parse_long_name(str, defs, flags, fmt, parsed_until, err);
2✔
393
  if (!node)
2✔
394
    goto fail;
2✔
395

396
  fmt = NULL; // owned by the node, now
×
397

398
  if ((*parsed_until)[0] == '}')
×
399
  {
400
    (*parsed_until)++;
×
401
    return node;
×
402
  }
403

404
  node_free(&node);
×
405

406
fail:
607✔
407
  FREE(&fmt);
607✔
408
  return NULL;
607✔
409
}
410

411
/**
412
 * skip_until_ch - Search a string for a terminator character
413
 * @param str        Start of string
414
 * @param terminator Character to find
415
 * @retval ptr Position of terminator character, or end-of-string
416
 */
417
const char *skip_until_ch(const char *str, char terminator)
21✔
418
{
419
  while (str[0] != '\0')
144✔
420
  {
421
    if (*str == terminator)
141✔
422
      break;
423

424
    if (str[0] == '\\') // Literal character
124✔
425
    {
426
      if (str[1] == '\0')
2✔
427
        return str + 1;
1✔
428

429
      str++;
1✔
430
    }
431

432
    str++;
123✔
433
  }
434

435
  return str;
436
}
437

438
/**
439
 * node_expando_parse_enclosure - Parse an enclosed Expando
440
 * @param[in]  str          String to parse
441
 * @param[in]  did          Domain ID
442
 * @param[in]  uid          Unique ID
443
 * @param[in]  terminator   Terminating character
444
 * @param[in]  fmt          Formatting info
445
 * @param[out] parsed_until First character after the parsed string
446
 * @param[out] err          Buffer for errors
447
 * @retval ptr New ExpandoNode
448
 */
449
struct ExpandoNode *node_expando_parse_enclosure(const char *str, int did,
18✔
450
                                                 int uid, char terminator,
451
                                                 struct ExpandoFormat *fmt,
452
                                                 const char **parsed_until,
453
                                                 struct ExpandoParseError *err)
454

455
{
456
  str++; // skip opening char
18✔
457

458
  const char *expando_end = skip_until_ch(str, terminator);
18✔
459

460
  if (*expando_end != terminator)
18✔
461
  {
462
    err->position = expando_end;
2✔
463
    snprintf(err->message, sizeof(err->message),
2✔
464
             // L10N: Expando is missing a terminator character
465
             //       e.g. "%[..." is missing the final ']'
466
             _("Expando is missing terminator: '%c'"), terminator);
2✔
467
    return NULL;
2✔
468
  }
469

470
  *parsed_until = expando_end + 1;
16✔
471

472
  struct ExpandoNode *node = node_expando_new(fmt, did, uid);
16✔
473

474
  struct Buffer *buf = buf_pool_get();
16✔
475
  for (; str < expando_end; str++)
122✔
476
  {
477
    if (str[0] == '\\')
106✔
478
      continue;
1✔
479
    buf_addch(buf, str[0]);
105✔
480
  }
481

482
  node->text = buf_strdup(buf);
16✔
483
  buf_pool_release(&buf);
16✔
484

485
  return node;
16✔
486
}
487

488
/**
489
 * add_color - Add a colour code to a buffer
490
 * @param buf Buffer for colour code
491
 * @param cid Colour
492
 */
493
void add_color(struct Buffer *buf, enum ColorId cid)
25✔
494
{
495
  ASSERT(cid < MT_COLOR_MAX);
25✔
496

497
  buf_addch(buf, MUTT_SPECIAL_INDEX);
25✔
498
  buf_addch(buf, cid);
25✔
499
}
25✔
500

501
/**
502
 * node_expando_render - Render an Expando Node - Implements ExpandoNode::render() - @ingroup expando_render
503
 */
504
int node_expando_render(const struct ExpandoNode *node,
307✔
505
                        const struct ExpandoRenderCallback *erc, struct Buffer *buf,
506
                        int max_cols, void *data, MuttFormatFlags flags)
507
{
508
  ASSERT(node->type == ENT_EXPANDO);
307✔
509

510
  struct Buffer *buf_expando = buf_pool_get();
307✔
511
  struct Buffer *buf_format = buf_pool_get();
307✔
512

513
  const struct ExpandoFormat *fmt = node->format;
307✔
514
  const struct NodeExpandoPrivate *priv = node->ndata;
307✔
515

516
  // ---------------------------------------------------------------------------
517
  // Numbers and strings get treated slightly differently. We prefer strings.
518
  // This allows dates to be stored as 1729850182, but displayed as "2024-10-25".
519

520
  const struct ExpandoRenderCallback *erc_match = find_get_string(erc, node->did, node->uid);
307✔
521
  if (erc_match)
307✔
522
  {
523
    erc_match->get_string(node, data, flags, buf_expando);
188✔
524

525
    if (fmt && fmt->lower)
188✔
526
      buf_lower_special(buf_expando);
1✔
527
  }
528
  else
529
  {
530
    erc_match = find_get_number(erc, node->did, node->uid);
119✔
531
    ASSERT(erc_match && "Unknown UID");
119✔
532

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

535
    int precision = 1;
536

537
    if (fmt)
119✔
538
    {
539
      precision = fmt->max_cols;
106✔
540
      if ((precision < 0) && (fmt->leader == '0'))
106✔
541
        precision = fmt->min_cols;
6✔
542
    }
543

544
    if (num < 0)
119✔
545
      precision--; // Allow space for the '-' sign
39✔
546

547
    buf_printf(buf_expando, "%.*ld", precision, num);
119✔
548
  }
549

550
  // ---------------------------------------------------------------------------
551

552
  int max = max_cols;
553
  int min = 0;
554

555
  if (fmt)
137✔
556
  {
557
    min = fmt->min_cols;
124✔
558
    if (fmt->max_cols > 0)
124✔
559
      max = MIN(max_cols, fmt->max_cols);
49✔
560
  }
561

562
  const enum FormatJustify just = fmt ? fmt->justification : JUSTIFY_LEFT;
307✔
563

564
  int total_cols = format_string(buf_format, min, max, just, ' ', buf_string(buf_expando),
614✔
565
                                 buf_len(buf_expando), priv->has_tree);
307✔
566

567
  if (!buf_is_empty(buf_format))
307✔
568
  {
569
    if (priv->color > -1)
299✔
570
      add_color(buf, priv->color);
12✔
571

572
    buf_addstr(buf, buf_string(buf_format));
598✔
573

574
    if (priv->color > -1)
299✔
575
      add_color(buf, MT_COLOR_INDEX);
12✔
576
  }
577

578
  buf_pool_release(&buf_format);
307✔
579
  buf_pool_release(&buf_expando);
307✔
580

581
  return total_cols;
307✔
582
}
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