Search Provider

Search is a central concept in the GNOME user experience. The search entry in the shell overview is the place to go for quick searches.

A search provider is a mechanism by which an application can expose its search capabilities to GNOME Shell. When the user types anything in the shell's search entry, the text is forwarded to all known search providers, and the results are relayed back for display.

In the shell overview, search hits are grouped against their respective applications and a maximum of three is shown per application. The user can either select an individual result, in which case the application SHOULD open it; or she can select the application icon, in which case it COULD show an in-app view of all the results from this specific application without any limitation.

The exact meaning of open depends on the application in question. Files and Documents offer a preview of the item's content; Software shows an UI to install the application; and Terminal windows are simply brought into focus. If possible, the applications SHOULD offer a way to go 'back' to its search view, which should be pre-populated with the same search that was done in the shell. This lets the user continue to refine his search inside the application.

Applications should be prepared to handle repeated queries as the user types more characters into the shell search entry.

gnome-shell-search-results.png

Basics

For an application to become a search provider, it should implement the following D-Bus interface:

<node>
  <interface name="org.gnome.Shell.SearchProvider2">

    <method name="GetInitialResultSet">
      <arg type="as" name="terms" direction="in" />
      <arg type="as" name="results" direction="out" />
    </method>

    <method name="GetSubsearchResultSet">
      <arg type="as" name="previous_results" direction="in" />
      <arg type="as" name="terms" direction="in" />
      <arg type="as" name="results" direction="out" />
    </method>

    <method name="GetResultMetas">
      <arg type="as" name="identifiers" direction="in" />
      <arg type="aa{sv}" name="metas" direction="out" />
    </method>

    <method name="ActivateResult">
      <arg type="s" name="identifier" direction="in" />
      <arg type="as" name="terms" direction="in" />
      <arg type="u" name="timestamp" direction="in" />
    </method>

    <method name="LaunchSearch">
      <arg type="as" name="terms" direction="in" />
      <arg type="u" name="timestamp" direction="in" />
    </method>

  </interface>
</node>

You can also find a copy of this D-Bus interface definition at $(datadir)/dbus-1/interfaces/org.gnome.ShellSearchProvider2.xml

Register the search provider with GNOME Shell by dropping a file in $(datadir)/gnome-shell/search-providers.

Let's assume that we have an application called Foo Bar that is D-Bus activatable, where Foo is the project-wide namespace and Bar is the name of the application. Then we can create a file called foo.bar.search-provider.ini as:

[Shell Search Provider]
DesktopId=foo.Bar.desktop
BusName=foo.Bar
ObjectPath=/foo/Bar/SearchProvider
Version=2

Restart the shell to have it detect your search provider. Search queries from the user will now be forwarded to your application.

Configuration

The GNOME control-center has a settings panel that allows the user to configure which search providers to use in the shell, and in what order to show their results. This is handled completely between the control-center and the shell, your application is not involved other than providing the name its desktop file in the DesktopId key. Both the shell and the control-center use this key to find the icon and name to use in the UI to represent your search provider.

search-provider-configuration.png

Details

Keep in mind is that activating the search provider by entering text in the shell search entry should not visibly launch the application. It is still possible to implement the search provider in the same binary as the application itself (which has the advantage that you can share the search implementation between the search provider and the in-application search); just make sure that activating the application starts it in 'service' mode. The startup() vfunc should not open any windows, that should only be done in activate() or open().

Another point to keep in mind is that searching in the shell should not affect the UI of your application if it is already running until the user explicitly chooses to open (one or all) search results with the application. Once the user does that, it is expected that you reuse the already open window and switch it to the search view.

All methods of the SearchProvider interface should be implemented asynchronously, in particular, you should handle overlapping subsearch requests, as the user keeps typing in the shell search entry. A common way to deal with this is to cancel the previous search request when a new one comes in.

The SearchProvider interface

GetInitialResultSet :: (as) → (as)

GetInitialResultSet is called when a new search is started. It gets an array of search terms as arguments, and should return an array of result IDs. gnome-shell will call GetResultMetas for (some) of these result IDs to get details about the result that can be be displayed in the result list.

GetSubsearchResultSet :: (as,as) → (as)

GetSubsearchResultSet is called to refine the initial search results when the user types more characters in the search entry. It gets the previous search results and the current search terms as arguments, and should return an array of result IDs, just like GetInitialResulSet.

GetResultMetas :: (as) → (aa{sv})

GetResultMetas is called to obtain detailed information for results. It gets an array of result IDs as arguments, and should return a matching array of dictionaries (ie one a{sv} for each passed-in result ID). The following pieces of information should be provided for each result:

  • "id": the result ID
  • "name": the display name for the result
  • "icon": a serialized GIcon (see g_icon_serialize()), or alternatively,
  • "gicon": a textual representation of a GIcon (see g_icon_to_string()), or alternativly,
  • "icon-data": a tuple of type (iiibiiay) describing a pixbuf with width, height, rowstride, has-alpha, bits-per-sample, and image data
  • "description": an optional short description (1-2 lines)

ActivateResult :: (s,as,u) → ()

ActivateResult is called when the user clicks on an individual result to open it in the application. The arguments are the result ID, the current search terms and a timestamp.

LaunchSearch :: (as,u) → ()

LaunchSearch is called when the user clicks on the provider icon to display more search results in the application. The arguments are the current search terms and a timestamp.

Implementation

You can add a search provider to an application that is using GtkApplication by exporting the GDBusInterfaceSkeleton that implementing the search provider interface in the dbus_register() vfunc. Often the search provider is in the same binary as the application itself because it has the advantage of being able to share the search implementation, but this is not mandatory.

