mirror of
https://gitlab.gnome.org/GNOME/glib.git
synced 2025-01-23 20:46:14 +01:00
Add g_desktop_app_info_search()
The first time this function is called we load all of the keyfiles in the directory, ignoring the 'Hidden' ones and build an index out of the interesting fields using g_str_tokenize_and_fold(). We do prefix matching on the tokens to find relevent desktop files. Right now this is implemented as a hashtable that we iterate over, checking prefixes on each token. This could possibly be sped up by creating an array, but it's already pretty fast... https://bugzilla.gnome.org/show_bug.cgi?id=711557
This commit is contained in:
parent
c45b26d61f
commit
3d32d5359a
@ -150,6 +150,7 @@ typedef struct
|
||||
GLocalDirectoryMonitor *monitor;
|
||||
GHashTable *app_names;
|
||||
gboolean is_setup;
|
||||
GHashTable *memory_index;
|
||||
} DesktopFileDir;
|
||||
|
||||
static DesktopFileDir *desktop_file_dirs;
|
||||
@ -245,6 +246,276 @@ add_to_table_if_appropriate (GHashTable *apps,
|
||||
g_hash_table_insert (apps, g_strdup (info->desktop_id), info);
|
||||
}
|
||||
|
||||
enum
|
||||
{
|
||||
DESKTOP_KEY_Comment,
|
||||
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_GenericName] = 2,
|
||||
[DESKTOP_KEY_Keywords] = 3,
|
||||
[DESKTOP_KEY_X_GNOME_FullName] = 4,
|
||||
[DESKTOP_KEY_Comment] = 5
|
||||
};
|
||||
|
||||
static gchar *
|
||||
desktop_key_get_name (guint key_id)
|
||||
{
|
||||
switch (key_id)
|
||||
{
|
||||
case DESKTOP_KEY_Comment:
|
||||
return "Comment";
|
||||
case DESKTOP_KEY_GenericName:
|
||||
return "GenericName";
|
||||
case DESKTOP_KEY_Keywords:
|
||||
return "Keywords";
|
||||
case DESKTOP_KEY_Name:
|
||||
return "Name";
|
||||
case DESKTOP_KEY_X_GNOME_FullName:
|
||||
return "X-GNOME-FullName";
|
||||
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;
|
||||
};
|
||||
|
||||
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
|
||||
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;
|
||||
|
||||
return ra->category - rb->category;
|
||||
}
|
||||
|
||||
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,
|
||||
@ -332,6 +603,157 @@ desktop_file_dir_unindexed_get_all (DesktopFileDir *dir,
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct _MemoryIndexEntry MemoryIndexEntry;
|
||||
typedef GHashTable MemoryIndex;
|
||||
|
||||
struct _MemoryIndexEntry
|
||||
{
|
||||
const gchar *app_name; /* pointer to the hashtable key */
|
||||
gint match_category;
|
||||
MemoryIndexEntry *next;
|
||||
};
|
||||
|
||||
static void
|
||||
memory_index_entry_free (gpointer data)
|
||||
{
|
||||
MemoryIndexEntry *mie = data;
|
||||
|
||||
while (mie)
|
||||
{
|
||||
MemoryIndexEntry *next = mie->next;
|
||||
|
||||
g_slice_free (MemoryIndexEntry, mie);
|
||||
mie = next;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
memory_index_add_token (MemoryIndex *mi,
|
||||
const gchar *token,
|
||||
gint match_category,
|
||||
const gchar *app_name)
|
||||
{
|
||||
MemoryIndexEntry *mie, *first;
|
||||
|
||||
mie = g_slice_new (MemoryIndexEntry);
|
||||
mie->app_name = app_name;
|
||||
mie->match_category = match_category;
|
||||
|
||||
first = g_hash_table_lookup (mi, token);
|
||||
|
||||
if (first)
|
||||
{
|
||||
mie->next = first->next;
|
||||
first->next = mie;
|
||||
}
|
||||
else
|
||||
{
|
||||
mie->next = NULL;
|
||||
g_hash_table_insert (mi, g_strdup (token), mie);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
memory_index_add_string (MemoryIndex *mi,
|
||||
const gchar *string,
|
||||
gint match_category,
|
||||
const gchar *app_name)
|
||||
{
|
||||
gchar **tokens, **alternates;
|
||||
gint i;
|
||||
|
||||
tokens = g_str_tokenize_and_fold (string, NULL, &alternates);
|
||||
|
||||
for (i = 0; tokens[i]; i++)
|
||||
memory_index_add_token (mi, tokens[i], match_category, app_name);
|
||||
|
||||
for (i = 0; alternates[i]; i++)
|
||||
memory_index_add_token (mi, alternates[i], match_category, app_name);
|
||||
|
||||
g_strfreev (alternates);
|
||||
g_strfreev (tokens);
|
||||
}
|
||||
|
||||
static MemoryIndex *
|
||||
memory_index_new (void)
|
||||
{
|
||||
return g_hash_table_new_full (g_str_hash, g_str_equal, g_free, memory_index_entry_free);
|
||||
}
|
||||
|
||||
static void
|
||||
desktop_file_dir_unindexed_setup_search (DesktopFileDir *dir)
|
||||
{
|
||||
GHashTableIter iter;
|
||||
gpointer app, path;
|
||||
|
||||
dir->memory_index = memory_index_new ();
|
||||
|
||||
/* Nothing to search? */
|
||||
if (dir->app_names == NULL)
|
||||
return;
|
||||
|
||||
g_hash_table_iter_init (&iter, dir->app_names);
|
||||
while (g_hash_table_iter_next (&iter, &app, &path))
|
||||
{
|
||||
GKeyFile *key_file;
|
||||
|
||||
if (desktop_file_dir_app_name_is_masked (dir, app))
|
||||
continue;
|
||||
|
||||
key_file = g_key_file_new ();
|
||||
|
||||
if (g_key_file_load_from_file (key_file, path, G_KEY_FILE_NONE, NULL) &&
|
||||
!g_key_file_get_boolean (key_file, "Desktop Entry", "Hidden", NULL))
|
||||
{
|
||||
/* Index the interesting keys... */
|
||||
gint i;
|
||||
|
||||
for (i = 0; i < G_N_ELEMENTS (desktop_key_match_category); i++)
|
||||
{
|
||||
gchar *value;
|
||||
|
||||
if (!desktop_key_match_category[i])
|
||||
continue;
|
||||
|
||||
value = g_key_file_get_locale_string (key_file, "Desktop Entry", desktop_key_get_name (i), NULL, NULL);
|
||||
|
||||
if (value)
|
||||
memory_index_add_string (dir->memory_index, value, desktop_key_match_category[i], app);
|
||||
|
||||
g_free (value);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* DesktopFileDir "API" {{{2 */
|
||||
|
||||
/*< internal >
|
||||
@ -375,6 +797,12 @@ desktop_file_dir_reset (DesktopFileDir *dir)
|
||||
dir->app_names = NULL;
|
||||
}
|
||||
|
||||
if (dir->memory_index)
|
||||
{
|
||||
g_hash_table_unref (dir->memory_index);
|
||||
dir->memory_index = NULL;
|
||||
}
|
||||
|
||||
dir->is_setup = FALSE;
|
||||
}
|
||||
|
||||
@ -442,6 +870,20 @@ desktop_file_dir_get_all (DesktopFileDir *dir,
|
||||
desktop_file_dir_unindexed_get_all (dir, apps);
|
||||
}
|
||||
|
||||
/*< internal >
|
||||
* desktop_file_dir_search:
|
||||
* @dir: a #DesktopFilEDir
|
||||
* @term: a normalised and casefolded search term
|
||||
*
|
||||
* Finds the names of applications in @dir that match @term.
|
||||
*/
|
||||
static void
|
||||
desktop_file_dir_search (DesktopFileDir *dir,
|
||||
const gchar *search_token)
|
||||
{
|
||||
desktop_file_dir_unindexed_search (dir, search_token);
|
||||
}
|
||||
|
||||
/* Lock/unlock and global setup API {{{2 */
|
||||
|
||||
static void
|
||||
@ -3054,6 +3496,89 @@ g_app_info_get_default_for_uri_scheme (const char *uri_scheme)
|
||||
|
||||
/* "Get all" API {{{2 */
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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)
|
||||
{
|
||||
gchar **search_tokens;
|
||||
gint last_category = -1;
|
||||
gchar ***results;
|
||||
gint n_categories = 0;
|
||||
gint start_of_category;
|
||||
gint i, j;
|
||||
|
||||
search_tokens = g_str_tokenize_and_fold (search_string, NULL, NULL);
|
||||
|
||||
desktop_file_dirs_lock ();
|
||||
|
||||
reset_total_search_results ();
|
||||
|
||||
for (i = 0; i < n_desktop_file_dirs; 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 ();
|
||||
}
|
||||
|
||||
sort_total_search_results ();
|
||||
|
||||
/* 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++;
|
||||
}
|
||||
|
||||
results = g_new (gchar **, n_categories + 1);
|
||||
|
||||
/* Start loading into the results list */
|
||||
start_of_category = 0;
|
||||
for (i = 0; i < n_categories; i++)
|
||||
{
|
||||
gint n_items_in_category = 0;
|
||||
gint this_category;
|
||||
gint j;
|
||||
|
||||
this_category = static_total_results[start_of_category].category;
|
||||
|
||||
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++;
|
||||
|
||||
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;
|
||||
|
||||
desktop_file_dirs_unlock ();
|
||||
|
||||
g_strfreev (search_tokens);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* g_app_info_get_all:
|
||||
*
|
||||
|
@ -164,6 +164,8 @@ gboolean g_desktop_app_info_launch_uris_as_manager (GDesktopAppInfo
|
||||
gpointer pid_callback_data,
|
||||
GError **error);
|
||||
|
||||
GLIB_AVAILABLE_IN_2_40
|
||||
gchar *** g_desktop_app_info_search (const gchar *search_string);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user