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