b7c097ba79
This change enables mapping each collected file to a Promise in asynchronous operations. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2364>
334 lines
10 KiB
JavaScript
334 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 [];
|
|
|
|
for (const {dir} of FileUtils.collectFromDatadirs('search-providers', false))
|
|
loadRemoteSearchProvider(dir);
|
|
|
|
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);
|
|
}
|
|
};
|