From d10c9654fa2a4a2286509d781f69b34ef66fd612 Mon Sep 17 00:00:00 2001 From: deepend Date: Thu, 26 Feb 2026 08:35:37 -0700 Subject: [PATCH] Added a GTK3 theme directory resolver that supports versioned theme folders (gtk-3.x) and selects the best match for the running GTK minor version, instead of assuming only gtk-3.0. This improves compatibility with real-world GTK3 themes that split selectors/styles by GTK minor versions. Updated theme application to load CSS/resources from the resolved gtk-3.x folder and updated the error path/message when a valid gtk-3.x/gtk.css layout is missing. Exposed the resolver in the GTK frontend header so other GTK UI code can validate theme layouts consistently. Updated the Preferences GTK3 theme picker to validate themes via the resolver (so themes with e.g. gtk-3.24/gtk.css now appear as valid). Updated archive import validation to recognize gtk-3.x directories (not just gtk-3.0) and adjusted user-facing validation messages accordingly. --- src/common/zoitechat.c | 6 +-- src/fe-gtk/fe-gtk.c | 120 +++++++++++++++++++++++++++++++++++++---- src/fe-gtk/fe-gtk.h | 5 +- src/fe-gtk/setup.c | 7 ++- 4 files changed, 120 insertions(+), 18 deletions(-) diff --git a/src/common/zoitechat.c b/src/common/zoitechat.c index d421a023..b2da65a2 100644 --- a/src/common/zoitechat.c +++ b/src/common/zoitechat.c @@ -413,7 +413,7 @@ zoitechat_find_gtk3_theme_root (const char *search_dir, if (g_file_test (child, G_FILE_TEST_IS_DIR)) { - if (strcmp (name, "gtk-3.0") == 0) + if (g_str_has_prefix (name, "gtk-3.")) { char *gtk_css = g_build_filename (child, "gtk.css", NULL); @@ -813,10 +813,10 @@ zoitechat_import_gtk3_theme_archive (const char *archive_path, if (missing_gtk_css) g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, - _("Archive contains a gtk-3.0 directory, but gtk-3.0/gtk.css is missing.")); + _("Archive contains a gtk-3.x directory, but gtk.css is missing.")); else g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, - _("Archive is not a GTK3 theme. Expected layout: /gtk-3.0/gtk.css.")); + _("Archive is not a GTK3 theme. Expected layout: /gtk-3.x/gtk.css.")); goto cleanup; } diff --git a/src/fe-gtk/fe-gtk.c b/src/fe-gtk/fe-gtk.c index a845de10..02c34712 100644 --- a/src/fe-gtk/fe-gtk.c +++ b/src/fe-gtk/fe-gtk.c @@ -591,6 +591,111 @@ static gboolean auto_dark_mode_enabled = FALSE; static gboolean dark_mode_state_initialized = FALSE; +static gboolean +fe_parse_gtk3_minor_from_dirname (const char *name, gint *minor_out) +{ + const char *prefix = "gtk-3."; + char *endptr = NULL; + long value; + + if (!name || !g_str_has_prefix (name, prefix)) + return FALSE; + + value = strtol (name + strlen (prefix), &endptr, 10); + if (endptr == name + strlen (prefix) || *endptr != '\0' || value < 0 || value > G_MAXINT) + return FALSE; + + if (minor_out) + *minor_out = (gint) value; + return TRUE; +} + +gboolean +fe_resolve_gtk3_theme_dir (const char *theme_root, + char **gtk3_dir_out, + gboolean *has_dark_css_out) +{ + GDir *dir; + const char *entry; + gint runtime_minor = gtk_get_minor_version (); + gint best_minor = -1; + gint best_distance = G_MAXINT; + char *best_dir = NULL; + gboolean has_dark = FALSE; + + if (gtk3_dir_out) + *gtk3_dir_out = NULL; + if (has_dark_css_out) + *has_dark_css_out = FALSE; + + if (!theme_root || !*theme_root) + return FALSE; + + dir = g_dir_open (theme_root, 0, NULL); + if (!dir) + return FALSE; + + while ((entry = g_dir_read_name (dir)) != NULL) + { + char *candidate_dir; + char *gtk_css; + char *gtk_dark_css; + gint minor; + gint distance; + + if (!fe_parse_gtk3_minor_from_dirname (entry, &minor)) + continue; + + candidate_dir = g_build_filename (theme_root, entry, NULL); + if (!g_file_test (candidate_dir, G_FILE_TEST_IS_DIR)) + { + g_free (candidate_dir); + continue; + } + + gtk_css = g_build_filename (candidate_dir, "gtk.css", NULL); + if (!g_file_test (gtk_css, G_FILE_TEST_IS_REGULAR)) + { + g_free (gtk_css); + g_free (candidate_dir); + continue; + } + g_free (gtk_css); + + distance = (minor <= runtime_minor) + ? (runtime_minor - minor) + : (10000 + (minor - runtime_minor)); + + if (!best_dir || distance < best_distance || (distance == best_distance && minor > best_minor)) + { + g_free (best_dir); + best_dir = candidate_dir; + best_minor = minor; + best_distance = distance; + + gtk_dark_css = g_build_filename (best_dir, "gtk-dark.css", NULL); + has_dark = g_file_test (gtk_dark_css, G_FILE_TEST_IS_REGULAR); + g_free (gtk_dark_css); + + candidate_dir = NULL; + } + + g_free (candidate_dir); + } + + g_dir_close (dir); + + if (!best_dir) + return FALSE; + + if (gtk3_dir_out) + *gtk3_dir_out = g_strdup (best_dir); + if (has_dark_css_out) + *has_dark_css_out = has_dark; + g_free (best_dir); + return TRUE; +} + static GtkCssProvider *gtk3_theme_provider = NULL; static char *gtk3_theme_provider_name = NULL; static gboolean gtk3_theme_provider_dark = FALSE; @@ -680,21 +785,16 @@ fe_apply_gtk3_theme_with_reload (const char *theme_name, gboolean force_reload, } theme_dir = g_build_filename (get_xdir (), "gtk3-themes", theme_name, NULL); - gtk3_dir = g_build_filename (theme_dir, "gtk-3.0", NULL); - gtk_css = g_build_filename (gtk3_dir, "gtk.css", NULL); - gtk_dark_css = g_build_filename (gtk3_dir, "gtk-dark.css", NULL); - gtk_resource = g_build_filename (gtk3_dir, "gtk.gresource", NULL); - - if (!g_file_test (gtk_css, G_FILE_TEST_IS_REGULAR)) + if (!fe_resolve_gtk3_theme_dir (theme_dir, >k3_dir, NULL)) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, - _("GTK3 theme '%s' is missing gtk-3.0/gtk.css."), theme_name); - g_free (gtk_dark_css); - g_free (gtk_css); - g_free (gtk3_dir); + _("GTK3 theme '%s' is missing a valid gtk-3.x/gtk.css directory."), theme_name); g_free (theme_dir); return FALSE; } + gtk_css = g_build_filename (gtk3_dir, "gtk.css", NULL); + gtk_dark_css = g_build_filename (gtk3_dir, "gtk-dark.css", NULL); + gtk_resource = g_build_filename (gtk3_dir, "gtk.gresource", NULL); if (dark && g_file_test (gtk_dark_css, G_FILE_TEST_IS_REGULAR)) selected_css = gtk_dark_css; diff --git a/src/fe-gtk/fe-gtk.h b/src/fe-gtk/fe-gtk.h index a91a8f09..ffe33ad6 100644 --- a/src/fe-gtk/fe-gtk.h +++ b/src/fe-gtk/fe-gtk.h @@ -195,7 +195,10 @@ void fe_refresh_auto_dark_mode (void); gboolean fe_apply_theme_for_mode (unsigned int mode, gboolean *palette_changed); gboolean fe_apply_gtk3_theme (const char *theme_name, GError **error); gboolean fe_apply_gtk3_theme_with_reload (const char *theme_name, gboolean force_reload, - GError **error); + GError **error); +gboolean fe_resolve_gtk3_theme_dir (const char *theme_root, + char **gtk3_dir_out, + gboolean *has_dark_css_out); void fe_apply_theme_to_toplevel (GtkWidget *window); #define SPELL_ENTRY_GET_TEXT(e) ((char *)(gtk_entry_get_text (GTK_ENTRY(e)))) diff --git a/src/fe-gtk/setup.c b/src/fe-gtk/setup.c index 26200d27..33cfc882 100644 --- a/src/fe-gtk/setup.c +++ b/src/fe-gtk/setup.c @@ -1928,13 +1928,12 @@ setup_gtk3_theme_populate (setup_theme_ui *ui) while ((name = g_dir_read_name (dir))) { char *theme_path; - char *gtk_css_path; + char *gtk3_dir = NULL; theme_path = g_build_filename (themes_dir, name, NULL); - gtk_css_path = g_build_filename (theme_path, "gtk-3.0", "gtk.css", NULL); if (g_file_test (theme_path, G_FILE_TEST_IS_DIR) - && g_file_test (gtk_css_path, G_FILE_TEST_IS_REGULAR)) + && fe_resolve_gtk3_theme_dir (theme_path, >k3_dir, NULL)) { gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (ui->gtk3_combo), name); g_ptr_array_add (ui->gtk3_theme_paths, theme_path); @@ -1942,7 +1941,7 @@ setup_gtk3_theme_populate (setup_theme_ui *ui) theme_path = NULL; } - g_free (gtk_css_path); + g_free (gtk3_dir); g_free (theme_path); }