/* * SPDX-FileCopyrightText: 2026 Sébastien Helleu * * SPDX-License-Identifier: GPL-3.0-or-later * * This file is part of WeeChat, the extensible chat client. * * WeeChat 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 3 of the License, or * (at your option) any later version. * * WeeChat 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 WeeChat. If not, see . */ /* Themes: named bundles of option overrides applied via /theme command */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include "weechat.h" #include "core-arraylist.h" #include "core-config.h" #include "core-config-file.h" #include "core-dir.h" #include "core-hashtable.h" #include "core-hook.h" #include "core-string.h" #include "core-theme.h" #include "core-version.h" #include "../gui/gui-chat.h" #include "../gui/gui-color.h" #include "../gui/gui-window.h" #include "../plugins/weechat-plugin.h" struct t_theme *themes = NULL; struct t_theme *last_theme = NULL; int theme_applying = 0; /* * Searches for a theme by name in the in-memory registry. * * Returns pointer to theme found, NULL if not found. */ struct t_theme * theme_search (const char *name) { struct t_theme *ptr_theme; if (!name) return NULL; for (ptr_theme = themes; ptr_theme; ptr_theme = ptr_theme->next_theme) { if (strcmp (ptr_theme->name, name) == 0) return ptr_theme; } return NULL; } /* * Builds a "YYYY-MM-DD HH:MM:SS" timestamp string for "now" (local time). * * Returned string is allocated; caller frees. */ char * theme_format_now (void) { time_t time_now; struct tm *local_time; char buf[32]; time_now = time (NULL); local_time = localtime (&time_now); if (!local_time) return strdup (""); if (strftime (buf, sizeof (buf), "%Y-%m-%d %H:%M:%S", local_time) == 0) return strdup (""); return strdup (buf); } /* * Allocates a new theme with name and empty metadata; does not link it * into the registry. * * Returns the new theme, NULL on error. */ struct t_theme * theme_alloc (const char *name) { struct t_theme *new_theme; new_theme = calloc (1, sizeof (*new_theme)); if (!new_theme) return NULL; new_theme->name = strdup (name); new_theme->description = strdup (""); new_theme->date = theme_format_now (); new_theme->weechat_version = strdup (version_get_version ()); if (!new_theme->name || !new_theme->description || !new_theme->date || !new_theme->weechat_version) { free (new_theme->name); free (new_theme->description); free (new_theme->date); free (new_theme->weechat_version); free (new_theme); return NULL; } return new_theme; } /* * Frees one contribution (does not unlink from the parent theme list). */ void theme_contribution_free (struct t_theme_contribution *contribution) { if (!contribution) return; hashtable_free (contribution->overrides); free (contribution); } /* * Frees a theme (does not unlink from registry; caller handles that). */ void theme_free (struct t_theme *theme) { struct t_theme_contribution *ptr_contribution, *next_contribution; if (!theme) return; ptr_contribution = theme->contributions; while (ptr_contribution) { next_contribution = ptr_contribution->next_contribution; theme_contribution_free (ptr_contribution); ptr_contribution = next_contribution; } free (theme->name); free (theme->description); free (theme->date); free (theme->weechat_version); free (theme); } /* * Merges entries from src into dst (overwrites duplicate keys). */ void theme_merge_overrides_cb (void *data, struct t_hashtable *hashtable, const void *key, const void *value) { struct t_hashtable *dst = (struct t_hashtable *)data; /* make C compiler happy */ (void) hashtable; hashtable_set (dst, (const char *)key, (const char *)value); } /* * Searches the contribution belonging to the given (plugin, script) pair * in a theme's contribution list. Returns NULL if not found. */ struct t_theme_contribution * theme_search_contribution (struct t_theme *theme, struct t_weechat_plugin *plugin, const void *script) { struct t_theme_contribution *ptr; if (!theme) return NULL; for (ptr = theme->contributions; ptr; ptr = ptr->next_contribution) { if ((ptr->plugin == plugin) && (ptr->script == script)) return ptr; } return NULL; } /* * Allocates a new contribution and appends it to a theme's list. * * Returns the new contribution, NULL on error. */ struct t_theme_contribution * theme_contribution_new (struct t_theme *theme, struct t_weechat_plugin *plugin, const void *script) { struct t_theme_contribution *new_contribution; new_contribution = calloc (1, sizeof (*new_contribution)); if (!new_contribution) return NULL; new_contribution->plugin = plugin; new_contribution->script = script; new_contribution->overrides = hashtable_new (32, WEECHAT_HASHTABLE_STRING, WEECHAT_HASHTABLE_STRING, NULL, NULL); if (!new_contribution->overrides) { free (new_contribution); return NULL; } new_contribution->prev_contribution = theme->last_contribution; new_contribution->next_contribution = NULL; if (theme->last_contribution) theme->last_contribution->next_contribution = new_contribution; else theme->contributions = new_contribution; theme->last_contribution = new_contribution; return new_contribution; } /* * Registers a contribution to a theme. * * Identity is the (plugin, script) pair: * - plugin == NULL && script == NULL => core * - plugin != NULL && script == NULL => plugin-level * - plugin != NULL && script != NULL => individual script * * If a contribution for the same (plugin, script) already exists under * the named theme, the new overrides are merged into it (later keys * win). Otherwise a new contribution is appended. Across distinct * contributors, contributions are applied in list order at apply-time * (later contributions override earlier ones for duplicate keys). * * The "overrides" hashtable passed in is read-only here; the caller * retains ownership and may free it after the call. * * Returns pointer to the theme (existing or newly created), NULL on * error. */ struct t_theme * theme_register (struct t_weechat_plugin *plugin, const void *script, const char *name, struct t_hashtable *overrides) { struct t_theme *theme; struct t_theme_contribution *contribution; if (!name || !name[0]) return NULL; theme = theme_search (name); if (!theme) { theme = theme_alloc (name); if (!theme) return NULL; theme->prev_theme = last_theme; theme->next_theme = NULL; if (last_theme) last_theme->next_theme = theme; else themes = theme; last_theme = theme; } if (overrides) { contribution = theme_search_contribution (theme, plugin, script); if (!contribution) { contribution = theme_contribution_new (theme, plugin, script); if (!contribution) return theme; /* theme exists but contribution failed */ } hashtable_map (overrides, &theme_merge_overrides_cb, contribution->overrides); } return theme; } /* * Drops one contribution from a theme (unlinks and frees it). */ void theme_drop_contribution (struct t_theme *theme, struct t_theme_contribution *contribution) { if (!theme || !contribution) return; if (contribution->prev_contribution) { contribution->prev_contribution->next_contribution = contribution->next_contribution; } else { theme->contributions = contribution->next_contribution; } if (contribution->next_contribution) { contribution->next_contribution->prev_contribution = contribution->prev_contribution; } else { theme->last_contribution = contribution->prev_contribution; } theme_contribution_free (contribution); } /* * Drops all contributions owned by a given plugin (across every theme). * Called from the plugin-unload lifecycle (next commit). * * Contributions whose script is non-NULL are kept (they belong to * individual scripts and are cleaned up separately on script unload). */ void theme_unregister_plugin (struct t_weechat_plugin *plugin) { struct t_theme *ptr_theme; struct t_theme_contribution *ptr_contribution, *next_contribution; if (!plugin) return; for (ptr_theme = themes; ptr_theme; ptr_theme = ptr_theme->next_theme) { ptr_contribution = ptr_theme->contributions; while (ptr_contribution) { next_contribution = ptr_contribution->next_contribution; if ((ptr_contribution->plugin == plugin) && (ptr_contribution->script == NULL)) { theme_drop_contribution (ptr_theme, ptr_contribution); } ptr_contribution = next_contribution; } } } /* * Drops all contributions owned by a given script (across every theme). * Called from the script-unload lifecycle (a later commit). */ void theme_unregister_script (struct t_weechat_plugin *plugin, const void *script) { struct t_theme *ptr_theme; struct t_theme_contribution *ptr_contribution, *next_contribution; if (!plugin || !script) return; for (ptr_theme = themes; ptr_theme; ptr_theme = ptr_theme->next_theme) { ptr_contribution = ptr_theme->contributions; while (ptr_contribution) { next_contribution = ptr_contribution->next_contribution; if ((ptr_contribution->plugin == plugin) && (ptr_contribution->script == script)) { theme_drop_contribution (ptr_theme, ptr_contribution); } ptr_contribution = next_contribution; } } } /* * Returns the total number of overrides across all contributions of a * theme. Duplicate keys (across contributions) are counted multiple * times; the actual merged-unique count is at most this number. */ int theme_overrides_count (struct t_theme *theme) { struct t_theme_contribution *ptr; int n; if (!theme) return 0; n = 0; for (ptr = theme->contributions; ptr; ptr = ptr->next_contribution) n += ptr->overrides->items_count; return n; } /* * Returns the effective value of an option override across the theme's * contributions (later contributions win). Returns NULL if no * contribution provides the key. */ const char * theme_get_override (struct t_theme *theme, const char *option_name) { struct t_theme_contribution *ptr; const char *value, *latest; if (!theme || !option_name) return NULL; latest = NULL; for (ptr = theme->contributions; ptr; ptr = ptr->next_contribution) { value = (const char *)hashtable_get (ptr->overrides, option_name); if (value) latest = value; } return latest; } /* * Compares two themes by name (callback used by arraylist sort). * * Returns negative, zero, or positive value (like strcmp). */ int theme_list_cmp_cb (void *data, struct t_arraylist *arraylist, void *pointer1, void *pointer2) { /* make C compiler happy */ (void) data; (void) arraylist; return strcmp (((struct t_theme *)pointer1)->name, ((struct t_theme *)pointer2)->name); } /* * Returns an arraylist of t_theme * for all registered themes (built-ins). * * The returned arraylist owns no data; callers must not free its items. * Returns NULL on allocation failure. */ struct t_arraylist * theme_list (void) { struct t_arraylist *list; struct t_theme *ptr_theme; list = arraylist_new (8, 1, 0, &theme_list_cmp_cb, NULL, NULL, NULL); if (!list) return NULL; for (ptr_theme = themes; ptr_theme; ptr_theme = ptr_theme->next_theme) arraylist_add (list, ptr_theme); return list; } /* * Builds the on-disk path for a user theme: * "/themes/.theme". * * Returned string is allocated; caller frees. Returns NULL on error. */ char * theme_user_file_path (const char *name) { char *path = NULL; if (!name || !name[0]) return NULL; string_asprintf (&path, "%s/themes/%s.theme", weechat_config_dir, name); return path; } /* * Builds a unique backup theme name "backup-YYYYMMDD-HHMMSS-uuuuuu". * * Returned string is allocated; caller frees. Returns NULL on error. */ char * theme_make_backup_name (void) { struct timeval tv; struct tm *local_time; char buf[128]; if (gettimeofday (&tv, NULL) != 0) return NULL; local_time = localtime (&tv.tv_sec); if (!local_time) return NULL; snprintf (buf, sizeof (buf), "backup-%04d%02d%02d-%02d%02d%02d-%06ld", local_time->tm_year + 1900, local_time->tm_mon + 1, local_time->tm_mday, local_time->tm_hour, local_time->tm_min, local_time->tm_sec, (long)tv.tv_usec); return strdup (buf); } /* * Writes a snapshot of themable options to a .theme file at * "/themes/.theme". * * The themes directory is created if missing. The file contains an * [info] section (name, description, date, weechat version) followed by * an [options] section. * * If "diff_only" is non-zero, only options whose value differs from * their default (config_file_option_has_changed) are written. If zero, * every themable option is written (full snapshot). * * Returns 1 on success, 0 on error. */ int theme_write_file (const char *name, const char *description, int diff_only) { char *path, *dir, *value, *now; FILE *file; struct t_config_file *ptr_config; struct t_config_section *ptr_section; struct t_config_option *ptr_option; if (!name || !name[0]) return 0; path = NULL; dir = NULL; string_asprintf (&dir, "%s/themes", weechat_config_dir); if (!dir) return 0; dir_mkdir (dir, 0755); free (dir); path = theme_user_file_path (name); if (!path) return 0; file = fopen (path, "w"); free (path); if (!file) return 0; now = theme_format_now (); fprintf (file, "[info]\n"); fprintf (file, "name = \"%s\"\n", name); fprintf (file, "description = \"%s\"\n", (description) ? description : ""); fprintf (file, "date = \"%s\"\n", (now) ? now : ""); fprintf (file, "weechat = \"%s\"\n", version_get_version ()); fprintf (file, "\n[options]\n"); free (now); for (ptr_config = config_files; ptr_config; ptr_config = ptr_config->next_config) { for (ptr_section = ptr_config->sections; ptr_section; ptr_section = ptr_section->next_section) { for (ptr_option = ptr_section->options; ptr_option; ptr_option = ptr_option->next_option) { if (!ptr_option->themable) continue; if (diff_only && !config_file_option_has_changed (ptr_option)) continue; value = config_file_option_value_to_string ( ptr_option, 0, 0, 1); fprintf (file, "%s.%s.%s = %s\n", ptr_config->name, ptr_section->name, ptr_option->name, (value) ? value : "\"\""); free (value); } } } fclose (file); return 1; } /* * Creates a timestamped backup theme file with the current themable state. * * Returned string is the backup name (caller frees), NULL on failure. */ char * theme_make_backup (void) { char *name; name = theme_make_backup_name (); if (!name) return NULL; if (!theme_write_file ( name, _("Automatic backup written before /theme apply"), 0)) /* full snapshot: backups must round-trip exactly */ { free (name); return NULL; } return name; } /* * Applies one override entry (callback for hashtable_map during apply). * * Refuses entries pointing to options that do not exist or that are not * themable, logging a warning to the core buffer; the apply itself still * proceeds with the remaining entries. */ void theme_apply_set_option_cb (void *data, struct t_hashtable *hashtable, const void *key, const void *value) { struct t_config_option *option = NULL; /* make C compiler happy */ (void) data; (void) hashtable; config_file_search_with_string ((const char *)key, NULL, NULL, &option, NULL); if (!option) { gui_chat_printf (NULL, _("%sTheme: option \"%s\" not found, skipped"), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], (const char *)key); return; } if (!option->themable) { gui_chat_printf ( NULL, _("%sTheme: option \"%s\" is not themable, skipped"), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], (const char *)key); return; } config_file_option_set (option, (const char *)value, 1); } /* * Strips one optional pair of matching surrounding quotes (' or ") from * the in-place string; returns a pointer that may differ from the input * (advances past an opening quote). */ char * theme_file_strip_quotes (char *value) { size_t len; if (!value) return value; len = strlen (value); if ((len >= 2) && (((value[0] == '"') && (value[len - 1] == '"')) || ((value[0] == '\'') && (value[len - 1] == '\'')))) { value[len - 1] = '\0'; return value + 1; } return value; } /* * Parses a .theme file into a transient t_theme. * * The file uses two INI-like sections: [info] (keys: name, description, * date, weechat) and [options] (key = full option name like * "irc.color.input_nick", value = string). Unknown [info] keys produce a * warning and are ignored; unknown sections produce a warning and the * lines in them are skipped. * * Returns a heap-allocated t_theme (caller frees with theme_free), or * NULL if the file cannot be opened. */ struct t_theme * theme_file_parse (const char *path) { FILE *file; char line[8192], *ptr, *end, *eq, *key, *value; int line_number, in_options; struct t_theme *theme; struct t_theme_contribution *contribution; if (!path) return NULL; file = fopen (path, "r"); if (!file) return NULL; theme = theme_alloc (""); if (!theme) { fclose (file); return NULL; } /* file themes carry a single anonymous (plugin=NULL, script=NULL) contribution holding everything in the [options] section */ contribution = theme_contribution_new (theme, NULL, NULL); if (!contribution) { theme_free (theme); fclose (file); return NULL; } /* clear the placeholder name; the file should provide it */ free (theme->name); theme->name = NULL; /* description/date/weechat_version come from the file too */ free (theme->description); theme->description = NULL; free (theme->date); theme->date = NULL; free (theme->weechat_version); theme->weechat_version = NULL; line_number = 0; in_options = 0; while (fgets (line, sizeof (line) - 1, file)) { line_number++; /* trim trailing CR / LF */ end = strchr (line, '\r'); if (end) *end = '\0'; end = strchr (line, '\n'); if (end) *end = '\0'; /* skip leading whitespace */ ptr = line; while ((ptr[0] == ' ') || (ptr[0] == '\t')) ptr++; /* skip empty lines and comments */ if (!ptr[0] || (ptr[0] == '#')) continue; /* section header */ if (ptr[0] == '[') { end = strchr (ptr, ']'); if (!end) { gui_chat_printf ( NULL, _("%s%s: line %d: malformed section header"), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], path, line_number); continue; } *end = '\0'; if (strcmp (ptr + 1, "info") == 0) { in_options = 0; } else if (strcmp (ptr + 1, "options") == 0) { in_options = 1; } else { gui_chat_printf ( NULL, _("%s%s: line %d: ignoring unknown section \"%s\""), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], path, line_number, ptr + 1); in_options = -1; /* skip lines until next known section */ } continue; } if (in_options < 0) continue; /* "key = value" */ eq = strchr (ptr, '='); if (!eq) { gui_chat_printf ( NULL, _("%s%s: line %d: missing '=' separator"), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], path, line_number); continue; } /* trim key */ key = ptr; end = eq - 1; while ((end > key) && ((end[0] == ' ') || (end[0] == '\t'))) end--; end[1] = '\0'; /* trim value */ value = eq + 1; while ((value[0] == ' ') || (value[0] == '\t')) value++; end = value + strlen (value) - 1; while ((end > value) && ((end[0] == ' ') || (end[0] == '\t'))) end--; end[1] = '\0'; value = theme_file_strip_quotes (value); if (in_options) { hashtable_set (contribution->overrides, key, value); } else { /* [info] section */ if (strcmp (key, "name") == 0) { free (theme->name); theme->name = strdup (value); } else if (strcmp (key, "description") == 0) { free (theme->description); theme->description = strdup (value); } else if (strcmp (key, "date") == 0) { free (theme->date); theme->date = strdup (value); } else if (strcmp (key, "weechat") == 0) { free (theme->weechat_version); theme->weechat_version = strdup (value); } else { gui_chat_printf ( NULL, _("%s%s: line %d: ignoring unknown [info] key \"%s\""), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], path, line_number, key); } } } fclose (file); if (!theme->name) theme->name = strdup (""); if (!theme->description) theme->description = strdup (""); if (!theme->date) theme->date = strdup (""); if (!theme->weechat_version) theme->weechat_version = strdup (""); return theme; } /* * Applies a theme registered in memory. * * If weechat.look.theme_backup is on (and the target name does not begin * with "backup-"), a backup file is written first; on backup failure the * apply is aborted before any option is changed. * * Iterates the theme's overrides with theme_applying=1 so the per-option * change callbacks skip their gui refresh; a single refresh is performed * at the end. * * Returns WEECHAT_RC_OK on success, WEECHAT_RC_ERROR if the theme name * is unknown or the backup could not be created. */ int theme_apply (const char *name) { struct t_theme *file_theme = NULL; struct t_theme *theme = NULL; struct t_theme_contribution *ptr_contribution; char *path = NULL; char *backup_name = NULL; if (!name || !name[0]) return WEECHAT_RC_ERROR; /* Resolution: a user file with the given name shadows any built-in of the same name. Read the file transiently (parse, apply, free) so user themes have no steady-state memory footprint. */ path = theme_user_file_path (name); if (path && (access (path, R_OK) == 0)) { file_theme = theme_file_parse (path); if (!file_theme) { gui_chat_printf (NULL, _("%sFailed to parse theme file \"%s\""), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], path); free (path); return WEECHAT_RC_ERROR; } theme = file_theme; } else { theme = theme_search (name); if (!theme) { gui_chat_printf (NULL, _("%sTheme \"%s\" not found"), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], name); free (path); return WEECHAT_RC_ERROR; } } free (path); /* create a backup of current themable state, if enabled */ if (CONFIG_BOOLEAN(config_look_theme_backup) && (strncmp (name, "backup-", 7) != 0)) { backup_name = theme_make_backup (); if (!backup_name) { theme_free (file_theme); gui_chat_printf ( NULL, _("%sUnable to create theme backup; aborting apply " "(disable option weechat.look.theme_backup to force)"), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR]); return WEECHAT_RC_ERROR; } } /* Apply each contribution in order; per-option refreshes are suppressed via the theme_applying flag (see config_change_color). Later contributions naturally win for duplicate keys because config_file_option_set is called for each in sequence. */ theme_applying = 1; for (ptr_contribution = theme->contributions; ptr_contribution; ptr_contribution = ptr_contribution->next_contribution) { hashtable_map (ptr_contribution->overrides, &theme_apply_set_option_cb, NULL); } theme_applying = 0; /* file_theme (if any) is transient: discard now */ theme_free (file_theme); /* single refresh at the end */ if (gui_init_ok) { gui_color_init_weechat (); gui_window_ask_refresh (1); } /* persist the active theme label */ config_file_option_set (config_look_theme, name, 1); /* tell the user about the backup */ if (backup_name) { gui_chat_printf ( NULL, _("Previous state saved as theme \"%s\"; to restore: " "/theme apply %s"), backup_name, backup_name); free (backup_name); } hook_signal_send ("theme_applied", WEECHAT_HOOK_SIGNAL_STRING, (char *)name); return WEECHAT_RC_OK; } /* * Resets every themable option to its default value. * * Same backup-first safety as theme_apply: if weechat.look.theme_backup * is on, a backup file is written before any option is touched, and the * reset is aborted if the backup cannot be written. The active-theme * label (weechat.look.theme) is reset to its default (empty string). * * Returns WEECHAT_RC_OK on success, WEECHAT_RC_ERROR if the backup is * required but failed. */ int theme_reset (void) { struct t_config_file *ptr_config; struct t_config_section *ptr_section; struct t_config_option *ptr_option; char *backup_name = NULL; if (CONFIG_BOOLEAN(config_look_theme_backup)) { backup_name = theme_make_backup (); if (!backup_name) { gui_chat_printf ( NULL, _("%sUnable to create theme backup; aborting reset " "(disable option weechat.look.theme_backup to force)"), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR]); return WEECHAT_RC_ERROR; } } /* reset every themable option to its default value; per-option gui refreshes are suppressed via theme_applying */ theme_applying = 1; for (ptr_config = config_files; ptr_config; ptr_config = ptr_config->next_config) { for (ptr_section = ptr_config->sections; ptr_section; ptr_section = ptr_section->next_section) { for (ptr_option = ptr_section->options; ptr_option; ptr_option = ptr_option->next_option) { if (ptr_option->themable) config_file_option_reset (ptr_option, 1); } } } theme_applying = 0; if (gui_init_ok) { gui_color_init_weechat (); gui_window_ask_refresh (1); } /* clear active-theme label */ config_file_option_reset (config_look_theme, 1); if (backup_name) { gui_chat_printf ( NULL, _("Previous state saved as theme \"%s\"; to restore: " "/theme apply %s"), backup_name, backup_name); free (backup_name); } hook_signal_send ("theme_applied", WEECHAT_HOOK_SIGNAL_STRING, (char *)""); return WEECHAT_RC_OK; } /* * Saves the current themable options to a user theme file. * * Refuses names that match a built-in theme (registered via API) or * that start with "backup-" (reserved for automatic backups). If * "full" is non-zero, every themable option is written; otherwise * only options whose value differs from their default are written. * * Returns WEECHAT_RC_OK on success, WEECHAT_RC_ERROR on validation or * I/O failure. */ int theme_save (const char *name, int full) { if (!name || !name[0]) return WEECHAT_RC_ERROR; if (strncmp (name, "backup-", 7) == 0) { gui_chat_printf ( NULL, _("%sName \"%s\" is reserved for automatic backups"), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], name); return WEECHAT_RC_ERROR; } if (theme_search (name)) { gui_chat_printf ( NULL, _("%sName \"%s\" is reserved for a built-in theme"), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], name); return WEECHAT_RC_ERROR; } if (!theme_write_file (name, NULL, (full) ? 0 : 1)) { gui_chat_printf (NULL, _("%sFailed to save theme \"%s\""), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], name); return WEECHAT_RC_ERROR; } gui_chat_printf (NULL, _("Theme saved: %s"), name); return WEECHAT_RC_OK; } /* * Deletes a user theme file. * * Refuses names registered as built-in themes (they have no file). * Returns WEECHAT_RC_OK on success, WEECHAT_RC_ERROR otherwise. */ int theme_delete (const char *name) { char *path; if (!name || !name[0]) return WEECHAT_RC_ERROR; if (theme_search (name)) { gui_chat_printf ( NULL, _("%sCannot delete built-in theme \"%s\""), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], name); return WEECHAT_RC_ERROR; } path = theme_user_file_path (name); if (!path) return WEECHAT_RC_ERROR; if (unlink (path) != 0) { gui_chat_printf (NULL, _("%sFailed to delete theme \"%s\""), gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], name); free (path); return WEECHAT_RC_ERROR; } gui_chat_printf (NULL, _("Theme deleted: %s"), name); free (path); return WEECHAT_RC_OK; } /* * Initializes the theme subsystem. * * The registry starts empty; built-in themes are registered later (by * core and by plugins/scripts at their own init time). */ void theme_init (void) { themes = NULL; last_theme = NULL; theme_applying = 0; } /* * Frees all registered themes and clears the registry. */ void theme_end (void) { struct t_theme *ptr_theme, *next_theme; ptr_theme = themes; while (ptr_theme) { next_theme = ptr_theme->next_theme; theme_free (ptr_theme); ptr_theme = next_theme; } themes = NULL; last_theme = NULL; }