mirror of
https://github.com/weechat/weechat.git
synced 2026-06-12 14:14:48 +02:00
core: implement /theme save and /theme delete
Add two complementary subcommands:
/theme save <name> [-full]: writes a user theme file at
${weechat_config_dir}/themes/<name>.theme containing the current
themable options. By default only options whose value differs from
their default (config_file_option_has_changed) are written, which
keeps the file small and focused. Pass "-full" to write every
themable option (matches the format used by automatic backups).
Name validation: refuses any name matching a built-in theme (those
are reserved for in-memory registrations) and any name starting
with "backup-" (reserved for /theme apply backups). Both checks
print an error and abort without writing.
/theme delete <name>: removes ${weechat_config_dir}/themes/<name>.theme
via unlink. Refuses to delete a name registered as a built-in
theme (a built-in has no file on disk to delete, even if the user
has a shadowing file of the same name they cannot remove it this
way; they can rename or delete it manually).
The full-snapshot writer used by /theme apply backups is refactored
into theme_write_file (name, description, diff_only). It is reused
by theme_make_backup (diff_only=0) and theme_save (diff_only inverted
from the user's -full flag).
Bug fix while at it: the writer was previously calling
config_file_option_value_to_string (ptr_option, 0, 1, 0); the third
and fourth arguments are "use_colors" and "use_delimiters", so the
call inserted GUI color escape codes into the file output and skipped
quoting strings. Corrected to (ptr_option, 0, 0, 1) so plain text
with proper string quoting is written; the change also fixes the
content of files produced by theme_make_backup in the previous commit.
This commit is contained in:
@@ -7333,6 +7333,23 @@ COMMAND_CALLBACK(theme)
|
||||
return theme_apply (argv[2]);
|
||||
}
|
||||
|
||||
/* "/theme save <name> [-full]": write a user theme file */
|
||||
if (string_strcmp (argv[1], "save") == 0)
|
||||
{
|
||||
COMMAND_MIN_ARGS(3, "save");
|
||||
return theme_save (argv[2],
|
||||
((argc >= 4)
|
||||
&& (string_strcmp (argv[3], "-full") == 0))
|
||||
? 1 : 0);
|
||||
}
|
||||
|
||||
/* "/theme delete <name>": remove a user theme file */
|
||||
if (string_strcmp (argv[1], "delete") == 0)
|
||||
{
|
||||
COMMAND_MIN_ARGS(3, "delete");
|
||||
return theme_delete (argv[2]);
|
||||
}
|
||||
|
||||
/* "/theme info <name>": show details about a theme */
|
||||
if (string_strcmp (argv[1], "info") == 0)
|
||||
{
|
||||
@@ -10057,6 +10074,8 @@ command_init (void)
|
||||
/* TRANSLATORS: only text between angle brackets (eg: "<name>") may be translated */
|
||||
N_("[list [-backups]]"
|
||||
" || apply <name>"
|
||||
" || save <name> [-full]"
|
||||
" || delete <name>"
|
||||
" || info <name>"),
|
||||
CMD_ARGS_DESC(
|
||||
N_("raw[list]: list registered themes and any *.theme files in "
|
||||
@@ -10068,6 +10087,14 @@ command_init (void)
|
||||
"value from the theme); if a file named <name>.theme "
|
||||
"exists in directory \"themes\" it shadows any built-in "
|
||||
"theme of the same name"),
|
||||
N_("raw[save]: save current themable options to a file "
|
||||
"<name>.theme in directory \"themes\"; by default only "
|
||||
"options whose value differs from their default are "
|
||||
"written, use \"-full\" to write every themable option; "
|
||||
"the name must not match a built-in theme or start with "
|
||||
"\"backup-\""),
|
||||
N_("raw[delete]: delete a user theme file (refuses to delete "
|
||||
"built-in themes, which have no file)"),
|
||||
N_("raw[info]: display details on a theme (name, description, "
|
||||
"creation date, WeeChat version, number of option overrides)"),
|
||||
N_("name: name of a theme"),
|
||||
@@ -10086,6 +10113,8 @@ command_init (void)
|
||||
"weechat.look.theme_backup.")),
|
||||
"list -backups"
|
||||
" || apply"
|
||||
" || save -full"
|
||||
" || delete"
|
||||
" || info",
|
||||
&command_theme, NULL, NULL);
|
||||
hook_command (
|
||||
|
||||
+112
-6
@@ -308,18 +308,22 @@ theme_make_backup_name (void)
|
||||
}
|
||||
|
||||
/*
|
||||
* Writes a full snapshot of every themable option to a .theme file at
|
||||
* Writes a snapshot of themable options 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.
|
||||
* 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_full (const char *name, const char *description)
|
||||
theme_write_file (const char *name, const char *description, int diff_only)
|
||||
{
|
||||
char *path, *dir, *value, *now;
|
||||
FILE *file;
|
||||
@@ -368,8 +372,10 @@ theme_write_file_full (const char *name, const char *description)
|
||||
{
|
||||
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, 1, 0);
|
||||
ptr_option, 0, 0, 1);
|
||||
fprintf (file, "%s.%s.%s = %s\n",
|
||||
ptr_config->name, ptr_section->name,
|
||||
ptr_option->name,
|
||||
@@ -397,9 +403,10 @@ theme_make_backup (void)
|
||||
name = theme_make_backup_name ();
|
||||
if (!name)
|
||||
return NULL;
|
||||
if (!theme_write_file_full (
|
||||
if (!theme_write_file (
|
||||
name,
|
||||
_("Automatic backup written before /theme apply")))
|
||||
_("Automatic backup written before /theme apply"),
|
||||
0)) /* full snapshot: backups must round-trip exactly */
|
||||
{
|
||||
free (name);
|
||||
return NULL;
|
||||
@@ -773,6 +780,105 @@ theme_apply (const char *name)
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -45,6 +45,8 @@ extern struct t_theme *theme_register (const char *name,
|
||||
struct t_hashtable *overrides);
|
||||
extern struct t_arraylist *theme_list (void);
|
||||
extern int theme_apply (const char *name);
|
||||
extern int theme_save (const char *name, int full);
|
||||
extern int theme_delete (const char *name);
|
||||
extern char *theme_make_backup (void);
|
||||
extern char *theme_user_file_path (const char *name);
|
||||
extern struct t_theme *theme_file_parse (const char *path);
|
||||
|
||||
@@ -47,7 +47,8 @@ extern struct t_theme *theme_alloc (const char *name);
|
||||
extern void theme_free (struct t_theme *theme);
|
||||
extern char *theme_user_file_path (const char *name);
|
||||
extern char *theme_make_backup_name (void);
|
||||
extern int theme_write_file_full (const char *name, const char *description);
|
||||
extern int theme_write_file (const char *name, const char *description,
|
||||
int diff_only);
|
||||
extern char *theme_file_strip_quotes (char *value);
|
||||
extern struct t_theme *theme_file_parse (const char *path);
|
||||
}
|
||||
@@ -330,22 +331,22 @@ TEST(CoreTheme, MakeBackupName)
|
||||
|
||||
/*
|
||||
* Test functions:
|
||||
* theme_write_file_full
|
||||
* theme_write_file
|
||||
*/
|
||||
|
||||
TEST(CoreTheme, WriteFileFull)
|
||||
TEST(CoreTheme, WriteFile)
|
||||
{
|
||||
char *path, line[8192];
|
||||
FILE *file;
|
||||
int saw_info, saw_name, saw_description, saw_date, saw_weechat;
|
||||
int saw_options_section, saw_an_option;
|
||||
int saw_options_section, full_options, diff_options;
|
||||
|
||||
/* refuse empty/NULL */
|
||||
LONGS_EQUAL(0, theme_write_file_full (NULL, NULL));
|
||||
LONGS_EQUAL(0, theme_write_file_full ("", NULL));
|
||||
LONGS_EQUAL(0, theme_write_file (NULL, NULL, 0));
|
||||
LONGS_EQUAL(0, theme_write_file ("", NULL, 0));
|
||||
|
||||
/* write a valid file */
|
||||
LONGS_EQUAL(1, theme_write_file_full ("test_wrt", "a description"));
|
||||
/* full snapshot: every themable option is written */
|
||||
LONGS_EQUAL(1, theme_write_file ("test_wrt", "a description", 0));
|
||||
|
||||
path = theme_user_file_path ("test_wrt");
|
||||
CHECK(path != NULL);
|
||||
@@ -354,7 +355,7 @@ TEST(CoreTheme, WriteFileFull)
|
||||
CHECK(file != NULL);
|
||||
|
||||
saw_info = saw_name = saw_description = saw_date = saw_weechat = 0;
|
||||
saw_options_section = saw_an_option = 0;
|
||||
saw_options_section = full_options = 0;
|
||||
while (fgets (line, sizeof (line) - 1, file))
|
||||
{
|
||||
if (strncmp (line, "[info]", 6) == 0)
|
||||
@@ -372,7 +373,7 @@ TEST(CoreTheme, WriteFileFull)
|
||||
else if (saw_options_section
|
||||
&& (strchr (line, '=') != NULL)
|
||||
&& (strchr (line, '.') != NULL))
|
||||
saw_an_option = 1;
|
||||
full_options++;
|
||||
}
|
||||
fclose (file);
|
||||
|
||||
@@ -382,7 +383,30 @@ TEST(CoreTheme, WriteFileFull)
|
||||
LONGS_EQUAL(1, saw_date);
|
||||
LONGS_EQUAL(1, saw_weechat);
|
||||
LONGS_EQUAL(1, saw_options_section);
|
||||
LONGS_EQUAL(1, saw_an_option);
|
||||
CHECK(full_options > 10); /* core has many themable options */
|
||||
|
||||
unlink (path);
|
||||
|
||||
/* diff-only snapshot in a freshly initialized config writes very
|
||||
few (typically zero) [options] entries — never more than the
|
||||
full snapshot */
|
||||
LONGS_EQUAL(1, theme_write_file ("test_wrt", NULL, 1));
|
||||
|
||||
file = fopen (path, "r");
|
||||
CHECK(file != NULL);
|
||||
diff_options = 0;
|
||||
saw_options_section = 0;
|
||||
while (fgets (line, sizeof (line) - 1, file))
|
||||
{
|
||||
if (strncmp (line, "[options]", 9) == 0)
|
||||
saw_options_section = 1;
|
||||
else if (saw_options_section
|
||||
&& (strchr (line, '=') != NULL)
|
||||
&& (strchr (line, '.') != NULL))
|
||||
diff_options++;
|
||||
}
|
||||
fclose (file);
|
||||
CHECK(diff_options < full_options);
|
||||
|
||||
unlink (path);
|
||||
free (path);
|
||||
@@ -619,6 +643,80 @@ TEST(CoreTheme, FileParse)
|
||||
unlink (path);
|
||||
}
|
||||
|
||||
/*
|
||||
* Test functions:
|
||||
* theme_save
|
||||
*/
|
||||
|
||||
TEST(CoreTheme, Save)
|
||||
{
|
||||
char *path;
|
||||
struct stat st;
|
||||
|
||||
/* NULL / empty => error, no file */
|
||||
LONGS_EQUAL(WEECHAT_RC_ERROR, theme_save (NULL, 0));
|
||||
LONGS_EQUAL(WEECHAT_RC_ERROR, theme_save ("", 0));
|
||||
|
||||
/* reserved "backup-" prefix => error */
|
||||
LONGS_EQUAL(WEECHAT_RC_ERROR, theme_save ("backup-anything", 0));
|
||||
LONGS_EQUAL(WEECHAT_RC_ERROR, theme_save ("backup-anything", 1));
|
||||
|
||||
/* name colliding with a built-in is refused */
|
||||
theme_register ("dark", NULL);
|
||||
LONGS_EQUAL(WEECHAT_RC_ERROR, theme_save ("dark", 0));
|
||||
LONGS_EQUAL(WEECHAT_RC_ERROR, theme_save ("dark", 1));
|
||||
|
||||
/* happy path: sparse save => file exists */
|
||||
LONGS_EQUAL(WEECHAT_RC_OK, theme_save ("save_test", 0));
|
||||
path = theme_user_file_path ("save_test");
|
||||
CHECK(path != NULL);
|
||||
LONGS_EQUAL(0, stat (path, &st));
|
||||
unlink (path);
|
||||
free (path);
|
||||
|
||||
/* happy path: full snapshot => file exists, bigger than sparse */
|
||||
LONGS_EQUAL(WEECHAT_RC_OK, theme_save ("save_test", 1));
|
||||
path = theme_user_file_path ("save_test");
|
||||
CHECK(path != NULL);
|
||||
LONGS_EQUAL(0, stat (path, &st));
|
||||
CHECK(st.st_size > 0);
|
||||
unlink (path);
|
||||
free (path);
|
||||
}
|
||||
|
||||
/*
|
||||
* Test functions:
|
||||
* theme_delete
|
||||
*/
|
||||
|
||||
TEST(CoreTheme, Delete)
|
||||
{
|
||||
char *path;
|
||||
struct stat st;
|
||||
|
||||
/* NULL / empty => error */
|
||||
LONGS_EQUAL(WEECHAT_RC_ERROR, theme_delete (NULL));
|
||||
LONGS_EQUAL(WEECHAT_RC_ERROR, theme_delete (""));
|
||||
|
||||
/* refuses to delete a built-in (no file to delete) */
|
||||
theme_register ("dark", NULL);
|
||||
LONGS_EQUAL(WEECHAT_RC_ERROR, theme_delete ("dark"));
|
||||
|
||||
/* missing file => error */
|
||||
LONGS_EQUAL(WEECHAT_RC_ERROR, theme_delete ("does_not_exist"));
|
||||
|
||||
/* happy path: write a file via theme_save (also ensures the themes
|
||||
directory exists), delete it, confirm it is gone */
|
||||
LONGS_EQUAL(WEECHAT_RC_OK, theme_save ("del_test", 0));
|
||||
path = theme_user_file_path ("del_test");
|
||||
CHECK(path != NULL);
|
||||
LONGS_EQUAL(0, stat (path, &st));
|
||||
|
||||
LONGS_EQUAL(WEECHAT_RC_OK, theme_delete ("del_test"));
|
||||
LONGS_EQUAL(-1, stat (path, &st));
|
||||
free (path);
|
||||
}
|
||||
|
||||
/*
|
||||
* Test functions:
|
||||
* theme_init
|
||||
|
||||
Reference in New Issue
Block a user