diff options
Diffstat (limited to 'extensions/46/vertical-workspaces/lib/recentFilesSearchProvider.js')
-rw-r--r-- | extensions/46/vertical-workspaces/lib/recentFilesSearchProvider.js | 316 |
1 files changed, 316 insertions, 0 deletions
diff --git a/extensions/46/vertical-workspaces/lib/recentFilesSearchProvider.js b/extensions/46/vertical-workspaces/lib/recentFilesSearchProvider.js new file mode 100644 index 0000000..f050cf9 --- /dev/null +++ b/extensions/46/vertical-workspaces/lib/recentFilesSearchProvider.js @@ -0,0 +1,316 @@ +/** + * Vertical Workspaces + * recentFilesSearchProvider.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022 - 2024 + * @license GPL-3.0 + */ + +'use strict'; + +import GLib from 'gi://GLib'; +import St from 'gi://St'; +import Gio from 'gi://Gio'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; + +let Me; +let opt; +// gettext +let _; + +// prefix helps to eliminate results from other search providers +// so it needs to be something less common +// needs to be accessible from vw module +export const PREFIX = 'fq//'; +const ID = 'recent-files'; + +export const RecentFilesSearchProviderModule = class { + // export for other modules + static _PREFIX = PREFIX; + constructor(me) { + Me = me; + opt = Me.opt; + _ = Me.gettext; + + this._firstActivation = true; + this.moduleEnabled = false; + this._recentFilesSearchProvider = null; + this._enableTimeoutId = 0; + } + + cleanGlobals() { + Me = null; + opt = null; + _ = null; + } + + update(reset) { + this.moduleEnabled = opt.get('recentFilesSearchProviderModule'); + + reset = reset || !this.moduleEnabled; + + if (reset && !this._firstActivation) { + this._disableModule(); + } else if (!reset) { + this._firstActivation = false; + this._activateModule(); + } + if (reset && this._firstActivation) + console.debug(' RecentFilesSearchProviderModule - 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._recentFilesSearchProvider) { + this._recentFilesSearchProvider = new RecentFilesSearchProvider(); + this._registerProvider(this._recentFilesSearchProvider); + } + this._enableTimeoutId = 0; + return GLib.SOURCE_REMOVE; + } + ); + + console.debug(' RecentFilesSearchProviderModule - Activated'); + } + + _disableModule() { + if (this._recentFilesSearchProvider) { + this._unregisterProvider(this._recentFilesSearchProvider); + this._recentFilesSearchProvider = null; + } + if (this._enableTimeoutId) { + GLib.source_remove(this._enableTimeoutId); + this._enableTimeoutId = 0; + } + + console.debug(' RecentFilesSearchProviderModule - Disabled'); + } + + _registerProvider(provider) { + const searchResults = Main.overview.searchController._searchResults; + provider.searchInProgress = false; + + searchResults._providers.push(provider); + + // create results display and add it to the _content + searchResults._ensureProviderDisplay.bind(searchResults)(provider); + } + + _unregisterProvider(provider) { + const searchResults = Main.overview.searchController._searchResults; + searchResults._unregisterProvider(provider); + } +}; + +class RecentFilesSearchProvider { + constructor() { + this.id = ID; + const appId = 'org.gnome.Nautilus.desktop'; + + // A real appInfo created from a commandline has often issues with overriding get_id() method, so we use dict instead + this.appInfo = { + get_id: () => appId, + get_name: () => _('Recent Files'), + get_icon: () => Gio.icon_new_for_string('focus-windows-symbolic'), + should_show: () => true, + get_commandline: () => '/usr/bin/nautilus -w recent:///', + launch: () => {}, + }; + + this.canLaunchSearch = true; + this.isRemoteProvider = false; + + this._recentFilesManager = new RecentFilesManager(); + } + + getInitialResultSet(terms/* , cancellable*/) { + const rfm = this._recentFilesManager; + rfm.loadFromFile(); + + const uris = rfm.getUris(); + const dict = {}; + for (let uri of uris) { + dict[uri] = {}; + dict[uri]['uri'] = uri; + dict[uri]['path'] = rfm.getPath(uri); + dict[uri]['filename'] = rfm.getDisplayName(uri); + dict[uri]['dir'] = rfm.getDirPath(uri); + dict[uri]['age'] = rfm.getAge(uri); + dict[uri]['appInfo'] = rfm.getDefaultAppAppInfo(uri); + } + this.files = dict; + + return new Promise(resolve => resolve(this._getResultSet(terms))); + } + + _getResultSet(terms) { + if (!terms[0].startsWith(PREFIX)) + return []; + // do not modify original terms + let termsCopy = [...terms]; + // search for terms without prefix + termsCopy[0] = termsCopy[0].replace(PREFIX, ''); + + const candidates = Object.values(this.files); + const _terms = [].concat(termsCopy); + + const term = _terms.join(' '); + + const results = []; + let m; + for (let file of candidates) { + if (opt.SEARCH_FUZZY) + m = Me.Util.fuzzyMatch(term, file.filename); + else + m = Me.Util.strictMatch(term, file.filename); + + if (m !== -1) + results.push(file); + } + + results.sort((a, b) => a.age > b.age); + + const resultIds = results.map(item => item.uri); + return resultIds; + } + + getResultMetas(resultIds/* , callback = null*/) { + const metas = resultIds.map(id => this.getResultMeta(id)); + return new Promise(resolve => resolve(metas)); + } + + getResultMeta(resultId) { + const result = this.files[resultId]; + return { + 'id': resultId, + 'name': `${Math.floor(result.age)}: ${result.filename}`, + 'description': `${result.dir}`, + 'createIcon': size => + this._recentFilesManager.getDefaultAppIcon(resultId, size), + }; + } + + launchSearch(terms, timeStamp) { + const appInfo = Gio.AppInfo.create_from_commandline('/usr/bin/nautilus -w recent:///', 'Nautilus', null); + appInfo.launch([], global.create_app_launch_context(timeStamp, -1)); + + // unlike on 42, on 44 if a window with the same uri is already open it will not get focus/activation + // Gio.app_info_launch_default_for_uri('recent:///', global.create_app_launch_context(timeStamp, -1)); + + // following solution for some reason ignores the recent:/// uri + // this.appInfo.launch_uris(['recent:///'], global.create_app_launch_context(timeStamp, -1)); + } + + activateResult(resultId, terms, timeStamp) { + const uri = resultId; + const context = global.create_app_launch_context(timeStamp, -1); + if (Me.Util.isShiftPressed()) { + Main.overview.toggle(); + this.appInfo.launch_uris([uri], context); + } else if (Gio.app_info_launch_default_for_uri(uri, context)) { + // update recent list after successful activation + this._recentFilesManager.updateAdded(resultId); + this._recentFilesManager.saveToFile(); + } else { + this.appInfo.launch_uris([uri], context); + } + } + + filterResults(results /* , maxResults*/) { + // return results.slice(0, maxResults); + return results.slice(0, 20); + } + + getSubsearchResultSet(previousResults, terms/* , cancellable*/) { + return this.getInitialResultSet(terms); + } +} + +class RecentFilesManager { + constructor(path) { + path = path ?? GLib.build_filenamev([GLib.get_user_data_dir(), 'recently-used.xbel']); + this._recentlyUsedPath = path; + this._bookmarks = new GLib.BookmarkFile(); + } + + loadFromFile() { + try { + this._bookmarks.load_from_file(this._recentlyUsedPath); + } catch (e) { + if (!e.matches(GLib.BookmarkFileError, GLib.BookmarkFileError.FILE_NOT_FOUND)) + console.error(`Could not open recent files: ${e.message}`); + } + } + + saveToFile() { + try { + this._bookmarks.to_file(this._recentlyUsedPath); + } catch (e) { + if (!e.matches(GLib.BookmarkFileError, GLib.BookmarkFileError.FILE_NOT_FOUND)) + console.error(`Could not open recent files to save data: ${e.message}`); + } + } + + getUris() { + return this._bookmarks.get_uris(); + } + + getPath(uri) { + // GLib.filename_from_uri() removes uri schema and converts string to utf-8 + return GLib.filename_from_uri(uri)[0]; // result is array + } + + getDisplayName(uri) { + const path = this.getPath(uri); + return GLib.filename_display_basename(path); + } + + getDirPath(uri) { + const path = this.getPath(uri); + const filename = this.getDisplayName(uri); + return path.replace(`${filename}`, ''); + } + + getMimeType(uri) { + return this._bookmarks.get_mime_type(uri); + } + + getAdded(uri) { + return this._bookmarks.get_added(uri); + } + + updateAdded(uri) { + this._bookmarks.set_added_date_time(uri, GLib.DateTime.new_now_local()); + } + + // age in days (float) + getAge(uri) { + return (Date.now() / 1000 - this._bookmarks.get_added(uri)) / 60 / 60 / 24; + } + + getDefaultAppAppInfo(uri) { + const mimeType = this.getMimeType(uri); + return Gio.AppInfo.get_default_for_type(mimeType, false); + } + + getDefaultAppIcon(uri, size) { + let icon, gicon; + + const appInfo = this.getDefaultAppAppInfo(uri); + if (appInfo) + gicon = appInfo.get_icon(); + + if (gicon) + icon = new St.Icon({ gicon, icon_size: size }); + else + icon = new St.Icon({ icon_name: 'icon-missing', icon_size: size }); + + return icon; + } +} |