Added SILC Thread Queue API
[crypto.git] / apps / irssi / src / fe-common / core / themes.c
1 /*
2  themes.c : irssi
3
4     Copyright (C) 1999-2000 Timo Sirainen
5
6     This program is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     This program is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with this program; if not, write to the Free Software
18     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19 */
20
21 #include "module.h"
22 #include "module-formats.h"
23 #include "signals.h"
24 #include "commands.h"
25 #include "levels.h"
26 #include "misc.h"
27 #include "special-vars.h"
28 #include "lib-config/iconfig.h"
29 #include "settings.h"
30
31 #include "themes.h"
32 #include "printtext.h"
33
34 #include "default-theme.h"
35
36 GSList *themes;
37 THEME_REC *current_theme;
38 GHashTable *default_formats;
39
40 static int init_finished;
41 static char *init_errors;
42 static THEME_REC *internal_theme;
43
44 static int theme_read(THEME_REC *theme, const char *path);
45
46 THEME_REC *theme_create(const char *path, const char *name)
47 {
48         THEME_REC *rec;
49
50         g_return_val_if_fail(path != NULL, NULL);
51         g_return_val_if_fail(name != NULL, NULL);
52
53         rec = g_new0(THEME_REC, 1);
54         rec->refcount = 1;
55         rec->path = g_strdup(path);
56         rec->name = g_strdup(name);
57         rec->abstracts = g_hash_table_new((GHashFunc) g_str_hash,
58                                           (GCompareFunc) g_str_equal);
59         rec->modules = g_hash_table_new((GHashFunc) g_istr_hash,
60                                         (GCompareFunc) g_istr_equal);
61         themes = g_slist_append(themes, rec);
62         signal_emit("theme created", 1, rec);
63
64         return rec;
65 }
66
67 static void theme_abstract_destroy(char *key, char *value)
68 {
69         g_free(key);
70         g_free(value);
71 }
72
73 static void theme_module_destroy(const char *key, MODULE_THEME_REC *rec)
74 {
75         int n;
76
77         for (n = 0; n < rec->count; n++) {
78                 g_free_not_null(rec->formats[n]);
79                 g_free_not_null(rec->expanded_formats[n]);
80         }
81         g_free(rec->formats);
82         g_free(rec->expanded_formats);
83
84         g_free(rec->name);
85         g_free(rec);
86 }
87
88 static void theme_real_destroy(THEME_REC *rec)
89 {
90         g_hash_table_foreach(rec->abstracts, (GHFunc) theme_abstract_destroy, NULL);
91         g_hash_table_destroy(rec->abstracts);
92         g_hash_table_foreach(rec->modules, (GHFunc) theme_module_destroy, NULL);
93         g_hash_table_destroy(rec->modules);
94
95         g_slist_foreach(rec->replace_values, (GFunc) g_free, NULL);
96         g_slist_free(rec->replace_values);
97
98         g_free(rec->path);
99         g_free(rec->name);
100         g_free(rec);
101 }
102
103 static void theme_unref(THEME_REC *rec)
104 {
105         if (--rec->refcount == 0)
106                 theme_real_destroy(rec);
107 }
108
109 void theme_destroy(THEME_REC *rec)
110 {
111         themes = g_slist_remove(themes, rec);
112         signal_emit("theme destroyed", 1, rec);
113
114         theme_unref(rec);
115 }
116
117 static char *theme_replace_expand(THEME_REC *theme, int index,
118                                   char default_fg, char default_bg,
119                                   char *last_fg, char *last_bg,
120                                   char chr, int flags)
121 {
122         GSList *rec;
123         char *ret, *abstract, data[2];
124
125         rec = g_slist_nth(theme->replace_values, index);
126         g_return_val_if_fail(rec != NULL, NULL);
127
128         data[0] = chr; data[1] = '\0';
129
130         abstract = rec->data;
131         abstract = theme_format_expand_data(theme, (const char **) &abstract,
132                                             default_fg, default_bg,
133                                             last_fg, last_bg, flags);
134         ret = parse_special_string(abstract, NULL, NULL, data, NULL,
135                                    PARSE_FLAG_ONLY_ARGS);
136         g_free(abstract);
137         return ret;
138 }
139
140 static const char *fgcolorformats = "nkrgybmpcwKRGYBMPCW";
141 static const char *bgcolorformats = "n01234567";
142
143 #define IS_FGCOLOR_FORMAT(c) \
144         ((c) != '\0' && strchr(fgcolorformats, c) != NULL)
145 #define IS_BGCOLOR_FORMAT(c) \
146         ((c) != '\0' && strchr(bgcolorformats, c) != NULL)
147
148 /* append "variable" part in $variable, ie. not the contents of the variable */
149 static void theme_format_append_variable(GString *str, const char **format)
150 {
151         const char *orig;
152         char *value, *args[1] = { NULL };
153         int free_ret;
154
155         orig = *format;
156         (*format)++;
157
158         value = parse_special((char **) format, NULL, NULL,
159                               args, &free_ret, NULL, PARSE_FLAG_ONLY_ARGS);
160         if (free_ret) g_free(value);
161
162         if (**format != '\0')
163                 (*format)++;
164
165         /* append the variable name */
166         value = g_strndup(orig, (int) (*format-orig));
167         g_string_append(str, value);
168         g_free(value);
169 }
170
171 /* append next "item", either a character, $variable or %format */
172 static void theme_format_append_next(THEME_REC *theme, GString *str,
173                                      const char **format,
174                                      char default_fg, char default_bg,
175                                      char *last_fg, char *last_bg,
176                                      int flags)
177 {
178         int index;
179         unsigned char chr;
180
181         chr = **format;
182         if ((chr == '$' || chr == '%') &&
183             (*format)[1] == '\0') {
184                 /* last char, always append */
185                 g_string_append_c(str, chr);
186                 (*format)++;
187                 return;
188         }
189
190         if (chr == '$') {
191                 /* $variable .. we'll always need to skip this, since it
192                    may contain characters that are in replace chars. */
193                 theme_format_append_variable(str, format);
194                 return;
195         }
196
197         if (**format == '%') {
198                 /* format */
199                 (*format)++;
200                 if (**format != '{' && **format != '}') {
201                         chr = **format;
202                         if (**format == 'n') {
203                                 /* %n = change to default color */
204                                 g_string_append(str, "%n");
205
206                                 if (default_bg != 'n') {
207                                         g_string_append_c(str, '%');
208                                         g_string_append_c(str, default_bg);
209                                 }
210                                 if (default_fg != 'n') {
211                                         g_string_append_c(str, '%');
212                                         g_string_append_c(str, default_fg);
213                                 }
214
215                                 *last_fg = default_fg;
216                                 *last_bg = default_bg;
217                         } else {
218                                 if (IS_FGCOLOR_FORMAT(chr))
219                                         *last_fg = chr;
220                                 if (IS_BGCOLOR_FORMAT(chr))
221                                         *last_bg = chr;
222                                 g_string_append_c(str, '%');
223                                 g_string_append_c(str, chr);
224                         }
225                         (*format)++;
226                         return;
227                 }
228
229                 /* %{ or %} gives us { or } char - keep the % char
230                    though to make sure {} isn't treated as abstract */
231                 g_string_append_c(str, '%');
232                 chr = **format;
233         }
234
235         index = (flags & EXPAND_FLAG_IGNORE_REPLACES) ? -1 :
236                 theme->replace_keys[(int) (unsigned char) chr];
237         if (index == -1)
238                 g_string_append_c(str, chr);
239         else {
240                 char *value;
241
242                 value = theme_replace_expand(theme, index,
243                                              default_fg, default_bg,
244                                              last_fg, last_bg, chr, flags);
245                 g_string_append(str, value);
246                 g_free(value);
247         }
248
249         (*format)++;
250 }
251
252 /* returns TRUE if data is empty, or the data is a $variable which is empty */
253 static int data_is_empty(const char **data)
254 {
255         /* since we don't know the real argument list, assume there's always
256            an argument in them */
257         static char *arglist[] = {
258                 "x", "x", "x", "x", "x", "x","x", "x", "x", "x",
259                 NULL
260         };
261         SERVER_REC *server;
262         const char *p;
263         char *ret;
264         int free_ret, empty;
265
266         p = *data;
267         while (*p == ' ') p++;
268
269         if (*p == '}') {
270                 /* empty */
271                 *data = p+1;
272                 return TRUE;
273         }
274
275         if (*p != '$') {
276                 /* not empty */
277                 return FALSE;
278         }
279
280         /* variable - check if it's empty */
281         p++;
282
283         server = active_win == NULL ? NULL :
284                 active_win->active_server != NULL ?
285                 active_win->active_server : active_win->connect_server;
286
287         ret = parse_special((char **) &p, server,
288                             active_win == NULL ? NULL : active_win->active,
289                             arglist, &free_ret, NULL, 0);
290         p++;
291
292         while (*p == ' ') p++;
293         empty = *p == '}' && (ret == NULL || *ret == '\0');
294         if (free_ret) g_free(ret);
295
296         if (empty) {
297                 /* empty */
298                 *data = p+1;
299                 return TRUE;
300         }
301
302         return FALSE;
303 }
304
305 /* return "data" from {abstract data} string */
306 char *theme_format_expand_get(THEME_REC *theme, const char **format)
307 {
308         GString *str;
309         char *ret, dummy;
310         int braces = 1; /* we start with one brace opened */
311
312         str = g_string_new(NULL);
313         while (**format != '\0' && braces != 0) {
314                 if (**format == '{')
315                         braces++;
316                 else if (**format == '}')
317                         braces--;
318                 else if ((braces > 1) && (**format == ' ')) {
319                         g_string_append(str, "\\x20");
320                         (*format)++;
321                         continue;
322                 } else {
323                         theme_format_append_next(theme, str, format,
324                                                  'n', 'n',
325                                                  &dummy, &dummy, 0);
326                         continue;
327                 }
328                 
329                 if (braces == 0) {
330                         (*format)++;
331                         break;
332                 }
333
334                 g_string_append_c(str, **format);
335                 (*format)++;
336         }
337
338         ret = str->str;
339         g_string_free(str, FALSE);
340         return ret;
341 }
342
343 /* expand a single {abstract ...data... } */
344 static char *theme_format_expand_abstract(THEME_REC *theme,
345                                           const char **formatp,
346                                           char default_fg, char default_bg,
347                                           int flags)
348 {
349         GString *str;
350         const char *p, *format;
351         char *abstract, *data, *ret;
352         int len;
353
354         format = *formatp;
355
356         /* get abstract name first */
357         p = format;
358         while (*p != '\0' && *p != ' ' &&
359                *p != '{' && *p != '}') p++;
360         if (*p == '\0' || p == format)
361                 return NULL; /* error */
362
363         len = (int) (p-format);
364         abstract = g_strndup(format, len);
365
366         /* skip the following space, if there's any more spaces they're
367            treated as arguments */
368         if (*p == ' ') {
369                 len++;
370                 if ((flags & EXPAND_FLAG_IGNORE_EMPTY) && data_is_empty(&p)) {
371                         *formatp = p;
372                         g_free(abstract);
373                         return NULL;
374                 }
375         }
376         *formatp = format+len;
377
378         /* get the abstract data */
379         data = g_hash_table_lookup(theme->abstracts, abstract);
380         g_free(abstract);
381         if (data == NULL) {
382                 /* unknown abstract, just display the data */
383                 data = "$0-";
384         }
385         abstract = g_strdup(data);
386
387         /* we'll need to get the data part. it may contain
388            more abstracts, they are _NOT_ expanded. */
389         data = theme_format_expand_get(theme, formatp);
390         len = strlen(data);
391
392         if (len > 1 && i_isdigit(data[len-1]) && data[len-2] == '$') {
393                 /* ends with $<digit> .. this breaks things if next
394                    character is digit or '-' */
395                 char digit, *tmp;
396
397                 tmp = data;
398                 digit = tmp[len-1];
399                 tmp[len-1] = '\0';
400
401                 data = g_strdup_printf("%s{%c}", tmp, digit);
402                 g_free(tmp);
403         }
404
405         ret = parse_special_string(abstract, NULL, NULL, data, NULL,
406                                    PARSE_FLAG_ONLY_ARGS);
407         g_free(abstract);
408         g_free(data);
409         str = g_string_new(NULL);
410         p = ret;
411         while (*p != '\0') {
412                 if (*p == '\\') {
413                         int chr;
414                         p++;
415                         chr = expand_escape(&p);
416                         g_string_append_c(str, chr != -1 ? chr : *p);
417                 } else
418                         g_string_append_c(str, *p);
419                 p++;
420         }
421         g_free(ret);
422         abstract = str->str;
423         g_string_free(str, FALSE);
424
425         /* abstract may itself contain abstracts or replaces */
426         p = abstract;
427         ret = theme_format_expand_data(theme, &p, default_fg, default_bg,
428                                        &default_fg, &default_bg,
429                                        flags | EXPAND_FLAG_LASTCOLOR_ARG);
430         g_free(abstract);
431         return ret;
432 }
433
434 /* expand the data part in {abstract data} */
435 char *theme_format_expand_data(THEME_REC *theme, const char **format,
436                                char default_fg, char default_bg,
437                                char *save_last_fg, char *save_last_bg,
438                                int flags)
439 {
440         GString *str;
441         char *ret, *abstract;
442         char last_fg, last_bg;
443         int recurse_flags;
444
445         last_fg = default_fg;
446         last_bg = default_bg;
447         recurse_flags = flags & EXPAND_FLAG_RECURSIVE_MASK;
448
449         str = g_string_new(NULL);
450         while (**format != '\0') {
451                 if ((flags & EXPAND_FLAG_ROOT) == 0 && **format == '}') {
452                         /* ignore } if we're expanding original string */
453                         (*format)++;
454                         break;
455                 }
456
457                 if (**format != '{') {
458                         if ((flags & EXPAND_FLAG_LASTCOLOR_ARG) &&
459                             **format == '$' && (*format)[1] == '0') {
460                                 /* save the color before $0 ..
461                                    this is for the %n replacing */
462                                 if (save_last_fg != NULL) {
463                                         *save_last_fg = last_fg;
464                                         save_last_fg = NULL;
465                                 }
466                                 if (save_last_bg != NULL) {
467                                         *save_last_bg = last_bg;
468                                         save_last_bg = NULL;
469                                 }
470                         }
471
472                         theme_format_append_next(theme, str, format,
473                                                  default_fg, default_bg,
474                                                  &last_fg, &last_bg,
475                                                  recurse_flags);
476                         continue;
477                 }
478
479                 (*format)++;
480                 if (**format == '\0' || **format == '}')
481                         break; /* error */
482
483                 /* get a single {...} */
484                 abstract = theme_format_expand_abstract(theme, format,
485                                                         last_fg, last_bg,
486                                                         recurse_flags);
487                 if (abstract != NULL) {
488                         g_string_append(str, abstract);
489                         g_free(abstract);
490                 }
491         }
492
493         if ((flags & EXPAND_FLAG_LASTCOLOR_ARG) == 0) {
494                 /* save the last color */
495                 if (save_last_fg != NULL)
496                         *save_last_fg = last_fg;
497                 if (save_last_bg != NULL)
498                         *save_last_bg = last_bg;
499         }
500
501         ret = str->str;
502         g_string_free(str, FALSE);
503         return ret;
504 }
505
506 #define IS_OLD_FORMAT(code, last_fg, last_bg) \
507         (((code) == 'n' && (last_fg) == 'n' && (last_bg) == 'n') || \
508         ((code) != 'n' && ((code) == (last_fg) || (code) == (last_bg))))
509
510 static char *theme_format_compress_colors(THEME_REC *theme, const char *format)
511 {
512         GString *str;
513         char *ret, last_fg, last_bg;
514
515         str = g_string_new(NULL);
516
517         last_fg = last_bg = '\0';
518         while (*format != '\0') {
519                 if (*format == '$') {
520                         /* $variable, skrip it entirely */
521                         theme_format_append_variable(str, &format);
522                         last_fg = last_bg = '\0';
523                 } else if (*format != '%') {
524                         /* a normal character */
525                         g_string_append_c(str, *format);
526                         format++;
527                 } else {
528                         /* %format */
529                         format++;
530                         if (IS_OLD_FORMAT(*format, last_fg, last_bg)) {
531                                 /* active color set again */
532                         } else if (IS_FGCOLOR_FORMAT(*format) &&
533                                    format[1] == '%' &&
534                                    IS_FGCOLOR_FORMAT(format[2]) &&
535                                    (*format != 'n' || format[2] == 'n')) {
536                                 /* two fg colors in a row. bg colors are
537                                    so rare that we don't bother checking
538                                    them */
539                         } else {
540                                 /* some format, add it */
541                                 g_string_append_c(str, '%');
542                                 g_string_append_c(str, *format);
543
544                                 if (IS_FGCOLOR_FORMAT(*format))
545                                         last_fg = *format;
546                                 if (IS_BGCOLOR_FORMAT(*format))
547                                         last_bg = *format;
548                         }
549                         format++;
550                 }
551         }
552
553         ret = str->str;
554         g_string_free(str, FALSE);
555         return ret;
556 }
557
558 char *theme_format_expand(THEME_REC *theme, const char *format)
559 {
560         char *data, *ret;
561
562         g_return_val_if_fail(theme != NULL, NULL);
563         g_return_val_if_fail(format != NULL, NULL);
564
565         data = theme_format_expand_data(theme, &format, 'n', 'n', NULL, NULL,
566                                         EXPAND_FLAG_ROOT);
567         ret = theme_format_compress_colors(theme, data);
568         g_free(data);
569         return ret;
570 }
571
572 static MODULE_THEME_REC *theme_module_create(THEME_REC *theme, const char *module)
573 {
574         MODULE_THEME_REC *rec;
575         FORMAT_REC *formats;
576
577         rec = g_hash_table_lookup(theme->modules, module);
578         if (rec != NULL) return rec;
579
580         formats = g_hash_table_lookup(default_formats, module);
581         g_return_val_if_fail(formats != NULL, NULL);
582
583         rec = g_new0(MODULE_THEME_REC, 1);
584         rec->name = g_strdup(module);
585
586         for (rec->count = 0; formats[rec->count].def != NULL; rec->count++) ;
587         rec->formats = g_new0(char *, rec->count);
588         rec->expanded_formats = g_new0(char *, rec->count);
589
590         g_hash_table_insert(theme->modules, rec->name, rec);
591         return rec;
592 }
593
594 static void theme_read_replaces(CONFIG_REC *config, THEME_REC *theme)
595 {
596         GSList *tmp;
597         CONFIG_NODE *node;
598         const char *p;
599         int index;
600
601         /* reset replace keys */
602         for (index = 0; index < 256; index++)
603                 theme->replace_keys[index] = -1;
604         index = 0;
605
606         node = config_node_traverse(config, "replaces", FALSE);
607         if (node == NULL || node->type !=  NODE_TYPE_BLOCK) return;
608
609         for (tmp = node->value; tmp != NULL; tmp = tmp->next) {
610                 node = tmp->data;
611
612                 if (node->key != NULL && node->value != NULL) {
613                         for (p = node->key; *p != '\0'; p++)
614                                 theme->replace_keys[(int) (unsigned char) *p] = index;
615
616                         theme->replace_values =
617                                 g_slist_append(theme->replace_values,
618                                                g_strdup(node->value));
619                         index++;
620                 }
621         }
622 }
623
624 static void theme_read_abstracts(CONFIG_REC *config, THEME_REC *theme)
625 {
626         GSList *tmp;
627         CONFIG_NODE *node;
628         gpointer oldkey, oldvalue;
629
630         node = config_node_traverse(config, "abstracts", FALSE);
631         if (node == NULL || node->type !=  NODE_TYPE_BLOCK) return;
632
633         for (tmp = node->value; tmp != NULL; tmp = tmp->next) {
634                 node = tmp->data;
635
636                 if (node->key == NULL || node->value == NULL)
637                         continue;
638
639                 if (g_hash_table_lookup_extended(theme->abstracts, node->key,
640                                                  &oldkey, &oldvalue)) {
641                         /* new values override old ones */
642                         g_hash_table_remove(theme->abstracts, oldkey);
643                         g_free(oldkey);
644                         g_free(oldvalue);
645                 }
646
647                 g_hash_table_insert(theme->abstracts, g_strdup(node->key),
648                                     g_strdup(node->value));
649         }
650 }
651
652 static void theme_set_format(THEME_REC *theme, MODULE_THEME_REC *rec,
653                              const char *module,
654                              const char *key, const char *value)
655 {
656         int num;
657
658         num = format_find_tag(module, key);
659         if (num != -1) {
660                 rec->formats[num] = g_strdup(value);
661                 rec->expanded_formats[num] = theme_format_expand(theme, value);
662         }
663 }
664
665 static void theme_read_formats(THEME_REC *theme, const char *module,
666                                CONFIG_REC *config, MODULE_THEME_REC *rec)
667 {
668         CONFIG_NODE *node;
669         GSList *tmp;
670
671         node = config_node_traverse(config, "formats", FALSE);
672         if (node == NULL) return;
673         node = config_node_section(node, module, -1);
674         if (node == NULL) return;
675
676         for (tmp = node->value; tmp != NULL; tmp = tmp->next) {
677                 node = tmp->data;
678
679                 if (node->key != NULL && node->value != NULL) {
680                         theme_set_format(theme, rec, module,
681                                          node->key, node->value);
682                 }
683         }
684 }
685
686 static void theme_init_module(THEME_REC *theme, const char *module,
687                               CONFIG_REC *config)
688 {
689         MODULE_THEME_REC *rec;
690         FORMAT_REC *formats;
691         int n;
692
693         formats = g_hash_table_lookup(default_formats, module);
694         g_return_if_fail(formats != NULL);
695
696         rec = theme_module_create(theme, module);
697
698         if (config != NULL)
699                 theme_read_formats(theme, module, config, rec);
700
701         /* expand the remaining formats */
702         for (n = 0; n < rec->count; n++) {
703                 if (rec->expanded_formats[n] == NULL) {
704                         rec->expanded_formats[n] =
705                                 theme_format_expand(theme, formats[n].def);
706                 }
707         }
708 }
709
710 static void sig_print_errors(void)
711 {
712         init_finished = TRUE;
713
714         if (init_errors != NULL) {
715                 signal_emit("gui dialog", 2, "error", init_errors);
716                 g_free(init_errors);
717         }
718 }
719
720 static void theme_read_module(THEME_REC *theme, const char *module)
721 {
722         CONFIG_REC *config;
723
724         config = config_open(theme->path, -1);
725         if (config != NULL)
726                 config_parse(config);
727
728         theme_init_module(theme, module, config);
729
730         if (config != NULL) config_close(config);
731 }
732
733 static void themes_read_module(const char *module)
734 {
735         g_slist_foreach(themes, (GFunc) theme_read_module, (void *) module);
736 }
737
738 static void theme_remove_module(THEME_REC *theme, const char *module)
739 {
740         MODULE_THEME_REC *rec;
741
742         rec = g_hash_table_lookup(theme->modules, module);
743         if (rec == NULL) return;
744
745         g_hash_table_remove(theme->modules, module);
746         theme_module_destroy(module, rec);
747 }
748
749 static void themes_remove_module(const char *module)
750 {
751         g_slist_foreach(themes, (GFunc) theme_remove_module, (void *) module);
752 }
753
754 void theme_register_module(const char *module, FORMAT_REC *formats)
755 {
756         if (g_hash_table_lookup(default_formats, module) != NULL)
757                 return;
758
759         g_hash_table_insert(default_formats, g_strdup(module), formats);
760         themes_read_module(module);
761 }
762
763 void theme_unregister_module(const char *module)
764 {
765         gpointer key, value;
766
767         if (default_formats == NULL)
768                 return; /* already uninitialized */
769
770         if (!g_hash_table_lookup_extended(default_formats, module, &key, &value))
771                 return;
772
773         g_hash_table_remove(default_formats, key);
774         g_free(key);
775
776         themes_remove_module(module);
777 }
778
779 void theme_set_default_abstract(const char *key, const char *value)
780 {
781         gpointer oldkey, oldvalue;
782
783         if (g_hash_table_lookup_extended(internal_theme->abstracts, key,
784                                          &oldkey, &oldvalue)) {
785                 /* new values override old ones */
786                 g_hash_table_remove(internal_theme->abstracts, oldkey);
787                 g_free(oldkey);
788                 g_free(oldvalue);
789         }
790
791         g_hash_table_insert(internal_theme->abstracts,
792                             g_strdup(key), g_strdup(value));
793 }
794
795 static THEME_REC *theme_find(const char *name)
796 {
797         GSList *tmp;
798
799         for (tmp = themes; tmp != NULL; tmp = tmp->next) {
800                 THEME_REC *rec = tmp->data;
801
802                 if (g_strcasecmp(rec->name, name) == 0)
803                         return rec;
804         }
805
806         return NULL;
807 }
808
809 static void window_themes_update(void)
810 {
811         GSList *tmp;
812
813         for (tmp = windows; tmp != NULL; tmp = tmp->next) {
814                 WINDOW_REC *rec = tmp->data;
815
816                 if (rec->theme_name != NULL)
817                         rec->theme = theme_load(rec->theme_name);
818         }
819 }
820
821 THEME_REC *theme_load(const char *setname)
822 {
823         THEME_REC *theme, *oldtheme;
824         struct stat statbuf;
825         char *fname, *name, *p;
826
827         name = g_strdup(setname);
828         p = strrchr(name, '.');
829         if (p != NULL && strcmp(p, ".theme") == 0) {
830                 /* remove the trailing .theme */
831                 *p = '\0';
832         }
833
834         theme = theme_find(name);
835
836         /* check home dir */
837         fname = g_strdup_printf("%s/%s.theme", get_irssi_dir(), name);
838         if (stat(fname, &statbuf) != 0) {
839                 /* check global config dir */
840                 g_free(fname);
841                 fname = g_strdup_printf(THEMESDIR"/%s.theme", name);
842                 if (stat(fname, &statbuf) != 0) {
843                         /* theme not found */
844                         g_free(fname);
845                         g_free(name);
846                         return theme; /* use the one in memory if possible */
847                 }
848         }
849
850         if (theme != NULL && theme->last_modify == statbuf.st_mtime) {
851                 /* theme not modified, use the one already in memory */
852                 g_free(fname);
853                 g_free(name);
854                 return theme;
855         }
856
857         oldtheme = theme;
858         theme = theme_create(fname, name);
859         theme->last_modify = statbuf.st_mtime;
860         if (!theme_read(theme, theme->path)) {
861                 /* error reading .theme file */
862                 theme_destroy(theme);
863                 theme = NULL;
864         }
865
866         if (oldtheme != NULL && theme != NULL) {
867                 theme_destroy(oldtheme);
868                 if (current_theme == oldtheme)
869                         current_theme = theme;
870                 window_themes_update();
871         }
872
873         g_free(fname);
874         g_free(name);
875         return theme;
876 }
877
878 static void copy_abstract_hash(char *key, char *value, GHashTable *dest)
879 {
880         g_hash_table_insert(dest, g_strdup(key), g_strdup(value));
881 }
882
883 static void theme_copy_abstracts(THEME_REC *dest, THEME_REC *src)
884 {
885         g_hash_table_foreach(src->abstracts, (GHFunc) copy_abstract_hash,
886                              dest->abstracts);
887 }
888
889 typedef struct {
890         THEME_REC *theme;
891         CONFIG_REC *config;
892 } THEME_READ_REC;
893
894 static void theme_read_modules(const char *module, void *value,
895                                THEME_READ_REC *rec)
896 {
897         theme_init_module(rec->theme, module, rec->config);
898 }
899
900 static void read_error(const char *str)
901 {
902         char *old;
903
904         if (init_finished)
905                 printtext(NULL, NULL, MSGLEVEL_CLIENTERROR, "%s", str);
906         else if (init_errors == NULL)
907                 init_errors = g_strdup(str);
908         else {
909                 old = init_errors;
910                 init_errors = g_strconcat(init_errors, "\n", str, NULL);
911                 g_free(old);
912         }
913 }
914
915 static int theme_read(THEME_REC *theme, const char *path)
916 {
917         CONFIG_REC *config;
918         THEME_READ_REC rec;
919         char *str;
920
921         config = config_open(path, -1) ;
922         if (config == NULL) {
923                 /* didn't exist or no access? */
924                 str = g_strdup_printf("Error reading theme file %s: %s",
925                                       path, g_strerror(errno));
926                 read_error(str);
927                 g_free(str);
928                 return FALSE;
929         }
930
931         if (path == NULL)
932                 config_parse_data(config, default_theme, "internal");
933         else
934                 config_parse(config);
935
936         if (config_last_error(config) != NULL) {
937                 str = g_strdup_printf("Ignored errors in theme %s:\n%s",
938                                       theme->name, config_last_error(config));
939                 read_error(str);
940                 g_free(str);
941         }
942
943         theme->default_color =
944                 config_get_int(config, NULL, "default_color", -1);
945         theme->info_eol = config_get_bool(config, NULL, "info_eol", FALSE);
946
947         /* FIXME: remove after 0.7.99 */
948         if (theme->default_color == 0 &&
949             config_get_int(config, NULL, "default_real_color", -1) != -1)
950                 theme->default_color = -1;
951         theme_read_replaces(config, theme);
952
953         if (path != NULL)
954                 theme_copy_abstracts(theme, internal_theme);
955         theme_read_abstracts(config, theme);
956
957         rec.theme = theme;
958         rec.config = config;
959         g_hash_table_foreach(default_formats,
960                              (GHFunc) theme_read_modules, &rec);
961         config_close(config);
962
963         return TRUE;
964 }
965
966 typedef struct {
967         char *name;
968         char *short_name;
969 } THEME_SEARCH_REC;
970
971 static int theme_search_equal(THEME_SEARCH_REC *r1, THEME_SEARCH_REC *r2)
972 {
973         return g_strcasecmp(r1->short_name, r2->short_name);
974 }
975
976 static void theme_get_modules(char *module, FORMAT_REC *formats, GSList **list)
977 {
978         THEME_SEARCH_REC *rec;
979
980         rec = g_new(THEME_SEARCH_REC, 1);
981         rec->name = module;
982         rec->short_name = strrchr(module, '/');
983         if (rec->short_name != NULL)
984                 rec->short_name++; else rec->short_name = module;
985         *list = g_slist_insert_sorted(*list, rec, (GCompareFunc) theme_search_equal);
986 }
987
988 static GSList *get_sorted_modules(void)
989 {
990         GSList *list;
991
992         list = NULL;
993         g_hash_table_foreach(default_formats, (GHFunc) theme_get_modules, &list);
994         return list;
995 }
996
997 static THEME_SEARCH_REC *theme_search(GSList *list, const char *module)
998 {
999         THEME_SEARCH_REC *rec;
1000
1001         while (list != NULL) {
1002                 rec = list->data;
1003
1004                 if (g_strcasecmp(rec->short_name, module) == 0)
1005                         return rec;
1006                 list = list->next;
1007         }
1008
1009         return NULL;
1010 }
1011
1012 static void theme_show(THEME_SEARCH_REC *rec, const char *key, const char *value, int reset)
1013 {
1014         MODULE_THEME_REC *theme;
1015         FORMAT_REC *formats;
1016         const char *text, *last_title;
1017         int n, first;
1018
1019         formats = g_hash_table_lookup(default_formats, rec->name);
1020         theme = g_hash_table_lookup(current_theme->modules, rec->name);
1021
1022         last_title = NULL; first = TRUE;
1023         for (n = 1; formats[n].def != NULL; n++) {
1024                 text = theme != NULL && theme->formats[n] != NULL ?
1025                         theme->formats[n] : formats[n].def;
1026
1027                 if (formats[n].tag == NULL)
1028                         last_title = text;
1029                 else if ((value != NULL && key != NULL && g_strcasecmp(formats[n].tag, key) == 0) ||
1030                          (value == NULL && (key == NULL || stristr(formats[n].tag, key) != NULL))) {
1031                         if (first) {
1032                                 printformat(NULL, NULL, MSGLEVEL_CLIENTCRAP, TXT_FORMAT_TITLE, rec->short_name, formats[0].def);
1033                                 first = FALSE;
1034                         }
1035                         if (last_title != NULL)
1036                                 printformat(NULL, NULL, MSGLEVEL_CLIENTCRAP, TXT_FORMAT_SUBTITLE, last_title);
1037                         if (reset || value != NULL) {
1038                                 theme = theme_module_create(current_theme, rec->name);
1039                                 g_free_not_null(theme->formats[n]);
1040                                 g_free_not_null(theme->expanded_formats[n]);
1041
1042                                 text = reset ? formats[n].def : value;
1043                                 theme->formats[n] = reset ? NULL : g_strdup(value);
1044                                 theme->expanded_formats[n] = theme_format_expand(current_theme, text);
1045                         }
1046                         printformat(NULL, NULL, MSGLEVEL_CLIENTCRAP, TXT_FORMAT_ITEM, formats[n].tag, text);
1047                         last_title = NULL;
1048                 }
1049         }
1050 }
1051
1052 /* SYNTAX: FORMAT [-delete | -reset] [<module>] [<key> [<value>]] */
1053 static void cmd_format(const char *data)
1054 {
1055         GHashTable *optlist;
1056         GSList *tmp, *modules;
1057         char *module, *key, *value;
1058         void *free_arg;
1059         int reset;
1060
1061         if (!cmd_get_params(data, &free_arg, 3 | PARAM_FLAG_GETREST | PARAM_FLAG_OPTIONS,
1062                             "format", &optlist, &module, &key, &value))
1063                 return;
1064
1065         modules = get_sorted_modules();
1066         if (*module == '\0')
1067                 module = NULL;
1068         else if (theme_search(modules, module) == NULL) {
1069                 /* first argument isn't module.. */
1070                 cmd_params_free(free_arg);
1071                 if (!cmd_get_params(data, &free_arg, 2 | PARAM_FLAG_GETREST | PARAM_FLAG_OPTIONS,
1072                                     "format", &optlist, &key, &value))
1073                         return;
1074                 module = NULL;
1075         }
1076
1077         reset = FALSE;
1078         if (*key == '\0') key = NULL;
1079         if (g_hash_table_lookup(optlist, "reset"))
1080                 reset = TRUE;
1081         else if (g_hash_table_lookup(optlist, "delete"))
1082                 value = "";
1083         else if (*value == '\0')
1084                 value = NULL;
1085
1086         for (tmp = modules; tmp != NULL; tmp = tmp->next) {
1087                 THEME_SEARCH_REC *rec = tmp->data;
1088
1089                 if (module == NULL || g_strcasecmp(rec->short_name, module) == 0)
1090                         theme_show(rec, key, value, reset);
1091         }
1092         g_slist_foreach(modules, (GFunc) g_free, NULL);
1093         g_slist_free(modules);
1094
1095         cmd_params_free(free_arg);
1096 }
1097
1098 typedef struct {
1099         CONFIG_REC *config;
1100         int save_all;
1101 } THEME_SAVE_REC;
1102
1103 static void module_save(const char *module, MODULE_THEME_REC *rec,
1104                         THEME_SAVE_REC *data)
1105 {
1106         CONFIG_NODE *fnode, *node;
1107         FORMAT_REC *formats;
1108         int n;
1109
1110         formats = g_hash_table_lookup(default_formats, rec->name);
1111         if (formats == NULL) return;
1112
1113         fnode = config_node_traverse(data->config, "formats", TRUE);
1114
1115         node = config_node_section(fnode, rec->name, NODE_TYPE_BLOCK);
1116         for (n = 1; formats[n].def != NULL; n++) {
1117                 if (rec->formats[n] != NULL) {
1118                         config_node_set_str(data->config, node, formats[n].tag,
1119                                             rec->formats[n]);
1120                 } else if (data->save_all && formats[n].tag != NULL) {
1121                         config_node_set_str(data->config, node, formats[n].tag,
1122                                             formats[n].def);
1123                 }
1124         }
1125
1126         if (node->value == NULL) {
1127                 /* not modified, don't keep the empty section */
1128                 config_node_remove(data->config, fnode, node);
1129                 if (fnode->value == NULL) {
1130                         config_node_remove(data->config,
1131                                            data->config->mainnode, fnode);
1132                 }
1133         }
1134 }
1135
1136 static void theme_save(THEME_REC *theme, int save_all)
1137 {
1138         CONFIG_REC *config;
1139         THEME_SAVE_REC data;
1140         char *path;
1141         int ok;
1142
1143         config = config_open(theme->path, -1);
1144         if (config != NULL)
1145                 config_parse(config);
1146         else {
1147                 if (g_strcasecmp(theme->name, "default") == 0) {
1148                         config = config_open(NULL, -1);
1149                         config_parse_data(config, default_theme, "internal");
1150                         config_change_file_name(config, theme->path, 0660);
1151                 } else {
1152                         config = config_open(theme->path, 0660);
1153                         if (config == NULL)
1154                                 return;
1155                         config_parse(config);
1156                 }
1157         }
1158
1159         data.config = config;
1160         data.save_all = save_all;
1161         g_hash_table_foreach(theme->modules, (GHFunc) module_save, &data);
1162
1163         /* always save the theme to ~/.irssi/ */
1164         path = g_strdup_printf("%s/%s", get_irssi_dir(),
1165                                g_basename(theme->path));
1166         ok = config_write(config, path, 0660) == 0;
1167
1168         printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE,
1169                     ok ? TXT_THEME_SAVED : TXT_THEME_SAVE_FAILED,
1170                     path, config_last_error(config));
1171
1172         g_free(path);
1173         config_close(config);
1174 }
1175
1176 /* save changed formats, -format saves all */
1177 static void cmd_save(const char *data)
1178 {
1179         GSList *tmp;
1180         GHashTable *optlist;
1181         void *free_arg;
1182         char *fname;
1183         int saveall;
1184
1185         if (!cmd_get_params(data, &free_arg, 1 | PARAM_FLAG_OPTIONS,
1186                             "save", &optlist, &fname))
1187                 return;
1188
1189         saveall = g_hash_table_lookup(optlist, "formats") != NULL;
1190         for (tmp = themes; tmp != NULL; tmp = tmp->next) {
1191                 THEME_REC *theme = tmp->data;
1192
1193                 theme_save(theme, saveall);
1194         }
1195
1196         cmd_params_free(free_arg);
1197 }
1198
1199 static void complete_format_list(THEME_SEARCH_REC *rec, const char *key, GList **list)
1200 {
1201         FORMAT_REC *formats;
1202         int n, len;
1203
1204         formats = g_hash_table_lookup(default_formats, rec->name);
1205
1206         len = strlen(key);
1207         for (n = 1; formats[n].def != NULL; n++) {
1208                 const char *item = formats[n].tag;
1209
1210                 if (item != NULL && g_strncasecmp(item, key, len) == 0)
1211                         *list = g_list_append(*list, g_strdup(item));
1212         }
1213 }
1214
1215 static GList *completion_get_formats(const char *module, const char *key)
1216 {
1217         GSList *modules, *tmp;
1218         GList *list;
1219
1220         g_return_val_if_fail(key != NULL, NULL);
1221
1222         list = NULL;
1223
1224         modules = get_sorted_modules();
1225         if (*module == '\0' || theme_search(modules, module) != NULL) {
1226                 for (tmp = modules; tmp != NULL; tmp = tmp->next) {
1227                         THEME_SEARCH_REC *rec = tmp->data;
1228
1229                         if (*module == '\0' || g_strcasecmp(rec->short_name, module) == 0)
1230                                 complete_format_list(rec, key, &list);
1231                 }
1232         }
1233         g_slist_foreach(modules, (GFunc) g_free, NULL);
1234         g_slist_free(modules);
1235
1236         return list;
1237 }
1238
1239 static void sig_complete_format(GList **list, WINDOW_REC *window,
1240                                 const char *word, const char *line, int *want_space)
1241 {
1242         const char *ptr;
1243         int words;
1244
1245         g_return_if_fail(list != NULL);
1246         g_return_if_fail(word != NULL);
1247         g_return_if_fail(line != NULL);
1248
1249         ptr = line;
1250
1251         words = 0;
1252         if (*ptr != '\0') {
1253                 do {
1254                         ptr++;
1255                         words++;
1256                         ptr = strchr(ptr, ' ');
1257                 } while (ptr != NULL);
1258         }
1259
1260         if (words > 2)
1261                 return;
1262
1263         *list = completion_get_formats(line, word);
1264         if (*list != NULL) signal_stop();
1265 }
1266
1267 static void change_theme(const char *name, int verbose)
1268 {
1269         THEME_REC *rec;
1270
1271         rec = theme_load(name);
1272         if (rec != NULL) {
1273                 current_theme = rec;
1274                 signal_emit("theme changed", 1, rec);
1275
1276                 if (verbose) {
1277                         printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE,
1278                                     TXT_THEME_CHANGED,
1279                                     rec->name, rec->path);
1280                 }
1281         } else if (verbose) {
1282                 printformat(NULL, NULL, MSGLEVEL_CLIENTERROR,
1283                             TXT_THEME_NOT_FOUND, name);
1284         }
1285 }
1286
1287 static void read_settings(void)
1288 {
1289         const char *theme;
1290         int len;
1291
1292         theme = settings_get_str("theme");
1293         len = strlen(current_theme->name);
1294         if (strcmp(current_theme->name, theme) != 0 &&
1295             (strncmp(current_theme->name, theme, len) != 0 ||
1296              strcmp(theme+len, ".theme") != 0))
1297                 change_theme(theme, TRUE);
1298 }
1299
1300 void themes_reload(void)
1301 {
1302         GSList *refs;
1303         char *fname;
1304
1305         /* increase every theme's refcount, and destroy them. this way if
1306            we want to use the theme before it's reloaded we don't crash. */
1307         refs = NULL;
1308         while (themes != NULL) {
1309                 THEME_REC *theme = themes->data;
1310
1311                 refs = g_slist_prepend(refs, theme);
1312
1313                 theme->refcount++;
1314                 theme_destroy(theme);
1315         }
1316
1317         /* first there's default theme.. */
1318         current_theme = theme_load("default");
1319         if (current_theme == NULL) {
1320                 fname = g_strdup_printf("%s/default.theme", get_irssi_dir());
1321                 current_theme = theme_create(fname, "default");
1322                 current_theme->default_color = -1;
1323                 theme_read(current_theme, NULL);
1324                 g_free(fname);
1325         }
1326
1327         window_themes_update();
1328         change_theme(settings_get_str("theme"), FALSE);
1329
1330         while (refs != NULL) {
1331                 theme_unref(refs->data);
1332                 refs = g_slist_remove(refs, refs->data);
1333         }
1334 }
1335
1336 static THEME_REC *read_internal_theme(void)
1337 {
1338         CONFIG_REC *config;
1339         THEME_REC *theme;
1340
1341         theme = theme_create("internal", "_internal");
1342
1343         config = config_open(NULL, -1);
1344         config_parse_data(config, default_theme, "internal");
1345         theme_read_abstracts(config, theme);
1346         config_close(config);
1347
1348         return theme;
1349 }
1350
1351 void themes_init(void)
1352 {
1353         settings_add_str("lookandfeel", "theme", "default");
1354
1355         default_formats = g_hash_table_new((GHashFunc) g_str_hash,
1356                                            (GCompareFunc) g_str_equal);
1357         internal_theme = read_internal_theme();
1358
1359         init_finished = FALSE;
1360         init_errors = NULL;
1361
1362         themes = NULL;
1363         themes_reload();
1364
1365         command_bind("format", NULL, (SIGNAL_FUNC) cmd_format);
1366         command_bind("save", NULL, (SIGNAL_FUNC) cmd_save);
1367         signal_add("complete command format", (SIGNAL_FUNC) sig_complete_format);
1368         signal_add("irssi init finished", (SIGNAL_FUNC) sig_print_errors);
1369         signal_add("setup changed", (SIGNAL_FUNC) read_settings);
1370         signal_add("setup reread", (SIGNAL_FUNC) themes_reload);
1371
1372         command_set_options("format", "delete reset");
1373         command_set_options("save", "formats");
1374 }
1375
1376 void themes_deinit(void)
1377 {
1378         while (themes != NULL)
1379                 theme_destroy(themes->data);
1380         theme_destroy(internal_theme);
1381
1382         g_hash_table_destroy(default_formats);
1383         default_formats = NULL;
1384
1385         command_unbind("format", (SIGNAL_FUNC) cmd_format);
1386         command_unbind("save", (SIGNAL_FUNC) cmd_save);
1387         signal_remove("complete command format", (SIGNAL_FUNC) sig_complete_format);
1388         signal_remove("irssi init finished", (SIGNAL_FUNC) sig_print_errors);
1389         signal_remove("setup changed", (SIGNAL_FUNC) read_settings);
1390         signal_remove("setup reread", (SIGNAL_FUNC) themes_reload);
1391 }