X-Git-Url: http://review.tizen.org/git/?a=blobdiff_plain;f=gio%2Fgdesktopappinfo.c;h=7bfc904719c73e3af44f8a15d5423aa16336c77c;hb=2e5bd8cf47f9e1559ccc44823a2f321b8ff8c1ea;hp=f557538441b4ac74f59e095308547498f720031e;hpb=e738a8dd8ca3d3dd327bc5a3bbfd151858738609;p=platform%2Fupstream%2Fglib.git diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c index f557538..7bfc904 100644 --- a/gio/gdesktopappinfo.c +++ b/gio/gdesktopappinfo.c @@ -1,5 +1,5 @@ /* GIO - GLib Input, Output and Streaming Library - * + * * Copyright (C) 2006-2007 Red Hat, Inc. * Copyright © 2007 Ryan Lortie * @@ -14,28 +14,29 @@ * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General - * Public License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place, Suite 330, - * Boston, MA 02111-1307, USA. + * Public License along with this library; if not, see . * * Author: Alexander Larsson + * Ryan Lortie */ +/* Prelude {{{1 */ + #include "config.h" #include #include #include -#include #ifdef HAVE_CRT_EXTERNS_H #include #endif -#undef G_DISABLE_DEPRECATED - #include "gcontenttypeprivate.h" #include "gdesktopappinfo.h" +#ifdef G_OS_UNIX +#include "glib-unix.h" +#endif #include "gfile.h" #include "gioerror.h" #include "gthemedicon.h" @@ -44,6 +45,8 @@ #include "glibintl.h" #include "giomodule-priv.h" #include "gappinfo.h" +#include "gappinfoprivate.h" +#include "glocaldirectorymonitor.h" /** @@ -55,29 +58,32 @@ * #GDesktopAppInfo is an implementation of #GAppInfo based on * desktop files. * - * Note that <gio/gdesktopappinfo.h> belongs to - * the UNIX-specific GIO interfaces, thus you have to use the - * gio-unix-2.0.pc pkg-config file when using it. + * Note that `` belongs to the UNIX-specific + * GIO interfaces, thus you have to use the `gio-unix-2.0.pc` pkg-config + * file when using it. */ #define DEFAULT_APPLICATIONS_GROUP "Default Applications" #define ADDED_ASSOCIATIONS_GROUP "Added Associations" #define REMOVED_ASSOCIATIONS_GROUP "Removed Associations" #define MIME_CACHE_GROUP "MIME Cache" +#define GENERIC_NAME_KEY "GenericName" #define FULL_NAME_KEY "X-GNOME-FullName" +#define KEYWORDS_KEY "Keywords" +#define STARTUP_WM_CLASS_KEY "StartupWMClass" + +enum { + PROP_0, + PROP_FILENAME +}; static void g_desktop_app_info_iface_init (GAppInfoIface *iface); -static GList * get_all_desktop_entries_for_mime_type (const char *base_mime_type, - const char **except, - gboolean include_fallback, - char **explicit_default); -static void mime_info_cache_reload (const char *dir); static gboolean g_desktop_app_info_ensure_saved (GDesktopAppInfo *info, - GError **error); + GError **error); /** * GDesktopAppInfo: - * + * * Information about an installed application from a desktop file. */ struct _GDesktopAppInfo @@ -86,26 +92,33 @@ struct _GDesktopAppInfo char *desktop_id; char *filename; + char *app_id; + + GKeyFile *keyfile; char *name; - /* FIXME: what about GenericName ? */ + char *generic_name; char *fullname; char *comment; char *icon_name; GIcon *icon; + char **keywords; char **only_show_in; char **not_show_in; char *try_exec; char *exec; char *binary; char *path; + char *categories; + char *startup_wm_class; + char **mime_types; + char **actions; guint nodisplay : 1; guint hidden : 1; guint terminal : 1; guint startup_notify : 1; guint no_fuse : 1; - /* FIXME: what about StartupWMClass ? */ }; typedef enum { @@ -117,361 +130,1867 @@ typedef enum { } UpdateMimeFlags; G_DEFINE_TYPE_WITH_CODE (GDesktopAppInfo, g_desktop_app_info, G_TYPE_OBJECT, - G_IMPLEMENT_INTERFACE (G_TYPE_APP_INFO, - g_desktop_app_info_iface_init)) - -static gpointer -search_path_init (gpointer data) -{ - char **args = NULL; - const char * const *data_dirs; - const char *user_data_dir; - int i, length, j; - - data_dirs = g_get_system_data_dirs (); - length = g_strv_length ((char **) data_dirs); - - args = g_new (char *, length + 2); - - j = 0; - user_data_dir = g_get_user_data_dir (); - args[j++] = g_build_filename (user_data_dir, "applications", NULL); - for (i = 0; i < length; i++) - args[j++] = g_build_filename (data_dirs[i], - "applications", NULL); - args[j++] = NULL; - - return args; -} - -static const char * const * -get_applications_search_path (void) -{ - static GOnce once_init = G_ONCE_INIT; - return g_once (&once_init, search_path_init, NULL); -} + G_IMPLEMENT_INTERFACE (G_TYPE_APP_INFO, g_desktop_app_info_iface_init)) -static void -g_desktop_app_info_finalize (GObject *object) +/* DesktopFileDir implementation {{{1 */ + +typedef struct { - GDesktopAppInfo *info; + gchar *path; + gchar *alternatively_watching; + gboolean is_config; + gboolean is_setup; + GLocalDirectoryMonitor *monitor; + GHashTable *app_names; + GHashTable *mime_tweaks; + GHashTable *memory_index; + GHashTable *memory_implementations; +} DesktopFileDir; + +static DesktopFileDir *desktop_file_dirs; +static guint n_desktop_file_dirs; +static const guint desktop_file_dir_user_config_index = 0; +static guint desktop_file_dir_user_data_index; +static GMutex desktop_file_dir_lock; + +/* Monitor 'changed' signal handler {{{2 */ +static void desktop_file_dir_reset (DesktopFileDir *dir); + +/*< internal > + * desktop_file_dir_get_alternative_dir: + * @dir: a #DesktopFileDir + * + * Gets the "alternative" directory to monitor in case the path + * doesn't exist. + * + * If the path exists this will return NULL, otherwise it will return a + * parent directory of the path. + * + * This is used to avoid inotify on a non-existent directory (which + * results in polling). + * + * See https://bugzilla.gnome.org/show_bug.cgi?id=522314 for more info. + */ +static gchar * +desktop_file_dir_get_alternative_dir (DesktopFileDir *dir) +{ + gchar *parent; - info = G_DESKTOP_APP_INFO (object); + /* If the directory itself exists then we need no alternative. */ + if (g_access (dir->path, R_OK | X_OK) == 0) + return NULL; - g_free (info->desktop_id); - g_free (info->filename); - g_free (info->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->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_OBJECT_CLASS (g_desktop_app_info_parent_class)->finalize (object); -} + /* Otherwise, try the parent directories until we find one. */ + parent = g_path_get_dirname (dir->path); -static void -g_desktop_app_info_class_init (GDesktopAppInfoClass *klass) -{ - GObjectClass *gobject_class = G_OBJECT_CLASS (klass); - - gobject_class->finalize = g_desktop_app_info_finalize; + while (g_access (parent, R_OK | X_OK) != 0) + { + gchar *tmp = parent; + + parent = g_path_get_dirname (tmp); + + /* If somehow we get to '/' or '.' then just stop... */ + if (g_str_equal (parent, tmp)) + { + g_free (tmp); + break; + } + + g_free (tmp); + } + + return parent; } static void -g_desktop_app_info_init (GDesktopAppInfo *local) +desktop_file_dir_changed (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) { -} + DesktopFileDir *dir = user_data; + gboolean do_nothing = FALSE; + + /* We are not interested in receiving notifications forever just + * because someone asked about one desktop file once. + * + * After we receive the first notification, reset the dir, destroying + * the monitor. We will take this as a hint, next time that we are + * asked, that we need to check if everything is up to date. + * + * If this is a notification for a parent directory (because the + * desktop directory didn't exist) then we shouldn't fire the signal + * unless something actually changed. + */ + g_mutex_lock (&desktop_file_dir_lock); -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); - + if (dir->alternatively_watching) + { + gchar *alternative_dir; + + alternative_dir = desktop_file_dir_get_alternative_dir (dir); + do_nothing = alternative_dir && g_str_equal (dir->alternatively_watching, alternative_dir); + g_free (alternative_dir); + } + + if (!do_nothing) + desktop_file_dir_reset (dir); + + g_mutex_unlock (&desktop_file_dir_lock); + + /* Notify anyone else who may be interested */ + if (!do_nothing) + g_app_info_monitor_fire (); } -/** - * g_desktop_app_info_new_from_keyfile: - * @key_file: an opened #GKeyFile - * - * Creates a new #GDesktopAppInfo. +/* Internal utility functions {{{2 */ + +/*< internal > + * desktop_file_dir_app_name_is_masked: + * @dir: a #DesktopFileDir + * @app_name: an application ID * - * Returns: a new #GDesktopAppInfo or %NULL on error. + * Checks if @app_name is masked for @dir. * - * Since: 2.18 - **/ -GDesktopAppInfo * -g_desktop_app_info_new_from_keyfile (GKeyFile *key_file) + * An application is masked if a similarly-named desktop file exists in + * a desktop file directory with higher precedence. Masked desktop + * files should be ignored. + */ +static gboolean +desktop_file_dir_app_name_is_masked (DesktopFileDir *dir, + const gchar *app_name) { - GDesktopAppInfo *info; - char *start_group; - char *type; - char *try_exec; - - start_group = g_key_file_get_start_group (key_file); - if (start_group == NULL || strcmp (start_group, G_KEY_FILE_DESKTOP_GROUP) != 0) + while (dir > desktop_file_dirs) { - g_free (start_group); - return NULL; - } - g_free (start_group); + dir--; - 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 NULL; + if (dir->app_names && g_hash_table_contains (dir->app_names, app_name)) + return TRUE; } - 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 NULL; - } - g_free (t); - } + return FALSE; +} - info = g_object_new (G_TYPE_DESKTOP_APP_INFO, NULL); - info->filename = NULL; +static const gchar * const * +get_lowercase_current_desktops (void) +{ + static gchar **result; - info->name = g_key_file_get_locale_string (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, NULL, NULL); - info->fullname = g_key_file_get_locale_string (key_file, G_KEY_FILE_DESKTOP_GROUP, FULL_NAME_KEY, 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 = g_key_file_get_string (key_file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, NULL); - 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->icon = NULL; - if (info->icon_name) + if (g_once_init_enter (&result)) { - 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 + const gchar *envvar; + gchar **tmp; + + envvar = g_getenv ("XDG_CURRENT_DESKTOP"); + + if (envvar) { - char *p; + gint i, j; - /* 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; + tmp = g_strsplit (envvar, G_SEARCHPATH_SEPARATOR_S, 0); - info->icon = g_themed_icon_new (info->icon_name); + for (i = 0; tmp[i]; i++) + for (j = 0; tmp[i][j]; j++) + tmp[i][j] = g_ascii_tolower (tmp[i][j]); } - } - - if (info->exec) - info->binary = binary_from_exec (info->exec); - - if (info->path && info->path[0] == '\0') - { - g_free (info->path); - info->path = NULL; + else + tmp = g_new0 (gchar *, 0 + 1); + + g_once_init_leave (&result, tmp); } - return info; + return (const gchar **) result; } -/** - * g_desktop_app_info_new_from_filename: - * @filename: the path of a desktop file, in the GLib filename encoding - * - * Creates a new #GDesktopAppInfo. - * - * Returns: a new #GDesktopAppInfo or %NULL on error. - **/ -GDesktopAppInfo * -g_desktop_app_info_new_from_filename (const char *filename) +static const gchar * const * +get_current_desktops (const gchar *value) { - GKeyFile *key_file; - GDesktopAppInfo *info = NULL; + static gchar **result; - key_file = g_key_file_new (); - - if (g_key_file_load_from_file (key_file, - filename, - G_KEY_FILE_NONE, - NULL)) + if (g_once_init_enter (&result)) { - info = g_desktop_app_info_new_from_keyfile (key_file); - if (info) - info->filename = g_strdup (filename); - } + gchar **tmp; - g_key_file_free (key_file); + if (!value) + value = g_getenv ("XDG_CURRENT_DESKTOP"); - return info; + if (!value) + value = ""; + + tmp = g_strsplit (value, ":", 0); + + g_once_init_leave (&result, tmp); + } + + return (const gchar **) result; } -/** - * g_desktop_app_info_new: - * @desktop_id: the desktop file id - * - * Creates a new #GDesktopAppInfo based on a desktop file id. - * - * A desktop file id is the basename of the desktop file, including the - * .desktop extension. GIO is looking for a desktop file with this name - * in the applications subdirectories of the XDG data - * directories (i.e. the directories specified in the - * XDG_DATA_HOME and XDG_DATA_DIRS environment - * variables). GIO also supports the prefix-to-subdirectory mapping that is - * described in the Menu Spec - * (i.e. a desktop id of kde-foo.desktop will match - * /usr/share/applications/kde/foo.desktop). - * - * Returns: a new #GDesktopAppInfo, or %NULL if no desktop file with that id +/*< internal > + * add_to_table_if_appropriate: + * @apps: a string to GDesktopAppInfo hash table + * @app_name: the name of the application + * @info: a #GDesktopAppInfo, or NULL + * + * If @info is non-%NULL and non-hidden, then add it to @apps, using + * @app_name as a key. + * + * If @info is non-%NULL then this function will consume the passed-in + * reference. */ -GDesktopAppInfo * -g_desktop_app_info_new (const char *desktop_id) +static void +add_to_table_if_appropriate (GHashTable *apps, + const gchar *app_name, + GDesktopAppInfo *info) { - GDesktopAppInfo *appinfo; - const char * const *dirs; - char *basename; - int i; - - dirs = get_applications_search_path (); + if (!info) + return; - basename = g_strdup (desktop_id); - - for (i = 0; dirs[i] != NULL; i++) + if (info->hidden) { - char *filename; - char *p; + g_object_unref (info); + return; + } - filename = g_build_filename (dirs[i], desktop_id, NULL); - appinfo = g_desktop_app_info_new_from_filename (filename); - g_free (filename); - if (appinfo != NULL) - goto found; - - p = basename; - while ((p = strchr (p, '-')) != NULL) - { - *p = '/'; - - filename = g_build_filename (dirs[i], basename, NULL); - appinfo = g_desktop_app_info_new_from_filename (filename); - g_free (filename); - if (appinfo != NULL) - goto found; - *p = '-'; - p++; - } - } - - g_free (basename); - return NULL; + g_free (info->desktop_id); + info->desktop_id = g_strdup (app_name); - found: - g_free (basename); - - appinfo->desktop_id = g_strdup (desktop_id); + g_hash_table_insert (apps, g_strdup (info->desktop_id), info); +} - if (g_desktop_app_info_get_is_hidden (appinfo)) +enum +{ + DESKTOP_KEY_Comment, + DESKTOP_KEY_Exec, + DESKTOP_KEY_GenericName, + DESKTOP_KEY_Keywords, + DESKTOP_KEY_Name, + DESKTOP_KEY_X_GNOME_FullName, + + N_DESKTOP_KEYS +}; + +const gchar desktop_key_match_category[N_DESKTOP_KEYS] = { + /* Note: lower numbers are a better match. + * + * In case we want two keys to match at the same level, we can just + * use the same number for the two different keys. + */ + [DESKTOP_KEY_Name] = 1, + [DESKTOP_KEY_Exec] = 2, + [DESKTOP_KEY_Keywords] = 3, + [DESKTOP_KEY_GenericName] = 4, + [DESKTOP_KEY_X_GNOME_FullName] = 5, + [DESKTOP_KEY_Comment] = 6 +}; + +static gchar * +desktop_key_get_name (guint key_id) +{ + switch (key_id) { - g_object_unref (appinfo); - appinfo = NULL; + case DESKTOP_KEY_Comment: + return "Comment"; + case DESKTOP_KEY_Exec: + return "Exec"; + case DESKTOP_KEY_GenericName: + return GENERIC_NAME_KEY; + case DESKTOP_KEY_Keywords: + return KEYWORDS_KEY; + case DESKTOP_KEY_Name: + return "Name"; + case DESKTOP_KEY_X_GNOME_FullName: + return FULL_NAME_KEY; + default: + g_assert_not_reached (); } - - return appinfo; } -static GAppInfo * -g_desktop_app_info_dup (GAppInfo *appinfo) +/* Search global state {{{2 + * + * We only ever search under a global lock, so we can use (and reuse) + * some global data to reduce allocations made while searching. + * + * In short, we keep around arrays of results that we expand as needed + * (and never shrink). + * + * static_token_results: this is where we append the results for each + * token within a given desktop directory, as we handle it (which is + * a union of all matches for this term) + * + * static_search_results: this is where we build the complete results + * for a single directory (which is an intersection of the matches + * found for each term) + * + * static_total_results: this is where we build the complete results + * across all directories (which is a union of the matches found in + * each directory) + * + * The app_names that enter these tables are always pointer-unique (in + * the sense that string equality is the same as pointer equality). + * This can be guaranteed for two reasons: + * + * - we mask appids so that a given appid will only ever appear within + * the highest-precedence directory that contains it. We never + * return search results from a lower-level directory if a desktop + * file exists in a higher-level one. + * + * - within a given directory, the string is unique because it's the + * key in the hashtable of all app_ids for that directory. + * + * We perform a merging of the results in merge_token_results(). This + * works by ordering the two lists and moving through each of them (at + * the same time) looking for common elements, rejecting uncommon ones. + * "Order" here need not mean any particular thing, as long as it is + * some order. Because of the uniqueness of our strings, we can use + * pointer order. That's what's going on in compare_results() below. + */ +struct search_result { - GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); - GDesktopAppInfo *new_info; - - new_info = g_object_new (G_TYPE_DESKTOP_APP_INFO, NULL); - - new_info->filename = g_strdup (info->filename); - new_info->desktop_id = g_strdup (info->desktop_id); - - new_info->name = g_strdup (info->name); - new_info->fullname = g_strdup (info->fullname); - new_info->comment = g_strdup (info->comment); - new_info->nodisplay = info->nodisplay; - new_info->icon_name = g_strdup (info->icon_name); - if (info->icon) - new_info->icon = g_object_ref (info->icon); - new_info->only_show_in = g_strdupv (info->only_show_in); - new_info->not_show_in = g_strdupv (info->not_show_in); - new_info->try_exec = g_strdup (info->try_exec); - new_info->exec = g_strdup (info->exec); - new_info->binary = g_strdup (info->binary); - new_info->path = g_strdup (info->path); - new_info->hidden = info->hidden; - new_info->terminal = info->terminal; - new_info->startup_notify = info->startup_notify; - - return G_APP_INFO (new_info); -} + const gchar *app_name; + gint category; +}; -static gboolean -g_desktop_app_info_equal (GAppInfo *appinfo1, - GAppInfo *appinfo2) +static struct search_result *static_token_results; +static gint static_token_results_size; +static gint static_token_results_allocated; +static struct search_result *static_search_results; +static gint static_search_results_size; +static gint static_search_results_allocated; +static struct search_result *static_total_results; +static gint static_total_results_size; +static gint static_total_results_allocated; + +/* And some functions for performing nice operations against it */ +static gint +compare_results (gconstpointer a, + gconstpointer b) { - GDesktopAppInfo *info1 = G_DESKTOP_APP_INFO (appinfo1); - GDesktopAppInfo *info2 = G_DESKTOP_APP_INFO (appinfo2); + const struct search_result *ra = a; + const struct search_result *rb = b; - if (info1->desktop_id == NULL || - info2->desktop_id == NULL) - return info1 == info2; + if (ra->app_name < rb->app_name) + return -1; - return strcmp (info1->desktop_id, info2->desktop_id) == 0; + else if (ra->app_name > rb->app_name) + return 1; + + else + return ra->category - rb->category; } -static const char * -g_desktop_app_info_get_id (GAppInfo *appinfo) +static gint +compare_categories (gconstpointer a, + gconstpointer b) { - GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); + const struct search_result *ra = a; + const struct search_result *rb = b; - return info->desktop_id; + return ra->category - rb->category; } -static const char * +static void +add_token_result (const gchar *app_name, + guint16 category) +{ + if G_UNLIKELY (static_token_results_size == static_token_results_allocated) + { + static_token_results_allocated = MAX (16, static_token_results_allocated * 2); + static_token_results = g_renew (struct search_result, static_token_results, static_token_results_allocated); + } + + static_token_results[static_token_results_size].app_name = app_name; + static_token_results[static_token_results_size].category = category; + static_token_results_size++; +} + +static void +merge_token_results (gboolean first) +{ + qsort (static_token_results, static_token_results_size, sizeof (struct search_result), compare_results); + + /* If this is the first token then we are basically merging a list with + * itself -- we only perform de-duplication. + * + * If this is not the first token then we are doing a real merge. + */ + if (first) + { + const gchar *last_name = NULL; + gint i; + + /* We must de-duplicate, but we do so by taking the best category + * in each case. + * + * The final list can be as large as the input here, so make sure + * we have enough room (even if it's too much room). + */ + + if G_UNLIKELY (static_search_results_allocated < static_token_results_size) + { + static_search_results_allocated = static_token_results_allocated; + static_search_results = g_renew (struct search_result, + static_search_results, + static_search_results_allocated); + } + + for (i = 0; i < static_token_results_size; i++) + { + /* The list is sorted so that the best match for a given id + * will be at the front, so once we have copied an id, skip + * the rest of the entries for the same id. + */ + if (static_token_results[i].app_name == last_name) + continue; + + last_name = static_token_results[i].app_name; + + static_search_results[static_search_results_size++] = static_token_results[i]; + } + } + else + { + const gchar *last_name = NULL; + gint i, j = 0; + gint k = 0; + + /* We only ever remove items from the results list, so no need to + * resize to ensure that we have enough room. + */ + for (i = 0; i < static_token_results_size; i++) + { + if (static_token_results[i].app_name == last_name) + continue; + + last_name = static_token_results[i].app_name; + + /* Now we only want to have a result in static_search_results + * if we already have it there *and* we have it in + * static_token_results as well. The category will be the + * lesser of the two. + * + * Skip past the results in static_search_results that are not + * going to be matches. + */ + while (k < static_search_results_size && + static_search_results[k].app_name < static_token_results[i].app_name) + k++; + + if (k < static_search_results_size && + static_search_results[k].app_name == static_token_results[i].app_name) + { + /* We have a match. + * + * Category should be the worse of the two (ie: + * numerically larger). + */ + static_search_results[j].app_name = static_search_results[k].app_name; + static_search_results[j].category = MAX (static_search_results[k].category, + static_token_results[i].category); + j++; + } + } + + static_search_results_size = j; + } + + /* Clear it out for next time... */ + static_token_results_size = 0; +} + +static void +reset_total_search_results (void) +{ + static_total_results_size = 0; +} + +static void +sort_total_search_results (void) +{ + qsort (static_total_results, static_total_results_size, sizeof (struct search_result), compare_categories); +} + +static void +merge_directory_results (void) +{ + if G_UNLIKELY (static_total_results_size + static_search_results_size > static_total_results_allocated) + { + static_total_results_allocated = MAX (16, static_total_results_allocated); + while (static_total_results_allocated < static_total_results_size + static_search_results_size) + static_total_results_allocated *= 2; + static_total_results = g_renew (struct search_result, static_total_results, static_total_results_allocated); + } + + memcpy (static_total_results + static_total_results_size, + static_search_results, + static_search_results_size * sizeof (struct search_result)); + + static_total_results_size += static_search_results_size; + + /* Clear it out for next time... */ + static_search_results_size = 0; +} + +/* Support for unindexed DesktopFileDirs {{{2 */ +static void +get_apps_from_dir (GHashTable **apps, + const char *dirname, + const char *prefix) +{ + const char *basename; + GDir *dir; + + 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, unaliased_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; j < strv_len; 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) +{ + 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_ASSOCIATIONS_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_ASSOCIATIONS_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_ASSOCIATIONS_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_APPLICATIONS_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_APPLICATIONS_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_GROUP, 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_GROUP, 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_GROUP, 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_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_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 (); + dir->memory_implementations = 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... */ + gchar **implements; + 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); + } + + /* 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], 0, app); + g_strfreev (implements); + } + + 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)); + } +} + +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)); +} + +/* 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->alternatively_watching) + { + g_free (dir->alternatively_watching); + dir->alternatively_watching = NULL; + } + + 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; + } + + if (dir->memory_implementations) + { + g_hash_table_unref (dir->memory_implementations); + dir->memory_implementations = 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) +{ + const gchar *watch_dir; + + g_assert (!dir->is_setup); + + g_assert (!dir->alternatively_watching); + g_assert (!dir->monitor); + + dir->alternatively_watching = desktop_file_dir_get_alternative_dir (dir); + watch_dir = dir->alternatively_watching ? dir->alternatively_watching : dir->path; + + /* There is a very thin race here if the watch_dir has been _removed_ + * between when we checked for it and when we establish the watch. + * Removes probably don't happen in usual operation, and even if it + * does (and we catch the unlikely race), the only degradation is that + * we will fall back to polling. + */ + dir->monitor = g_local_directory_monitor_new_in_worker (watch_dir, 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 + * + * 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); +} + +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 +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)) + { + g_object_unref (info); + return NULL; + } + return info; +} + +/** + * g_desktop_app_info_new_from_filename: + * @filename: the path of a desktop file, in the GLib filename encoding + * + * Creates a new #GDesktopAppInfo. + * + * Returns: a new #GDesktopAppInfo or %NULL on error. + **/ +GDesktopAppInfo * +g_desktop_app_info_new_from_filename (const char *filename) +{ + GDesktopAppInfo *info = NULL; + + info = g_object_new (G_TYPE_DESKTOP_APP_INFO, "filename", filename, NULL); + if (!g_desktop_app_info_load_file (info)) + { + g_object_unref (info); + return NULL; + } + return info; +} + +/** + * g_desktop_app_info_new: + * @desktop_id: the desktop file id + * + * Creates a new #GDesktopAppInfo based on a desktop file id. + * + * A desktop file id is the basename of the desktop file, including the + * .desktop extension. GIO is looking for a desktop file with this name + * in the `applications` subdirectories of the XDG + * data directories (i.e. the directories specified in the `XDG_DATA_HOME` + * and `XDG_DATA_DIRS` environment variables). GIO also supports the + * prefix-to-subdirectory mapping that is described in the + * [Menu Spec](http://standards.freedesktop.org/menu-spec/latest/) + * (i.e. a desktop id of kde-foo.desktop will match + * `/usr/share/applications/kde/foo.desktop`). + * + * Returns: a new #GDesktopAppInfo, or %NULL if no desktop file with that id + */ +GDesktopAppInfo * +g_desktop_app_info_new (const char *desktop_id) +{ + GDesktopAppInfo *appinfo = NULL; + guint i; + + desktop_file_dirs_lock (); + + for (i = 0; i < n_desktop_file_dirs; i++) + { + appinfo = desktop_file_dir_get_app (&desktop_file_dirs[i], desktop_id); + + if (appinfo) + break; + } + + desktop_file_dirs_unlock (); + + if (appinfo == NULL) + return NULL; + + g_free (appinfo->desktop_id); + appinfo->desktop_id = g_strdup (desktop_id); + + if (g_desktop_app_info_get_is_hidden (appinfo)) + { + g_object_unref (appinfo); + appinfo = NULL; + } + + return appinfo; +} + +static GAppInfo * +g_desktop_app_info_dup (GAppInfo *appinfo) +{ + GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); + GDesktopAppInfo *new_info; + + new_info = g_object_new (G_TYPE_DESKTOP_APP_INFO, NULL); + + new_info->filename = g_strdup (info->filename); + new_info->desktop_id = g_strdup (info->desktop_id); + + if (info->keyfile) + new_info->keyfile = g_key_file_ref (info->keyfile); + + new_info->name = g_strdup (info->name); + new_info->generic_name = g_strdup (info->generic_name); + new_info->fullname = g_strdup (info->fullname); + new_info->keywords = g_strdupv (info->keywords); + new_info->comment = g_strdup (info->comment); + new_info->nodisplay = info->nodisplay; + new_info->icon_name = g_strdup (info->icon_name); + if (info->icon) + new_info->icon = g_object_ref (info->icon); + new_info->only_show_in = g_strdupv (info->only_show_in); + new_info->not_show_in = g_strdupv (info->not_show_in); + new_info->try_exec = g_strdup (info->try_exec); + new_info->exec = g_strdup (info->exec); + new_info->binary = g_strdup (info->binary); + new_info->path = g_strdup (info->path); + new_info->app_id = g_strdup (info->app_id); + new_info->hidden = info->hidden; + new_info->terminal = info->terminal; + new_info->startup_notify = info->startup_notify; + + return G_APP_INFO (new_info); +} + +/* GAppInfo interface implementation functions {{{2 */ + +static gboolean +g_desktop_app_info_equal (GAppInfo *appinfo1, + GAppInfo *appinfo2) +{ + GDesktopAppInfo *info1 = G_DESKTOP_APP_INFO (appinfo1); + GDesktopAppInfo *info2 = G_DESKTOP_APP_INFO (appinfo2); + + if (info1->desktop_id == NULL || + info2->desktop_id == NULL) + return info1 == info2; + + return strcmp (info1->desktop_id, info2->desktop_id) == 0; +} + +static const char * +g_desktop_app_info_get_id (GAppInfo *appinfo) +{ + GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); + + return info->desktop_id; +} + +static const char * g_desktop_app_info_get_name (GAppInfo *appinfo) { GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); @@ -498,7 +2017,7 @@ g_desktop_app_info_get_display_name (GAppInfo *appinfo) * A desktop file is hidden if the Hidden key in it is * set to True. * - * Returns: %TRUE if hidden, %FALSE otherwise. + * Returns: %TRUE if hidden, %FALSE otherwise. **/ gboolean g_desktop_app_info_get_is_hidden (GDesktopAppInfo *info) @@ -527,7 +2046,7 @@ static const char * g_desktop_app_info_get_description (GAppInfo *appinfo) { GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); - + return info->comment; } @@ -535,7 +2054,7 @@ static const char * g_desktop_app_info_get_executable (GAppInfo *appinfo) { GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); - + return info->binary; } @@ -543,7 +2062,7 @@ static const char * g_desktop_app_info_get_commandline (GAppInfo *appinfo) { GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); - + return info->exec; } @@ -555,57 +2074,181 @@ g_desktop_app_info_get_icon (GAppInfo *appinfo) return info->icon; } +/** + * g_desktop_app_info_get_categories: + * @info: a #GDesktopAppInfo + * + * Gets the categories from the desktop file. + * + * Returns: The unparsed Categories key from the desktop file; + * i.e. no attempt is made to split it by ';' or validate it. + */ +const char * +g_desktop_app_info_get_categories (GDesktopAppInfo *info) +{ + return info->categories; +} + +/** + * g_desktop_app_info_get_keywords: + * @info: a #GDesktopAppInfo + * + * Gets the keywords from the desktop file. + * + * Returns: (transfer none): The value of the Keywords key + * + * Since: 2.32 + */ +const char * const * +g_desktop_app_info_get_keywords (GDesktopAppInfo *info) +{ + return (const char * const *)info->keywords; +} + +/** + * g_desktop_app_info_get_generic_name: + * @info: a #GDesktopAppInfo + * + * Gets the generic name from the destkop file. + * + * Returns: The value of the GenericName key + */ +const char * +g_desktop_app_info_get_generic_name (GDesktopAppInfo *info) +{ + return info->generic_name; +} + +/** + * g_desktop_app_info_get_nodisplay: + * @info: a #GDesktopAppInfo + * + * Gets the value of the NoDisplay key, which helps determine if the + * application info should be shown in menus. See + * #G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY and g_app_info_should_show(). + * + * Returns: The value of the NoDisplay key + * + * Since: 2.30 + */ +gboolean +g_desktop_app_info_get_nodisplay (GDesktopAppInfo *info) +{ + return info->nodisplay; +} + +/** + * g_desktop_app_info_get_show_in: + * @info: a #GDesktopAppInfo + * @desktop_env: (nullable): a string specifying a desktop name + * + * Checks if the application info should be shown in menus that list available + * applications for a specific name of the desktop, based on the + * `OnlyShowIn` and `NotShowIn` keys. + * + * @desktop_env should typically be given as %NULL, in which case the + * `XDG_CURRENT_DESKTOP` environment variable is consulted. If you want + * to override the default mechanism then you may specify @desktop_env, + * but this is not recommended. + * + * Note that g_app_info_should_show() for @info will include this check (with + * %NULL for @desktop_env) as well as additional checks. + * + * Returns: %TRUE if the @info should be shown in @desktop_env according to the + * `OnlyShowIn` and `NotShowIn` keys, %FALSE + * otherwise. + * + * Since: 2.30 + */ +gboolean +g_desktop_app_info_get_show_in (GDesktopAppInfo *info, + const gchar *desktop_env) +{ + const gchar *specified_envs[] = { desktop_env, NULL }; + const gchar * const *envs; + gint i; + + g_return_val_if_fail (G_IS_DESKTOP_APP_INFO (info), FALSE); + + if (desktop_env) + envs = specified_envs; + else + envs = get_current_desktops (NULL); + + for (i = 0; envs[i]; i++) + { + gint j; + + if (info->only_show_in) + for (j = 0; info->only_show_in[j]; j++) + if (g_str_equal (info->only_show_in[j], envs[i])) + return TRUE; + + if (info->not_show_in) + for (j = 0; info->not_show_in[j]; j++) + if (g_str_equal (info->not_show_in[j], envs[i])) + return FALSE; + } + + return info->only_show_in == NULL; +} + +/* Launching... {{{2 */ + static char * expand_macro_single (char macro, char *uri) { GFile *file; char *result = NULL; - char *path, *name; + char *path = NULL; + char *name; file = g_file_new_for_uri (uri); - path = g_file_get_path (file); - g_object_unref (file); - + switch (macro) { case 'u': - case 'U': + case 'U': result = g_shell_quote (uri); break; case 'f': case 'F': + path = g_file_get_path (file); if (path) - result = g_shell_quote (path); + result = g_shell_quote (path); break; case 'd': case 'D': + path = g_file_get_path (file); if (path) { name = g_path_get_dirname (path); - result = g_shell_quote (name); + result = g_shell_quote (name); g_free (name); } break; case 'n': case 'N': + path = g_file_get_path (file); if (path) { name = g_path_get_basename (path); - result = g_shell_quote (name); + result = g_shell_quote (name); g_free (name); } break; } + g_object_unref (file); g_free (path); - + return result; } static void -expand_macro (char macro, - GString *exec, - GDesktopAppInfo *info, +expand_macro (char macro, + GString *exec, + GDesktopAppInfo *info, GList **uri_list) { GList *uris = *uri_list; @@ -626,18 +2269,18 @@ expand_macro (char macro, if (!info->no_fuse) { switch (macro) - { - case 'u': - force_file_uri_macro = 'f'; - force_file_uri = TRUE; - break; - case 'U': - force_file_uri_macro = 'F'; - force_file_uri = TRUE; - break; - default: - break; - } + { + case 'u': + force_file_uri_macro = 'f'; + force_file_uri = TRUE; + break; + case 'U': + force_file_uri_macro = 'F'; + force_file_uri = TRUE; + break; + default: + break; + } } switch (macro) @@ -647,11 +2290,11 @@ expand_macro (char macro, case 'd': case 'n': if (uris) - { - uri = uris->data; + { + uri = uris->data; if (!force_file_uri || - /* Pass URI if it contains an anchor */ - strchr (uri, '#') != NULL) + /* Pass URI if it contains an anchor */ + strchr (uri, '#') != NULL) { expanded = expand_macro_single (macro, uri); } @@ -662,27 +2305,27 @@ expand_macro (char macro, expanded = expand_macro_single (macro, uri); } - if (expanded) - { - g_string_append (exec, expanded); - g_free (expanded); - } - uris = uris->next; - } + if (expanded) + { + g_string_append (exec, expanded); + g_free (expanded); + } + uris = uris->next; + } break; - case 'U': + case 'U': case 'F': case 'D': case 'N': while (uris) - { - uri = uris->data; - + { + uri = uris->data; + if (!force_file_uri || - /* Pass URI if it contains an anchor */ - strchr (uri, '#') != NULL) + /* Pass URI if it contains an anchor */ + strchr (uri, '#') != NULL) { expanded = expand_macro_single (macro, uri); } @@ -693,44 +2336,44 @@ expand_macro (char macro, expanded = expand_macro_single (macro, uri); } - if (expanded) - { - g_string_append (exec, expanded); - g_free (expanded); - } - - uris = uris->next; - - if (uris != NULL && expanded) - g_string_append_c (exec, ' '); - } + if (expanded) + { + g_string_append (exec, expanded); + g_free (expanded); + } + + uris = uris->next; + + if (uris != NULL && expanded) + g_string_append_c (exec, ' '); + } break; case 'i': if (info->icon_name) - { - g_string_append (exec, "--icon "); + { + g_string_append (exec, "--icon "); expanded = g_shell_quote (info->icon_name); - g_string_append (exec, expanded); + g_string_append (exec, expanded); g_free (expanded); - } + } break; case 'c': - if (info->name) + if (info->name) { expanded = g_shell_quote (info->name); - g_string_append (exec, expanded); + g_string_append (exec, expanded); g_free (expanded); } break; case 'k': - if (info->filename) + if (info->filename) { expanded = g_shell_quote (info->filename); - g_string_append (exec, expanded); + g_string_append (exec, expanded); g_free (expanded); } break; @@ -742,23 +2385,24 @@ expand_macro (char macro, g_string_append_c (exec, '%'); break; } - + *uri_list = uris; } static gboolean expand_application_parameters (GDesktopAppInfo *info, - GList **uris, - int *argc, - char ***argv, - GError **error) + const gchar *exec_line, + GList **uris, + int *argc, + char ***argv, + GError **error) { GList *uri_list = *uris; - const char *p = info->exec; + const char *p = exec_line; GString *expanded_exec; gboolean res; - if (info->exec == NULL) + if (exec_line == NULL) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("Desktop file didn't specify Exec field")); @@ -770,12 +2414,12 @@ expand_application_parameters (GDesktopAppInfo *info, while (*p) { if (p[0] == '%' && p[1] != '\0') - { - expand_macro (p[1], expanded_exec, info, uris); - p++; - } + { + expand_macro (p[1], expanded_exec, info, uris); + p++; + } else - g_string_append_c (expanded_exec, *p); + g_string_append_c (expanded_exec, *p); p++; } @@ -795,7 +2439,7 @@ expand_application_parameters (GDesktopAppInfo *info, static gboolean prepend_terminal_to_vector (int *argc, - char ***argv) + char ***argv) { #ifndef G_OS_WIN32 char **real_argv; @@ -805,24 +2449,24 @@ prepend_terminal_to_vector (int *argc, int term_argc = 0; char *check; char **the_argv; - + g_return_val_if_fail (argc != NULL, FALSE); g_return_val_if_fail (argv != NULL, FALSE); - + /* sanity */ if(*argv == NULL) *argc = 0; - + the_argv = *argv; /* compute size if not given */ if (*argc < 0) { for (i = 0; the_argv[i] != NULL; i++) - ; + ; *argc = i; } - + term_argc = 2; term_argv = g_new0 (char *, 3); @@ -837,39 +2481,39 @@ prepend_terminal_to_vector (int *argc, else { if (check == NULL) - check = g_find_program_in_path ("nxterm"); + check = g_find_program_in_path ("nxterm"); if (check == NULL) - check = g_find_program_in_path ("color-xterm"); + check = g_find_program_in_path ("color-xterm"); if (check == NULL) - check = g_find_program_in_path ("rxvt"); + check = g_find_program_in_path ("rxvt"); if (check == NULL) - check = g_find_program_in_path ("xterm"); + check = g_find_program_in_path ("xterm"); if (check == NULL) - check = g_find_program_in_path ("dtterm"); + check = g_find_program_in_path ("dtterm"); if (check == NULL) - { - check = g_strdup ("xterm"); - g_warning ("couldn't find a terminal, falling back to xterm"); - } + { + check = g_strdup ("xterm"); + g_warning ("couldn't find a terminal, falling back to xterm"); + } term_argv[0] = check; term_argv[1] = g_strdup ("-e"); } real_argc = term_argc + *argc; real_argv = g_new (char *, real_argc + 1); - + for (i = 0; i < term_argc; i++) real_argv[i] = term_argv[i]; - + for (j = 0; j < *argc; j++, i++) real_argv[i] = (char *)the_argv[j]; - + real_argv[i] = NULL; - + g_free (*argv); *argv = real_argv; *argc = real_argc; - + /* we use g_free here as we sucked all the inner strings * out from it into real_argv */ g_free (term_argv); @@ -900,9 +2544,8 @@ typedef struct { GSpawnChildSetupFunc user_setup; gpointer user_setup_data; - char *display; - char *sn_id; - char *desktop_file; + + char *pid_envvar; } ChildSetupData; static void @@ -910,20 +2553,22 @@ child_setup (gpointer user_data) { ChildSetupData *data = user_data; - if (data->display) - g_setenv ("DISPLAY", data->display, TRUE); - - if (data->sn_id) - g_setenv ("DESKTOP_STARTUP_ID", data->sn_id, TRUE); - - if (data->desktop_file) + if (data->pid_envvar) { - gchar pid[20]; - - g_setenv ("GIO_LAUNCHED_DESKTOP_FILE", data->desktop_file, TRUE); - - g_snprintf (pid, 20, "%ld", (long)getpid ()); - g_setenv ("GIO_LAUNCHED_DESKTOP_FILE_PID", pid, TRUE); + pid_t pid = getpid (); + char buf[20]; + int i; + + /* Write the pid into the space already reserved for it in the + * environment array. We can't use sprintf because it might + * malloc, so we do it by hand. It's simplest to write the pid + * out backwards first, then copy it over. + */ + for (i = 0; pid; i++, pid /= 10) + buf[i] = (pid % 10) + '0'; + for (i--; i >= 0; i--) + *(data->pid_envvar++) = buf[i]; + *data->pid_envvar = '\0'; } if (data->user_setup) @@ -932,17 +2577,18 @@ child_setup (gpointer user_data) static void notify_desktop_launch (GDBusConnection *session_bus, - GDesktopAppInfo *info, - long pid, - const char *display, - const char *sn_id, - GList *uris) + GDesktopAppInfo *info, + long pid, + const char *display, + const char *sn_id, + GList *uris) { GDBusMessage *msg; GVariantBuilder uri_variant; GVariantBuilder extras_variant; GList *iter; const char *desktop_file_id; + const char *gio_desktop_file; if (session_bus == NULL) return; @@ -954,9 +2600,22 @@ notify_desktop_launch (GDBusConnection *session_bus, g_variant_builder_init (&extras_variant, G_VARIANT_TYPE ("a{sv}")); if (sn_id != NULL && g_utf8_validate (sn_id, -1, NULL)) g_variant_builder_add (&extras_variant, "{sv}", - "startup-id", - g_variant_new ("s", - sn_id)); + "startup-id", + g_variant_new ("s", + sn_id)); + gio_desktop_file = g_getenv ("GIO_LAUNCHED_DESKTOP_FILE"); + if (gio_desktop_file != NULL) + g_variant_builder_add (&extras_variant, "{sv}", + "origin-desktop-file", + g_variant_new_bytestring (gio_desktop_file)); + if (g_get_prgname () != NULL) + g_variant_builder_add (&extras_variant, "{sv}", + "origin-prgname", + g_variant_new_bytestring (g_get_prgname ())); + g_variant_builder_add (&extras_variant, "{sv}", + "origin-pid", + g_variant_new ("x", + (gint64)getpid ())); if (info->filename) desktop_file_id = info->filename; @@ -964,127 +2623,166 @@ notify_desktop_launch (GDBusConnection *session_bus, desktop_file_id = info->desktop_id; else desktop_file_id = ""; - + msg = g_dbus_message_new_signal ("/org/gtk/gio/DesktopAppInfo", - "org.gtk.gio.DesktopAppInfo", - "Launched"); + "org.gtk.gio.DesktopAppInfo", + "Launched"); g_dbus_message_set_body (msg, g_variant_new ("(@aysxasa{sv})", - g_variant_new_bytestring (desktop_file_id), - display ? display : "", - (gint64)pid, - &uri_variant, - &extras_variant)); + g_variant_new_bytestring (desktop_file_id), + display ? display : "", + (gint64)pid, + &uri_variant, + &extras_variant)); g_dbus_connection_send_message (session_bus, - msg, 0, - NULL, - NULL); + msg, 0, + NULL, + NULL); g_object_unref (msg); } #define _SPAWN_FLAGS_DEFAULT (G_SPAWN_SEARCH_PATH) static gboolean -_g_desktop_app_info_launch_uris_internal (GAppInfo *appinfo, - GList *uris, - GAppLaunchContext *launch_context, - GSpawnFlags spawn_flags, - GSpawnChildSetupFunc user_setup, - gpointer user_setup_data, - GDesktopAppLaunchCallback pid_callback, - gpointer pid_callback_data, - GError **error) +g_desktop_app_info_launch_uris_with_spawn (GDesktopAppInfo *info, + GDBusConnection *session_bus, + const gchar *exec_line, + GList *uris, + GAppLaunchContext *launch_context, + GSpawnFlags spawn_flags, + GSpawnChildSetupFunc user_setup, + gpointer user_setup_data, + GDesktopAppLaunchCallback pid_callback, + gpointer pid_callback_data, + GError **error) { - GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); - GDBusConnection *session_bus; gboolean completed = FALSE; GList *old_uris; - char **argv; + char **argv, **envp; int argc; ChildSetupData data; - g_return_val_if_fail (appinfo != NULL, FALSE); + g_return_val_if_fail (info != NULL, FALSE); argv = NULL; - session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + if (launch_context) + envp = g_app_launch_context_get_environment (launch_context); + else + envp = g_get_environ (); do { GPid pid; GList *launched_uris; GList *iter; + char *display, *sn_id = NULL; old_uris = uris; - if (!expand_application_parameters (info, &uris, - &argc, &argv, error)) - goto out; + if (!expand_application_parameters (info, exec_line, &uris, &argc, &argv, error)) + goto out; /* Get the subset of URIs we're launching with this process */ launched_uris = NULL; for (iter = old_uris; iter != NULL && iter != uris; iter = iter->next) - launched_uris = g_list_prepend (launched_uris, iter->data); + launched_uris = g_list_prepend (launched_uris, iter->data); launched_uris = g_list_reverse (launched_uris); - + if (info->terminal && !prepend_terminal_to_vector (&argc, &argv)) - { - g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("Unable to find terminal required for application")); - goto out; - } + goto out; + } data.user_setup = user_setup; data.user_setup_data = user_setup_data; - data.display = NULL; - data.sn_id = NULL; - data.desktop_file = info->filename; + if (info->filename) + { + envp = g_environ_setenv (envp, + "GIO_LAUNCHED_DESKTOP_FILE", + info->filename, + TRUE); + envp = g_environ_setenv (envp, + "GIO_LAUNCHED_DESKTOP_FILE_PID", + "XXXXXXXXXXXXXXXXXXXX", /* filled in child_setup */ + TRUE); + data.pid_envvar = (char *)g_environ_getenv (envp, "GIO_LAUNCHED_DESKTOP_FILE_PID"); + } + else + { + data.pid_envvar = NULL; + } + + display = NULL; + sn_id = NULL; if (launch_context) - { - GList *launched_files = create_files_for_uris (launched_uris); + { + GList *launched_files = create_files_for_uris (launched_uris); - data.display = g_app_launch_context_get_display (launch_context, - appinfo, - launched_files); + display = g_app_launch_context_get_display (launch_context, + G_APP_INFO (info), + launched_files); + if (display) + envp = g_environ_setenv (envp, "DISPLAY", display, TRUE); - if (info->startup_notify) - data.sn_id = g_app_launch_context_get_startup_notify_id (launch_context, - appinfo, - launched_files); - g_list_foreach (launched_files, (GFunc)g_object_unref, NULL); - g_list_free (launched_files); - } + if (info->startup_notify) + { + sn_id = g_app_launch_context_get_startup_notify_id (launch_context, + G_APP_INFO (info), + launched_files); + if (sn_id) + envp = g_environ_setenv (envp, "DESKTOP_STARTUP_ID", sn_id, TRUE); + } + + g_list_free_full (launched_files, g_object_unref); + } if (!g_spawn_async (info->path, - argv, - NULL, - spawn_flags, - child_setup, - &data, - &pid, - error)) - { - if (data.sn_id) - g_app_launch_context_launch_failed (launch_context, data.sn_id); - - g_free (data.sn_id); - g_free (data.display); - g_list_free (launched_uris); - - goto out; - } + argv, + envp, + spawn_flags, + child_setup, + &data, + &pid, + error)) + { + if (sn_id) + g_app_launch_context_launch_failed (launch_context, sn_id); + + g_free (display); + g_free (sn_id); + g_list_free (launched_uris); + + goto out; + } if (pid_callback != NULL) - pid_callback (info, pid, pid_callback_data); + pid_callback (info, pid, pid_callback_data); + + if (launch_context != NULL) + { + GVariantBuilder builder; + GVariant *platform_data; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + g_variant_builder_add (&builder, "{sv}", "pid", g_variant_new_int32 (pid)); + if (sn_id) + g_variant_builder_add (&builder, "{sv}", "startup-notification-id", g_variant_new_string (sn_id)); + platform_data = g_variant_ref_sink (g_variant_builder_end (&builder)); + g_signal_emit_by_name (launch_context, "launched", info, platform_data); + g_variant_unref (platform_data); + } notify_desktop_launch (session_bus, - info, - pid, - data.display, - data.sn_id, - launched_uris); - - g_free (data.sn_id); - g_free (data.display); + info, + pid, + display, + sn_id, + launched_uris); + + g_free (display); + g_free (sn_id); g_list_free (launched_uris); g_strfreev (argv); @@ -1092,39 +2790,159 @@ _g_desktop_app_info_launch_uris_internal (GAppInfo *appinfo, } while (uris != NULL); - /* TODO - need to handle the process exiting immediately - * after launching an app. See http://bugzilla.gnome.org/606960 - */ - if (session_bus != NULL) - g_object_unref (session_bus); - completed = TRUE; out: g_strfreev (argv); + g_strfreev (envp); return completed; } +static gchar * +object_path_from_appid (const gchar *app_id) +{ + gchar *path; + gint i, n; + + n = strlen (app_id); + path = g_malloc (n + 2); + + path[0] = '/'; + + for (i = 0; i < n; i++) + if (app_id[i] != '.') + path[i + 1] = app_id[i]; + else + path[i + 1] = '/'; + + path[i + 1] = '\0'; + + return path; +} + +static GVariant * +g_desktop_app_info_make_platform_data (GDesktopAppInfo *info, + GList *uris, + GAppLaunchContext *launch_context) +{ + GVariantBuilder builder; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT); + + if (launch_context) + { + GList *launched_files = create_files_for_uris (uris); + + if (info->startup_notify) + { + gchar *sn_id; + + sn_id = g_app_launch_context_get_startup_notify_id (launch_context, G_APP_INFO (info), launched_files); + if (sn_id) + g_variant_builder_add (&builder, "{sv}", "desktop-startup-id", g_variant_new_take_string (sn_id)); + } + + g_list_free_full (launched_files, g_object_unref); + } + + return g_variant_builder_end (&builder); +} + +static gboolean +g_desktop_app_info_launch_uris_with_dbus (GDesktopAppInfo *info, + GDBusConnection *session_bus, + GList *uris, + GAppLaunchContext *launch_context) +{ + GVariantBuilder builder; + gchar *object_path; + + g_return_val_if_fail (info != NULL, FALSE); + + g_variant_builder_init (&builder, G_VARIANT_TYPE_TUPLE); + + if (uris) + { + GList *iter; + + g_variant_builder_open (&builder, G_VARIANT_TYPE_STRING_ARRAY); + for (iter = uris; iter; iter = iter->next) + g_variant_builder_add (&builder, "s", iter->data); + g_variant_builder_close (&builder); + } + + g_variant_builder_add_value (&builder, g_desktop_app_info_make_platform_data (info, uris, launch_context)); + + /* 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... + */ + object_path = object_path_from_appid (info->app_id); + g_dbus_connection_call (session_bus, info->app_id, object_path, "org.freedesktop.Application", + uris ? "Open" : "Activate", g_variant_builder_end (&builder), + NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + g_free (object_path); + + return TRUE; +} + +static gboolean +g_desktop_app_info_launch_uris_internal (GAppInfo *appinfo, + GList *uris, + GAppLaunchContext *launch_context, + GSpawnFlags spawn_flags, + GSpawnChildSetupFunc user_setup, + gpointer user_setup_data, + GDesktopAppLaunchCallback pid_callback, + gpointer pid_callback_data, + GError **error) +{ + GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); + GDBusConnection *session_bus; + gboolean success = TRUE; + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + + if (session_bus && info->app_id) + g_desktop_app_info_launch_uris_with_dbus (info, session_bus, uris, launch_context); + 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, error); + + if (session_bus != NULL) + { + /* This asynchronous flush holds a reference until it completes, + * which ensures that the following unref won't immediately kill + * the connection if we were the initial owner. + */ + g_dbus_connection_flush (session_bus, NULL, NULL, NULL); + g_object_unref (session_bus); + } + + return success; +} + static gboolean g_desktop_app_info_launch_uris (GAppInfo *appinfo, - GList *uris, - GAppLaunchContext *launch_context, - GError **error) + GList *uris, + GAppLaunchContext *launch_context, + GError **error) { - return _g_desktop_app_info_launch_uris_internal (appinfo, uris, - launch_context, - _SPAWN_FLAGS_DEFAULT, - NULL, NULL, NULL, NULL, - error); + return g_desktop_app_info_launch_uris_internal (appinfo, uris, + launch_context, + _SPAWN_FLAGS_DEFAULT, + NULL, NULL, NULL, NULL, + error); } static gboolean g_desktop_app_info_supports_uris (GAppInfo *appinfo) { GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); - - return info->exec && + + return info->exec && ((strstr (info->exec, "%u") != NULL) || (strstr (info->exec, "%U") != NULL)); } @@ -1133,17 +2951,17 @@ static gboolean g_desktop_app_info_supports_files (GAppInfo *appinfo) { GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); - - return info->exec && + + return info->exec && ((strstr (info->exec, "%f") != NULL) || (strstr (info->exec, "%F") != NULL)); } static gboolean g_desktop_app_info_launch (GAppInfo *appinfo, - GList *files, - GAppLaunchContext *launch_context, - GError **error) + GList *files, + GAppLaunchContext *launch_context, + GError **error) { GList *uris; char *uri; @@ -1156,14 +2974,13 @@ g_desktop_app_info_launch (GAppInfo *appinfo, uris = g_list_prepend (uris, uri); files = files->next; } - + uris = g_list_reverse (uris); - + res = g_desktop_app_info_launch_uris (appinfo, uris, launch_context, error); - - g_list_foreach (uris, (GFunc)g_free, NULL); - g_list_free (uris); - + + g_list_free_full (uris, g_free); + return res; } @@ -1171,131 +2988,92 @@ g_desktop_app_info_launch (GAppInfo *appinfo, * g_desktop_app_info_launch_uris_as_manager: * @appinfo: a #GDesktopAppInfo * @uris: (element-type utf8): List of URIs - * @launch_context: a #GAppLaunchContext + * @launch_context: (allow-none): a #GAppLaunchContext * @spawn_flags: #GSpawnFlags, used for each process - * @user_setup: (scope call): a #GSpawnChildSetupFunc, used once for - * each process. - * @user_setup_data: (closure user_setup): User data for @user_setup - * @pid_callback: (scope call): Callback for child processes - * @pid_callback_data: (closure pid_callback): User data for @callback - * @error: a #GError + * @user_setup: (scope call) (allow-none): a #GSpawnChildSetupFunc, used once + * for each process. + * @user_setup_data: (closure user_setup) (allow-none): User data for @user_setup + * @pid_callback: (scope call) (allow-none): Callback for child processes + * @pid_callback_data: (closure pid_callback) (allow-none): User data for @callback + * @error: return location for a #GError, or %NULL * * This function performs the equivalent of g_app_info_launch_uris(), * but is intended primarily for operating system components that * launch applications. Ordinary applications should use * g_app_info_launch_uris(). * - * In contrast to g_app_info_launch_uris(), all processes created will - * always be run directly as children as if by the UNIX fork()/exec() - * calls. + * If the application is launched via traditional UNIX fork()/exec() + * then @spawn_flags, @user_setup and @user_setup_data are used for the + * call to g_spawn_async(). Additionally, @pid_callback (with + * @pid_callback_data) will be called to inform about the PID of the + * created process. * - * This guarantee allows additional control over the exact environment - * of the child processes, which is provided via a setup function - * @setup, as well as the process identifier of each child process via - * @pid_callback. See g_spawn_async() for more information about the - * semantics of the @setup function. + * If application launching occurs via some other mechanism (eg: D-Bus + * activation) then @spawn_flags, @user_setup, @user_setup_data, + * @pid_callback and @pid_callback_data are ignored. + * + * Returns: %TRUE on successful launch, %FALSE otherwise. */ gboolean g_desktop_app_info_launch_uris_as_manager (GDesktopAppInfo *appinfo, - GList *uris, - GAppLaunchContext *launch_context, - GSpawnFlags spawn_flags, - GSpawnChildSetupFunc user_setup, - gpointer user_setup_data, - GDesktopAppLaunchCallback pid_callback, - gpointer pid_callback_data, - GError **error) -{ - return _g_desktop_app_info_launch_uris_internal ((GAppInfo*)appinfo, - uris, - launch_context, - spawn_flags, - user_setup, - user_setup_data, - pid_callback, - pid_callback_data, - error); -} - -G_LOCK_DEFINE_STATIC (g_desktop_env); -static gchar *g_desktop_env = NULL; + GList *uris, + GAppLaunchContext *launch_context, + GSpawnFlags spawn_flags, + GSpawnChildSetupFunc user_setup, + gpointer user_setup_data, + GDesktopAppLaunchCallback pid_callback, + gpointer pid_callback_data, + GError **error) +{ + return g_desktop_app_info_launch_uris_internal ((GAppInfo*)appinfo, + uris, + launch_context, + spawn_flags, + user_setup, + user_setup_data, + pid_callback, + pid_callback_data, + error); +} + +/* OnlyShowIn API support {{{2 */ /** * g_desktop_app_info_set_desktop_env: * @desktop_env: a string specifying what desktop this is * * Sets the name of the desktop that the application is running in. - * This is used by g_app_info_should_show() to evaluate the - * OnlyShowIn and NotShowIn + * This is used by g_app_info_should_show() and + * g_desktop_app_info_get_show_in() to evaluate the + * `OnlyShowIn` and `NotShowIn` * desktop entry fields. * - * The Desktop - * Menu specification recognizes the following: - * - * GNOME - * KDE - * ROX - * XFCE - * Old - * - * * Should be called only once; subsequent calls are ignored. + * + * Deprecated:2.42:do not use this API. Since 2.42 the value of the + * `XDG_CURRENT_DESKTOP` environment variable will be used. */ void g_desktop_app_info_set_desktop_env (const gchar *desktop_env) { - G_LOCK (g_desktop_env); - if (!g_desktop_env) - g_desktop_env = g_strdup (desktop_env); - G_UNLOCK (g_desktop_env); + get_current_desktops (desktop_env); } static gboolean g_desktop_app_info_should_show (GAppInfo *appinfo) { GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); - gboolean found; - const gchar *desktop_env; - int i; if (info->nodisplay) return FALSE; - G_LOCK (g_desktop_env); - desktop_env = g_desktop_env; - G_UNLOCK (g_desktop_env); - - if (info->only_show_in) - { - if (desktop_env == NULL) - return FALSE; - - found = FALSE; - for (i = 0; info->only_show_in[i] != NULL; i++) - { - if (strcmp (info->only_show_in[i], desktop_env) == 0) - { - found = TRUE; - break; - } - } - if (!found) - return FALSE; - } - - if (info->not_show_in && desktop_env) - { - for (i = 0; info->not_show_in[i] != NULL; i++) - { - if (strcmp (info->not_show_in[i], desktop_env) == 0) - return FALSE; - } - } - - return TRUE; + return g_desktop_app_info_get_show_in (info, NULL); } +/* mime types/default apps support {{{2 */ + typedef enum { + CONF_DIR, APP_DIR, MIMETYPE_DIR } DirType; @@ -1307,10 +3085,23 @@ ensure_dir (DirType type, char *path, *display_name; int errsv; - if (type == APP_DIR) - path = g_build_filename (g_get_user_data_dir (), "applications", NULL); - else - path = g_build_filename (g_get_user_data_dir (), "mime", "packages", NULL); + switch (type) + { + case CONF_DIR: + path = g_build_filename (g_get_user_config_dir (), NULL); + break; + + case APP_DIR: + path = g_build_filename (g_get_user_data_dir (), "applications", NULL); + break; + + case MIMETYPE_DIR: + path = g_build_filename (g_get_user_data_dir (), "mime", "packages", NULL); + break; + + default: + g_assert_not_reached (); + } errno = 0; if (g_mkdir_with_parents (path, 0700) == 0) @@ -1334,16 +3125,15 @@ ensure_dir (DirType type, } static gboolean -update_mimeapps_list (const char *desktop_id, - const char *content_type, +update_mimeapps_list (const char *desktop_id, + const char *content_type, UpdateMimeFlags flags, - GError **error) + GError **error) { char *dirname, *filename, *string; GKeyFile *key_file; - gboolean load_succeeded, res, explicit_default; + gboolean load_succeeded, res; char **old_list, **list; - GList *system_list; gsize length, data_size; char *data; int i, j, k; @@ -1353,7 +3143,7 @@ update_mimeapps_list (const char *desktop_id, g_assert (!((flags & UPDATE_MIME_SET_DEFAULT) && (flags & UPDATE_MIME_SET_NON_DEFAULT))); - dirname = ensure_dir (APP_DIR, error); + dirname = ensure_dir (CONF_DIR, error); if (!dirname) return FALSE; @@ -1362,7 +3152,10 @@ update_mimeapps_list (const char *desktop_id, key_file = g_key_file_new (); load_succeeded = g_key_file_load_from_file (key_file, filename, G_KEY_FILE_NONE, NULL); - if (!load_succeeded || !g_key_file_has_group (key_file, ADDED_ASSOCIATIONS_GROUP)) + if (!load_succeeded || + (!g_key_file_has_group (key_file, ADDED_ASSOCIATIONS_GROUP) && + !g_key_file_has_group (key_file, REMOVED_ASSOCIATIONS_GROUP) && + !g_key_file_has_group (key_file, DEFAULT_APPLICATIONS_GROUP))) { g_key_file_free (key_file); key_file = g_key_file_new (); @@ -1379,8 +3172,6 @@ update_mimeapps_list (const char *desktop_id, content_types = g_key_file_get_keys (key_file, DEFAULT_APPLICATIONS_GROUP, NULL, NULL); } - explicit_default = FALSE; - for (k = 0; content_types && content_types[k]; k++) { /* set as default, if requested so */ @@ -1405,14 +3196,10 @@ update_mimeapps_list (const char *desktop_id, content_types[k], NULL); else - { - g_key_file_set_string (key_file, - DEFAULT_APPLICATIONS_GROUP, - content_types[k], - string); - - explicit_default = TRUE; - } + g_key_file_set_string (key_file, + DEFAULT_APPLICATIONS_GROUP, + content_types[k], + string); g_free (string); } @@ -1426,14 +3213,14 @@ update_mimeapps_list (const char *desktop_id, g_strfreev (content_types); content_types = g_key_file_get_keys (key_file, ADDED_ASSOCIATIONS_GROUP, NULL, NULL); } - + for (k = 0; content_types && content_types[k]; k++) - { + { /* Add to the right place in the list */ - + length = 0; old_list = g_key_file_get_string_list (key_file, ADDED_ASSOCIATIONS_GROUP, - content_types[k], &length, NULL); + content_types[k], &length, NULL); list = g_new (char *, 1 + length + 1); @@ -1452,21 +3239,21 @@ update_mimeapps_list (const char *desktop_id, if (old_list) { for (j = 0; old_list[j] != NULL; j++) - { - if (g_strcmp0 (old_list[j], desktop_id) != 0) + { + if (g_strcmp0 (old_list[j], desktop_id) != 0) { /* rewrite other entries if they're different from the new one */ list[i++] = g_strdup (old_list[j]); } - else if (flags & UPDATE_MIME_SET_NON_DEFAULT) - { + else if (flags & UPDATE_MIME_SET_NON_DEFAULT) + { /* we encountered an old entry which is equal to the one we're adding as non-default, * don't change its position in the list. */ - flags ^= UPDATE_MIME_SET_NON_DEFAULT; - list[i++] = g_strdup (old_list[j]); - } - } + flags ^= UPDATE_MIME_SET_NON_DEFAULT; + list[i++] = g_strdup (old_list[j]); + } + } } /* add it at the end of the list */ @@ -1474,45 +3261,23 @@ update_mimeapps_list (const char *desktop_id, list[i++] = g_strdup (desktop_id); list[i] = NULL; - + g_strfreev (old_list); if (list[0] == NULL || desktop_id == NULL) g_key_file_remove_key (key_file, - ADDED_ASSOCIATIONS_GROUP, - content_types[k], - NULL); + ADDED_ASSOCIATIONS_GROUP, + content_types[k], + NULL); else - { - g_key_file_set_string_list (key_file, - ADDED_ASSOCIATIONS_GROUP, - content_types[k], - (const char * const *)list, i); - - /* if we had no explicit default set, we should add the system default to the - * list, to avoid overriding it with applications from this list. - */ - if (!explicit_default) - { - system_list = get_all_desktop_entries_for_mime_type (content_type, (const char **) list, FALSE, NULL); - - if (system_list != NULL) - { - string = system_list->data; - - g_key_file_set_string (key_file, - DEFAULT_APPLICATIONS_GROUP, - content_types[k], - string); - } + g_key_file_set_string_list (key_file, + ADDED_ASSOCIATIONS_GROUP, + content_types[k], + (const char * const *)list, i); - g_list_free_full (system_list, g_free); - } - } - g_strfreev (list); } - + if (content_type) { /* reuse the list from above */ @@ -1523,13 +3288,13 @@ update_mimeapps_list (const char *desktop_id, content_types = g_key_file_get_keys (key_file, REMOVED_ASSOCIATIONS_GROUP, NULL, NULL); } - for (k = 0; content_types && content_types[k]; k++) + for (k = 0; content_types && content_types[k]; k++) { /* Remove from removed associations group (unless remove) */ - + length = 0; old_list = g_key_file_get_string_list (key_file, REMOVED_ASSOCIATIONS_GROUP, - content_types[k], &length, NULL); + content_types[k], &length, NULL); list = g_new (char *, 1 + length + 1); @@ -1539,41 +3304,41 @@ update_mimeapps_list (const char *desktop_id, if (old_list) { for (j = 0; old_list[j] != NULL; j++) - { - if (g_strcmp0 (old_list[j], desktop_id) != 0) - list[i++] = g_strdup (old_list[j]); - } + { + if (g_strcmp0 (old_list[j], desktop_id) != 0) + list[i++] = g_strdup (old_list[j]); + } } list[i] = NULL; - + g_strfreev (old_list); if (list[0] == NULL || desktop_id == NULL) g_key_file_remove_key (key_file, - REMOVED_ASSOCIATIONS_GROUP, - content_types[k], - NULL); + REMOVED_ASSOCIATIONS_GROUP, + content_types[k], + NULL); else g_key_file_set_string_list (key_file, - REMOVED_ASSOCIATIONS_GROUP, - content_types[k], - (const char * const *)list, i); + REMOVED_ASSOCIATIONS_GROUP, + content_types[k], + (const char * const *)list, i); g_strfreev (list); } - g_strfreev (content_types); + g_strfreev (content_types); data = g_key_file_to_data (key_file, &data_size, error); g_key_file_free (key_file); - + res = g_file_set_contents (filename, data, data_size, error); - mime_info_cache_reload (NULL); - + desktop_file_dirs_invalidate_user_config (); + g_free (filename); g_free (data); - + return res; } @@ -1587,6 +3352,13 @@ g_desktop_app_info_set_as_last_used_for_type (GAppInfo *appinfo, if (!g_desktop_app_info_ensure_saved (info, error)) return FALSE; + if (!info->desktop_id) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Application information lacks an identifier")); + return FALSE; + } + /* both add support for the content type and set as last used */ return update_mimeapps_list (info->desktop_id, content_type, UPDATE_MIME_SET_NON_DEFAULT | @@ -1596,14 +3368,21 @@ g_desktop_app_info_set_as_last_used_for_type (GAppInfo *appinfo, static gboolean g_desktop_app_info_set_as_default_for_type (GAppInfo *appinfo, - const char *content_type, - GError **error) + const char *content_type, + GError **error) { GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); if (!g_desktop_app_info_ensure_saved (info, error)) - return FALSE; - + return FALSE; + + if (!info->desktop_id) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Application information lacks an identifier")); + return FALSE; + } + return update_mimeapps_list (info->desktop_id, content_type, UPDATE_MIME_SET_DEFAULT, error); @@ -1611,12 +3390,11 @@ g_desktop_app_info_set_as_default_for_type (GAppInfo *appinfo, static void update_program_done (GPid pid, - gint status, - gpointer data) + gint status, + gpointer data) { /* Did the application exit correctly */ - if (WIFEXITED (status) && - WEXITSTATUS (status) == 0) + if (g_spawn_check_exit_status (status, NULL)) { /* Here we could clean out any caches in use */ } @@ -1624,67 +3402,67 @@ update_program_done (GPid pid, static void run_update_command (char *command, - char *subdir) -{ - char *argv[3] = { - NULL, - NULL, - NULL, - }; - GPid pid = 0; - GError *error = NULL; - - argv[0] = command; - argv[1] = g_build_filename (g_get_user_data_dir (), subdir, NULL); - - if (g_spawn_async ("/", argv, - NULL, /* envp */ - G_SPAWN_SEARCH_PATH | - G_SPAWN_STDOUT_TO_DEV_NULL | - G_SPAWN_STDERR_TO_DEV_NULL | - G_SPAWN_DO_NOT_REAP_CHILD, - NULL, NULL, /* No setup function */ - &pid, - &error)) - g_child_watch_add (pid, update_program_done, NULL); - else - { - /* If we get an error at this point, it's quite likely the user doesn't - * have an installed copy of either 'update-mime-database' or - * 'update-desktop-database'. I don't think we want to popup an error - * dialog at this point, so we just do a g_warning to give the user a - * chance of debugging it. - */ - g_warning ("%s", error->message); - } - - g_free (argv[1]); + char *subdir) +{ + char *argv[3] = { + NULL, + NULL, + NULL, + }; + GPid pid = 0; + GError *error = NULL; + + argv[0] = command; + argv[1] = g_build_filename (g_get_user_data_dir (), subdir, NULL); + + if (g_spawn_async ("/", argv, + NULL, /* envp */ + G_SPAWN_SEARCH_PATH | + G_SPAWN_STDOUT_TO_DEV_NULL | + G_SPAWN_STDERR_TO_DEV_NULL | + G_SPAWN_DO_NOT_REAP_CHILD, + NULL, NULL, /* No setup function */ + &pid, + &error)) + g_child_watch_add (pid, update_program_done, NULL); + else + { + /* If we get an error at this point, it's quite likely the user doesn't + * have an installed copy of either 'update-mime-database' or + * 'update-desktop-database'. I don't think we want to popup an error + * dialog at this point, so we just do a g_warning to give the user a + * chance of debugging it. + */ + g_warning ("%s", error->message); + } + + g_free (argv[1]); } static gboolean g_desktop_app_info_set_as_default_for_extension (GAppInfo *appinfo, - const char *extension, - GError **error) + const char *extension, + GError **error) { char *filename, *basename, *mimetype; char *dirname; gboolean res; if (!g_desktop_app_info_ensure_saved (G_DESKTOP_APP_INFO (appinfo), error)) - return FALSE; - + return FALSE; + dirname = ensure_dir (MIMETYPE_DIR, error); if (!dirname) return FALSE; - + basename = g_strdup_printf ("user-extension-%s.xml", extension); filename = g_build_filename (dirname, basename, NULL); g_free (basename); g_free (dirname); mimetype = g_strdup_printf ("application/x-extension-%s", extension); - - if (!g_file_test (filename, G_FILE_TEST_EXISTS)) + + if (!g_file_test (filename, G_FILE_TEST_EXISTS)) { char *contents; @@ -1703,26 +3481,26 @@ g_desktop_app_info_set_as_default_for_extension (GAppInfo *appinfo, run_update_command ("update-mime-database", "mime"); } g_free (filename); - + res = g_desktop_app_info_set_as_default_for_type (appinfo, - mimetype, - error); + mimetype, + error); g_free (mimetype); - + return res; } static gboolean g_desktop_app_info_add_supports_type (GAppInfo *appinfo, - const char *content_type, - GError **error) + const char *content_type, + GError **error) { GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); if (!g_desktop_app_info_ensure_saved (G_DESKTOP_APP_INFO (info), error)) - return FALSE; - + return FALSE; + return update_mimeapps_list (info->desktop_id, content_type, UPDATE_MIME_SET_NON_DEFAULT, error); @@ -1736,22 +3514,32 @@ g_desktop_app_info_can_remove_supports_type (GAppInfo *appinfo) static gboolean g_desktop_app_info_remove_supports_type (GAppInfo *appinfo, - const char *content_type, - GError **error) + const char *content_type, + GError **error) { GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); if (!g_desktop_app_info_ensure_saved (G_DESKTOP_APP_INFO (info), error)) return FALSE; - + return update_mimeapps_list (info->desktop_id, content_type, UPDATE_MIME_REMOVE, error); } +static const char ** +g_desktop_app_info_get_supported_types (GAppInfo *appinfo) +{ + GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); + + return (const char**) info->mime_types; +} + +/* Saving and deleting {{{2 */ + static gboolean g_desktop_app_info_ensure_saved (GDesktopAppInfo *info, - GError **error) + GError **error) { GKeyFile *key_file; char *dirname; @@ -1760,7 +3548,7 @@ g_desktop_app_info_ensure_saved (GDesktopAppInfo *info, gsize data_size; int fd; gboolean res; - + if (info->filename != NULL) return TRUE; @@ -1768,39 +3556,46 @@ g_desktop_app_info_ensure_saved (GDesktopAppInfo *info, * g_app_info_create_from_commandline. All other * object should have a filename */ - + dirname = ensure_dir (APP_DIR, error); if (!dirname) return FALSE; - + key_file = g_key_file_new (); g_key_file_set_string (key_file, G_KEY_FILE_DESKTOP_GROUP, - "Encoding", "UTF-8"); + "Encoding", "UTF-8"); g_key_file_set_string (key_file, G_KEY_FILE_DESKTOP_GROUP, - G_KEY_FILE_DESKTOP_KEY_VERSION, "1.0"); + G_KEY_FILE_DESKTOP_KEY_VERSION, "1.0"); g_key_file_set_string (key_file, G_KEY_FILE_DESKTOP_GROUP, - G_KEY_FILE_DESKTOP_KEY_TYPE, + G_KEY_FILE_DESKTOP_KEY_TYPE, G_KEY_FILE_DESKTOP_TYPE_APPLICATION); - if (info->terminal) + if (info->terminal) + g_key_file_set_boolean (key_file, G_KEY_FILE_DESKTOP_GROUP, + G_KEY_FILE_DESKTOP_KEY_TERMINAL, TRUE); + if (info->nodisplay) g_key_file_set_boolean (key_file, G_KEY_FILE_DESKTOP_GROUP, - G_KEY_FILE_DESKTOP_KEY_TERMINAL, TRUE); + G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY, TRUE); g_key_file_set_string (key_file, G_KEY_FILE_DESKTOP_GROUP, - G_KEY_FILE_DESKTOP_KEY_EXEC, info->exec); + G_KEY_FILE_DESKTOP_KEY_EXEC, info->exec); g_key_file_set_string (key_file, G_KEY_FILE_DESKTOP_GROUP, - G_KEY_FILE_DESKTOP_KEY_NAME, info->name); + G_KEY_FILE_DESKTOP_KEY_NAME, info->name); + + if (info->generic_name != NULL) + g_key_file_set_string (key_file, G_KEY_FILE_DESKTOP_GROUP, + GENERIC_NAME_KEY, info->generic_name); if (info->fullname != NULL) g_key_file_set_string (key_file, G_KEY_FILE_DESKTOP_GROUP, - FULL_NAME_KEY, info->fullname); + FULL_NAME_KEY, info->fullname); g_key_file_set_string (key_file, G_KEY_FILE_DESKTOP_GROUP, - G_KEY_FILE_DESKTOP_KEY_COMMENT, info->comment); - + G_KEY_FILE_DESKTOP_KEY_COMMENT, info->comment); + g_key_file_set_boolean (key_file, G_KEY_FILE_DESKTOP_GROUP, - G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY, TRUE); + G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY, TRUE); data = g_key_file_to_data (key_file, &data_size, NULL); g_key_file_free (key_file); @@ -1809,7 +3604,7 @@ g_desktop_app_info_ensure_saved (GDesktopAppInfo *info, filename = g_build_filename (dirname, desktop_id, NULL); g_free (desktop_id); g_free (dirname); - + fd = g_mkstemp (filename); if (fd == -1) { @@ -1817,7 +3612,7 @@ g_desktop_app_info_ensure_saved (GDesktopAppInfo *info, display_name = g_filename_display_name (filename); g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, - _("Can't create user desktop file %s"), display_name); + _("Can't create user desktop file %s"), display_name); g_free (display_name); g_free (filename); g_free (data); @@ -1826,9 +3621,11 @@ g_desktop_app_info_ensure_saved (GDesktopAppInfo *info, desktop_id = g_path_get_basename (filename); - close (fd); - + /* FIXME - actually handle error */ + (void) g_close (fd, NULL); + res = g_file_set_contents (filename, data, data_size, error); + g_free (data); if (!res) { g_free (desktop_id); @@ -1838,9 +3635,18 @@ g_desktop_app_info_ensure_saved (GDesktopAppInfo *info, info->filename = filename; info->desktop_id = desktop_id; - + run_update_command ("update-desktop-database", "applications"); - + + /* We just dropped a file in the user's desktop file directory. Save + * the monitor the bother of having to notice it and invalidate + * immediately. + * + * This means that calls directly following this will be able to see + * the results immediately. + */ + desktop_file_dirs_invalidate_user_data (); + return TRUE; } @@ -1862,9 +3668,9 @@ static gboolean g_desktop_app_info_delete (GAppInfo *appinfo) { GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); - + if (info->filename) - { + { if (g_remove (info->filename) == 0) { update_mimeapps_list (info->desktop_id, NULL, @@ -1883,22 +3689,29 @@ g_desktop_app_info_delete (GAppInfo *appinfo) return FALSE; } +/* Create for commandline {{{2 */ /** * g_app_info_create_from_commandline: * @commandline: the commandline to use * @application_name: (allow-none): the application name, or %NULL to use @commandline * @flags: flags that can specify details of the created #GAppInfo - * @error: a #GError location to store the error occuring, %NULL to ignore. + * @error: a #GError location to store the error occurring, %NULL to ignore. * * Creates a new #GAppInfo from the given information. * + * Note that for @commandline, the quoting rules of the Exec key of the + * [freedesktop.org Desktop Entry Specification](http://freedesktop.org/Standards/desktop-entry-spec) + * are applied. For example, if the @commandline contains + * percent-encoded URIs, the percent-character must be doubled in order to prevent it from + * being swallowed by Exec key unquoting. See the specification for exact quoting rules. + * * Returns: (transfer full): new #GAppInfo for given command. **/ GAppInfo * g_app_info_create_from_commandline (const char *commandline, - const char *application_name, - GAppInfoCreateFlags flags, - GError **error) + const char *application_name, + GAppInfoCreateFlags flags, + GError **error) { char **split; char *basename; @@ -1910,17 +3723,17 @@ g_app_info_create_from_commandline (const char *commandline, info->filename = NULL; info->desktop_id = NULL; - - info->terminal = flags & G_APP_INFO_CREATE_NEEDS_TERMINAL; - info->startup_notify = flags & G_APP_INFO_CREATE_SUPPORTS_STARTUP_NOTIFICATION; + + info->terminal = (flags & G_APP_INFO_CREATE_NEEDS_TERMINAL) != 0; + info->startup_notify = (flags & G_APP_INFO_CREATE_SUPPORTS_STARTUP_NOTIFICATION) != 0; info->hidden = FALSE; - if (flags & G_APP_INFO_CREATE_SUPPORTS_URIS) + if ((flags & G_APP_INFO_CREATE_SUPPORTS_URIS) != 0) info->exec = g_strconcat (commandline, " %u", NULL); else info->exec = g_strconcat (commandline, " %f", NULL); info->nodisplay = TRUE; info->binary = binary_from_exec (info->exec); - + if (application_name) info->name = g_strdup (application_name); else @@ -1931,13 +3744,15 @@ g_app_info_create_from_commandline (const char *commandline, g_strfreev (split); info->name = basename; if (info->name == NULL) - info->name = g_strdup ("custom"); + info->name = g_strdup ("custom"); } info->comment = g_strdup_printf (_("Custom definition for %s"), info->name); - + return G_APP_INFO (info); } +/* GAppInfo interface init */ + static void g_desktop_app_info_iface_init (GAppInfoIface *iface) { @@ -1963,30 +3778,115 @@ g_desktop_app_info_iface_init (GAppInfoIface *iface) iface->get_commandline = g_desktop_app_info_get_commandline; iface->get_display_name = g_desktop_app_info_get_display_name; iface->set_as_last_used_for_type = g_desktop_app_info_set_as_last_used_for_type; + iface->get_supported_types = g_desktop_app_info_get_supported_types; } -static gboolean -app_info_in_list (GAppInfo *info, - GList *list) +/* Recommended applications {{{2 */ + +/* Converts content_type into a list of itself with all of its parent + * types (if include_fallback is enabled) or just returns a single-item + * list with the unaliased content type. + */ +static gchar ** +get_list_of_mimetypes (const gchar *content_type, + gboolean include_fallback) { - while (list != NULL) + gchar *unaliased; + GPtrArray *array; + + array = g_ptr_array_new (); + unaliased = _g_unix_content_type_unalias (content_type); + g_ptr_array_add (array, unaliased); + + if (include_fallback) { - if (g_app_info_equal (info, list->data)) - return TRUE; - list = list->next; + gint i; + + /* Iterate the array as we grow it, until we have nothing more to add */ + for (i = 0; i < array->len; i++) + { + gchar **parents = _g_unix_content_type_get_parents (g_ptr_array_index (array, i)); + gint j; + + for (j = 0; parents[j]; j++) + /* Don't add duplicates */ + if (!array_contains (array, parents[j])) + g_ptr_array_add (array, parents[j]); + else + g_free (parents[j]); + + /* We already stole or freed each element. Free the container. */ + g_free (parents); + } } - return FALSE; + + g_ptr_array_add (array, NULL); + + return (gchar **) g_ptr_array_free (array, FALSE); +} + +static gchar ** +g_desktop_app_info_get_desktop_ids_for_content_type (const gchar *content_type, + gboolean include_fallback) +{ + GPtrArray *hits, *blacklist; + gchar **types; + gint i, j; + + hits = g_ptr_array_new (); + blacklist = g_ptr_array_new (); + + types = get_list_of_mimetypes (content_type, include_fallback); + + desktop_file_dirs_lock (); + + for (i = 0; types[i]; i++) + for (j = 0; j < n_desktop_file_dirs; j++) + desktop_file_dir_mime_lookup (&desktop_file_dirs[j], types[i], hits, blacklist); + + desktop_file_dirs_unlock (); + + g_ptr_array_add (hits, NULL); + + g_ptr_array_free (blacklist, TRUE); + g_strfreev (types); + + return (gchar **) g_ptr_array_free (hits, FALSE); +} + +static gchar ** +g_desktop_app_info_get_defaults_for_content_type (const gchar *content_type) +{ + GPtrArray *results; + gchar **types; + gint i, j; + + types = get_list_of_mimetypes (content_type, TRUE); + results = g_ptr_array_new (); + + desktop_file_dirs_lock (); + + for (i = 0; types[i]; i++) + for (j = 0; j < n_desktop_file_dirs; j++) + desktop_file_dir_default_lookup (&desktop_file_dirs[j], types[i], results); + + desktop_file_dirs_unlock (); + + g_ptr_array_add (results, NULL); + g_strfreev (types); + + return (gchar **) g_ptr_array_free (results, FALSE); } /** * g_app_info_get_recommended_for_type: * @content_type: the content type to find a #GAppInfo for - * + * * Gets a list of recommended #GAppInfos for a given content type, i.e. * those applications which claim to support the given content type exactly, * and not by MIME type subclassing. * Note that the first application of the list is the last used one, i.e. - * the last one for which #g_app_info_set_as_last_used_for_type has been + * the last one for which g_app_info_set_as_last_used_for_type() has been * called. * * Returns: (element-type GAppInfo) (transfer full): #GList of #GAppInfos @@ -1997,31 +3897,25 @@ app_info_in_list (GAppInfo *info, GList * g_app_info_get_recommended_for_type (const gchar *content_type) { - GList *desktop_entries, *l; + gchar **desktop_ids; GList *infos; - GDesktopAppInfo *info; + gint i; g_return_val_if_fail (content_type != NULL, NULL); - desktop_entries = get_all_desktop_entries_for_mime_type (content_type, NULL, FALSE, NULL); + desktop_ids = g_desktop_app_info_get_desktop_ids_for_content_type (content_type, FALSE); infos = NULL; - for (l = desktop_entries; l != NULL; l = l->next) + for (i = 0; desktop_ids[i]; i++) { - char *desktop_entry = l->data; + GDesktopAppInfo *info; - info = g_desktop_app_info_new (desktop_entry); + info = g_desktop_app_info_new (desktop_ids[i]); if (info) - { - if (app_info_in_list (G_APP_INFO (info), infos)) - g_object_unref (info); - else - infos = g_list_prepend (infos, info); - } - g_free (desktop_entry); + infos = g_list_prepend (infos, info); } - g_list_free (desktop_entries); + g_strfreev (desktop_ids); return g_list_reverse (infos); } @@ -2029,7 +3923,7 @@ g_app_info_get_recommended_for_type (const gchar *content_type) /** * g_app_info_get_fallback_for_type: * @content_type: the content type to find a #GAppInfo for - * + * * Gets a list of fallback #GAppInfos for a given content type, i.e. * those applications which claim to support the given content type * by MIME type subclassing and not directly. @@ -2042,34 +3936,38 @@ g_app_info_get_recommended_for_type (const gchar *content_type) GList * g_app_info_get_fallback_for_type (const gchar *content_type) { - GList *desktop_entries, *l; - GList *infos, *recommended_infos; - GDesktopAppInfo *info; + gchar **recommended_ids; + gchar **all_ids; + GList *infos; + gint i; g_return_val_if_fail (content_type != NULL, NULL); - desktop_entries = get_all_desktop_entries_for_mime_type (content_type, NULL, TRUE, NULL); - recommended_infos = g_app_info_get_recommended_for_type (content_type); + recommended_ids = g_desktop_app_info_get_desktop_ids_for_content_type (content_type, FALSE); + all_ids = g_desktop_app_info_get_desktop_ids_for_content_type (content_type, TRUE); infos = NULL; - for (l = desktop_entries; l != NULL; l = l->next) + for (i = 0; all_ids[i]; i++) { - char *desktop_entry = l->data; + GDesktopAppInfo *info; + gint j; + + /* Don't return the ones on the recommended list */ + for (j = 0; recommended_ids[j]; j++) + if (g_str_equal (all_ids[i], recommended_ids[j])) + break; + + if (recommended_ids[j]) + continue; + + info = g_desktop_app_info_new (all_ids[i]); - info = g_desktop_app_info_new (desktop_entry); if (info) - { - if (app_info_in_list (G_APP_INFO (info), infos) || - app_info_in_list (G_APP_INFO (info), recommended_infos)) - g_object_unref (info); - else - infos = g_list_prepend (infos, info); - } - g_free (desktop_entry); + infos = g_list_prepend (infos, info); } - g_list_free (desktop_entries); - g_list_free_full (recommended_infos, g_object_unref); + g_strfreev (recommended_ids); + g_strfreev (all_ids); return g_list_reverse (infos); } @@ -2077,8 +3975,11 @@ g_app_info_get_fallback_for_type (const gchar *content_type) /** * g_app_info_get_all_for_type: * @content_type: the content type to find a #GAppInfo for - * - * Gets a list of all #GAppInfos for a given content type. + * + * Gets a list of all #GAppInfos for a given content type, + * including the recommended and fallback #GAppInfos. See + * g_app_info_get_recommended_for_type() and + * g_app_info_get_fallback_for_type(). * * Returns: (element-type GAppInfo) (transfer full): #GList of #GAppInfos * for given @content_type or %NULL on error. @@ -2086,55 +3987,38 @@ g_app_info_get_fallback_for_type (const gchar *content_type) GList * g_app_info_get_all_for_type (const char *content_type) { - GList *desktop_entries, *l; + gchar **desktop_ids; GList *infos; - char *user_default = NULL; - GDesktopAppInfo *info; + gint i; g_return_val_if_fail (content_type != NULL, NULL); - - desktop_entries = get_all_desktop_entries_for_mime_type (content_type, NULL, TRUE, &user_default); - infos = NULL; - - /* put the user default in front of the list, for compatibility */ - if (user_default != NULL) - { - info = g_desktop_app_info_new (user_default); - - if (info != NULL) - infos = g_list_prepend (infos, info); - } - g_free (user_default); + desktop_ids = g_desktop_app_info_get_desktop_ids_for_content_type (content_type, TRUE); - for (l = desktop_entries; l != NULL; l = l->next) + infos = NULL; + for (i = 0; desktop_ids[i]; i++) { - char *desktop_entry = l->data; + GDesktopAppInfo *info; - info = g_desktop_app_info_new (desktop_entry); + info = g_desktop_app_info_new (desktop_ids[i]); if (info) - { - if (app_info_in_list (G_APP_INFO (info), infos)) - g_object_unref (info); - else - infos = g_list_prepend (infos, info); - } - g_free (desktop_entry); + infos = g_list_prepend (infos, info); } - g_list_free (desktop_entries); - + g_strfreev (desktop_ids); + return g_list_reverse (infos); } /** * g_app_info_reset_type_associations: - * @content_type: a content type + * @content_type: a content type * * Removes all changes to the type associations done by - * g_app_info_set_as_default_for_type(), - * g_app_info_set_as_default_for_extension(), - * g_app_info_add_supports_type() or g_app_info_remove_supports_type(). + * g_app_info_set_as_default_for_type(), + * g_app_info_set_as_default_for_extension(), + * g_app_info_add_supports_type() or + * g_app_info_remove_supports_type(). * * Since: 2.20 */ @@ -2151,69 +4035,66 @@ g_app_info_reset_type_associations (const char *content_type) * @content_type: the content type to find a #GAppInfo for * @must_support_uris: if %TRUE, the #GAppInfo is expected to * support URIs - * - * Gets the #GAppInfo that corresponds to a given content type. + * + * Gets the default #GAppInfo for a given content type. * * Returns: (transfer full): #GAppInfo for given @content_type or * %NULL on error. - **/ + */ GAppInfo * g_app_info_get_default_for_type (const char *content_type, - gboolean must_support_uris) + gboolean must_support_uris) { - GList *desktop_entries, *l; - char *user_default = NULL; + gchar **desktop_ids; GAppInfo *info; + gint i; g_return_val_if_fail (content_type != NULL, NULL); - - desktop_entries = get_all_desktop_entries_for_mime_type (content_type, NULL, TRUE, &user_default); - info = NULL; + desktop_ids = g_desktop_app_info_get_defaults_for_content_type (content_type); - if (user_default != NULL) + info = NULL; + for (i = 0; desktop_ids[i]; i++) { - info = (GAppInfo *) g_desktop_app_info_new (user_default); + info = (GAppInfo *) g_desktop_app_info_new (desktop_ids[i]); if (info) { - if (must_support_uris && !g_app_info_supports_uris (info)) - { - g_object_unref (info); - info = NULL; - } + if (!must_support_uris || g_app_info_supports_uris (info)) + break; + + g_object_unref (info); + info = NULL; } } - g_free (user_default); - - if (info != NULL) - { - g_list_free_full (desktop_entries, g_free); - return info; - } + g_strfreev (desktop_ids); - /* pick the first from the other list that matches our URI - * requirements. + /* If we can't find a default app for this content type, pick one from + * the list of all supported apps. This will be ordered by the user's + * preference and by "recommended" apps first, so the first one we + * find is probably the best fallback. */ - for (l = desktop_entries; l != NULL; l = l->next) + if (info == NULL) { - char *desktop_entry = l->data; + desktop_ids = g_desktop_app_info_get_desktop_ids_for_content_type (content_type, TRUE); - info = (GAppInfo *)g_desktop_app_info_new (desktop_entry); - if (info) - { - if (must_support_uris && !g_app_info_supports_uris (info)) - { - g_object_unref (info); - info = NULL; - } - else - break; - } - } - - g_list_free_full (desktop_entries, g_free); + for (i = 0; desktop_ids[i]; i++) + { + info = (GAppInfo *) g_desktop_app_info_new (desktop_ids[i]); + + if (info) + { + if (!must_support_uris || g_app_info_supports_uris (info)) + break; + + g_object_unref (info); + info = NULL; + } + } + + g_strfreev (desktop_ids); + } return info; } @@ -2222,13 +4103,13 @@ g_app_info_get_default_for_type (const char *content_type, * g_app_info_get_default_for_uri_scheme: * @uri_scheme: a string containing a URI scheme. * - * Gets the default application for launching applications - * using this URI scheme. A URI scheme is the initial part - * of the URI, up to but not including the ':', e.g. "http", + * Gets the default application for handling URIs with + * the given URI scheme. A URI scheme is the initial part + * of the URI, up to but not including the ':', e.g. "http", * "ftp" or "sip". - * + * * Returns: (transfer full): #GAppInfo for given @uri_scheme or %NULL on error. - **/ + */ GAppInfo * g_app_info_get_default_for_uri_scheme (const char *uri_scheme) { @@ -2237,864 +4118,197 @@ g_app_info_get_default_for_uri_scheme (const char *uri_scheme) scheme_down = g_ascii_strdown (uri_scheme, -1); content_type = g_strdup_printf ("x-scheme-handler/%s", scheme_down); - g_free (scheme_down); - app_info = g_app_info_get_default_for_type (content_type, FALSE); - g_free (content_type); - - return app_info; -} - -static void -get_apps_from_dir (GHashTable *apps, - const char *dirname, - const char *prefix) -{ - GDir *dir; - const char *basename; - char *filename, *subprefix, *desktop_id; - gboolean hidden; - GDesktopAppInfo *appinfo; - - dir = g_dir_open (dirname, 0, NULL); - if (dir) - { - while ((basename = g_dir_read_name (dir)) != NULL) - { - filename = g_build_filename (dirname, basename, NULL); - if (g_str_has_suffix (basename, ".desktop")) - { - desktop_id = g_strconcat (prefix, basename, NULL); - - /* Use _extended so we catch NULLs too (hidden) */ - if (!g_hash_table_lookup_extended (apps, desktop_id, NULL, NULL)) - { - appinfo = g_desktop_app_info_new_from_filename (filename); - hidden = FALSE; - - if (appinfo && g_desktop_app_info_get_is_hidden (appinfo)) - { - g_object_unref (appinfo); - appinfo = NULL; - hidden = TRUE; - } - - if (appinfo || hidden) - { - g_hash_table_insert (apps, g_strdup (desktop_id), appinfo); - - if (appinfo) - { - /* Reuse instead of strdup here */ - appinfo->desktop_id = desktop_id; - desktop_id = NULL; - } - } - } - g_free (desktop_id); - } - else - { - if (g_file_test (filename, G_FILE_TEST_IS_DIR)) - { - subprefix = g_strconcat (prefix, basename, "-", NULL); - get_apps_from_dir (apps, filename, subprefix); - g_free (subprefix); - } - } - g_free (filename); - } - g_dir_close (dir); - } -} - - -/** - * g_app_info_get_all: - * - * Gets a list of all of the applications currently registered - * on this system. - * - * For desktop files, this includes applications that have - * NoDisplay=true set or are excluded from - * display by means of OnlyShowIn or - * NotShowIn. See g_app_info_should_show(). - * The returned list does not include applications which have - * the Hidden key set. - * - * Returns: (element-type GAppInfo) (transfer full): a newly allocated #GList of references to #GAppInfos. - **/ -GList * -g_app_info_get_all (void) -{ - const char * const *dirs; - GHashTable *apps; - GHashTableIter iter; - gpointer value; - int i; - GList *infos; - - dirs = get_applications_search_path (); - - apps = g_hash_table_new_full (g_str_hash, g_str_equal, - g_free, NULL); - - - for (i = 0; dirs[i] != NULL; i++) - get_apps_from_dir (apps, dirs[i], ""); - - - infos = NULL; - g_hash_table_iter_init (&iter, apps); - while (g_hash_table_iter_next (&iter, NULL, &value)) - { - if (value) - infos = g_list_prepend (infos, value); - } - - g_hash_table_destroy (apps); - - return g_list_reverse (infos); -} - -/* Cacheing of mimeinfo.cache and defaults.list files */ - -typedef struct { - char *path; - GHashTable *mime_info_cache_map; - GHashTable *defaults_list_map; - GHashTable *mimeapps_list_added_map; - GHashTable *mimeapps_list_removed_map; - GHashTable *mimeapps_list_defaults_map; - time_t mime_info_cache_timestamp; - time_t defaults_list_timestamp; - time_t mimeapps_list_timestamp; -} MimeInfoCacheDir; - -typedef struct { - GList *dirs; /* mimeinfo.cache and defaults.list */ - GHashTable *global_defaults_cache; /* global results of defaults.list lookup and validation */ - time_t last_stat_time; - guint should_ping_mime_monitor : 1; -} MimeInfoCache; - -static MimeInfoCache *mime_info_cache = NULL; -G_LOCK_DEFINE_STATIC (mime_info_cache); - -static void mime_info_cache_dir_add_desktop_entries (MimeInfoCacheDir *dir, - const char *mime_type, - char **new_desktop_file_ids); - -static MimeInfoCache * mime_info_cache_new (void); - -static void -destroy_info_cache_value (gpointer key, - GList *value, - gpointer data) -{ - g_list_foreach (value, (GFunc)g_free, NULL); - g_list_free (value); -} - -static void -destroy_info_cache_map (GHashTable *info_cache_map) -{ - g_hash_table_foreach (info_cache_map, (GHFunc)destroy_info_cache_value, NULL); - g_hash_table_destroy (info_cache_map); -} - -static gboolean -mime_info_cache_dir_out_of_date (MimeInfoCacheDir *dir, - const char *cache_file, - time_t *timestamp) -{ - struct stat buf; - char *filename; - - filename = g_build_filename (dir->path, cache_file, NULL); - - if (g_stat (filename, &buf) < 0) - { - g_free (filename); - return TRUE; - } - g_free (filename); - - if (buf.st_mtime != *timestamp) - return TRUE; - - return FALSE; -} - -/* Call with lock held */ -static gboolean -remove_all (gpointer key, - gpointer value, - gpointer user_data) -{ - return TRUE; -} - - -static void -mime_info_cache_blow_global_cache (void) -{ - g_hash_table_foreach_remove (mime_info_cache->global_defaults_cache, - remove_all, NULL); -} - -static void -mime_info_cache_dir_init (MimeInfoCacheDir *dir) -{ - GError *load_error; - GKeyFile *key_file; - gchar *filename, **mime_types; - int i; - struct stat buf; - - load_error = NULL; - mime_types = NULL; - - if (dir->mime_info_cache_map != NULL && - !mime_info_cache_dir_out_of_date (dir, "mimeinfo.cache", - &dir->mime_info_cache_timestamp)) - return; - - if (dir->mime_info_cache_map != NULL) - destroy_info_cache_map (dir->mime_info_cache_map); - - dir->mime_info_cache_map = g_hash_table_new_full (g_str_hash, g_str_equal, - (GDestroyNotify) g_free, - NULL); - - key_file = g_key_file_new (); - - filename = g_build_filename (dir->path, "mimeinfo.cache", NULL); - - if (g_stat (filename, &buf) < 0) - goto error; - - if (dir->mime_info_cache_timestamp > 0) - mime_info_cache->should_ping_mime_monitor = TRUE; - - dir->mime_info_cache_timestamp = buf.st_mtime; - - g_key_file_load_from_file (key_file, filename, G_KEY_FILE_NONE, &load_error); - - g_free (filename); - filename = NULL; - - if (load_error != NULL) - goto error; - - mime_types = g_key_file_get_keys (key_file, MIME_CACHE_GROUP, - NULL, &load_error); - - if (load_error != NULL) - goto error; - - for (i = 0; mime_types[i] != NULL; i++) - { - gchar **desktop_file_ids; - char *unaliased_type; - desktop_file_ids = g_key_file_get_string_list (key_file, - MIME_CACHE_GROUP, - mime_types[i], - NULL, - NULL); - - if (desktop_file_ids == NULL) - continue; - - unaliased_type = _g_unix_content_type_unalias (mime_types[i]); - mime_info_cache_dir_add_desktop_entries (dir, - unaliased_type, - desktop_file_ids); - g_free (unaliased_type); - - g_strfreev (desktop_file_ids); - } - - g_strfreev (mime_types); - g_key_file_free (key_file); - - return; - error: - g_free (filename); - g_key_file_free (key_file); - - if (mime_types != NULL) - g_strfreev (mime_types); - - if (load_error) - g_error_free (load_error); -} - -static void -mime_info_cache_dir_init_defaults_list (MimeInfoCacheDir *dir) -{ - GKeyFile *key_file; - GError *load_error; - gchar *filename, **mime_types; - char *unaliased_type; - char **desktop_file_ids; - int i; - struct stat buf; - - load_error = NULL; - mime_types = NULL; - - if (dir->defaults_list_map != NULL && - !mime_info_cache_dir_out_of_date (dir, "defaults.list", - &dir->defaults_list_timestamp)) - return; - - if (dir->defaults_list_map != NULL) - g_hash_table_destroy (dir->defaults_list_map); - dir->defaults_list_map = g_hash_table_new_full (g_str_hash, g_str_equal, - g_free, (GDestroyNotify)g_strfreev); - - - key_file = g_key_file_new (); - - filename = g_build_filename (dir->path, "defaults.list", NULL); - if (g_stat (filename, &buf) < 0) - goto error; - - if (dir->defaults_list_timestamp > 0) - mime_info_cache->should_ping_mime_monitor = TRUE; - - dir->defaults_list_timestamp = buf.st_mtime; - - g_key_file_load_from_file (key_file, filename, G_KEY_FILE_NONE, &load_error); - g_free (filename); - filename = NULL; - - if (load_error != NULL) - goto error; - - mime_types = g_key_file_get_keys (key_file, DEFAULT_APPLICATIONS_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_APPLICATIONS_GROUP, - mime_types[i], - NULL, - NULL); - if (desktop_file_ids == NULL) - continue; - - unaliased_type = _g_unix_content_type_unalias (mime_types[i]); - g_hash_table_replace (dir->defaults_list_map, - unaliased_type, - desktop_file_ids); - } - - g_strfreev (mime_types); - } - - g_key_file_free (key_file); - return; - - error: - g_free (filename); - g_key_file_free (key_file); - - if (mime_types != NULL) - g_strfreev (mime_types); - - if (load_error) - g_error_free (load_error); -} - -static void -mime_info_cache_dir_init_mimeapps_list (MimeInfoCacheDir *dir) -{ - GKeyFile *key_file; - GError *load_error; - gchar *filename, **mime_types; - char *unaliased_type; - char **desktop_file_ids; - char *desktop_id; - int i; - struct stat buf; - - load_error = NULL; - mime_types = NULL; + g_free (scheme_down); + app_info = g_app_info_get_default_for_type (content_type, FALSE); + g_free (content_type); - if (dir->mimeapps_list_added_map != NULL && - !mime_info_cache_dir_out_of_date (dir, "mimeapps.list", - &dir->mimeapps_list_timestamp)) - return; - - if (dir->mimeapps_list_added_map != NULL) - g_hash_table_destroy (dir->mimeapps_list_added_map); - dir->mimeapps_list_added_map = g_hash_table_new_full (g_str_hash, g_str_equal, - g_free, (GDestroyNotify)g_strfreev); - - if (dir->mimeapps_list_removed_map != NULL) - g_hash_table_destroy (dir->mimeapps_list_removed_map); - dir->mimeapps_list_removed_map = g_hash_table_new_full (g_str_hash, g_str_equal, - g_free, (GDestroyNotify)g_strfreev); - - if (dir->mimeapps_list_defaults_map != NULL) - g_hash_table_destroy (dir->mimeapps_list_defaults_map); - dir->mimeapps_list_defaults_map = g_hash_table_new_full (g_str_hash, g_str_equal, - g_free, g_free); + return app_info; +} - key_file = g_key_file_new (); - - filename = g_build_filename (dir->path, "mimeapps.list", NULL); - if (g_stat (filename, &buf) < 0) - goto error; +/* "Get all" API {{{2 */ - if (dir->mimeapps_list_timestamp > 0) - mime_info_cache->should_ping_mime_monitor = TRUE; +/** + * 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 #GDesktopAppInfo + * objects. + * + * Since: 2.42 + **/ +GList * +g_desktop_app_info_get_implementations (const gchar *interface) +{ + GList *result = NULL; + GList **ptr; + gint i; - dir->mimeapps_list_timestamp = buf.st_mtime; + desktop_file_dirs_lock (); - g_key_file_load_from_file (key_file, filename, G_KEY_FILE_NONE, &load_error); - g_free (filename); - filename = NULL; + for (i = 0; i < n_desktop_file_dirs; i++) + desktop_file_dir_get_implementations (&desktop_file_dirs[i], &result, interface); - if (load_error != NULL) - goto error; + desktop_file_dirs_unlock (); - mime_types = g_key_file_get_keys (key_file, ADDED_ASSOCIATIONS_GROUP, - NULL, NULL); - if (mime_types != NULL) + ptr = &result; + while (*ptr) { - for (i = 0; mime_types[i] != NULL; i++) - { - desktop_file_ids = g_key_file_get_string_list (key_file, - ADDED_ASSOCIATIONS_GROUP, - mime_types[i], - NULL, - NULL); - if (desktop_file_ids == NULL) - continue; - - unaliased_type = _g_unix_content_type_unalias (mime_types[i]); - g_hash_table_replace (dir->mimeapps_list_added_map, - unaliased_type, - desktop_file_ids); - } - - g_strfreev (mime_types); - } + gchar *name = (*ptr)->data; + GDesktopAppInfo *app; - mime_types = g_key_file_get_keys (key_file, REMOVED_ASSOCIATIONS_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, - REMOVED_ASSOCIATIONS_GROUP, - mime_types[i], - NULL, - NULL); - if (desktop_file_ids == NULL) - continue; - - unaliased_type = _g_unix_content_type_unalias (mime_types[i]); - g_hash_table_replace (dir->mimeapps_list_removed_map, - unaliased_type, - desktop_file_ids); - } - - g_strfreev (mime_types); - } + app = g_desktop_app_info_new (name); + g_free (name); - mime_types = g_key_file_get_keys (key_file, DEFAULT_APPLICATIONS_GROUP, - NULL, NULL); - if (mime_types != NULL) - { - for (i = 0; mime_types[i] != NULL; i++) + if (app) { - desktop_id = g_key_file_get_string (key_file, - DEFAULT_APPLICATIONS_GROUP, - mime_types[i], - NULL); - if (desktop_id == NULL) - continue; - - unaliased_type = _g_unix_content_type_unalias (mime_types[i]); - g_hash_table_replace (dir->mimeapps_list_defaults_map, - unaliased_type, - desktop_id); + (*ptr)->data = app; + ptr = &(*ptr)->next; } - - g_strfreev (mime_types); + else + *ptr = g_list_delete_link (*ptr, *ptr); } - g_key_file_free (key_file); - return; - - error: - g_free (filename); - g_key_file_free (key_file); - - if (mime_types != NULL) - g_strfreev (mime_types); - - if (load_error) - g_error_free (load_error); + return result; } -static MimeInfoCacheDir * -mime_info_cache_dir_new (const char *path) +/** + * g_desktop_app_info_search: + * @search_string: the search string to use + * + * Searches desktop files for ones that match @search_string. + * + * The return value is an array of strvs. Each strv contains a list of + * applications that matched @search_string with an equal score. The + * outer list is sorted by score so that the first strv contains the + * best-matching applications, and so on. + * The algorithm for determining matches is undefined and may change at + * any time. + * + * Returns: (array zero-terminated=1) (element-type GStrv) (transfer full): a + * list of strvs. Free each item with g_strfreev() and free the outer + * list with g_free(). + */ +gchar *** +g_desktop_app_info_search (const gchar *search_string) { - MimeInfoCacheDir *dir; - - dir = g_new0 (MimeInfoCacheDir, 1); - dir->path = g_strdup (path); - - return dir; -} + gchar **search_tokens; + gint last_category = -1; + gchar ***results; + gint n_categories = 0; + gint start_of_category; + gint i, j; -static void -mime_info_cache_dir_free (MimeInfoCacheDir *dir) -{ - if (dir == NULL) - return; - - if (dir->mime_info_cache_map != NULL) - { - destroy_info_cache_map (dir->mime_info_cache_map); - dir->mime_info_cache_map = NULL; - - } - - if (dir->defaults_list_map != NULL) - { - g_hash_table_destroy (dir->defaults_list_map); - dir->defaults_list_map = NULL; - } - - if (dir->mimeapps_list_added_map != NULL) - { - g_hash_table_destroy (dir->mimeapps_list_added_map); - dir->mimeapps_list_added_map = NULL; - } - - if (dir->mimeapps_list_removed_map != NULL) - { - g_hash_table_destroy (dir->mimeapps_list_removed_map); - dir->mimeapps_list_removed_map = NULL; - } + search_tokens = g_str_tokenize_and_fold (search_string, NULL, NULL); - if (dir->mimeapps_list_defaults_map != NULL) - { - g_hash_table_destroy (dir->mimeapps_list_defaults_map); - dir->mimeapps_list_defaults_map = NULL; - } + desktop_file_dirs_lock (); - g_free (dir); -} + reset_total_search_results (); -static void -mime_info_cache_dir_add_desktop_entries (MimeInfoCacheDir *dir, - const char *mime_type, - char **new_desktop_file_ids) -{ - GList *desktop_file_ids; - int i; - - desktop_file_ids = g_hash_table_lookup (dir->mime_info_cache_map, - mime_type); - - for (i = 0; new_desktop_file_ids[i] != NULL; i++) + for (i = 0; i < n_desktop_file_dirs; i++) { - if (!g_list_find_custom (desktop_file_ids, new_desktop_file_ids[i], (GCompareFunc) strcmp)) - desktop_file_ids = g_list_append (desktop_file_ids, - g_strdup (new_desktop_file_ids[i])); + for (j = 0; search_tokens[j]; j++) + { + desktop_file_dir_search (&desktop_file_dirs[i], search_tokens[j]); + merge_token_results (j == 0); + } + merge_directory_results (); } - - g_hash_table_insert (dir->mime_info_cache_map, g_strdup (mime_type), desktop_file_ids); -} -static void -mime_info_cache_init_dir_lists (void) -{ - const char * const *dirs; - int i; - - mime_info_cache = mime_info_cache_new (); - - dirs = get_applications_search_path (); - - for (i = 0; dirs[i] != NULL; i++) - { - MimeInfoCacheDir *dir; - - dir = mime_info_cache_dir_new (dirs[i]); - - if (dir != NULL) - { - mime_info_cache_dir_init (dir); - mime_info_cache_dir_init_defaults_list (dir); - mime_info_cache_dir_init_mimeapps_list (dir); - - mime_info_cache->dirs = g_list_append (mime_info_cache->dirs, dir); - } - } -} + sort_total_search_results (); -static void -mime_info_cache_update_dir_lists (void) -{ - GList *tmp; - - tmp = mime_info_cache->dirs; - - while (tmp != NULL) - { - MimeInfoCacheDir *dir = (MimeInfoCacheDir *) tmp->data; + /* Count the total number of unique categories */ + for (i = 0; i < static_total_results_size; i++) + if (static_total_results[i].category != last_category) + { + last_category = static_total_results[i].category; + n_categories++; + } - /* No need to do this if we had file monitors... */ - mime_info_cache_blow_global_cache (); - mime_info_cache_dir_init (dir); - mime_info_cache_dir_init_defaults_list (dir); - mime_info_cache_dir_init_mimeapps_list (dir); - - tmp = tmp->next; - } -} + results = g_new (gchar **, n_categories + 1); -static void -mime_info_cache_init (void) -{ - G_LOCK (mime_info_cache); - if (mime_info_cache == NULL) - mime_info_cache_init_dir_lists (); - else - { - time_t now; - - time (&now); - if (now >= mime_info_cache->last_stat_time + 10) - { - mime_info_cache_update_dir_lists (); - mime_info_cache->last_stat_time = now; - } - } - - if (mime_info_cache->should_ping_mime_monitor) + /* Start loading into the results list */ + start_of_category = 0; + for (i = 0; i < n_categories; i++) { - /* g_idle_add (emit_mime_changed, NULL); */ - mime_info_cache->should_ping_mime_monitor = FALSE; - } - - G_UNLOCK (mime_info_cache); -} + gint n_items_in_category = 0; + gint this_category; + gint j; -static MimeInfoCache * -mime_info_cache_new (void) -{ - MimeInfoCache *cache; - - cache = g_new0 (MimeInfoCache, 1); - - cache->global_defaults_cache = g_hash_table_new_full (g_str_hash, g_str_equal, - (GDestroyNotify) g_free, - (GDestroyNotify) g_free); - return cache; -} + this_category = static_total_results[start_of_category].category; -static void -mime_info_cache_free (MimeInfoCache *cache) -{ - if (cache == NULL) - return; - - g_list_foreach (cache->dirs, - (GFunc) mime_info_cache_dir_free, - NULL); - g_list_free (cache->dirs); - g_hash_table_destroy (cache->global_defaults_cache); - g_free (cache); -} + while (start_of_category + n_items_in_category < static_total_results_size && + static_total_results[start_of_category + n_items_in_category].category == this_category) + n_items_in_category++; -/** - * mime_info_cache_reload: - * @dir: directory path which needs reloading. - * - * Reload the mime information for the @dir. - */ -static void -mime_info_cache_reload (const char *dir) -{ - /* FIXME: just reload the dir that needs reloading, - * don't blow the whole cache - */ - if (mime_info_cache != NULL) - { - G_LOCK (mime_info_cache); - mime_info_cache_free (mime_info_cache); - mime_info_cache = NULL; - G_UNLOCK (mime_info_cache); + results[i] = g_new (gchar *, n_items_in_category + 1); + for (j = 0; j < n_items_in_category; j++) + results[i][j] = g_strdup (static_total_results[start_of_category + j].app_name); + results[i][j] = NULL; + + start_of_category += n_items_in_category; } -} + results[i] = NULL; -static GList * -append_desktop_entry (GList *list, - const char *desktop_entry, - GList *removed_entries) -{ - /* Add if not already in list, and valid */ - if (!g_list_find_custom (list, desktop_entry, (GCompareFunc) strcmp) && - !g_list_find_custom (removed_entries, desktop_entry, (GCompareFunc) strcmp)) - list = g_list_prepend (list, g_strdup (desktop_entry)); - - return list; + desktop_file_dirs_unlock (); + + g_strfreev (search_tokens); + + return results; } /** - * get_all_desktop_entries_for_mime_type: - * @mime_type: a mime type. - * @except: NULL or a strv list + * g_app_info_get_all: * - * Returns all the desktop ids for @mime_type. The desktop files - * are listed in an order so that default applications are listed before - * non-default ones, and handlers for inherited mimetypes are listed - * after the base ones. + * Gets a list of all of the applications currently registered + * on this system. * - * Optionally doesn't list the desktop ids given in the @except + * For desktop files, this includes applications that have + * `NoDisplay=true` set or are excluded from display by means + * of `OnlyShowIn` or `NotShowIn`. See g_app_info_should_show(). + * The returned list does not include applications which have + * the `Hidden` key set. * - * Return value: a #GList containing the desktop ids which claim - * to handle @mime_type. - */ -static GList * -get_all_desktop_entries_for_mime_type (const char *base_mime_type, - const char **except, - gboolean include_fallback, - char **explicit_default) -{ - GList *desktop_entries, *removed_entries, *list, *dir_list, *tmp; - MimeInfoCacheDir *dir; - char *mime_type, *default_entry = NULL; - const char *entry; - char **mime_types; - char **default_entries; - char **removed_associations; - int i, j, k; - GPtrArray *array; - char **anc; - - mime_info_cache_init (); - - if (include_fallback) - { - /* collect all ancestors */ - mime_types = _g_unix_content_type_get_parents (base_mime_type); - array = g_ptr_array_new (); - for (i = 0; mime_types[i]; i++) - g_ptr_array_add (array, mime_types[i]); - g_free (mime_types); - for (i = 0; i < array->len; i++) - { - anc = _g_unix_content_type_get_parents (g_ptr_array_index (array, i)); - for (j = 0; anc[j]; j++) - { - for (k = 0; k < array->len; k++) - { - if (strcmp (anc[j], g_ptr_array_index (array, k)) == 0) - break; - } - if (k == array->len) /* not found */ - g_ptr_array_add (array, g_strdup (anc[j])); - } - g_strfreev (anc); - } - g_ptr_array_add (array, NULL); - mime_types = (char **)g_ptr_array_free (array, FALSE); - } - else - { - mime_types = g_malloc0 (2 * sizeof (gchar *)); - mime_types[0] = g_strdup (base_mime_type); - mime_types[1] = NULL; - } - - G_LOCK (mime_info_cache); - - removed_entries = NULL; - desktop_entries = NULL; + * Returns: (element-type GAppInfo) (transfer full): a newly allocated #GList of references to #GAppInfos. + **/ +GList * +g_app_info_get_all (void) +{ + GHashTable *apps; + GHashTableIter iter; + gpointer value; + int i; + GList *infos; - for (i = 0; except != NULL && except[i] != NULL; i++) - removed_entries = g_list_prepend (removed_entries, g_strdup (except[i])); - - for (i = 0; mime_types[i] != NULL; i++) - { - mime_type = mime_types[i]; + apps = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); - /* Go through all apps listed in user and system dirs */ - for (dir_list = mime_info_cache->dirs; - dir_list != NULL; - dir_list = dir_list->next) - { - dir = dir_list->data; + desktop_file_dirs_lock (); - /* Pick the explicit default application */ - entry = g_hash_table_lookup (dir->mimeapps_list_defaults_map, mime_type); + for (i = 0; i < n_desktop_file_dirs; i++) + desktop_file_dir_get_all (&desktop_file_dirs[i], apps); - if (entry != NULL) - { - /* Save the default entry if it's the first one we encounter */ - if (default_entry == NULL) - default_entry = g_strdup (entry); - } + desktop_file_dirs_unlock (); - /* Then added associations from mimeapps.list */ - default_entries = g_hash_table_lookup (dir->mimeapps_list_added_map, mime_type); - for (j = 0; default_entries != NULL && default_entries[j] != NULL; j++) - desktop_entries = append_desktop_entry (desktop_entries, default_entries[j], removed_entries); - - /* Then removed associations from mimeapps.list */ - removed_associations = g_hash_table_lookup (dir->mimeapps_list_removed_map, mime_type); - for (j = 0; removed_associations != NULL && removed_associations[j] != NULL; j++) - removed_entries = append_desktop_entry (removed_entries, removed_associations[j], NULL); - - /* Then system defaults (or old per-user config) (using removed associations from this dir or earlier) */ - default_entries = g_hash_table_lookup (dir->defaults_list_map, mime_type); - for (j = 0; default_entries != NULL && default_entries[j] != NULL; j++) - desktop_entries = append_desktop_entry (desktop_entries, default_entries[j], removed_entries); - } - - /* Go through all entries that support the mimetype */ - for (dir_list = mime_info_cache->dirs; - dir_list != NULL; - dir_list = dir_list->next) - { - dir = dir_list->data; - - list = g_hash_table_lookup (dir->mime_info_cache_map, mime_type); - for (tmp = list; tmp != NULL; tmp = tmp->next) - desktop_entries = append_desktop_entry (desktop_entries, tmp->data, removed_entries); - } + infos = NULL; + g_hash_table_iter_init (&iter, apps); + while (g_hash_table_iter_next (&iter, NULL, &value)) + { + if (value) + infos = g_list_prepend (infos, value); } - - G_UNLOCK (mime_info_cache); - - g_strfreev (mime_types); - - if (explicit_default != NULL) - *explicit_default = default_entry; - else - g_free (default_entry); - g_list_foreach (removed_entries, (GFunc)g_free, NULL); - g_list_free (removed_entries); + g_hash_table_destroy (apps); - desktop_entries = g_list_reverse (desktop_entries); - - return desktop_entries; + return infos; } -/* GDesktopAppInfoLookup interface: */ +/* GDesktopAppInfoLookup interface {{{2 */ + +G_GNUC_BEGIN_IGNORE_DEPRECATIONS typedef GDesktopAppInfoLookupIface GDesktopAppInfoLookupInterface; G_DEFINE_INTERFACE (GDesktopAppInfoLookup, g_desktop_app_info_lookup, G_TYPE_OBJECT) @@ -3104,12 +4318,14 @@ g_desktop_app_info_lookup_default_init (GDesktopAppInfoLookupInterface *iface) { } +/* "Get for mime type" APIs {{{2 */ + /** * g_desktop_app_info_lookup_get_default_for_uri_scheme: * @lookup: a #GDesktopAppInfoLookup * @uri_scheme: a string containing a URI scheme. * - * Gets the default application for launching applications + * Gets the default application for launching applications * using this URI scheme for a particular GDesktopAppInfoLookup * implementation. * @@ -3117,20 +4333,268 @@ g_desktop_app_info_lookup_default_init (GDesktopAppInfoLookupInterface *iface) * to implement g_app_info_get_default_for_uri_scheme() backends * in a GIO module. There is no reason for applications to use it * directly. Applications should use g_app_info_get_default_for_uri_scheme(). - * + * * Returns: (transfer full): #GAppInfo for given @uri_scheme or %NULL on error. * * Deprecated: The #GDesktopAppInfoLookup interface is deprecated and unused by gio. */ GAppInfo * g_desktop_app_info_lookup_get_default_for_uri_scheme (GDesktopAppInfoLookup *lookup, - const char *uri_scheme) + const char *uri_scheme) { GDesktopAppInfoLookupIface *iface; - + g_return_val_if_fail (G_IS_DESKTOP_APP_INFO_LOOKUP (lookup), NULL); iface = G_DESKTOP_APP_INFO_LOOKUP_GET_IFACE (lookup); return (* iface->get_default_for_uri_scheme) (lookup, uri_scheme); } + +G_GNUC_END_IGNORE_DEPRECATIONS + +/* Misc getter APIs {{{2 */ + +/** + * g_desktop_app_info_get_startup_wm_class: + * @info: a #GDesktopAppInfo that supports startup notify + * + * Retrieves the StartupWMClass field from @info. This represents the + * WM_CLASS property of the main window of the application, if launched + * through @info. + * + * Returns: (transfer none): the startup WM class, or %NULL if none is set + * in the desktop file. + * + * Since: 2.34 + */ +const char * +g_desktop_app_info_get_startup_wm_class (GDesktopAppInfo *info) +{ + g_return_val_if_fail (G_IS_DESKTOP_APP_INFO (info), NULL); + + return info->startup_wm_class; +} + +/** + * g_desktop_app_info_get_string: + * @info: a #GDesktopAppInfo + * @key: the key to look up + * + * Looks up a string value in the keyfile backing @info. + * + * The @key is looked up in the "Desktop Entry" group. + * + * Returns: a newly allocated string, or %NULL if the key + * is not found + * + * Since: 2.36 + */ +char * +g_desktop_app_info_get_string (GDesktopAppInfo *info, + const char *key) +{ + g_return_val_if_fail (G_IS_DESKTOP_APP_INFO (info), NULL); + + return g_key_file_get_string (info->keyfile, + G_KEY_FILE_DESKTOP_GROUP, key, NULL); +} + +/** + * g_desktop_app_info_get_boolean: + * @info: a #GDesktopAppInfo + * @key: the key to look up + * + * Looks up a boolean value in the keyfile backing @info. + * + * The @key is looked up in the "Desktop Entry" group. + * + * Returns: the boolean value, or %FALSE if the key + * is not found + * + * Since: 2.36 + */ +gboolean +g_desktop_app_info_get_boolean (GDesktopAppInfo *info, + const char *key) +{ + g_return_val_if_fail (G_IS_DESKTOP_APP_INFO (info), FALSE); + + return g_key_file_get_boolean (info->keyfile, + G_KEY_FILE_DESKTOP_GROUP, key, NULL); +} + +/** + * g_desktop_app_info_has_key: + * @info: a #GDesktopAppInfo + * @key: the key to look up + * + * Returns whether @key exists in the "Desktop Entry" group + * of the keyfile backing @info. + * + * Returns: %TRUE if the @key exists + * + * Since: 2.36 + */ +gboolean +g_desktop_app_info_has_key (GDesktopAppInfo *info, + const char *key) +{ + g_return_val_if_fail (G_IS_DESKTOP_APP_INFO (info), FALSE); + + return g_key_file_has_key (info->keyfile, + G_KEY_FILE_DESKTOP_GROUP, key, NULL); +} + +/* Desktop actions support {{{2 */ + +/** + * g_desktop_app_info_list_actions: + * @info: a #GDesktopAppInfo + * + * Returns the list of "additional application actions" supported on the + * desktop file, as per the desktop file specification. + * + * As per the specification, this is the list of actions that are + * explicitly listed in the "Actions" key of the [Desktop Entry] group. + * + * Returns: (array zero-terminated=1) (element-type utf8) (transfer none): a list of strings, always non-%NULL + * + * Since: 2.38 + **/ +const gchar * const * +g_desktop_app_info_list_actions (GDesktopAppInfo *info) +{ + g_return_val_if_fail (G_IS_DESKTOP_APP_INFO (info), NULL); + + return (const gchar **) info->actions; +} + +static gboolean +app_info_has_action (GDesktopAppInfo *info, + const gchar *action_name) +{ + gint i; + + for (i = 0; info->actions[i]; i++) + if (g_str_equal (info->actions[i], action_name)) + return TRUE; + + return FALSE; +} + +/** + * g_desktop_app_info_get_action_name: + * @info: a #GDesktopAppInfo + * @action_name: the name of the action as from + * g_desktop_app_info_list_actions() + * + * Gets the user-visible display name of the "additional application + * action" specified by @action_name. + * + * This corresponds to the "Name" key within the keyfile group for the + * action. + * + * Returns: (transfer full): the locale-specific action name + * + * Since: 2.38 + */ +gchar * +g_desktop_app_info_get_action_name (GDesktopAppInfo *info, + const gchar *action_name) +{ + gchar *group_name; + gchar *result; + + g_return_val_if_fail (G_IS_DESKTOP_APP_INFO (info), NULL); + g_return_val_if_fail (action_name != NULL, NULL); + g_return_val_if_fail (app_info_has_action (info, action_name), NULL); + + group_name = g_strdup_printf ("Desktop Action %s", action_name); + result = g_key_file_get_locale_string (info->keyfile, group_name, "Name", NULL, NULL); + g_free (group_name); + + /* The spec says that the Name field must be given. + * + * If it's not, let's follow the behaviour of our get_name() + * implementation above and never return %NULL. + */ + if (result == NULL) + result = g_strdup (_("Unnamed")); + + return result; +} + +/** + * g_desktop_app_info_launch_action: + * @info: a #GDesktopAppInfo + * @action_name: the name of the action as from + * g_desktop_app_info_list_actions() + * @launch_context: (allow-none): a #GAppLaunchContext + * + * Activates the named application action. + * + * You may only call this function on action names that were + * returned from g_desktop_app_info_list_actions(). + * + * Note that if the main entry of the desktop file indicates that the + * application supports startup notification, and @launch_context is + * non-%NULL, then startup notification will be used when activating the + * action (and as such, invocation of the action on the receiving side + * must signal the end of startup notification when it is completed). + * This is the expected behaviour of applications declaring additional + * actions, as per the desktop file specification. + * + * As with g_app_info_launch() there is no way to detect failures that + * occur while using this function. + * + * Since: 2.38 + */ +void +g_desktop_app_info_launch_action (GDesktopAppInfo *info, + const gchar *action_name, + GAppLaunchContext *launch_context) +{ + GDBusConnection *session_bus; + + g_return_if_fail (G_IS_DESKTOP_APP_INFO (info)); + g_return_if_fail (action_name != NULL); + g_return_if_fail (app_info_has_action (info, action_name)); + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + + if (session_bus && info->app_id) + { + gchar *object_path; + + object_path = object_path_from_appid (info->app_id); + g_dbus_connection_call (session_bus, info->app_id, object_path, + "org.freedesktop.Application", "ActivateAction", + g_variant_new ("(sav@a{sv})", action_name, NULL, + g_desktop_app_info_make_platform_data (info, NULL, launch_context)), + NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + g_free (object_path); + } + else + { + gchar *group_name; + gchar *exec_line; + + group_name = g_strdup_printf ("Desktop Action %s", action_name); + exec_line = g_key_file_get_string (info->keyfile, group_name, "Exec", NULL); + g_free (group_name); + + if (exec_line) + g_desktop_app_info_launch_uris_with_spawn (info, session_bus, exec_line, NULL, launch_context, + _SPAWN_FLAGS_DEFAULT, NULL, NULL, NULL, NULL, NULL); + } + + if (session_bus != NULL) + { + g_dbus_connection_flush (session_bus, NULL, NULL, NULL); + g_object_unref (session_bus); + } +} +/* Epilogue {{{1 */ + +/* vim:set foldmethod=marker: */