From 4c782007bf100a3820dc265bca6fceda17991a77 Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Mon, 22 Jun 2009 10:43:38 +0200 Subject: Move all data about an adium theme into EmpathyAdiumData struct and share it with all EmpathyThemeAdium instances. --- libempathy-gtk/empathy-theme-adium.c | 492 +++++++++++++++++++-------------- libempathy-gtk/empathy-theme-adium.h | 14 +- libempathy-gtk/empathy-theme-manager.c | 24 +- 3 files changed, 318 insertions(+), 212 deletions(-) (limited to 'libempathy-gtk') diff --git a/libempathy-gtk/empathy-theme-adium.c b/libempathy-gtk/empathy-theme-adium.c index 9e5ff73af..6a2f0610b 100644 --- a/libempathy-gtk/empathy-theme-adium.c +++ b/libempathy-gtk/empathy-theme-adium.c @@ -26,6 +26,8 @@ #include #include +#include + #include #include @@ -46,34 +48,40 @@ #define MESSAGE_JOIN_PERIOD 5*60 typedef struct { + EmpathyAdiumData *data; EmpathySmileyManager *smiley_manager; EmpathyContact *last_contact; time_t last_timestamp; gboolean page_loaded; GList *message_queue; - gchar *path; - gchar *default_avatar_filename; - gchar *default_incoming_avatar_filename; - gchar *default_outgoing_avatar_filename; - gchar *template_html; - gchar *basedir; - gchar *in_content_html; - gsize in_content_len; - gchar *in_nextcontent_html; - gsize in_nextcontent_len; - gchar *out_content_html; - gsize out_content_len; - gchar *out_nextcontent_html; - gsize out_nextcontent_len; - gchar *status_html; - gsize status_len; } EmpathyThemeAdiumPriv; +struct _EmpathyAdiumData { + guint ref_count; + gchar *path; + gchar *basedir; + gchar *default_avatar_filename; + gchar *default_incoming_avatar_filename; + gchar *default_outgoing_avatar_filename; + gchar *template_html; + gchar *in_content_html; + gsize in_content_len; + gchar *in_nextcontent_html; + gsize in_nextcontent_len; + gchar *out_content_html; + gsize out_content_len; + gchar *out_nextcontent_html; + gsize out_nextcontent_len; + gchar *status_html; + gsize status_len; + GHashTable *info; +}; + static void theme_adium_iface_init (EmpathyChatViewIface *iface); enum { PROP_0, - PROP_PATH, + PROP_ADIUM_DATA, }; G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium, @@ -81,154 +89,6 @@ G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium, G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW, theme_adium_iface_init)); -static void -theme_adium_load (EmpathyThemeAdium *theme) -{ - EmpathyThemeAdiumPriv *priv = GET_PRIV (theme); - gchar *file; - gchar *template_html = NULL; - gsize template_len; - gchar *footer_html = NULL; - gsize footer_len; - GString *string; - gchar **strv = NULL; - gchar *css_path; - guint len = 0; - guint i = 0; - gchar *basedir_uri; - GValue *value; - GHashTable *theme_info; - const gchar *variant = NULL; - const gchar *font_family = NULL; - gint font_size = 0; - WebKitWebSettings *webkit_settings; - - priv->basedir = g_strconcat (priv->path, G_DIR_SEPARATOR_S "Contents" G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL); - basedir_uri = g_strconcat ("file://", priv->basedir, NULL); - - /* Load html files */ - file = g_build_filename (priv->basedir, "Incoming", "Content.html", NULL); - g_file_get_contents (file, &priv->in_content_html, &priv->in_content_len, NULL); - g_free (file); - - file = g_build_filename (priv->basedir, "Incoming", "NextContent.html", NULL); - g_file_get_contents (file, &priv->in_nextcontent_html, &priv->in_nextcontent_len, NULL); - g_free (file); - - file = g_build_filename (priv->basedir, "Outgoing", "Content.html", NULL); - g_file_get_contents (file, &priv->out_content_html, &priv->out_content_len, NULL); - g_free (file); - - file = g_build_filename (priv->basedir, "Outgoing", "NextContent.html", NULL); - g_file_get_contents (file, &priv->out_nextcontent_html, &priv->out_nextcontent_len, NULL); - g_free (file); - - file = g_build_filename (priv->basedir, "Status.html", NULL); - g_file_get_contents (file, &priv->status_html, &priv->status_len, NULL); - g_free (file); - - file = g_build_filename (priv->basedir, "Footer.html", NULL); - g_file_get_contents (file, &footer_html, &footer_len, NULL); - g_free (file); - - file = g_build_filename (priv->basedir, "Incoming", "buddy_icon.png", NULL); - if (g_file_test (file, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) { - priv->default_incoming_avatar_filename = file; - } else { - g_free (file); - } - - file = g_build_filename (priv->basedir, "Outgoing", "buddy_icon.png", NULL); - if (g_file_test (file, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) { - priv->default_outgoing_avatar_filename = file; - } else { - g_free (file); - } - - css_path = g_build_filename (priv->basedir, "main.css", NULL); - - /* There is 2 formats for Template.html: The old one has 4 parameters, - * the new one has 5 parameters. */ - file = g_build_filename (priv->basedir, "Template.html", NULL); - if (g_file_get_contents (file, &template_html, &template_len, NULL)) { - strv = g_strsplit (template_html, "%@", -1); - len = g_strv_length (strv); - } - g_free (file); - - if (len != 5 && len != 6) { - /* Either the theme has no template or it don't have the good - * number of parameters. Fallback to use our own template. */ - g_free (template_html); - g_strfreev (strv); - - file = empathy_file_lookup ("Template.html", "data"); - g_file_get_contents (file, &template_html, &template_len, NULL); - g_free (file); - strv = g_strsplit (template_html, "%@", -1); - len = g_strv_length (strv); - } - - file = g_build_filename (priv->path, "Contents", "Info.plist", NULL); - value = empathy_plist_parse_from_file (file); - g_free (file); - - if (value) { - theme_info = g_value_get_boxed (value); - variant = tp_asv_get_string (theme_info, "DefaultVariant"); - font_family = tp_asv_get_string (theme_info, "DefaultFontFamily"); - font_size = tp_asv_get_int32 (theme_info, "DefaultFontSize", NULL); - } - - /* Replace %@ with the needed information in the template html. */ - string = g_string_sized_new (template_len); - g_string_append (string, strv[i++]); - g_string_append (string, priv->basedir); - g_string_append (string, strv[i++]); - if (len == 6) { - /* We include main.css by default */ - g_string_append_printf (string, "@import url(\"%s\");", css_path); - g_string_append (string, strv[i++]); - if (variant) { - g_string_append (string, "Variants/"); - g_string_append (string, variant); - g_string_append (string, ".css"); - } - } else { - /* FIXME: We should set main.css OR the variant css */ - g_string_append (string, css_path); - } - g_string_append (string, strv[i++]); - g_string_append (string, ""); /* We don't want header */ - g_string_append (string, strv[i++]); - /* FIXME: We should replace adium %macros% in footer */ - if (footer_html) { - g_string_append (string, footer_html); - } - g_string_append (string, strv[i++]); - priv->template_html = g_string_free (string, FALSE); - - /* Load the template */ - webkit_settings = webkit_web_settings_new (); - if (font_family) { - g_object_set (G_OBJECT (webkit_settings), "default-font-family", font_family, NULL); - } - if (font_size) { - g_object_set (G_OBJECT (webkit_settings), "default-font-size", font_size, NULL); - } - - webkit_web_view_set_settings (WEBKIT_WEB_VIEW (theme), webkit_settings); - webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (theme), - priv->template_html, basedir_uri); - - g_object_unref (webkit_settings); - g_free (basedir_uri); - g_free (footer_html); - g_free (template_html); - g_free (css_path); - g_strfreev (strv); -} - static WebKitNavigationResponse theme_adium_navigation_requested_cb (WebKitWebView *view, WebKitWebFrame *frame, @@ -585,17 +445,17 @@ theme_adium_append_message (EmpathyChatView *view, } if (!avatar_filename) { if (empathy_contact_is_user (sender)) { - avatar_filename = priv->default_outgoing_avatar_filename; + avatar_filename = priv->data->default_outgoing_avatar_filename; } else { - avatar_filename = priv->default_incoming_avatar_filename; + avatar_filename = priv->data->default_incoming_avatar_filename; } if (!avatar_filename) { - if (!priv->default_avatar_filename) { - priv->default_avatar_filename = + if (!priv->data->default_avatar_filename) { + priv->data->default_avatar_filename = empathy_filename_from_icon_name ("stock_person", GTK_ICON_SIZE_DIALOG); } - avatar_filename = priv->default_avatar_filename; + avatar_filename = priv->data->default_avatar_filename; } } @@ -610,13 +470,13 @@ theme_adium_append_message (EmpathyChatView *view, func = "appendNextMessage"; if (empathy_contact_is_user (sender)) { message_classes = "consecutive incoming message"; - html = priv->out_nextcontent_html; - len = priv->out_nextcontent_len; + html = priv->data->out_nextcontent_html; + len = priv->data->out_nextcontent_len; } if (!html) { message_classes = "consecutive message outgoing"; - html = priv->in_nextcontent_html; - len = priv->in_nextcontent_len; + html = priv->data->in_nextcontent_html; + len = priv->data->in_nextcontent_len; } } if (!html) { @@ -624,15 +484,15 @@ theme_adium_append_message (EmpathyChatView *view, if (!message_classes) { message_classes = "incoming message"; } - html = priv->out_content_html; - len = priv->out_content_len; + html = priv->data->out_content_html; + len = priv->data->out_content_len; } if (!html) { if (!message_classes) { message_classes = "message outgoing"; } - html = priv->in_content_html; - len = priv->in_content_len; + html = priv->data->in_content_html; + len = priv->data->in_content_len; } } @@ -657,9 +517,10 @@ theme_adium_append_event (EmpathyChatView *view, EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view); EmpathyThemeAdiumPriv *priv = GET_PRIV (theme); - if (priv->status_html) { + if (priv->data->status_html) { theme_adium_append_html (theme, "appendMessage", - priv->status_html, priv->status_len, + priv->data->status_html, + priv->data->status_len, str, NULL, NULL, NULL, NULL, "event", empathy_time_get_current ()); } @@ -698,9 +559,10 @@ theme_adium_clear (EmpathyChatView *view) gchar *basedir_uri; priv->page_loaded = FALSE; - basedir_uri = g_strconcat ("file://", priv->basedir, NULL); + basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL); webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (view), - priv->template_html, basedir_uri); + priv->data->template_html, + basedir_uri); g_free (basedir_uri); } @@ -798,17 +660,7 @@ theme_adium_finalize (GObject *object) { EmpathyThemeAdiumPriv *priv = GET_PRIV (object); - g_free (priv->basedir); - g_free (priv->template_html); - g_free (priv->in_content_html); - g_free (priv->in_nextcontent_html); - g_free (priv->out_content_html); - g_free (priv->out_nextcontent_html); - g_free (priv->default_avatar_filename); - g_free (priv->default_incoming_avatar_filename); - g_free (priv->default_outgoing_avatar_filename); - g_free (priv->path); - g_free (priv->status_html); + empathy_adium_data_unref (priv->data); G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object); } @@ -834,7 +686,32 @@ theme_adium_dispose (GObject *object) static void theme_adium_constructed (GObject *object) { - theme_adium_load (EMPATHY_THEME_ADIUM (object)); + EmpathyThemeAdiumPriv *priv = GET_PRIV (object); + gchar *basedir_uri; + const gchar *font_family = NULL; + gint font_size = 0; + WebKitWebSettings *webkit_settings; + + /* Set default settings */ + font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily"); + font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL); + webkit_settings = webkit_web_settings_new (); + if (font_family) { + g_object_set (G_OBJECT (webkit_settings), "default-font-family", font_family, NULL); + } + if (font_size) { + g_object_set (G_OBJECT (webkit_settings), "default-font-size", font_size, NULL); + } + webkit_web_view_set_settings (WEBKIT_WEB_VIEW (object), webkit_settings); + + /* Load template */ + basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL); + webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (object), + priv->data->template_html, + basedir_uri); + + g_object_unref (webkit_settings); + g_free (basedir_uri); } static void @@ -846,8 +723,8 @@ theme_adium_get_property (GObject *object, EmpathyThemeAdiumPriv *priv = GET_PRIV (object); switch (param_id) { - case PROP_PATH: - g_value_set_string (value, priv->path); + case PROP_ADIUM_DATA: + g_value_set_boxed (value, priv->data); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); @@ -864,9 +741,9 @@ theme_adium_set_property (GObject *object, EmpathyThemeAdiumPriv *priv = GET_PRIV (object); switch (param_id) { - case PROP_PATH: - g_free (priv->path); - priv->path = g_value_dup_string (value); + case PROP_ADIUM_DATA: + g_assert (priv->data == NULL); + priv->data = g_value_dup_boxed (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); @@ -886,11 +763,11 @@ empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass) object_class->set_property = theme_adium_set_property; g_object_class_install_property (object_class, - PROP_PATH, - g_param_spec_string ("path", - "The theme path", - "Path to the adium theme", - g_get_home_dir (), + PROP_ADIUM_DATA, + g_param_spec_boxed ("adium-data", + "The theme data", + "Data for the adium theme", + EMPATHY_TYPE_ADIUM_DATA, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE)); @@ -920,17 +797,17 @@ empathy_theme_adium_init (EmpathyThemeAdium *theme) } EmpathyThemeAdium * -empathy_theme_adium_new (const gchar *path) +empathy_theme_adium_new (EmpathyAdiumData *data) { - g_return_val_if_fail (empathy_theme_adium_is_valid (path), NULL); + g_return_val_if_fail (data != NULL, NULL); return g_object_new (EMPATHY_TYPE_THEME_ADIUM, - "path", path, + "adium-data", data, NULL); } gboolean -empathy_theme_adium_is_valid (const gchar *path) +empathy_adium_path_is_valid (const gchar *path) { gboolean ret; gchar *file; @@ -947,3 +824,202 @@ empathy_theme_adium_is_valid (const gchar *path) return ret; } +GType +empathy_adium_data_get_type (void) +{ + static GType type_id = 0; + + if (!type_id) + { + type_id = g_boxed_type_register_static ("EmpathyAdiumData", + (GBoxedCopyFunc) empathy_adium_data_ref, + (GBoxedFreeFunc) empathy_adium_data_unref); + } + + return type_id; +} + +EmpathyAdiumData * +empathy_adium_data_new (const gchar *path) +{ + EmpathyAdiumData *data; + gchar *file; + gchar *template_html = NULL; + gsize template_len; + gchar *footer_html = NULL; + gsize footer_len; + GString *string; + gchar **strv = NULL; + gchar *css_path; + guint len = 0; + guint i = 0; + GValue *value; + + g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL); + + data = g_slice_new0 (EmpathyAdiumData); + data->ref_count = 1; + data->path = g_strdup (path); + data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents" + G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL); + + /* Load html files */ + file = g_build_filename (data->basedir, "Incoming", "Content.html", NULL); + g_file_get_contents (file, &data->in_content_html, &data->in_content_len, NULL); + g_free (file); + + file = g_build_filename (data->basedir, "Incoming", "NextContent.html", NULL); + g_file_get_contents (file, &data->in_nextcontent_html, &data->in_nextcontent_len, NULL); + g_free (file); + + file = g_build_filename (data->basedir, "Outgoing", "Content.html", NULL); + g_file_get_contents (file, &data->out_content_html, &data->out_content_len, NULL); + g_free (file); + + file = g_build_filename (data->basedir, "Outgoing", "NextContent.html", NULL); + g_file_get_contents (file, &data->out_nextcontent_html, &data->out_nextcontent_len, NULL); + g_free (file); + + file = g_build_filename (data->basedir, "Status.html", NULL); + g_file_get_contents (file, &data->status_html, &data->status_len, NULL); + g_free (file); + + file = g_build_filename (data->basedir, "Footer.html", NULL); + g_file_get_contents (file, &footer_html, &footer_len, NULL); + g_free (file); + + file = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL); + if (g_file_test (file, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) { + data->default_incoming_avatar_filename = file; + } else { + g_free (file); + } + + file = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL); + if (g_file_test (file, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) { + data->default_outgoing_avatar_filename = file; + } else { + g_free (file); + } + + css_path = g_build_filename (data->basedir, "main.css", NULL); + + /* There is 2 formats for Template.html: The old one has 4 parameters, + * the new one has 5 parameters. */ + file = g_build_filename (data->basedir, "Template.html", NULL); + if (g_file_get_contents (file, &template_html, &template_len, NULL)) { + strv = g_strsplit (template_html, "%@", -1); + len = g_strv_length (strv); + } + g_free (file); + + if (len != 5 && len != 6) { + /* Either the theme has no template or it don't have the good + * number of parameters. Fallback to use our own template. */ + g_free (template_html); + g_strfreev (strv); + + file = empathy_file_lookup ("Template.html", "data"); + g_file_get_contents (file, &template_html, &template_len, NULL); + g_free (file); + strv = g_strsplit (template_html, "%@", -1); + len = g_strv_length (strv); + } + + /* Load Info.plist into a hash table */ + file = g_build_filename (data->path, "Contents", "Info.plist", NULL); + value = empathy_plist_parse_from_file (file); + g_free (file); + if (value) { + data->info = g_value_dup_boxed (value); + tp_g_value_slice_free (value); + } + + /* Replace %@ with the needed information in the template html. */ + string = g_string_sized_new (template_len); + g_string_append (string, strv[i++]); + g_string_append (string, data->basedir); + g_string_append (string, strv[i++]); + if (len == 6) { + const gchar *variant; + + /* We include main.css by default */ + g_string_append_printf (string, "@import url(\"%s\");", css_path); + g_string_append (string, strv[i++]); + variant = tp_asv_get_string (data->info, "DefaultVariant"); + if (variant) { + g_string_append (string, "Variants/"); + g_string_append (string, variant); + g_string_append (string, ".css"); + } + } else { + /* FIXME: We should set main.css OR the variant css */ + g_string_append (string, css_path); + } + g_string_append (string, strv[i++]); + g_string_append (string, ""); /* We don't want header */ + g_string_append (string, strv[i++]); + /* FIXME: We should replace adium %macros% in footer */ + if (footer_html) { + g_string_append (string, footer_html); + } + g_string_append (string, strv[i++]); + data->template_html = g_string_free (string, FALSE); + + g_free (footer_html); + g_free (template_html); + g_free (css_path); + g_strfreev (strv); + + return data; +} + +EmpathyAdiumData * +empathy_adium_data_ref (EmpathyAdiumData *data) +{ + g_return_val_if_fail (data != NULL, NULL); + + data->ref_count++; + + return data; +} + +void +empathy_adium_data_unref (EmpathyAdiumData *data) +{ + g_return_if_fail (data != NULL); + + data->ref_count--; + if (data->ref_count == 0) { + g_free (data->path); + g_free (data->basedir); + g_free (data->template_html); + g_free (data->in_content_html); + g_free (data->in_nextcontent_html); + g_free (data->out_content_html); + g_free (data->out_nextcontent_html); + g_free (data->default_avatar_filename); + g_free (data->default_incoming_avatar_filename); + g_free (data->default_outgoing_avatar_filename); + g_free (data->status_html); + g_hash_table_unref (data->info); + g_slice_free (EmpathyAdiumData, data); + } +} + +GHashTable * +empathy_adium_data_get_info (EmpathyAdiumData *data) +{ + g_return_val_if_fail (data != NULL, NULL); + + return data->info; +} + +const gchar * +empathy_adium_data_get_path (EmpathyAdiumData *data) +{ + g_return_val_if_fail (data != NULL, NULL); + + return data->path; +} + diff --git a/libempathy-gtk/empathy-theme-adium.h b/libempathy-gtk/empathy-theme-adium.h index b62675680..868703073 100644 --- a/libempathy-gtk/empathy-theme-adium.h +++ b/libempathy-gtk/empathy-theme-adium.h @@ -37,6 +37,7 @@ G_BEGIN_DECLS typedef struct _EmpathyThemeAdium EmpathyThemeAdium; typedef struct _EmpathyThemeAdiumClass EmpathyThemeAdiumClass; +typedef struct _EmpathyAdiumData EmpathyAdiumData; struct _EmpathyThemeAdium { WebKitWebView parent; @@ -48,8 +49,17 @@ struct _EmpathyThemeAdiumClass { }; GType empathy_theme_adium_get_type (void) G_GNUC_CONST; -EmpathyThemeAdium *empathy_theme_adium_new (const gchar *path); -gboolean empathy_theme_adium_is_valid (const gchar *path); +EmpathyThemeAdium *empathy_theme_adium_new (EmpathyAdiumData *data); + +gboolean empathy_adium_path_is_valid (const gchar *path); + +#define EMPATHY_TYPE_ADIUM_DATA (empathy_adium_data_get_type ()) +GType empathy_adium_data_get_type (void) G_GNUC_CONST; +EmpathyAdiumData *empathy_adium_data_new (const gchar *path); +EmpathyAdiumData *empathy_adium_data_ref (EmpathyAdiumData *data); +void empathy_adium_data_unref (EmpathyAdiumData *data); +GHashTable *empathy_adium_data_get_info (EmpathyAdiumData *data); +const gchar *empathy_adium_data_get_path (EmpathyAdiumData *data); G_END_DECLS diff --git a/libempathy-gtk/empathy-theme-manager.c b/libempathy-gtk/empathy-theme-manager.c index c6da8ea68..348d6ec70 100644 --- a/libempathy-gtk/empathy-theme-manager.c +++ b/libempathy-gtk/empathy-theme-manager.c @@ -329,8 +329,28 @@ empathy_theme_manager_create_view (EmpathyThemeManager *manager) #ifdef HAVE_WEBKIT if (strcmp (priv->name, "adium") == 0) { - if (empathy_theme_adium_is_valid (priv->adium_path)) { - return EMPATHY_CHAT_VIEW (empathy_theme_adium_new (priv->adium_path)); + if (empathy_adium_path_is_valid (priv->adium_path)) { + static EmpathyAdiumData *data = NULL; + EmpathyThemeAdium *theme_adium; + + if (data && + !tp_strdiff (empathy_adium_data_get_path (data), + priv->adium_path)) { + /* Theme did not change, reuse data */ + theme_adium = empathy_theme_adium_new (data); + return EMPATHY_CHAT_VIEW (theme_adium); + } + + /* Theme changed, drop old data if any and + * load a new one */ + if (data) { + empathy_adium_data_unref (data); + data = NULL; + } + + data = empathy_adium_data_new (priv->adium_path); + theme_adium = empathy_theme_adium_new (data); + return EMPATHY_CHAT_VIEW (theme_adium); } else { /* The adium path is not valid, fallback to classic theme */ return EMPATHY_CHAT_VIEW (theme_manager_create_irc_view (manager)); -- cgit v1.2.3