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:
<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>
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:
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:
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:
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:
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);
}
}