diff --git a/CHANGELOG.md b/CHANGELOG.md index 431f030cb..eeead4c51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ SPDX-License-Identifier: GPL-3.0-or-later ### Added -- core: add `/theme` command with subcommands `list`, `apply`, `reset`, `save`, `delete`, `info`, automatic backup of current themable options before apply, and built-in "light" theme +- core: add `/theme` command with subcommands `list`, `apply`, `reset`, `save`, `rename`, `delete`, `info`, automatic backup of current themable options before apply, and built-in "light" theme - core: detect terminal background on first start and automatically apply the built-in "light" theme when a light terminal is detected - core: add `themable` flag on configuration options (auto-set for color options; explicit opt-in for string options containing `${color:...}` references via the `type|themable` syntax) - core: add option weechat.look.theme (informational, set by `/theme apply`) diff --git a/doc/de/weechat_user.de.adoc b/doc/de/weechat_user.de.adoc index 369d4decf..41faa3f84 100644 --- a/doc/de/weechat_user.de.adoc +++ b/doc/de/weechat_user.de.adoc @@ -2283,6 +2283,19 @@ Reserved names (built-in theme names like `+light+` and any name starting with `+backup-+`) are refused. Files live at `+${weechat_config_dir}/themes/.theme+`. +Rename a user theme (typical use: keep a useful automatic backup +under a meaningful name): + +---- +/theme rename backup-20260525-094210-123456 mybackup +---- + +Built-in themes have no file and cannot be renamed; the target name +cannot match a built-in name or start with `+backup-+`, and the +target file must not already exist. The `+[info]+` `+name+` field +inside the file is rewritten so `/theme info` reports the new name +consistently. + Delete a user theme: ---- diff --git a/doc/en/weechat_user.en.adoc b/doc/en/weechat_user.en.adoc index a623d7560..84122e338 100644 --- a/doc/en/weechat_user.en.adoc +++ b/doc/en/weechat_user.en.adoc @@ -2270,6 +2270,19 @@ Reserved names (built-in theme names like `+light+` and any name starting with `+backup-+`) are refused. Files live at `+${weechat_config_dir}/themes/.theme+`. +Rename a user theme (typical use: keep a useful automatic backup +under a meaningful name): + +---- +/theme rename backup-20260525-094210-123456 mybackup +---- + +Built-in themes have no file and cannot be renamed; the target name +cannot match a built-in name or start with `+backup-+`, and the +target file must not already exist. The `+[info]+` `+name+` field +inside the file is rewritten so `/theme info` reports the new name +consistently. + Delete a user theme: ---- diff --git a/doc/fr/weechat_user.fr.adoc b/doc/fr/weechat_user.fr.adoc index 266e0a1ae..a948dc0f9 100644 --- a/doc/fr/weechat_user.fr.adoc +++ b/doc/fr/weechat_user.fr.adoc @@ -2319,6 +2319,20 @@ Les noms réservés (noms de thèmes intégrés comme `+light+` et tout nom commençant par `+backup-+`) sont refusés. Les fichiers sont placés dans `+${weechat_config_dir}/themes/.theme+`. +Renommer un thème utilisateur (usage typique : conserver une +sauvegarde automatique utile sous un nom plus parlant) : + +---- +/theme rename backup-20260525-094210-123456 maSauvegarde +---- + +Les thèmes intégrés n'ont pas de fichier et ne peuvent pas être +renommés ; le nom cible ne peut pas correspondre à un nom intégré ni +commencer par `+backup-+`, et le fichier cible ne doit pas déjà +exister. Le champ `+name+` de la section `+[info]+` à l'intérieur du +fichier est réécrit afin que `/theme info` affiche le nouveau nom de +manière cohérente. + Supprimer un thème utilisateur : ---- diff --git a/doc/it/weechat_user.it.adoc b/doc/it/weechat_user.it.adoc index b6ccde67e..14f6da472 100644 --- a/doc/it/weechat_user.it.adoc +++ b/doc/it/weechat_user.it.adoc @@ -2525,6 +2525,19 @@ Reserved names (built-in theme names like `+light+` and any name starting with `+backup-+`) are refused. Files live at `+${weechat_config_dir}/themes/.theme+`. +Rename a user theme (typical use: keep a useful automatic backup +under a meaningful name): + +---- +/theme rename backup-20260525-094210-123456 mybackup +---- + +Built-in themes have no file and cannot be renamed; the target name +cannot match a built-in name or start with `+backup-+`, and the +target file must not already exist. The `+[info]+` `+name+` field +inside the file is rewritten so `/theme info` reports the new name +consistently. + Delete a user theme: ---- diff --git a/doc/ja/weechat_user.ja.adoc b/doc/ja/weechat_user.ja.adoc index a353d861c..d271f08f4 100644 --- a/doc/ja/weechat_user.ja.adoc +++ b/doc/ja/weechat_user.ja.adoc @@ -2461,6 +2461,19 @@ Reserved names (built-in theme names like `+light+` and any name starting with `+backup-+`) are refused. Files live at `+${weechat_config_dir}/themes/.theme+`. +Rename a user theme (typical use: keep a useful automatic backup +under a meaningful name): + +---- +/theme rename backup-20260525-094210-123456 mybackup +---- + +Built-in themes have no file and cannot be renamed; the target name +cannot match a built-in name or start with `+backup-+`, and the +target file must not already exist. The `+[info]+` `+name+` field +inside the file is rewritten so `/theme info` reports the new name +consistently. + Delete a user theme: ---- diff --git a/doc/pl/weechat_user.pl.adoc b/doc/pl/weechat_user.pl.adoc index 878c91fdb..345ce7371 100644 --- a/doc/pl/weechat_user.pl.adoc +++ b/doc/pl/weechat_user.pl.adoc @@ -2277,6 +2277,19 @@ Reserved names (built-in theme names like `+light+` and any name starting with `+backup-+`) are refused. Files live at `+${weechat_config_dir}/themes/.theme+`. +Rename a user theme (typical use: keep a useful automatic backup +under a meaningful name): + +---- +/theme rename backup-20260525-094210-123456 mybackup +---- + +Built-in themes have no file and cannot be renamed; the target name +cannot match a built-in name or start with `+backup-+`, and the +target file must not already exist. The `+[info]+` `+name+` field +inside the file is rewritten so `/theme info` reports the new name +consistently. + Delete a user theme: ---- diff --git a/doc/sr/weechat_user.sr.adoc b/doc/sr/weechat_user.sr.adoc index 41173e1f4..9e7287d07 100644 --- a/doc/sr/weechat_user.sr.adoc +++ b/doc/sr/weechat_user.sr.adoc @@ -2179,6 +2179,19 @@ Reserved names (built-in theme names like `+light+` and any name starting with `+backup-+`) are refused. Files live at `+${weechat_config_dir}/themes/.theme+`. +Rename a user theme (typical use: keep a useful automatic backup +under a meaningful name): + +---- +/theme rename backup-20260525-094210-123456 mybackup +---- + +Built-in themes have no file and cannot be renamed; the target name +cannot match a built-in name or start with `+backup-+`, and the +target file must not already exist. The `+[info]+` `+name+` field +inside the file is rewritten so `/theme info` reports the new name +consistently. + Delete a user theme: ---- diff --git a/src/core/core-command.c b/src/core/core-command.c index 9671e3391..43130c55b 100644 --- a/src/core/core-command.c +++ b/src/core/core-command.c @@ -7349,6 +7349,13 @@ COMMAND_CALLBACK(theme) ? 1 : 0); } + /* "/theme rename ": rename a user theme file */ + if (string_strcmp (argv[1], "rename") == 0) + { + COMMAND_MIN_ARGS(4, "rename"); + return theme_rename (argv[2], argv[3]); + } + /* "/theme delete ": remove a user theme file */ if (string_strcmp (argv[1], "delete") == 0) { @@ -10081,6 +10088,7 @@ command_init (void) " || apply " " || reset" " || save [-full]" + " || rename " " || delete " " || info "), CMD_ARGS_DESC( @@ -10101,6 +10109,11 @@ command_init (void) "written, use \"-full\" to write every themable option; " "the name must not match a built-in theme or start with " "\"backup-\""), + N_("raw[rename]: rename a user theme file (typically to " + "give an automatic backup a meaningful name); refuses to " + "rename built-in themes, refuses target names matching a " + "built-in or starting with \"backup-\", and refuses if " + "the target file already exists"), 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, " @@ -10123,6 +10136,7 @@ command_init (void) " || apply %(theme_themes_all)" " || reset" " || save %(theme_themes_user) -full" + " || rename %(theme_themes_files)" " || delete %(theme_themes_user)" " || info %(theme_themes_all)", &command_theme, NULL, NULL); diff --git a/src/core/core-completion.c b/src/core/core-completion.c index bdcde1fe5..99906dfb9 100644 --- a/src/core/core-completion.c +++ b/src/core/core-completion.c @@ -2086,6 +2086,40 @@ completion_list_add_theme_themes_user_cb (const void *pointer, void *data, return WEECHAT_RC_OK; } +/* + * Add every on-disk theme file (user files + backups, no built-ins) + * to the completion list; suitable for /theme rename which can take a + * backup as its source. + */ + +int +completion_list_add_theme_themes_files_cb (const void *pointer, void *data, + const char *completion_item, + struct t_gui_buffer *buffer, + struct t_gui_completion *completion) +{ + struct t_completion_theme_dir ctx; + char *dir; + + /* make C compiler happy */ + (void) pointer; + (void) data; + (void) completion_item; + (void) buffer; + + dir = NULL; + string_asprintf (&dir, "%s/themes", weechat_config_dir); + if (dir) + { + ctx.completion = completion; + ctx.show_backups = 1; + dir_exec_on_files (dir, 0, 0, &completion_theme_add_file_cb, &ctx); + free (dir); + } + + return WEECHAT_RC_OK; +} + /* * Add a secured data to completion list. */ @@ -2483,6 +2517,10 @@ completion_init (void) hook_completion (NULL, "theme_themes_user", N_("names of user theme files (excludes built-ins and backups)"), &completion_list_add_theme_themes_user_cb, NULL, NULL); + hook_completion (NULL, "theme_themes_files", + N_("names of theme files on disk (user files + backups, " + "no built-ins)"), + &completion_list_add_theme_themes_files_cb, NULL, NULL); hook_completion (NULL, "secured_data", N_("names of secured data (file sec.conf, section data)"), &completion_list_add_secured_data_cb, NULL, NULL); diff --git a/src/core/core-theme.c b/src/core/core-theme.c index 0727896f3..b0560148a 100644 --- a/src/core/core-theme.c +++ b/src/core/core-theme.c @@ -1175,6 +1175,160 @@ theme_save (const char *name, int full) return WEECHAT_RC_OK; } +/* + * Rename a user theme file. + * + * Refuse to rename a built-in (no file) or to a name reserved for + * built-ins or automatic backups. The target name must not already + * exist on disk. The file content is copied with the [info] name + * field rewritten so the parsed theme name stays consistent with the + * new filename. If "weechat.look.theme" was pointing at the old name, + * it is updated to the new name. + * + * Return WEECHAT_RC_OK on success, WEECHAT_RC_ERROR on validation or + * I/O failure (in which case no file is created or removed). + */ + +int +theme_rename (const char *old_name, const char *new_name) +{ + char *old_path, *new_path, line[2048]; + FILE *fin, *fout; + const char *trimmed; + int in_info, name_done; + + if (!old_name || !old_name[0] || !new_name || !new_name[0]) + return WEECHAT_RC_ERROR; + + if (theme_search (old_name)) + { + gui_chat_printf (NULL, + _("%sCannot rename built-in theme \"%s\""), + gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], + old_name); + return WEECHAT_RC_ERROR; + } + + if (strcmp (old_name, new_name) == 0) + { + gui_chat_printf (NULL, + _("%sNew name is the same as old name"), + gui_chat_prefix[GUI_CHAT_PREFIX_ERROR]); + return WEECHAT_RC_ERROR; + } + + if (strncmp (new_name, "backup-", 7) == 0) + { + gui_chat_printf (NULL, + _("%sName \"%s\" is reserved for automatic backups"), + gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], + new_name); + return WEECHAT_RC_ERROR; + } + + if (theme_search (new_name)) + { + gui_chat_printf (NULL, + _("%sName \"%s\" is reserved for a built-in theme"), + gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], + new_name); + return WEECHAT_RC_ERROR; + } + + old_path = theme_user_file_path (old_name); + if (!old_path) + return WEECHAT_RC_ERROR; + if (access (old_path, R_OK) != 0) + { + gui_chat_printf (NULL, + _("%sTheme \"%s\" not found"), + gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], + old_name); + free (old_path); + return WEECHAT_RC_ERROR; + } + + new_path = theme_user_file_path (new_name); + if (!new_path) + { + free (old_path); + return WEECHAT_RC_ERROR; + } + if (access (new_path, F_OK) == 0) + { + gui_chat_printf (NULL, + _("%sTheme \"%s\" already exists"), + gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], + new_name); + free (old_path); + free (new_path); + return WEECHAT_RC_ERROR; + } + + fin = fopen (old_path, "r"); + fout = (fin) ? fopen (new_path, "w") : NULL; + if (!fin || !fout) + { + if (fin) + fclose (fin); + gui_chat_printf (NULL, + _("%sFailed to rename theme \"%s\" to \"%s\""), + gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], + old_name, new_name); + free (old_path); + free (new_path); + return WEECHAT_RC_ERROR; + } + + in_info = 0; + name_done = 0; + while (fgets (line, sizeof (line), fin)) + { + trimmed = line; + while (*trimmed == ' ' || *trimmed == '\t') + trimmed++; + if (*trimmed == '[') + { + in_info = (strncmp (trimmed, "[info]", 6) == 0); + fputs (line, fout); + continue; + } + if (in_info && !name_done + && trimmed[0] == 'n' && trimmed[1] == 'a' + && trimmed[2] == 'm' && trimmed[3] == 'e' + && (trimmed[4] == ' ' || trimmed[4] == '\t' || trimmed[4] == '=')) + { + fprintf (fout, "name = \"%s\"\n", new_name); + name_done = 1; + continue; + } + fputs (line, fout); + } + fclose (fin); + if (fclose (fout) != 0 || unlink (old_path) != 0) + { + unlink (new_path); + gui_chat_printf (NULL, + _("%sFailed to rename theme \"%s\" to \"%s\""), + gui_chat_prefix[GUI_CHAT_PREFIX_ERROR], + old_name, new_name); + free (old_path); + free (new_path); + return WEECHAT_RC_ERROR; + } + + if (strcmp (CONFIG_STRING(config_look_theme), old_name) == 0) + config_file_option_set (config_look_theme, new_name, 1); + + gui_chat_printf (NULL, + _("Theme \"%s\" renamed to \"%s\""), + old_name, new_name); + + free (old_path); + free (new_path); + return WEECHAT_RC_OK; +} + /* * Delete a user theme file. * diff --git a/src/core/core-theme.h b/src/core/core-theme.h index bd3ea80a3..da403d1e8 100644 --- a/src/core/core-theme.h +++ b/src/core/core-theme.h @@ -71,6 +71,7 @@ extern struct t_arraylist *theme_list (void); extern int theme_apply (const char *name); extern int theme_reset (void); extern int theme_save (const char *name, int full); +extern int theme_rename (const char *old_name, const char *new_name); extern int theme_delete (const char *name); extern char *theme_make_backup (void); extern char *theme_user_file_path (const char *name); diff --git a/tests/unit/core/test-core-theme.cpp b/tests/unit/core/test-core-theme.cpp index 6ab99b9f4..c9281e5e8 100644 --- a/tests/unit/core/test-core-theme.cpp +++ b/tests/unit/core/test-core-theme.cpp @@ -949,6 +949,82 @@ TEST(CoreTheme, Delete) free (path); } +/* + * Test functions: + * theme_rename + */ + +TEST(CoreTheme, Rename) +{ + char *src_path, *dst_path; + struct stat st; + FILE *file; + char buf[2048]; + size_t len; + + /* NULL / empty arguments => error */ + LONGS_EQUAL(WEECHAT_RC_ERROR, theme_rename (NULL, "dst")); + LONGS_EQUAL(WEECHAT_RC_ERROR, theme_rename ("src", NULL)); + LONGS_EQUAL(WEECHAT_RC_ERROR, theme_rename ("", "dst")); + LONGS_EQUAL(WEECHAT_RC_ERROR, theme_rename ("src", "")); + + /* refuses to rename a built-in (no file to rename) */ + theme_register (NULL, NULL, "dark", NULL); + LONGS_EQUAL(WEECHAT_RC_ERROR, theme_rename ("dark", "renamed")); + + /* refuses target == reserved "backup-" prefix */ + LONGS_EQUAL(WEECHAT_RC_OK, theme_save ("rn_src", 0)); + LONGS_EQUAL(WEECHAT_RC_ERROR, theme_rename ("rn_src", "backup-foo")); + + /* refuses target == built-in name */ + LONGS_EQUAL(WEECHAT_RC_ERROR, theme_rename ("rn_src", "dark")); + + /* refuses same name */ + LONGS_EQUAL(WEECHAT_RC_ERROR, theme_rename ("rn_src", "rn_src")); + + /* source missing => error */ + LONGS_EQUAL(WEECHAT_RC_ERROR, theme_rename ("does_not_exist", "rn_dst")); + + /* refuses target that already exists */ + LONGS_EQUAL(WEECHAT_RC_OK, theme_save ("rn_dst", 0)); + LONGS_EQUAL(WEECHAT_RC_ERROR, theme_rename ("rn_src", "rn_dst")); + LONGS_EQUAL(WEECHAT_RC_OK, theme_delete ("rn_dst")); + + /* happy path: rename moves the file and rewrites the [info] name */ + src_path = theme_user_file_path ("rn_src"); + dst_path = theme_user_file_path ("rn_dst"); + CHECK(src_path != NULL); + CHECK(dst_path != NULL); + + LONGS_EQUAL(WEECHAT_RC_OK, theme_rename ("rn_src", "rn_dst")); + + /* old file gone, new file exists */ + LONGS_EQUAL(-1, stat (src_path, &st)); + LONGS_EQUAL(0, stat (dst_path, &st)); + + /* [info] name field inside the renamed file is updated */ + file = fopen (dst_path, "r"); + CHECK(file != NULL); + len = fread (buf, 1, sizeof (buf) - 1, file); + buf[len] = '\0'; + fclose (file); + CHECK(strstr (buf, "name = \"rn_dst\"") != NULL); + CHECK(strstr (buf, "name = \"rn_src\"") == NULL); + + /* if weechat.look.theme pointed at the old name, the label moves too */ + LONGS_EQUAL(WEECHAT_RC_OK, theme_save ("rn_active", 0)); + config_file_option_set (config_look_theme, "rn_active", 1); + LONGS_EQUAL(WEECHAT_RC_OK, theme_rename ("rn_active", "rn_moved")); + STRCMP_EQUAL("rn_moved", CONFIG_STRING(config_look_theme)); + + /* cleanup */ + config_file_option_reset (config_look_theme, 1); + theme_delete ("rn_dst"); + theme_delete ("rn_moved"); + free (src_path); + free (dst_path); +} + /* * Test functions: * theme_init