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