gnome-shell/js/ui/remoteSearch.js
Florian Müllner 119581a4cb search: Use async functions instead of callbacks
Our search provider API has to be asynchronous to support remote
providers. It doesn't have to be based on callbacks though, now
that async functions provide a nicer alternative.

That is particularly true after gjs's D-Bus wrapper started to
generate promise-based method variants.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2344>
2022-08-01 18:51:14 +00:00

331 lines
10 KiB
JavaScript

// -*- 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].deep_unpack();
}
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.ActivateResultRemote(id);
}
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.ActivateResultRemote(id, terms, global.get_current_time());
}
launchSearch(terms) {
this.proxy.LaunchSearchRemote(terms, global.get_current_time());
}
};