diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c index e020230986f40dd609cc3883ea6ae8932c4442f2..711ef3c29a6ef735643a93291b759fb2c9f35dc1 100644 --- a/gio/gdesktopappinfo.c +++ b/gio/gdesktopappinfo.c @@ -77,10 +77,12 @@ #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" #define STARTUP_WM_CLASS_KEY "StartupWMClass" +#define INTENT_FDO_TERMINAL1 "org.freedesktop.Terminal1" enum { PROP_0, @@ -119,6 +121,7 @@ struct _GDesktopAppInfo char *startup_wm_class; char **mime_types; char **actions; + char **intents; guint nodisplay : 1; guint hidden : 1; @@ -150,8 +153,8 @@ typedef struct GFileMonitor *monitor; GHashTable *app_names; GHashTable *mime_tweaks; + GHashTable *intent_tweaks; GHashTable *memory_index; - GHashTable *memory_implementations; } DesktopFileDir; static GPtrArray *desktop_file_dirs = NULL; @@ -812,6 +815,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 +1088,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); } @@ -1188,7 +1385,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) @@ -1208,7 +1404,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++) @@ -1246,12 +1441,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", "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); @@ -1350,9 +1539,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; @@ -1372,17 +1561,56 @@ desktop_file_dir_unindexed_default_lookup (DesktopFileDir *dir, } static void -desktop_file_dir_unindexed_get_implementations (DesktopFileDir *dir, - GList **results, - const gchar *interface) +unindexed_intent_lookup (DesktopFileDir *dir, + const char *interface, + const char *scope, + gboolean is_cache, + GPtrArray *results) { - MemoryIndexEntry *mie; + UnindexedIntentTweaks *tweaks; + gchar **list; - if (!dir->memory_index) - desktop_file_dir_unindexed_setup_search (dir); + 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 (mie = g_hash_table_lookup (dir->memory_implementations, interface); mie; mie = mie->next) - *results = g_list_prepend (*results, g_strdup (mie->app_name)); + 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 */ @@ -1466,11 +1694,7 @@ 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; } @@ -1583,7 +1807,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 @@ -1591,11 +1815,47 @@ 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_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_default_lookup (dir, mime_type, 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 > @@ -1612,14 +1872,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 @@ -1741,6 +1993,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); } @@ -2027,6 +2280,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) @@ -2250,6 +2504,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); } @@ -2400,6 +2655,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] @@ -3296,6 +3581,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, @@ -3348,19 +3789,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"); @@ -3392,8 +3833,6 @@ g_desktop_app_info_launch_uris_with_dbus (GDesktopAppInfo *info, if (ruris != uris) g_list_free_full (ruris, g_free); - - return TRUE; } static gboolean @@ -3412,22 +3851,63 @@ 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) - /* 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); + terminal_info = g_desktop_app_info_get_for_terminal1 (info); + + if (session_bus && terminal_info) + { + GVariant *call_data = NULL; + char *startup_id = NULL; + + 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. + * 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) { @@ -3439,6 +3919,8 @@ g_desktop_app_info_launch_uris_internal (GAppInfo *appinfo, g_object_unref (session_bus); } + g_clear_object (&terminal_info); + return success; } @@ -3516,18 +3998,55 @@ 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 = NULL; + char *startup_id = NULL; + + 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 * 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)); @@ -3549,10 +4068,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); @@ -3560,6 +4081,7 @@ launch_uris_bus_get_cb (GObject *object, } } + g_clear_object (&terminal_info); g_clear_object (&session_bus); } @@ -4704,7 +5226,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++) @@ -4745,6 +5267,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) { @@ -4765,52 +5323,105 @@ g_app_info_get_default_for_uri_scheme_impl (const char *uri_scheme) /* "Get all" API {{{2 */ /** - * g_desktop_app_info_get_implementations: - * @interface: the name of the interface + * g_desktop_app_info_get_for_intent: + * @interface: the intent interface to find `GAppInfo`s for + * @scope: (nullable): an optional scope * - * Gets all applications that implement @interface. + * 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 of the application. + * 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.42 + * Since: 2.86 **/ GList * -g_desktop_app_info_get_implementations (const gchar *interface) +g_desktop_app_info_get_for_intent (const gchar *interface, + const char *scope) { - GList *result = NULL; - GList **ptr; - guint i; + GPtrArray *results = g_ptr_array_new (); + GList *infos = NULL; 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 (); + /* Collect all the default apps for the interface and scope */ + for (guint 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); + } - ptr = &result; - while (*ptr) + /* Collect all the default apps for the interface and scope */ + for (guint i = 0; i < desktop_file_dirs->len; i++) { - gchar *name = (*ptr)->data; - GDesktopAppInfo *app; + desktop_file_dir_intent_lookup (g_ptr_array_index (desktop_file_dirs, i), + interface, + scope, + results); + } - app = g_desktop_app_info_new (name); - g_free (name); + /* (If any), see if one of those apps is installed... */ + for (guint i = 0; i < results->len; i++) + { + const char *desktop_id = g_ptr_array_index (results, i); - if (app) + for (guint j = 0; j < desktop_file_dirs->len; j++) { - (*ptr)->data = app; - ptr = &(*ptr)->next; + 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)); } - else - *ptr = g_list_delete_link (*ptr, *ptr); } - return result; + desktop_file_dirs_unlock (); + g_ptr_array_unref (results); + + return infos; +} + +/** + * g_desktop_app_info_get_implementations: + * @interface: the name of the interface + * + * Gets all applications that implement @interface. + * + * An application implements an interface if that interface is listed in + * the `Implements` line of the desktop file of the application. + * + * Returns: (element-type GDesktopAppInfo) (transfer full): a list of + * [class@GioUnix.DesktopAppInfo] objects. + * + * Since: 2.42 + **/ +GList * +g_desktop_app_info_get_implementations (const gchar *interface) +{ + return g_desktop_app_info_get_for_intent (interface, NULL); } /** diff --git a/gio/gdesktopappinfo.h b/gio/gdesktopappinfo.h index 7981a026f7e2dfdd1dec38106d421ebffc155fe2..2a7dd06592ef9faf98eb6eae9a052a6228eafe29 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_88 +const char * const *g_desktop_app_info_get_intents (GDesktopAppInfo *info); +GIO_AVAILABLE_IN_2_88 +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 @@ -192,8 +197,14 @@ 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, + 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/apps.c b/gio/tests/apps.c index d656b6c92640c22bc35f9ab3f7aaf48e52891ffb..191baf65933025cbdd071e58678aed965b7b944e 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")) 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 0000000000000000000000000000000000000000..af98a9387a796d017c39e2f5de8175c5ea0b6d61 --- /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 57d71f7a05b4f70476a9a8e85af1026eb4dea495..cd620cc29cab34f5098e8f4b9d57cce9e9dae4c7 100644 --- a/gio/tests/file.c +++ b/gio/tests/file.c @@ -2860,9 +2860,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); @@ -2943,9 +2943,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, diff --git a/gio/tests/mimeapps.c b/gio/tests/mimeapps.c index 46862414871ab8bc5b00f30c4b3143089cd8a94a..547af16f654b3f145a4e23fe37924a93ad448c5f 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 (); diff --git a/glib/gkeyfile.c b/glib/gkeyfile.c index 7991c964d27bfcc73b65e919c1e292a80b60592a..e46142c064717ae356f7c4978122150955c5d3ab 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.88 + */ + /** * G_KEY_FILE_DESKTOP_TYPE_APPLICATION: * diff --git a/glib/gkeyfile.h b/glib/gkeyfile.h index 9d026d681d167ffbb45ce1fd1be8c34c2059eea2..e53688e14e0fe48d43a39cb693b7e46602b28eef 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"