From 5460855ea410caa8fcfc9f9411319f73a381923c Mon Sep 17 00:00:00 2001 From: deepend Date: Mon, 16 Feb 2026 20:26:45 -0700 Subject: [PATCH 1/9] =?UTF-8?q?Implemented=20a=20Windows-specific=20theme?= =?UTF-8?q?=20detection=20path=20with=20High=20Contrast=20safeguards,=20in?= =?UTF-8?q?cluding=20helpers=20to=20detect=20HC=20mode,=20read=20Windows?= =?UTF-8?q?=20system=20theme=20preference=20from=20the=20registry,=20and?= =?UTF-8?q?=20gate=20native=20titlebar=20dark=20mode=20updates=20when=20HC?= =?UTF-8?q?=20is=20active.=20Also=20added=20DWM=20header=20usage=20for=20n?= =?UTF-8?q?ative=20titlebar=20APIs.=20Added=20a=20unified=20theme=20applic?= =?UTF-8?q?ation=20pipeline=20via=20fe=5Fapply=5Ftheme=5Ffor=5Fmode()=20an?= =?UTF-8?q?d=20fe=5Fapply=5Ftheme=5Fto=5Ftoplevel(),=20and=20routed=20auto?= =?UTF-8?q?-mode=20refresh=20logic=20through=20it=20so=20palette=20+=20GTK?= =?UTF-8?q?=20input=20styling=20are=20applied=20consistently=20from=20one?= =?UTF-8?q?=20place.=20Switched=20startup=20and=20settings-apply=20flows?= =?UTF-8?q?=20to=20use=20the=20unified=20theming=20function,=20replacing?= =?UTF-8?q?=20direct=20palette-only=20calls=20so=20system-mode=20detection?= =?UTF-8?q?=20=E2=86=92=20theme=20apply=20is=20consistent=20across=20initi?= =?UTF-8?q?alization=20and=20preferences=20changes.=20Applied=20native=20t?= =?UTF-8?q?itlebar=20updates=20when=20top-level=20windows=20are=20shown=20?= =?UTF-8?q?and=20during=20setup=20reapply=20across=20open=20sessions,=20an?= =?UTF-8?q?d=20wired=20Windows=20builds=20to=20link=20dwmapi=20in=20both?= =?UTF-8?q?=20Meson=20and=20MSVC=20project=20files.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fe-gtk/fe-gtk.c | 144 ++++++++++++++++++++++++++++++++--------- src/fe-gtk/fe-gtk.h | 2 + src/fe-gtk/maingui.c | 4 ++ src/fe-gtk/meson.build | 1 + src/fe-gtk/setup.c | 4 +- 5 files changed, 122 insertions(+), 33 deletions(-) diff --git a/src/fe-gtk/fe-gtk.c b/src/fe-gtk/fe-gtk.c index 5345ab5f..9a1365c7 100644 --- a/src/fe-gtk/fe-gtk.c +++ b/src/fe-gtk/fe-gtk.c @@ -28,6 +28,7 @@ #ifdef WIN32 #include +#include #else #include #endif @@ -285,6 +286,8 @@ const char cursor_color_rc[] = "}" "widget \"*.zoitechat-inputbox\" style : application \"xc-ib-st\""; +InputStyle *create_input_style (InputStyle *style); + static const char adwaita_workaround_rc[] = "style \"zoitechat-input-workaround\"" "{" @@ -301,6 +304,85 @@ static const char adwaita_workaround_rc[] = "}" "widget \"*.zoitechat-inputbox\" style \"zoitechat-input-workaround\""; +#ifdef G_OS_WIN32 +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +static gboolean +fe_win32_high_contrast_is_enabled (void) +{ + HIGHCONTRASTW hc; + + ZeroMemory (&hc, sizeof (hc)); + hc.cbSize = sizeof (hc); + if (!SystemParametersInfoW (SPI_GETHIGHCONTRAST, sizeof (hc), &hc, 0)) + return FALSE; + + return (hc.dwFlags & HCF_HIGHCONTRASTON) != 0; +} + +static gboolean +fe_win32_try_get_system_dark (gboolean *prefer_dark) +{ + DWORD value = 1; + DWORD value_size = sizeof (value); + LSTATUS status; + + status = RegGetValueW (HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", + L"AppsUseLightTheme", + RRF_RT_REG_DWORD, + NULL, + &value, + &value_size); + if (status != ERROR_SUCCESS) + status = RegGetValueW (HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", + L"SystemUsesLightTheme", + RRF_RT_REG_DWORD, + NULL, + &value, + &value_size); + + if (status != ERROR_SUCCESS) + return FALSE; + + *prefer_dark = (value == 0); + return TRUE; +} + +static void +fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark_mode) +{ + HWND hwnd; + BOOL use_dark; + + if (!window || !gtk_widget_get_realized (window)) + return; + + hwnd = gdk_win32_window_get_handle (gtk_widget_get_window (window)); + if (!hwnd) + return; + + if (fe_win32_high_contrast_is_enabled ()) + return; + + use_dark = dark_mode ? TRUE : FALSE; + DwmSetWindowAttribute (hwnd, + DWMWA_USE_IMMERSIVE_DARK_MODE, + &use_dark, + sizeof (use_dark)); +} +#else +static void +fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark_mode) +{ + (void) window; + (void) dark_mode; +} +#endif + static gboolean fe_system_prefers_dark (void) { @@ -309,42 +391,16 @@ fe_system_prefers_dark (void) char *theme_name = NULL; #ifdef G_OS_WIN32 gboolean have_win_pref = FALSE; + + if (fe_win32_high_contrast_is_enabled ()) + return FALSE; #endif if (!settings) return FALSE; #ifdef G_OS_WIN32 - { - DWORD value = 1; - DWORD value_size = sizeof (value); - LSTATUS status; - - status = RegGetValueW (HKEY_CURRENT_USER, - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", - L"AppsUseLightTheme", - RRF_RT_REG_DWORD, - NULL, - &value, - &value_size); - if (status != ERROR_SUCCESS) - status = RegGetValueW (HKEY_CURRENT_USER, - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", - L"SystemUsesLightTheme", - RRF_RT_REG_DWORD, - NULL, - &value, - &value_size); - - if (status == ERROR_SUCCESS) - { - prefer_dark = (value == 0); - have_win_pref = TRUE; - } - } -#endif - -#ifdef G_OS_WIN32 + have_win_pref = fe_win32_try_get_system_dark (&prefer_dark); if (!have_win_pref) #endif if (g_object_class_find_property (G_OBJECT_GET_CLASS (settings), @@ -388,7 +444,7 @@ fe_auto_dark_mode_changed (GtkSettings *settings, GParamSpec *pspec, gpointer da return; auto_dark_mode_enabled = enabled; - palette_apply_dark_mode (enabled); + fe_apply_theme_for_mode (ZOITECHAT_DARK_MODE_AUTO, NULL); setup_apply_real (0, TRUE, FALSE, FALSE); } @@ -404,6 +460,30 @@ fe_refresh_auto_dark_mode (void) fe_auto_dark_mode_changed (NULL, NULL, NULL); } +gboolean +fe_apply_theme_for_mode (unsigned int mode, gboolean *palette_changed) +{ + gboolean enabled = fe_dark_mode_is_enabled_for (mode); + gboolean changed = palette_apply_dark_mode (enabled); + + if (palette_changed) + *palette_changed = changed; + + if (input_style) + create_input_style (input_style); + + return enabled; +} + +void +fe_apply_theme_to_toplevel (GtkWidget *window) +{ + if (!window) + return; + + fe_win32_apply_native_titlebar (window, fe_dark_mode_is_enabled ()); +} + gboolean fe_dark_mode_is_enabled_for (unsigned int mode) { @@ -625,7 +705,7 @@ fe_init (void) GtkSettings *settings; palette_load (); - palette_apply_dark_mode (fe_dark_mode_is_enabled ()); + fe_apply_theme_for_mode (prefs.hex_gui_dark_mode, NULL); key_init (); pixmaps_init (); diff --git a/src/fe-gtk/fe-gtk.h b/src/fe-gtk/fe-gtk.h index 03e55508..626bfb2a 100644 --- a/src/fe-gtk/fe-gtk.h +++ b/src/fe-gtk/fe-gtk.h @@ -211,6 +211,8 @@ gboolean fe_dark_mode_is_enabled (void); gboolean fe_dark_mode_is_enabled_for (unsigned int mode); void fe_set_auto_dark_mode_state (gboolean enabled); void fe_refresh_auto_dark_mode (void); +gboolean fe_apply_theme_for_mode (unsigned int mode, gboolean *palette_changed); +void fe_apply_theme_to_toplevel (GtkWidget *window); #define SPELL_ENTRY_GET_TEXT(e) ((char *)(gtk_entry_get_text (GTK_ENTRY(e)))) #define SPELL_ENTRY_SET_TEXT(e,txt) gtk_entry_set_text(GTK_ENTRY(e),txt) diff --git a/src/fe-gtk/maingui.c b/src/fe-gtk/maingui.c index 5a59b876..ec5e6b6c 100644 --- a/src/fe-gtk/maingui.c +++ b/src/fe-gtk/maingui.c @@ -3944,6 +3944,7 @@ mg_create_topwindow (session *sess) mg_place_userlist_and_chanview (sess->gui); gtk_widget_show (win); + fe_apply_theme_to_toplevel (win); #ifdef G_OS_WIN32 parent_win = gtk_widget_get_window (win); @@ -4118,6 +4119,7 @@ mg_create_tabwindow (session *sess) mg_place_userlist_and_chanview (sess->gui); gtk_widget_show (win); + fe_apply_theme_to_toplevel (win); #ifdef G_OS_WIN32 parent_win = gtk_widget_get_window (win); @@ -4141,6 +4143,8 @@ mg_apply_setup (void) ((xtext_buffer *)sess->res->buffer)->needs_recalc = TRUE; if (!sess->gui->is_tab || !done_main) mg_place_userlist_and_chanview (sess->gui); + if (sess->gui->window) + fe_apply_theme_to_toplevel (sess->gui->window); if (sess->gui->is_tab) done_main = TRUE; list = list->next; diff --git a/src/fe-gtk/meson.build b/src/fe-gtk/meson.build index a845ab05..b66dfd73 100644 --- a/src/fe-gtk/meson.build +++ b/src/fe-gtk/meson.build @@ -77,6 +77,7 @@ zoitechat_gtk_ldflags = [] if host_machine.system() == 'windows' zoitechat_gtk_sources += 'notifications/notification-windows.c' + zoitechat_gtk_deps += cc.find_library('dwmapi', required: true) # TODO: mingw doesn't have these headers or libs # add_languages('cpp') diff --git a/src/fe-gtk/setup.c b/src/fe-gtk/setup.c index 0402e27e..84eff508 100644 --- a/src/fe-gtk/setup.c +++ b/src/fe-gtk/setup.c @@ -3059,7 +3059,9 @@ setup_apply (struct zoitechatprefs *pr) * the preference flips but the palette stays the same (aka: "nothing happens"). */ { - gboolean pal_changed = palette_apply_dark_mode (fe_dark_mode_is_enabled_for (prefs.hex_gui_dark_mode)); + gboolean pal_changed = FALSE; + + fe_apply_theme_for_mode (prefs.hex_gui_dark_mode, &pal_changed); if (prefs.hex_gui_dark_mode != old_dark_mode || pal_changed) color_change = TRUE; } From 242f4a6dec12e334b945cb268649d6e904c86aee Mon Sep 17 00:00:00 2001 From: deepend-tildeclub <58404188+deepend-tildeclub@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:29:25 -0700 Subject: [PATCH 2/9] Update fe-gtk.vcxproj with new configurations --- src/fe-gtk/fe-gtk.vcxproj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/fe-gtk/fe-gtk.vcxproj b/src/fe-gtk/fe-gtk.vcxproj index aa9bd954..f166ba03 100644 --- a/src/fe-gtk/fe-gtk.vcxproj +++ b/src/fe-gtk/fe-gtk.vcxproj @@ -1,4 +1,4 @@ - + v142 @@ -30,24 +30,24 @@ WIN32;NDEBUG;_WINDOWS;$(OwnFlags);%(PreprocessorDefinitions) - ..\common;$(ZoiteChatLib);$(DepsRoot)\include;$(OpenSslInclude);$(Glib);$(Gtk);%(AdditionalIncludeDirectories) + ..\common;$(ZoiteChatLib);$(DepsRoot)\include;$(OpenSslInclude);$(Glib);$(Gtk);%(AdditionalIncludeDirectories) 4244;%(DisableSpecificWarnings) $(DepsRoot)\lib;%(AdditionalLibraryDirectories) - $(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;comsupp.lib;%(AdditionalDependencies) + $(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;comsupp.lib;dwmapi.lib;%(AdditionalDependencies) mainCRTStartup WIN32;_WIN64;_AMD64_;NDEBUG;_WINDOWS;$(OwnFlags);%(PreprocessorDefinitions) - ..\common;$(ZoiteChatLib);$(DepsRoot)\include;$(OpenSslInclude);$(Glib);$(Gtk);%(AdditionalIncludeDirectories) + ..\common;$(ZoiteChatLib);$(DepsRoot)\include;$(OpenSslInclude);$(Glib);$(Gtk);%(AdditionalIncludeDirectories) 4244;4267;%(DisableSpecificWarnings) $(DepsRoot)\lib;%(AdditionalLibraryDirectories) - $(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;comsupp.lib;%(AdditionalDependencies) + $(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;comsupp.lib;dwmapi.lib;%(AdditionalDependencies) mainCRTStartup From 78754ff1944648ad13156c1b9723bbade488cc28 Mon Sep 17 00:00:00 2001 From: deepend Date: Mon, 16 Feb 2026 20:41:27 -0700 Subject: [PATCH 3/9] Added a Windows-only fe_apply_windows_theme(gboolean dark) function under #ifdef G_OS_WIN32 that: sets gtk-application-prefer-dark-theme when that GtkSettings property exists, and applies an app-level GtkCssProvider with dark/light fallback classes (zoitechat-dark / zoitechat-light) to keep non-chat widgets aligned with mode changes. Updated the shared theme apply path so fe_apply_theme_for_mode() still applies the palette layer (palette_apply_dark_mode) and now also applies the Windows GTK layer, ensuring auto/manual modes flow through one path. Extended toplevel theme application to add/remove the new dark/light CSS classes on Windows GTK3 windows, while preserving native titlebar dark-mode handling. Made AUTO mode resolve from the cached auto_dark_mode_enabled state, and initialized that state in fe_init() before the first fe_apply_theme_for_mode() call so initial and subsequent behavior are consistent. --- src/fe-gtk/fe-gtk.c | 68 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/src/fe-gtk/fe-gtk.c b/src/fe-gtk/fe-gtk.c index 9a1365c7..f823fdc8 100644 --- a/src/fe-gtk/fe-gtk.c +++ b/src/fe-gtk/fe-gtk.c @@ -427,6 +427,46 @@ fe_system_prefers_dark (void) static gboolean auto_dark_mode_enabled = FALSE; +#ifdef G_OS_WIN32 +static void +fe_apply_windows_theme (gboolean dark) +{ + GtkSettings *settings = gtk_settings_get_default (); + + if (settings && g_object_class_find_property (G_OBJECT_GET_CLASS (settings), + "gtk-application-prefer-dark-theme")) + { + g_object_set (settings, "gtk-application-prefer-dark-theme", dark, NULL); + } + +#if HAVE_GTK3 + { + static GtkCssProvider *win_theme_provider = NULL; + GdkScreen *screen = gdk_screen_get_default (); + const char *css = + "window.zoitechat-dark, .zoitechat-dark {" + "background-color: #202020;" + "color: #f0f0f0;" + "}" + "window.zoitechat-light, .zoitechat-light {" + "background-color: #f6f6f6;" + "color: #101010;" + "}"; + + if (!win_theme_provider) + win_theme_provider = gtk_css_provider_new (); + + gtk_css_provider_load_from_data (win_theme_provider, css, -1, NULL); + if (screen) + gtk_style_context_add_provider_for_screen ( + screen, + GTK_STYLE_PROVIDER (win_theme_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } +#endif +} +#endif + static void fe_auto_dark_mode_changed (GtkSettings *settings, GParamSpec *pspec, gpointer data) { @@ -466,6 +506,10 @@ fe_apply_theme_for_mode (unsigned int mode, gboolean *palette_changed) gboolean enabled = fe_dark_mode_is_enabled_for (mode); gboolean changed = palette_apply_dark_mode (enabled); +#ifdef G_OS_WIN32 + fe_apply_windows_theme (enabled); +#endif + if (palette_changed) *palette_changed = changed; @@ -481,6 +525,20 @@ fe_apply_theme_to_toplevel (GtkWidget *window) if (!window) return; +#if defined(G_OS_WIN32) && HAVE_GTK3 + { + GtkStyleContext *context = gtk_widget_get_style_context (window); + gboolean dark = fe_dark_mode_is_enabled (); + + if (context) + { + gtk_style_context_remove_class (context, "zoitechat-dark"); + gtk_style_context_remove_class (context, "zoitechat-light"); + gtk_style_context_add_class (context, dark ? "zoitechat-dark" : "zoitechat-light"); + } + } +#endif + fe_win32_apply_native_titlebar (window, fe_dark_mode_is_enabled ()); } @@ -495,7 +553,7 @@ fe_dark_mode_is_enabled_for (unsigned int mode) return FALSE; case ZOITECHAT_DARK_MODE_AUTO: default: - return fe_system_prefers_dark (); + return auto_dark_mode_enabled; } } @@ -705,6 +763,10 @@ fe_init (void) GtkSettings *settings; palette_load (); + settings = gtk_settings_get_default (); + if (settings) + auto_dark_mode_enabled = fe_system_prefers_dark (); + fe_apply_theme_for_mode (prefs.hex_gui_dark_mode, NULL); key_init (); pixmaps_init (); @@ -719,12 +781,10 @@ fe_init (void) input_style = create_input_style (gtk_style_new ()); #endif - settings = gtk_settings_get_default (); if (settings) { - auto_dark_mode_enabled = fe_system_prefers_dark (); g_signal_connect (settings, "notify::gtk-application-prefer-dark-theme", - G_CALLBACK (fe_auto_dark_mode_changed), NULL); + G_CALLBACK (fe_auto_dark_mode_changed), NULL); g_signal_connect (settings, "notify::gtk-theme-name", G_CALLBACK (fe_auto_dark_mode_changed), NULL); } From e424d9325cda9ad097ca438db5c850e284ae4b0d Mon Sep 17 00:00:00 2001 From: deepend-tildeclub <58404188+deepend-tildeclub@users.noreply.github.com> Date: Tue, 17 Feb 2026 09:39:01 -0700 Subject: [PATCH 4/9] Update Python plugin version to 2.18.0-pre1 --- plugins/python/python.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/python/python.py b/plugins/python/python.py index a2cdb094..bab732f7 100644 --- a/plugins/python/python.py +++ b/plugins/python/python.py @@ -19,7 +19,7 @@ else: if not hasattr(sys, 'argv'): sys.argv = [''] -VERSION = b'2.0' # Sync with zoitechat.__version__ +VERSION = b'2.18.0-pre1' # Sync with zoitechat.__version__ PLUGIN_NAME = ffi.new('char[]', b'Python') PLUGIN_DESC = ffi.new('char[]', b'Python %d.%d scripting interface' % (sys.version_info[0], sys.version_info[1])) PLUGIN_VERSION = ffi.new('char[]', VERSION) @@ -591,4 +591,4 @@ def _on_plugin_deinit(): except KeyError: pass - return 1 \ No newline at end of file + return 1 From 7fd8e5455dc18b2a4e8ac09726ec16cbd2d78448 Mon Sep 17 00:00:00 2001 From: deepend Date: Tue, 17 Feb 2026 10:51:42 -0700 Subject: [PATCH 5/9] fixing zoitechat/hexchat theme usage. --- src/common/outbound.c | 16 ++++-- src/common/zoitechat.c | 111 ++++++++++++++++++++++++++++++++++++++--- src/common/zoitechat.h | 1 + src/fe-gtk/setup.c | 55 +------------------- 4 files changed, 120 insertions(+), 63 deletions(-) diff --git a/src/common/outbound.c b/src/common/outbound.c index aae7c830..3a4d6a10 100644 --- a/src/common/outbound.c +++ b/src/common/outbound.c @@ -3783,9 +3783,19 @@ cmd_url (struct session *sess, char *tbuf, char *word[], char *word_eol[]) if (zoitechat_import_theme (theme_path, &error)) { - message = g_strdup_printf (_("Theme \"%s\" imported."), basename); - fe_message (message, FE_MSG_INFO); - g_free (message); + if (zoitechat_apply_theme (basename, &error)) + { + message = g_strdup_printf (_("Theme \"%s\" imported and applied."), basename); + fe_message (message, FE_MSG_INFO); + handle_command (sess, "gui apply", FALSE); + g_free (message); + } + else + { + fe_message (error ? error->message : _("Theme imported, but failed to apply."), + FE_MSG_ERROR); + g_clear_error (&error); + } } else { diff --git a/src/common/zoitechat.c b/src/common/zoitechat.c index d1b962b8..dbb6e749 100644 --- a/src/common/zoitechat.c +++ b/src/common/zoitechat.c @@ -247,6 +247,85 @@ zoitechat_remote_win32 (void) } #endif + +static gboolean +zoitechat_copy_theme_file (const char *src, const char *dest, GError **error) +{ + char *data = NULL; + gsize len = 0; + + if (!g_file_get_contents (src, &data, &len, error)) + return FALSE; + + if (!g_file_set_contents (dest, data, len, error)) + { + g_free (data); + return FALSE; + } + + g_free (data); + return TRUE; +} + +gboolean +zoitechat_apply_theme (const char *theme_name, GError **error) +{ + char *theme_dir; + char *colors_src; + char *colors_dest; + char *events_src; + char *events_dest; + gboolean ok = FALSE; + + if (!theme_name || !*theme_name) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + _("No theme name specified.")); + return FALSE; + } + + theme_dir = g_build_filename (get_xdir (), "themes", theme_name, NULL); + colors_src = g_build_filename (theme_dir, "colors.conf", NULL); + colors_dest = g_build_filename (get_xdir (), "colors.conf", NULL); + events_src = g_build_filename (theme_dir, "pevents.conf", NULL); + events_dest = g_build_filename (get_xdir (), "pevents.conf", NULL); + + if (!g_file_test (colors_src, G_FILE_TEST_IS_REGULAR)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + _("This theme is missing a colors.conf file.")); + goto cleanup; + } + + if (!zoitechat_copy_theme_file (colors_src, colors_dest, error)) + goto cleanup; + + if (g_file_test (events_src, G_FILE_TEST_IS_REGULAR)) + { + if (!zoitechat_copy_theme_file (events_src, events_dest, error)) + goto cleanup; + } + else if (g_file_test (events_dest, G_FILE_TEST_EXISTS)) + { + if (g_unlink (events_dest) != 0) + { + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), + _("Failed to remove existing event settings.")); + goto cleanup; + } + } + + ok = TRUE; + +cleanup: + g_free (events_dest); + g_free (events_src); + g_free (colors_dest); + g_free (colors_src); + g_free (theme_dir); + return ok; +} + gboolean zoitechat_import_theme (const char *path, GError **error) { @@ -754,9 +833,19 @@ irc_init (session *sess) if (zoitechat_import_theme (theme_path, &error)) { - message = g_strdup_printf (_("Theme \"%s\" imported."), basename); - fe_message (message, FE_MSG_INFO); - g_free (message); + if (zoitechat_apply_theme (basename, &error)) + { + message = g_strdup_printf (_("Theme \"%s\" imported and applied."), basename); + fe_message (message, FE_MSG_INFO); + handle_command (sess, "gui apply", FALSE); + g_free (message); + } + else + { + fe_message (error ? error->message : _("Theme imported, but failed to apply."), + FE_MSG_ERROR); + g_clear_error (&error); + } } else { @@ -796,9 +885,19 @@ irc_init (session *sess) if (zoitechat_import_theme (theme_path, &error)) { - message = g_strdup_printf (_("Theme \"%s\" imported."), basename); - fe_message (message, FE_MSG_INFO); - g_free (message); + if (zoitechat_apply_theme (basename, &error)) + { + message = g_strdup_printf (_("Theme \"%s\" imported and applied."), basename); + fe_message (message, FE_MSG_INFO); + handle_command (sess, "gui apply", FALSE); + g_free (message); + } + else + { + fe_message (error ? error->message : _("Theme imported, but failed to apply."), + FE_MSG_ERROR); + g_clear_error (&error); + } } else { diff --git a/src/common/zoitechat.h b/src/common/zoitechat.h index 810394d6..64e08bde 100644 --- a/src/common/zoitechat.h +++ b/src/common/zoitechat.h @@ -31,6 +31,7 @@ gboolean zoitechat_theme_path_from_arg (const char *arg, char **path_out); gboolean zoitechat_import_theme (const char *path, GError **error); +gboolean zoitechat_apply_theme (const char *theme_name, GError **error); #ifdef USE_OPENSSL #ifdef __APPLE__ diff --git a/src/fe-gtk/setup.c b/src/fe-gtk/setup.c index 84eff508..0825bc35 100644 --- a/src/fe-gtk/setup.c +++ b/src/fe-gtk/setup.c @@ -2045,22 +2045,6 @@ setup_theme_show_message (GtkMessageType message_type, const char *primary) gtk_widget_destroy (dialog); } -static gboolean -setup_theme_copy_file (const char *src, const char *dest, GError **error) -{ - GFile *src_file; - GFile *dest_file; - gboolean success; - - src_file = g_file_new_for_path (src); - dest_file = g_file_new_for_path (dest); - success = g_file_copy (src_file, dest_file, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, error); - g_object_unref (src_file); - g_object_unref (dest_file); - - return success; -} - static void setup_theme_populate (setup_theme_ui *ui) { @@ -2138,11 +2122,6 @@ setup_theme_apply_cb (GtkWidget *button, gpointer user_data) GtkWidget *dialog; gint response; char *theme; - char *theme_dir = NULL; - char *colors_src = NULL; - char *colors_dest = NULL; - char *events_src = NULL; - char *events_dest = NULL; GError *error = NULL; theme = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (ui->combo)); @@ -2161,40 +2140,13 @@ setup_theme_apply_cb (GtkWidget *button, gpointer user_data) return; } - theme_dir = g_build_filename (get_xdir (), "themes", theme, NULL); - colors_src = g_build_filename (theme_dir, "colors.conf", NULL); - colors_dest = g_build_filename (get_xdir (), "colors.conf", NULL); - - if (!g_file_test (colors_src, G_FILE_TEST_IS_REGULAR)) - { - setup_theme_show_message (GTK_MESSAGE_ERROR, _("This theme is missing a colors.conf file.")); - goto cleanup; - } - - if (!setup_theme_copy_file (colors_src, colors_dest, &error)) + if (!zoitechat_apply_theme (theme, &error)) { setup_theme_show_message (GTK_MESSAGE_ERROR, error ? error->message : _("Failed to apply theme.")); g_clear_error (&error); goto cleanup; } - events_src = g_build_filename (theme_dir, "pevents.conf", NULL); - events_dest = g_build_filename (get_xdir (), "pevents.conf", NULL); - - if (g_file_test (events_src, G_FILE_TEST_IS_REGULAR)) - { - if (!setup_theme_copy_file (events_src, events_dest, &error)) - { - setup_theme_show_message (GTK_MESSAGE_ERROR, error ? error->message : _("Failed to apply event settings.")); - g_clear_error (&error); - goto cleanup; - } - } - else if (g_file_test (events_dest, G_FILE_TEST_EXISTS)) - { - g_unlink (events_dest); - } - palette_load (); palette_apply_dark_mode (fe_dark_mode_is_enabled ()); color_change = TRUE; @@ -2203,11 +2155,6 @@ setup_theme_apply_cb (GtkWidget *button, gpointer user_data) setup_theme_show_message (GTK_MESSAGE_INFO, _("Theme applied. Some changes may require a restart to take full effect.")); cleanup: - g_free (events_dest); - g_free (events_src); - g_free (colors_dest); - g_free (colors_src); - g_free (theme_dir); g_free (theme); } From 60251952340f057f66ef6a12563187b4ce8aabc1 Mon Sep 17 00:00:00 2001 From: deepend Date: Tue, 17 Feb 2026 13:24:42 -0700 Subject: [PATCH 6/9] =?UTF-8?q?fixed=20the=20delayed=20selection=20paint?= =?UTF-8?q?=20by=20changing=20gtk=5Fxtext=5Frender=5Fents()=20to=20queue?= =?UTF-8?q?=20a=20GTK=20redraw=20when=20called=20outside=20::draw=20on=20G?= =?UTF-8?q?TK3,=20instead=20of=20attempting=20direct=20window=20painting.?= =?UTF-8?q?=20This=20addresses=20the=20=E2=80=9Cdouble-click/drag=20select?= =?UTF-8?q?ion=20only=20appears=20after=20some=20other=20window=20change?= =?UTF-8?q?=E2=80=9D=20behavior.=20Added=20an=20in-code=20comment=20explai?= =?UTF-8?q?ning=20why=20this=20path=20is=20needed=20(GTK3/Wayland=20frame-?= =?UTF-8?q?driven=20presentation).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fe-gtk/xtext.c | 77 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/src/fe-gtk/xtext.c b/src/fe-gtk/xtext.c index de908181..3b7bc853 100644 --- a/src/fe-gtk/xtext.c +++ b/src/fe-gtk/xtext.c @@ -195,6 +195,8 @@ static void gtk_xtext_search_textentry_fini (gpointer, gpointer); static void gtk_xtext_search_fini (xtext_buffer *); static gboolean gtk_xtext_search_init (xtext_buffer *buf, const gchar *text, gtk_xtext_search_flags flags, GError **perr); static char * gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent, int *ret_off, int *ret_len, GSList **slp); +static gboolean gtk_xtext_word_select_char (const unsigned char *ch); +static gboolean gtk_xtext_get_word_select_range (GtkXText *xtext, int x, int y, textentry **ret_ent, int *ret_off, int *ret_len); static inline void gtk_xtext_cursor_unref (GdkCursor *cursor) @@ -1856,10 +1858,12 @@ gtk_xtext_selection_draw (GtkXText * xtext, GdkEventMotion * event, gboolean ren if (xtext->word_select) { /* a word selection cannot be started if the cursor is out of bounds in gtk_xtext_button_press */ - gtk_xtext_get_word (xtext, low_x, low_y, NULL, &low_offs, NULL, NULL); + if (!gtk_xtext_get_word_select_range (xtext, low_x, low_y, NULL, &low_offs, NULL)) + gtk_xtext_get_word (xtext, low_x, low_y, NULL, &low_offs, NULL, NULL); /* in case the cursor is out of bounds we keep offset_end from gtk_xtext_find_char and fix the length */ - if (gtk_xtext_get_word (xtext, high_x, high_y, NULL, &high_offs, &high_len, NULL) == NULL) + if (!gtk_xtext_get_word_select_range (xtext, high_x, high_y, NULL, &high_offs, &high_len) && + gtk_xtext_get_word (xtext, high_x, high_y, NULL, &high_offs, &high_len, NULL) == NULL) high_len = high_offs == high_ent->str_len? 0: -1; /* -1 for the space, 0 if at the end */ high_offs += high_len; if (low_y < 0) @@ -2134,6 +2138,57 @@ gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent, return word; } +static gboolean +gtk_xtext_word_select_char (const unsigned char *ch) +{ + gunichar uc; + + if (!ch || !*ch) + return FALSE; + + uc = g_utf8_get_char_validated ((const gchar *)ch, -1); + if (uc == (gunichar)-1 || uc == (gunichar)-2) + return FALSE; + return g_unichar_isalnum (uc) || uc == '_' || uc == '-'; +} + +static gboolean +gtk_xtext_get_word_select_range (GtkXText *xtext, int x, int y, textentry **ret_ent, int *ret_off, int *ret_len) +{ + textentry *ent; + int offset; + unsigned char *start, *end; + + ent = gtk_xtext_find_char (xtext, x, y, &offset, NULL); + if (!ent || offset < 0 || offset >= ent->str_len) + return FALSE; + + start = ent->str + offset; + end = g_utf8_find_next_char (start, ent->str + ent->str_len); + if (!gtk_xtext_word_select_char (start)) + return FALSE; + + while (start > ent->str) + { + unsigned char *prev = g_utf8_find_prev_char (ent->str, start); + if (!prev || !gtk_xtext_word_select_char (prev)) + break; + start = prev; + } + + while (end && end < ent->str + ent->str_len && gtk_xtext_word_select_char (end)) + end = g_utf8_find_next_char (end, ent->str + ent->str_len); + + if (ret_ent) + *ret_ent = ent; + if (ret_off) + *ret_off = (int)(start - ent->str); + if (ret_len) + *ret_len = (int)(end - start); + + return TRUE; +} + static void gtk_xtext_unrender_hilight (GtkXText *xtext) { @@ -2581,7 +2636,8 @@ gtk_xtext_button_press (GtkWidget * widget, GdkEventButton * event) if (event->type == GDK_2BUTTON_PRESS) /* WORD select */ { gtk_xtext_check_mark_stamp (xtext, mask); - if (gtk_xtext_get_word (xtext, x, y, &ent, &offset, &len, 0)) + if (gtk_xtext_get_word_select_range (xtext, x, y, &ent, &offset, &len) || + gtk_xtext_get_word (xtext, x, y, &ent, &offset, &len, 0)) { if (len == 0) return FALSE; @@ -4324,6 +4380,21 @@ gtk_xtext_nth (GtkXText *xtext, int line, int *subline) static int gtk_xtext_render_ents (GtkXText * xtext, textentry * enta, textentry * entb) { + /* + * On GTK3 (especially Wayland), event handlers are outside ::draw and direct + * window painting may not be presented immediately. Queue a frame instead so + * selections appear right away. + */ +#if HAVE_GTK3 + if (xtext->draw_cr == NULL) + { + GtkWidget *w = GTK_WIDGET (xtext); + if (gtk_widget_get_realized (w)) + gtk_widget_queue_draw (w); + return 0; + } +#endif + textentry *ent, *orig_ent, *tmp_ent; int line; int lines_max; From eb112f9cd7992bedae11d9b8f98b0c5d07ef3bc3 Mon Sep 17 00:00:00 2001 From: deepend Date: Tue, 17 Feb 2026 14:26:02 -0700 Subject: [PATCH 7/9] appended lines were scheduled with a timer (REFRESH_TIMEOUT * 2 where REFRESH_TIMEOUT is 20), which effectively adds ~40ms before draw in this path. I changed that specific path to render on the next idle cycle (g_idle_add) so locally echoed sent messages appear sooner. --- src/fe-gtk/xtext.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/fe-gtk/xtext.c b/src/fe-gtk/xtext.c index 3b7bc853..9f9d81d3 100644 --- a/src/fe-gtk/xtext.c +++ b/src/fe-gtk/xtext.c @@ -5426,10 +5426,11 @@ gtk_xtext_append_entry (xtext_buffer *buf, textentry * ent, time_t stamp) g_source_remove (buf->xtext->io_tag); buf->xtext->io_tag = 0; } - buf->xtext->add_io_tag = g_timeout_add (REFRESH_TIMEOUT * 2, - (GSourceFunc) - gtk_xtext_render_page_timeout, - buf->xtext); + /* Render new lines as soon as the UI is idle to avoid a noticeable + * delay between pressing Enter and seeing your own message. */ + buf->xtext->add_io_tag = g_idle_add ((GSourceFunc) + gtk_xtext_render_page_timeout, + buf->xtext); } } if (buf->scrollbar_down) From 97cbe98b3d9d64be1686bc669047502c24e86f71 Mon Sep 17 00:00:00 2001 From: deepend Date: Tue, 17 Feb 2026 14:33:16 -0700 Subject: [PATCH 8/9] added a min_indent threshold and only grow indent when the change is meaningful (greater than one space width), then only run the expensive gtk_xtext_recalc_widths() path when indent truly increased. I also kept the existing correctness behavior: when indent does materially change, it still fixes/recalculates and updates ent->indent/force_render as before. --- src/fe-gtk/xtext.c | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/fe-gtk/xtext.c b/src/fe-gtk/xtext.c index 9f9d81d3..aff3b989 100644 --- a/src/fe-gtk/xtext.c +++ b/src/fe-gtk/xtext.c @@ -5462,6 +5462,7 @@ gtk_xtext_append_indent (xtext_buffer *buf, int space; int tempindent; int left_width; + int min_indent; if (left_len == -1) left_len = strlen (left_text); @@ -5500,24 +5501,32 @@ gtk_xtext_append_indent (xtext_buffer *buf, else space = 0; + min_indent = MARGIN + space; + /* do we need to auto adjust the separator position? */ if (buf->xtext->auto_indent && buf->indent < buf->xtext->max_auto_indent && - ent->indent < MARGIN + space) + ent->indent < min_indent) { - tempindent = MARGIN + space + buf->xtext->space_width + left_width; + tempindent = min_indent + buf->xtext->space_width + left_width; - if (tempindent > buf->indent) + /* Ignore tiny one-pixel style nudges. + * They can trigger expensive full-width recalculations and are + * perceived as a slight delay when sending messages with indenting on. */ + if (tempindent > buf->indent + buf->xtext->space_width) buf->indent = tempindent; if (buf->indent > buf->xtext->max_auto_indent) buf->indent = buf->xtext->max_auto_indent; - gtk_xtext_fix_indent (buf); - gtk_xtext_recalc_widths (buf, FALSE); + if (buf->indent > ent->indent + left_width + buf->xtext->space_width) + { + gtk_xtext_fix_indent (buf); + gtk_xtext_recalc_widths (buf, FALSE); - ent->indent = (buf->indent - left_width) - buf->xtext->space_width; - buf->xtext->force_render = TRUE; + ent->indent = (buf->indent - left_width) - buf->xtext->space_width; + buf->xtext->force_render = TRUE; + } } gtk_xtext_append_entry (buf, ent, stamp); From ae50735311d2ebc8d235c0187737cce3d4f7ed4c Mon Sep 17 00:00:00 2001 From: deepend Date: Tue, 17 Feb 2026 14:49:15 -0700 Subject: [PATCH 9/9] =?UTF-8?q?Identified=20and=20fixed=20an=20obvious=20U?= =?UTF-8?q?I-path=20delay=20in=20message=20rendering:=20when=20a=20new=20l?= =?UTF-8?q?ine=20is=20appended=20and=20the=20user=20is=20already=20at=20th?= =?UTF-8?q?e=20bottom=20of=20the=20channel=20buffer=20(scrollbar=5Fdown),?= =?UTF-8?q?=20the=20code=20now=20renders=20immediately=20instead=20of=20al?= =?UTF-8?q?ways=20waiting=20for=20an=20idle=20callback.=20This=20removes?= =?UTF-8?q?=20the=20subtle=20=E2=80=9Csent=20but=20appears=20a=20moment=20?= =?UTF-8?q?later=E2=80=9D=20effect=20in=20large-scrollback=20channels.=20P?= =?UTF-8?q?reserved=20the=20previous=20idle-batched=20behavior=20for=20use?= =?UTF-8?q?rs=20who=20are=20not=20at=20the=20bottom=20(scrolling=20history?= =?UTF-8?q?),=20so=20performance-friendly=20deferred=20redraws=20still=20a?= =?UTF-8?q?pply=20in=20that=20case.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fe-gtk/xtext.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/fe-gtk/xtext.c b/src/fe-gtk/xtext.c index aff3b989..02e334d5 100644 --- a/src/fe-gtk/xtext.c +++ b/src/fe-gtk/xtext.c @@ -5426,11 +5426,16 @@ gtk_xtext_append_entry (xtext_buffer *buf, textentry * ent, time_t stamp) g_source_remove (buf->xtext->io_tag); buf->xtext->io_tag = 0; } - /* Render new lines as soon as the UI is idle to avoid a noticeable - * delay between pressing Enter and seeing your own message. */ - buf->xtext->add_io_tag = g_idle_add ((GSourceFunc) - gtk_xtext_render_page_timeout, - buf->xtext); + /* When at the bottom of the buffer, render immediately so long + * scrollback doesn't delay newly-sent messages appearing. + * Otherwise, keep idle batching to avoid extra redraws while + * scrolling around old content. */ + if (buf->scrollbar_down) + gtk_xtext_render_page_timeout (buf->xtext); + else + buf->xtext->add_io_tag = g_idle_add ((GSourceFunc) + gtk_xtext_render_page_timeout, + buf->xtext); } } if (buf->scrollbar_down)