[glib/wip/gdesktopappinfo: 6/7] Add g_desktop_app_info_search()



commit 02afbd5ddae4e67a69d0cd2763e2a3805d2d0942
Author: Ryan Lortie <desrt desrt ca>
Date:   Sun Jul 28 13:41:22 2013 -0400

    Add g_desktop_app_info_search()

 gio/gdesktopappinfo.c |  930 +++++++++++++++++++++++++++++++++++++++++++++++--
 gio/gdesktopappinfo.h |    8 +
 2 files changed, 916 insertions(+), 22 deletions(-)
---
diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c
index 8ece3d8..20a46fb 100644
--- a/gio/gdesktopappinfo.c
+++ b/gio/gdesktopappinfo.c
@@ -29,6 +29,7 @@
 #include <errno.h>
 #include <string.h>
 #include <unistd.h>
+#include <locale.h>
 
 #ifdef HAVE_CRT_EXTERNS_H
 #include <crt_externs.h>
@@ -148,6 +149,143 @@ G_DEFINE_TYPE_WITH_CODE (GDesktopAppInfo, g_desktop_app_info, G_TYPE_OBJECT,
 G_LOCK_DEFINE_STATIC (g_desktop_env);
 static gchar *g_desktop_env = NULL;
 
+/* locale utilities {{{1 */
+
+/* We need more control over the output of this function than we get
+ * from the public APIs of glib, so roll our own version of this.
+ *
+ * We don't make this a public API of GLib because it's extremely
+ * difficult to get this working correctly on Windows, but we don't have
+ * to worry about that here.
+ *
+ * If LANGUAGE is given (and l10n is enabled), we want the list of
+ * languages from this environment variable.  Otherwise, we want a
+ * single language code.  If l10n is disabled, NULL will do.
+ *
+ * Language codes should have the form xx[_YY][ zzz] 
+ */
+static gchar **
+get_locale_list (void)
+{
+  const gchar *lc_messages;
+  const gchar *language;
+
+  /* We really should be checking LC_MESSAGES, but it's not in POSIX.
+   * If we don't have it, use LC_CTYPE instead and manually check the
+   * variables that may have impacted the setting of LC_MESSAGES if we
+   * had it.
+   *
+   * Of course, none of this is particularly threadsafe...
+   */
+#ifdef LC_MESSAGES
+  lc_messages = setlocale (LC_MESSAGES, NULL);
+#else
+  lc_messages = setlocale (LC_CTYPE, NULL);
+
+  /* If LC_CTYPE is the "C" locale then this is a good indication that
+   * l10n has not been enabled for this program.
+   *
+   * Otherwise, do some more checking to determine what should be the
+   * correct LC_MESSAGES value.
+   */
+  if (lc_messages != NULL && !g_str_equal (lc_messages, "C") && !g_str_equal (lc_messages, "POSIX"))
+    {
+      /* We want to consult the value of LC_MESSAGES to get a more
+       * specific value than we saw from LC_CTYPE, but we only want to
+       * do this in the case that LC_ALL was not set.
+       *
+       * We do not consult LANG because this would already have impacted
+       * the setting of LC_CTYPE as well.
+       */
+      if (!g_getenv ("LC_ALL"))
+        {
+          const gchar *var;
+
+          var = g_getenv ("LC_MESSAGES");
+
+          if (var)
+            lc_messages = var;
+        }
+    }
+#endif
+
+  /* Hopefully we now have a reasonable value for 'lc_messages'.
+   *
+   * If it's a C locale, just return NULL now, ignoring LANGUAGE.
+   */
+  if (lc_messages == NULL || g_str_equal (lc_messages, "C") || g_str_equal (lc_messages, "POSIX"))
+    return NULL;
+
+  /* Otherwise, check LANGUAGE. */
+  language = g_getenv ("LANGUAGE");
+
+  /* If LANGUAGE was set, split it out and return it. */
+  if (language)
+    return g_strsplit (language, ":", 0);
+
+  /* Otherwise, return the LC_MESSAGES value as a one-item array. */
+  {
+    gchar **locales;
+
+    locales = g_new (gchar *, 1 + 1);
+    locales[0] = g_strdup (lc_messages);
+    locales[1] = NULL;
+
+    return locales;
+  }
+}
+
+/* This function returns an array of arrays of strings such that each
+ * item in the outermost array corresponds to one language that the user
+ * has selected (with the first item being the most-preferred language).
+ *
+ * Each inner array gives a priority-ordered list of locales that would
+ * be a suitable match for that language.
+ *
+ * The idea is that for each item in the outer array, we should have a
+ * corresponding locale in the desktop file index that we perform
+ * searches on, even if it is the C locale (since missing translations
+ * would result in the C locale being used anyway).
+ *
+ * The very first matching locale is the one we use for querying
+ * strings, however.
+ */
+static const gchar * const * const *
+get_exploded_locale_list (void)
+{
+  static gchar ***list;
+
+  if (g_once_init_enter (&list))
+    {
+      gchar ***result;
+      gchar **locales;
+      gint i, n;
+
+      locales = get_locale_list ();
+      if (locales == NULL || locales[0] == NULL)
+        {
+          g_free (locales);
+          locales = g_new (gchar *, 2);
+          locales[0] = g_strdup ("C");
+          locales[1] = NULL;
+        }
+
+      n = g_strv_length (locales);
+
+      result = g_new (gchar **, n + 1);
+      for (i = 0; i < n; i++)
+        result[i] = g_get_locale_variants (locales[i]);
+      result[i] = NULL;
+
+      g_once_init_leave (&list, result);
+      g_free (locales);
+    }
+
+  return (const gchar * const * const *) list;
+}
+
+/* DesktopFileDirIndex implementation {{{1 */
+
 /* DesktopFileDir implementation {{{1 */
 
 typedef struct
@@ -157,6 +295,11 @@ typedef struct
   GHashTable                 *app_names;
   gboolean                    is_setup;
   struct dfi_index           *dfi;
+  gint8                      *key_id_map;
+  guint16                     key_id_map_length;
+  guint16                    *locale_ids;
+  guint16                     desktop_entry_id;
+  GHashTable                 *memory_index;
 } DesktopFileDir;
 
 static DesktopFileDir *desktop_file_dirs;
