Rocco Rutte:
[apps/madmutt.git] / color.c
1 /*
2  * Copyright notice from original mutt:
3  * Copyright (C) 1996-2002 Michael R. Elkins <me@mutt.org>
4  *
5  * This file is part of mutt-ng, see http://www.muttng.org/.
6  * It's licensed under the GNU General Public License,
7  * please see the file GPL in the top level source directory.
8  */
9
10 #if HAVE_CONFIG_H
11 # include "config.h"
12 #endif
13
14 #include "mutt.h"
15 #include "mutt_curses.h"
16 #include "mapping.h"
17
18 #include "lib/mem.h"
19 #include "lib/intl.h"
20 #include "lib/str.h"
21
22 #include <string.h>
23 #include <stdlib.h>
24 #include <ctype.h>
25
26 /* globals */
27 int *ColorQuote;
28 int ColorQuoteUsed;
29 int ColorDefs[MT_COLOR_MAX];
30 COLOR_LINE *ColorHdrList = NULL;
31 COLOR_LINE *ColorBodyList = NULL;
32 COLOR_LINE *ColorIndexList = NULL;
33
34 /* local to this file */
35 static int ColorQuoteSize;
36
37 #ifdef HAVE_COLOR
38
39 #define COLOR_DEFAULT (-2)
40
41 typedef struct color_list {
42   short fg;
43   short bg;
44   short index;
45   short count;
46   struct color_list *next;
47 } COLOR_LIST;
48
49 static COLOR_LIST *ColorList = NULL;
50 static int UserColors = 0;
51
52 static struct mapping_t Colors[] = {
53   {"black", COLOR_BLACK},
54   {"blue", COLOR_BLUE},
55   {"cyan", COLOR_CYAN},
56   {"green", COLOR_GREEN},
57   {"magenta", COLOR_MAGENTA},
58   {"red", COLOR_RED},
59   {"white", COLOR_WHITE},
60   {"yellow", COLOR_YELLOW},
61 #if defined (USE_SLANG_CURSES) || defined (HAVE_USE_DEFAULT_COLORS)
62   {"default", COLOR_DEFAULT},
63 #endif
64   {0, 0}
65 };
66
67 #endif /* HAVE_COLOR */
68
69 static struct mapping_t Fields[] = {
70   {"hdrdefault", MT_COLOR_HDEFAULT},
71   {"quoted", MT_COLOR_QUOTED},
72   {"signature", MT_COLOR_SIGNATURE},
73   {"indicator", MT_COLOR_INDICATOR},
74   {"status", MT_COLOR_STATUS},
75   {"tree", MT_COLOR_TREE},
76   {"error", MT_COLOR_ERROR},
77   {"normal", MT_COLOR_NORMAL},
78   {"tilde", MT_COLOR_TILDE},
79   {"markers", MT_COLOR_MARKERS},
80   {"header", MT_COLOR_HEADER},
81   {"body", MT_COLOR_BODY},
82   {"message", MT_COLOR_MESSAGE},
83   {"attachment", MT_COLOR_ATTACHMENT},
84   {"search", MT_COLOR_SEARCH},
85   {"bold", MT_COLOR_BOLD},
86   {"underline", MT_COLOR_UNDERLINE},
87   {"index", MT_COLOR_INDEX},
88   {"sidebar_new", MT_COLOR_NEW},
89   {"sidebar", MT_COLOR_SIDEBAR},
90   {"sidebar_flagged", MT_COLOR_FLAGGED},
91   {NULL, 0}
92 };
93
94 #define COLOR_QUOTE_INIT        8
95
96 static COLOR_LINE *mutt_new_color_line (void)
97 {
98   COLOR_LINE *p = safe_calloc (1, sizeof (COLOR_LINE));
99
100   p->fg = p->bg = -1;
101
102   return (p);
103 }
104
105 static void mutt_free_color_line (COLOR_LINE ** l, int free_colors)
106 {
107   COLOR_LINE *tmp;
108
109   if (!l || !*l)
110     return;
111
112   tmp = *l;
113
114 #ifdef HAVE_COLOR
115   if (free_colors && tmp->fg != -1 && tmp->bg != -1)
116     mutt_free_color (tmp->fg, tmp->bg);
117 #endif
118
119   /* we should really introduce a container
120    * type for regular expressions.
121    */
122
123   regfree (&tmp->rx);
124   mutt_pattern_free (&tmp->color_pattern);
125   FREE (&tmp->pattern);
126   FREE (l);
127 }
128
129 void ci_start_color (void)
130 {
131   memset (ColorDefs, A_NORMAL, sizeof (int) * MT_COLOR_MAX);
132   ColorQuote = (int *) safe_malloc (COLOR_QUOTE_INIT * sizeof (int));
133   memset (ColorQuote, A_NORMAL, sizeof (int) * COLOR_QUOTE_INIT);
134   ColorQuoteSize = COLOR_QUOTE_INIT;
135   ColorQuoteUsed = 0;
136
137   /* set some defaults */
138   ColorDefs[MT_COLOR_STATUS] = A_REVERSE;
139   ColorDefs[MT_COLOR_INDICATOR] = A_REVERSE;
140   ColorDefs[MT_COLOR_SEARCH] = A_REVERSE;
141   ColorDefs[MT_COLOR_MARKERS] = A_REVERSE;
142   /* special meaning: toggle the relevant attribute */
143   ColorDefs[MT_COLOR_BOLD] = 0;
144   ColorDefs[MT_COLOR_UNDERLINE] = 0;
145
146 #ifdef HAVE_COLOR
147   start_color ();
148 #endif
149 }
150
151 #ifdef HAVE_COLOR
152
153 #ifdef USE_SLANG_CURSES
154 static char *get_color_name (char *dest, size_t destlen, int val)
155 {
156   static char *missing[3] = { "brown", "lightgray", "default" };
157   int i;
158
159   switch (val) {
160   case COLOR_YELLOW:
161     strfcpy (dest, missing[0], destlen);
162     return dest;
163
164   case COLOR_WHITE:
165     strfcpy (dest, missing[1], destlen);
166     return dest;
167
168   case COLOR_DEFAULT:
169     strfcpy (dest, missing[2], destlen);
170     return dest;
171   }
172
173   for (i = 0; Colors[i].name; i++) {
174     if (Colors[i].value == val) {
175       strfcpy (dest, Colors[i].name, destlen);
176       return dest;
177     }
178   }
179
180   /* Sigh. If we got this far, the color is of the form 'colorN'
181    * Slang can handle this itself, so just return 'colorN'
182    */
183
184   snprintf (dest, destlen, "color%d", val);
185   return dest;
186 }
187 #endif
188
189 int mutt_alloc_color (int fg, int bg)
190 {
191   COLOR_LIST *p = ColorList;
192   int i;
193
194 #if defined (USE_SLANG_CURSES)
195   char fgc[SHORT_STRING], bgc[SHORT_STRING];
196 #endif
197
198   /* check to see if this color is already allocated to save space */
199   while (p) {
200     if (p->fg == fg && p->bg == bg) {
201       (p->count)++;
202       return (COLOR_PAIR (p->index));
203     }
204     p = p->next;
205   }
206
207   /* check to see if there are colors left */
208   if (++UserColors > COLOR_PAIRS)
209     return (A_NORMAL);
210
211   /* find the smallest available index (object) */
212   i = 1;
213   FOREVER {
214     p = ColorList;
215     while (p) {
216       if (p->index == i)
217         break;
218       p = p->next;
219     }
220     if (p == NULL)
221       break;
222     i++;
223   }
224
225   p = (COLOR_LIST *) safe_malloc (sizeof (COLOR_LIST));
226   p->next = ColorList;
227   ColorList = p;
228
229   p->index = i;
230   p->count = 1;
231   p->bg = bg;
232   p->fg = fg;
233
234 #if defined (USE_SLANG_CURSES)
235   if (fg == COLOR_DEFAULT || bg == COLOR_DEFAULT)
236     SLtt_set_color (i, NULL, get_color_name (fgc, sizeof (fgc), fg),
237                     get_color_name (bgc, sizeof (bgc), bg));
238   else
239 #elif defined (HAVE_USE_DEFAULT_COLORS)
240   if (fg == COLOR_DEFAULT)
241     fg = -1;
242   if (bg == COLOR_DEFAULT)
243     bg = -1;
244 #endif
245
246   init_pair (i, fg, bg);
247
248   dprint (1, (debugfile, "mutt_alloc_color(): Color pairs used so far: %d\n",
249               UserColors));
250
251   return (COLOR_PAIR (p->index));
252 }
253
254 void mutt_free_color (int fg, int bg)
255 {
256   COLOR_LIST *p, *q;
257
258   p = ColorList;
259   while (p) {
260     if (p->fg == fg && p->bg == bg) {
261       (p->count)--;
262       if (p->count > 0)
263         return;
264
265       UserColors--;
266       dprint (1,
267               (debugfile, "mutt_free_color(): Color pairs used so far: %d\n",
268                UserColors));
269
270       if (p == ColorList) {
271         ColorList = ColorList->next;
272         FREE (&p);
273         return;
274       }
275       q = ColorList;
276       while (q) {
277         if (q->next == p) {
278           q->next = p->next;
279           FREE (&p);
280           return;
281         }
282         q = q->next;
283       }
284       /* can't get here */
285     }
286     p = p->next;
287   }
288 }
289
290 #endif /* HAVE_COLOR */
291
292
293 #ifdef HAVE_COLOR
294
295 static int
296 parse_color_name (const char *s, int *col, int *attr, int brite, BUFFER * err)
297 {
298   char *eptr;
299
300   if (safe_strncasecmp (s, "bright", 6) == 0) {
301     *attr |= brite;
302     s += 6;
303   }
304
305   /* allow aliases for xterm color resources */
306   if (safe_strncasecmp (s, "color", 5) == 0) {
307     s += 5;
308     *col = strtol (s, &eptr, 10);
309     if (!*s || *eptr || *col < 0 ||
310         (*col >= COLORS && !option (OPTNOCURSES) && has_colors ())) {
311       snprintf (err->data, err->dsize, _("%s: color not supported by term"),
312                 s);
313       return (-1);
314     }
315   }
316   else if ((*col = mutt_getvaluebyname (s, Colors)) == -1) {
317     snprintf (err->data, err->dsize, _("%s: no such color"), s);
318     return (-1);
319   }
320
321   return 0;
322 }
323
324 #endif
325
326
327 /* usage: uncolor index pattern [pattern...]
328  *        unmono  index pattern [pattern...]
329  */
330
331 static int
332 _mutt_parse_uncolor (BUFFER * buf, BUFFER * s, unsigned long data,
333                      BUFFER * err, short parse_uncolor);
334
335
336 #ifdef HAVE_COLOR
337
338 int mutt_parse_uncolor (BUFFER * buf, BUFFER * s, unsigned long data,
339                         BUFFER * err)
340 {
341   return _mutt_parse_uncolor (buf, s, data, err, 1);
342 }
343
344 #endif
345
346 int mutt_parse_unmono (BUFFER * buf, BUFFER * s, unsigned long data,
347                        BUFFER * err)
348 {
349   return _mutt_parse_uncolor (buf, s, data, err, 0);
350 }
351
352 static int
353 _mutt_parse_uncolor (BUFFER * buf, BUFFER * s, unsigned long data,
354                      BUFFER * err, short parse_uncolor)
355 {
356   int object = 0, do_cache = 0;
357   COLOR_LINE *tmp, *last = NULL;
358
359   mutt_extract_token (buf, s, 0);
360
361   if ((object = mutt_getvaluebyname (buf->data, Fields)) == -1) {
362     snprintf (err->data, err->dsize, _("%s: no such object"), buf->data);
363     return (-1);
364   }
365
366   if (safe_strncmp (buf->data, "index", 5) != 0) {
367     snprintf (err->data, err->dsize,
368               _("%s: command valid only for index object"),
369               parse_uncolor ? "uncolor" : "unmono");
370     return (-1);
371   }
372
373   if (!MoreArgs (s)) {
374     snprintf (err->data, err->dsize,
375               _("%s: too few arguments"),
376               parse_uncolor ? "uncolor" : "unmono");
377     return (-1);
378   }
379
380   if (
381 #ifdef HAVE_COLOR
382        /* we're running without curses */
383        option (OPTNOCURSES)
384        ||                       /* we're parsing an uncolor command, and have no colors */
385        (parse_uncolor && !has_colors ())
386        /* we're parsing an unmono command, and have colors */
387        || (!parse_uncolor && has_colors ())
388 #else
389        /* We don't even have colors compiled in */
390        parse_uncolor
391 #endif
392     ) {
393     /* just eat the command, but don't do anything real about it */
394     do
395       mutt_extract_token (buf, s, 0);
396     while (MoreArgs (s));
397
398     return 0;
399   }
400
401
402   do {
403     mutt_extract_token (buf, s, 0);
404     if (!safe_strcmp ("*", buf->data)) {
405       for (tmp = ColorIndexList; tmp;) {
406         if (!do_cache)
407           do_cache = 1;
408         last = tmp;
409         tmp = tmp->next;
410         mutt_free_color_line (&last, parse_uncolor);
411       }
412       ColorIndexList = NULL;
413     }
414     else {
415       for (last = NULL, tmp = ColorIndexList; tmp;
416            last = tmp, tmp = tmp->next) {
417         if (!safe_strcmp (buf->data, tmp->pattern)) {
418           if (!do_cache)
419             do_cache = 1;
420           dprint (1,
421                   (debugfile, "Freeing pattern \"%s\" from ColorIndexList\n",
422                    tmp->pattern));
423           if (last)
424             last->next = tmp->next;
425           else
426             ColorIndexList = tmp->next;
427           mutt_free_color_line (&tmp, parse_uncolor);
428           break;
429         }
430       }
431     }
432   }
433   while (MoreArgs (s));
434
435
436   if (do_cache && !option (OPTNOCURSES)) {
437     int i;
438
439     set_option (OPTFORCEREDRAWINDEX);
440     /* force re-caching of index colors */
441     for (i = 0; Context && i < Context->msgcount; i++)
442       Context->hdrs[i]->pair = 0;
443   }
444   return (0);
445 }
446
447
448 static int
449 add_pattern (COLOR_LINE ** top, const char *s, int sensitive,
450              int fg, int bg, int attr, BUFFER * err, int is_index)
451 {
452
453   /* is_index used to store compiled pattern
454    * only for `index' color object 
455    * when called from mutt_parse_color() */
456
457   COLOR_LINE *tmp = *top;
458
459   while (tmp) {
460     if (sensitive) {
461       if (safe_strcmp (s, tmp->pattern) == 0)
462         break;
463     }
464     else {
465       if (safe_strcasecmp (s, tmp->pattern) == 0)
466         break;
467     }
468     tmp = tmp->next;
469   }
470
471   if (tmp) {
472 #ifdef HAVE_COLOR
473     if (fg != -1 && bg != -1) {
474       if (tmp->fg != fg || tmp->bg != bg) {
475         mutt_free_color (tmp->fg, tmp->bg);
476         tmp->fg = fg;
477         tmp->bg = bg;
478         attr |= mutt_alloc_color (fg, bg);
479       }
480       else
481         attr |= (tmp->pair & ~A_BOLD);
482     }
483 #endif /* HAVE_COLOR */
484     tmp->pair = attr;
485   }
486   else {
487     int r;
488     char buf[STRING];
489
490     tmp = mutt_new_color_line ();
491     if (is_index) {
492       int i;
493
494       strfcpy (buf, NONULL (s), sizeof (buf));
495       mutt_check_simple (buf, sizeof (buf), NONULL (SimpleSearch));
496       if ((tmp->color_pattern =
497            mutt_pattern_comp (buf, M_FULL_MSG, err)) == NULL) {
498         mutt_free_color_line (&tmp, 1);
499         return -1;
500       }
501       /* force re-caching of index colors */
502       for (i = 0; Context && i < Context->msgcount; i++)
503         Context->hdrs[i]->pair = 0;
504     }
505     else
506       if ((r =
507            REGCOMP (&tmp->rx, s,
508                     (sensitive ? mutt_which_case (s) : REG_ICASE))) != 0) {
509       regerror (r, &tmp->rx, err->data, err->dsize);
510       mutt_free_color_line (&tmp, 1);
511       return (-1);
512     }
513     tmp->next = *top;
514     tmp->pattern = safe_strdup (s);
515 #ifdef HAVE_COLOR
516     if (fg != -1 && bg != -1) {
517       tmp->fg = fg;
518       tmp->bg = bg;
519       attr |= mutt_alloc_color (fg, bg);
520     }
521 #endif
522     tmp->pair = attr;
523     *top = tmp;
524   }
525
526   return 0;
527 }
528
529 static int
530 parse_object (BUFFER * buf, BUFFER * s, int *o, int *ql, BUFFER * err)
531 {
532   int q_level = 0;
533   char *eptr;
534
535   if (!MoreArgs (s)) {
536     strfcpy (err->data, _("Missing arguments."), err->dsize);
537     return -1;
538   }
539
540   mutt_extract_token (buf, s, 0);
541   if (!safe_strncmp (buf->data, "quoted", 6)) {
542     if (buf->data[6]) {
543       *ql = strtol (buf->data + 6, &eptr, 10);
544       if (*eptr || q_level < 0) {
545         snprintf (err->data, err->dsize, _("%s: no such object"), buf->data);
546         return -1;
547       }
548     }
549     else
550       *ql = 0;
551
552     *o = MT_COLOR_QUOTED;
553   }
554   else if ((*o = mutt_getvaluebyname (buf->data, Fields)) == -1) {
555     snprintf (err->data, err->dsize, _("%s: no such object"), buf->data);
556     return (-1);
557   }
558
559   return 0;
560 }
561
562 typedef int (*parser_callback_t) (BUFFER *, BUFFER *, int *, int *, int *,
563                                   BUFFER *);
564
565 #ifdef HAVE_COLOR
566
567 static int
568 parse_color_pair (BUFFER * buf, BUFFER * s, int *fg, int *bg, int *attr,
569                   BUFFER * err)
570 {
571   if (!MoreArgs (s)) {
572     strfcpy (err->data, _("color: too few arguments"), err->dsize);
573     return (-1);
574   }
575
576   mutt_extract_token (buf, s, 0);
577
578   if (parse_color_name (buf->data, fg, attr, A_BOLD, err) != 0)
579     return (-1);
580
581   if (!MoreArgs (s)) {
582     strfcpy (err->data, _("color: too few arguments"), err->dsize);
583     return (-1);
584   }
585
586   mutt_extract_token (buf, s, 0);
587
588   if (parse_color_name (buf->data, bg, attr, A_BLINK, err) != 0)
589     return (-1);
590
591   return 0;
592 }
593
594 #endif
595
596 static int
597 parse_attr_spec (BUFFER * buf, BUFFER * s, int *fg, int *bg, int *attr,
598                  BUFFER * err)
599 {
600
601   if (fg)
602     *fg = -1;
603   if (bg)
604     *bg = -1;
605
606   if (!MoreArgs (s)) {
607     strfcpy (err->data, _("mono: too few arguments"), err->dsize);
608     return (-1);
609   }
610
611   mutt_extract_token (buf, s, 0);
612
613   if (ascii_strcasecmp ("bold", buf->data) == 0)
614     *attr |= A_BOLD;
615   else if (ascii_strcasecmp ("underline", buf->data) == 0)
616     *attr |= A_UNDERLINE;
617   else if (ascii_strcasecmp ("none", buf->data) == 0)
618     *attr = A_NORMAL;
619   else if (ascii_strcasecmp ("reverse", buf->data) == 0)
620     *attr |= A_REVERSE;
621   else if (ascii_strcasecmp ("standout", buf->data) == 0)
622     *attr |= A_STANDOUT;
623   else if (ascii_strcasecmp ("normal", buf->data) == 0)
624     *attr = A_NORMAL;           /* needs use = instead of |= to clear other bits */
625   else {
626     snprintf (err->data, err->dsize, _("%s: no such attribute"), buf->data);
627     return (-1);
628   }
629
630   return 0;
631 }
632
633 static int fgbgattr_to_color (int fg, int bg, int attr)
634 {
635 #ifdef HAVE_COLOR
636   if (fg != -1 && bg != -1)
637     return attr | mutt_alloc_color (fg, bg);
638   else
639 #endif
640     return attr;
641 }
642
643 /* usage: color <object> <fg> <bg> [ <regexp> ] 
644  *        mono  <object> <attr> [ <regexp> ]
645  */
646
647 static int
648 _mutt_parse_color (BUFFER * buf, BUFFER * s, BUFFER * err,
649                    parser_callback_t callback, short dry_run)
650 {
651   int object = 0, attr = 0, fg = 0, bg = 0, q_level = 0;
652   int r = 0;
653
654   if (parse_object (buf, s, &object, &q_level, err) == -1)
655     return -1;
656
657   if (callback (buf, s, &fg, &bg, &attr, err) == -1)
658     return -1;
659
660   /* extract a regular expression if needed */
661
662   if (object == MT_COLOR_HEADER || object == MT_COLOR_BODY
663       || object == MT_COLOR_INDEX) {
664     if (!MoreArgs (s)) {
665       strfcpy (err->data, _("too few arguments"), err->dsize);
666       return (-1);
667     }
668
669     mutt_extract_token (buf, s, 0);
670   }
671
672   if (MoreArgs (s)) {
673     strfcpy (err->data, _("too many arguments"), err->dsize);
674     return (-1);
675   }
676
677   /* dry run? */
678
679   if (dry_run)
680     return 0;
681
682
683 #ifdef HAVE_COLOR
684 # ifdef HAVE_USE_DEFAULT_COLORS
685   if (!option (OPTNOCURSES) && has_colors ()
686       /* delay use_default_colors() until needed, since it initializes things */
687       && (fg == COLOR_DEFAULT || bg == COLOR_DEFAULT)
688       && use_default_colors () != OK) {
689     strfcpy (err->data, _("default colors not supported"), err->dsize);
690     return (-1);
691   }
692 # endif /* HAVE_USE_DEFAULT_COLORS */
693 #endif
694
695   if (object == MT_COLOR_HEADER)
696     r = add_pattern (&ColorHdrList, buf->data, 0, fg, bg, attr, err, 0);
697   else if (object == MT_COLOR_BODY)
698     r = add_pattern (&ColorBodyList, buf->data, 1, fg, bg, attr, err, 0);
699   else if (object == MT_COLOR_INDEX) {
700     r = add_pattern (&ColorIndexList, buf->data, 1, fg, bg, attr, err, 1);
701     set_option (OPTFORCEREDRAWINDEX);
702   }
703   else if (object == MT_COLOR_QUOTED) {
704     if (q_level >= ColorQuoteSize) {
705       safe_realloc (&ColorQuote, (ColorQuoteSize += 2) * sizeof (int));
706       ColorQuote[ColorQuoteSize - 2] = ColorDefs[MT_COLOR_QUOTED];
707       ColorQuote[ColorQuoteSize - 1] = ColorDefs[MT_COLOR_QUOTED];
708     }
709     if (q_level >= ColorQuoteUsed)
710       ColorQuoteUsed = q_level + 1;
711     if (q_level == 0) {
712       ColorDefs[MT_COLOR_QUOTED] = fgbgattr_to_color (fg, bg, attr);
713
714       ColorQuote[0] = ColorDefs[MT_COLOR_QUOTED];
715       for (q_level = 1; q_level < ColorQuoteUsed; q_level++) {
716         if (ColorQuote[q_level] == A_NORMAL)
717           ColorQuote[q_level] = ColorDefs[MT_COLOR_QUOTED];
718       }
719     }
720     else
721       ColorQuote[q_level] = fgbgattr_to_color (fg, bg, attr);
722   }
723   else
724     ColorDefs[object] = fgbgattr_to_color (fg, bg, attr);
725
726 #ifdef HAVE_COLOR
727 # ifdef HAVE_BKGDSET
728   if (object == MT_COLOR_NORMAL && !option (OPTNOCURSES) && has_colors ())
729     BKGDSET (MT_COLOR_NORMAL);
730 # endif
731 #endif
732
733   return (r);
734 }
735
736 #ifdef HAVE_COLOR
737
738 int mutt_parse_color (BUFFER * buff, BUFFER * s, unsigned long data,
739                       BUFFER * err)
740 {
741   int dry_run = 0;
742
743   if (option (OPTNOCURSES) || !has_colors ())
744     dry_run = 1;
745
746   return _mutt_parse_color (buff, s, err, parse_color_pair, dry_run);
747 }
748
749 #endif
750
751 int mutt_parse_mono (BUFFER * buff, BUFFER * s, unsigned long data,
752                      BUFFER * err)
753 {
754   int dry_run = 0;
755
756 #ifdef HAVE_COLOR
757   if (option (OPTNOCURSES) || has_colors ())
758     dry_run = 1;
759 #else
760   if (option (OPTNOCURSES))
761     dry_run = 1;
762 #endif
763
764   return _mutt_parse_color (buff, s, err, parse_attr_spec, dry_run);
765 }