Writing a 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. Basics ------ For an application to become a search provider, it should implement the following D-Bus interface: .. code:: .. note:: You can also find a copy of this D-Bus interface definition at ``$(datadir)/dbus-1/interfaces/org.gnome.ShellSearchProvider2.xml`` Registering a new search provider ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to register the search provider with GNOME Shell, you must provide a key/value file in ``$(datadir)/gnome-shell/search-providers`` for your provider. 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 After you restart the Shell, the queries will now be forwarded to the search provider using the given D-Bus name. 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. 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()`` virtual function 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 alternatively, * "icon-data": a tuple of type ``(iiibiiay)`` describing a pixbuf with width, height, rowstride, has-alpha, bits-per-sample, n-channels, and image data * "description": an optional short description (1-2 lines) * "clipboardText": an optional text to send to the clipboard on activation ``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. You should generate the ``GDBusInterfaceSkeleton`` using ``gdbus-codegen`` through the "gnome" Meson module: .. code:: gnome = import('gnome') sp_sources = gnome.gdbus_codegen( 'shell-search-provider-generated', sources: 'org.gnome.Shell.SearchProvider2.xml', interface_prefix : 'org.gnome.', namespace : 'Bar', ) Then you will need to override the ``dbus_register()`` and ``dbus_unregister()`` virtual functions of your ``GtkApplication`` in order to export and unexport the interface at the given path: .. code:: c 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); } static gboolean bar_application_dbus_register (GApplication *application, GDBusConnection *connection, const gchar *object_path, GError **error) { BarApplication *self = BAR_APPLICATION (application); // Chain up to the parent's implementation if (!G_APPLICATION_CLASS (bar_application_parent_class) ->dbus_register (application, connection, object_path, error)) return FALSE; g_autofree search_provider_path = g_strconcat (object_path, "/SearchProvider", NULL); // Export the SearchProvider interface to the given path if (!bar_search_provider_dbus_export (self->search_provider, connection, search_provider_path, error)) return FALSE; return TRUE; } static void bar_application_dbus_unregister (GApplication *application, GDBusConnection *connection, const gchar *object_path) { BarApplication *self = BAR_APPLICATION (application); g_autofree char *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); } You will need to create a search provider object as the implementation of the interface: .. code:: c G_DECLARE_FINAL_TYPE (BarSearchProvider, bar_search_provider, BAR, SEARCH_PROVIDER, GObject) struct _BarSearchProvider { GObject parent_instance; ShellSearchProvider2 *skeleton; }; 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); } .. important:: If your D-Bus method handlers are asynchronous, then you should hold a reference to BarApplication using ``g_application_hold()`` and then release the reference using ``g_application_release()`` when you are done. Use the corresponding ``shell_search_provider2_complete*`` methods to indicate completion and send back results, if any. Here's a pair of convenience wrapper methods for exporting and unexporting the skeleton: .. code:: c 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); } }