打算 hack 一下 GNOME Shell 的搜索功能,所以稍微调查了一下。

Javascript-side

首先当然是从 Javascript 代码下手啦。首先找到 js/ui/search.js

不过从这里我们也能看出其工作方式:

  • GridSearchResultListSearchResult 决定了搜索结果呈现的形态是 App 图标矩阵还是列表。
  • ProviderInfo 是搜索结果左边的 Button。
  • updateSearch 等接口摆明了结果可以异步获取。

它给出了搜索界面有关组件的接口,但并没有任何和搜索本身相关的东西。很快发现

this._registerProvider(new AppDisplay.AppSearchProvider());

于是找到 js/ui/appDisplay.js 下,就找到了我们要找的 AppSearchProvider,重点关注

    getInitialResultSet: function(terms, callback, cancellable) {
        let query = terms.join(' ');
        let groups = Shell.AppSystem.search(query);
        let usage = Shell.AppUsage.get_default();
        let results = [];
        groups.forEach(function(group) {
            group = group.filter(function(appID) {
                let app = Gio.DesktopAppInfo.new(appID);
                return app && app.should_show();
            });
            results = results.concat(group.sort(function(a, b) {
                return usage.compare('', a, b);
            }));
        });
        callback(results);
    }

再找 Shell.AppSystem.search,发现居然跑到 C 代码里了,实现是 shell-app-system.c 中 的 shell_app_system_search()

char ***
shell_app_system_search (const char *search_string)
{
  char ***results = g_desktop_app_info_search (search_string);
  char ***groups, **ids;

  for (groups = results; *groups; groups++)
    for (ids = *groups; *ids; ids++)
      if (!g_utf8_validate (*ids, -1, NULL))
        **ids = '\0';

  return results;
}

GLib-side

熟练地调出 DevHelp 查 g_desktop_app_info_search:「The algorithm for determining matches is undefined and may change at any time.」噗…(吐血

得,继续查源码吧。再经过一番毫不费力的追踪,找到了这样两个比较重要的函数:

static void
desktop_file_dir_unindexed_setup_search (DesktopFileDir *dir)
{
  GHashTableIter iter;
  gpointer app, path;

  dir->memory_index = memory_index_new ();
  dir->memory_implementations = memory_index_new ();

  /* Nothing to search? */
  if (dir->app_names == NULL)
    return;

  g_hash_table_iter_init (&iter, dir->app_names);
  while (g_hash_table_iter_next (&iter, &app, &path))
    {
      GKeyFile *key_file;

      if (desktop_file_dir_app_name_is_masked (dir, app))
        continue;

      key_file = g_key_file_new ();

      if (g_key_file_load_from_file (key_file, path, G_KEY_FILE_NONE, NULL) &&
          !g_key_file_get_boolean (key_file, "Desktop Entry", "Hidden", NULL))
        {
          /* Index the interesting keys... */
          gchar **implements;
          gint i;

          for (i = 0; i < G_N_ELEMENTS (desktop_key_match_category); i++)
            {
              const gchar *value;
              gchar *raw;

              if (!desktop_key_match_category[i])
                continue;

              raw = g_key_file_get_locale_string (key_file, "Desktop Entry", desktop_key_get_name (i), NULL, NULL);
              value = raw;

              if (i == DESKTOP_KEY_Exec && raw != NULL)
                {
                  /* Special handling: only match basename of first field */
                  gchar *space;
                  gchar *slash;

                  /* Remove extra arguments, if any */
                  space = raw + strcspn (raw, " \t\n"); /* IFS */
                  *space = '\0';

                  /* Skip the pathname, if any */
                  if ((slash = strrchr (raw, '/')))
                    value = slash + 1;
                }

              if (value)
                memory_index_add_string (dir->memory_index, value, desktop_key_match_category[i], app);

              g_free (raw);
            }

          /* Make note of the Implements= line */
          implements = g_key_file_get_string_list (key_file, "Desktop Entry", "Implements", NULL, NULL);
          for (i = 0; implements && implements[i]; i++)
            memory_index_add_token (dir->memory_implementations, implements[i], 0, app);
          g_strfreev (implements);
        }

      g_key_file_free (key_file);
    }
}

static void
desktop_file_dir_unindexed_search (DesktopFileDir  *dir,
                                   const gchar     *search_token)
{
  GHashTableIter iter;
  gpointer key, value;

  if (!dir->memory_index)
    desktop_file_dir_unindexed_setup_search (dir);

  g_hash_table_iter_init (&iter, dir->memory_index);
  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      MemoryIndexEntry *mie = value;

      if (!g_str_has_prefix (key, search_token))
        continue;

      while (mie)
        {
          add_token_result (mie->app_name, mie->match_category);
          mie = mie->next;
        }
    }
}

看看 g_str_has_prefix,本来想发一通牢骚,不过克制住了,毕竟,此处无声胜有声……

所以呢?

可以看出 GLib 匹配时是粗暴地把 .application 拆成 token,然后比较前缀。那么他所谓的 the interesting keys 是些什么呢?

enum
{
  DESKTOP_KEY_Comment,
  DESKTOP_KEY_Exec,
  DESKTOP_KEY_GenericName,
  DESKTOP_KEY_Keywords,
  DESKTOP_KEY_Name,
  DESKTOP_KEY_X_GNOME_FullName,

  N_DESKTOP_KEYS
};

const gchar desktop_key_match_category[N_DESKTOP_KEYS] = {
  /* Note: lower numbers are a better match.
   *
   * In case we want two keys to match at the same level, we can just
   * use the same number for the two different keys.
   */
  [DESKTOP_KEY_Name]             = 1,
  [DESKTOP_KEY_Exec]             = 2,
  [DESKTOP_KEY_Keywords]         = 3,
  [DESKTOP_KEY_GenericName]      = 4,
  [DESKTOP_KEY_X_GNOME_FullName] = 5,
  [DESKTOP_KEY_Comment]          = 6
};

可见,就是 Exec Name Keywords Name GenericName X_GNOME_FullName,并按下面这个数组的顺序依次匹配。

总之,知道这些应该就够了,毕竟要 hack 的是 GNOME Shell,而 GLib 我们没法直接 hack,对吧。