+ dir = g_dir_open (dirname, 0, NULL);
+
+ if (dir == NULL)
+ return;
+
+ while ((basename = g_dir_read_name (dir)) != NULL)
+ {
+ gchar *filename;
+
+ filename = g_build_filename (dirname, basename, NULL);
+
+ if (g_str_has_suffix (basename, ".desktop"))
+ {
+ gchar *app_name;
+
+ app_name = g_strconcat (prefix, basename, NULL);
+
+ if (*apps == NULL)
+ *apps = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+ g_hash_table_insert (*apps, app_name, g_strdup (filename));
+ }
+ else if (g_file_test (filename, G_FILE_TEST_IS_DIR))
+ {
+ gchar *subprefix;
+
+ subprefix = g_strconcat (prefix, basename, "-", NULL);
+ get_apps_from_dir (apps, filename, subprefix);
+ g_free (subprefix);
+ }
+
+ g_free (filename);
+ }
+
+ g_dir_close (dir);
+}
+
+typedef struct
+{
+ gchar **additions;
+ gchar **removals;
+ gchar **defaults;
+} UnindexedMimeTweaks;
+
+static void
+free_mime_tweaks (gpointer data)
+{
+ UnindexedMimeTweaks *tweaks = data;
+
+ g_strfreev (tweaks->additions);
+ g_strfreev (tweaks->removals);
+ g_strfreev (tweaks->defaults);
+
+ g_slice_free (UnindexedMimeTweaks, tweaks);
+}
+
+static UnindexedMimeTweaks *
+desktop_file_dir_unindexed_get_tweaks (DesktopFileDir *dir,
+ const gchar *mime_type)
+{
+ UnindexedMimeTweaks *tweaks;
+ gchar *unaliased_type;
+
+ unaliased_type = _g_unix_content_type_unalias (mime_type);
+ tweaks = g_hash_table_lookup (dir->mime_tweaks, mime_type);
+
+ if (tweaks == NULL)
+ {
+ tweaks = g_slice_new0 (UnindexedMimeTweaks);
+ g_hash_table_insert (dir->mime_tweaks, unaliased_type, tweaks);
+ }
+ else
+ g_free (unaliased_type);
+
+ return tweaks;
+}
+
+/* consumes 'to_add' */
+static void
+expand_strv (gchar ***strv_ptr,
+ gchar **to_add,
+ gchar * const *blacklist)
+{
+ guint strv_len, add_len;
+ gchar **strv;
+ guint i, j;
+
+ if (!*strv_ptr)
+ {
+ *strv_ptr = to_add;
+ return;
+ }
+
+ strv = *strv_ptr;
+ strv_len = g_strv_length (strv);
+ add_len = g_strv_length (to_add);
+ strv = g_renew (gchar *, strv, strv_len + add_len + 1);
+
+ for (i = 0; to_add[i]; i++)
+ {
+ /* Don't add blacklisted strings */
+ if (blacklist)
+ for (j = 0; blacklist[j]; j++)
+ if (g_str_equal (to_add[i], blacklist[j]))
+ goto no_add;
+
+ /* Don't add duplicates already in the list */
+ for (j = 0; strv[j]; j++)
+ if (g_str_equal (to_add[i], strv[j]))
+ goto no_add;
+
+ strv[strv_len++] = to_add[i];
+ continue;
+
+no_add:
+ g_free (to_add[i]);
+ }
+
+ strv[strv_len] = NULL;
+ *strv_ptr = strv;
+
+ g_free (to_add);
+}
+
+static void
+desktop_file_dir_unindexed_read_mimeapps_list (DesktopFileDir *dir,
+ const gchar *filename,
+ const gchar *added_group,
+ gboolean tweaks_permitted)
+{
+ const gchar default_group[] = "Default Applications";
+ const gchar removed_group[] = "Removed Assocations";
+ UnindexedMimeTweaks *tweaks;
+ char **desktop_file_ids;
+ GKeyFile *key_file;
+ gchar **mime_types;
+ int i;
+
+ 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;
+ }
+
+ mime_types = g_key_file_get_keys (key_file, added_group, NULL, NULL);
+
+ if G_UNLIKELY (mime_types != NULL && !tweaks_permitted)
+ {
+ g_warning ("%s contains a [%s] group, but it is not permitted here. Only the non-desktop-specific "
+ "mimeapps.list file may add or remove associations.", filename, added_group);
+ g_strfreev (mime_types);
+ mime_types = NULL;
+ }
+
+ if (mime_types != NULL)
+ {
+ for (i = 0; mime_types[i] != NULL; i++)
+ {
+ desktop_file_ids = g_key_file_get_string_list (key_file, added_group, mime_types[i], NULL, NULL);
+
+ if (desktop_file_ids)
+ {
+ tweaks = desktop_file_dir_unindexed_get_tweaks (dir, mime_types[i]);
+ expand_strv (&tweaks->additions, desktop_file_ids, tweaks->removals);
+ }
+ }
+
+ g_strfreev (mime_types);
+ }
+
+ mime_types = g_key_file_get_keys (key_file, removed_group, NULL, NULL);
+
+ if G_UNLIKELY (mime_types != NULL && !tweaks_permitted)
+ {
+ g_warning ("%s contains a [%s] group, but it is not permitted here. Only the non-desktop-specific "
+ "mimeapps.list file may add or remove associations.", filename, removed_group);
+ g_strfreev (mime_types);
+ mime_types = NULL;
+ }
+
+ if (mime_types != NULL)
+ {
+ for (i = 0; mime_types[i] != NULL; i++)
+ {
+ desktop_file_ids = g_key_file_get_string_list (key_file, removed_group, mime_types[i], NULL, NULL);
+
+ if (desktop_file_ids)
+ {
+ tweaks = desktop_file_dir_unindexed_get_tweaks (dir, mime_types[i]);
+ expand_strv (&tweaks->removals, desktop_file_ids, tweaks->additions);
+ }
+ }
+
+ g_strfreev (mime_types);
+ }
+
+ mime_types = g_key_file_get_keys (key_file, default_group, NULL, NULL);
+
+ if (mime_types != NULL)
+ {
+ for (i = 0; mime_types[i] != NULL; i++)
+ {
+ desktop_file_ids = g_key_file_get_string_list (key_file, default_group, mime_types[i], NULL, NULL);
+
+ if (desktop_file_ids)
+ {
+ tweaks = desktop_file_dir_unindexed_get_tweaks (dir, mime_types[i]);
+ expand_strv (&tweaks->defaults, desktop_file_ids, NULL);
+ }
+ }
+
+ g_strfreev (mime_types);
+ }
+
+ g_key_file_free (key_file);
+}
+
+static void
+desktop_file_dir_unindexed_read_mimeapps_lists (DesktopFileDir *dir)
+{
+ const gchar * const *desktops;
+ gchar *filename;
+ gint i;
+
+ dir->mime_tweaks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, free_mime_tweaks);
+
+ /* We process in order of precedence, using a blacklisting 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 (i = 0; desktops[i]; i++)
+ {
+ filename = g_strdup_printf ("%s/%s-mimeapps.list", dir->path, desktops[i]);
+ desktop_file_dir_unindexed_read_mimeapps_list (dir, filename, "Added Associations", FALSE);
+ g_free (filename);
+ }
+
+ /* Next, the non-desktop-specific mimeapps.list */
+ filename = g_strdup_printf ("%s/mimeapps.list", dir->path);
+ desktop_file_dir_unindexed_read_mimeapps_list (dir, filename, "Added Associations", TRUE);
+ g_free (filename);
+
+ /* The remaining files are only checked for in directories that might
+ * contain desktop files (ie: not the config dirs).
+ */
+ if (dir->is_config)
+ return;
+
+ /* We have 'defaults.list' which was only ever understood by GLib. It
+ * exists widely, but it has never been part of any spec and it should
+ * be treated as deprecated. This will be removed in a future
+ * version.
+ */
+ filename = g_strdup_printf ("%s/defaults.list", dir->path);
+ desktop_file_dir_unindexed_read_mimeapps_list (dir, filename, "Added Associations", FALSE);
+ g_free (filename);
+
+ /* Finally, the mimeinfo.cache, which is just a cached copy of what we
+ * would find in the MimeTypes= lines of all of the desktop files.
+ */
+ filename = g_strdup_printf ("%s/mimeinfo.cache", dir->path);
+ desktop_file_dir_unindexed_read_mimeapps_list (dir, filename, "MIME Cache", 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_mimeapps_lists (dir);
+}
+
+static GDesktopAppInfo *
+desktop_file_dir_unindexed_get_app (DesktopFileDir *dir,
+ const gchar *desktop_id)
+{
+ const gchar *filename;
+
+ filename = g_hash_table_lookup (dir->app_names, desktop_id);
+
+ if (!filename)
+ return NULL;
+
+ return g_desktop_app_info_new_from_filename (filename);
+}
+
+static void
+desktop_file_dir_unindexed_get_all (DesktopFileDir *dir,
+ GHashTable *apps)
+{
+ GHashTableIter iter;
+ gpointer app_name;
+ gpointer filename;
+
+ if (dir->app_names == NULL)
+ return;
+
+ g_hash_table_iter_init (&iter, dir->app_names);
+ while (g_hash_table_iter_next (&iter, &app_name, &filename))
+ {
+ if (desktop_file_dir_app_name_is_masked (dir, app_name))
+ continue;
+
+ add_to_table_if_appropriate (apps, app_name, g_desktop_app_info_new_from_filename (filename));
+ }
+}
+
+typedef struct _MemoryIndexEntry MemoryIndexEntry;
+typedef GHashTable MemoryIndex;
+
+struct _MemoryIndexEntry
+{
+ const gchar *app_name; /* pointer to the hashtable key */
+ gint match_category;
+ MemoryIndexEntry *next;
+};
+
+static void
+memory_index_entry_free (gpointer data)
+{
+ MemoryIndexEntry *mie = data;
+
+ while (mie)
+ {
+ MemoryIndexEntry *next = mie->next;
+
+ g_slice_free (MemoryIndexEntry, mie);
+ mie = next;
+ }
+}
+
+static void
+memory_index_add_token (MemoryIndex *mi,
+ const gchar *token,
+ gint match_category,
+ const gchar *app_name)
+{
+ MemoryIndexEntry *mie, *first;
+
+ mie = g_slice_new (MemoryIndexEntry);
+ mie->app_name = app_name;
+ mie->match_category = match_category;
+
+ first = g_hash_table_lookup (mi, token);
+
+ if (first)
+ {
+ mie->next = first->next;
+ first->next = mie;
+ }
+ else
+ {
+ mie->next = NULL;
+ g_hash_table_insert (mi, g_strdup (token), mie);
+ }
+}
+
+static void
+memory_index_add_string (MemoryIndex *mi,
+ const gchar *string,
+ gint match_category,
+ const gchar *app_name)
+{
+ gchar **tokens, **alternates;
+ gint i;
+
+ tokens = g_str_tokenize_and_fold (string, NULL, &alternates);
+
+ for (i = 0; tokens[i]; i++)
+ memory_index_add_token (mi, tokens[i], match_category, app_name);
+
+ for (i = 0; alternates[i]; i++)
+ memory_index_add_token (mi, alternates[i], match_category, app_name);
+
+ g_strfreev (alternates);
+ g_strfreev (tokens);
+}
+
+static MemoryIndex *
+memory_index_new (void)
+{
+ return g_hash_table_new_full (g_str_hash, g_str_equal, g_free, memory_index_entry_free);
+}
+
+static void
+desktop_file_dir_unindexed_setup_search (DesktopFileDir *dir)
+{
+ GHashTableIter iter;
+ gpointer app, path;
+
+ dir->memory_index = memory_index_new ();
+
+ /* Nothing to search? */
+ if (dir->app_names == NULL)
+ return;
+
+ g_hash_table_iter_init (&iter, dir->app_names);
+ while (g_hash_table_iter_next (&iter, &app, &path))
+ {
+ GKeyFile *key_file;
+
+ if (desktop_file_dir_app_name_is_masked (dir, app))
+ continue;
+
+ key_file = g_key_file_new ();
+
+ if (g_key_file_load_from_file (key_file, path, G_KEY_FILE_NONE, NULL) &&
+ !g_key_file_get_boolean (key_file, "Desktop Entry", "Hidden", NULL))
+ {
+ /* Index the interesting keys... */
+ gint i;
+
+ for (i = 0; i < G_N_ELEMENTS (desktop_key_match_category); i++)
+ {
+ const gchar *value;
+ gchar *raw;
+
+ if (!desktop_key_match_category[i])
+ continue;
+
+ raw = g_key_file_get_locale_string (key_file, "Desktop Entry", desktop_key_get_name (i), NULL, NULL);
+ value = raw;
+
+ if (i == DESKTOP_KEY_Exec && raw != NULL)
+ {
+ /* Special handling: only match basename of first field */
+ gchar *space;
+ gchar *slash;
+
+ /* Remove extra arguments, if any */
+ space = raw + strcspn (raw, " \t\n"); /* IFS */
+ *space = '\0';
+
+ /* Skip the pathname, if any */
+ if ((slash = strrchr (raw, '/')))
+ value = slash + 1;
+ }
+
+ if (value)
+ memory_index_add_string (dir->memory_index, value, desktop_key_match_category[i], app);
+
+ g_free (raw);
+ }
+ }
+
+ g_key_file_free (key_file);
+ }
+}
+
+static void
+desktop_file_dir_unindexed_search (DesktopFileDir *dir,
+ const gchar *search_token)
+{
+ GHashTableIter iter;
+ gpointer key, value;
+
+ if (!dir->memory_index)
+ desktop_file_dir_unindexed_setup_search (dir);
+
+ g_hash_table_iter_init (&iter, dir->memory_index);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ {
+ MemoryIndexEntry *mie = value;
+
+ if (!g_str_has_prefix (key, search_token))
+ continue;
+
+ while (mie)
+ {
+ add_token_result (mie->app_name, mie->match_category);
+ mie = mie->next;
+ }
+ }
+}
+
+static gboolean
+array_contains (GPtrArray *array,
+ const gchar *str)
+{
+ gint i;
+
+ for (i = 0; i < array->len; i++)
+ if (g_str_equal (array->pdata[i], str))
+ return TRUE;
+
+ return FALSE;
+}
+
+static void
+desktop_file_dir_unindexed_mime_lookup (DesktopFileDir *dir,
+ const gchar *mime_type,
+ GPtrArray *hits,
+ GPtrArray *blacklist)
+{
+ UnindexedMimeTweaks *tweaks;
+ gint i;
+
+ tweaks = g_hash_table_lookup (dir->mime_tweaks, mime_type);
+
+ if (!tweaks)
+ return;
+
+ if (tweaks->additions)
+ {
+ for (i = 0; tweaks->additions[i]; i++)
+ {
+ gchar *app_name = tweaks->additions[i];
+
+ if (!desktop_file_dir_app_name_is_masked (dir, app_name) &&
+ !array_contains (blacklist, app_name) && !array_contains (hits, app_name))
+ g_ptr_array_add (hits, g_strdup (app_name));
+ }
+ }
+
+ if (tweaks->removals)
+ {
+ for (i = 0; tweaks->removals[i]; i++)
+ {
+ gchar *app_name = tweaks->removals[i];
+
+ if (!desktop_file_dir_app_name_is_masked (dir, app_name) &&
+ !array_contains (blacklist, app_name) && !array_contains (hits, app_name))
+ g_ptr_array_add (blacklist, app_name);
+ }
+ }
+}
+
+static void
+desktop_file_dir_unindexed_default_lookup (DesktopFileDir *dir,
+ const gchar *mime_type,
+ GPtrArray *results)
+{
+ UnindexedMimeTweaks *tweaks;
+ gint i;
+
+ tweaks = g_hash_table_lookup (dir->mime_tweaks, mime_type);
+
+ if (!tweaks || !tweaks->defaults)
+ return;
+
+ for (i = 0; tweaks->defaults[i]; i++)
+ {
+ gchar *app_name = tweaks->defaults[i];
+
+ if (!array_contains (results, app_name))
+ g_ptr_array_add (results, g_strdup (app_name));
+ }
+}
+
+/* DesktopFileDir "API" {{{2 */
+
+/*< internal >
+ * desktop_file_dir_create:
+ * @array: the #GArray to add a new item to
+ * @data_dir: an XDG_DATA_DIR
+ *
+ * Creates a #DesktopFileDir for the corresponding @data_dir, adding it
+ * to @array.
+ */
+static void
+desktop_file_dir_create (GArray *array,
+ const gchar *data_dir)
+{
+ DesktopFileDir dir = { 0, };
+
+ dir.path = g_build_filename (data_dir, "applications", NULL);
+
+ g_array_append_val (array, dir);
+}
+
+/*< internal >
+ * desktop_file_dir_create:
+ * @array: the #GArray to add a new item to
+ * @config_dir: an XDG_CONFIG_DIR
+ *
+ * Just the same as desktop_file_dir_create() except that it does not
+ * add the "applications" directory. It also marks the directory as
+ * config-only, which prevents us from attempting to find desktop files
+ * here.
+ */
+static void
+desktop_file_dir_create_for_config (GArray *array,
+ const gchar *config_dir)
+{
+ DesktopFileDir dir = { 0, };
+
+ dir.path = g_strdup (config_dir);
+ dir.is_config = TRUE;
+
+ g_array_append_val (array, dir);
+}
+
+/*< internal >
+ * desktop_file_dir_reset:
+ * @dir: a #DesktopFileDir
+ *
+ * Cleans up @dir, releasing most resources that it was using.
+ */
+static void
+desktop_file_dir_reset (DesktopFileDir *dir)
+{
+ if (dir->monitor)
+ {
+ g_signal_handlers_disconnect_by_func (dir->monitor, desktop_file_dir_changed, dir);
+ g_object_unref (dir->monitor);
+ dir->monitor = NULL;
+ }
+
+ if (dir->app_names)
+ {
+ g_hash_table_unref (dir->app_names);
+ dir->app_names = NULL;
+ }
+
+ if (dir->memory_index)
+ {
+ g_hash_table_unref (dir->memory_index);
+ dir->memory_index = NULL;
+ }
+
+ if (dir->mime_tweaks)
+ {
+ g_hash_table_unref (dir->mime_tweaks);
+ dir->mime_tweaks = NULL;
+ }
+
+ dir->is_setup = FALSE;
+}
+
+/*< internal >
+ * desktop_file_dir_init:
+ * @dir: a #DesktopFileDir
+ *
+ * Does initial setup for @dir
+ *
+ * You should only call this if @dir is not already setup.
+ */
+static void
+desktop_file_dir_init (DesktopFileDir *dir)
+{
+ g_assert (!dir->is_setup);
+
+ g_assert (!dir->monitor);
+ dir->monitor = g_local_directory_monitor_new_in_worker (dir->path, G_FILE_MONITOR_NONE, NULL);
+
+ if (dir->monitor)
+ {
+ g_signal_connect (dir->monitor, "changed", G_CALLBACK (desktop_file_dir_changed), dir);
+ g_local_directory_monitor_start (dir->monitor);
+ }
+
+ desktop_file_dir_unindexed_init (dir);
+
+ dir->is_setup = TRUE;
+}
+
+/*< internal >
+ * desktop_file_dir_get_app:
+ * @dir: a DesktopFileDir
+ * @desktop_id: the desktop ID to load
+ *
+ * Creates the #GDesktopAppInfo for the given @desktop_id if it exists
+ * within @dir, even if it is hidden.
+ *
+ * This function does not check if @desktop_id would be masked by a
+ * directory with higher precedence. The caller must do so.
+ */
+static GDesktopAppInfo *
+desktop_file_dir_get_app (DesktopFileDir *dir,
+ const gchar *desktop_id)
+{
+ if (!dir->app_names)
+ return NULL;
+
+ return desktop_file_dir_unindexed_get_app (dir, desktop_id);
+}
+
+/*< internal >
+ * desktop_file_dir_get_all:
+ * @dir: a DesktopFileDir
+ * @apps: a #GHashTable<string, GDesktopAppInfo>
+ *
+ * Loads all desktop files in @dir and adds them to @apps, careful to
+ * ensure we don't add any files masked by a similarly-named file in a
+ * higher-precedence directory.
+ */
+static void
+desktop_file_dir_get_all (DesktopFileDir *dir,
+ GHashTable *apps)
+{
+ desktop_file_dir_unindexed_get_all (dir, apps);
+}
+
+/*< internal >
+ * desktop_file_dir_mime_lookup:
+ * @dir: a #DesktopFileDir
+ * @mime_type: the mime type to look up
+ * @hits: the array to store the hits
+ * @blacklist: the array to store the blacklist
+ *
+ * Does a lookup of a mimetype against one desktop file directory,
+ * recording any hits and blacklisting and "Removed" associations (so
+ * later directories don't record them as hits).
+ *
+ * The items added to @hits are duplicated, but the ones in @blacklist
+ * are weak pointers. This facilitates simply freeing the blacklist
+ * (which is only used for internal bookkeeping) but using the pdata of
+ * @hits as the result of the operation.
+ */
+static void
+desktop_file_dir_mime_lookup (DesktopFileDir *dir,
+ const gchar *mime_type,
+ GPtrArray *hits,
+ GPtrArray *blacklist)
+{
+ desktop_file_dir_unindexed_mime_lookup (dir, mime_type, hits, blacklist);
+}
+
+/*< internal >
+ * desktop_file_dir_default_lookup:
+ * @dir: a #DesktopFileDir
+ * @mime_type: the mime type to look up
+ * @results: an array to store the results in
+ *
+ * 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_unindexed_default_lookup (dir, mime_type, results);
+}
+
+/*< internal >
+ * desktop_file_dir_search:
+ * @dir: a #DesktopFileDir
+ * @term: a normalised and casefolded search term
+ *
+ * Finds the names of applications in @dir that match @term.
+ */
+static void
+desktop_file_dir_search (DesktopFileDir *dir,
+ const gchar *search_token)
+{
+ desktop_file_dir_unindexed_search (dir, search_token);
+}
+
+/* Lock/unlock and global setup API {{{2 */
+
+static void
+desktop_file_dirs_lock (void)
+{
+ gint i;
+
+ g_mutex_lock (&desktop_file_dir_lock);
+
+ if (desktop_file_dirs == NULL)
+ {
+ const char * const *dirs;
+ GArray *tmp;
+ gint i;
+
+ tmp = g_array_new (FALSE, FALSE, sizeof (DesktopFileDir));
+
+ /* First, the configs. Highest priority: the user's ~/.config */
+ desktop_file_dir_create_for_config (tmp, g_get_user_config_dir ());
+
+ /* Next, the system configs (/etc/xdg, and so on). */
+ dirs = g_get_system_config_dirs ();
+ for (i = 0; dirs[i]; i++)
+ desktop_file_dir_create_for_config (tmp, dirs[i]);
+
+ /* Now the data. Highest priority: the user's ~/.local/share/applications */
+ desktop_file_dir_user_data_index = tmp->len;
+ desktop_file_dir_create (tmp, g_get_user_data_dir ());
+
+ /* Following that, XDG_DATA_DIRS/applications, in order */
+ dirs = g_get_system_data_dirs ();
+ for (i = 0; dirs[i]; i++)
+ desktop_file_dir_create (tmp, dirs[i]);
+
+ /* The list of directories will never change after this. */
+ desktop_file_dirs = (DesktopFileDir *) tmp->data;
+ n_desktop_file_dirs = tmp->len;
+
+ g_array_free (tmp, FALSE);
+ }
+
+ for (i = 0; i < n_desktop_file_dirs; i++)
+ if (!desktop_file_dirs[i].is_setup)
+ desktop_file_dir_init (&desktop_file_dirs[i]);
+}
+
+static void
+desktop_file_dirs_unlock (void)
+{
+ g_mutex_unlock (&desktop_file_dir_lock);
+}
+
+static void
+desktop_file_dirs_invalidate_user_config (void)
+{
+ g_mutex_lock (&desktop_file_dir_lock);
+
+ if (n_desktop_file_dirs)
+ desktop_file_dir_reset (&desktop_file_dirs[desktop_file_dir_user_config_index]);
+
+ g_mutex_unlock (&desktop_file_dir_lock);
+}
+
+static void
+desktop_file_dirs_invalidate_user_data (void)
+{
+ g_mutex_lock (&desktop_file_dir_lock);
+
+ if (n_desktop_file_dirs)
+ desktop_file_dir_reset (&desktop_file_dirs[desktop_file_dir_user_data_index]);
+
+ g_mutex_unlock (&desktop_file_dir_lock);
+}
+
+/* GDesktopAppInfo implementation {{{1 */
+/* GObject implementation {{{2 */
+static void
+g_desktop_app_info_finalize (GObject *object)
+{
+ GDesktopAppInfo *info;
+
+ info = G_DESKTOP_APP_INFO (object);
+
+ g_free (info->desktop_id);
+ g_free (info->filename);
+
+ if (info->keyfile)
+ g_key_file_unref (info->keyfile);
+
+ g_free (info->name);
+ g_free (info->generic_name);
+ g_free (info->fullname);
+ g_free (info->comment);
+ g_free (info->icon_name);
+ if (info->icon)
+ g_object_unref (info->icon);
+ g_strfreev (info->keywords);
+ g_strfreev (info->only_show_in);
+ g_strfreev (info->not_show_in);
+ g_free (info->try_exec);
+ g_free (info->exec);
+ g_free (info->binary);
+ g_free (info->path);
+ g_free (info->categories);
+ g_free (info->startup_wm_class);
+ g_strfreev (info->mime_types);
+ g_free (info->app_id);
+ g_strfreev (info->actions);
+
+ G_OBJECT_CLASS (g_desktop_app_info_parent_class)->finalize (object);
+}
+
+static void
+g_desktop_app_info_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GDesktopAppInfo *self = G_DESKTOP_APP_INFO (object);
+
+ switch (prop_id)
+ {
+ case PROP_FILENAME:
+ self->filename = g_value_dup_string (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+g_desktop_app_info_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GDesktopAppInfo *self = G_DESKTOP_APP_INFO (object);
+
+ switch (prop_id)
+ {
+ case PROP_FILENAME:
+ g_value_set_string (value, self->filename);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+g_desktop_app_info_class_init (GDesktopAppInfoClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->get_property = g_desktop_app_info_get_property;
+ gobject_class->set_property = g_desktop_app_info_set_property;
+ gobject_class->finalize = g_desktop_app_info_finalize;
+
+ /**
+ * GDesktopAppInfo:filename:
+ *
+ * The origin filename of this #GDesktopAppInfo
+ */
+ g_object_class_install_property (gobject_class,
+ PROP_FILENAME,
+ g_param_spec_string ("filename", "Filename", "", NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+}
+
+static void
+g_desktop_app_info_init (GDesktopAppInfo *local)
+{
+}
+
+/* Construction... {{{2 */
+
+/*< internal >
+ * binary_from_exec:
+ * @exec: an exec line
+ *
+ * Returns the first word in an exec line (ie: the binary name).
+ *
+ * If @exec is " progname --foo %F" then returns "progname".
+ */
+static char *
+binary_from_exec (const char *exec)
+{
+ const char *p, *start;
+
+ p = exec;
+ while (*p == ' ')
+ p++;
+ start = p;
+ while (*p != ' ' && *p != 0)
+ p++;
+
+ return g_strndup (start, p - start);
+}
+
+static gboolean
+g_desktop_app_info_load_from_keyfile (GDesktopAppInfo *info,
+ GKeyFile *key_file)
+{
+ char *start_group;
+ char *type;
+ char *try_exec;
+ char *exec;
+ gboolean bus_activatable;
+
+ start_group = g_key_file_get_start_group (key_file);
+ if (start_group == NULL || strcmp (start_group, G_KEY_FILE_DESKTOP_GROUP) != 0)
+ {
+ g_free (start_group);
+ return FALSE;
+ }
+ g_free (start_group);
+
+ type = g_key_file_get_string (key_file,
+ G_KEY_FILE_DESKTOP_GROUP,
+ G_KEY_FILE_DESKTOP_KEY_TYPE,
+ NULL);
+ if (type == NULL || strcmp (type, G_KEY_FILE_DESKTOP_TYPE_APPLICATION) != 0)
+ {
+ g_free (type);
+ return FALSE;
+ }
+ g_free (type);
+
+ try_exec = g_key_file_get_string (key_file,
+ G_KEY_FILE_DESKTOP_GROUP,
+ G_KEY_FILE_DESKTOP_KEY_TRY_EXEC,
+ NULL);
+ if (try_exec && try_exec[0] != '\0')
+ {
+ char *t;
+ t = g_find_program_in_path (try_exec);
+ if (t == NULL)
+ {
+ g_free (try_exec);
+ return FALSE;
+ }
+ g_free (t);
+ }
+
+ exec = g_key_file_get_string (key_file,
+ G_KEY_FILE_DESKTOP_GROUP,
+ G_KEY_FILE_DESKTOP_KEY_EXEC,
+ NULL);
+ if (exec && exec[0] != '\0')
+ {
+ gint argc;
+ char **argv;
+ if (!g_shell_parse_argv (exec, &argc, &argv, NULL))
+ {
+ g_free (exec);
+ g_free (try_exec);
+ return FALSE;
+ }
+ else
+ {
+ char *t;
+ t = g_find_program_in_path (argv[0]);
+ g_strfreev (argv);
+
+ if (t == NULL)
+ {
+ g_free (exec);
+ g_free (try_exec);
+ return FALSE;
+ }
+ g_free (t);
+ }
+ }
+
+ info->name = g_key_file_get_locale_string (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, NULL, NULL);
+ info->generic_name = g_key_file_get_locale_string (key_file, G_KEY_FILE_DESKTOP_GROUP, GENERIC_NAME_KEY, NULL, NULL);
+ info->fullname = g_key_file_get_locale_string (key_file, G_KEY_FILE_DESKTOP_GROUP, FULL_NAME_KEY, NULL, NULL);
+ info->keywords = g_key_file_get_locale_string_list (key_file, G_KEY_FILE_DESKTOP_GROUP, KEYWORDS_KEY, NULL, NULL, NULL);
+ info->comment = g_key_file_get_locale_string (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_COMMENT, NULL, NULL);
+ info->nodisplay = g_key_file_get_boolean (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY, NULL) != FALSE;
+ info->icon_name = g_key_file_get_locale_string (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, NULL, NULL);
+ info->only_show_in = g_key_file_get_string_list (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ONLY_SHOW_IN, NULL, NULL);
+ info->not_show_in = g_key_file_get_string_list (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NOT_SHOW_IN, NULL, NULL);
+ info->try_exec = try_exec;
+ info->exec = exec;
+ info->path = g_key_file_get_string (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_PATH, NULL);
+ info->terminal = g_key_file_get_boolean (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TERMINAL, NULL) != FALSE;
+ info->startup_notify = g_key_file_get_boolean (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_STARTUP_NOTIFY, NULL) != FALSE;
+ info->no_fuse = g_key_file_get_boolean (key_file, G_KEY_FILE_DESKTOP_GROUP, "X-GIO-NoFuse", NULL) != FALSE;
+ info->hidden = g_key_file_get_boolean (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_HIDDEN, NULL) != FALSE;
+ info->categories = g_key_file_get_string (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_CATEGORIES, NULL);
+ info->startup_wm_class = g_key_file_get_string (key_file, G_KEY_FILE_DESKTOP_GROUP, STARTUP_WM_CLASS_KEY, NULL);
+ 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);
+
+ /* Remove the special-case: no Actions= key just means 0 extra actions */
+ if (info->actions == NULL)
+ info->actions = g_new0 (gchar *, 0 + 1);
+
+ info->icon = NULL;
+ if (info->icon_name)
+ {
+ if (g_path_is_absolute (info->icon_name))
+ {
+ GFile *file;
+
+ file = g_file_new_for_path (info->icon_name);
+ info->icon = g_file_icon_new (file);
+ g_object_unref (file);
+ }
+ else
+ {
+ char *p;
+
+ /* Work around a common mistake in desktop files */
+ if ((p = strrchr (info->icon_name, '.')) != NULL &&
+ (strcmp (p, ".png") == 0 ||
+ strcmp (p, ".xpm") == 0 ||
+ strcmp (p, ".svg") == 0))
+ *p = 0;
+
+ info->icon = g_themed_icon_new (info->icon_name);
+ }
+ }
+
+ if (info->exec)
+ info->binary = binary_from_exec (info->exec);
+
+ if (info->path && info->path[0] == '\0')
+ {
+ g_free (info->path);
+ info->path = NULL;
+ }
+
+ /* Can only be DBusActivatable if we know the filename, which means
+ * that this won't work for the load-from-keyfile case.
+ */
+ if (bus_activatable && info->filename)
+ {
+ gchar *basename;
+ gchar *last_dot;
+
+ basename = g_path_get_basename (info->filename);
+ last_dot = strrchr (basename, '.');
+
+ if (last_dot && g_str_equal (last_dot, ".desktop"))
+ {
+ *last_dot = '\0';
+
+ if (g_dbus_is_interface_name (basename))
+ info->app_id = g_strdup (basename);
+ }
+
+ g_free (basename);
+ }
+
+ info->keyfile = g_key_file_ref (key_file);
+
+ return TRUE;
+}
+
+static gboolean
+g_desktop_app_info_load_file (GDesktopAppInfo *self)
+{
+ GKeyFile *key_file;
+ gboolean retval = FALSE;
+
+ g_return_val_if_fail (self->filename != NULL, FALSE);
+
+ self->desktop_id = g_path_get_basename (self->filename);
+
+ key_file = g_key_file_new ();
+
+ if (g_key_file_load_from_file (key_file, self->filename, G_KEY_FILE_NONE, NULL))
+ retval = g_desktop_app_info_load_from_keyfile (self, key_file);
+
+ g_key_file_unref (key_file);
+ return retval;
+}
+
+/**
+ * g_desktop_app_info_new_from_keyfile:
+ * @key_file: an opened #GKeyFile
+ *
+ * Creates a new #GDesktopAppInfo.
+ *
+ * Returns: a new #GDesktopAppInfo or %NULL on error.
+ *
+ * Since: 2.18
+ **/
+GDesktopAppInfo *
+g_desktop_app_info_new_from_keyfile (GKeyFile *key_file)
+{
+ GDesktopAppInfo *info;
+
+ info = g_object_new (G_TYPE_DESKTOP_APP_INFO, NULL);
+ info->filename = NULL;
+ if (!g_desktop_app_info_load_from_keyfile (info, key_file))