1
0
mirror of https://github.com/weechat/weechat.git synced 2026-06-28 13:56:37 +02:00
Files
weechat/src/core/core-theme.c
T
Sébastien Helleu eecf1e3e29 core: implement theme file parsing and transient file reads in /theme apply
Add a small INI-style parser for *.theme files and wire it into the
/theme command so user themes living in directory "themes" inside the
WeeChat configuration directory can be applied (and inspected) without
ever being cached.

Parser (theme_file_parse in core-theme.c) accepts two sections:

  [info]
  name = "..."          \ shown by /theme info; ignored for apply
  description = "..."   |
  date = "..."          |
  weechat = "..."       /
  (unknown keys are ignored with a warning)

  [options]
  full.option.name = "value"

Surrounding single or double quotes around a value are stripped (same
rule used by the regular config file reader). The parsed result is a
heap-allocated t_theme; the caller frees with theme_free.

Resolution rule in theme_apply: if the path
"${weechat_config_dir}/themes/<name>.theme" is readable it is parsed
and used (file shadows any built-in of the same name); otherwise the
built-in registry is consulted. The transient t_theme is freed before
the final refresh, so user themes have no steady-state memory
footprint regardless of how many .theme files have accumulated.

/theme list now also scans the themes directory and appends user
files to the listing (each marked "(file)"). backup-*.theme are
hidden by default; pass "-backups" to include them.

/theme info <name> works for both sources: file path is shown when the
information comes from disk; "built-in (in-memory)" otherwise.
2026-06-03 22:26:13 +02:00

810 lines
21 KiB
C

/*
* SPDX-FileCopyrightText: 2026 Sébastien Helleu <flashcode@flashtux.org>
*
* 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 <https://www.gnu.org/licenses/>.
*/
/* Themes: named bundles of option overrides applied via /theme command */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#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 ());
new_theme->overrides = hashtable_new (32,
WEECHAT_HASHTABLE_STRING,
WEECHAT_HASHTABLE_STRING,
NULL, NULL);
if (!new_theme->name || !new_theme->description
|| !new_theme->date || !new_theme->weechat_version
|| !new_theme->overrides)
{
free (new_theme->name);
free (new_theme->description);
free (new_theme->date);
free (new_theme->weechat_version);
hashtable_free (new_theme->overrides);
free (new_theme);
return NULL;
}
return new_theme;
}
/*
* Frees a theme (does not unlink from registry; caller handles that).
*/
void
theme_free (struct t_theme *theme)
{
if (!theme)
return;
free (theme->name);
free (theme->description);
free (theme->date);
free (theme->weechat_version);
hashtable_free (theme->overrides);
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);
}
/*
* Registers a theme by name with a set of option overrides.
*
* If a theme with the given name already exists, the provided overrides
* are merged into the existing theme's hashtable (later registrations
* override earlier ones for duplicate keys). This lets plugins/scripts
* register their per-theme contributions without coordinating with core.
*
* The "overrides" hashtable passed in is read-only from this function's
* perspective; the caller retains ownership and may free it.
*
* Returns pointer to theme (existing or newly created), NULL on error.
*/
struct t_theme *
theme_register (const char *name, struct t_hashtable *overrides)
{
struct t_theme *theme;
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)
{
hashtable_map (overrides,
&theme_merge_overrides_cb,
theme->overrides);
}
return theme;
}
/*
* 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:
* "<weechat_config_dir>/themes/<name>.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 full snapshot of every themable option to a .theme file at
* "<weechat_config_dir>/themes/<name>.theme".
*
* The themes directory is created if missing. The file contains an
* [info] section (name, description, date, weechat version) followed by
* an [options] section listing every themable option's current value.
*
* Returns 1 on success, 0 on error.
*/
int
theme_write_file_full (const char *name, const char *description)
{
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;
value = config_file_option_value_to_string (
ptr_option, 0, 1, 0);
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_full (
name,
_("Automatic backup written before /theme apply")))
{
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;
if (!path)
return NULL;
file = fopen (path, "r");
if (!file)
return NULL;
theme = theme_alloc ("");
if (!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 (theme->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 *registry_theme = NULL;
struct t_hashtable *overrides = NULL;
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;
}
overrides = file_theme->overrides;
}
else
{
registry_theme = theme_search (name);
if (!registry_theme)
{
gui_chat_printf (NULL,
_("%sTheme \"%s\" not found"),
gui_chat_prefix[GUI_CHAT_PREFIX_ERROR],
name);
free (path);
return WEECHAT_RC_ERROR;
}
overrides = registry_theme->overrides;
}
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 override; per-option refreshes are suppressed via the
theme_applying flag (see config_change_color) */
theme_applying = 1;
hashtable_map (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;
}
/*
* 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;
}