Let's assume that we have an application called Foo Bar, where Foo is the project-wide namespace and Bar is the name of the application. Following are some typical examples of how it would look like in different languages.

C example

The dbus_register() and dbus_unregister() vfuncs are overridden in BarApplication:

G_DEFINE_TYPE (BarApplication, bar_application, GTK_TYPE_APPLICATION);

static void
bar_application_class_init (BarApplicationClass *class)
{
  GApplicationClass *application_class = G_APPLICATION_CLASS (class);

  application_class->dbus_register = bar_application_dbus_register;
  application_class->dbus_unregister = bar_application_dbus_unregister;
}

GtkApplication *
bar_application_new (void)
{
  return g_object_new (BAR_TYPE_APPLICATION,
                       "application-id", "foo.bar",
                       NULL);
}

The application has claimed foo.bar on the bus and the object is exported at /foo/Bar/SearchProvider:

static gboolean
bar_application_dbus_register (GApplication *application,
                               GDBusConnection *connection,
                               const gchar *object_path,
                               GError **error)
{
  BarApplication *self = BAR_APPLICATION (application);
  gboolean ret_val = FALSE;
  gchar *search_provider_path = NULL;

  if (!G_APPLICATION_CLASS (bar_application_parent_class)
         ->dbus_register (application,
                          connection,
                          object_path,
                          error))
    goto out;

  search_provider_path = g_strconcat (object_path, "/SearchProvider", NULL);
  if (!bar_search_provider_dbus_export (self->search_provider,
                                        connection,
                                        search_provider_path,
                                        error))
    goto out;

  ret_val = TRUE;

 out:
  g_free (search_provider_path);
  return ret_val;
}

And then removed during cleanup:

static void
bar_application_dbus_unregister (GApplication *application,
                                 GDBusConnection *connection,
                                 const gchar *object_path)
{
  BarApplication *self = BAR_APPLICATION (application);
  gchar *search_provider_path = NULL;

  search_provider_path = g_strconcat (object_path, "/SearchProvider", NULL);
  bar_search_provider_dbus_unexport (self->search_provider,
                                     connection,
                                     search_provider_path);

  G_APPLICATION_CLASS (bar_application_parent_class)
    ->dbus_unregister (application,
                       connection,
                       object_path);

  g_free (search_provider_path);
}

You can generate a GDBusInterfaceSkeleton that implements the org.gnome.ShellSearchProvider2 interface using gdbus-codegen in Makefile.am:

shell-search-provider2.h shell-search-provider2.c: org.gnome.ShellSearchProvider2.xml
        $(AM_V_GEN)gdbus-codegen \
                --c-namespace Shell \
                --generate-c-code shell-search-provider2 \
                --interface-prefix org.gnome.Shell. \
                $<

You can either depend on GNOME Shell to provide the D-Bus interface definition in org.gnome.ShellSearchProvider2.xml or copy it into your source tree.

This is then used in BarSearchProvider which has the guts of the search provider implementation:

struct _BarSearchProvider
{
  GObject parent_instance;
  ShellSearchProvider2 *skeleton;
};

struct _BarSearchProviderClass
{
  GObjectClass parent_class;
};

G_DEFINE_TYPE (BarSearchProvider, bar_search_provider, G_TYPE_OBJECT);

static void
bar_search_provider_init (BarSearchProvider *self)
{
  self->skeleton = shell_search_provider2_skeleton_new ();

  g_signal_connect_swapped (self->skeleton,
                            "handle-activate-result",
                            G_CALLBACK (bar_search_provider_activate_result),
                            self);
  g_signal_connect_swapped (self->skeleton,
                            "handle-get-initial-result-set",
                            G_CALLBACK (bar_search_provider_get_initial_result_set),
                            self);
  g_signal_connect_swapped (self->skeleton,
                            "handle-get-subsearch-result-set",
                            G_CALLBACK (bar_search_provider_get_subsearch_result_set),
                            self);
  g_signal_connect_swapped (self->skeleton,
                            "handle-get-result-metas",
                            G_CALLBACK (bar_search_provider_get_result_metas),
                            self);
  g_signal_connect_swapped (self->skeleton,
                            "handle-launch-search",
                            G_CALLBACK (bar_search_provider_launch_search),
                            self);
}

If your D-Bus method handlers are asynchronous, then you should hold a reference to BarApplication using g_application_hold and g_application_release it when you are done. Use the corresponding shell_search_provider2_complete* methods to indicate completion and send back results, if any.

Convenience wrapper methods to export and unexport the skeleton:

gboolean
bar_search_provider_dbus_export (BarSearchProvider *self,
                                 GDBusConnection *connection,
                                 const gchar *object_path,
                                 GError **error)
{
  return g_dbus_interface_skeleton_export (
           G_DBUS_INTERFACE_SKELETON (self->skeleton),
           connection,
           object_path,
           error);
}

void
bar_search_provider_dbus_unexport (BarSearchProvider *self,
                                   GDBusConnection *connection,
                                   const gchar *object_path)
{
  if (g_dbus_interface_skeleton_has_connection (
        G_DBUS_INTERFACE_SKELETON (self->skeleton),
        connection))
    {
      g_dbus_interface_skeleton_unexport_from_connection (
        G_DBUS_INTERFACE_SKELETON (self->skeleton),
        connection);
    }
}

Examples

Many GNOME application have search providers, here are some examples you can study:

HowDoI/SearchProvider (last edited 2017-08-28 08:21:19 by NielsDeGraef)