@@ -249,6 +392,168 @@ add_to_table_if_appropriate (GHashTable      *apps,
   g_hash_table_insert (apps, g_strdup (info->filename), info);
 }
 
+
+enum
+{
+  DESKTOP_KEY_nil,
+
+  /* NB: must keep this list sorted */
+  DESKTOP_KEY_Actions,
+  DESKTOP_KEY_Categories,
+  DESKTOP_KEY_Comment,
+  DESKTOP_KEY_DBusActivatable,
+  DESKTOP_KEY_Exec,
+  DESKTOP_KEY_GenericName,
+  DESKTOP_KEY_Hidden,
+  DESKTOP_KEY_Icon,
+  DESKTOP_KEY_Keywords,
+  DESKTOP_KEY_MimeType,
+  DESKTOP_KEY_Name,
+  DESKTOP_KEY_NoDisplay,
+  DESKTOP_KEY_NotShowIn,
+  DESKTOP_KEY_OnlyShowIn,
+  DESKTOP_KEY_Path,
+  DESKTOP_KEY_StartupNotify,
+  DESKTOP_KEY_StartupWMClass,
+  DESKTOP_KEY_Terminal,
+  DESKTOP_KEY_TryExec,
+  DESKTOP_KEY_Type,
+  DESKTOP_KEY_Version,
+  DESKTOP_KEY_X_GNOME_FullName,
+
+  N_DESKTOP_KEYS
+};
+
+const gchar *desktop_key_names[N_DESKTOP_KEYS];
+
+const gchar desktop_key_match_category[N_DESKTOP_KEYS] = {
+  /* NB: no harm in repeating numbers in case of a tie */
+  [DESKTOP_KEY_Name]             = 1,
+  [DESKTOP_KEY_GenericName]      = 2,
+  [DESKTOP_KEY_Keywords]         = 3,
+  [DESKTOP_KEY_X_GNOME_FullName] = 4,
+  [DESKTOP_KEY_Comment]          = 5
+};
+
+#define DESKTOP_KEY_NUM_MATCH_CATEGORIES  5
+
+static void
+desktop_key_init (void)
+{
+  if G_LIKELY (desktop_key_names[1])
+    return;
+
+  desktop_key_names[DESKTOP_KEY_Actions]                = g_intern_static_string ("Actions");
+  desktop_key_names[DESKTOP_KEY_Categories]             = g_intern_static_string ("Categories");
+  desktop_key_names[DESKTOP_KEY_Comment]                = g_intern_static_string ("Comment");
+  desktop_key_names[DESKTOP_KEY_DBusActivatable]        = g_intern_static_string ("DBusActivatable");
+  desktop_key_names[DESKTOP_KEY_Exec]                   = g_intern_static_string ("Exec");
+  desktop_key_names[DESKTOP_KEY_GenericName]            = g_intern_static_string ("GenericName");
+  desktop_key_names[DESKTOP_KEY_Hidden]                 = g_intern_static_string ("Hidden");
+  desktop_key_names[DESKTOP_KEY_Icon]                   = g_intern_static_string ("Icon");
+  desktop_key_names[DESKTOP_KEY_Keywords]               = g_intern_static_string ("Keywords");
+  desktop_key_names[DESKTOP_KEY_MimeType]               = g_intern_static_string ("MimeType");
+  desktop_key_names[DESKTOP_KEY_Name]                   = g_intern_static_string ("Name");
+  desktop_key_names[DESKTOP_KEY_NoDisplay]              = g_intern_static_string ("NoDisplay");
+  desktop_key_names[DESKTOP_KEY_NotShowIn]              = g_intern_static_string ("NotShowIn");
+  desktop_key_names[DESKTOP_KEY_OnlyShowIn]             = g_intern_static_string ("OnlyShowIn");
+  desktop_key_names[DESKTOP_KEY_Path]                   = g_intern_static_string ("Path");
+  desktop_key_names[DESKTOP_KEY_StartupNotify]          = g_intern_static_string ("StartupNotify");
+  desktop_key_names[DESKTOP_KEY_StartupWMClass]         = g_intern_static_string ("StartupWMClass");
+  desktop_key_names[DESKTOP_KEY_Terminal]               = g_intern_static_string ("Terminal");
+  desktop_key_names[DESKTOP_KEY_TryExec]                = g_intern_static_string ("TryExec");
+  desktop_key_names[DESKTOP_KEY_Type]                   = g_intern_static_string ("Type");
+  desktop_key_names[DESKTOP_KEY_Version]                = g_intern_static_string ("Version");
+  desktop_key_names[DESKTOP_KEY_X_GNOME_FullName]       = g_intern_static_string ("X-GNOME-FullName");
+}
+
+static void
+insert_into_list (GSList      **categories,
+                  const gchar  *item,
+                  gint          into_bucket,
+                  gint          max_hits)
+{
+  gint n_hits = 0;
+  gint i;
+
+  for (i = 0; i <= into_bucket; i++)
+    {
+      GSList *node;
+
+      /* If we have enough hits from previous categories, stop.
+       *
+       * Note.  This check will not be applied after counting up the
+       * hits in categories[into_bucket] and that's intentional: we
+       * don't want to toss out our new item if it has the same score as
+       * items we're already returning.
+       *
+       * That might mean that we get more than max_hits items, but
+       * that's the desired behaviour.
+       */
+      if (max_hits >= 0 && n_hits >= max_hits)
+        return;
+
+      /* We want to do the dup-checking in all cases, though */
+      for (node = categories[i]; node; node = node->next)
+        {
+          /* Pointer comparison is good enough because we're using the
+           * name from the desktop index (where it is unique) and
+           * because we'll never see the same desktop file name across
+           * multiple indexes due to our use of masking.
+           */
+          if (node->data == item)
+            return;
+
+          n_hits++;
+        }
+    }
+
+  /* No duplicate found at this level or any before, and not too many
+   * items in earlier buckets, so add our item.
+   */
+  categories[into_bucket] = g_slist_prepend (categories[into_bucket], (gchar *) item);
+  n_hits++;
+
+  /* We may need to remove ourselves from a later category if we already
+   * had a weaker hit for this item.
+   *
+   * We may also need to blow away an entire category if n_hits is big
+   * enough.
+   */
+  for (i = into_bucket + 1; i < DESKTOP_KEY_NUM_MATCH_CATEGORIES; i++)
+    {
+      if (!categories[i])
+        continue;
+
+      if (max_hits >= 0 && n_hits >= max_hits)
+        {
+          g_slist_free (categories[i]);
+          categories[i] = NULL;
+        }
+      else
+        {
+          GSList **ptr;
+
+          for (ptr = &categories[i]; *ptr; ptr = &(*ptr)->next)
+            if ((*ptr)->data == item)
+              {
+                /* Found us! */
+                *ptr = g_slist_delete_link (*ptr, *ptr);
+                n_hits--;
+
+                /* Since we effectively didn't add any items this time
+                 * around, we know we will not need to blow away later
+                 * categories.  We also know that we will not appear in
+                 * a later category, since we appeared here.
+                 *
+                 * So, we're done.
+                 */
+                return;
+              }
+        }
+    }
+}
+
 /* Support for unindexed DesktopFileDirs {{{2 */
 static void
 get_apps_from_dir (GHashTable **apps,
@@ -336,39 +641,346 @@ desktop_file_dir_unindexed_get_all (DesktopFileDir *dir,
     }
 }
 
