/** * V-Shell (Vertical Workspaces) * extensionsSearchProvider.js * * @author GdH * @copyright 2022 - 2023 * @license GPL-3.0 */ 'use strict'; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const St = imports.gi.St; const Shell = imports.gi.Shell; const Clutter = imports.gi.Clutter; const GObject = imports.gi.GObject; const Main = imports.ui.main; const ExtensionState = { 1: 'ENABLED', 2: 'DISABLED', 3: 'ERROR', 4: 'INCOMPATIBLE', 5: 'DOWNLOADING', 6: 'INITIALIZED', 7: 'DISABLING', 8: 'ENABLING', }; let Me; let opt; // gettext let _; let _toggleTimeout; // prefix helps to eliminate results from other search providers // so it needs to be something less common // needs to be accessible from vw module const PREFIX = 'eq//'; var ExtensionsSearchProviderModule = class { // export for other modules static _PREFIX = PREFIX; constructor(me) { Me = me; opt = Me.opt; _ = Me.gettext; this._firstActivation = true; this.moduleEnabled = false; this._extensionsSearchProvider = null; this._enableTimeoutId = 0; } cleanGlobals() { Me = null; opt = null; _ = null; } update(reset) { if (_toggleTimeout) GLib.source_remove(_toggleTimeout); this.moduleEnabled = opt.get('extensionsSearchProviderModule'); reset = reset || !this.moduleEnabled; if (reset && !this._firstActivation) { this._disableModule(); } else if (!reset) { this._firstActivation = false; this._activateModule(); } if (reset && this._firstActivation) console.debug(' ExtensionsSearchProviderModule - Keeping untouched'); } _activateModule() { // delay because Fedora had problem to register a new provider soon after Shell restarts this._enableTimeoutId = GLib.timeout_add( GLib.PRIORITY_DEFAULT, 2000, () => { if (!this._extensionsSearchProvider) { this._extensionsSearchProvider = new extensionsSearchProvider(opt); this._getOverviewSearchResult()._registerProvider(this._extensionsSearchProvider); } this._enableTimeoutId = 0; return GLib.SOURCE_REMOVE; } ); console.debug(' ExtensionsSearchProviderModule - Activated'); } _disableModule() { if (this._enableTimeoutId) { GLib.source_remove(this._enableTimeoutId); this._enableTimeoutId = 0; } if (this._extensionsSearchProvider) { this._getOverviewSearchResult()._unregisterProvider(this._extensionsSearchProvider); this._extensionsSearchProvider = null; } console.debug(' ExtensionsSearchProviderModule - Disabled'); } _getOverviewSearchResult() { return Main.overview._overview.controls._searchController._searchResults; } }; class extensionsSearchProvider { constructor() { this.id = 'extensions'; const appSystem = Shell.AppSystem.get_default(); let appInfo = appSystem.lookup_app('com.matjakeman.ExtensionManager.desktop')?.get_app_info(); if (!appInfo) appInfo = appSystem.lookup_app('org.gnome.Extensions.desktop')?.get_app_info(); if (!appInfo) appInfo = Gio.AppInfo.create_from_commandline('/usr/bin/gnome-extensions-app', 'Extensions', null); appInfo.get_description = () => _('Search extensions'); appInfo.get_name = () => _('Extensions'); appInfo.get_id = () => 'org.gnome.Extensions.desktop'; appInfo.get_icon = () => Gio.icon_new_for_string('application-x-addon'); appInfo.should_show = () => true; this.appInfo = appInfo; this.canLaunchSearch = true; this.isRemoteProvider = false; } getInitialResultSet(terms, callback /* , cancellable = null*/) { // In GS 43 callback arg has been removed /* if (Me.shellVersion >= 43) cancellable = callback; */ const extensions = {}; Main.extensionManager._extensions.forEach( e => { extensions[e.uuid] = e; } ); this.extensions = extensions; if (Me.shellVersion >= 43) return new Promise(resolve => resolve(this._getResultSet(terms))); else callback(this._getResultSet(terms)); return null; } _getResultSet(terms) { // do not modify original terms let termsCopy = [...terms]; // search for terms without prefix termsCopy[0] = termsCopy[0].replace(PREFIX, ''); const candidates = this.extensions; const _terms = [].concat(termsCopy); const term = _terms.join(' '); const results = []; let m; for (let id in candidates) { const extension = this.extensions[id]; const text = extension.metadata.name + (extension.state === 1 ? 'enabled' : '') + ([6, 2].includes(extension.state) ? 'disabled' : ''); if (opt.SEARCH_FUZZY) m = Me.Util.fuzzyMatch(term, text); else m = Me.Util.strictMatch(term, text); if (m !== -1) results.push({ weight: m, id }); } // sort alphabetically results.sort((a, b) => this.extensions[a.id].metadata.name.localeCompare(this.extensions[b.id].metadata.name)); // enabled first // results.sort((a, b) => this.extensions[a.id].state !== 1 && this.extensions[b.id].state === 1); // incompatible last results.sort((a, b) => this.extensions[a.id].state === 4 && this.extensions[b.id].state !== 4); this.resultIds = results.map(item => item.id); return this.resultIds; } getResultMetas(resultIds, callback = null) { const metas = resultIds.map(id => this.getResultMeta(id)); if (Me.shellVersion >= 43) return new Promise(resolve => resolve(metas)); else if (callback) callback(metas); return null; } getResultMeta(resultId) { const result = this.extensions[resultId]; const versionName = result.metadata['version-name'] ?? ''; let version = result.metadata['version'] ?? ''; version = versionName && version ? `/${version}` : version; const versionStr = `${versionName}${version}`; return { 'id': resultId, 'name': `${result.metadata.name}`, 'version': versionStr, 'description': versionStr, // description will be updated in result object 'createIcon': size => { let icon = this.getIcon(result, size); return icon; }, }; } getIcon(extension, size) { let opacity = 0; let iconName = 'process-stop-symbolic'; switch (extension.state) { case 1: if (extension.hasUpdate) iconName = 'software-update-available'; // 'software-update-available-symbolic'; else iconName = 'object-select';// 'object-select-symbolic'; opacity = 255; break; case 3: if (Main.extensionManager._enabledExtensions.includes(extension.uuid)) iconName = 'emblem-ok-symbolic'; else iconName = 'dialog-error'; opacity = 100; break; case 4: iconName = 'software-update-urgent'; // 'software-update-urgent-symbolic'; opacity = 100; break; } if (extension.hasUpdate) { iconName = 'software-update-available'; // 'software-update-available-symbolic'; opacity = 100; } const icon = new St.Icon({ icon_name: iconName, icon_size: size }); icon.set({ reactive: true, opacity, }); return icon; } createResultObject(meta) { return new ListSearchResult(this, meta, this.extensions[meta.id]); } launchSearch(terms, timeStamp) { this.appInfo.launch([], global.create_app_launch_context(timeStamp, -1), null); } activateResult(resultId/* terms, timeStamp*/) { const extension = this.extensions[resultId]; if (Me.Util.isShiftPressed()) this._toggleExtension(extension); else if (extension.hasPrefs) Me.Util.openPreferences(extension.metadata); } filterResults(results /* , maxResults*/) { // return results.slice(0, maxResults); return results; } getSubsearchResultSet(previousResults, terms, callback) { if (Me.shellVersion < 43) { this.getSubsearchResultSet42(terms, callback); return null; } return this.getInitialResultSet(terms); } getSubsearchResultSet42(terms, callback) { callback(this._getResultSet(terms)); } } const ListSearchResult = GObject.registerClass( class ListSearchResult extends St.Button { _init(provider, metaInfo, extension) { this.provider = provider; this.metaInfo = metaInfo; this.extension = extension; super._init({ reactive: true, can_focus: true, track_hover: true, }); this.style_class = 'list-search-result'; let content = new St.BoxLayout({ style_class: 'list-search-result-content', vertical: false, x_align: Clutter.ActorAlign.START, x_expand: true, y_expand: true, }); this.set_child(content); let titleBox = new St.BoxLayout({ style_class: 'list-search-result-title', y_align: Clutter.ActorAlign.CENTER, }); content.add_child(titleBox); // An icon for, or thumbnail of, content let icon = this.metaInfo['createIcon'](this.ICON_SIZE); let iconBox = new St.Button(); iconBox.set_child(icon); titleBox.add(iconBox); iconBox.set_style('border: 1px solid rgba(200,200,200,0.2); padding: 2px; border-radius: 8px;'); this._iconBox = iconBox; this.icon = icon; iconBox.connect('clicked', () => { this._toggleExtension(); return Clutter.EVENT_STOP; }); let title = new St.Label({ text: this.metaInfo['name'], y_align: Clutter.ActorAlign.CENTER, opacity: extension.hasPrefs ? 255 : 150, }); titleBox.add_child(title); this.label_actor = title; this._descriptionLabel = new St.Label({ style_class: 'list-search-result-description', y_align: Clutter.ActorAlign.CENTER, }); content.add_child(this._descriptionLabel); this._highlightTerms(); this.connect('destroy', () => { if (_toggleTimeout) { GLib.source_remove(_toggleTimeout); _toggleTimeout = 0; } }); } _toggleExtension() { const state = this.extension.state; if (![1, 2, 6, 3].includes(state) || this.extension.metadata.uuid.includes('vertical-workspaces')) return; if ([2, 6].includes(state)) Main.extensionManager.enableExtension(this.extension.uuid); else if ([1, 3].includes(state)) Main.extensionManager.disableExtension(this.extension.uuid); if (_toggleTimeout) GLib.source_remove(_toggleTimeout); _toggleTimeout = GLib.timeout_add(GLib.PRIORITY_LOW, 200, () => { if ([7, 8].includes(this.extension.state)) return GLib.SOURCE_CONTINUE; this.icon?.destroy(); this.icon = this.metaInfo['createIcon'](this.ICON_SIZE); this._iconBox.set_child(this.icon); this._highlightTerms(); _toggleTimeout = 0; return GLib.SOURCE_REMOVE; } ); } get ICON_SIZE() { return 24; } _highlightTerms() { const extension = this.extension; const state = extension.state === 4 ? ExtensionState[this.extension.state] : ''; const error = extension.state === 3 ? ` ERROR: ${this.extension.error}` : ''; const update = extension.hasUpdate ? ' | UPDATE PENDING' : ''; const text = `${this.metaInfo.version} ${state}${error}${update}`; let markup = text;// this.metaInfo['description'].split('\n')[0]; this._descriptionLabel.clutter_text.set_markup(markup); } vfunc_clicked() { this.activate(); } activate() { this.provider.activateResult(this.metaInfo.id); if (this.metaInfo.clipboardText) { St.Clipboard.get_default().set_text( St.ClipboardType.CLIPBOARD, this.metaInfo.clipboardText); } Main.overview.toggle(); } });