diff options
Diffstat (limited to 'raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/locations.js')
| -rw-r--r-- | raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/locations.js | 1535 |
1 files changed, 1535 insertions, 0 deletions
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/locations.js b/raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/locations.js new file mode 100644 index 0000000..534466c --- /dev/null +++ b/raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/locations.js @@ -0,0 +1,1535 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +import { + Gio, + GioUnix, + GLib, + GObject, + Shell, + St, +} from './dependencies/gi.js'; + +import {ShellMountOperation} from './dependencies/shell/ui.js'; + +import { + Docking, + Utils, +} from './imports.js'; + +import {Extension} from './dependencies/shell/extensions/extension.js'; + +// Use __ () and N__() for the extension gettext domain, and reuse +// the shell domain with the default _() and N_() +const {gettext: __} = Extension; + +const {signals: Signals} = imports; + +const FALLBACK_REMOVABLE_MEDIA_ICON = 'drive-removable-media'; +const FALLBACK_TRASH_ICON = 'user-trash'; +const FILE_MANAGER_DESKTOP_APP_ID = 'org.gnome.Nautilus.desktop'; +const ATTRIBUTE_METADATA_CUSTOM_ICON = 'metadata::custom-icon'; +const TRASH_URI = 'trash://'; +const UPDATE_TRASH_DELAY = 1000; +const LAUNCH_HANDLER_MAX_WAIT = 200; + +const NautilusFileOperations2Interface = '<node>\ + <interface name="org.gnome.Nautilus.FileOperations2">\ + <method name="EmptyTrash">\ + <arg type="b" name="ask_confirmation" direction="in"/>\ + <arg type="a{sv}" name="platform_data" direction="in"/>\ + </method>\ + </interface>\ +</node>'; + +const NautilusFileOperations2ProxyInterface = + Gio.DBusProxy.makeProxyWrapper(NautilusFileOperations2Interface); + +const Labels = Object.freeze({ + LOCATION_WINDOWS: Symbol('location-windows'), + WINDOWS_CHANGED: Symbol('windows-changed'), +}); + +const GJS_SUPPORTS_FILE_IFACE_PROMISES = imports.system.version >= 17101; + +if (GJS_SUPPORTS_FILE_IFACE_PROMISES) { + Gio._promisify(Gio.File.prototype, 'query_info_async'); + Gio._promisify(Gio.File.prototype, 'query_default_handler_async'); +} + + +/** + * + */ +function makeNautilusFileOperationsProxy() { + const proxy = new NautilusFileOperations2ProxyInterface( + Gio.DBus.session, + 'org.gnome.Nautilus', + '/org/gnome/Nautilus/FileOperations2', (_p, error) => { + if (error) + logError(error, 'Error connecting to Nautilus'); + } + ); + + proxy.platformData = params => { + const defaultParams = { + parentHandle: '', + timestamp: global.get_current_time(), + windowPosition: 'center', + }; + const {parentHandle, timestamp, windowPosition} = { + ...defaultParams, + ...params, + }; + + return { + 'parent-handle': new GLib.Variant('s', parentHandle), + 'timestamp': new GLib.Variant('u', timestamp), + 'window-position': new GLib.Variant('s', windowPosition), + }; + }; + + return proxy; +} + +export const LocationAppInfo = GObject.registerClass({ + Implements: [Gio.AppInfo], + Properties: { + 'location': GObject.ParamSpec.object( + 'location', 'location', 'location', + GObject.ParamFlags.READWRITE, + Gio.File.$gtype), + 'name': GObject.ParamSpec.string( + 'name', 'name', 'name', + GObject.ParamFlags.READWRITE, + null), + 'icon': GObject.ParamSpec.object( + 'icon', 'icon', 'icon', + GObject.ParamFlags.READWRITE, + Gio.Icon.$gtype), + 'cancellable': GObject.ParamSpec.object( + 'cancellable', 'cancellable', 'cancellable', + GObject.ParamFlags.READWRITE, + Gio.Cancellable.$gtype), + }, +}, class LocationAppInfo extends GioUnix.DesktopAppInfo { + static get GJS_BINARY_PATH() { + if (!this._gjsBinaryPath) + this._gjsBinaryPath = GLib.find_program_in_path('gjs'); + + return this._gjsBinaryPath; + } + + list_actions() { + return []; + } + + get_action_name() { + return null; + } + + get_boolean() { + return false; + } + + vfunc_dup() { + return new LocationAppInfo({ + location: this.location, + name: this.name, + icon: this.icon, + cancellable: this.cancellable, + }); + } + + vfunc_equal(other) { + if (this.location) + return this.location.equal(other?.location); + + return this.name === other.name && + (this.icon ? this.icon.equal(other?.icon) : !other?.icon); + } + + vfunc_get_id() { + return 'location:%s'.format(this.location?.get_uri()); + } + + vfunc_get_name() { + return this.name; + } + + vfunc_get_description() { + return null; + } + + vfunc_get_executable() { + return null; + } + + vfunc_get_icon() { + return this.icon; + } + + vfunc_launch(files, context) { + if (files?.length) { + throw new GLib.Error(Gio.IOErrorEnum, + Gio.IOErrorEnum.NOT_SUPPORTED, 'Launching with files not supported'); + } + + return this.getHandlerApp().launch_uris([this.location.get_uri()], context); + } + + vfunc_supports_uris() { + return false; + } + + vfunc_supports_files() { + return false; + } + + vfunc_launch_uris(uris, context) { + return this.launch(uris, context); + } + + vfunc_should_show() { + return true; + } + + vfunc_set_as_default_for_type() { + throw new GLib.Error(Gio.IOErrorEnum, + Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported'); + } + + vfunc_set_as_default_for_extension() { + throw new GLib.Error(Gio.IOErrorEnum, + Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported'); + } + + vfunc_add_supports_type() { + throw new GLib.Error(Gio.IOErrorEnum, + Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported'); + } + + vfunc_can_remove_supports_type() { + return false; + } + + vfunc_remove_supports_type() { + return false; + } + + vfunc_can_delete() { + return false; + } + + vfunc_do_delete() { + return false; + } + + vfunc_get_commandline() { + try { + return this.getHandlerApp().get_commandline(); + } catch { + return this._getFallbackCommandLine(); + } + } + + vfunc_get_display_name() { + return this.name; + } + + vfunc_set_as_last_used_for_type() { + throw new GLib.Error(Gio.IOErrorEnum, + Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported'); + } + + vfunc_get_supported_types() { + return []; + } + + _getFallbackCommandLine() { + return `gio open ${this.location?.get_uri()}`; + } + + async _queryLocationIcons(params) { + const icons = {standard: null, custom: null}; + if (!this.location) + return icons; + + const cancellable = params.cancellable ?? this.cancellable; + const iconsQuery = []; + if (params?.standard) + iconsQuery.push(Gio.FILE_ATTRIBUTE_STANDARD_ICON); + + if (params?.custom) + iconsQuery.push(ATTRIBUTE_METADATA_CUSTOM_ICON); + + if (!iconsQuery.length) + throw new Error('Invalid Query Location Icons parameters'); + + let info; + try { + if (!GJS_SUPPORTS_FILE_IFACE_PROMISES) { + Gio._promisify(this.location.constructor.prototype, + 'query_info_async', 'query_info_finish'); + } + info = await this.location.query_info_async( + iconsQuery.join(','), + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_LOW, cancellable); + if (info.has_attribute(Gio.FILE_ATTRIBUTE_STANDARD_ICON)) + icons.standard = info.get_icon(); + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND) || + e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED)) + return icons; + throw e; + } + + const customIcon = info.get_attribute_string(ATTRIBUTE_METADATA_CUSTOM_ICON); + if (customIcon) { + const customIconFile = GLib.uri_parse_scheme(customIcon) + ? Gio.File.new_for_uri(customIcon) : Gio.File.new_for_path(customIcon); + const iconFileInfo = await customIconFile.query_info_async( + Gio.FILE_ATTRIBUTE_STANDARD_TYPE, + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_LOW, cancellable); + + if (iconFileInfo.get_file_type() === Gio.FileType.REGULAR) + icons.custom = Gio.FileIcon.new(customIconFile); + } + + return icons; + } + + async _updateLocationIcon(params = {standard: true, custom: true}) { + const cancellable = new Utils.CancellableChild(this.cancellable); + + try { + this._updateIconCancellable?.cancel(); + this._updateIconCancellable = cancellable; + + const icons = await this._queryLocationIcons({cancellable, ...params}); + const icon = icons.custom ?? icons.standard; + + if (icon && !icon.equal(this.icon)) + this.icon = icon; + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e, 'Impossible to update icon for %s'.format(this.get_id())); + } finally { + cancellable.cancel(); + if (this._updateIconCancellable === cancellable) + delete this._updateIconCancellable; + } + } + + async _getHandlerAppAsync(cancellable) { + if (!this.location) + return null; + + try { + if (!GJS_SUPPORTS_FILE_IFACE_PROMISES) { + Gio._promisify(this.location.constructor.prototype, + 'query_default_handler_async', + 'query_default_handler_finish'); + } + + return await this.location.query_default_handler_async( + GLib.PRIORITY_DEFAULT, cancellable); + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED)) + return getFileManagerApp()?.appInfo; + + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + logError(e, 'Impossible to find an URI handler for %s'.format( + this.get_id())); + } + + throw e; + } + } + + _getHandlerAppFromWorker(cancellable) { + const locationsWorker = GLib.build_filenamev([ + Docking.DockManager.extension.path, + 'locationsWorker.js', + ]); + const locationsWorkerArgs = [LocationAppInfo.GJS_BINARY_PATH, '-m', + locationsWorker, 'handler', this.location.get_uri(), + '--timeout', `${LAUNCH_HANDLER_MAX_WAIT}`]; + const subProcess = Gio.Subprocess.new(locationsWorkerArgs, + Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE); + + try { + const [, stdOut, stdErr] = subProcess.communicate(null, cancellable); + subProcess.wait(cancellable); + const errorCode = subProcess.get_exit_status(); + const textDecoder = new TextDecoder(); + + if (errorCode) { + const errorLines = textDecoder.decode(stdErr.toArray()).split('\n'); + const error = new GLib.Error(Gio.IOErrorEnum, + errorCode === GLib.MAXUINT8 ? 0 : errorCode, errorLines[0]); + error.stack = `${errorLines.slice(3).join('\n')}${error.stack}`; + throw error; + } + + const desktopId = textDecoder.decode(stdOut.toArray()).trim(); + const handlerApp = Shell.AppSystem.get_default().lookup_app(desktopId)?.appInfo; + return handlerApp; + } finally { + subProcess.force_exit(); + } + } + + getHandlerApp() { + if (this._handlerApp) + return this._handlerApp; + + if (!this.location) + return null; + + const cancellable = new Utils.CancellableChild(this.cancellable); + + try { + if (LocationAppInfo.GJS_BINARY_PATH) + this._handlerApp = this._getHandlerAppFromWorker(cancellable); + else + this._handlerApp = this.location.query_default_handler(cancellable); + + if (!this._handlerApp) { + throw new GLib.Error(Gio.IOErrorEnum, + Gio.IOErrorEnum.NOT_FOUND, `Handler for ${this.location} not found`); + } + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED)) + return getFileManagerApp()?.appInfo; + + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + logError(e, 'Impossible to find an URI handler for %s'.format( + this.get_id())); + } + + throw e; + } + + return this._handlerApp; + } + + destroy() { + this.location = null; + this.icon = null; + this.name = null; + this._handlerApp = null; + this.cancellable?.cancel(); + } +}); + +const RemovableAction = Object.freeze({ + MOUNT: 'mount', + UNMOUNT: 'unmount', + EJECT: 'eject', +}); + +const MountableVolumeAppInfo = GObject.registerClass({ + Implements: [Gio.AppInfo], + Properties: { + 'volume': GObject.ParamSpec.object( + 'volume', 'volume', 'volume', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + Gio.Volume.$gtype), + 'mount': GObject.ParamSpec.object( + 'mount', 'mount', 'mount', + GObject.ParamFlags.READWRITE, + Gio.Mount.$gtype), + 'busy': GObject.ParamSpec.boolean( + 'busy', 'busy', 'busy', + GObject.ParamFlags.READWRITE, + false), + }, +}, +class MountableVolumeAppInfo extends LocationAppInfo { + _init(volume, cancellable = null) { + super._init({ + volume, + cancellable, + }); + + this._signalsHandler = new Utils.GlobalSignalsHandler(); + + const updateAndMonitor = () => { + this._update(); + this._monitorChanges(); + }; + updateAndMonitor(); + this._mountChanged = this.connect('notify::mount', updateAndMonitor); + + if (!this.mount && this.volume.get_identifier('class') === 'network') { + // For some devices the mount point isn't advertised promptly + // even if it's already existing, and there's no signaling about + this._lazyUpdater = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 2, () => { + this._update(); + delete this._lazyUpdater; + return GLib.SOURCE_REMOVE; + }); + } + } + + get busy() { + return !!this._currentAction; + } + + get currentAction() { + return this._currentAction; + } + + destroy() { + if (this._lazyUpdater) { + GLib.source_remove(this._lazyUpdater); + delete this._lazyUpdater; + } + this.disconnect(this._mountChanged); + this.mount = null; + this._signalsHandler.destroy(); + + super.destroy(); + } + + vfunc_dup() { + return new MountableVolumeAppInfo({ + volume: this.volume, + cancellable: this.cancellable, + }); + } + + vfunc_get_id() { + const uuid = this.mount?.get_uuid() ?? this.volume.get_uuid(); + return uuid ? 'mountable-volume:%s'.format(uuid) : super.vfunc_get_id(); + } + + vfunc_equal(other) { + if (this.volume === other?.volume && this.mount === other?.mount) + return true; + + return this.get_id() === other?.get_id(); + } + + list_actions() { + const actions = []; + const {mount} = this; + + if (mount) { + if (this.mount.can_unmount()) + actions.push(RemovableAction.UNMOUNT); + if (this.mount.can_eject()) + actions.push(RemovableAction.EJECT); + + return actions; + } + + if (this.volume.can_mount()) + actions.push(RemovableAction.MOUNT); + if (this.volume.can_eject()) + actions.push(RemovableAction.EJECT); + + return actions; + } + + get_action_name(action) { + switch (action) { + case RemovableAction.MOUNT: + return __('Mount'); + case RemovableAction.UNMOUNT: + return __('Unmount'); + case RemovableAction.EJECT: + return __('Eject'); + default: + return null; + } + } + + vfunc_launch(files, context) { + if (this.mount || files?.length) + return super.vfunc_launch(files, context); + + this.mountAndLaunch(files, context); + return true; + } + + _update() { + this.mount = this.volume.get_mount(); + + const removable = this.mount ?? this.volume; + this.name = removable.get_name(); + this.icon = removable.get_icon(); + + this.location = this.mount?.get_default_location() ?? + this.volume.get_activation_root(); + + this._updateLocationIcon({custom: true}); + } + + _monitorChanges() { + this._signalsHandler.destroy(); + + const removable = this.mount ?? this.volume; + this._signalsHandler.add(removable, 'changed', () => this._update()); + + if (this.mount) { + this._signalsHandler.add(this.mount, 'pre-unmount', () => this._update()); + this._signalsHandler.add(this.mount, 'unmounted', () => this._update()); + } + } + + async mountAndLaunch(files, context) { + if (this.mount) + return super.vfunc_launch(files, context); + + try { + await this.launchAction(RemovableAction.MOUNT); + if (!this.mount) { + throw new Error('No mounted location to open for %s'.format( + this.get_id())); + } + + return super.vfunc_launch(files, context); + } catch (e) { + logError(e, 'Mount and launch %s'.format(this.get_id())); + return false; + } + } + + _notifyActionError(action, message) { + switch (action) { + case RemovableAction.MOUNT: + global.notify_error(__('Failed to mount “%s”'.format( + this.get_name())), message); + break; + + case RemovableAction.UNMOUNT: + global.notify_error(__('Failed to unmount “%s”'.format( + this.get_name())), message); + break; + + case RemovableAction.EJECT: + global.notify_error(__('Failed to eject “%s”'.format( + this.get_name())), message); + break; + } + } + + async launchAction(action) { + if (!this.list_actions().includes(action)) + throw new Error('Action %s is not supported by %s', action, this); + + switch (this._currentAction) { + case RemovableAction.MOUNT: + this._notifyActionError(action, + __('Mount operation already in progress')); + break; + + case RemovableAction.UNMOUNT: + this._notifyActionError(action, + __('Unmount operation already in progress')); + break; + + case RemovableAction.EJECT: + this._notifyActionError(action, + __('Eject operation already in progress')); + break; + + default: + if (this._currentAction) { + throw new Error('Another action %s is being performed in %s'.format( + this._currentAction, this)); + } + } + + this._currentAction = action; + this.notify('busy'); + const removable = this.mount ?? this.volume; + const operation = new ShellMountOperation.ShellMountOperation(removable); + try { + switch (action) { + case RemovableAction.MOUNT: + await this.volume.mount(Gio.MountMountFlags.NONE, operation.mountOp, + this.cancellable); + return true; + + case RemovableAction.UNMOUNT: + await this.mount.unmount_with_operation(Gio.MountUnmountFlags.FORCE, + operation.mountOp, this.cancellable); + return true; + + case RemovableAction.EJECT: + await removable.eject_with_operation(Gio.MountUnmountFlags.FORCE, + operation.mountOp, this.cancellable); + return true; + + default: + logError(new Error(), 'No action %s on removable %s'.format(action, + removable.get_name())); + return false; + } + } catch (e) { + if (action === RemovableAction.MOUNT && + e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.ALREADY_MOUNTED)) + return true; + else if (action === RemovableAction.UNMOUNT && + e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED)) + return true; + + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED)) + this._notifyActionError(action, e.message); + + if (action === RemovableAction.MOUNT && this._isEncryptedMountError(e)) { + delete this._currentAction; + operation.close(); + return this.launchAction(action); + } + + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + logError(e, 'Impossible to %s removable %s'.format(action, + removable.get_name())); + } + + return false; + } finally { + delete this._currentAction; + this.notify('busy'); + this._update(); + operation.close(); + } + } + + _isEncryptedMountError(error) { + // FIXME: we will always get G_IO_ERROR_FAILED from the gvfs udisks + // backend, see https://bugs.freedesktop.org/show_bug.cgi?id=51271 + + if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED)) + return false; + + // cryptsetup + if (error.message.includes('No key available with this passphrase')) + return true; + + // udisks (no password) + if (error.message.includes('No key available to unlock device')) + return true; + + // libblockdev wrong password opening LUKS device + if (error.message.includes('Failed to activate device: Incorrect passphrase')) + return true; + + // cryptsetup returns EINVAL in many cases, including wrong TCRYPT password/parameters + if (error.message.includes('Failed to load device\'s parameters: Invalid argument') || + error.message.includes(`Failed to load device's parameters: ${GLib.strerror(22 /* EINVAL */)}`)) + return true; + + // cryptsetup returns EPERM when the TCRYPT header can't be decrypted + // with the provided password/parameters. + if (error.message.includes('Failed to load device\'s parameters: Operation not permitted') || + error.message.includes(`Failed to load device's parameters: ${GLib.strerror(1 /* EPERM */)}`)) + return true; + + return false; + } +}); + +const TrashAppInfo = GObject.registerClass({ + Implements: [Gio.AppInfo], + Properties: { + 'empty': GObject.ParamSpec.boolean( + 'empty', 'empty', 'empty', + GObject.ParamFlags.READWRITE, + true), + }, +}, +class TrashAppInfo extends LocationAppInfo { + static initPromises(file) { + if (TrashAppInfo._promisified) + return; + + const trashProto = file.constructor.prototype; + Gio._promisify(Gio.FileEnumerator.prototype, 'close_async', 'close_finish'); + Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async', 'next_files_finish'); + Gio._promisify(trashProto, 'enumerate_children_async', 'enumerate_children_finish'); + Gio._promisify(trashProto, 'query_info_async', 'query_info_finish'); + TrashAppInfo._promisified = true; + } + + _init(cancellable = null) { + super._init({ + location: Gio.file_new_for_uri(TRASH_URI), + name: __('Trash'), + icon: Gio.ThemedIcon.new(FALLBACK_TRASH_ICON), + cancellable, + }); + TrashAppInfo.initPromises(this.location); + + try { + this._monitor = this.location.monitor_directory(0, this.cancellable); + this._schedUpdateId = 0; + this._monitorChangedId = this._monitor.connect('changed', () => + this._onTrashChange()); + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + return; + logError(e, 'Impossible to monitor trash'); + } + this._updateTrash(); + + this.connect('notify::empty', () => this._updateLocationIcon()); + this.notify('empty'); + } + + destroy() { + if (this._schedUpdateId) { + GLib.source_remove(this._schedUpdateId); + this._schedUpdateId = 0; + } + this._updateTrashCancellable?.cancel(); + this._monitor?.disconnect(this._monitorChangedId); + this._monitor = null; + + super.destroy(); + } + + list_actions() { + return this.empty ? [] : ['empty-trash']; + } + + get_action_name(action) { + switch (action) { + case 'empty-trash': + return __('Empty Trash'); + default: + return null; + } + } + + _onTrashChange() { + if (this._schedUpdateId) { + GLib.source_remove(this._schedUpdateId); + this._schedUpdateId = 0; + } + + if (this._monitor.is_cancelled()) + return; + + this._schedUpdateId = GLib.timeout_add(GLib.PRIORITY_LOW, + UPDATE_TRASH_DELAY, () => { + this._schedUpdateId = 0; + this._updateTrash(); + return GLib.SOURCE_REMOVE; + }); + } + + async _updateTrash() { + const priority = GLib.PRIORITY_LOW; + this._updateTrashCancellable?.cancel(); + const cancellable = new Utils.CancellableChild(this.cancellable); + this._updateTrashCancellable = cancellable; + + try { + const trashInfo = await this.location.query_info_async( + Gio.FILE_ATTRIBUTE_TRASH_ITEM_COUNT, + Gio.FileQueryInfoFlags.NONE, + priority, cancellable); + this.empty = !trashInfo.get_attribute_uint32( + Gio.FILE_ATTRIBUTE_TRASH_ITEM_COUNT); + return; + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e, 'Impossible to get trash children from infos'); + } finally { + cancellable.cancel(); + if (this._updateIconCancellable === cancellable) + delete this._updateTrashCancellable; + } + + try { + const childrenEnumerator = await this.location.enumerate_children_async( + Gio.FILE_ATTRIBUTE_STANDARD_TYPE, Gio.FileQueryInfoFlags.NONE, + priority, cancellable); + const children = await childrenEnumerator.next_files_async(1, + priority, cancellable); + this.empty = !children.length; + + await childrenEnumerator.close_async(priority, null); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e, 'Impossible to enumerate trash children'); + } finally { + cancellable.cancel(); + if (this._updateIconCancellable === cancellable) + delete this._updateTrashCancellable; + } + } + + launchAction(action, timestamp) { + if (!this.list_actions().includes(action)) + throw new Error('Action %s is not supported by %s', action, this); + + const nautilus = makeNautilusFileOperationsProxy(); + const askConfirmation = true; + nautilus.EmptyTrashRemote(askConfirmation, + nautilus.platformData({timestamp}), (_p, error) => { + if (error) + logError(error, 'Empty trash failed'); + }, this.cancellable); + } +}); + +/** + * @param shellApp + */ +function wrapWindowsBackedApp(shellApp) { + if (shellApp._dtdData) + throw new Error('%s has been already wrapped'.format(shellApp)); + + shellApp._dtdData = { + windows: [], + state: undefined, + startingWorkspace: 0, + isFocused: false, + proxyProperties: [], + sources: new Set(), + signalConnections: new Utils.GlobalSignalsHandler(), + methodInjections: new Utils.InjectionsHandler(), + propertyInjections: new Utils.PropertyInjectionsHandler(), + addProxyProperties(parent, proxyProperties) { + Object.entries(proxyProperties).forEach(([p, o]) => { + const publicProp = o.public ? p : `_${p}`; + const get = o.getter && o.value instanceof Function + ? () => this[p]() : () => this[p]; + Object.defineProperty(parent, publicProp, Object.assign({ + get, + set: v => (this[p] = v), + configurable: true, + enumerable: !!o.enumerable, + }, o.readOnly ? {set: undefined} : {})); + if (o.value) + this[p] = o.value; + this.proxyProperties.push(publicProp); + }); + }, + destroy() { + this.windows = []; + this.proxyProperties = []; + this.sources.forEach(s => GLib.source_remove(s)); + this.sources.clear(); + this.signalConnections.destroy(); + this.methodInjections.destroy(); + this.propertyInjections.destroy(); + }, + }; + + shellApp._dtdData.addProxyProperties(shellApp, { + windows: {}, + state: {}, + startingWorkspace: {}, + isFocused: {public: true}, + signalConnections: {readOnly: true}, + sources: {readOnly: true}, + checkFocused: {}, + setDtdData: {}, + }); + + shellApp._setDtdData = function (data, params = {}) { + for (const [name, value] of Object.entries(data)) { + if (params.readOnly && name in this._dtdData) + throw new Error('Property %s is already defined'.format(name)); + const defaultParams = {public: true, readOnly: true}; + this._dtdData.addProxyProperties(this, { + [name]: {...defaultParams, ...params, value}, + }); + } + }; + + const m = (...args) => shellApp._dtdData.methodInjections.add(shellApp, ...args); + const p = (...args) => shellApp._dtdData.propertyInjections.add(shellApp, ...args); + + // mi is Method injector, pi is Property injector + shellApp._setDtdData({mi: m, pi: p}, {public: false}); + + m('get_state', () => shellApp._state ?? shellApp._getStateByWindows()); + p('state', {get: () => shellApp.get_state()}); + + m('get_windows', () => shellApp._windows); + m('get_n_windows', () => shellApp._windows.length); + m('get_pids', () => shellApp._windows.reduce((pids, w) => { + if (w.get_pid() > 0 && !pids.includes(w.get_pid())) + pids.push(w.get_pid()); + return pids; + }, [])); + m('is_on_workspace', (_om, workspace) => shellApp._windows.some(w => + w.get_workspace() === workspace) || + (shellApp.state === Shell.AppState.STARTING && + [-1, workspace.index()].includes(shellApp._startingWorkspace))); + m('request_quit', () => shellApp._windows.filter(w => + w.can_close()).forEach(w => w.delete(global.get_current_time()))); + + shellApp._setDtdData({ + _getStateByWindows() { + return this.get_n_windows() ? Shell.AppState.RUNNING : Shell.AppState.STOPPED; + }, + + _updateWindows() { + throw new GObject.NotImplementedError(`_updateWindows in ${this.constructor.name}`); + }, + + _notifyStateChanged() { + Shell.AppSystem.get_default().emit('app-state-changed', this); + this.notify('state'); + }, + + _setState(state) { + const oldState = this.state; + this._state = state; + + if (this.state !== oldState) + this._notifyStateChanged(); + }, + + _setWindows(windows) { + const oldState = this.state; + const oldWindows = this._windows.slice(); + const result = {windowsChanged: false, stateChanged: false}; + this._state = undefined; + + if (windows.length !== oldWindows.length || + windows.some((win, index) => win !== oldWindows[index])) { + this._windows = windows.filter(w => !w.is_override_redirect()); + this.emit('windows-changed'); + result.windowsChanged = true; + } + + if (this.state !== oldState) { + this._notifyStateChanged(); + this._checkFocused(); + result.stateChanged = true; + } + + return result; + }, + }, {readOnly: false}); + + shellApp._sources.add(GLib.idle_add(GLib.DEFAULT_PRIORITY, () => { + shellApp._updateWindows(); + shellApp._sources.delete(GLib.main_current_source().source_id); + return GLib.SOURCE_REMOVE; + })); + + const windowTracker = Shell.WindowTracker.get_default(); + shellApp._checkFocused = function () { + if (this._windows.some(w => w.has_focus())) { + this.isFocused = true; + windowTracker.notify('focus-app'); + } else if (this.isFocused) { + this.isFocused = false; + windowTracker.notify('focus-app'); + } + }; + + shellApp._checkFocused(); + shellApp._signalConnections.add(global.display, 'notify::focus-window', () => + shellApp._checkFocused()); + + // Re-implements shell_app_activate_window for generic activation and alt-tab support + m('activate_window', function (_om, window, timestamp) { + /* eslint-disable no-invalid-this */ + if (!window) + [window] = this.get_windows(); + else if (!this._windows.includes(window)) + return; + + const currentWorkspace = global.workspace_manager.get_active_workspace(); + const workspace = window.get_workspace(); + const sameWorkspaceWindows = this.get_windows().filter(w => + w.get_workspace() === workspace); + sameWorkspaceWindows.forEach(w => w.raise()); + + if (workspace !== currentWorkspace) + workspace.activate_with_focus(window, timestamp); + else + window.activate(timestamp); + /* eslint-enable no-invalid-this */ + }); + + // Re-implements shell_app_activate_full for generic activation and dash support + m('activate_full', function (_om, workspace, timestamp) { + /* eslint-disable no-invalid-this */ + if (!timestamp) + timestamp = global.get_current_time(); + + switch (this.state) { + case Shell.AppState.STOPPED: + try { + this._startingWorkspace = workspace; + this._setState(Shell.AppState.STARTING); + this.launch(timestamp, workspace, Shell.AppLaunchGpu.APP_PREF); + } catch (e) { + logError(e); + this._setState(Shell.AppState.STOPPED); + global.notify_error(_('Failed to launch “%s”'.format( + this.get_name())), e.message); + } + break; + case Shell.AppState.RUNNING: + this.activate_window(null, timestamp); + break; + } + /* eslint-enable no-invalid-this */ + }); + + m('activate', () => shellApp.activate_full(-1, 0)); + + m('compare', (_om, other) => Utils.shellAppCompare(shellApp, other)); + + const {destroy: defaultDestroy} = shellApp; + shellApp.destroy = function () { + /* eslint-disable no-invalid-this */ + this._dtdData.proxyProperties.forEach(prop => delete this[prop]); + this._dtdData.destroy(); + this._dtdData = undefined; + this.appInfo.destroy?.(); + this.destroy = defaultDestroy; + defaultDestroy?.call(this); + /* eslint-enable no-invalid-this */ + }; + + return shellApp; +} + +/** + * We can't inherit from Shell.App as it's a final type, so let's patch it + * + * @param params + */ +function makeLocationApp(params) { + if (!(params?.appInfo instanceof LocationAppInfo)) + throw new TypeError('Invalid location'); + + const {fallbackIconName} = params; + delete params.fallbackIconName; + + const shellApp = new Shell.App(params); + wrapWindowsBackedApp(shellApp); + + shellApp._setDtdData({ + location: () => shellApp.appInfo.location, + isTrash: shellApp.appInfo instanceof TrashAppInfo, + }, {getter: true, enumerable: true}); + + shellApp._mi('toString', defaultToString => + '[LocationApp "%s" - %s]'.format(shellApp.get_id(), + defaultToString.call(shellApp))); + + shellApp._mi('launch', (_om, timestamp, workspace, _gpuPref) => + shellApp.appInfo.launch([], + global.create_app_launch_context(timestamp, workspace))); + + shellApp._mi('launch_action', (_om, actionName, ...args) => + shellApp.appInfo.launchAction(actionName, ...args)); + + shellApp._mi('create_icon_texture', (_om, iconSize) => new St.Icon({ + iconSize, + gicon: shellApp.icon, + fallbackIconName, + })); + + // FIXME: We need to add a new API to Nautilus to open new windows + shellApp._mi('can_open_new_window', () => { + try { + if (!shellApp.get_n_windows()) + return true; + + const handlerApp = shellApp.appInfo.getHandlerApp(); + + if (handlerApp.has_key('SingleMainWindow')) + return !handlerApp.get_boolean('SingleMainWindow'); + + if (handlerApp.has_key('X-GNOME-SingleWindow')) + return !handlerApp.get_boolean('X-GNOME-SingleWindow'); + + if (handlerApp.get_commandline()?.split(' ').includes('--new-window')) + return true; + + const [window] = shellApp.get_windows(); + if (window && window.get_gtk_window_object_path()) + return window.get_gtk_application_id() === null; + + return true; + } catch { + return false; + } + }); + + shellApp._mi('open_new_window', function (_om, workspace) { + /* eslint-disable no-invalid-this */ + const context = global.create_app_launch_context(0, workspace); + if (!this.get_n_windows()) { + this.appInfo.launch([], context); + return; + } + const appId = this.appInfo.get_id(); + Gio.AppInfo.create_from_commandline(this.appInfo.get_commandline(), + this.appInfo.get_id(), appId + ? Gio.AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION + : Gio.AppInfoCreateFlags.NONE).launch_uris( + [this.appInfo.location.get_uri()], context); + /* eslint-enable no-invalid-this */ + }); + + if (shellApp.appInfo instanceof MountableVolumeAppInfo) { + shellApp._mi('get_busy', function (parentGetBusy) { + /* eslint-disable no-invalid-this */ + if (this.appInfo.busy) + return true; + return parentGetBusy.call(this); + /* eslint-enable no-invalid-this */ + }); + shellApp._pi('busy', {get: () => shellApp.get_busy()}); + shellApp._signalConnections.add(shellApp.appInfo, 'notify::busy', _ => + shellApp.notify('busy')); + } + + shellApp._mi('get_windows', function () { + /* eslint-disable no-invalid-this */ + if (this._needsResort) + this._sortWindows(); + return this._windows; + /* eslint-enable no-invalid-this */ + }); + + const {fm1Client} = Docking.DockManager.getDefault(); + shellApp._setDtdData({ + _needsResort: true, + + _windowsOrderChanged() { + this._needsResort = true; + this.emit('windows-changed'); + }, + + _sortWindows() { + this._windows.sort(Utils.shellWindowsCompare); + this._needsResort = false; + }, + + _updateWindows() { + const windows = fm1Client.getWindows(this.location?.get_uri()).sort( + Utils.shellWindowsCompare); + const {windowsChanged} = this._setWindows(windows); + + if (!windowsChanged) + return; + + this._signalConnections.removeWithLabel(Labels.LOCATION_WINDOWS); + windows.forEach(w => + this._signalConnections.addWithLabel(Labels.LOCATION_WINDOWS, w, + 'notify::user-time', () => { + if (w !== this._windows[0]) + this._windowsOrderChanged(); + })); + }, + }, {readOnly: false}); + + shellApp._signalConnections.add(fm1Client, 'windows-changed', () => + shellApp._updateWindows()); + shellApp._signalConnections.add(shellApp.appInfo, 'notify::icon', () => + shellApp.notify('icon')); + shellApp._signalConnections.add(global.workspaceManager, + 'workspace-switched', () => shellApp._windowsOrderChanged()); + + return shellApp; +} + +/** + * + */ +function getFileManagerApp() { + return Shell.AppSystem.get_default().lookup_app(FILE_MANAGER_DESKTOP_APP_ID); +} + +/** + * + */ +export function wrapFileManagerApp() { + const fileManagerApp = getFileManagerApp(); + if (!fileManagerApp) + return null; + + if (fileManagerApp._dtdData) + return fileManagerApp; + + const originalGetWindows = fileManagerApp.get_windows; + wrapWindowsBackedApp(fileManagerApp); + + const {removables, trash} = Docking.DockManager.getDefault(); + fileManagerApp._signalConnections.addWithLabel(Labels.WINDOWS_CHANGED, + fileManagerApp, 'windows-changed', () => { + fileManagerApp.stop_emission_by_name('windows-changed'); + // Let's wait for the location app to take control before of us + const id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => { + fileManagerApp._sources.delete(id); + fileManagerApp._updateWindows(); + return GLib.SOURCE_REMOVE; + }); + fileManagerApp._sources.add(id); + }); + + fileManagerApp._signalConnections.add(global.workspaceManager, + 'workspace-switched', () => { + fileManagerApp._signalConnections.blockWithLabel(Labels.WINDOWS_CHANGED); + fileManagerApp.emit('windows-changed'); + fileManagerApp._signalConnections.unblockWithLabel(Labels.WINDOWS_CHANGED); + }); + + if (removables) { + fileManagerApp._signalConnections.add(removables, 'changed', () => + fileManagerApp._updateWindows()); + fileManagerApp._signalConnections.add(removables, 'windows-changed', () => + fileManagerApp._updateWindows()); + } + + if (trash?.getApp()) { + fileManagerApp._signalConnections.add(trash.getApp(), 'windows-changed', () => + fileManagerApp._updateWindows()); + } + + fileManagerApp._updateWindows = function () { + const locationWindows = []; + getRunningApps().forEach(a => locationWindows.push(...a.get_windows())); + const windows = originalGetWindows.call(this).filter(w => + !locationWindows.includes(w)); + + this._signalConnections.blockWithLabel(Labels.WINDOWS_CHANGED); + this._setWindows(windows); + this._signalConnections.unblockWithLabel(Labels.WINDOWS_CHANGED); + }; + + fileManagerApp._mi('toString', defaultToString => + '[FileManagerApp - %s]'.format(defaultToString.call(fileManagerApp))); + + return fileManagerApp; +} + +/** + * + */ +export function unWrapFileManagerApp() { + const fileManagerApp = getFileManagerApp(); + if (!fileManagerApp || !fileManagerApp._dtdData) + return; + + fileManagerApp.destroy(); +} + +/** + * This class maintains a Shell.App representing the Trash and keeps it + * up-to-date as the trash fills and is emptied over time. + */ +export class Trash { + destroy() { + this._trashApp?.destroy(); + } + + _ensureApp() { + if (this._trashApp) + return; + + this._trashApp = makeLocationApp({ + appInfo: new TrashAppInfo(new Gio.Cancellable()), + fallbackIconName: FALLBACK_TRASH_ICON, + }); + } + + getApp() { + this._ensureApp(); + return this._trashApp; + } +} + +/** + * This class maintains Shell.App representations for removable devices + * plugged into the system, and keeps the list of Apps up-to-date as + * devices come and go and are mounted and unmounted. + */ +export class Removables { + static initVolumePromises(object) { + // TODO: This can be simplified using actual interface type when we + // can depend on gjs 1.72 + if (!(object instanceof Gio.Volume) || object.constructor.prototype._d2dPromisified) + return; + + Gio._promisify(object.constructor.prototype, 'mount', 'mount_finish'); + Gio._promisify(object.constructor.prototype, 'eject_with_operation', + 'eject_with_operation_finish'); + object.constructor.prototype._d2dPromisified = true; + } + + static initMountPromises(object) { + // TODO: This can be simplified using actual interface type when we + // can depend on gjs 1.72 + if (!(object instanceof Gio.Mount) || object.constructor.prototype._d2dPromisified) + return; + + Gio._promisify(object.constructor.prototype, 'eject_with_operation', + 'eject_with_operation_finish'); + Gio._promisify(object.constructor.prototype, 'unmount_with_operation', + 'unmount_with_operation_finish'); + object.constructor.prototype._d2dPromisified = true; + } + + constructor() { + this._signalsHandler = new Utils.GlobalSignalsHandler(); + + this._monitor = Gio.VolumeMonitor.get(); + this._cancellable = new Gio.Cancellable(); + + this._monitor.get_mounts().forEach(m => Removables.initMountPromises(m)); + this._updateVolumes(); + + this._signalsHandler.add([ + this._monitor, + 'volume-added', + (_, volume) => this._onVolumeAdded(volume), + ], [ + this._monitor, + 'volume-removed', + (_, volume) => this._onVolumeRemoved(volume), + ], [ + this._monitor, + 'mount-added', + (_, mount) => this._onMountAdded(mount), + ], [ + Docking.DockManager.settings, + 'changed::show-mounts-only-mounted', + () => this._updateVolumes(), + ], [ + Docking.DockManager.settings, + 'changed::show-mounts-network', + () => this._updateVolumes(), + ]); + } + + destroy() { + this._volumeApps.forEach(a => a.destroy()); + this._volumeApps = []; + this._cancellable.cancel(); + this._cancellable = null; + this._signalsHandler.destroy(); + this._monitor = null; + } + + _updateVolumes() { + this._volumeApps?.forEach(a => a.destroy()); + this._volumeApps = []; + this.emit('changed'); + + this._monitor.get_volumes().forEach(v => this._onVolumeAdded(v)); + } + + _onVolumeAdded(volume) { + Removables.initVolumePromises(volume); + + if (!Docking.DockManager.settings.showMountsNetwork && + volume.get_identifier('class') === 'network') + return; + + + const mount = volume.get_mount(); + if (mount) { + if (mount.is_shadowed()) + return; + if (!mount.can_eject() && !mount.can_unmount()) + return; + } else { + if (Docking.DockManager.settings.showMountsOnlyMounted) + return; + if (!volume.can_mount() && !volume.can_eject()) + return; + } + + const appInfo = new MountableVolumeAppInfo(volume, + new Utils.CancellableChild(this._cancellable)); + const volumeApp = makeLocationApp({ + appInfo, + fallbackIconName: FALLBACK_REMOVABLE_MEDIA_ICON, + }); + + volumeApp._signalConnections.add(volumeApp, 'windows-changed', + () => this.emit('windows-changed', volumeApp)); + + if (Docking.DockManager.settings.showMountsOnlyMounted) { + volumeApp._signalConnections.add(appInfo, 'notify::mount', + () => !appInfo.mount && this._onVolumeRemoved(appInfo.volume)); + } + + this._volumeApps.push(volumeApp); + this.emit('changed'); + } + + _onVolumeRemoved(volume) { + const volumeIndex = this._volumeApps.findIndex(({appInfo}) => + appInfo.volume === volume); + if (volumeIndex !== -1) { + const [volumeApp] = this._volumeApps.splice(volumeIndex, 1); + // We don't care about cancelling the ongoing operations from now on. + volumeApp.appInfo.cancellable = null; + volumeApp.destroy(); + this.emit('changed'); + } + } + + _onMountAdded(mount) { + Removables.initMountPromises(mount); + + if (!Docking.DockManager.settings.showMountsOnlyMounted) + return; + + if (!this._volumeApps.find(({appInfo}) => appInfo.mount === mount)) { + // In some Gio.Mount implementations the volume may be set after + // mount is emitted, so we could just ignore it as we'll get it + // later via volume-added + const volume = mount.get_volume(); + if (volume) + this._onVolumeAdded(volume); + } + } + + getApps() { + return this._volumeApps; + } +} +Signals.addSignalMethods(Removables.prototype); + +/** + * + */ +function getApps() { + const dockManager = Docking.DockManager.getDefault(); + const locationApps = []; + + if (dockManager.removables) + locationApps.push(...dockManager.removables.getApps()); + + if (dockManager.trash) + locationApps.push(dockManager.trash.getApp()); + + return locationApps; +} + +/** + * + */ +export function getRunningApps() { + return getApps().filter(a => a.state === Shell.AppState.RUNNING); +} + +/** + * + */ +export function getStartingApps() { + return getApps().filter(a => a.state === Shell.AppState.STARTING); +} |