| /* GIO - GLib Input, Output and Streaming Library |
| * |
| * Copyright (C) 2006-2007 Red Hat, Inc. |
| * Copyright © 2007 Ryan Lortie |
| * |
| * SPDX-License-Identifier: LGPL-2.1-or-later |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2.1 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * 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, see <http://www.gnu.org/licenses/>. |
| * |
| * Author: Alexander Larsson <alexl@redhat.com> |
| * Ryan Lortie <desrt@desrt.ca> |
| */ |
| |
| /* Prelude {{{1 */ |
| |
| #include "config.h" |
| |
| /* For the #GDesktopAppInfoLookup macros; since macro deprecation is implemented |
| * in the preprocessor, we need to define this before including glib.h*/ |
| #ifndef GLIB_DISABLE_DEPRECATION_WARNINGS |
| #define GLIB_DISABLE_DEPRECATION_WARNINGS |
| #endif |
| |
| #include <errno.h> |
| #include <string.h> |
| #include <unistd.h> |
| |
| #ifdef HAVE_CRT_EXTERNS_H |
| #include <crt_externs.h> |
| #endif |
| |
| #include "gcontenttypeprivate.h" |
| #include "gdesktopappinfo.h" |
| #ifdef G_OS_UNIX |
| #include "glib-unix.h" |
| #endif |
| #include "gfile.h" |
| #include "gioerror.h" |
| #include "gthemedicon.h" |
| #include "gfileicon.h" |
| #include <glib/gstdio.h> |
| #include "glibintl.h" |
| #include "glib-private.h" |
| #include "giomodule-priv.h" |
| #include "gappinfo.h" |
| #include "gappinfoprivate.h" |
| #include "glocalfilemonitor.h" |
| #include "gutilsprivate.h" |
| |
| #ifdef G_OS_UNIX |
| #include "gdocumentportal.h" |
| #endif |
| |
| /** |
| * GDesktopAppInfo: |
| * |
| * `GDesktopAppInfo` is an implementation of [iface@Gio.AppInfo] 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 or the `GioUnix-2.0` GIR namespace 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 gboolean g_desktop_app_info_ensure_saved (GDesktopAppInfo *info, |
| GError **error); |
| static gboolean g_desktop_app_info_load_file (GDesktopAppInfo *self); |
| |
| struct _GDesktopAppInfo |
| { |
| GObject parent_instance; |
| |
| char *desktop_id; |
| char *filename; |
| char *app_id; |
| |
| GKeyFile *keyfile; |
| |
| char *name; |
| 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; |
| }; |
| |
| typedef enum { |
| UPDATE_MIME_NONE = 1 << 0, |
| UPDATE_MIME_SET_DEFAULT = 1 << 1, |
| UPDATE_MIME_SET_NON_DEFAULT = 1 << 2, |
| UPDATE_MIME_REMOVE = 1 << 3, |
| UPDATE_MIME_SET_LAST_USED = 1 << 4, |
| } 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)) |
| |
| /* DesktopFileDir implementation {{{1 */ |
| |
| typedef struct |
| { |
| gatomicrefcount ref_count; |
| gchar *path; |
| gchar *alternatively_watching; |
| gboolean is_config; |
| gboolean is_setup; |
| GFileMonitor *monitor; |
| GHashTable *app_names; |
| GHashTable *mime_tweaks; |
| GHashTable *memory_index; |
| GHashTable *memory_implementations; |
| } DesktopFileDir; |
| |
| static GPtrArray *desktop_file_dirs = NULL; |
| static const gchar *desktop_file_dirs_config_dir = NULL; |
| static DesktopFileDir *desktop_file_dir_user_config = NULL; /* (owned) */ |
| static DesktopFileDir *desktop_file_dir_user_data = NULL; /* (owned) */ |
| static GMutex desktop_file_dir_lock; |
| static const gchar *gio_launch_desktop_path = NULL; |
| |
| /* Monitor 'changed' signal handler {{{2 */ |
| static void desktop_file_dir_reset (DesktopFileDir *dir); |
| |
| static DesktopFileDir * |
| desktop_file_dir_ref (DesktopFileDir *dir) |
| { |
| g_atomic_ref_count_inc (&dir->ref_count); |
| |
| return dir; |
| } |
| |
| static void |
| desktop_file_dir_unref (DesktopFileDir *dir) |
| { |
| if (g_atomic_ref_count_dec (&dir->ref_count)) |
| { |
| desktop_file_dir_reset (dir); |
| g_free (dir->path); |
| g_free (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; |
| |
| /* If the directory itself exists then we need no alternative. */ |
| if (g_access (dir->path, R_OK | X_OK) == 0) |
| return NULL; |
| |
| /* Otherwise, try the parent directories until we find one. */ |
| parent = g_path_get_dirname (dir->path); |
| |
| 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 |
| 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); |
| |
| 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 (); |
| } |
| |
| /* Internal utility functions {{{2 */ |
| |
| /*< internal > |
| * desktop_file_dir_app_name_is_masked: |
| * @dir: a #DesktopFileDir |
| * @app_name: an application ID |
| * |
| * Checks if @app_name is masked for @dir. |
| * |
| * 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) |
| { |
| guint i; |
| |
| for (i = 0; i < desktop_file_dirs->len; i++) |
| { |
| DesktopFileDir *i_dir = g_ptr_array_index (desktop_file_dirs, i); |
| |
| if (dir == i_dir) |
| return FALSE; |
| if (i_dir->app_names && g_hash_table_contains (i_dir->app_names, app_name)) |
| return TRUE; |
| } |
| |
| return FALSE; |
| } |
| |
| /* Not much to go on from https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html |
| * so validate it as a non-empty alphanumeric ASCII string with `-` and `_` allowed. |
| * |
| * Validation is important as the desktop IDs are used to construct filenames, |
| * and may be set by an unprivileged caller if running in a setuid program. */ |
| static gboolean |
| validate_xdg_desktop (const gchar *desktop) |
| { |
| gsize i; |
| |
| for (i = 0; desktop[i] != '\0'; i++) |
| if (desktop[i] != '-' && desktop[i] != '_' && |
| !g_ascii_isalnum (desktop[i])) |
| return FALSE; |
| |
| if (i == 0) |
| return FALSE; |
| |
| return TRUE; |
| } |
| |
| static char ** |
| get_valid_current_desktops (const char *value) |
| { |
| char **tmp; |
| gsize i; |
| GPtrArray *valid_desktops; |
| |
| if (value == NULL) |
| value = g_getenv ("XDG_CURRENT_DESKTOP"); |
| if (value == NULL) |
| value = ""; |
| |
| tmp = g_strsplit (value, G_SEARCHPATH_SEPARATOR_S, 0); |
| valid_desktops = g_ptr_array_new_full (g_strv_length (tmp) + 1, g_free); |
| for (i = 0; tmp[i]; i++) |
| { |
| if (validate_xdg_desktop (tmp[i])) |
| g_ptr_array_add (valid_desktops, tmp[i]); |
| else |
| g_free (tmp[i]); |
| } |
| g_ptr_array_add (valid_desktops, NULL); |
| g_free (tmp); |
| tmp = (char **) g_ptr_array_steal (valid_desktops, NULL); |
| g_ptr_array_unref (valid_desktops); |
| return tmp; |
| } |
| |
| static const gchar * const * |
| get_lowercase_current_desktops (void) |
| { |
| static gchar **result; |
| |
| if (g_once_init_enter_pointer (&result)) |
| { |
| char **tmp = get_valid_current_desktops (NULL); |
| gsize i, j; |
| |
| for (i = 0; tmp[i]; i++) |
| { |
| /* Convert to lowercase. */ |
| for (j = 0; tmp[i][j]; j++) |
| tmp[i][j] = g_ascii_tolower (tmp[i][j]); |
| } |
| |
| g_once_init_leave_pointer (&result, tmp); |
| } |
| |
| return (const gchar **) result; |
| } |
| |
| static const gchar * const * |
| get_current_desktops (const gchar *value) |
| { |
| static gchar **result; |
| |
| if (g_once_init_enter_pointer (&result)) |
| { |
| char **tmp = get_valid_current_desktops (value); |
| |
| g_once_init_leave_pointer (&result, tmp); |
| } |
| |
| return (const gchar **) result; |
| } |
| |
| /*< 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. |
| */ |
| static void |
| add_to_table_if_appropriate (GHashTable *apps, |
| const gchar *app_name, |
| GDesktopAppInfo *info) |
| { |
| if (!info) |
| return; |
| |
| if (info->hidden) |
| { |
| g_object_unref (info); |
| return; |
| } |
| |
| g_free (info->desktop_id); |
| info->desktop_id = g_strdup (app_name); |
| |
| g_hash_table_insert (apps, g_strdup (info->desktop_id), info); |
| } |
| |
| enum |
| { |
| 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 |
| }; |
| |
| typedef enum { |
| /* Lower numbers have higher priority. |
| * Prefix match should put before substring match, independently of |
| * category relevance, i.e. a prefix match in 'Keyword' category will |
| * come before a substring match in a more relevant category like 'Name'. |
| */ |
| MATCH_TYPE_PREFIX = 1, |
| MATCH_TYPE_SUBSTRING = 2 |
| } MatchType; |
| |
| /* Common prefix commands to ignore from Exec= lines */ |
| const char * const exec_key_match_blocklist[] = { |
| "bash", |
| "env", |
| "flatpak", |
| "gjs", |
| "pkexec", |
| "python", |
| "python2", |
| "python3", |
| "sh", |
| "wine", |
| "wine64", |
| NULL |
| }; |
| |
| static gchar * |
| desktop_key_get_name (guint key_id) |
| { |
| switch (key_id) |
| { |
| 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 (); |
| } |
| } |
| |
| /* 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 |
| { |
| const gchar *app_name; |
| gint category; |
| gint match_type; |
| }; |
| |
| 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) |
| { |
| const struct search_result *ra = a; |
| const struct search_result *rb = b; |
| |
| if (ra->app_name < rb->app_name) |
| { |
| return -1; |
| } |
| else if (ra->app_name > rb->app_name) |
| { |
| return 1; |
| } |
| else |
| { |
| /* We prioritize prefix matches over category relevance e.g. a prefix match in 'Keyword' |
| * category is better than a substring match in a more relevance category like 'Name'. |
| */ |
| if (ra->match_type != rb->match_type) |
| return ra->match_type - rb->match_type; |
| |
| return ra->category - rb->category; |
| } |
| } |
| |
| static gint |
| compare_categories (gconstpointer a, |
| gconstpointer b) |
| { |
| const struct search_result *ra = a; |
| const struct search_result *rb = b; |
| |
| /* We prioritize prefix matches over category relevance e.g. a prefix match in 'Keyword' |
| * category is better than a substring match in a more relevance category like 'Name'. |
| */ |
| if (ra->match_type != rb->match_type) |
| return ra->match_type - rb->match_type; |
| |
| return ra->category - rb->category; |
| } |
| |
| static void |
| add_token_result (const gchar *app_name, |
| guint16 category, |
| guint16 match_type) |
| { |
| 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[static_token_results_size].match_type = match_type; |
| static_token_results_size++; |
| } |
| |
| static void |
| merge_token_results (gboolean first) |
| { |
| if (static_token_results_size != 0) |
| 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). |
| * |
| * Match type should also be the worse, so if an app has two |
| * prefix matches it will has higher priority than one prefix |
| * matches and one substring matches, for example, LibreOffice |
| * Writer should be higher priority than LibreOffice Draw with |
| * `lib w`. |
| * |
| * (This ignores the difference between partly prefix matches and |
| * all substring matches, however most time we just focus on exact |
| * prefix matches, who cares the 10th-20th search results?) |
| */ |
| 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); |
| static_search_results[j].match_type = MAX (static_search_results[k].match_type, |
| static_token_results[i].match_type); |
| 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) |
| { |
| if (static_total_results_size != 0) |
| 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); |
| } |
| |
| if (static_search_results_size != 0) |
| 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 *blocklist) |
| { |
| 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 blocklisted strings */ |
| if (blocklist) |
| for (j = 0; blocklist[j]; j++) |
| if (g_str_equal (to_add[i], blocklist[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 blocklisting approach to |
| * avoid recording later instructions that conflict with ones we found |
| * earlier. |
| * |
| * We first start with the XDG_CURRENT_DESKTOP files, in precedence |
| * order. |
| */ |
| desktops = get_lowercase_current_desktops (); |
| for (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 * |
| g_desktop_app_info_new_from_filename_unlocked (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_clear_object (&info); |
| |
| return info; |
| } |
| |
| 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_unlocked (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_unlocked (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; |
| gsize 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; |
| |
| /* Don't match on blocklisted binaries like interpreters */ |
| if (g_strv_contains (exec_key_match_blocklist, value)) |
| value = NULL; |
| } |
| |
| 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; |
| |
| g_assert (search_token != NULL); |
| |
| 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; |
| const char *p; |
| MatchType match_type; |
| |
| /* strstr(haystack, needle) returns haystack if needle is empty, so if |
| * needle is not empty and return value equals to haystack means a prefix |
| * match. |
| */ |
| p = strstr (key, search_token); |
| if (p == NULL) |
| continue; |
| else if (p == key && *search_token != '\0') |
| match_type = MATCH_TYPE_PREFIX; |
| else |
| match_type = MATCH_TYPE_SUBSTRING; |
| |
| while (mie) |
| { |
| add_token_result (mie->app_name, mie->match_category, match_type); |
| mie = mie->next; |
| } |
| } |
| } |
| |
| static gboolean |
| array_contains (GPtrArray *array, |
| const gchar *str) |
| { |
| guint 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 *blocklist) |
| { |
| 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 (blocklist, app_name) && !array_contains (hits, app_name)) |
| g_ptr_array_add (hits, 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 (blocklist, app_name) && !array_contains (hits, app_name)) |
| g_ptr_array_add (blocklist, 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, 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_new: |
| * @data_dir: an XDG_DATA_DIR |
| * |
| * Creates a #DesktopFileDir for the corresponding @data_dir. |
| */ |
| static DesktopFileDir * |
| desktop_file_dir_new (const gchar *data_dir) |
| { |
| DesktopFileDir *dir = g_new0 (DesktopFileDir, 1); |
| |
| g_atomic_ref_count_init (&dir->ref_count); |
| dir->path = g_build_filename (data_dir, "applications", NULL); |
| |
| return g_steal_pointer (&dir); |
| } |
| |
| /*< internal > |
| * desktop_file_dir_new_for_config: |
| * @config_dir: an XDG_CONFIG_DIR |
| * |
| * Just the same as desktop_file_dir_new() 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 DesktopFileDir * |
| desktop_file_dir_new_for_config (const gchar *config_dir) |
| { |
| DesktopFileDir *dir = g_new0 (DesktopFileDir, 1); |
| |
| g_atomic_ref_count_init (&dir->ref_count); |
| dir->path = g_strdup (config_dir); |
| dir->is_config = TRUE; |
| |
| return g_steal_pointer (&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_file_monitor_cancel (dir->monitor); |
| 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; |
| } |
| |
| static void |
| closure_notify_cb (gpointer data, |
| GClosure *closure) |
| { |
| DesktopFileDir *dir = data; |
| desktop_file_dir_unref (dir); |
| } |
| |
| /*< 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_file_monitor_new_in_worker (watch_dir, TRUE, G_FILE_MONITOR_NONE, |
| desktop_file_dir_changed, |
| desktop_file_dir_ref (dir), |
| closure_notify_cb, NULL); |
| |
| desktop_file_dir_unindexed_init (dir); |
| |
| dir->is_setup = TRUE; |
| } |
| |
| /*< internal > |
| * desktop_file_dir_get_app: |
| * @dir: a DesktopFileDir |
| * @desktop_id: the desktop ID to load |
| * |
| * Creates the #GDesktopAppInfo for the given @desktop_id if it exists |
| * within @dir, even if it is hidden. |
| * |
| * This function does not check if @desktop_id would be masked by a |
| * directory with higher precedence. The caller must do so. |
| */ |
| static GDesktopAppInfo * |
| desktop_file_dir_get_app (DesktopFileDir *dir, |
| const gchar *desktop_id) |
| { |
| if (!dir->app_names) |
| return NULL; |
| |
| return desktop_file_dir_unindexed_get_app (dir, desktop_id); |
| } |
| |
| /*< internal > |
| * desktop_file_dir_get_all: |
| * @dir: a DesktopFileDir |
| * @apps: a #GHashTable<string, GDesktopAppInfo> |
| * |
| * Loads all desktop files in @dir and adds them to @apps, careful to |
| * ensure we don't add any files masked by a similarly-named file in a |
| * higher-precedence directory. |
| */ |
| static void |
| desktop_file_dir_get_all (DesktopFileDir *dir, |
| GHashTable *apps) |
| { |
| desktop_file_dir_unindexed_get_all (dir, apps); |
| } |
| |
| /*< internal > |
| * desktop_file_dir_mime_lookup: |
| * @dir: a #DesktopFileDir |
| * @mime_type: the mime type to look up |
| * @hits: the array to store the hits |
| * @blocklist: the array to store the blocklist |
| * |
| * Does a lookup of a mimetype against one desktop file directory, |
| * recording any hits and blocklisting and "Removed" associations (so |
| * later directories don't record them as hits). |
| * |
| * The items added to @hits are duplicated, but the ones in @blocklist |
| * are weak pointers. This facilitates simply freeing the blocklist |
| * (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 *blocklist) |
| { |
| desktop_file_dir_unindexed_mime_lookup (dir, mime_type, hits, blocklist); |
| } |
| |
| /*< 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) |
| { |
| guint i; |
| const gchar *user_config_dir = g_get_user_config_dir (); |
| |
| g_mutex_lock (&desktop_file_dir_lock); |
| |
| /* If the XDG dirs configuration has changed (expected only during tests), |
| * clear and reload the state. */ |
| if (desktop_file_dirs_config_dir != NULL && |
| g_strcmp0 (desktop_file_dirs_config_dir, user_config_dir) != 0) |
| { |
| g_debug ("%s: Resetting desktop app info dirs from %s to %s", |
| G_STRFUNC, desktop_file_dirs_config_dir, user_config_dir); |
| |
| g_ptr_array_set_size (desktop_file_dirs, 0); |
| g_clear_pointer (&desktop_file_dir_user_config, desktop_file_dir_unref); |
| g_clear_pointer (&desktop_file_dir_user_data, desktop_file_dir_unref); |
| } |
| |
| if (desktop_file_dirs == NULL || desktop_file_dirs->len == 0) |
| { |
| const char * const *dirs; |
| gint i; |
| |
| if (desktop_file_dirs == NULL) |
| desktop_file_dirs = g_ptr_array_new_with_free_func ((GDestroyNotify) desktop_file_dir_unref); |
| |
| /* First, the configs. Highest priority: the user's ~/.config */ |
| desktop_file_dir_user_config = desktop_file_dir_new_for_config (user_config_dir); |
| g_ptr_array_add (desktop_file_dirs, desktop_file_dir_ref (desktop_file_dir_user_config)); |
| |
| /* Next, the system configs (/etc/xdg, and so on). */ |
| dirs = g_get_system_config_dirs (); |
| for (i = 0; dirs[i]; i++) |
| g_ptr_array_add (desktop_file_dirs, desktop_file_dir_new_for_config (dirs[i])); |
| |
| /* Now the data. Highest priority: the user's ~/.local/share/applications */ |
| desktop_file_dir_user_data = desktop_file_dir_new (g_get_user_data_dir ()); |
| g_ptr_array_add (desktop_file_dirs, desktop_file_dir_ref (desktop_file_dir_user_data)); |
| |
| /* Following that, XDG_DATA_DIRS/applications, in order */ |
| dirs = g_get_system_data_dirs (); |
| for (i = 0; dirs[i]; i++) |
| g_ptr_array_add (desktop_file_dirs, desktop_file_dir_new (dirs[i])); |
| |
| /* The list of directories will never change after this, unless |
| * g_get_user_config_dir() changes due to %G_TEST_OPTION_ISOLATE_DIRS. */ |
| desktop_file_dirs_config_dir = user_config_dir; |
| } |
| |
| for (i = 0; i < desktop_file_dirs->len; i++) |
| if (!((DesktopFileDir *) g_ptr_array_index (desktop_file_dirs, i))->is_setup) |
| desktop_file_dir_init (g_ptr_array_index (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 (desktop_file_dir_user_config != NULL) |
| desktop_file_dir_reset (desktop_file_dir_user_config); |
| |
| g_mutex_unlock (&desktop_file_dir_lock); |
| } |
| |
| static void |
| desktop_file_dirs_invalidate_user_data (void) |
| { |
| g_mutex_lock (&desktop_file_dir_lock); |
| |
| if (desktop_file_dir_user_data != NULL) |
| desktop_file_dir_reset (desktop_file_dir_user_data); |
| |
| 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", NULL, NULL, 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); |
| } |
| |
| /*< internal > |
| * g_desktop_app_info_get_desktop_id_for_filename |
| * @self: #GDesktopAppInfo to get desktop id of |
| * |
| * Tries to find the desktop ID for a particular `.desktop` filename, as per the |
| * [Desktop Entry Specification](https://specifications.freedesktop.org/desktop- |
| * entry-spec/desktop-entry-spec-latest.html#desktop-file-id). |
| * |
| * Returns: desktop id or basename if filename is unknown. |
| */ |
| static char * |
| g_desktop_app_info_get_desktop_id_for_filename (GDesktopAppInfo *self) |
| { |
| guint i; |
| gchar *desktop_id = NULL; |
| |
| g_return_val_if_fail (self->filename != NULL, NULL); |
| |
| for (i = 0; i < desktop_file_dirs->len; i++) |
| { |
| DesktopFileDir *dir = g_ptr_array_index (desktop_file_dirs, i); |
| GHashTable *app_names; |
| GHashTableIter iter; |
| gpointer key, value; |
| |
| app_names = dir->app_names; |
| |
| if (!app_names) |
| continue; |
| |
| g_hash_table_iter_init (&iter, app_names); |
| while (g_hash_table_iter_next (&iter, &key, &value)) |
| { |
| if (!strcmp (value, self->filename)) |
| { |
| desktop_id = g_strdup (key); |
| break; |
| } |
| } |
| |
| if (desktop_id) |
| break; |
| } |
| |
| if (!desktop_id) |
| desktop_id = g_path_get_basename (self->filename); |
| |
| return g_steal_pointer (&desktop_id); |
| } |
| |
| static gboolean |
| g_desktop_app_info_load_from_keyfile (GDesktopAppInfo *info, |
| GKeyFile *key_file) |
| { |
| char *start_group; |
| char *type; |
| char *try_exec; |
| char *exec; |
| char *path; |
| 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); |
| |
| path = g_key_file_get_string (key_file, |
| G_KEY_FILE_DESKTOP_GROUP, |
| G_KEY_FILE_DESKTOP_KEY_PATH, NULL); |
| |
| 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; |
| /* Use the desktop file path (if any) as working dir to search program */ |
| t = GLIB_PRIVATE_CALL (g_find_program_for_path) (try_exec, NULL, path); |
| if (t == NULL) |
| { |
| g_free (path); |
| 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 (path); |
| g_free (exec); |
| g_free (try_exec); |
| return FALSE; |
| } |
| else |
| { |
| char *t; |
| |
| /* Since @exec is not an empty string, there must be at least one |
| * argument, so dereferencing argv[0] should return non-NULL. */ |
| g_assert (argc > 0); |
| /* Use the desktop file path (if any) as working dir to search program */ |
| t = GLIB_PRIVATE_CALL (g_find_program_for_path) (argv[0], NULL, path); |
| g_strfreev (argv); |
| |
| if (t == NULL) |
| { |
| g_free (path); |
| 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_steal_pointer (&path); |
| 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_name (basename) && basename[0] != ':') |
| info->app_id = g_strdup (basename); |
| } |
| |
| g_free (basename); |
| } |
| |
| if (info->filename) |
| info->desktop_id = g_desktop_app_info_get_desktop_id_for_filename (info); |
| |
| 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); |
| |
| 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: (nullable): 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; |
| |
| desktop_file_dirs_lock (); |
| |
| if (!g_desktop_app_info_load_from_keyfile (info, key_file)) |
| g_clear_object (&info); |
| |
| desktop_file_dirs_unlock (); |
| |
| return info; |
| } |
| |
| /** |
| * g_desktop_app_info_new_from_filename: |
| * @filename: (type filename): the path of a desktop file, in the GLib |
| * filename encoding |
| * |
| * Creates a new #GDesktopAppInfo. |
| * |
| * Returns: (nullable): a new #GDesktopAppInfo or %NULL on error. |
| **/ |
| GDesktopAppInfo * |
| g_desktop_app_info_new_from_filename (const char *filename) |
| { |
| GDesktopAppInfo *info = NULL; |
| |
| desktop_file_dirs_lock (); |
| |
| info = g_desktop_app_info_new_from_filename_unlocked (filename); |
| |
| desktop_file_dirs_unlock (); |
| |
| 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: (nullable): a new #GDesktopAppInfo, or %NULL if no desktop |
| * file with that id exists. |
| */ |
| GDesktopAppInfo * |
| g_desktop_app_info_new (const char *desktop_id) |
| { |
| GDesktopAppInfo *appinfo = NULL; |
| guint i; |
| |
| desktop_file_dirs_lock (); |
| |
| for (i = 0; i < desktop_file_dirs->len; i++) |
| { |
| appinfo = desktop_file_dir_get_app (g_ptr_array_index (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); |
| |
| if (info->name == NULL) |
| return _("Unnamed"); |
| return info->name; |
| } |
| |
| static const char * |
| g_desktop_app_info_get_display_name (GAppInfo *appinfo) |
| { |
| GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); |
| |
| if (info->fullname == NULL) |
| return g_desktop_app_info_get_name (appinfo); |
| return info->fullname; |
| } |
| |
| /** |
| * g_desktop_app_info_get_is_hidden: |
| * @info: a #GDesktopAppInfo. |
| * |
| * A desktop file is hidden if the Hidden key in it is |
| * set to True. |
| * |
| * Returns: %TRUE if hidden, %FALSE otherwise. |
| **/ |
| gboolean |
| g_desktop_app_info_get_is_hidden (GDesktopAppInfo *info) |
| { |
| return info->hidden; |
| } |
| |
| /** |
| * g_desktop_app_info_get_filename: |
| * @info: a #GDesktopAppInfo |
| * |
| * When @info was created from a known filename, return it. In some |
| * situations such as the #GDesktopAppInfo returned from |
| * g_desktop_app_info_new_from_keyfile(), this function will return %NULL. |
| * |
| * Returns: (nullable) (type filename): The full path to the file for @info, |
| * or %NULL if not known. |
| * Since: 2.24 |
| */ |
| const char * |
| g_desktop_app_info_get_filename (GDesktopAppInfo *info) |
| { |
| return info->filename; |
| } |
| |
| static const char * |
| g_desktop_app_info_get_description (GAppInfo *appinfo) |
| { |
| GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); |
| |
| return info->comment; |
| } |
| |
| static const char * |
| g_desktop_app_info_get_executable (GAppInfo *appinfo) |
| { |
| GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); |
| |
| return info->binary; |
| } |
| |
| static const char * |
| g_desktop_app_info_get_commandline (GAppInfo *appinfo) |
| { |
| GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); |
| |
| return info->exec; |
| } |
| |
| static GIcon * |
| g_desktop_app_info_get_icon (GAppInfo *appinfo) |
| { |
| GDesktopAppInfo *info = G_DESKTOP_APP_INFO (appinfo); |
| |
| return info->icon; |
| } |
| |
| /** |
| * g_desktop_app_info_get_categories: |
| * @info: a #GDesktopAppInfo |
| * |
| * Gets the categories from the desktop file. |
| * |
| * Returns: (nullable): 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 desktop file. |
| * |
| * Returns: (nullable): 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, const char *uri) |
| { |
| GFile *file; |
| char *result = NULL; |
| char *path = NULL; |
| char *name; |
| |
| file = g_file_new_for_uri (uri); |
| |
| switch (macro) |
| { |
| 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); |
| break; |
| case 'd': |
| case 'D': |
| path = g_file_get_path (file); |
| if (path) |
| { |
| name = g_path_get_dirname (path); |
| 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); |
| g_free (name); |
| } |
| break; |
| } |
| |
| g_object_unref (file); |
| g_free (path); |
| |
| return result; |
| } |
| |
| static char * |
| expand_macro_uri (char macro, const char *uri, gboolean force_file_uri, char force_file_uri_macro) |
| { |
| char *expanded = NULL; |
| |
| g_return_val_if_fail (uri != NULL, NULL); |
| |
| if (!force_file_uri || |
| /* Pass URI if it contains an anchor */ |
| strchr (uri, '#') != NULL) |
| { |
| expanded = expand_macro_single (macro, uri); |
| } |
| else |
| { |
| expanded = expand_macro_single (force_file_uri_macro, uri); |
| if (expanded == NULL) |
| expanded = expand_macro_single (macro, uri); |
| } |
| |
| return expanded; |
| } |
| |
| static void |
| expand_macro (char macro, |
| GString *exec, |
| GDesktopAppInfo *info, |
| GList **uri_list) |
| { |
| GList *uris = *uri_list; |
| char *expanded = NULL; |
| gboolean force_file_uri; |
| char force_file_uri_macro; |
| const char *uri; |
| |
| g_return_if_fail (exec != NULL); |
| |
| /* On %u and %U, pass POSIX file path pointing to the URI via |
| * the FUSE mount in ~/.gvfs. Note that if the FUSE daemon isn't |
| * running or the URI doesn't have a POSIX file path via FUSE |
| * we'll just pass the URI. |
| */ |
| force_file_uri_macro = macro; |
| force_file_uri = FALSE; |
| 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; |
| } |
| } |
| |
| switch (macro) |
| { |
| case 'u': |
| case 'f': |
| case 'd': |
| case 'n': |
| if (uris) |
| { |
| uri = uris->data; |
| expanded = expand_macro_uri (macro, uri, |
| force_file_uri, force_file_uri_macro); |
| if (expanded) |
| { |
| g_string_append (exec, expanded); |
| g_free (expanded); |
| } |
| uris = uris->next; |
| } |
| |
| break; |
| |
| case 'U': |
| case 'F': |
| case 'D': |
| case 'N': |
| while (uris) |
| { |
| uri = uris->data; |
| expanded = expand_macro_uri (macro, uri, |
| force_file_uri, force_file_uri_macro); |
| 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 "); |
| expanded = g_shell_quote (info->icon_name); |
| g_string_append (exec, expanded); |
| g_free (expanded); |
| } |
| break; |
| |
| case 'c': |
| if (info->name) |
| { |
| expanded = g_shell_quote (info->name); |
| g_string_append (exec, expanded); |
| g_free (expanded); |
| } |
| break; |
| |
| case 'k': |
| if (info->filename) |
| { |
| expanded = g_shell_quote (info->filename); |
| g_string_append (exec, expanded); |
| g_free (expanded); |
| } |
| break; |
| |
| case 'm': /* deprecated */ |
| break; |
| |
| case '%': |
| g_string_append_c (exec, '%'); |
| break; |
| } |
| |
| *uri_list = uris; |
| } |
| |
| static gboolean |
| expand_application_parameters (GDesktopAppInfo *info, |
| const gchar *exec_line, |
| GList **uris, |
| int *argc, |
| char ***argv, |
| GError **error) |
| { |
| GList *uri_list = *uris; |
| const char *p = exec_line; |
| GString *expanded_exec; |
| gboolean res; |
| |
| if (exec_line == NULL) |
| { |
| g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, |
| _("Desktop file didn’t specify Exec field")); |
| return FALSE; |
| } |
| |
| expanded_exec = g_string_new (NULL); |
| |
| while (*p) |
| { |
| if (p[0] == '%' && p[1] != '\0') |
| { |
| expand_macro (p[1], expanded_exec, info, uris); |
| p++; |
| } |
| else |
| g_string_append_c (expanded_exec, *p); |
| |
| p++; |
| } |
| |
| /* No file substitutions */ |
| if (uri_list == *uris && uri_list != NULL) |
| { |
| /* If there is no macro default to %f. This is also what KDE does */ |
| g_string_append_c (expanded_exec, ' '); |
| expand_macro ('f', expanded_exec, info, uris); |
| } |
| |
| res = g_shell_parse_argv (expanded_exec->str, argc, argv, error); |
| g_string_free (expanded_exec, TRUE); |
| return res; |
| } |
| |
| static gboolean |
| prepend_terminal_to_vector (int *argc, |
| char ***argv, |
| const char *path, |
| const char *working_dir) |
| { |
| #ifndef G_OS_WIN32 |
| char **real_argv; |
| size_t real_argc; |
| size_t i; |
| size_t term_argc; |
| char *found_terminal; |
| char **the_argv; |
| const char *term_arg; |
| static const struct { |
| const char *exec; |
| const char *exec_arg; |
| } known_terminals[] = { |
| { "xdg-terminal-exec", NULL }, |
| { "kgx", "-e" }, |
| { "gnome-terminal", "--" }, |
| { "mate-terminal", "-x" }, |
| { "xfce4-terminal", "-x" }, |
| { "tilix", "-e" }, |
| { "konsole", "-e" }, |
| { "nxterm", "-e" }, |
| { "color-xterm", "-e" }, |
| { "rxvt", "-e" }, |
| { "dtterm", "-e" }, |
| { "xterm", "-e" } |
| }; |
| |
| 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 ((*argc) = 0; the_argv[*argc] != NULL; (*argc)++) |
| ; |
| } |
| |
| for (i = 0, found_terminal = NULL; i < G_N_ELEMENTS (known_terminals); i++) |
| { |
| found_terminal = GLIB_PRIVATE_CALL (g_find_program_for_path) (known_terminals[i].exec, |
| path, working_dir); |
| if (found_terminal != NULL) |
| { |
| term_arg = known_terminals[i].exec_arg; |
| break; |
| } |
| } |
| |
| if (found_terminal == NULL) |
| { |
| g_debug ("Couldn’t find a known terminal"); |
| return FALSE; |
| } |
| |
| /* check if the terminal require an option */ |
| term_argc = term_arg ? 2 : 1; |
| |
| real_argc = term_argc + *argc; |
| real_argv = g_new (char *, real_argc + 1); |
| |
| i = 0; |
| real_argv[i++] = found_terminal; |
| |
| if (term_arg) |
| real_argv[i++] = g_strdup (term_arg); |
| |
| g_assert (i == term_argc); |
| for (int j = 0; j < *argc; j++) |
| real_argv[i++] = the_argv[j]; |
| |
| real_argv[i] = NULL; |
| |
| g_free (*argv); |
| *argv = real_argv; |
| *argc = real_argc; |
| |
| return TRUE; |
| #else |
| return FALSE; |
| #endif /* G_OS_WIN32 */ |
| } |
| |
| static GList * |
| create_files_for_uris (GList *uris) |
| { |
| GList *res; |
| GList *iter; |
| |
| res = NULL; |
| |
| for (iter = uris; iter; iter = iter->next) |
| { |
| GFile *file = g_file_new_for_uri ((char *)iter->data); |
| res = g_list_prepend (res, file); |
| } |
| |
| return g_list_reverse (res); |
| } |
| |
| static void |
| notify_desktop_launch (GDBusConnection *session_bus, |
| 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; |
| |
| g_variant_builder_init (&uri_variant, G_VARIANT_TYPE ("as")); |
| for (iter = uris; iter; iter = iter->next) |
| g_variant_builder_add (&uri_variant, "s", iter->data); |
| |
| 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)); |
| 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; |
| else if (info->desktop_id) |
| 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"); |
| 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_dbus_connection_send_message (session_bus, |
| msg, 0, |
| NULL, |
| NULL); |
| g_object_unref (msg); |
| } |
| |
| static void |
| emit_launch_started (GAppLaunchContext *context, |
| GDesktopAppInfo *info, |
| const gchar *startup_id) |
| { |
| GVariantBuilder builder; |
| GVariant *platform_data = NULL; |
| |
| if (startup_id) |
| { |
| g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); |
| g_variant_builder_add (&builder, "{sv}", |
| "startup-notification-id", |
| g_variant_new_string (startup_id)); |
| platform_data = g_variant_ref_sink (g_variant_builder_end (&builder)); |
| } |
| g_signal_emit_by_name (context, "launch-started", info, platform_data); |
| g_clear_pointer (&platform_data, g_variant_unref); |
| } |
| |
| #define _SPAWN_FLAGS_DEFAULT (G_SPAWN_SEARCH_PATH) |
| |
| static gboolean |
| 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, |
| gint stdin_fd, |
| gint stdout_fd, |
| gint stderr_fd, |
| GError **error) |
| { |
| gboolean completed = FALSE; |
| GList *old_uris; |
| GList *dup_uris; |
| |
| char **argv, **envp; |
| int argc; |
| |
| g_return_val_if_fail (info != NULL, FALSE); |
| |
| argv = NULL; |
| |
| if (launch_context) |
| envp = g_app_launch_context_get_environment (launch_context); |
| else |
| envp = g_get_environ (); |
| |
| /* The GList* passed to expand_application_parameters() will be modified |
| * internally by expand_macro(), so we need to pass a copy of it instead, |
| * and also use that copy to control the exit condition of the loop below. |
| */ |
| dup_uris = uris; |
| do |
| { |
| GPid pid; |
| GList *launched_uris; |
| GList *iter; |
| char *sn_id = NULL; |
| char **wrapped_argv; |
| int i; |
| |
| old_uris = dup_uris; |
| if (!expand_application_parameters (info, exec_line, &dup_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 != dup_uris; iter = iter->next) |
| 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_environ_getenv (envp, "PATH"), |
| info->path)) |
| { |
| g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, |
| _("Unable to find terminal required for application")); |
| goto out; |
| } |
| |
| if (info->filename) |
| envp = g_environ_setenv (envp, |
| "GIO_LAUNCHED_DESKTOP_FILE", |
| info->filename, |
| TRUE); |
| |
| sn_id = NULL; |
| if (launch_context) |
| { |
| GList *launched_files = create_files_for_uris (launched_uris); |
| |
| 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); |
| envp = g_environ_setenv (envp, "XDG_ACTIVATION_TOKEN", sn_id, TRUE); |
| } |
| } |
| |
| g_list_free_full (launched_files, g_object_unref); |
| |
| emit_launch_started (launch_context, info, sn_id); |
| } |
| |
| g_assert (argc > 0); |
| |
| if (!g_path_is_absolute (argv[0]) || |
| !g_file_test (argv[0], G_FILE_TEST_IS_EXECUTABLE) || |
| g_file_test (argv[0], G_FILE_TEST_IS_DIR)) |
| { |
| char *program = g_steal_pointer (&argv[0]); |
| char *program_path = NULL; |
| |
| if (!g_path_is_absolute (program)) |
| { |
| const char *env_path = g_environ_getenv (envp, "PATH"); |
| |
| program_path = GLIB_PRIVATE_CALL (g_find_program_for_path) (program, |
| env_path, |
| info->path); |
| } |
| |
| if (program_path) |
| { |
| argv[0] = g_steal_pointer (&program_path); |
| } |
| else |
| { |
| if (sn_id) |
| g_app_launch_context_launch_failed (launch_context, sn_id); |
| |
| g_set_error (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT, |
| _("Program ‘%s’ not found in $PATH"), |
| program); |
| |
| g_free (program); |
| g_clear_pointer (&sn_id, g_free); |
| g_clear_list (&launched_uris, NULL); |
| goto out; |
| } |
| |
| g_free (program); |
| } |
| |
| if (g_once_init_enter_pointer (&gio_launch_desktop_path)) |
| { |
| const gchar *tmp = NULL; |
| gboolean is_setuid = GLIB_PRIVATE_CALL (g_check_setuid) (); |
| |
| /* Allow test suite to specify path to gio-launch-desktop */ |
| if (!is_setuid) |
| tmp = g_getenv ("GIO_LAUNCH_DESKTOP"); |
| |
| /* Allow build system to specify path to gio-launch-desktop */ |
| if (tmp == NULL && g_file_test (GIO_LAUNCH_DESKTOP, G_FILE_TEST_IS_EXECUTABLE)) |
| tmp = GIO_LAUNCH_DESKTOP; |
| |
| /* Fall back on usual searching in $PATH */ |
| if (tmp == NULL) |
| tmp = "gio-launch-desktop"; |
| g_once_init_leave_pointer (&gio_launch_desktop_path, tmp); |
| } |
| |
| wrapped_argv = g_new (char *, argc + 2); |
| wrapped_argv[0] = g_strdup (gio_launch_desktop_path); |
| |
| for (i = 0; i < argc; i++) |
| wrapped_argv[i + 1] = g_steal_pointer (&argv[i]); |
| |
| wrapped_argv[i + 1] = NULL; |
| g_free (argv); |
| argv = NULL; |
| |
| if (!g_spawn_async_with_fds (info->path, |
| wrapped_argv, |
| envp, |
| spawn_flags, |
| user_setup, |
| user_setup_data, |
| &pid, |
| stdin_fd, |
| stdout_fd, |
| stderr_fd, |
| error)) |
| { |
| if (sn_id) |
| g_app_launch_context_launch_failed (launch_context, sn_id); |
| |
| g_free (sn_id); |
| g_list_free (launched_uris); |
| g_clear_pointer (&wrapped_argv, g_strfreev); |
| |
| goto out; |
| } |
| |
| if (pid_callback != NULL) |
| 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, |
| NULL, |
| sn_id, |
| launched_uris); |
| |
| g_free (sn_id); |
| g_list_free (launched_uris); |
| |
| g_strfreev (wrapped_argv); |
| wrapped_argv = NULL; |
| } |
| while (dup_uris != NULL); |
| |
| completed = TRUE; |
| |
| out: |
| g_strfreev (argv); |
| g_strfreev (envp); |
| |
| return completed; |
| } |
| |
| static gchar * |
| object_path_from_appid (const gchar *appid) |
| { |
| gchar *appid_path, *iter; |
| |
| appid_path = g_strconcat ("/", appid, NULL); |
| for (iter = appid_path; *iter; iter++) |
| { |
| if (*iter == '.') |
| *iter = '/'; |
| |
| if (*iter == '-') |
| *iter = '_'; |
| } |
| |
| return appid_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_string (sn_id)); |
| g_variant_builder_add (&builder, "{sv}", "activation-token", g_variant_new_take_string (g_steal_pointer (&sn_id))); |
| } |
| } |
| |
| g_list_free_full (launched_files, g_object_unref); |
| } |
| |
| return g_variant_builder_end (&builder); |
| } |
| |
| typedef struct |
| { |
| GDesktopAppInfo *info; /* (owned) */ |
| GAppLaunchContext *launch_context; /* (owned) (nullable) */ |
| GAsyncReadyCallback callback; |
| gchar *startup_id; /* (owned) (nullable) */ |
| gpointer user_data; |
| } LaunchUrisWithDBusData; |
| |
| static void |
| launch_uris_with_dbus_data_free (LaunchUrisWithDBusData *data) |
| { |
| g_clear_object (&data->info); |
| g_clear_object (&data->launch_context); |
| g_free (data->startup_id); |
| |
| g_free (data); |
| } |
| |
| static void |
| launch_uris_with_dbus_signal_cb (GObject *object, |
| GAsyncResult *result, |
| gpointer user_data) |
| { |
| LaunchUrisWithDBusData *data = user_data; |
| GVariantBuilder builder; |
| |
| if (data->launch_context) |
| { |
| if (g_task_had_error (G_TASK (result))) |
| { |
| if (data->startup_id != NULL) |
| g_app_launch_context_launch_failed (data->launch_context, data->startup_id); |
| } |
| else |
| { |
| GVariant *platform_data; |
| |
| g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); |
| /* the docs guarantee `pid` will be set, but we can’t |
| * easily know it for a D-Bus process, so set it to zero */ |
| g_variant_builder_add (&builder, "{sv}", "pid", g_variant_new_int32 (0)); |
| if (data->startup_id) |
| g_variant_builder_add (&builder, "{sv}", |
| "startup-notification-id", |
| g_variant_new_string (data->startup_id)); |
| platform_data = g_variant_ref_sink (g_variant_builder_end (&builder)); |
| g_signal_emit_by_name (data->launch_context, |
| "launched", |
| data->info, |
| platform_data); |
| g_variant_unref (platform_data); |
| } |
| } |
| |
| if (data->callback) |
| data->callback (object, result, data->user_data); |
| else if (!g_task_had_error (G_TASK (result))) |
| g_variant_unref (g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), |
| result, NULL)); |
| |
| launch_uris_with_dbus_data_free (data); |
| } |
| |
| static void |
| launch_uris_with_dbus (GDesktopAppInfo *info, |
| GDBusConnection *session_bus, |
| GList *uris, |
| GAppLaunchContext *launch_context, |
| GCancellable *cancellable, |
| GAsyncReadyCallback callback, |
| gpointer user_data) |
| { |
| GVariant *platform_data; |
| GVariantBuilder builder; |
| GVariantDict dict; |
| gchar *object_path; |
| LaunchUrisWithDBusData *data; |
| |
| 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); |
| } |
| |
| platform_data = g_desktop_app_info_make_platform_data (info, uris, launch_context); |
| |
| g_variant_builder_add_value (&builder, platform_data); |
| object_path = object_path_from_appid (info->app_id); |
| |
| data = g_new0 (LaunchUrisWithDBusData, 1); |
| data->info = g_object_ref (info); |
| data->callback = callback; |
| data->user_data = user_data; |
| data->launch_context = launch_context ? g_object_ref (launch_context) : NULL; |
| g_variant_dict_init (&dict, platform_data); |
| g_variant_dict_lookup (&dict, "desktop-startup-id", "s", &data->startup_id); |
| |
| if (launch_context) |
| emit_launch_started (launch_context, info, data->startup_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, |
| cancellable, launch_uris_with_dbus_signal_cb, g_steal_pointer (&data)); |
| g_free (object_path); |
| |
| g_variant_dict_clear (&dict); |
| } |
| |
| static gboolean |
| g_desktop_app_info_launch_uris_with_dbus (GDesktopAppInfo *info, |
| GDBusConnection *session_bus, |
| GList *uris, |
| GAppLaunchContext *launch_context, |
| GCancellable *cancellable, |
| GAsyncReadyCallback callback, |
| gpointer user_data) |
| { |
| GList *ruris = uris; |
| char *app_id = NULL; |
| |
| g_return_val_if_fail (info != NULL, FALSE); |
| |
| #ifdef G_OS_UNIX |
| app_id = g_desktop_app_info_get_string (info, "X-Flatpak"); |
| if (app_id && *app_id) |
| { |
| ruris = g_document_portal_add_documents (uris, app_id, NULL); |
| if (ruris == NULL) |
| ruris = uris; |
|