1
0
mirror of https://github.com/weechat/weechat.git synced 2026-07-05 17:23:15 +02:00

core: implement /theme apply with themable enforcement and auto-backup

Implement /theme apply <name> for themes currently in the in-memory
registry. The file-shadowing branch (read a .theme file from
${weechat_config_dir}/themes/ when no built-in matches) is added in
the next commit together with the parser.

Apply algorithm (theme_apply in core-theme.c):

- Look up the theme in the registry; abort with an error if unknown.
- If weechat.look.theme_backup is on and the target name does not
  begin with "backup-", write a full snapshot of every themable
  option to ${weechat_config_dir}/themes/backup-<timestamp>.theme
  via theme_make_backup; abort the apply if the backup cannot be
  written, so the user can always undo.
- Iterate the theme's overrides with theme_applying=1 so the
  per-option config_change_color skips its gui refresh; for each
  entry look up the option, refuse it if missing or non-themable
  (warning to core buffer), otherwise call config_file_option_set.
- Perform a single gui_color_init_weechat + gui_window_ask_refresh
  at the end.
- Persist the active label in weechat.look.theme and send signal
  "theme_applied" with the name as data.

Add the new option weechat.look.theme_backup (boolean, default on)
which controls the backup-or-abort behavior described above.

Wire the new /theme apply subcommand into core-command.c with the
existing /theme registration; update help text accordingly.
This commit is contained in:
Sébastien Helleu
2026-07-04 14:51:58 +02:00
parent abea2e1449
commit 23a8a97ad9
21 changed files with 1205 additions and 32 deletions
+292
View File
@@ -25,16 +25,25 @@
#include "config.h"
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.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"
@@ -248,6 +257,289 @@ theme_list (void)
return list;
}
/*
* Build the on-disk path for a user theme:
* "<weechat_config_dir>/themes/<name>.theme".
*
* Return NULL on error.
*
* Note: result must be freed after use.
*/
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;
}
/*
* Build a unique backup theme name "backup-YYYYMMDD-HHMMSS-uuuuuu".
*
* Return NULL on error.
*
* Note: result must be freed after use.
*/
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);
}
/*
* Write 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.
*
* Return:
* 1: success
* 0: 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, 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;
}
/*
* Create a timestamped backup theme file with the current themable state.
*
* Return the backup name, NULL on failure.
*
* Note: result must be freed after use.
*/
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;
}
/*
* Apply one override entry (callback for hashtable_map during apply).
*
* Refuse 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);
}
/*
* Apply 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.
*
* Iterate 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.
*
* Return:
* WEECHAT_RC_OK: success
* WEECHAT_RC_ERROR: theme name is unknown or the backup could not be created
*/
int
theme_apply (const char *name)
{
struct t_theme *theme;
char *backup_name = NULL;
if (!name || !name[0])
return WEECHAT_RC_ERROR;
theme = theme_search (name);
if (!theme)
{
gui_chat_printf (NULL,
_("%sTheme \"%s\" not found"),
gui_chat_prefix[GUI_CHAT_PREFIX_ERROR],
name);
return WEECHAT_RC_ERROR;
}
/* 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)
{
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 (theme->overrides, &theme_apply_set_option_cb, NULL);
theme_applying = 0;
/* 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;
}
/*
* Initialize the theme subsystem.
*