summaryrefslogtreecommitdiffstats
path: root/js/ui/remoteSearch.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/remoteSearch.js')
-rw-r--r--js/ui/remoteSearch.js332
1 files changed, 332 insertions, 0 deletions
diff --git a/js/ui/remoteSearch.js b/js/ui/remoteSearch.js
new file mode 100644
index 0000000..87ee384
--- /dev/null
+++ b/js/ui/remoteSearch.js
@@ -0,0 +1,332 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported loadRemoteSearchProviders */
+
+const { GdkPixbuf, Gio, GLib, Shell, St } = imports.gi;
+
+const FileUtils = imports.misc.fileUtils;
+
+const KEY_FILE_GROUP = 'Shell Search Provider';
+
+const SearchProviderIface = `
+<node>
+<interface name="org.gnome.Shell.SearchProvider">
+<method name="GetInitialResultSet">
+ <arg type="as" direction="in" />
+ <arg type="as" direction="out" />
+</method>
+<method name="GetSubsearchResultSet">
+ <arg type="as" direction="in" />
+ <arg type="as" direction="in" />
+ <arg type="as" direction="out" />
+</method>
+<method name="GetResultMetas">
+ <arg type="as" direction="in" />
+ <arg type="aa{sv}" direction="out" />
+</method>
+<method name="ActivateResult">
+ <arg type="s" direction="in" />
+</method>
+</interface>
+</node>`;
+
+const SearchProvider2Iface = `
+<node>
+<interface name="org.gnome.Shell.SearchProvider2">
+<method name="GetInitialResultSet">
+ <arg type="as" direction="in" />
+ <arg type="as" direction="out" />
+</method>
+<method name="GetSubsearchResultSet">
+ <arg type="as" direction="in" />
+ <arg type="as" direction="in" />
+ <arg type="as" direction="out" />
+</method>
+<method name="GetResultMetas">
+ <arg type="as" direction="in" />
+ <arg type="aa{sv}" direction="out" />
+</method>
+<method name="ActivateResult">
+ <arg type="s" direction="in" />
+ <arg type="as" direction="in" />
+ <arg type="u" direction="in" />
+</method>
+<method name="LaunchSearch">
+ <arg type="as" direction="in" />
+ <arg type="u" direction="in" />
+</method>
+</interface>
+</node>`;
+
+var SearchProviderProxyInfo = Gio.DBusInterfaceInfo.new_for_xml(SearchProviderIface);
+var SearchProvider2ProxyInfo = Gio.DBusInterfaceInfo.new_for_xml(SearchProvider2Iface);
+
+/**
+ * loadRemoteSearchProviders:
+ *
+ * @param {Gio.Settings} searchSettings - search settings
+ * @returns {RemoteSearchProvider[]} - the list of remote providers
+ */
+function loadRemoteSearchProviders(searchSettings) {
+ let objectPaths = {};
+ let loadedProviders = [];
+
+ function loadRemoteSearchProvider(file) {
+ let keyfile = new GLib.KeyFile();
+ let path = file.get_path();
+
+ try {
+ keyfile.load_from_file(path, 0);
+ } catch (e) {
+ return;
+ }
+
+ if (!keyfile.has_group(KEY_FILE_GROUP))
+ return;
+
+ let remoteProvider;
+ try {
+ let group = KEY_FILE_GROUP;
+ let busName = keyfile.get_string(group, 'BusName');
+ let objectPath = keyfile.get_string(group, 'ObjectPath');
+
+ if (objectPaths[objectPath])
+ return;
+
+ let appInfo = null;
+ try {
+ let desktopId = keyfile.get_string(group, 'DesktopId');
+ appInfo = Gio.DesktopAppInfo.new(desktopId);
+ if (!appInfo.should_show())
+ return;
+ } catch (e) {
+ log(`Ignoring search provider ${path}: missing DesktopId`);
+ return;
+ }
+
+ let autoStart = true;
+ try {
+ autoStart = keyfile.get_boolean(group, 'AutoStart');
+ } catch (e) {
+ // ignore error
+ }
+
+ let version = '1';
+ try {
+ version = keyfile.get_string(group, 'Version');
+ } catch (e) {
+ // ignore error
+ }
+
+ if (version >= 2)
+ remoteProvider = new RemoteSearchProvider2(appInfo, busName, objectPath, autoStart);
+ else
+ remoteProvider = new RemoteSearchProvider(appInfo, busName, objectPath, autoStart);
+
+ remoteProvider.defaultEnabled = true;
+ try {
+ remoteProvider.defaultEnabled = !keyfile.get_boolean(group, 'DefaultDisabled');
+ } catch (e) {
+ // ignore error
+ }
+
+ objectPaths[objectPath] = remoteProvider;
+ loadedProviders.push(remoteProvider);
+ } catch (e) {
+ log(`Failed to add search provider ${path}: ${e}`);
+ }
+ }
+
+ if (searchSettings.get_boolean('disable-external'))
+ return [];
+
+ FileUtils.collectFromDatadirs('search-providers', false, loadRemoteSearchProvider);
+
+ let sortOrder = searchSettings.get_strv('sort-order');
+
+ // Special case gnome-control-center to be always active and always first
+ sortOrder.unshift('org.gnome.Settings.desktop');
+
+ const disabled = searchSettings.get_strv('disabled');
+ const enabled = searchSettings.get_strv('enabled');
+
+ loadedProviders = loadedProviders.filter(provider => {
+ let appId = provider.appInfo.get_id();
+
+ if (provider.defaultEnabled)
+ return !disabled.includes(appId);
+ else
+ return enabled.includes(appId);
+ });
+
+ loadedProviders.sort((providerA, providerB) => {
+ let idxA, idxB;
+ let appIdA, appIdB;
+
+ appIdA = providerA.appInfo.get_id();
+ appIdB = providerB.appInfo.get_id();
+
+ idxA = sortOrder.indexOf(appIdA);
+ idxB = sortOrder.indexOf(appIdB);
+
+ // if no provider is found in the order, use alphabetical order
+ if ((idxA == -1) && (idxB == -1)) {
+ let nameA = providerA.appInfo.get_name();
+ let nameB = providerB.appInfo.get_name();
+
+ return GLib.utf8_collate(nameA, nameB);
+ }
+
+ // if providerA isn't found, it's sorted after providerB
+ if (idxA == -1)
+ return 1;
+
+ // if providerB isn't found, it's sorted after providerA
+ if (idxB == -1)
+ return -1;
+
+ // finally, if both providers are found, return their order in the list
+ return idxA - idxB;
+ });
+
+ return loadedProviders;
+}
+
+var RemoteSearchProvider = class {
+ constructor(appInfo, dbusName, dbusPath, autoStart, proxyInfo) {
+ if (!proxyInfo)
+ proxyInfo = SearchProviderProxyInfo;
+
+ let gFlags = Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES;
+ if (autoStart)
+ gFlags |= Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION;
+ else
+ gFlags |= Gio.DBusProxyFlags.DO_NOT_AUTO_START;
+
+ this.proxy = new Gio.DBusProxy({
+ g_bus_type: Gio.BusType.SESSION,
+ g_name: dbusName,
+ g_object_path: dbusPath,
+ g_interface_info: proxyInfo,
+ g_interface_name: proxyInfo.name,
+ gFlags,
+ });
+ this.proxy.init_async(GLib.PRIORITY_DEFAULT, null);
+
+ this.appInfo = appInfo;
+ this.id = appInfo.get_id();
+ this.isRemoteProvider = true;
+ this.canLaunchSearch = false;
+ }
+
+ createIcon(size, meta) {
+ let gicon = null;
+ let icon = null;
+
+ if (meta['icon']) {
+ gicon = Gio.icon_deserialize(meta['icon']);
+ } else if (meta['gicon']) {
+ gicon = Gio.icon_new_for_string(meta['gicon']);
+ } else if (meta['icon-data']) {
+ const [
+ width, height, rowStride, hasAlpha,
+ bitsPerSample, nChannels_, data,
+ ] = meta['icon-data'];
+ gicon = Shell.util_create_pixbuf_from_data(data, GdkPixbuf.Colorspace.RGB, hasAlpha,
+ bitsPerSample, width, height, rowStride);
+ }
+
+ if (gicon)
+ icon = new St.Icon({ gicon, icon_size: size });
+ return icon;
+ }
+
+ filterResults(results, maxNumber) {
+ if (results.length <= maxNumber)
+ return results;
+
+ let regularResults = results.filter(r => !r.startsWith('special:'));
+ let specialResults = results.filter(r => r.startsWith('special:'));
+
+ return regularResults.slice(0, maxNumber).concat(specialResults.slice(0, maxNumber));
+ }
+
+ async getInitialResultSet(terms, cancellable) {
+ try {
+ const [results] = await this.proxy.GetInitialResultSetAsync(terms, cancellable);
+ return results;
+ } catch (error) {
+ if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ log(`Received error from D-Bus search provider ${this.id}: ${error}`);
+ return [];
+ }
+ }
+
+ async getSubsearchResultSet(previousResults, newTerms, cancellable) {
+ try {
+ const [results] = await this.proxy.GetSubsearchResultSetAsync(previousResults, newTerms, cancellable);
+ return results;
+ } catch (error) {
+ if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ log(`Received error from D-Bus search provider ${this.id}: ${error}`);
+ return [];
+ }
+ }
+
+ async getResultMetas(ids, cancellable) {
+ let metas;
+ try {
+ [metas] = await this.proxy.GetResultMetasAsync(ids, cancellable);
+ } catch (error) {
+ if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ log(`Received error from D-Bus search provider ${this.id} during GetResultMetas: ${error}`);
+ return [];
+ }
+
+ let resultMetas = [];
+ for (let i = 0; i < metas.length; i++) {
+ for (let prop in metas[i]) {
+ // we can use the serialized icon variant directly
+ if (prop !== 'icon')
+ metas[i][prop] = metas[i][prop].deepUnpack();
+ }
+
+ resultMetas.push({
+ id: metas[i]['id'],
+ name: metas[i]['name'],
+ description: metas[i]['description'],
+ createIcon: size => this.createIcon(size, metas[i]),
+ clipboardText: metas[i]['clipboardText'],
+ });
+ }
+ return resultMetas;
+ }
+
+ activateResult(id) {
+ this.proxy.ActivateResultAsync(id).catch(logError);
+ }
+
+ launchSearch(_terms) {
+ // the provider is not compatible with the new version of the interface, launch
+ // the app itself but warn so we can catch the error in logs
+ log(`Search provider ${this.appInfo.get_id()} does not implement LaunchSearch`);
+ this.appInfo.launch([], global.create_app_launch_context(0, -1));
+ }
+};
+
+var RemoteSearchProvider2 = class extends RemoteSearchProvider {
+ constructor(appInfo, dbusName, dbusPath, autoStart) {
+ super(appInfo, dbusName, dbusPath, autoStart, SearchProvider2ProxyInfo);
+
+ this.canLaunchSearch = true;
+ }
+
+ activateResult(id, terms) {
+ this.proxy.ActivateResultAsync(
+ id, terms, global.get_current_time()).catch(logError);
+ }
+
+ launchSearch(terms) {
+ this.proxy.LaunchSearchAsync(
+ terms, global.get_current_time()).catch(logError);
+ }
+};