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