-/* Support for indexed DesktopFileDirs {{{2 */
+typedef struct _MemoryIndexEntry MemoryIndexEntry;
+typedef GHashTable MemoryIndex;
 
-#if 0
-enum
+struct _MemoryIndexEntry
 {
-  DFI_ID_THIS_LOCALE,
+  const gchar      *app_name; /* pointer to the hashtable key */
+  gint              match_category;
+  MemoryIndexEntry *next;
+};
 
-  DFI_ID_Desktop_Entry,
+static void
+memory_index_entry_free (gpointer data)
+{
+  MemoryIndexEntry *mie = data;
 
-  DFI_ID_Name,
-  DFI_ID_Keywords,
-  DFI_ID_GenericName,
-  DFI_ID_X_GNOME_FullName,
-  DFI_ID_Comment,
+  while (mie)
+    {
+      MemoryIndexEntry *next = mie->next;
 
-  N_DFI_IDS
-};
-#endif
+      g_slice_free (MemoryIndexEntry, mie);
+      mie = next;
+    }
+}
+
+static void
+memory_index_add_token (MemoryIndex *mi,
+                        const gchar *token_start,
+                        const gchar *token_end,
+                        gint         match_category,
+                        const gchar *app_name)
+{
+  MemoryIndexEntry *mie, *first;
+  gchar *normal;
+  gchar *folded;
+
+  normal = g_utf8_normalize (token_start, token_end - token_start, G_NORMALIZE_ALL_COMPOSE);
+
+  /* TODO: Invent time machine.  Converse with Mustafa Ataturk... */
+  if (strstr (normal, "ı") || strstr (normal, "İ"))
+    {
+      gchar *s = normal;
+      GString *tmp;
+
+      tmp = g_string_new (NULL);
+
+      while (*s)
+        {
+          gchar *i, *I, *e;
+
+          i = strstr (s, "ı");
+          I = strstr (s, "İ");
+
+          if (!i && !I)
+            break;
+          else if (i && !I)
+            e = i;
+          else if (I && !i)
+            e = I;
+          else if (i < I)
+            e = i;
+          else
+            e = I;
+
+          g_string_append_len (tmp, s, e - s);
+          g_string_append_c (tmp, 'i');
+          s = g_utf8_next_char (e);
+        }
+
+      g_string_append (tmp, s);
+      g_free (normal);
+      normal = g_string_free (tmp, FALSE);
+    }
+
+  folded = g_utf8_casefold (normal, -1);
+
+  mie = g_slice_new (MemoryIndexEntry);
+  mie->app_name = app_name;
+  mie->match_category = match_category;
+
+  first = g_hash_table_lookup (mi, folded);
+
+  if (first)
+    {
+      mie->next = first->next;
+      first->next = mie;
+      g_free (folded); /* not using this... */
+    }
+  else
+    {
+      mie->next = NULL;
+      g_hash_table_insert (mi, folded, mie);
+    }
+
+  g_free (normal);
+}
+
+static void
+memory_index_add_string (MemoryIndex *mi,
+                         const gchar *string,
+                         gint         match_category,
+                         const gchar *app_name)
+{
+  const gchar *start = NULL;
+  const gchar *s;
+
+  for (s = string; *s; s = g_utf8_next_char (s))
+    {
+      gunichar c = g_utf8_get_char (s);
+
+      if (start == NULL)
+        {
+          if (g_unichar_isalnum (c))
+            start = s;
+        }
+      else
+        {
+          if (!g_unichar_isalnum (c))
+            {
+              memory_index_add_token (mi, start, s, match_category, app_name);
+              start = NULL;
+            }
+        }
+    }
+
+  if (start)
+    memory_index_add_token (mi, start, s, match_category, app_name);
+}
+
+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;
+
+  desktop_key_init ();
+
+  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;
+
+      key_file = g_key_file_new ();
+
+      if (g_key_file_load_from_file (key_file, path, G_KEY_FILE_NONE, 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_names[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,
+                                   GSList         **categories,
+                                   const gchar     *term,
+                                   gint             max_hits)
+{
+  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, term))
+        continue;
+
+      while (mie)
+        {
+          insert_into_list (categories, mie->app_name, mie->match_category - 1, max_hits);
+          mie = mie->next;
+        }
+    }
+}
+
+/* Support for indexed DesktopFileDirs {{{2 */
 
 static void
 desktop_file_dir_indexed_init (DesktopFileDir *dir)
 {
-  const struct dfi_string_list *app_names;
-  guint n_app_names, i;
+  desktop_key_init ();
+
+  /* Find the locale IDs to use */
+  {
+    const gchar * const * const *language_names;
+    const struct dfi_string_list *locale_names;
+    gint i, j, k, n;
+
+    /* From get_exploded_locale_list() we will get an array that looks
+     * something like so:
+     *
+     *  [ ['eo', 'C'], ['de_CH', 'de', 'C'], ['fr_CA', 'fr', 'C'] ]
+     *
+     * We want to produce a list of IDs such each item in the list
+     * corresponds to the 'best match' for each item in the original
+     * list.
+     *
+     * For example, if the index has entries for 'eo', 'fr_CA' and 'de',
+     * but not for 'de_CH' then our created array would have the ID
+     * numbers corresponding to the following locales:
+     *
+     *  [ eo, de, fr_CA ]
+     *
+     * In the event that the best match for a particular item in the
+     * list was the C locale, we simply exclude that item from being
+     * added.  So for example, if 'eo' was not in the index, we would
+     * not add the 'C' locale, but would rather just have this list:
+     *
+     *  [ de, fr_CA ]
+     *
+     * The C locale is always ID 0, so we use that as a terminator.
+     *
+     * As a minor nag, this will cause problems if "C" was explicitly
+     * listed among the items in LANGUAGE= but this would be a pretty
+     * silly thing to do, since lookups in C never fail and it's already
+     * the universal fallback...
+     */
+    locale_names = dfi_index_get_locale_names (dir->dfi);
+    language_names = get_exploded_locale_list ();
+    for (n = 0; language_names[n]; n++)
+      ;
+
+    /* Allocate an array under the assumption that we will fill it.  If
+     * we don't, it's no big loss.
+     *
+     * Note: the search code assumes that locale_ids will always have at
+     * least two items, and that even if locale_ids[0] is zero,
+     * locale_ids[1] will also be zero.
+     *
+     * Do not modify this allocation in a way that would invalidate that
+     * assumption.
+     */
+    g_assert (n != 0);
+    dir->locale_ids = g_new0 (guint16, n + 1);
+    i = 0;  /* We use 'i' as a moving index into locale_ids, as we add items */
+
+    /* Iterate over our locales, looking up the ID for each */
+    for (j = 0; language_names[j]; j++)
+      for (k = 0; language_names[j][k] && !g_str_equal (language_names[j][k], "C"); k++)
+        {
+          gint result = dfi_string_list_binary_search (locale_names, dir->dfi, language_names[j][k]);
+
+          if (result >= 0)
+            {
+              dir->locale_ids[i++] = result;
+              break;
+            }
+        }
+    g_assert (i <= n);
+  }
+
+  /* Populate the app names list. */
+  {
+    const struct dfi_string_list *app_names;
+    guint n_app_names, i;
+
+    app_names = dfi_index_get_app_names (dir->dfi);
+    n_app_names = dfi_string_list_get_length (app_names);
+
+    /* Store the index of each application so we don't need to do binary
+     * searches to find applications.
+     */
+    dir->app_names = g_hash_table_new (g_str_hash, g_str_equal);
+
+    for (i = 0; i < n_app_names; i++)
+      g_hash_table_insert (dir->app_names,
+                           (gchar *) dfi_string_list_get_string_at_index (app_names, dir->dfi, i),
+                           GUINT_TO_POINTER (i));
+  }
 
-  dir->app_names = g_hash_table_new (g_str_hash, g_str_equal);
-  app_names = dfi_index_get_app_names (dir->dfi);
-  n_app_names = dfi_string_list_get_length (app_names);
+  /* Find the ID of the [Desktop Entry] group */
+  {
+    const struct dfi_string_list *group_names;
 
-  for (i = 0; i < n_app_names; i++)
-    g_hash_table_insert (dir->app_names,
-                         (gchar *) dfi_string_list_get_string_at_index (app_names, dir->dfi, i),
-                         GUINT_TO_POINTER (i));
+    group_names = dfi_index_get_group_names (dir->dfi);
+
+    dir->desktop_entry_id = dfi_string_list_binary_search (group_names, dir->dfi, "Desktop Entry");
+  }
+
+  /* Populate the key id map */
+  {
+    const struct dfi_string_list *key_names;
+    guint n_key_names;
+    guint i = 0;
+    guint j = 1;
+
+    key_names = dfi_index_get_key_names (dir->dfi);
+    n_key_names = dfi_string_list_get_length (key_names);
+
+    dir->key_id_map = g_malloc0 (n_key_names);
+    dir->key_id_map_length = n_key_names;
+
+    while (i < n_key_names && j < N_DESKTOP_KEYS)
+      {
+        const gchar *keyname = dfi_string_list_get_string_at_index (key_names, dir->dfi, i);
+        gint cmp;
+
+        cmp = strcmp (keyname, desktop_key_names[j]);
+
+        if (cmp == 0)
+          dir->key_id_map[i++] = j++;
+        else if (cmp < 0)
+          i++;
+        else
+          j++;
+      }
+  }
 }
 
 static GDesktopAppInfo *
