Theme files saved with /theme save and automatic backups now always
contain every themable option (full snapshot), so a theme file is
self-contained and round-trips exactly regardless of the current
configuration. The diff-only mode and the "-full" argument of
/theme save are removed.
Detect the terminal background (via COLORFGBG or an OSC 11 query) before
GUI init and, on the first start, automatically apply the built-in
"light" theme when a light terminal is detected.
The function gui_term_theme_is_light returns an int (1 if light, 0 otherwise,
0 being the safe value when detection is unsure).
Add six entries to the "Added" section of Version 4.10.0 in
CHANGELOG.md:
- the /theme command with its subcommands (list/apply/save/delete/
info) and the automatic backup mechanism, plus the shipped
"light" built-in theme;
- the themable flag on configuration options;
- the weechat.look.theme and weechat.look.theme_backup options;
- the theme_register plugin/script API function;
- the t:themable filter in fset.
No UPGRADING.md entry: the change is purely additive — default option
values are unchanged, so existing users see no visible difference
until they explicitly /theme apply light. weechat.look.theme is a new
option (empty by default at first launch) and weechat.look.theme_backup
defaults to "on".
Add the "Themes" section to user guides and plugin API references in
every language where the corresponding adoc exists.
User guide (10 files): English (the new prose) and French (a full
translation, with section title "Thèmes"). For the other languages —
German, Italian, Japanese, Polish, Serbian — the body is the English
text with only the section title localized where the existing Colors
section is localized (Themen / Motywy / Теме); Italian and Japanese
keep the English "Themes" title to match their existing English-only
section titles. Coverage by file:
- doc/en/weechat_user.en.adoc (new English section)
- doc/fr/weechat_user.fr.adoc (full French translation)
- doc/de/weechat_user.de.adoc (English body, "Themen" title)
- doc/it/weechat_user.it.adoc (English body and title)
- doc/ja/weechat_user.ja.adoc (English body and title)
- doc/pl/weechat_user.pl.adoc (English body, "Motywy" title)
- doc/sr/weechat_user.sr.adoc (English body, "Теме" title)
Plugin API reference (5 files): same approach — English plus full
French translation; Italian, Japanese and Serbian keep the English
body with their conventional section title:
- doc/en/weechat_plugin_api.en.adoc (new English section)
- doc/fr/weechat_plugin_api.fr.adoc (full French translation)
- doc/it/weechat_plugin_api.it.adoc (English body and title)
- doc/ja/weechat_plugin_api.ja.adoc (English body and title)
- doc/sr/weechat_plugin_api.sr.adoc (English body, "Теме" title)
User-guide content covers themable options, /theme apply with the
automatic backup mechanism, /theme save and /theme delete with the
reserved-name rules, the .theme file format with a sample, and the
user-file-shadows-built-in resolution order. The API-reference content
documents weechat_theme_register (prototype, arguments, return value,
C example, Python example) with notes on the themable flag and
per-script auto-cleanup on script unload.
The /theme command's CMD_ARGS_DESC help text and the cmdline option
descriptions are picked up automatically by the doc generator
(doc-autogen), so no manual entries are needed there.
Make individual scripts the unit of ownership for theme contributions
so that loading a script that calls weechat.theme_register(...) and
later unloading it correctly removes the script's overrides.
Plugin API addition (weechat-plugin.h):
- new function pointer t_weechat_plugin.theme_unregister_script
delegates to core's theme_unregister_script.
- new convenience macro weechat_theme_unregister_script(script).
- WEECHAT_PLUGIN_API_VERSION bumped to 20260527-02.
Script API additions (plugin-script-api.{c,h}):
- new plugin_script_api_theme_register (plugin, script, name,
overrides) forwards to the plugin API with the script pointer
as the contribution owner, so contributions are tracked
per-script (not per script-language plugin).
Lifecycle wiring (plugin-script.c):
- new file-local plugin_script_remove_themes (plugin, script)
calls weechat_theme_unregister_script.
- plugin_script_remove now calls it alongside the other
plugin_script_remove_* helpers, so script-unload tears down
everything (configs, bar items, themes, hooks).
Eight language bindings updated to call
plugin_script_api_theme_register instead of weechat_theme_register
directly, so they pass the script pointer as owner:
- python, perl, ruby, lua, tcl, javascript, php, guile.
The behavior change is end-to-end visible only at script unload:
before, an unloaded script's theme overrides lingered in the
registry forever and would be re-applied on the next /theme apply;
after, they vanish when the script unloads. /plugin unload of an
entire script-language plugin already worked via commit 24's hook
in plugin_remove (which drops both plugin-only contributions and
their script-owned children when the language plugin itself goes
away, because the contribution's plugin pointer matches).
No new unit tests in this commit: the underlying theme_unregister_script
function is covered by TEST(CoreTheme, UnregisterByOwner), and the
remaining changes are plumbing.
Call theme_unregister_plugin (plugin) from plugin_remove, right after
config_file_free_all_plugin and before unhook_all_plugin. This drops
every theme contribution whose plugin pointer matches the plugin being
unloaded, so subsequent /theme apply runs no longer try to set
options for an unloaded plugin and per-plugin contributions don't
outlive the plugin.
Script-owned contributions (plugin != NULL && script != NULL) are
left intact - they are cleaned up per-script in the next commit.
This commit is wiring only: the underlying theme_unregister_plugin
function and its semantics are already covered by
TEST(CoreTheme, UnregisterByOwner) in the previous commit.
Refactor the theme registry to store one sub-table per contributor
instead of a single merged hashtable. Each registered theme now holds
a linked list of t_theme_contribution entries:
struct t_theme_contribution {
struct t_weechat_plugin *plugin; /* NULL = core */
const void *script; /* NULL for non-script */
struct t_hashtable *overrides;
...
};
Identity of a contributor is the (plugin, script) pair:
- (NULL, NULL) -> core (theme_builtin_init)
- (plugin, NULL) -> plugin-level contribution
- (plugin, script) -> individual script (filled in by next commit)
theme_register is now (plugin, script, name, overrides). It searches
the existing contributions for a matching (plugin, script) and merges
the new overrides into it; otherwise it appends a fresh contribution.
The public macro weechat_theme_register(name, overrides) still takes
two args - it now expands to pass weechat_plugin and NULL for script.
theme_apply iterates contributions in list order, calling
config_file_option_set for each entry; later contributions naturally
win for duplicate keys.
Two new internal helpers prepare for the lifecycle work in the next
two commits:
- theme_unregister_plugin (plugin): drops every contribution owned
by that plugin (with script == NULL).
- theme_unregister_script (plugin, script): drops every contribution
owned by that script.
Neither is called yet; the auto-purge wiring lands in commits 24
(plugin_unloaded signal) and 25 (script API + script-unload hook).
Other touched code:
- core-theme-builtin.c switches to theme_register (NULL, NULL, ...).
- core-command.c /theme info uses theme_overrides_count helper
instead of reaching into theme->overrides (which no longer
exists).
- WEECHAT_PLUGIN_API_VERSION bumped to 20260527-01 (function-pointer
signature change).
Two new tests cover the new semantics:
- UnregisterByOwner: registers four contributions from distinct
(plugin, script) pairs, then prunes by plugin and by script,
asserting per-contribution removal.
- RegisterMergesPerContributor: two successive register calls from
the same (plugin, script) merge into a single contribution with
later keys overriding earlier ones.
Existing tests are updated to use the new theme_register signature,
theme_overrides_count, and theme_get_override (replacing direct
access to theme->overrides->items_count and hashtable_get on
theme->overrides). No plugin or script call sites change - the
public weechat_theme_register macro keeps the same shape.
Add trigger-theme.{c,h} with the trigger plugin contribution to the
built-in "light" theme: 6 entries (flag_command, flag_conditions,
flag_post_action, flag_regex, flag_return_code, regex) tuned for a
light-background terminal.
Same 2D-array pattern as the other plugin contributions.
Default option values are NOT changed.
Add spell-theme.{c,h} with the spell plugin contribution to the
built-in "light" theme: 1 entry (spell.color.misspelled=red) tuned for
a light-background terminal.
Same 2D-array pattern as the other plugin contributions.
Default option values are NOT changed.
Add script-theme.{c,h} with the script plugin contribution to the
built-in "light" theme: 24 entries on script.color.* (status_* and
text_*) tuned for a light-background terminal.
Same 2D-array pattern as the other plugin contributions.
Default option values are NOT changed.
Add relay-theme.{c,h} with the relay plugin contribution to the
built-in "light" theme: 5 entries (status_auth_failed, status_authenticating,
status_connecting, status_disconnected, text_selected) tuned for a
light-background terminal.
Same 2D-array pattern as the other plugin contributions.
Default option values are NOT changed.
Add logger-theme.{c,h} with the logger plugin contribution to the
built-in "light" theme: 2 entries (logger.color.backlog_end=darkgray,
logger.color.backlog_line=darkgray) tuned for a light-background
terminal.
Same 2D-array pattern as the other plugin contributions.
Default option values are NOT changed.
Add exec-theme.{c,h} with the exec plugin contribution to the built-in
"light" theme: 2 entries (exec.color.flag_finished=red,
exec.color.flag_running=green) tuned for a light-background terminal.
Same 2D-array pattern as the other plugin contributions.
Default option values are NOT changed.
Add xfer-theme.{c,h} with the xfer plugin contribution to the built-in
"light" theme: 7 overrides on xfer.color.* (status_aborted,
status_active, status_connecting, status_done, status_failed,
status_waiting, text_selected) tuned for a light-background terminal.
Same NULL-terminated 2D-array pattern as the irc, fset and buflist
contributions; xfer_theme_init() is called once from
weechat_plugin_init after xfer_config_init / xfer_config_read.
Default option values are NOT changed.
Add buflist-theme.{c,h} with the buflist plugin contribution to the
built-in "light" theme: 5 overrides on buflist.format.* options
(buffer_current, hotlist_low, hotlist_message, lag, number) tuned for
a light-background terminal. Each target is a "string|themable" option
holding an evaluated format expression with embedded ${color:...}
references.
Follows the same pattern as the irc and fset contributions: a
NULL-terminated 2D string table consumed by a tiny local register
helper that builds a hashtable and calls weechat_theme_register;
buflist_theme_init() is called once from weechat_plugin_init after
buflist_config_init / buflist_config_read.
Default option values are NOT changed.
Add fset-theme.{c,h} with the fset plugin contribution to the built-in
"light" theme: 47 overrides on fset.color.* options (line backgrounds,
selected-row tuning, title and value colors) tuned for a
light-background terminal.
The table is a NULL-terminated 2-column array of strings (no struct
wrapper, matching the pattern adopted for the irc contribution).
fset_theme_register builds a hashtable from the table and calls
weechat_theme_register; fset_theme_init() is called once from
weechat_plugin_init after fset_config_init / fset_config_read.
Default option values are NOT changed.
Add irc-theme.{c,h} with the IRC plugin contribution to the built-in
"light" theme: 9 overrides on irc.color.* options tuned for a
light-background terminal (input_nick, item_lag_finished,
item_tls_version_deprecated, list_buffer_line_selected,
message_chghost, message_setname, nick_prefixes, topic_new, topic_old).
The registration is a small wrapper around weechat_theme_register that
builds a hashtable from a static (option, value) table and frees it
after the call. irc_theme_init() is called from weechat_plugin_init
after irc_config_init/read so the option names are already created
when the theme registry references them.
Default option values are NOT changed.
Add a small core-theme-builtin.c module containing the core
contribution to the "light" theme: 33 overrides for
"weechat.bar.{status,title}.color_*" and "weechat.color.*" tuned for
light-background terminals.
theme_builtin_init() builds a hashtable from the static entry table and
calls theme_register("light", overrides), then frees the temporary
hashtable. It is called once from weechat_init right after theme_init.
Calling it twice is a no-op (the registry merges identical keys).
Default option values are NOT changed. Existing configs render exactly
as before; users opt in with "/theme apply light".
Add TEST(CoreTheme, BuiltinInit) covering:
- the "light" theme is absent before theme_builtin_init();
- it is present after, with >= 30 overrides;
- three spot-checked values match the source table;
- calling theme_builtin_init() a second time does not change the
override count.
Plugins contribute their own "light" overrides via weechat_theme_register
in subsequent commits.
Add weechat.theme_register (name, overrides) to all eight script
languages. Each binding is a mechanical translation of the same
signature:
- name: string
- overrides: language-native dict / hash / associative array of
full_option_name -> value strings
- returns: pointer-as-string of the registered t_theme (empty
string on failure)
Each binding converts the dict to a struct t_hashtable using the
existing per-language helper (weechat_python_dict_to_hashtable,
weechat_perl_hash_to_hashtable, weechat_ruby_hash_to_hashtable,
weechat_lua_tohashtable, weechat_tcl_dict_to_hashtable,
weechat_js_object_to_hashtable, weechat_php_array_to_hashtable,
weechat_guile_alist_to_hashtable), calls weechat_theme_register,
frees the temporary hashtable, and returns the result. The new
function is registered right after the config_* functions so the API
listing stays grouped by topic.
PHP also receives a new arginfo entry (string, array -> string) in
both weechat-php_arginfo.h and weechat-php_legacy_arginfo.h.
This is plumbing only - the underlying theme_register function is
already covered by tests/unit/core/test-core-theme.cpp
(TEST(CoreTheme, Register)). No script-side tests are added here.
Add a single new entry point to the plugin API:
struct t_theme *weechat_theme_register (const char *name,
struct t_hashtable *overrides);
Plugins call this at init time to contribute their per-theme color (or
other themable) overrides for a built-in theme like "dark". The
overrides hashtable maps full option names ("irc.color.input_nick") to
their string values; the caller retains ownership and may free it
right after the call. Repeated calls with the same theme name merge
into the existing registry entry, so each plugin can declare its own
contributions independently of core and of other plugins.
Wiring:
- struct t_theme forward-declared in weechat-plugin.h alongside the
other opaque types.
- theme_register function pointer added to t_weechat_plugin.
- weechat_theme_register convenience macro added.
- plugin.c initializes the pointer to core's theme_register.
- WEECHAT_PLUGIN_API_VERSION bumped to 20260526-01.
This commit is plumbing only: the underlying theme_register function
already has unit-test coverage in tests/unit/core/test-core-theme.cpp
(TEST(CoreTheme, Register)), so no new tests are added here.
Add two completion items hooked alongside "layouts_names":
- "theme_themes_all": all theme names (built-ins from the registry
plus every *.theme file in <weechat_config_dir>/themes/, including
backup-*.theme). Used by tab-complete on /theme apply and
/theme info.
- "theme_themes_user": user theme files only (excludes built-ins
and backup-*.theme). Used by tab-complete on /theme save and
/theme delete, so users cannot accidentally try to overwrite a
built-in name or save a name colliding with the reserved backup
prefix.
Both callbacks share a small dir_exec_on_files-based helper to filter
the themes directory. The /theme command's completion template in
core-command.c is updated to reference these new items.
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.
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.
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 behaviour described above.
Wire the new /theme apply subcommand into core-command.c with the
existing /theme registration; update help text accordingly.
Add the /theme command with two read-only subcommands for now:
- /theme (or /theme list): list registered themes; the active theme
(matching weechat.look.theme) is marked with "->".
- /theme info <name>: show name, description, creation date, WeeChat
version and override count of a theme.
Both subcommands only consider themes present in the in-memory
registry (registered via core/plugins/scripts). User theme files on
disk are not yet handled: the file parser and transient file reads
land in a later commit together with /theme apply.
Add a new string option "weechat.look.theme" holding the name of the
last theme applied via the upcoming /theme command. It is set
automatically by /theme apply and persisted on disk for /theme info to
display after restart; it is NOT re-applied at startup (the user's
saved color values win to avoid clobbering manual post-apply tweaks).
Amend config_change_color so it skips the gui_color_init_weechat ()
and gui_window_ask_refresh (1) calls when theme_applying is set.
/theme apply will set this flag while iterating overrides so the N
individual option changes do not trigger N redundant screen refreshes;
the apply path then performs a single refresh at the end.
Introduce a new module (core-theme.{c,h}) holding the in-memory registry
of built-in themes used by the upcoming /theme command:
- struct t_theme stores name, description, date and weechat version
captured at registration time, plus a hashtable of overrides keyed by
full option name (file.section.option) -> value string.
- theme_register (name, overrides) creates a new theme or merges the
given overrides into an existing one (later calls override duplicate
keys); this is the API plugins and scripts will use to contribute
per-theme color values.
- theme_search and theme_list provide lookup and ordered enumeration.
- theme_init / theme_end are called from weechat_init / weechat_end.
The theme_applying flag is declared here but not yet consumed (it will
gate config_change_color in the next commit to avoid N redundant
window refreshes during /theme apply).
User theme files are not handled by this module: they are read
transiently inside /theme apply (a later commit) and never cached.
Extend the "t:" filter so the special value "themable" matches every
option whose new themable flag is set, regardless of type (color,
string, integer, boolean, enum). This makes the flag interactively
discoverable in the fset buffer and is the natural way to inspect the
surface area that an upcoming /theme command will be allowed to touch.
The themable flag of an option is now mirrored on struct t_fset_option,
exposed via hdata ("themable", integer) and infolist ("themable",
integer), and printed in the log.
Add an "int themable" field on struct t_config_option. The flag is set
automatically for every CONFIG_OPTION_TYPE_COLOR option, and may be set
explicitly on any other type by suffixing the type argument with
"|themable" in the call to config_file_new_option (e.g. "string|themable"
for a string option whose value contains "${color:...}" references).
Opt in the relevant string options in core (buffer_time_format,
day_change_message_*, item_time_format, nick_color_force, prefix_*,
chat_nick_colors, eval_syntax_colors, color palette aliases) and in the
buflist, fset, irc, relay plugins.
The flag is exposed via hdata, infolist, and print_log so scripts and
/debug can read it. This is the foundation for an upcoming /theme
command that will only be allowed to modify themable options.
A relay client could send data with no end-of-line (an unterminated method
or header line) and dribble its payload, making WeeChat accumulate it in the
partial message buffer that grew without limit, until all memory was
exhausted. This path is reachable before authentication during websocket
initialization with the "weechat" and "irc" protocols.
The accumulated partial message is now bounded by
RELAY_HTTP_PARTIAL_MESSAGE_MAX_LENGTH: once the limit is reached, the extra
data is ignored.