/* themes.c : irssi Copyright (C) 1999-2000 Timo Sirainen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "module.h" #include "module-formats.h" #include "signals.h" #include "commands.h" #include "levels.h" #include "misc.h" #include "special-vars.h" #include "lib-config/iconfig.h" #include "settings.h" #include "themes.h" #include "printtext.h" #include "default-theme.h" GSList *themes; THEME_REC *current_theme; GHashTable *default_formats; static int init_finished; static char *init_errors; static THEME_REC *internal_theme; static int theme_read(THEME_REC *theme, const char *path); THEME_REC *theme_create(const char *path, const char *name) { THEME_REC *rec; g_return_val_if_fail(path != NULL, NULL); g_return_val_if_fail(name != NULL, NULL); rec = g_new0(THEME_REC, 1); rec->refcount = 1; rec->path = g_strdup(path); rec->name = g_strdup(name); rec->abstracts = g_hash_table_new((GHashFunc) g_str_hash, (GCompareFunc) g_str_equal); rec->modules = g_hash_table_new((GHashFunc) g_istr_hash, (GCompareFunc) g_istr_equal); themes = g_slist_append(themes, rec); signal_emit("theme created", 1, rec); return rec; } static void theme_abstract_destroy(char *key, char *value) { g_free(key); g_free(value); } static void theme_module_destroy(const char *key, MODULE_THEME_REC *rec) { int n; for (n = 0; n < rec->count; n++) { g_free_not_null(rec->formats[n]); g_free_not_null(rec->expanded_formats[n]); } g_free(rec->formats); g_free(rec->expanded_formats); g_free(rec->name); g_free(rec); } static void theme_real_destroy(THEME_REC *rec) { g_hash_table_foreach(rec->abstracts, (GHFunc) theme_abstract_destroy, NULL); g_hash_table_destroy(rec->abstracts); g_hash_table_foreach(rec->modules, (GHFunc) theme_module_destroy, NULL); g_hash_table_destroy(rec->modules); g_slist_foreach(rec->replace_values, (GFunc) g_free, NULL); g_slist_free(rec->replace_values); g_free(rec->path); g_free(rec->name); g_free(rec); } static void theme_unref(THEME_REC *rec) { if (--rec->refcount == 0) theme_real_destroy(rec); } void theme_destroy(THEME_REC *rec) { themes = g_slist_remove(themes, rec); signal_emit("theme destroyed", 1, rec); theme_unref(rec); } static char *theme_replace_expand(THEME_REC *theme, int index, theme_rm_col default_fg, theme_rm_col default_bg, theme_rm_col *last_fg, theme_rm_col *last_bg, char chr, int flags) { GSList *rec; char *ret, *abstract, data[2]; rec = g_slist_nth(theme->replace_values, index); g_return_val_if_fail(rec != NULL, NULL); data[0] = chr; data[1] = '\0'; abstract = rec->data; abstract = theme_format_expand_data(theme, (const char **) &abstract, default_fg, default_bg, last_fg, last_bg, (flags | EXPAND_FLAG_IGNORE_REPLACES)); ret = parse_special_string(abstract, NULL, NULL, data, NULL, PARSE_FLAG_ONLY_ARGS); g_free(abstract); return ret; } static const char *fgcolorformats = "nkrgybmpcwKRGYBMPCW"; static const char *bgcolorformats = "n01234567"; #define IS_FGCOLOR_FORMAT(c) \ ((c) != '\0' && strchr(fgcolorformats, c) != NULL) #define IS_BGCOLOR_FORMAT(c) \ ((c) != '\0' && strchr(bgcolorformats, c) != NULL) /* append "variable" part in $variable, ie. not the contents of the variable */ static void theme_format_append_variable(GString *str, const char **format) { const char *orig; char *value, *args[1] = { NULL }; int free_ret; orig = *format; (*format)++; value = parse_special((char **) format, NULL, NULL, args, &free_ret, NULL, PARSE_FLAG_ONLY_ARGS); if (free_ret) g_free(value); if (**format != '\0') (*format)++; /* append the variable name */ value = g_strndup(orig, (int) (*format-orig)); g_string_append(str, value); g_free(value); } static inline int chr_is_valid_rgb(const char format[]) { int tmp; for (tmp = 1; tmp < 7; ++tmp) { if (!isxdigit(format[tmp])) return tmp; } return 0; } static inline int chr_is_valid_ext(const char format[]) { if (format[1] < '0' || format[1] > '7') return 1; if (format[1] == '7') { if (!isalpha(format[2]) || format[2] == 'y' || format[2] == 'Y' || format[2] =='z' || format[2] == 'Z') return 2; } else if (format[1] == '0') { if (!isxdigit(format[2])) return 2; } else if (!isalnum(format[2])) return 2; return 0; } /* append next "item", either a character, $variable or %format */ static void theme_format_append_next(THEME_REC *theme, GString *str, const char **format, theme_rm_col default_fg, theme_rm_col default_bg, theme_rm_col *last_fg, theme_rm_col *last_bg, int flags) { int index; unsigned char chr; char *t; chr = **format; if ((chr == '$' || chr == '%') && (*format)[1] == '\0') { /* last char, always append */ g_string_append_c(str, chr); (*format)++; return; } if (chr == '$') { /* $variable .. we'll always need to skip this, since it may contain characters that are in replace chars. */ theme_format_append_variable(str, format); return; } if (**format == '%') { /* format */ (*format)++; if (**format != '{' && **format != '}') { chr = **format; if (**format == 'n') { /* %n = change to default color */ g_string_append(str, "%n"); if (default_bg.m[0] != 'n') { g_string_append_c(str, '%'); g_string_append(str, default_bg.m); } if (default_fg.m[0] != 'n') { g_string_append_c(str, '%'); g_string_append(str, default_fg.m); } *last_fg = default_fg; *last_bg = default_bg; } else if (chr == 'z' || chr == 'Z') { if (chr_is_valid_rgb(*format) == 0) { t = chr == 'z' ? (*last_bg).m : (*last_fg).m; strncpy(t, *format, 7); t[7] = '\0'; g_string_append_c(str, '%'); g_string_append(str, t); (*format)+=6; } else { g_string_append(str, "%%"); g_string_append_c(str, **format); } } else if (chr == 'x' || chr == 'X') { if (chr_is_valid_ext(*format) == 0) { t = chr == 'x' ? (*last_bg).m : (*last_fg).m; strncpy(t, *format, 3); t[3] = '\0'; g_string_append_c(str, '%'); g_string_append(str, t); (*format)+=2; } else { g_string_append(str, "%%"); g_string_append_c(str, **format); } } else { if (IS_FGCOLOR_FORMAT(chr)) { (*last_fg).m[0] = chr; (*last_fg).m[1] = '\0'; } if (IS_BGCOLOR_FORMAT(chr)) { (*last_bg).m[0] = chr; (*last_bg).m[1] = '\0'; } g_string_append_c(str, '%'); g_string_append_c(str, chr); } (*format)++; return; } /* %{ or %} gives us { or } char - keep the % char though to make sure {} isn't treated as abstract */ g_string_append_c(str, '%'); chr = **format; } index = (flags & EXPAND_FLAG_IGNORE_REPLACES) ? -1 : theme->replace_keys[(int) (unsigned char) chr]; if (index == -1) g_string_append_c(str, chr); else { char *value; value = theme_replace_expand(theme, index, default_fg, default_bg, last_fg, last_bg, chr, flags); g_string_append(str, value); g_free(value); } (*format)++; } /* returns TRUE if data is empty, or the data is a $variable which is empty */ static int data_is_empty(const char **data) { /* since we don't know the real argument list, assume there's always an argument in them */ static char *arglist[] = { "x", "x", "x", "x", "x", "x","x", "x", "x", "x", NULL }; SERVER_REC *server; const char *p; char *ret; int free_ret, empty; p = *data; while (*p == ' ') p++; if (*p == '}') { /* empty */ *data = p+1; return TRUE; } if (*p != '$') { /* not empty */ return FALSE; } /* variable - check if it's empty */ p++; server = active_win == NULL ? NULL : active_win->active_server != NULL ? active_win->active_server : active_win->connect_server; ret = parse_special((char **) &p, server, active_win == NULL ? NULL : active_win->active, arglist, &free_ret, NULL, 0); p++; while (*p == ' ') p++; empty = *p == '}' && (ret == NULL || *ret == '\0'); if (free_ret) g_free(ret); if (empty) { /* empty */ *data = p+1; return TRUE; } return FALSE; } /* return "data" from {abstract data} string */ char *theme_format_expand_get(THEME_REC *theme, const char **format) { GString *str; char *ret; theme_rm_col dummy, reset; int braces = 1; /* we start with one brace opened */ dummy.m[0] = '\0'; strcpy(reset.m, "n"); str = g_string_new(NULL); while (**format != '\0' && braces != 0) { if (**format == '{') braces++; else if (**format == '}') braces--; else if ((braces > 1) && (**format == ' ')) { g_string_append(str, "\\x20"); (*format)++; continue; } else { theme_format_append_next(theme, str, format, reset, reset, &dummy, &dummy, EXPAND_FLAG_IGNORE_REPLACES); continue; } if (braces == 0) { (*format)++; break; } g_string_append_c(str, **format); (*format)++; } ret = str->str; g_string_free(str, FALSE); return ret; } static char *theme_format_expand_data_rec(THEME_REC *theme, const char **format, theme_rm_col default_fg, theme_rm_col default_bg, theme_rm_col *save_last_fg, theme_rm_col *save_last_bg, int flags, GTree *block_list); /* expand a single {abstract ...data... } */ static char *theme_format_expand_abstract(THEME_REC *theme, const char **formatp, theme_rm_col *last_fg, theme_rm_col *last_bg, int flags, GTree *block_list) { GString *str; const char *p, *format; char *abstract, *data, *ret, *blocking; theme_rm_col default_fg, default_bg; int len; format = *formatp; default_fg = *last_fg; default_bg = *last_bg; /* get abstract name first */ p = format; while (*p != '\0' && *p != ' ' && *p != '{' && *p != '}') p++; if (*p == '\0' || p == format) return NULL; /* error */ len = (int) (p-format); abstract = g_strndup(format, len); /* skip the following space, if there's any more spaces they're treated as arguments */ if (*p == ' ') { len++; if ((flags & EXPAND_FLAG_IGNORE_EMPTY) && data_is_empty(&p)) { *formatp = p; g_free(abstract); return NULL; } } *formatp = format+len; if (block_list == NULL) { block_list = g_tree_new_full((GCompareDataFunc) g_strcmp0, NULL, g_free, NULL); } else { g_tree_ref(block_list); } /* get the abstract data */ data = g_hash_table_lookup(theme->abstracts, abstract); if (data == NULL || g_tree_lookup(block_list, abstract) != NULL) { /* unknown abstract, just display the data */ data = "$0-"; g_free(abstract); blocking = NULL; } else { blocking = abstract; g_tree_insert(block_list, blocking, blocking); } abstract = g_strdup(data); /* we'll need to get the data part. it may contain more abstracts, they are _NOT_ expanded. */ data = theme_format_expand_get(theme, formatp); len = strlen(data); if (len > 1 && i_isdigit(data[len-1]) && data[len-2] == '$') { /* ends with $ .. this breaks things if next character is digit or '-' */ char digit, *tmp; tmp = data; digit = tmp[len-1]; tmp[len-1] = '\0'; data = g_strdup_printf("%s{%c}", tmp, digit); g_free(tmp); } ret = parse_special_string(abstract, NULL, NULL, data, NULL, PARSE_FLAG_ONLY_ARGS); g_free(abstract); g_free(data); str = g_string_new(NULL); p = ret; while (*p != '\0') { if (*p == '\\' && p[1] != '\0') { int chr; p++; chr = expand_escape(&p); g_string_append_c(str, chr != -1 ? chr : *p); } else g_string_append_c(str, *p); p++; } g_free(ret); abstract = str->str; g_string_free(str, FALSE); /* abstract may itself contain abstracts or replaces */ p = abstract; ret = theme_format_expand_data_rec(theme, &p, default_fg, default_bg, last_fg, last_bg, flags | EXPAND_FLAG_LASTCOLOR_ARG, block_list); g_free(abstract); if (blocking != NULL) { g_tree_remove(block_list, blocking); } g_tree_unref(block_list); return ret; } static char *theme_format_expand_data_rec(THEME_REC *theme, const char **format, theme_rm_col default_fg, theme_rm_col default_bg, theme_rm_col *save_last_fg, theme_rm_col *save_last_bg, int flags, GTree *block_list) { GString *str; char *ret, *abstract; theme_rm_col last_fg, last_bg; int recurse_flags; last_fg = default_fg; last_bg = default_bg; recurse_flags = flags & EXPAND_FLAG_RECURSIVE_MASK; str = g_string_new(NULL); while (**format != '\0') { if ((flags & EXPAND_FLAG_ROOT) == 0 && **format == '}') { /* ignore } if we're expanding original string */ (*format)++; break; } if (**format != '{') { if ((flags & EXPAND_FLAG_LASTCOLOR_ARG) && **format == '$' && (*format)[1] == '0') { /* save the color before $0 .. this is for the %n replacing */ if (save_last_fg != NULL) { *save_last_fg = last_fg; save_last_fg = NULL; } if (save_last_bg != NULL) { *save_last_bg = last_bg; save_last_bg = NULL; } } theme_format_append_next(theme, str, format, default_fg, default_bg, &last_fg, &last_bg, recurse_flags); continue; } (*format)++; if (**format == '\0' || **format == '}') break; /* error */ /* get a single {...} */ abstract = theme_format_expand_abstract(theme, format, &last_fg, &last_bg, recurse_flags, block_list); if (abstract != NULL) { g_string_append(str, abstract); g_free(abstract); } } /* save the last color */ if (save_last_fg != NULL) *save_last_fg = last_fg; if (save_last_bg != NULL) *save_last_bg = last_bg; ret = str->str; g_string_free(str, FALSE); return ret; } /* expand the data part in {abstract data} */ char *theme_format_expand_data(THEME_REC *theme, const char **format, theme_rm_col default_fg, theme_rm_col default_bg, theme_rm_col *save_last_fg, theme_rm_col *save_last_bg, int flags) { return theme_format_expand_data_rec(theme, format, default_fg, default_bg, save_last_bg, save_last_bg, flags, NULL); } #define IS_OLD_FORMAT(code, last_fg, last_bg) \ (((code) == 'n' && (last_fg) == 'n' && (last_bg) == 'n') || \ ((code) != 'n' && ((code) == (last_fg) || (code) == (last_bg)))) static char *theme_format_compress_colors(THEME_REC *theme, const char *format) { GString *str; char *ret; char last_fg, last_bg; str = g_string_new(NULL); last_fg = last_bg = '\0'; while (*format != '\0') { if (*format == '$') { /* $variable, skrip it entirely */ theme_format_append_variable(str, &format); last_fg = last_bg = '\0'; } else if (*format != '%') { /* a normal character */ g_string_append_c(str, *format); format++; } else if (format[1] != '\0') { /* %format */ format++; if (IS_OLD_FORMAT(*format, last_fg, last_bg)) { /* active color set again */ } else if (IS_FGCOLOR_FORMAT(*format) && format[1] == '%' && IS_FGCOLOR_FORMAT(format[2]) && (*format != 'n' || format[2] == 'n')) { /* two fg colors in a row. bg colors are so rare that we don't bother checking them */ } else { /* some format, add it */ g_string_append_c(str, '%'); g_string_append_c(str, *format); if (IS_FGCOLOR_FORMAT(*format)) last_fg = *format; else if (*format == 'Z' || *format == 'X') last_fg = '\0'; if (IS_BGCOLOR_FORMAT(*format)) last_bg = *format; else if (*format == 'z' || *format == 'x') last_bg = '\0'; } format++; } else { /* % at end of string */ format++; g_string_append_c(str, '%'); g_string_append_c(str, '%'); } } ret = str->str; g_string_free(str, FALSE); return ret; } char *theme_format_expand(THEME_REC *theme, const char *format) { char *data, *ret; theme_rm_col reset; strcpy(reset.m, "n"); g_return_val_if_fail(theme != NULL, NULL); g_return_val_if_fail(format != NULL, NULL); data = theme_format_expand_data(theme, &format, reset, reset, NULL, NULL, EXPAND_FLAG_ROOT); ret = theme_format_compress_colors(theme, data); g_free(data); return ret; } static MODULE_THEME_REC *theme_module_create(THEME_REC *theme, const char *module) { MODULE_THEME_REC *rec; FORMAT_REC *formats; rec = g_hash_table_lookup(theme->modules, module); if (rec != NULL) return rec; formats = g_hash_table_lookup(default_formats, module); g_return_val_if_fail(formats != NULL, NULL); rec = g_new0(MODULE_THEME_REC, 1); rec->name = g_strdup(module); for (rec->count = 0; formats[rec->count].def != NULL; rec->count++) ; rec->formats = g_new0(char *, rec->count); rec->expanded_formats = g_new0(char *, rec->count); g_hash_table_insert(theme->modules, rec->name, rec); return rec; } static void theme_read_replaces(CONFIG_REC *config, THEME_REC *theme) { GSList *tmp; CONFIG_NODE *node; const char *p; int index; /* reset replace keys */ for (index = 0; index < 256; index++) theme->replace_keys[index] = -1; index = 0; node = config_node_traverse(config, "replaces", FALSE); if (node == NULL || node->type != NODE_TYPE_BLOCK) return; for (tmp = node->value; tmp != NULL; tmp = tmp->next) { node = tmp->data; if (node->key != NULL && node->value != NULL) { for (p = node->key; *p != '\0'; p++) theme->replace_keys[(int) (unsigned char) *p] = index; theme->replace_values = g_slist_append(theme->replace_values, g_strdup(node->value)); index++; } } } static void theme_read_abstracts(CONFIG_REC *config, THEME_REC *theme) { GSList *tmp; CONFIG_NODE *node; gpointer oldkey, oldvalue; node = config_node_traverse(config, "abstracts", FALSE); if (node == NULL || node->type != NODE_TYPE_BLOCK) return; for (tmp = node->value; tmp != NULL; tmp = tmp->next) { node = tmp->data; if (node->key == NULL || node->value == NULL) continue; if (g_hash_table_lookup_extended(theme->abstracts, node->key, &oldkey, &oldvalue)) { /* new values override old ones */ g_hash_table_remove(theme->abstracts, oldkey); g_free(oldkey); g_free(oldvalue); } g_hash_table_insert(theme->abstracts, g_strdup(node->key), g_strdup(node->value)); } } static void theme_set_format(THEME_REC *theme, MODULE_THEME_REC *rec, const char *module, const char *key, const char *value) { int num; num = format_find_tag(module, key); if (num != -1) { rec->formats[num] = g_strdup(value); rec->expanded_formats[num] = theme_format_expand(theme, value); } } static void theme_read_formats(THEME_REC *theme, const char *module, CONFIG_REC *config, MODULE_THEME_REC *rec) { CONFIG_NODE *node; GSList *tmp; node = config_node_traverse(config, "formats", FALSE); if (node == NULL) return; node = config_node_section(config, node, module, -1); if (node == NULL) return; for (tmp = node->value; tmp != NULL; tmp = tmp->next) { node = tmp->data; if (node->key != NULL && node->value != NULL) { theme_set_format(theme, rec, module, node->key, node->value); } } } static void theme_init_module(THEME_REC *theme, const char *module, CONFIG_REC *config) { MODULE_THEME_REC *rec; FORMAT_REC *formats; int n; formats = g_hash_table_lookup(default_formats, module); g_return_if_fail(formats != NULL); rec = theme_module_create(theme, module); if (config != NULL) theme_read_formats(theme, module, config, rec); /* expand the remaining formats */ for (n = 0; n < rec->count; n++) { if (rec->expanded_formats[n] == NULL) { rec->expanded_formats[n] = theme_format_expand(theme, formats[n].def); } } } static void sig_print_errors(void) { init_finished = TRUE; if (init_errors != NULL) { signal_emit("gui dialog", 2, "error", init_errors); g_free(init_errors); } } static void theme_read_module(THEME_REC *theme, const char *module) { CONFIG_REC *config; config = config_open(theme->path, -1); if (config != NULL) config_parse(config); theme_init_module(theme, module, config); if (config != NULL) config_close(config); } static void themes_read_module(const char *module) { g_slist_foreach(themes, (GFunc) theme_read_module, (void *) module); } static void theme_remove_module(THEME_REC *theme, const char *module) { MODULE_THEME_REC *rec; rec = g_hash_table_lookup(theme->modules, module); if (rec == NULL) return; g_hash_table_remove(theme->modules, module); theme_module_destroy(module, rec); } static void themes_remove_module(const char *module) { g_slist_foreach(themes, (GFunc) theme_remove_module, (void *) module); } void theme_register_module(const char *module, FORMAT_REC *formats) { if (g_hash_table_lookup(default_formats, module) != NULL) return; g_hash_table_insert(default_formats, g_strdup(module), formats); themes_read_module(module); } void theme_unregister_module(const char *module) { gpointer key, value; if (default_formats == NULL) return; /* already uninitialized */ if (!g_hash_table_lookup_extended(default_formats, module, &key, &value)) return; g_hash_table_remove(default_formats, key); g_free(key); themes_remove_module(module); } void theme_set_default_abstract(const char *key, const char *value) { gpointer oldkey, oldvalue; if (g_hash_table_lookup_extended(internal_theme->abstracts, key, &oldkey, &oldvalue)) { /* new values override old ones */ g_hash_table_remove(internal_theme->abstracts, oldkey); g_free(oldkey); g_free(oldvalue); } g_hash_table_insert(internal_theme->abstracts, g_strdup(key), g_strdup(value)); } static THEME_REC *theme_find(const char *name) { GSList *tmp; for (tmp = themes; tmp != NULL; tmp = tmp->next) { THEME_REC *rec = tmp->data; if (g_ascii_strcasecmp(rec->name, name) == 0) return rec; } return NULL; } static void window_themes_update(void) { GSList *tmp; for (tmp = windows; tmp != NULL; tmp = tmp->next) { WINDOW_REC *rec = tmp->data; if (rec->theme_name != NULL) rec->theme = theme_load(rec->theme_name); } } THEME_REC *theme_load(const char *setname) { THEME_REC *theme, *oldtheme; struct stat statbuf; char *fname, *name, *p; name = g_strdup(setname); p = strrchr(name, '.'); if (p != NULL && g_strcmp0(p, ".theme") == 0) { /* remove the trailing .theme */ *p = '\0'; } theme = theme_find(name); /* check home dir */ fname = g_strdup_printf("%s/%s.theme", get_irssi_dir(), name); if (stat(fname, &statbuf) != 0) { /* check global config dir */ g_free(fname); fname = g_strdup_printf(THEMESDIR"/%s.theme", name); if (stat(fname, &statbuf) != 0) { /* theme not found */ g_free(fname); g_free(name); return theme; /* use the one in memory if possible */ } } if (theme != NULL && theme->last_modify == statbuf.st_mtime) { /* theme not modified, use the one already in memory */ g_free(fname); g_free(name); return theme; } oldtheme = theme; theme = theme_create(fname, name); theme->last_modify = statbuf.st_mtime; if (!theme_read(theme, theme->path)) { /* error reading .theme file */ theme_destroy(theme); theme = NULL; } if (oldtheme != NULL && theme != NULL) { theme_destroy(oldtheme); if (current_theme == oldtheme) current_theme = theme; window_themes_update(); } g_free(fname); g_free(name); return theme; } static void copy_abstract_hash(char *key, char *value, GHashTable *dest) { g_hash_table_insert(dest, g_strdup(key), g_strdup(value)); } static void theme_copy_abstracts(THEME_REC *dest, THEME_REC *src) { g_hash_table_foreach(src->abstracts, (GHFunc) copy_abstract_hash, dest->abstracts); } typedef struct { THEME_REC *theme; CONFIG_REC *config; } THEME_READ_REC; static void theme_read_modules(const char *module, void *value, THEME_READ_REC *rec) { theme_init_module(rec->theme, module, rec->config); } static void read_error(const char *str) { char *old; if (init_finished) printtext(NULL, NULL, MSGLEVEL_CLIENTERROR, "%s", str); else if (init_errors == NULL) init_errors = g_strdup(str); else { old = init_errors; init_errors = g_strconcat(init_errors, "\n", str, NULL); g_free(old); } } static int theme_read(THEME_REC *theme, const char *path) { CONFIG_REC *config; THEME_READ_REC rec; char *str; config = config_open(path, -1) ; if (config == NULL) { /* didn't exist or no access? */ str = g_strdup_printf("Error reading theme file %s: %s", path, g_strerror(errno)); read_error(str); g_free(str); return FALSE; } if (path == NULL) config_parse_data(config, default_theme, "internal"); else config_parse(config); if (config_last_error(config) != NULL) { str = g_strdup_printf("Ignored errors in theme %s:\n%s", theme->name, config_last_error(config)); read_error(str); g_free(str); } theme->default_color = config_get_int(config, NULL, "default_color", -1); theme->info_eol = config_get_bool(config, NULL, "info_eol", FALSE); theme_read_replaces(config, theme); if (path != NULL) theme_copy_abstracts(theme, internal_theme); theme_read_abstracts(config, theme); rec.theme = theme; rec.config = config; g_hash_table_foreach(default_formats, (GHFunc) theme_read_modules, &rec); config_close(config); return TRUE; } typedef struct { char *name; char *short_name; } THEME_SEARCH_REC; static int theme_search_equal(THEME_SEARCH_REC *r1, THEME_SEARCH_REC *r2) { return g_ascii_strcasecmp(r1->short_name, r2->short_name); } static void theme_get_modules(char *module, FORMAT_REC *formats, GSList **list) { THEME_SEARCH_REC *rec; rec = g_new(THEME_SEARCH_REC, 1); rec->name = module; rec->short_name = strrchr(module, '/'); if (rec->short_name != NULL) rec->short_name++; else rec->short_name = module; *list = g_slist_insert_sorted(*list, rec, (GCompareFunc) theme_search_equal); } static GSList *get_sorted_modules(void) { GSList *list; list = NULL; g_hash_table_foreach(default_formats, (GHFunc) theme_get_modules, &list); return list; } static THEME_SEARCH_REC *theme_search(GSList *list, const char *module) { THEME_SEARCH_REC *rec; while (list != NULL) { rec = list->data; if (g_ascii_strcasecmp(rec->short_name, module) == 0) return rec; list = list->next; } return NULL; } static void theme_show(THEME_SEARCH_REC *rec, const char *key, const char *value, int reset) { MODULE_THEME_REC *theme; FORMAT_REC *formats; const char *text, *last_title; int n, first; formats = g_hash_table_lookup(default_formats, rec->name); theme = g_hash_table_lookup(current_theme->modules, rec->name); last_title = NULL; first = TRUE; for (n = 1; formats[n].def != NULL; n++) { text = theme != NULL && theme->formats[n] != NULL ? theme->formats[n] : formats[n].def; if (formats[n].tag == NULL) last_title = text; else if ((value != NULL && key != NULL && g_ascii_strcasecmp(formats[n].tag, key) == 0) || (value == NULL && (key == NULL || stristr(formats[n].tag, key) != NULL))) { if (first) { printformat(NULL, NULL, MSGLEVEL_CLIENTCRAP, TXT_FORMAT_TITLE, rec->short_name, formats[0].def); first = FALSE; } if (last_title != NULL) printformat(NULL, NULL, MSGLEVEL_CLIENTCRAP, TXT_FORMAT_SUBTITLE, last_title); if (reset || value != NULL) { theme = theme_module_create(current_theme, rec->name); g_free_not_null(theme->formats[n]); g_free_not_null(theme->expanded_formats[n]); text = reset ? formats[n].def : value; theme->formats[n] = reset ? NULL : g_strdup(value); theme->expanded_formats[n] = theme_format_expand(current_theme, text); } printformat(NULL, NULL, MSGLEVEL_CLIENTCRAP, TXT_FORMAT_ITEM, formats[n].tag, text); last_title = NULL; } } } /* SYNTAX: FORMAT [-delete | -reset] [] [ []] */ static void cmd_format(const char *data) { GHashTable *optlist; GSList *tmp, *modules; char *module, *key, *value; void *free_arg; int reset; if (!cmd_get_params(data, &free_arg, 3 | PARAM_FLAG_GETREST | PARAM_FLAG_OPTIONS, "format", &optlist, &module, &key, &value)) return; modules = get_sorted_modules(); if (*module == '\0') module = NULL; else if (theme_search(modules, module) == NULL) { /* first argument isn't module.. */ cmd_params_free(free_arg); if (!cmd_get_params(data, &free_arg, 2 | PARAM_FLAG_GETREST | PARAM_FLAG_OPTIONS, "format", &optlist, &key, &value)) return; module = NULL; } reset = FALSE; if (*key == '\0') key = NULL; if (g_hash_table_lookup(optlist, "reset")) reset = TRUE; else if (g_hash_table_lookup(optlist, "delete")) value = ""; else if (*value == '\0') value = NULL; for (tmp = modules; tmp != NULL; tmp = tmp->next) { THEME_SEARCH_REC *rec = tmp->data; if (module == NULL || g_ascii_strcasecmp(rec->short_name, module) == 0) theme_show(rec, key, value, reset); } g_slist_foreach(modules, (GFunc) g_free, NULL); g_slist_free(modules); cmd_params_free(free_arg); } typedef struct { CONFIG_REC *config; int save_all; } THEME_SAVE_REC; static void module_save(const char *module, MODULE_THEME_REC *rec, THEME_SAVE_REC *data) { CONFIG_NODE *fnode, *node; FORMAT_REC *formats; int n; formats = g_hash_table_lookup(default_formats, rec->name); if (formats == NULL) return; fnode = config_node_traverse(data->config, "formats", TRUE); node = config_node_section(data->config, fnode, rec->name, NODE_TYPE_BLOCK); for (n = 1; formats[n].def != NULL; n++) { if (rec->formats[n] != NULL) { config_node_set_str(data->config, node, formats[n].tag, rec->formats[n]); } else if (data->save_all && formats[n].tag != NULL) { config_node_set_str(data->config, node, formats[n].tag, formats[n].def); } } if (node->value == NULL) { /* not modified, don't keep the empty section */ config_node_remove(data->config, fnode, node); if (fnode->value == NULL) { config_node_remove(data->config, data->config->mainnode, fnode); } } } static void theme_save(THEME_REC *theme, int save_all) { CONFIG_REC *config; THEME_SAVE_REC data; char *path; char *basename; int ok; config = config_open(theme->path, -1); if (config != NULL) config_parse(config); else { if (g_ascii_strcasecmp(theme->name, "default") == 0) { config = config_open(NULL, -1); config_parse_data(config, default_theme, "internal"); config_change_file_name(config, theme->path, 0660); } else { config = config_open(theme->path, 0660); if (config == NULL) return; config_parse(config); } } data.config = config; data.save_all = save_all; g_hash_table_foreach(theme->modules, (GHFunc) module_save, &data); basename = g_path_get_basename(theme->path); /* always save the theme to ~/.irssi/ */ path = g_strdup_printf("%s/%s", get_irssi_dir(), basename); ok = config_write(config, path, 0660) == 0; g_free(basename); printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE, ok ? TXT_THEME_SAVED : TXT_THEME_SAVE_FAILED, path, config_last_error(config)); g_free(path); config_close(config); } /* save changed formats, -format saves all */ static void cmd_save(const char *data) { GSList *tmp; GHashTable *optlist; void *free_arg; char *fname; int saveall; if (!cmd_get_params(data, &free_arg, 1 | PARAM_FLAG_OPTIONS, "save", &optlist, &fname)) return; saveall = g_hash_table_lookup(optlist, "formats") != NULL; for (tmp = themes; tmp != NULL; tmp = tmp->next) { THEME_REC *theme = tmp->data; theme_save(theme, saveall); } cmd_params_free(free_arg); } static void complete_format_list(THEME_SEARCH_REC *rec, const char *key, GList **list) { FORMAT_REC *formats; int n, len; formats = g_hash_table_lookup(default_formats, rec->name); len = strlen(key); for (n = 1; formats[n].def != NULL; n++) { const char *item = formats[n].tag; if (item != NULL && g_ascii_strncasecmp(item, key, len) == 0) *list = g_list_append(*list, g_strdup(item)); } } static GList *completion_get_formats(const char *module, const char *key) { GSList *modules, *tmp; GList *list; g_return_val_if_fail(key != NULL, NULL); list = NULL; modules = get_sorted_modules(); if (*module == '\0' || theme_search(modules, module) != NULL) { for (tmp = modules; tmp != NULL; tmp = tmp->next) { THEME_SEARCH_REC *rec = tmp->data; if (*module == '\0' || g_ascii_strcasecmp(rec->short_name, module) == 0) complete_format_list(rec, key, &list); } } g_slist_foreach(modules, (GFunc) g_free, NULL); g_slist_free(modules); return list; } static void sig_complete_format(GList **list, WINDOW_REC *window, const char *word, const char *line, int *want_space) { const char *ptr; int words; g_return_if_fail(list != NULL); g_return_if_fail(word != NULL); g_return_if_fail(line != NULL); ptr = line; words = 0; if (*ptr != '\0') { do { ptr++; words++; ptr = strchr(ptr, ' '); } while (ptr != NULL); } if (words > 2) return; *list = completion_get_formats(line, word); if (*list != NULL) signal_stop(); } static void change_theme(const char *name, int verbose) { THEME_REC *rec; rec = theme_load(name); if (rec != NULL) { current_theme = rec; signal_emit("theme changed", 1, rec); if (verbose) { printformat(NULL, NULL, MSGLEVEL_CLIENTNOTICE, TXT_THEME_CHANGED, rec->name, rec->path); } } else if (verbose) { printformat(NULL, NULL, MSGLEVEL_CLIENTERROR, TXT_THEME_NOT_FOUND, name); } } static void read_settings(void) { const char *theme; int len; theme = settings_get_str("theme"); len = strlen(current_theme->name); if (g_strcmp0(current_theme->name, theme) != 0 && (strncmp(current_theme->name, theme, len) != 0 || g_strcmp0(theme+len, ".theme") != 0)) change_theme(theme, TRUE); } void themes_reload(void) { GSList *refs; char *fname; /* increase every theme's refcount, and destroy them. this way if we want to use the theme before it's reloaded we don't crash. */ refs = NULL; while (themes != NULL) { THEME_REC *theme = themes->data; refs = g_slist_prepend(refs, theme); theme->refcount++; theme_destroy(theme); } /* first there's default theme.. */ current_theme = theme_load("default"); if (current_theme == NULL) { fname = g_strdup_printf("%s/default.theme", get_irssi_dir()); current_theme = theme_create(fname, "default"); current_theme->default_color = -1; theme_read(current_theme, NULL); g_free(fname); } window_themes_update(); change_theme(settings_get_str("theme"), FALSE); while (refs != NULL) { theme_unref(refs->data); refs = g_slist_remove(refs, refs->data); } } static THEME_REC *read_internal_theme(void) { CONFIG_REC *config; THEME_REC *theme; theme = theme_create("internal", "_internal"); theme->refcount++; theme_destroy(theme); config = config_open(NULL, -1); config_parse_data(config, default_theme, "internal"); theme_read_abstracts(config, theme); config_close(config); return theme; } void themes_init(void) { settings_add_str("lookandfeel", "theme", "default"); default_formats = g_hash_table_new((GHashFunc) g_str_hash, (GCompareFunc) g_str_equal); internal_theme = read_internal_theme(); init_finished = FALSE; init_errors = NULL; themes_reload(); command_bind("format", NULL, (SIGNAL_FUNC) cmd_format); command_bind("save", NULL, (SIGNAL_FUNC) cmd_save); signal_add("complete command format", (SIGNAL_FUNC) sig_complete_format); signal_add("irssi init finished", (SIGNAL_FUNC) sig_print_errors); signal_add("setup changed", (SIGNAL_FUNC) read_settings); signal_add("setup reread", (SIGNAL_FUNC) themes_reload); command_set_options("format", "delete reset"); command_set_options("save", "formats"); } void themes_deinit(void) { while (themes != NULL) theme_destroy(themes->data); theme_destroy(internal_theme); g_hash_table_destroy(default_formats); default_formats = NULL; command_unbind("format", (SIGNAL_FUNC) cmd_format); command_unbind("save", (SIGNAL_FUNC) cmd_save); signal_remove("complete command format", (SIGNAL_FUNC) sig_complete_format); signal_remove("irssi init finished", (SIGNAL_FUNC) sig_print_errors); signal_remove("setup changed", (SIGNAL_FUNC) read_settings); signal_remove("setup reread", (SIGNAL_FUNC) themes_reload); }