@@ -410,6 +1022,96 @@ desktop_file_dir_indexed_get_all (DesktopFileDir *dir,
     }
 }
 
+static void
+desktop_file_dir_indexed_search (DesktopFileDir  *dir,
+                                 GSList         **categories,
+                                 const gchar     *term,
+                                 gint             max_hits)
+{
+  const struct dfi_pointer_array *text_indexes;
+  const struct dfi_string_list *app_names;
+  const struct dfi_text_index *text_index;
+  const struct dfi_text_index_item *start;
+  const struct dfi_text_index_item *end;
+  const struct dfi_text_index_item *item;
+  gint i;
+
+  /* We search every locale...
+   *
+   * If the user lists off multiple languages in LANGUAGE then it's
+   * probably because they understand multiple languages.
+   *
+   * If a particular app is translated or not for a given locale is
+   * semi-random, so don't let the existence of a translation in a
+   * higher-priority language impact the ability to find an app by
+   * searching in the lower-priority language.
+   *
+   * Note: if the only available locale is C, then we want to allow
+   * searching there...  This will be the case if locale_list[0] is 0,
+   * so we always permit i = 0 to loop.
+   *
+   * Note: locale_ids always has at least two items in it, and it is
+   * allocated zero-filled, so locale_ids[1] is safe, even if
+   * locale_ids[0] was 0.
+   */
+  for (i = 0; i == 0 || dir->locale_ids[i]; i++)
+    {
+      text_indexes = dfi_index_get_text_indexes (dir->dfi);
+      text_index = dfi_text_index_from_pointer (dir->dfi,
+                                                dfi_pointer_array_get_pointer (text_indexes, 
dir->locale_ids[i]));
+      if (text_index == NULL)
+        return;
+
+      dfi_text_index_prefix_search (text_index, dir->dfi, term, &start, &end);
+
+      app_names = dfi_index_get_app_names (dir->dfi);
+
+      for (item = start; item < end; item++)
+        {
+          const dfi_id *ids;
+          guint n_ids;
+
+          gint j;
+
+          ids = dfi_text_index_item_get_ids (item, dir->dfi, &n_ids);
+
+          if (n_ids % 3 != 0)
+            continue;
+
+          for (j = 0; j < n_ids; j += 3)
+            {
+              guint16 app_id, group_id, key_id;
+              const gchar *app_name;
+              guint8 match_category;
+
+              app_id   = dfi_id_get (ids[j + 0]);
+              group_id = dfi_id_get (ids[j + 1]);
+              key_id   = dfi_id_get (ids[j + 2]);
+
+              app_name = dfi_string_list_get_string_at_index (app_names, dir->dfi, app_id);
+              if (!app_name)
+                continue;
+
+              if (desktop_file_dir_app_name_is_masked (dir, app_name))
+                continue;
+
+              if (group_id != dir->desktop_entry_id)
+                continue;
+
+              if (key_id >= dir->key_id_map_length)
+                continue;
+
+              match_category = desktop_key_match_category[dir->key_id_map[key_id]];
+
+              if (!match_category)
+                continue;
+
+              insert_into_list (categories, app_name, match_category - 1, max_hits);
+            }
+        }
+    }
+}
+
 /* DesktopFileDir "API" {{{2 */
 
 /*< internal >
@@ -459,6 +1161,27 @@ desktop_file_dir_reset (DesktopFileDir *dir)
       dir->dfi = NULL;
     }
 
+  if (dir->key_id_map)
+    {
+      g_free (dir->key_id_map);
+      dir->key_id_map = NULL;
+    }
+
+  if (dir->locale_ids)
+    {
+      g_free (dir->locale_ids);
+      dir->locale_ids = NULL;
+    }
+
+  if (dir->memory_index)
+    {
+      g_hash_table_unref (dir->memory_index);
+      dir->memory_index = NULL;
+    }
+
+  dir->key_id_map_length = 0;
+  dir->desktop_entry_id = 0;
+
   dir->is_setup = FALSE;
 }
 
@@ -537,6 +1260,27 @@ desktop_file_dir_get_all (DesktopFileDir *dir,
     desktop_file_dir_unindexed_get_all (dir, apps);
 }
 
+#define N_SEARCH_BUCKETS 1
+
+/*< 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,
+                         GSList         **categories,
+                         const gchar     *term,
+                         gint             max_hits)
+{
+  if (dir->dfi)
+    desktop_file_dir_indexed_search (dir, categories, term, max_hits);
+  else
+    desktop_file_dir_unindexed_search (dir, categories, term, max_hits);
+}
+
 /* Lock/unlock and global setup API {{{2 */
 
 static void
