From 3dac269c6a323f8679442a8817a16c8df7af9476 Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Fri, 12 Sep 2025 00:07:45 +0200 Subject: [PATCH 01/12] gdesktopappinfo: Rename lookup_default to mime_lookup_default We can lookup other default things, too. It also makes it more symmetrical to the already existing mime_lookup. --- gio/gdesktopappinfo.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c index db58e02a28..c007577710 100644 --- a/gio/gdesktopappinfo.c +++ b/gio/gdesktopappinfo.c @@ -1349,9 +1349,9 @@ desktop_file_dir_unindexed_mime_lookup (DesktopFileDir *dir, } static void -desktop_file_dir_unindexed_default_lookup (DesktopFileDir *dir, - const gchar *mime_type, - GPtrArray *results) +desktop_file_dir_unindexed_mime_lookup_default (DesktopFileDir *dir, + const gchar *mime_type, + GPtrArray *results) { UnindexedMimeTweaks *tweaks; gint i; @@ -1582,7 +1582,7 @@ desktop_file_dir_mime_lookup (DesktopFileDir *dir, } /*< internal > - * desktop_file_dir_default_lookup: + * desktop_file_dir_mime_lookup_default: * @dir: a #DesktopFileDir * @mime_type: the mime type to look up * @results: an array to store the results in @@ -1590,11 +1590,11 @@ desktop_file_dir_mime_lookup (DesktopFileDir *dir, * Collects the "default" applications for a given mime type from @dir. */ static void -desktop_file_dir_default_lookup (DesktopFileDir *dir, - const gchar *mime_type, - GPtrArray *results) +desktop_file_dir_mime_lookup_default (DesktopFileDir *dir, + const gchar *mime_type, + GPtrArray *results) { - desktop_file_dir_unindexed_default_lookup (dir, mime_type, results); + desktop_file_dir_unindexed_mime_lookup_default (dir, mime_type, results); } /*< internal > @@ -4664,7 +4664,7 @@ g_app_info_get_default_for_type_impl (const char *content_type, { /* Collect all the default apps for this type */ for (j = 0; j < desktop_file_dirs->len; j++) - desktop_file_dir_default_lookup (g_ptr_array_index (desktop_file_dirs, j), types[i], results); + desktop_file_dir_mime_lookup_default (g_ptr_array_index (desktop_file_dirs, j), types[i], results); /* Consider the associations as well... */ for (j = 0; j < desktop_file_dirs->len; j++) -- GitLab From d029877c5de6dbc1afc238dbd3b11f138ac4cdcb Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Wed, 10 Sep 2025 18:15:46 +0200 Subject: [PATCH 02/12] gkeyfile: Add G_KEY_FILE_DESKTOP_KEY_IMPLEMENTS This has been defined for a while now and we already look up that key, so let's just add it to the public API as well. --- gio/gdesktopappinfo.c | 5 ++++- glib/gkeyfile.c | 9 +++++++++ glib/gkeyfile.h | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c index c007577710..4c2eca8e46 100644 --- a/gio/gdesktopappinfo.c +++ b/gio/gdesktopappinfo.c @@ -1247,7 +1247,10 @@ desktop_file_dir_unindexed_setup_search (DesktopFileDir *dir) } /* Make note of the Implements= line */ - implements = g_key_file_get_string_list (key_file, "Desktop Entry", "Implements", NULL, NULL); + implements = g_key_file_get_string_list (key_file, + "Desktop Entry", + G_KEY_FILE_DESKTOP_KEY_IMPLEMENTS, + NULL, NULL); for (i = 0; implements && implements[i]; i++) memory_index_add_token (dir->memory_implementations, implements[i], i, 0, app); g_strfreev (implements); diff --git a/glib/gkeyfile.c b/glib/gkeyfile.c index 7991c964d2..27faa95150 100644 --- a/glib/gkeyfile.c +++ b/glib/gkeyfile.c @@ -474,6 +474,15 @@ * Since: 2.38 */ +/** + * G_KEY_FILE_DESKTOP_KEY_IMPLEMENTS: + * + * A key under [const@GLib.KEY_FILE_DESKTOP_GROUP], whose value is a string list + * giving the available intents. + * + * Since: 2.86 + */ + /** * G_KEY_FILE_DESKTOP_TYPE_APPLICATION: * diff --git a/glib/gkeyfile.h b/glib/gkeyfile.h index 9d026d681d..e53688e14e 100644 --- a/glib/gkeyfile.h +++ b/glib/gkeyfile.h @@ -322,6 +322,7 @@ gboolean g_key_file_remove_group (GKeyFile *key_file, #define G_KEY_FILE_DESKTOP_KEY_URL "URL" #define G_KEY_FILE_DESKTOP_KEY_DBUS_ACTIVATABLE "DBusActivatable" #define G_KEY_FILE_DESKTOP_KEY_ACTIONS "Actions" +#define G_KEY_FILE_DESKTOP_KEY_IMPLEMENTS "Implements" #define G_KEY_FILE_DESKTOP_TYPE_APPLICATION "Application" #define G_KEY_FILE_DESKTOP_TYPE_LINK "Link" -- GitLab From ce131268be0b94b3f8e71507869eadfb9a7887a2 Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Wed, 10 Sep 2025 18:21:36 +0200 Subject: [PATCH 03/12] gdesktopappinfo: Add get_intents and has_intent methods Besides finding apps which implement certain intents, we might also have an existing GDesktopAppInfo for which we want to know if an intent is supported or which intents are implemented. --- gio/gdesktopappinfo.c | 34 ++++++++++++++++++++++++++++++++++ gio/gdesktopappinfo.h | 5 +++++ 2 files changed, 39 insertions(+) diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c index 4c2eca8e46..6aa685d178 100644 --- a/gio/gdesktopappinfo.c +++ b/gio/gdesktopappinfo.c @@ -119,6 +119,7 @@ struct _GDesktopAppInfo char *startup_wm_class; char **mime_types; char **actions; + char **intents; guint nodisplay : 1; guint hidden : 1; @@ -1743,6 +1744,7 @@ g_desktop_app_info_finalize (GObject *object) g_strfreev (info->mime_types); g_free (info->app_id); g_strfreev (info->actions); + g_strfreev (info->intents); G_OBJECT_CLASS (g_desktop_app_info_parent_class)->finalize (object); } @@ -2029,6 +2031,7 @@ g_desktop_app_info_load_from_keyfile (GDesktopAppInfo *info, info->mime_types = g_key_file_get_string_list (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_MIME_TYPE, NULL, NULL); bus_activatable = g_key_file_get_boolean (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_DBUS_ACTIVATABLE, NULL); info->actions = g_key_file_get_string_list (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ACTIONS, NULL, NULL); + info->intents = g_key_file_get_string_list (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_IMPLEMENTS, NULL, NULL); /* Remove the special-case: no Actions= key just means 0 extra actions */ if (info->actions == NULL) @@ -2252,6 +2255,7 @@ g_desktop_app_info_dup (GAppInfo *appinfo) new_info->hidden = info->hidden; new_info->terminal = info->terminal; new_info->startup_notify = info->startup_notify; + new_info->intents = g_strdupv (info->intents); return G_APP_INFO (new_info); } @@ -2402,6 +2406,36 @@ g_desktop_app_info_get_keywords (GDesktopAppInfo *info) return (const char * const *)info->keywords; } +/** + * g_desktop_app_info_get_intents: + * @info: a [class@GioUnix.DesktopAppInfo] + * + * Gets the implemented intents from the desktop file. + * + * Returns: (transfer none): The value of the + * [`Implements` key](https://specifications.freedesktop.org/desktop-entry-spec/latest/recognized-keys.html) + * + * Since: 2.86 + */ +const char * const * +g_desktop_app_info_get_intents (GDesktopAppInfo *info) +{ + return (const char * const *)info->intents; +} + +gboolean +g_desktop_app_info_has_intent (GDesktopAppInfo *info, + const char *intent) +{ + for (size_t i = 0; info->intents && info->intents[i]; i++) + { + if (g_str_equal (info->intents[i], intent)) + return TRUE; + } + + return FALSE; +} + /** * g_desktop_app_info_get_generic_name: * @info: a [class@GioUnix.DesktopAppInfo] diff --git a/gio/gdesktopappinfo.h b/gio/gdesktopappinfo.h index 7981a026f7..45c0567b26 100644 --- a/gio/gdesktopappinfo.h +++ b/gio/gdesktopappinfo.h @@ -62,6 +62,11 @@ GIO_AVAILABLE_IN_2_30 const char * g_desktop_app_info_get_categories (GDesktopAppInfo *info); GIO_AVAILABLE_IN_2_30 const char * const *g_desktop_app_info_get_keywords (GDesktopAppInfo *info); +GIO_AVAILABLE_IN_2_86 +const char * const *g_desktop_app_info_get_intents (GDesktopAppInfo *info); +GIO_AVAILABLE_IN_2_86 +gboolean g_desktop_app_info_has_intent (GDesktopAppInfo *info, + const char *intent); GIO_AVAILABLE_IN_2_30 gboolean g_desktop_app_info_get_nodisplay (GDesktopAppInfo *info); GIO_AVAILABLE_IN_2_30 -- GitLab From 5825aee3e171cedba64d05549289fe66a023761f Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Wed, 10 Sep 2025 18:27:54 +0200 Subject: [PATCH 04/12] gdesktopappinfo: Add support for the intentapps specification Add support for the intentapps-spec, inspired by the mimeapps-spec. The specification describes directories to look for `intentapps.list` files, the format for this file and relevant fields in the desktop entry. The new functions takes an interface and an optional scope. The interfaces correspond to the `Implements` key and the scope may be used by interfaces to define different default applications for a specific context. --- gio/gdesktopappinfo.c | 404 ++++++++++++++++++++++++++++++++++++++++++ gio/gdesktopappinfo.h | 6 + gio/tests/mimeapps.c | 119 ++++++++++++- 3 files changed, 526 insertions(+), 3 deletions(-) diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c index 6aa685d178..13a8d3c7ae 100644 --- a/gio/gdesktopappinfo.c +++ b/gio/gdesktopappinfo.c @@ -77,6 +77,7 @@ #define ADDED_ASSOCIATIONS_GROUP "Added Associations" #define REMOVED_ASSOCIATIONS_GROUP "Removed Associations" #define MIME_CACHE_GROUP "MIME Cache" +#define INTENT_CACHE_GROUP "Intent Cache" #define GENERIC_NAME_KEY "GenericName" #define FULL_NAME_KEY "X-GNOME-FullName" #define KEYWORDS_KEY "Keywords" @@ -151,6 +152,7 @@ typedef struct GFileMonitor *monitor; GHashTable *app_names; GHashTable *mime_tweaks; + GHashTable *intent_tweaks; GHashTable *memory_index; GHashTable *memory_implementations; } DesktopFileDir; @@ -812,6 +814,48 @@ get_apps_from_dir (GHashTable **apps, g_dir_close (dir); } +typedef struct +{ + char **defaults; /* (owned); list of priority ordered desktop file IDs */ + GHashTable *defaults_scopes; /* (owned) (element-type utf8 GStrv); maps scope to list of priority ordered desktop file IDs */ + gchar **cache; /* (owned); list of desktop file IDs */ + GHashTable *cache_scopes; /* (owned) (element-type utf8 GStrv); maps scope to list of desktop file IDs */ +} UnindexedIntentTweaks; + +static void +free_intent_tweaks (gpointer data) +{ + UnindexedIntentTweaks *apps = data; + + g_strfreev (apps->defaults); + g_hash_table_unref (apps->defaults_scopes); + g_strfreev (apps->cache); + g_hash_table_unref (apps->cache_scopes); + g_free (apps); +} + +static UnindexedIntentTweaks * +desktop_file_dir_unindexed_get_intent_tweaks (DesktopFileDir *dir, + const char *interface) +{ + UnindexedIntentTweaks *tweaks; + + tweaks = g_hash_table_lookup (dir->intent_tweaks, interface); + if (tweaks == NULL) + { + tweaks = g_new0 (UnindexedIntentTweaks, 1); + tweaks->defaults_scopes = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, + (GDestroyNotify) g_strfreev); + tweaks->cache_scopes = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, + (GDestroyNotify) g_strfreev); + g_hash_table_insert (dir->intent_tweaks, g_strdup (interface), tweaks); + } + + return tweaks; +} + typedef struct { gchar **additions; @@ -1043,12 +1087,164 @@ desktop_file_dir_unindexed_read_mimeapps_lists (DesktopFileDir *dir) g_free (filename); } +static void +desktop_file_dir_unindexed_read_interface_scopes (GHashTable *tweak_scopes, + const char *filename, + GKeyFile *key_file, + const char *interface) +{ + char **scopes; + + scopes = g_key_file_get_keys (key_file, interface, NULL, NULL); + for (size_t i = 0; scopes != NULL && scopes[i] != NULL; i++) + { + GStrv scope_desktop_file_ids; + GStrv desktop_file_ids; + char *owned_key; + + desktop_file_ids = g_key_file_get_string_list (key_file, + interface, + scopes[i], + NULL, NULL); + + if (desktop_file_ids == NULL || desktop_file_ids[0] == NULL) + continue; + + if (!g_hash_table_steal_extended (tweak_scopes, scopes[i], + (gpointer *) &owned_key, + (gpointer *) &scope_desktop_file_ids)) + { + scope_desktop_file_ids = NULL; + owned_key = g_strdup (scopes[i]); + } + + expand_strv (&scope_desktop_file_ids, g_steal_pointer (&desktop_file_ids), NULL); + + g_hash_table_insert (tweak_scopes, + g_steal_pointer (&owned_key), + g_steal_pointer (&scope_desktop_file_ids)); + } + g_clear_pointer (&scopes, g_strfreev); +} + +static void +desktop_file_dir_unindexed_read_intentapps_list (DesktopFileDir *dir, + const gchar *filename, + const gchar *added_group, + gboolean is_cache) +{ + GKeyFile *key_file; + char **interfaces; + + /* This implements the XDG intent-apps-spec: + * https://gitlab.freedesktop.org/xdg/xdg-specs/-/merge_requests/81 + * TODO: ^ replace with canonical link when merged + */ + + key_file = g_key_file_new (); + if (!g_key_file_load_from_file (key_file, filename, G_KEY_FILE_NONE, NULL)) + { + g_key_file_free (key_file); + return; + } + + interfaces = g_key_file_get_keys (key_file, added_group, NULL, NULL); + for (size_t i = 0; interfaces != NULL && interfaces[i] != NULL; i++) + { + UnindexedIntentTweaks *tweaks; + gchar ***list; + GHashTable *scopes; + char **desktop_file_ids; + + tweaks = desktop_file_dir_unindexed_get_intent_tweaks (dir, interfaces[i]); + + if (is_cache) + { + list = &tweaks->cache; + scopes = tweaks->cache_scopes; + } + else + { + list = &tweaks->defaults; + scopes = tweaks->defaults_scopes; + } + + desktop_file_ids = g_key_file_get_string_list (key_file, + added_group, + interfaces[i], + NULL, NULL); + + if (desktop_file_ids) + expand_strv (list, g_steal_pointer (&desktop_file_ids), NULL); + + desktop_file_dir_unindexed_read_interface_scopes (scopes, + filename, + key_file, + interfaces[i]); + } + g_clear_pointer (&interfaces, g_strfreev); + + g_key_file_free (key_file); +} + +static void +desktop_file_dir_unindexed_read_intentapps_lists (DesktopFileDir *dir) +{ + const char * const *desktops; + char *filename; + + dir->intent_tweaks = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, + free_intent_tweaks); + + /* We process in order of precedence, using a blocklisting approach to + * avoid recording later instructions that conflict with ones we found + * earlier. + * + * We first start with the XDG_CURRENT_DESKTOP files, in precedence + * order. + */ + desktops = get_lowercase_current_desktops (); + for (size_t i = 0; desktops[i]; i++) + { + char *basename; + + basename = g_strdup_printf ("%s-intentapps.list", desktops[i]); + filename = g_build_filename (dir->path, basename, NULL); + desktop_file_dir_unindexed_read_intentapps_list (dir, filename, + DEFAULT_APPLICATIONS_GROUP, + FALSE); + g_free (basename); + g_free (filename); + } + + /* Next, the non-desktop-specific intentapps.list */ + filename = g_build_filename (dir->path, "intentapps.list", NULL); + desktop_file_dir_unindexed_read_intentapps_list (dir, filename, + DEFAULT_APPLICATIONS_GROUP, + FALSE); + g_free (filename); + + if (dir->is_config) + return; + + /* Finally, the intent.cache, which is just a cached copy of what we + * would find in the Implements= lines of all of the desktop files. + */ + filename = g_strdup_printf ("%s/intent.cache", dir->path); + desktop_file_dir_unindexed_read_intentapps_list (dir, filename, + INTENT_CACHE_GROUP, + TRUE); + g_free (filename); +} + static void desktop_file_dir_unindexed_init (DesktopFileDir *dir) { if (!dir->is_config) get_apps_from_dir (&dir->app_names, dir->path, ""); + desktop_file_dir_unindexed_read_intentapps_lists (dir); desktop_file_dir_unindexed_read_mimeapps_lists (dir); } @@ -1388,6 +1584,59 @@ desktop_file_dir_unindexed_get_implementations (DesktopFileDir *dir, *results = g_list_prepend (*results, g_strdup (mie->app_name)); } +static void +unindexed_intent_lookup (DesktopFileDir *dir, + const char *interface, + const char *scope, + gboolean is_cache, + GPtrArray *results) +{ + UnindexedIntentTweaks *tweaks; + gchar **list; + + tweaks = g_hash_table_lookup (dir->intent_tweaks, interface); + if (!tweaks) + return; + + if (scope && is_cache) + list = g_hash_table_lookup (tweaks->cache_scopes, scope); + else if (scope && !is_cache) + list = g_hash_table_lookup (tweaks->defaults_scopes, scope); + else if (!scope && is_cache) + list = tweaks->cache; + else if (!scope && !is_cache) + list = tweaks->defaults; + else + g_assert_not_reached (); + + for (size_t i = 0; list && list[i]; i++) + { + const char *app_name = list[i]; + + if (!desktop_file_dir_app_name_is_masked (dir, app_name) && + !array_contains (results, app_name)) + g_ptr_array_add (results, (char *)app_name); + } +} + +static void +desktop_file_dir_unindexed_intent_lookup (DesktopFileDir *dir, + const char *interface, + const char *scope, + GPtrArray *results) +{ + unindexed_intent_lookup (dir, interface, scope, TRUE, results); +} + +static void +desktop_file_dir_unindexed_intent_lookup_default (DesktopFileDir *dir, + const char *interface, + const char *scope, + GPtrArray *results) +{ + unindexed_intent_lookup (dir, interface, scope, FALSE, results); +} + /* DesktopFileDir "API" {{{2 */ /*< internal > @@ -1474,6 +1723,7 @@ desktop_file_dir_reset (DesktopFileDir *dir) g_hash_table_unref (dir->memory_implementations); dir->memory_implementations = NULL; } + g_clear_pointer (&dir->intent_tweaks, g_hash_table_unref); dir->is_setup = FALSE; } @@ -1601,6 +1851,42 @@ desktop_file_dir_mime_lookup_default (DesktopFileDir *dir, desktop_file_dir_unindexed_mime_lookup_default (dir, mime_type, results); } +/*< internal > + * desktop_file_dir_intent_lookup: + * @dir: a #DesktopFileDir + * @interface: the intent interface name + * @scope: (nullable): optional scope + * @results: an array to store the results in + * + * Collects the "applications for a given intent from @dir in order of priority. + */ +static void +desktop_file_dir_intent_lookup (DesktopFileDir *dir, + const char *interface, + const char *scope, + GPtrArray *results) +{ + desktop_file_dir_unindexed_intent_lookup (dir, interface, scope, results); +} + +/*< internal > + * desktop_file_dir_intent_lookup_default: + * @dir: a #DesktopFileDir + * @interface: the intent interface name + * @scope: (nullable): optional scope + * @results: an array to store the results in + * + * Collects the "default" applications for a given intent from @dir in order of priority. + */ +static void +desktop_file_dir_intent_lookup_default (DesktopFileDir *dir, + const char *interface, + const char *scope, + GPtrArray *results) +{ + desktop_file_dir_unindexed_intent_lookup_default (dir, interface, scope, results); +} + /*< internal > * desktop_file_dir_search: * @dir: a #DesktopFileDir @@ -4742,6 +5028,42 @@ out: return info; } +/** + * g_desktop_app_info_get_default_for_intent: + * @interface: the intent interface to find a `GAppInfo` for + * @scope: (nullable): an optional scope + * + * Gets the default [iface@Gio.AppInfo] that implement @interface and, if @scope + * is not %NULL, also supports @scope. + * + * An application implements an interface if that interface is listed in + * the `Implements` line of the desktop file. + * + * An application implements a scope of an interface, if the scope is listed in + * the `Supports` line of the interface section of the desktop file. + * + * Application priority can be adjusted via `intentapps.list` files. + * + * Returns: (transfer full) (nullable): `GAppInfo` for given @intent and @scope, + * or %NULL if not found. + * + * Since: 2.86 + */ +GAppInfo * +g_desktop_app_info_get_default_for_intent (const char *interface, + const char *scope) +{ + GAppInfo *def = NULL; + GList *apps; + + apps = g_desktop_app_info_get_for_intent (interface, scope); + if (apps) + def = g_object_ref (apps->data); + + g_list_free_full (apps, g_object_unref); + return def; +} + GAppInfo * g_app_info_get_default_for_uri_scheme_impl (const char *uri_scheme) { @@ -4761,6 +5083,88 @@ g_app_info_get_default_for_uri_scheme_impl (const char *uri_scheme) /* "Get all" API {{{2 */ +/** + * g_desktop_app_info_get_for_intent: + * @interface: the intent interface to find `GAppInfo`s for + * @scope: (nullable): an optional scope + * + * Gets all [iface@Gio.AppInfo]s that implement @interface and, if @scope is not + * %NULL, also supports @scope. + * + * Applications earlier in the list have higher priority. + * + * An application implements an interface if that interface is listed in + * the `Implements` line of the desktop file. + * + * An application implements a scope of an interface, if the scope is listed in + * the `Supports` line of the interface section of the desktop file. + * + * Application priority can be adjusted via `intentapps.list` files. + * + * Returns: (element-type GDesktopAppInfo) (transfer full): a list of + * [class@GioUnix.DesktopAppInfo] objects. + * + * Since: 2.86 + **/ +GList * +g_desktop_app_info_get_for_intent (const gchar *interface, + const char *scope) +{ + GPtrArray *results = g_ptr_array_new (); + GList *infos = NULL; + + desktop_file_dirs_lock (); + + /* Collect all the default apps for the interface and scope */ + for (size_t i = 0; i < desktop_file_dirs->len; i++) + { + desktop_file_dir_intent_lookup_default (g_ptr_array_index (desktop_file_dirs, i), + interface, + scope, + results); + } + + /* Collect all the default apps for the interface and scope */ + for (size_t i = 0; i < desktop_file_dirs->len; i++) + { + desktop_file_dir_intent_lookup (g_ptr_array_index (desktop_file_dirs, i), + interface, + scope, + results); + } + + /* (If any), see if one of those apps is installed... */ + for (size_t i = 0; i < results->len; i++) + { + const char *desktop_id = g_ptr_array_index (results, i); + + for (size_t j = 0; j < desktop_file_dirs->len; j++) + { + GDesktopAppInfo *info; + + info = desktop_file_dir_get_app (g_ptr_array_index (desktop_file_dirs, j), + desktop_id); + if (!info) + continue; + + if (!g_desktop_app_info_has_intent (info, interface)) + { + g_debug ("%s: %s claimed to, but does not support intent %s", + G_STRFUNC, info->filename, interface); + g_clear_object (&info); + continue; + } + + infos = g_list_append (infos, G_APP_INFO (info)); + } + } + + desktop_file_dirs_unlock (); + g_ptr_array_unref (results); + + return infos; +} + /** * g_desktop_app_info_get_implementations: * @interface: the name of the interface diff --git a/gio/gdesktopappinfo.h b/gio/gdesktopappinfo.h index 45c0567b26..a9170529d1 100644 --- a/gio/gdesktopappinfo.h +++ b/gio/gdesktopappinfo.h @@ -199,6 +199,12 @@ gchar *** g_desktop_app_info_search (const gchar *search_string); GIO_AVAILABLE_IN_2_42 GList *g_desktop_app_info_get_implementations (const gchar *interface); +GIO_AVAILABLE_IN_2_86 +GList *g_desktop_app_info_get_for_intent (const gchar *interface, + const char *scope); +GIO_AVAILABLE_IN_2_86 +GAppInfo *g_desktop_app_info_get_default_for_intent (const char *interface, + const char *scope); G_END_DECLS diff --git a/gio/tests/mimeapps.c b/gio/tests/mimeapps.c index 4686241487..547af16f65 100644 --- a/gio/tests/mimeapps.c +++ b/gio/tests/mimeapps.c @@ -39,7 +39,8 @@ const gchar *myapp_data = "Version=1.0\n" "Type=Application\n" "Exec=true %f\n" - "Name=my app\n"; + "Name=my app\n" + "Implements=org.freedesktop.UriHandler;org.freedesktop.Terminal\n"; const gchar *myapp2_data = "[Desktop Entry]\n" @@ -47,7 +48,11 @@ const gchar *myapp2_data = "Version=1.0\n" "Type=Application\n" "Exec=sleep %f\n" - "Name=my app 2\n"; + "Name=my app 2\n" + "Implements=org.freedesktop.UriHandler;org.freedesktop.Terminal\n" + "[org.freedesktop.UriHandler]\n" + "Supports=example.org;x.org\n" + "SomeExtraData=foo;bar\n"; const gchar *myapp3_data = "[Desktop Entry]\n" @@ -56,7 +61,10 @@ const gchar *myapp3_data = "Type=Application\n" "Exec=sleep 1\n" "Name=my app 3\n" - "MimeType=image/png;"; + "Implements=org.freedesktop.UriHandler\n" + "MimeType=image/png;" + "[org.freedesktop.UriHandler]\n" + "Supports=example.org;\n"; const gchar *myapp4_data = "[Desktop Entry]\n" @@ -95,6 +103,20 @@ const gchar *mimecache_data = "image/bmp=myapp4.desktop;myapp5.desktop;\n" "image/png=myapp3.desktop;\n"; +const gchar *intentapps_data = + "[Default Applications]\n" + "org.freedesktop.UriHandler=myapp2.desktop;\n" + "[org.freedesktop.UriHandler]\n" + "example.org=myapp3.desktop;\n"; + +const gchar *intentcache_data = + "[Intent Cache]\n" + "org.freedesktop.Terminal=myapp.desktop;myapp2.desktop;\n" + "org.freedesktop.UriHandler=myapp.desktop;myapp2.desktop;myapp3.desktop;\n" + "[org.freedesktop.UriHandler]\n" + "x.org=myapp2.desktop;\n" + "example.org=myapp2.desktop;myapp3.desktop\n"; + typedef struct { gchar *mimeapps_list_home; /* (owned) */ @@ -211,6 +233,43 @@ setup (Fixture *fixture, g_assert_no_error (error); g_free (name); + if (!GPOINTER_TO_INT (test_data)) + { + name = g_build_filename (apphome, "intentapps.list", NULL); + } + else + { + GFile *file_a = NULL, *file_b = NULL, *appdir = NULL; + + /* + * Ensure intentapps.list can be reachable via a symlink chain. + */ + appdir = g_file_new_for_path (apphome); + file_a = g_file_get_child (appdir, "intentapps.list"); + g_file_make_symbolic_link (file_a, "intentapps.list.b", NULL, &error); + g_assert_no_error (error); + g_object_unref (file_a); + + file_b = g_file_get_child (appdir, "intentapps.list.b"); + g_file_make_symbolic_link (file_b, "intentapps.list.c", NULL, &error); + g_assert_no_error (error); + g_object_unref (file_b); + + g_object_unref (appdir); + name = g_build_filename (apphome, "intentapps.list.c", NULL); + } + + g_test_message ("creating '%s'", name); + g_file_set_contents (name, intentapps_data, -1, &error); + g_assert_no_error (error); + g_free (name); + + name = g_build_filename (apphome, "intent.cache", NULL); + g_test_message ("creating '%s'", name); + g_file_set_contents (name, intentcache_data, -1, &error); + g_assert_no_error (error); + g_free (name); + g_free (apphome); g_free (appdir_name); g_free (mimeapps); @@ -637,6 +696,54 @@ test_mime_ignore_nonexisting (Fixture *fixture, g_assert_null (appinfo); } +static void +test_intent_api (Fixture *fixture, + gconstpointer test_data) +{ + GAppInfo *appinfo; + GAppInfo *appinfo2; + GAppInfo *appinfo3; + GAppInfo *def; + GList *infos, *l; + + appinfo = (GAppInfo*)g_desktop_app_info_new ("myapp.desktop"); + appinfo2 = (GAppInfo*)g_desktop_app_info_new ("myapp2.desktop"); + appinfo3 = (GAppInfo*)g_desktop_app_info_new ("myapp3.desktop"); + + def = g_desktop_app_info_get_default_for_intent ("org.freedesktop.NonExisting", NULL); + g_assert_null (def); + + def = g_desktop_app_info_get_default_for_intent ("org.freedesktop.Terminal", NULL); + g_assert_nonnull (def); + g_object_unref (def); + + def = g_desktop_app_info_get_default_for_intent ("org.freedesktop.Terminal", "NonExistingScope"); + g_assert_null (def); + + def = g_desktop_app_info_get_default_for_intent ("org.freedesktop.UriHandler", NULL); + g_assert_nonnull (def); + g_assert_true (g_app_info_equal (def, appinfo2)); + g_object_unref (def); + + def = g_desktop_app_info_get_default_for_intent ("org.freedesktop.UriHandler", "example.org"); + g_assert_nonnull (def); + g_assert_true (g_app_info_equal (def, appinfo3)); + g_object_unref (def); + + infos = g_desktop_app_info_get_for_intent ("org.freedesktop.UriHandler", "example.org"); + l = infos; + g_assert_nonnull (l->data); + g_assert_true (g_app_info_equal (l->data, appinfo3)); + l = l->next; + g_assert_nonnull (l->data); + g_assert_true (g_app_info_equal (l->data, appinfo2)); + + g_list_free_full (infos, g_object_unref); + g_object_unref (appinfo); + g_object_unref (appinfo2); + g_object_unref (appinfo3); +} + static void test_all (Fixture *fixture, gconstpointer test_data) @@ -682,6 +789,12 @@ main (int argc, char *argv[]) g_test_add ("/appinfo/mime-symlinked/ignore-nonexisting", Fixture, GINT_TO_POINTER (TRUE), setup, test_mime_ignore_nonexisting, teardown); + g_test_add ("/appinfo/intent/api", Fixture, GINT_TO_POINTER (FALSE), setup, + test_intent_api, teardown); + + g_test_add ("/appinfo/intent-symlinked/api", Fixture, GINT_TO_POINTER (TRUE), setup, + test_intent_api, teardown); + g_test_add ("/appinfo/all", Fixture, NULL, setup, test_all, teardown); return g_test_run (); -- GitLab From b1e205d995ed5e1ad75962ed4c6fb4a3231b507e Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Wed, 10 Sep 2025 18:14:30 +0200 Subject: [PATCH 05/12] tests: Update desktop-files with an intent-aware update-desktop-database The result is that we get the `intent.cache` cache for fast lookups of intents. In the next commit, we will switch over the implementation for finding apps which implement interfaces to the new one which uses `intentapps.list` and `intent.cache` instead of iterating over all the desktop files. --- gio/tests/desktop-files/usr/applications/intent.cache | 3 +++ gio/tests/file.c | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 gio/tests/desktop-files/usr/applications/intent.cache diff --git a/gio/tests/desktop-files/usr/applications/intent.cache b/gio/tests/desktop-files/usr/applications/intent.cache new file mode 100644 index 0000000000..af98a9387a --- /dev/null +++ b/gio/tests/desktop-files/usr/applications/intent.cache @@ -0,0 +1,3 @@ +[Intent Cache] +org.freedesktop.ImageProvider=cheese.desktop; +org.gnome.Shell.SearchProvider2=eog.desktop;gnome-contacts.desktop;gnome-music.desktop; diff --git a/gio/tests/file.c b/gio/tests/file.c index 6df726ee24..96c9eddf56 100644 --- a/gio/tests/file.c +++ b/gio/tests/file.c @@ -2746,9 +2746,9 @@ test_measure (void) g_assert_true (ok); g_assert_no_error (error); - g_assert_cmpuint (num_bytes, ==, 96702); + g_assert_cmpuint (num_bytes, ==, 96851); g_assert_cmpuint (num_dirs, ==, 6); - g_assert_cmpuint (num_files, ==, 34); + g_assert_cmpuint (num_files, ==, 35); g_object_unref (file); g_free (path); @@ -2829,9 +2829,9 @@ test_measure_async (void) file = g_file_new_for_path (path); g_free (path); - data->expected_bytes = 96702; + data->expected_bytes = 96851; data->expected_dirs = 6; - data->expected_files = 34; + data->expected_files = 35; g_file_measure_disk_usage_async (file, G_FILE_MEASURE_APPARENT_SIZE, -- GitLab From b8d1e0961306c3b4ab41c563c64bf6e6cc6f66ce Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Wed, 10 Sep 2025 18:31:31 +0200 Subject: [PATCH 06/12] gdesktopappinfo: Deprecate get_implementations and switch to new impl The new impl uses `intentapps.list` and `intent.cache` to look up GDestkopAppInfos which implement an interface. --- gio/gdesktopappinfo.c | 70 +------------------------------------------ gio/gdesktopappinfo.h | 2 +- gio/tests/apps.c | 2 +- 3 files changed, 3 insertions(+), 71 deletions(-) diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c index 13a8d3c7ae..a23e36e467 100644 --- a/gio/gdesktopappinfo.c +++ b/gio/gdesktopappinfo.c @@ -154,7 +154,6 @@ typedef struct GHashTable *mime_tweaks; GHashTable *intent_tweaks; GHashTable *memory_index; - GHashTable *memory_implementations; } DesktopFileDir; static GPtrArray *desktop_file_dirs = NULL; @@ -1384,7 +1383,6 @@ desktop_file_dir_unindexed_setup_search (DesktopFileDir *dir) gpointer app, path; dir->memory_index = memory_index_new (); - dir->memory_implementations = memory_index_new (); /* Nothing to search? */ if (dir->app_names == NULL) @@ -1404,7 +1402,6 @@ desktop_file_dir_unindexed_setup_search (DesktopFileDir *dir) !g_key_file_get_boolean (key_file, "Desktop Entry", "Hidden", NULL)) { /* Index the interesting keys... */ - gchar **implements; gsize i; for (i = 0; i < G_N_ELEMENTS (desktop_key_match_category); i++) @@ -1442,15 +1439,6 @@ desktop_file_dir_unindexed_setup_search (DesktopFileDir *dir) g_free (raw); } - - /* Make note of the Implements= line */ - implements = g_key_file_get_string_list (key_file, - "Desktop Entry", - G_KEY_FILE_DESKTOP_KEY_IMPLEMENTS, - NULL, NULL); - for (i = 0; implements && implements[i]; i++) - memory_index_add_token (dir->memory_implementations, implements[i], i, 0, app); - g_strfreev (implements); } g_key_file_free (key_file); @@ -1570,20 +1558,6 @@ desktop_file_dir_unindexed_mime_lookup_default (DesktopFileDir *dir, } } -static void -desktop_file_dir_unindexed_get_implementations (DesktopFileDir *dir, - GList **results, - const gchar *interface) -{ - MemoryIndexEntry *mie; - - if (!dir->memory_index) - desktop_file_dir_unindexed_setup_search (dir); - - for (mie = g_hash_table_lookup (dir->memory_implementations, interface); mie; mie = mie->next) - *results = g_list_prepend (*results, g_strdup (mie->app_name)); -} - static void unindexed_intent_lookup (DesktopFileDir *dir, const char *interface, @@ -1718,11 +1692,6 @@ desktop_file_dir_reset (DesktopFileDir *dir) dir->mime_tweaks = NULL; } - if (dir->memory_implementations) - { - g_hash_table_unref (dir->memory_implementations); - dir->memory_implementations = NULL; - } g_clear_pointer (&dir->intent_tweaks, g_hash_table_unref); dir->is_setup = FALSE; @@ -1901,14 +1870,6 @@ desktop_file_dir_search (DesktopFileDir *dir, desktop_file_dir_unindexed_search (dir, search_token); } -static void -desktop_file_dir_get_implementations (DesktopFileDir *dir, - GList **results, - const gchar *interface) -{ - desktop_file_dir_unindexed_get_implementations (dir, results, interface); -} - /* Lock/unlock and global setup API {{{2 */ static void @@ -5182,36 +5143,7 @@ g_desktop_app_info_get_for_intent (const gchar *interface, GList * g_desktop_app_info_get_implementations (const gchar *interface) { - GList *result = NULL; - GList **ptr; - guint i; - - desktop_file_dirs_lock (); - - for (i = 0; i < desktop_file_dirs->len; i++) - desktop_file_dir_get_implementations (g_ptr_array_index (desktop_file_dirs, i), &result, interface); - - desktop_file_dirs_unlock (); - - ptr = &result; - while (*ptr) - { - gchar *name = (*ptr)->data; - GDesktopAppInfo *app; - - app = g_desktop_app_info_new (name); - g_free (name); - - if (app) - { - (*ptr)->data = app; - ptr = &(*ptr)->next; - } - else - *ptr = g_list_delete_link (*ptr, *ptr); - } - - return result; + return g_desktop_app_info_get_for_intent (interface, NULL); } /** diff --git a/gio/gdesktopappinfo.h b/gio/gdesktopappinfo.h index a9170529d1..78861a7546 100644 --- a/gio/gdesktopappinfo.h +++ b/gio/gdesktopappinfo.h @@ -197,7 +197,7 @@ gboolean g_desktop_app_info_launch_uris_as_manager_with_fds (GDesktopAppInfo GIO_AVAILABLE_IN_2_40 gchar *** g_desktop_app_info_search (const gchar *search_string); -GIO_AVAILABLE_IN_2_42 +GIO_DEPRECATED_IN_2_86_FOR (g_desktop_app_info_get_for_intent) GList *g_desktop_app_info_get_implementations (const gchar *interface); GIO_AVAILABLE_IN_2_86 GList *g_desktop_app_info_get_for_intent (const gchar *interface, diff --git a/gio/tests/apps.c b/gio/tests/apps.c index d656b6c926..191baf6593 100644 --- a/gio/tests/apps.c +++ b/gio/tests/apps.c @@ -101,7 +101,7 @@ main (int argc, char **argv) { GList *results; - results = g_desktop_app_info_get_implementations (argv[2]); + results = g_desktop_app_info_get_for_intent (argv[2], NULL); print_app_list (results); } else if (g_str_equal (argv[1], "show-info")) -- GitLab From b67b6bab3b868556bd96685a078bfdde73d6a746 Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Wed, 10 Sep 2025 19:09:10 +0200 Subject: [PATCH 07/12] gdesktopappinfo: Return void instead of bool from launch_uris_with_dbus The function doesn't fail, so let's not pretend otherwise. --- gio/gdesktopappinfo.c | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c index a23e36e467..6907c9b712 100644 --- a/gio/gdesktopappinfo.c +++ b/gio/gdesktopappinfo.c @@ -3605,19 +3605,19 @@ launch_uris_with_dbus (GDesktopAppInfo *info, g_variant_dict_clear (&dict); } -static gboolean -g_desktop_app_info_launch_uris_with_dbus (GDesktopAppInfo *info, - GDBusConnection *session_bus, - GList *uris, - GAppLaunchContext *launch_context, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) +static void +g_desktop_app_info_launch_uris_with_dbus (GDesktopAppInfo *info, + GDBusConnection *session_bus, + GList *uris, + GAppLaunchContext *launch_context, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) { GList *ruris = uris; char *app_id = NULL; - g_return_val_if_fail (info != NULL, FALSE); + g_return_if_fail (info != NULL); #ifdef G_OS_UNIX app_id = g_desktop_app_info_get_string (info, "X-Flatpak"); @@ -3636,8 +3636,6 @@ g_desktop_app_info_launch_uris_with_dbus (GDesktopAppInfo *info, g_list_free_full (ruris, g_free); g_free (app_id); - - return TRUE; } static gboolean -- GitLab From ca3a7b531936dbdf0bb0983f555ed897dd810f25 Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Wed, 10 Sep 2025 19:11:16 +0200 Subject: [PATCH 08/12] gdesktopappinfo: Clean up the code style in some places The next commit will touch those functions and this keeps the noise down in the actual changes there. --- gio/gdesktopappinfo.c | 52 ++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c index 6907c9b712..3ab47b3d24 100644 --- a/gio/gdesktopappinfo.c +++ b/gio/gdesktopappinfo.c @@ -3659,17 +3659,32 @@ g_desktop_app_info_launch_uris_internal (GAppInfo *appinfo, session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); if (session_bus && info->app_id) - /* This is non-blocking API. Similar to launching via fork()/exec() - * we don't wait around to see if the program crashed during startup. - * This is what startup-notification's job is... - */ - g_desktop_app_info_launch_uris_with_dbus (info, session_bus, uris, launch_context, - NULL, NULL, NULL); + { + /* This is non-blocking API. Similar to launching via fork()/exec() + * we don't wait around to see if the program crashed during startup. + * This is what startup-notification's job is... + */ + g_desktop_app_info_launch_uris_with_dbus (info, + session_bus, + uris, + launch_context, + NULL, NULL, NULL); + } else - success = g_desktop_app_info_launch_uris_with_spawn (info, session_bus, info->exec, uris, launch_context, - spawn_flags, user_setup, user_setup_data, - pid_callback, pid_callback_data, - stdin_fd, stdout_fd, stderr_fd, error); + { + success = g_desktop_app_info_launch_uris_with_spawn (info, + session_bus, + info->exec, + uris, + launch_context, + spawn_flags, + user_setup, + user_setup_data, + pid_callback, + pid_callback_data, + stdin_fd, stdout_fd, stderr_fd, + error); + } if (session_bus != NULL) { @@ -3768,8 +3783,11 @@ launch_uris_bus_get_cb (GObject *object, * from the g_desktop_app_info_launch_uris_with_dbus() function, still * uses blocking calls. */ - g_desktop_app_info_launch_uris_with_dbus (info, session_bus, - data->uris, data->context, + + g_desktop_app_info_launch_uris_with_dbus (info, + session_bus, + data->uris, + data->context, cancellable, launch_uris_with_dbus_cb, g_steal_pointer (&task)); @@ -3791,10 +3809,12 @@ launch_uris_bus_get_cb (GObject *object, g_object_unref (task); } else if (session_bus) - g_dbus_connection_flush (session_bus, - cancellable, - launch_uris_flush_cb, - g_steal_pointer (&task)); + { + g_dbus_connection_flush (session_bus, + cancellable, + launch_uris_flush_cb, + g_steal_pointer (&task)); + } else { g_task_return_boolean (task, TRUE); -- GitLab From 00e1af97098d527528c83e4e1a21e43a916c438e Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Wed, 10 Sep 2025 19:12:05 +0200 Subject: [PATCH 09/12] gdesktopappinfo: Use intent org.freedesktop.Terminal1 for terminal apps We now have everything in place to quickly look up the default app which implements the org.freedesktop.Terminal1 intent. We can then call the dbus interface on that app to launch desktop files which require a terminal ro run, instead of exec-ing a terminal from the hard-coded list we currently have. --- gio/gdesktopappinfo.c | 224 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 222 insertions(+), 2 deletions(-) diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c index 3ab47b3d24..c7ea73ec0c 100644 --- a/gio/gdesktopappinfo.c +++ b/gio/gdesktopappinfo.c @@ -82,6 +82,7 @@ #define FULL_NAME_KEY "X-GNOME-FullName" #define KEYWORDS_KEY "Keywords" #define STARTUP_WM_CLASS_KEY "StartupWMClass" +#define INTENT_FDO_TERMINAL1 "org.freedesktop.Terminal1" enum { PROP_0, @@ -3553,6 +3554,162 @@ launch_uris_with_dbus_signal_cb (GObject *object, launch_uris_with_dbus_data_free (data); } +static GDesktopAppInfo * +g_desktop_app_info_get_for_terminal1 (GDesktopAppInfo *info) +{ + GDesktopAppInfo *terminal_info; + + if (!info->terminal) + return NULL; + + terminal_info = + (GDesktopAppInfo *) g_desktop_app_info_get_default_for_intent (INTENT_FDO_TERMINAL1, + NULL); + + if (terminal_info == NULL || terminal_info->app_id == NULL) + { + g_clear_object (&terminal_info); + return NULL; + } + + return g_steal_pointer (&terminal_info); +} + +static gboolean +prepare_launch_uris_with_terminal1 (GDesktopAppInfo *terminal_info, + GDesktopAppInfo *info, + GList *uris, + GAppLaunchContext *launch_context, + GVariant **call_data_out, + char **startup_id_out, + GError **error) +{ + GList *remaining_uris; + GVariantBuilder builder; + + g_variant_builder_init_static (&builder, G_VARIANT_TYPE_TUPLE); + + g_variant_builder_open (&builder, G_VARIANT_TYPE_ARRAY); + + /* invocations */ + remaining_uris = uris; + do + { + GVariantBuilder invocation; + GVariantBuilder exec; + GVariantBuilder env; + char **argv; + int argc; + + if (!expand_application_parameters (info, + info->exec, + &remaining_uris, + &argc, + &argv, + error)) + { + g_variant_builder_clear (&builder); + return FALSE; + } + + g_variant_builder_init_static (&invocation, G_VARIANT_TYPE_VARDICT); + + g_variant_builder_init_static (&exec, G_VARIANT_TYPE_BYTESTRING_ARRAY); + for (int i = 0; i < argc; i++) + g_variant_builder_add_value (&exec, g_variant_new_bytestring (argv[i])); + g_variant_builder_add (&invocation, "{sv}", + "exec", g_variant_builder_end (&exec)); + + g_variant_builder_init_static (&env, G_VARIANT_TYPE_BYTESTRING_ARRAY); + g_variant_builder_add (&invocation, "{sv}", + "env", g_variant_builder_end (&env)); + + g_variant_builder_add (&invocation, "{sv}", + "working_directory", + g_variant_new_bytestring (info->path ? info->path : "")); + + g_variant_builder_add_value (&builder, g_variant_builder_end (&invocation)); + + g_clear_pointer (&argv, g_strfreev); + } + while (remaining_uris != NULL); + + g_variant_builder_close (&builder); + + /* desktop file path (desktop_entry) */ + g_variant_builder_add_value (&builder, g_variant_new_bytestring (info->filename)); + + /* options */ + g_variant_builder_open (&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_close (&builder); + + /* platform data */ + { + GVariant *platform_data; + + platform_data = g_desktop_app_info_make_platform_data (terminal_info, + uris, + launch_context); + + if (startup_id_out) + { + GVariantDict dict; + + g_variant_dict_init (&dict, platform_data); + g_variant_dict_lookup (&dict, "desktop-startup-id", "s", startup_id_out); + g_variant_dict_clear (&dict); + } + + g_variant_builder_add_value (&builder, platform_data); + } + + *call_data_out = g_variant_builder_end (&builder); + + return TRUE; +} + +static gboolean +launch_uris_with_terminal1 (GDesktopAppInfo *terminal_info, + GDesktopAppInfo *info, + GDBusConnection *session_bus, + GVariant *call_data, + char *startup_id, + GAppLaunchContext *launch_context, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + LaunchUrisWithDBusData *data; + char *object_path; + + if (launch_context) + emit_launch_started (launch_context, terminal_info, startup_id); + + data = g_new0 (LaunchUrisWithDBusData, 1); + data->info = g_object_ref (info); + data->callback = callback; + data->user_data = user_data; + data->launch_context = launch_context ? g_object_ref (launch_context) : NULL; + data->startup_id = g_steal_pointer (&startup_id); + + object_path = object_path_from_appid (terminal_info->app_id); + g_dbus_connection_call (session_bus, + terminal_info->app_id, + object_path, + INTENT_FDO_TERMINAL1, + "LaunchCommand", + g_steal_pointer (&call_data), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + cancellable, + launch_uris_with_dbus_signal_cb, + g_steal_pointer (&data)); + g_free (object_path); + + return TRUE; +} + static void launch_uris_with_dbus (GDesktopAppInfo *info, GDBusConnection *session_bus, @@ -3654,11 +3811,37 @@ g_desktop_app_info_launch_uris_internal (GAppInfo *appinfo, { GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); GDBusConnection *session_bus; + GDesktopAppInfo *terminal_info; gboolean success = TRUE; session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); - if (session_bus && info->app_id) + terminal_info = g_desktop_app_info_get_for_terminal1 (info); + + if (session_bus && terminal_info) + { + GVariant *call_data; + char *startup_id; + + success = prepare_launch_uris_with_terminal1 (terminal_info, + info, + uris, + launch_context, + &call_data, + &startup_id, + error); + if (success) + { + launch_uris_with_terminal1 (terminal_info, + info, + session_bus, + g_steal_pointer (&call_data), + g_steal_pointer (&startup_id), + launch_context, + NULL, NULL, NULL); + } + } + else if (session_bus && info->app_id) { /* This is non-blocking API. Similar to launching via fork()/exec() * we don't wait around to see if the program crashed during startup. @@ -3696,6 +3879,8 @@ g_desktop_app_info_launch_uris_internal (GAppInfo *appinfo, g_object_unref (session_bus); } + g_clear_object (&terminal_info); + return success; } @@ -3773,11 +3958,45 @@ launch_uris_bus_get_cb (GObject *object, LaunchUrisData *data = g_task_get_task_data (task); GCancellable *cancellable = g_task_get_cancellable (task); GDBusConnection *session_bus; + GDesktopAppInfo *terminal_info; GError *local_error = NULL; session_bus = g_bus_get_finish (result, NULL); - if (session_bus && info->app_id) + terminal_info = g_desktop_app_info_get_for_terminal1 (info); + + if (session_bus && terminal_info) + { + gboolean success; + GVariant *call_data; + char *startup_id; + + success = prepare_launch_uris_with_terminal1 (terminal_info, + info, + data->uris, + data->context, + &call_data, + &startup_id, + &local_error); + if (!success) + { + g_task_return_error (task, g_steal_pointer (&local_error)); + g_object_unref (task); + } + else + { + launch_uris_with_terminal1 (terminal_info, + info, + session_bus, + g_steal_pointer (&call_data), + g_steal_pointer (&startup_id), + data->context, + cancellable, + launch_uris_with_dbus_cb, + g_steal_pointer (&task)); + } + } + else if (session_bus && info->app_id) { /* FIXME: The g_document_portal_add_documents() function, which is called * from the g_desktop_app_info_launch_uris_with_dbus() function, still @@ -3822,6 +4041,7 @@ launch_uris_bus_get_cb (GObject *object, } } + g_clear_object (&terminal_info); g_clear_object (&session_bus); } -- GitLab From 0af4d3696201e17150fee9da21ce03d1ca0096a8 Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Sat, 13 Sep 2025 18:31:04 +0200 Subject: [PATCH 10/12] gapinfo: Add function which finds app based on http(s) URIs For URIs with http or https schemes, we can search for apps which have a handler for a specific host and match the path according to a simple pattern. The apps are tried in the order given in intent-apps. This will be used to support deeplinking. --- gio/gappinfo.c | 98 +++++++++++++++++++++++++++++++++++++++++++ gio/gappinfo.h | 13 ++++++ gio/gappinfoprivate.h | 1 + gio/gdesktopappinfo.c | 88 ++++++++++++++++++++++++++++++++++++++ gio/gosxappinfo.m | 6 +++ gio/gwin32appinfo.c | 6 +++ 6 files changed, 212 insertions(+) diff --git a/gio/gappinfo.c b/gio/gappinfo.c index 65a5735df1..6a800cdeec 100644 --- a/gio/gappinfo.c +++ b/gio/gappinfo.c @@ -1149,6 +1149,104 @@ g_app_info_get_default_for_type_finish (GAsyncResult *result, return g_task_propagate_pointer (G_TASK (result), error); } +static void +get_default_for_http_thread (GTask *task, + gpointer object, + gpointer task_data, + GCancellable *cancellable) +{ + GUri *uri = task_data; + GAppInfo *info; + + info = g_app_info_get_default_for_uri_http (uri); + + if (!info) + { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + _("Failed to find default application for " + "http URI")); + return; + } + + g_task_return_pointer (task, g_steal_pointer (&info), g_object_unref); +} + +/** + * g_app_info_get_default_for_uri_http_async: + * @uri: a [class@Gio.Uri]. + * @cancellable: (nullable): a [class@Gio.Cancellable] + * @callback: (scope async) (nullable): a [type@Gio.AsyncReadyCallback] to call + * when the request is done + * @user_data: (nullable): data to pass to @callback + * + * Asynchronously gets the default application for handling the specific + * http URI. + * + * Since: 2.86 + */ +void +g_app_info_get_default_for_uri_http_async (GUri *uri, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_return_if_fail (uri != NULL); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (NULL, cancellable, callback, user_data); + g_task_set_source_tag (task, g_app_info_get_default_for_uri_http_async); + g_task_set_task_data (task, g_uri_ref (uri), (GDestroyNotify) g_uri_unref); + g_task_set_check_cancellable (task, TRUE); + g_task_run_in_thread (task, get_default_for_http_thread); + g_object_unref (task); +} + +/** + * g_app_info_get_default_for_uri_http_finish: + * @result: the async result + * + * Finishes a default [iface@Gio.AppInfo] lookup started by + * [func@Gio.AppInfo.get_default_for_uri_http_async]. + * + * If no [iface@Gio.AppInfo] is found, then @error will be set to + * [error@Gio.IOErrorEnum.NOT_FOUND]. + * + * Returns: (transfer full): [iface@Gio.AppInfo] for given @uri or + * `NULL` on error. + * + * Since: 2.86 + */ +GAppInfo * +g_app_info_get_default_for_uri_http_finish (GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, NULL), NULL); + g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == + g_app_info_get_default_for_uri_http_async, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +/** + * g_app_info_get_default_for_uri_http: + * @uri: a [class@Gio.Uri]. + * + * Gets the default application for handling the specific http URI. + * + * Returns: (transfer full) (nullable): [iface@Gio.AppInfo] for given + * @uri or `NULL` on error. + */ +GAppInfo * +g_app_info_get_default_for_uri_http (GUri *uri) +{ + g_return_val_if_fail (uri != NULL, NULL); + + return g_app_info_get_default_for_uri_http_impl (uri); +} + /** * g_app_info_launch_default_for_uri: * @uri: the uri to show diff --git a/gio/gappinfo.h b/gio/gappinfo.h index f42ebdddf7..893d93b75e 100644 --- a/gio/gappinfo.h +++ b/gio/gappinfo.h @@ -290,6 +290,19 @@ GIO_AVAILABLE_IN_2_50 gboolean g_app_info_launch_default_for_uri_finish (GAsyncResult *result, GError **error); +GIO_AVAILABLE_IN_2_86 +GAppInfo *g_app_info_get_default_for_uri_http (GUri *uri); + +GIO_AVAILABLE_IN_2_86 +void g_app_info_get_default_for_uri_http_async (GUri *uri, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +GIO_AVAILABLE_IN_2_86 +GAppInfo *g_app_info_get_default_for_uri_http_finish (GAsyncResult *result, + GError **error); + /** * GAppLaunchContext: diff --git a/gio/gappinfoprivate.h b/gio/gappinfoprivate.h index df6a2d3bfb..82f28de159 100644 --- a/gio/gappinfoprivate.h +++ b/gio/gappinfoprivate.h @@ -37,5 +37,6 @@ GAppInfo *g_app_info_get_default_for_type_impl (const char *content_type, gboolean must_support_uris); GAppInfo *g_app_info_get_default_for_uri_scheme_impl (const char *uri_scheme); GList *g_app_info_get_all_impl (void); +GAppInfo *g_app_info_get_default_for_uri_http_impl (GUri *uri); #endif /* __G_APP_INFO_PRIVATE_H__ */ diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c index c7ea73ec0c..e0cd3eb8c7 100644 --- a/gio/gdesktopappinfo.c +++ b/gio/gdesktopappinfo.c @@ -83,6 +83,7 @@ #define KEYWORDS_KEY "Keywords" #define STARTUP_WM_CLASS_KEY "StartupWMClass" #define INTENT_FDO_TERMINAL1 "org.freedesktop.Terminal1" +#define INTENT_FDO_DEEPLINK1 "org.freedesktop.handler.Deeplink1" enum { PROP_0, @@ -5280,6 +5281,93 @@ g_app_info_get_default_for_uri_scheme_impl (const char *uri_scheme) return app_info; } +static char * +get_match_path_for_uri (GUri *uri) +{ + GString *path_ref = NULL; + const char *path = NULL; + const char *query = NULL; + const char *fragment = NULL; + + /* Compose an absolute-ref ("/" path [ "?" query ] [ "#" fragment ]) */ + + path = g_uri_get_path (uri); + if (path != NULL && *path != '\0') + path_ref = g_string_new (path); + else + path_ref = g_string_new ("/"); + + query = g_uri_get_query (uri); + if (query != NULL && *query != '\0') + g_string_append_printf (path_ref, "?%s", query); + + fragment = g_uri_get_fragment (uri); + if (fragment != NULL && *fragment != '\0') + g_string_append_printf (path_ref, "#%s", fragment); + + return g_string_free (path_ref, FALSE); +} + +GAppInfo * +g_app_info_get_default_for_uri_http_impl (GUri *uri) +{ + GAppInfo *app_info = NULL; + GList *apps; + const gchar *host; + const gchar *scheme; + gchar *uri_path = NULL; + + scheme = g_uri_get_scheme (uri); + if (!g_str_equal (scheme, "http") && !g_str_equal (scheme, "https")) + { + g_debug ("Could not get a default for an http URI because " + "the scheme is not http or https"); + return NULL; + } + + host = g_uri_get_host (uri); + if (host == NULL) + { + g_debug ("Could not get a default for an http URI because " + "the URI contains no host"); + return NULL; + } + + apps = g_desktop_app_info_get_for_intent (INTENT_FDO_DEEPLINK1, host); + + if (apps) + uri_path = get_match_path_for_uri (uri); + + for (GList *l = apps; l != NULL; l = l->next) + { + GDesktopAppInfo *candidate = l->data; + GStrv patterns; + + patterns = g_key_file_get_string_list (candidate->keyfile, + INTENT_FDO_DEEPLINK1, + host, + NULL, NULL); + + for (gsize i = 0; patterns[i]; i++) + { + if (g_pattern_match_simple (patterns[i], uri_path)) + { + app_info = g_object_ref (G_APP_INFO (candidate)); + break; + } + } + g_clear_pointer (&patterns, g_strfreev); + + if (app_info) + break; + } + + g_list_free_full (apps, g_object_unref); + g_clear_pointer (&uri_path, g_free); + + return g_steal_pointer (&app_info); +} + /* "Get all" API {{{2 */ /** diff --git a/gio/gosxappinfo.m b/gio/gosxappinfo.m index 917d72bdde..307f6d8e1c 100644 --- a/gio/gosxappinfo.m +++ b/gio/gosxappinfo.m @@ -820,3 +820,9 @@ void g_app_info_reset_type_associations_impl (const char *content_type) { } + +GAppInfo * +g_app_info_get_default_for_uri_http_impl (GUri *uri) +{ + return NULL; +} diff --git a/gio/gwin32appinfo.c b/gio/gwin32appinfo.c index 39adfcaa30..a981c656fa 100644 --- a/gio/gwin32appinfo.c +++ b/gio/gwin32appinfo.c @@ -6060,3 +6060,9 @@ g_app_info_reset_type_associations_impl (const char *content_type) { /* nothing to do */ } + +GAppInfo * +g_app_info_get_default_for_uri_http_impl (GUri *uri) +{ + return NULL; +} -- GitLab From 75d980e1b92eb0b12cb362e0f8e83896292dadaa Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Sat, 13 Sep 2025 18:36:22 +0200 Subject: [PATCH 11/12] gappinfo: Pass GUri around instead of a string of the scheme We have to parse the URI anyway, so keeping the GUri around instead of a string makes no difference, but we need the host in the following commits. --- gio/gappinfo.c | 49 ++++++++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/gio/gappinfo.c b/gio/gappinfo.c index 6a800cdeec..6f9b0ef3e7 100644 --- a/gio/gappinfo.c +++ b/gio/gappinfo.c @@ -1267,18 +1267,23 @@ g_app_info_launch_default_for_uri (const char *uri, GAppLaunchContext *launch_context, GError **error) { - char *uri_scheme; + GUri *parsed; + const char *scheme = NULL; GAppInfo *app_info = NULL; gboolean res = FALSE; + g_return_val_if_fail (uri != NULL, FALSE); + + parsed = g_uri_parse (uri, G_URI_FLAGS_NONE, NULL); + if (parsed) + scheme = g_uri_get_scheme (parsed); + /* g_file_query_default_handler() calls * g_app_info_get_default_for_uri_scheme() too, but we have to do it * here anyway in case GFile can't parse @uri correctly. */ - uri_scheme = g_uri_parse_scheme (uri); - if (uri_scheme && uri_scheme[0] != '\0') - app_info = g_app_info_get_default_for_uri_scheme (uri_scheme); - g_free (uri_scheme); + if (scheme && scheme[0] != '\0') + app_info = g_app_info_get_default_for_uri_scheme (scheme); if (!app_info) { @@ -1333,12 +1338,15 @@ g_app_info_launch_default_for_uri (const char *uri, } #endif + g_clear_pointer (&parsed, g_uri_unref); + return res; } typedef struct { gchar *uri; + GUri *parsed; GAppLaunchContext *context; } LaunchDefaultForUriData; @@ -1347,6 +1355,7 @@ launch_default_for_uri_data_free (LaunchDefaultForUriData *data) { g_free (data->uri); g_clear_object (&data->context); + g_clear_pointer (&data->parsed, g_uri_unref); g_free (data); } @@ -1492,9 +1501,9 @@ launch_default_app_for_default_handler (GTask *task) } static void -launch_default_app_for_uri_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) +launch_default_app_for_uri_scheme_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) { GTask *task = G_TASK (user_data); GAppInfo *app_info; @@ -1541,29 +1550,31 @@ g_app_info_launch_default_for_uri_async (const char *uri, gpointer user_data) { GTask *task; - char *uri_scheme; + GUri *parsed; + const char *scheme = NULL; LaunchDefaultForUriData *data; g_return_if_fail (uri != NULL); + parsed = g_uri_parse (uri, G_URI_FLAGS_NONE, NULL); + if (parsed) + scheme = g_uri_get_scheme (parsed); + task = g_task_new (NULL, cancellable, callback, user_data); g_task_set_source_tag (task, g_app_info_launch_default_for_uri_async); data = g_new (LaunchDefaultForUriData, 1); data->uri = g_strdup (uri); + data->parsed = parsed ? g_uri_ref (parsed) : NULL; data->context = (context != NULL) ? g_object_ref (context) : NULL; - g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) launch_default_for_uri_data_free); + g_task_set_task_data (task, g_steal_pointer (&data), + (GDestroyNotify) launch_default_for_uri_data_free); - /* g_file_query_default_handler_async() calls - * g_app_info_get_default_for_uri_scheme() too, but we have to do it - * here anyway in case GFile can't parse @uri correctly. - */ - uri_scheme = g_uri_parse_scheme (uri); - if (uri_scheme && uri_scheme[0] != '\0') + if (scheme && scheme[0] != '\0') { - g_app_info_get_default_for_uri_scheme_async (uri_scheme, + g_app_info_get_default_for_uri_scheme_async (scheme, cancellable, - launch_default_app_for_uri_cb, + launch_default_app_for_uri_scheme_cb, g_steal_pointer (&task)); } else @@ -1571,7 +1582,7 @@ g_app_info_launch_default_for_uri_async (const char *uri, launch_default_app_for_default_handler (g_steal_pointer (&task)); } - g_free (uri_scheme); + g_clear_pointer (&parsed, g_uri_unref); } /** -- GitLab From 508fd88b5f76cd429c09a40fab5f263eff8c39cc Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Sat, 13 Sep 2025 18:37:11 +0200 Subject: [PATCH 12/12] gappinfo: Implement deeplinking in launch_default_for_uri If the scheme is http or https, we prefer the deeplink handler for URIs, if there is a matching one. Otherwise we fall back to the previous behavior. --- gio/gappinfo.c | 59 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/gio/gappinfo.c b/gio/gappinfo.c index 6f9b0ef3e7..c8bcfa8800 100644 --- a/gio/gappinfo.c +++ b/gio/gappinfo.c @@ -1278,11 +1278,14 @@ g_app_info_launch_default_for_uri (const char *uri, if (parsed) scheme = g_uri_get_scheme (parsed); + if (g_strcmp0 (scheme, "http") == 0 || g_strcmp0 (scheme, "https") == 0) + app_info = g_app_info_get_default_for_uri_http (parsed); + /* g_file_query_default_handler() calls * g_app_info_get_default_for_uri_scheme() too, but we have to do it * here anyway in case GFile can't parse @uri correctly. */ - if (scheme && scheme[0] != '\0') + if (!app_info && scheme && scheme[0] != '\0') app_info = g_app_info_get_default_for_uri_scheme (scheme); if (!app_info) @@ -1521,6 +1524,38 @@ launch_default_app_for_uri_scheme_cb (GObject *object, } } +static void +launch_default_app_for_uri_http_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GTask *task = G_TASK (user_data); + GAppInfo *app_info; + + app_info = g_app_info_get_default_for_uri_http_finish (result, NULL); + + if (!app_info) + { + LaunchDefaultForUriData *data; + const char *scheme; + GCancellable *cancellable; + + data = g_task_get_task_data (task); + scheme = g_uri_get_scheme (data->parsed); + cancellable = g_task_get_cancellable (task); + + g_app_info_get_default_for_uri_scheme_async (scheme, + cancellable, + launch_default_app_for_uri_scheme_cb, + g_steal_pointer (&task)); + } + else + { + launch_default_for_uri_launch_uris (g_steal_pointer (&task), + g_steal_pointer (&app_info)); + } +} + /** * g_app_info_launch_default_for_uri_async: * @uri: the uri to show @@ -1572,10 +1607,24 @@ g_app_info_launch_default_for_uri_async (const char *uri, if (scheme && scheme[0] != '\0') { - g_app_info_get_default_for_uri_scheme_async (scheme, - cancellable, - launch_default_app_for_uri_scheme_cb, - g_steal_pointer (&task)); + if (g_strcmp0 (scheme, "http") == 0 || g_strcmp0 (scheme, "https") == 0) + { + g_app_info_get_default_for_uri_http_async (parsed, + cancellable, + launch_default_app_for_uri_http_cb, + g_steal_pointer (&task)); + } + else + { + /* g_file_query_default_handler_async() calls + * g_app_info_get_default_for_uri_scheme() too, but we have to do it + * here anyway in case GFile can't parse @uri correctly. + */ + g_app_info_get_default_for_uri_scheme_async (scheme, + cancellable, + launch_default_app_for_uri_scheme_cb, + g_steal_pointer (&task)); + } } else { -- GitLab