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