@@ -3211,6 +3955,148 @@ g_app_info_get_default_for_uri_scheme (const char *uri_scheme)
 /* "Get all" API {{{2 */
 
 /**
+ * g_desktop_app_info_search:
+ * @token: the search token
+ * @sort_by: (scope call) (allow-none): a string sorting function
+ * @user_data: (closure sort_by): user_data for @sort_by
+ * @max_hits: the maximum number of hits, or -1
+ *
+ * Searches desktop files for ones that match @token.
+ *
+ * @token must be a casefolded single word.
+ *
+ * Returns: (transfer full): the names of desktop files that match
+ */
+gchar **
+g_desktop_app_info_search (const char             *token,
+                           GStringCompareDataFunc  sort_by,
+                           gpointer                user_data,
+                           gint                    max_hits)
+{
+  GSList *categories[DESKTOP_KEY_NUM_MATCH_CATEGORIES] = { NULL, };
+  gchar **results;
+  gint i, j, n;
+
+  desktop_file_dirs_lock ();
+
+  for (i = 0; i < n_desktop_file_dirs; i++)
+    desktop_file_dir_search (&desktop_file_dirs[i], categories, token, max_hits);
+
+  /* Must hold lock while processing results because otherwise strings
+   * could go missing out from under us.
+   */
+  n = 0;
+  for (i = 0; i < DESKTOP_KEY_NUM_MATCH_CATEGORIES; i++)
+    {
+      if (sort_by)
+        categories[i] = g_slist_sort_with_data (categories[i], (GCompareDataFunc) sort_by, user_data);
+
+      n += g_slist_length (categories[i]);
+
+      if (max_hits >= 0 && n >= max_hits)
+        {
+          n = max_hits;
+          break;
+        }
+    }
+
+  results = g_new (gchar *, n + 1);
+  j = 0; /* j used to fill 'results' */
+
+  for (i = 0; i < DESKTOP_KEY_NUM_MATCH_CATEGORIES; i++)
+    {
+      GSList *node = categories[i];
+
+      while (node && j < n)
+        {
+          GSList *next = node->next;
+
+          results[j++] = g_strdup (node->data);
+          g_slist_free_1 (node);
+          node = next;
+        }
+
+      /* Clean up any leftovers */
+      g_slist_free (node);
+    }
+
+  g_assert_cmpint (j, ==, n);
+  results[j] = NULL;
+
+  desktop_file_dirs_unlock ();
+
+  return results;
+}
+
+/**
+ * g_desktop_app_info_search_full:
+ * @token: the token to search for
+ * @max_hits: the approximate maximum number of hits to return, or -1
+ *
+ * Searches desktop files for ones that match @token.
+ *
+ * @token must be a casefolded single word.
+ *
+ * The return value is an array of strvs.  Each strv contains a list of
+ * applications that matched @token with an equal score.  The outer list
+ * is sorted by score so that the first strv contains the best-matching
+ * applications, and so on.
+ *
+ * This function will either return all items that matched with the same
+ * score, or none of them.  It is therefore possible that more than
+ * @max_hits items will be returned.
+ *
+ * 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_full (const gchar *token,
+                                gint         max_hits)
+{
+  GSList *categories[DESKTOP_KEY_NUM_MATCH_CATEGORIES] = { NULL, };
+  gchar ***results;
+  gint i, k;
+
+  desktop_file_dirs_lock ();
+
+  for (i = 0; i < n_desktop_file_dirs; i++)
+    desktop_file_dir_search (&desktop_file_dirs[i], categories, token, max_hits);
+
+  /* As above, must hold the lock. */
+  results = g_new (gchar **, DESKTOP_KEY_NUM_MATCH_CATEGORIES + 1);
+  k = 0; /* Used to fill 'results' */
+
+  /* Convert each 'category' into a strv and store in 'results' */
+  for (i = 0; i < DESKTOP_KEY_NUM_MATCH_CATEGORIES; i++)
+    {
+      gint length = g_slist_length (categories[i]);
+
+      /* Don't bother storing empty sublists */
+      if (length > 0)
+        {
+          GSList *node;
+          gint j;
+
+          results[k] = g_new (gchar *, length + 1);
+
+          j = 0;
+          for (node = categories[i]; node; node = node->next)
+            results[k][j++] = g_strdup (node->data);
+          g_assert_cmpint (j, ==, length);
+          results[k][j] = NULL;
+          k++;
+        }
+    }
+  g_assert_cmpint (k, <=, DESKTOP_KEY_NUM_MATCH_CATEGORIES);
+  results[k] = NULL;
+
+  desktop_file_dirs_unlock ();
+
+  return results;
+}
+
+/**
  * g_app_info_get_all:
  *
  * Gets a list of all of the applications currently registered
diff --git a/gio/gdesktopappinfo.h b/gio/gdesktopappinfo.h
index 5f7f68a..69cc810 100644
--- a/gio/gdesktopappinfo.h
+++ b/gio/gdesktopappinfo.h
@@ -165,6 +165,14 @@ gboolean    g_desktop_app_info_launch_uris_as_manager (GDesktopAppInfo
                                                       GError                    **error);
 
 
+GLIB_AVAILABLE_IN_2_30
+gchar ** g_desktop_app_info_search (const gchar *term,
+                                    GStringCompareDataFunc sort_by,
+                                    gpointer user_data,
+                                    gint max_hits);
+GLIB_AVAILABLE_IN_2_30
+gchar *** g_desktop_app_info_search_full (const gchar *term, gint max_hits);
+
 G_END_DECLS
 
 #endif /* __G_DESKTOP_APP_INFO_H__ */


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]