BossBey File Manager
PHP:
8.2.28
OS:
Linux
User:
www-data
Root
/
usr
/
share
/
cinnamon
/
js
/
ui
📤 Upload
📝 New File
📁 New Folder
Close
Editing: extension.js
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const ByteArray = imports.byteArray; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const Gtk = imports.gi.Gtk; const Signals = imports.signals; const St = imports.gi.St; const Meta = imports.gi.Meta; const AppletManager = imports.ui.appletManager; const Config = imports.misc.config; const DeskletManager = imports.ui.deskletManager; const ExtensionSystem = imports.ui.extensionSystem; const SearchProviderManager = imports.ui.searchProviderManager; const Main = imports.ui.main; const {requireModule, unloadModule, getModuleByIndex} = imports.misc.fileUtils; const {queryCollection} = imports.misc.util; var State = { INITIALIZING: 0, LOADED: 1, ERROR: 2, OUT_OF_DATE: 3, X11_ONLY: 4 }; // Xlets using imports.gi.NMClient. This should be removed in Cinnamon 4.2+, // after these applets have been updated on Spices. var knownCinnamon4Conflicts = [ // Applets 'turbonote@iksws.com.b', 'vnstat@linuxmint.com', 'netusagemonitor@pdcurtis', // Desklets 'netusage@30yavash.com' ]; var x11Only = [ "keyboard@cinnamon.org", "systray@cinnamon.org" ] // macro for creating extension types function _createExtensionType(name, folder, manager, overrides){ let type = { name: name, folder: folder, requiredFunctions: ["main"], requiredProperties: ["uuid", "name", "description"], niceToHaveProperties: [], roles: {}, callbacks: { finishExtensionLoad: manager.finishExtensionLoad, prepareExtensionUnload: manager.prepareExtensionUnload, prepareExtensionReload: manager.prepareExtensionReload }, userDir: GLib.build_filenamev([global.userdatadir, folder]), legacyMeta: {} }; Object.assign(type, overrides); // Add signal methods Signals.addSignalMethods(type); // create user directories if they don't exist. let dir = Gio.file_new_for_path(type.userDir) try { if(!dir.query_exists(null)) dir.make_directory_with_parents(null); } catch(e){ global.logError(e); } return type; } /** * const Type: * @EXTENSION: Cinnamon extensions * @APPLET: Cinnamon panel applets * * @name: Upper case first character name for printing messages * Also converted to lowercase to find the correct javascript file * @folder: The folder name within the system and user cinnamon folders * @requiredFunctions: Functions that must exist in the main javascript file * @requiredProperties: Properties that must be set in the metadata.json file * @niceToHaveProperties: Properties that are encouraged to be set in the metadata.json file * @roles: Roles an extension can assume. Values will be set internally, set to null. * key => name of the role, value => reference to the extension object * @callbacks: Callbacks used to do some manual actions on load / unload * * Extension types with some attributes helping to load these extension types. * Properties are nested, with lowerCamelCase properties (e.g. requiredFunctions) as sub-properties of CAPITAL one (EXTENSION). Thus they are referred to as, e.g., Type.EXTENSION.requiredFunctions */ var startTime; var extensions = []; var Type = { EXTENSION: _createExtensionType("Extension", "extensions", ExtensionSystem, { requiredFunctions: ["init", "disable", "enable"], requiredProperties: ["uuid", "name", "description", "cinnamon-version"], niceToHaveProperties: ["url"], }), APPLET: _createExtensionType("Applet", "applets", AppletManager, { roles: { notifications: null, windowlist: null, windowattentionhandler: null, panellauncher: null, tray: null } }), DESKLET: _createExtensionType("Desklet", "desklets", DeskletManager, { roles: { notifications: null, windowlist: null, windowattentionhandler: null } }), SEARCH_PROVIDER: _createExtensionType("Search provider", "search_providers", SearchProviderManager, { requiredFunctions: ["perform_search", "on_result_selected"] }) }; // Create a dummy metadata object when metadata parsing failed or was not done yet. function createMetaDummy(uuid, path, state) { return {name: uuid, description: 'Metadata load failed', state: state, path: path, error: ''}; } function getExtension(uuid) { return queryCollection(extensions, {uuid}); } function formatError(uuid, message) { return `[${uuid}]: ${message}`; } function logError(message, uuid, error, state) { let errorMessage = formatError(uuid, message); if (!error) { error = new Error(errorMessage, { cause: state }); } else { error.message = `\n${formatError(uuid, error.message)}`; error.message += `\n${errorMessage}`; } if (state !== State.X11_ONLY) { error.stack = error.stack.split('\n') .filter(function(line) { return !line.match(/<Promise>|wrapPromise/); }) .join('\n'); global.logError(error); } else { global.logWarning(error.message); } // An error during initialization leads to unloading the extension again. let extension = getExtension(uuid); if (extension) { extension.meta.state = state || State.ERROR; extension.meta.error += message; if (extension.meta.state === State.INITIALIZING) { extension.unlockRoles(); extension.unloadStylesheet(); extension.unloadIconDirectory(); forgetExtension(uuid, Type[extension.upperType]); } } error._alreadyLogged = true; return error; } function ensureFileExists(file) { if (!file.query_exists(null)) { throw logError(`File not found: ${file.get_path()}`); } } // The Extension object itself function Extension(type, uuid) { let extension = getExtension(uuid); if (extension) { return Promise.resolve(true); } let force = false; if (uuid.substr(0, 1) === '!') { uuid = uuid.replace(/^!/, ''); force = true; } let dir = findExtensionDirectory(uuid, type.userDir, type.folder); if (dir == null) { forgetExtension(uuid, type, true); return Promise.resolve(null); } return this._init(dir, type, uuid, force); } Extension.prototype = { _init: function(dir, type, uuid, force) { this.name = type.name; this.uuid = uuid; this.dir = dir; this.upperType = type.name.toUpperCase().replace(/\s/g, "_"); this.lowerType = type.name.toLowerCase().replace(/\s/g, "_"); this.theme = null; this.stylesheet = null; this.iconDirectory = null; this.meta = createMetaDummy(uuid, dir.get_path(), State.INITIALIZING); let isPotentialNMClientConflict = knownCinnamon4Conflicts.indexOf(uuid) > -1; const finishLoad = () => { // Many xlets still use appletMeta/deskletMeta to get the path type.legacyMeta[uuid] = {path: this.meta.path}; ensureFileExists(this.dir.get_child(`${this.lowerType}.js`)); this.loadStylesheet(this.dir.get_child('stylesheet.css')); if (this.stylesheet) { Main.themeManager.connect('theme-set', () => { this.loadStylesheet(this.dir.get_child('stylesheet.css')); }); } this.loadIconDirectory(this.dir); // get [extension/applet/desklet].js return requireModule( `${this.meta.path}/${this.lowerType}.js`, // path this.meta.path, // dir, this.meta, // meta this.lowerType, // type true, // async true // returnIndex ); }; return loadMetaData({ state: this.meta.state, path: this.meta.path, uuid: uuid, userDir: type.userDir, folder: type.folder, force: force }).then((meta) => { // Timer needs to start after the first initial I/O, otherwise every applet shows as taking 1-2 seconds to load. // Maybe because of how promises are wired up in CJS? // https://github.com/linuxmint/cjs/blob/055da399c794b0b4d76ecd7b5fabf7f960f77518/modules/_lie.js#L9 startTime = new Date().getTime(); this.meta = meta; if (!force) { this.validateMetaData(); } return findExtensionSubdirectory(this.dir).then((dir) => { this.dir = dir; this.meta.path = this.dir.get_path(); // If an xlet has known usage of imports.gi.NMClient, we require them to have a // 4.0 directory. It is the only way to assume they are patched for Cinnamon 4 from here. if (isPotentialNMClientConflict && this.meta.path.indexOf(`/4.0`) === -1) { throw new Error(`Found unpatched usage of imports.gi.NMClient for ${this.lowerType} ${uuid}`); } return finishLoad(); }); }).then((moduleIndex) => { if (moduleIndex == null) { throw new Error(`Could not find module index: ${moduleIndex}`); } this.moduleIndex = moduleIndex; for (let i = 0; i < type.requiredFunctions.length; i++) { let func = type.requiredFunctions[i]; if (!getModuleByIndex(moduleIndex)[func]) { throw new Error(`Function "${func}" is missing`); } } // Add the extension to the global collection extensions.push(this); if(!type.callbacks.finishExtensionLoad(extensions.length - 1)) { throw new Error(`${type.name} ${uuid}: Could not create ${this.lowerType} object.`); } this.finalize(); Main.cinnamonDBusService.EmitXletAddedComplete(true, uuid); }).catch((e) => { // Silently fail to load xlets that aren't actually installed - // but no error, since the user can't do anything about it anyhow // (short of editing gsettings). Silent failure is consistent with // other reactions in Cinnamon to missing items (e.g. panel launchers // just don't show up if their program isn't installed, but we don't // remove them or anything) Main.cinnamonDBusService.EmitXletAddedComplete(false, uuid); if (e.cause == null || e.cause !== State.X11_ONLY) { Main.xlet_startup_error = true; } forgetExtension(uuid, type); if (e._alreadyLogged) { return; } logError(`Error importing ${this.lowerType}.js from ${uuid}`, uuid, e); }); }, finalize: function() { this.meta.state = State.LOADED; Type[this.upperType].emit('extension-loaded', this.uuid); let endTime = new Date().getTime(); global.log(`Loaded ${this.lowerType} ${this.uuid} in ${endTime - startTime} ms`); startTime = new Date().getTime(); }, validateMetaData: function() { // Some properties are required to run if (x11Only.includes(this.meta.uuid) && Meta.is_wayland_compositor()) { throw logError("Extension not compatible with Wayland", this.uuid, null, State.X11_ONLY); } this.checkProperties(Type[this.upperType].requiredProperties, true); // Others are nice to have this.checkProperties(Type[this.upperType].niceToHaveProperties, false); if (this.meta.uuid != this.uuid) { throw logError(`uuid "${this.meta.uuid}" from metadata.json does not match directory name.`, this.uuid); } // If cinnamon versions are set check them if ('cinnamon-version' in this.meta && !versionCheck(this.meta['cinnamon-version'], Config.PACKAGE_VERSION)) { throw logError('Extension is not compatible with current Cinnamon version', this.uuid, null, State.OUT_OF_DATE); } // If a role is set, make sure it's a valid one let meta_role_list_str = this.meta['role']; if (meta_role_list_str) { let meta_roles = meta_role_list_str.replace(" ", "").split(","); for (let role of meta_roles) { if (!(role in Type[this.upperType].roles)) { throw logError(`Unknown role definition: ${role} in metadata.json`, this.uuid); } } } }, checkProperties: function(properties, fatal) { for (let i = 0; i < properties.length; i++) { if (!this.meta[properties[i]]) { let msg = `Missing property "${properties[i]}" in metadata.json`; if(fatal) throw logError(msg, this.uuid); else global.logWarning(formatError(this.uuid, msg)); } } }, loadStylesheet: function (file) { if (file.query_exists(null)) { try { let themeContext = St.ThemeContext.get_for_stage(global.stage); this.theme = themeContext.get_theme(); } catch (e) { throw logError('Error trying to get theme', this.uuid, e); } try { let path = file.get_path(); this.theme.load_stylesheet(path); this.stylesheet = path; } catch (e) { throw logError('Stylesheet parse error', this.uuid, e); } } }, unloadStylesheet: function () { if (this.theme != null && this.stylesheet != null) { try { this.theme.unload_stylesheet(this.stylesheet); } catch (e) { global.logError('Error unloading stylesheet', e); } } }, loadIconDirectory: function(dir) { let iconDir = dir.get_child("icons"); if (iconDir.query_exists(null)) { let path = iconDir.get_path(); this.iconDirectory = path; Gtk.IconTheme.get_default().append_search_path(path); } }, unloadIconDirectory: function() { if (this.iconDirectory) { let iconTheme = Gtk.IconTheme.get_default(); let searchPath = iconTheme.get_search_path(); for (let i = 0; i < searchPath.length; i++) { if (searchPath[i] == this.iconDirectory) { searchPath.splice(i,1); iconTheme.set_search_path(searchPath); break; } } } }, lockRole: function(roleProvider) { if (this.meta && this.meta.role) { let meta_role_list_str = this.meta.role; let meta_roles = meta_role_list_str.replace(" ", "").split(","); let avail_roles = []; for (let role of meta_roles) { if (Type[this.upperType].roles[role] !== this.uuid) { if (Type[this.upperType].roles[role] != null) { continue; } avail_roles.push(role); } } if (avail_roles.length == 0) { return false; } if (roleProvider != null) { for (let role of avail_roles) { Type[this.upperType].roles[role] = this.uuid; this.roleProvider = roleProvider; global.log(`Role locked: ${role}`); } } } return true; }, unlockRoles: function() { if (this.meta.role) { let meta_role_list_str = this.meta.role; let meta_roles = meta_role_list_str.replace(" ", "").split(","); for (let role of meta_roles) { if (Type[this.upperType].roles[role] === this.uuid) { Type[this.upperType].roles[role] = null; this.roleProvider = null; global.log(`Role unlocked: ${role}`); } } } } } /** * versionCheck: * @required: an array of minimum versions we are compatible with * @current: the version we have * * Check if a component is compatible for an extension. * @required is an array, and at least one version must be lower than the current version. * @current must be in the format <major>.<minor>.<point>.<micro> * <micro> is always ignored * <point> is ignored if not specified (so you can target the whole release) * <minor> and <major> must match * Each target version must be at least <major> and <minor> */ function versionCheck(required, current) { let currentArray = current.split('.'); let currentMajor = parseInt(currentArray[0]); let currentMinor = parseInt(currentArray[1]); for (let i = 0; i < required.length; i++) { let requiredArray = required[i].split('.'); requiredMajor = parseInt(requiredArray[0]); requiredMinor = parseInt(requiredArray[1]); if (currentMajor > requiredMajor || (currentMajor == requiredMajor && currentMinor >= requiredMinor)) { return true; } } return false; } /** * versionLeq: * @a (string): the first version * @b (string): the second version * * Returns: whether a <= b */ function versionLeq(a, b) { a = a.split('.'); b = b.split('.'); if (a.length == 2) a.push(0); if (b.length == 2) b.push(0); for (let i = 0; i < 3; i++) { if (a[i] == b[i]) continue; else if (a[i] > b[i]) return false; else return true; } return true; } // Returns a string version of a State value function getMetaStateString(state) { switch (state) { case State.INITIALIZING: return _("Initializing"); case State.LOADED: return _("Loaded"); case State.ERROR: return _("Error"); case State.OUT_OF_DATE: return _("Out of date"); case State.X11_ONLY: return _("Not compatible with Wayland"); } return 'Unknown'; // Not translated, shouldn't appear } /** * loadExtension: * * @uuid (string): uuid of xlet * @type (Extension.Type): type of xlet */ function loadExtension(uuid, type) { return new Extension(type, uuid); } /** * unloadExtension: * * @uuid (string): uuid of xlet * @type (Extension.Type): type of xlet * @deleteConfig (bool): delete also config files, defaults to true */ function unloadExtension(uuid, type, deleteConfig = true, reload = false) { let extensionIndex = queryCollection(extensions, {uuid}, true); if (extensionIndex > -1) { let extension = extensions[extensionIndex]; extension.unlockRoles(); // Try to disable it -- if it's ERROR'd, we can't guarantee that, // but it will be removed on next reboot, and hopefully nothing // broke too much. try { if (reload) { Type[extension.upperType].callbacks.prepareExtensionReload(extension); } Type[extension.upperType].callbacks.prepareExtensionUnload(extension, deleteConfig); } catch (e) { logError(`Error disabling ${extension.lowerType} ${extension.uuid}`, extension.uuid, e); } extension.unloadStylesheet(); extension.unloadIconDirectory(); Type[extension.upperType].emit('extension-unloaded', extension.uuid); forgetExtension(extensionIndex, uuid, type, true); } } function forgetExtension(extensionIndex, uuid, type, forgetMeta) { if (typeof extensions[extensionIndex] !== 'undefined') { unloadModule(extensions[extensionIndex].moduleIndex); try { delete imports[type.folder][uuid]; } catch (e) {} if (forgetMeta) { extensions[extensionIndex] = undefined; extensions.splice(extensionIndex, 1); } } } /** * reloadExtension: * * @uuid (string): uuid of xlet * @type (Extension.Type): type of xlet * * Reloads an xlet. Useful when the source has changed. */ function reloadExtension(uuid, type) { let extension = getExtension(uuid); if (extension) { unloadExtension(uuid, type, false, true); Main._addXletDirectoriesToSearchPath(); if (extension.meta.force_loaded) { uuid = "!" + uuid; } loadExtension(uuid, type); return; } loadExtension(uuid, type); } function findExtensionDirectory(uuid, userDir, folder) { let dir, dirPath; if (!GLib.getenv('CINNAMON_TROUBLESHOOT')) { dirPath = `${userDir}/${uuid}`; dir = Gio.file_new_for_path(dirPath); if (dir.query_file_type(Gio.FileQueryInfoFlags.NONE, null) === Gio.FileType.DIRECTORY) { return dir; } } let systemDataDirs = GLib.get_system_data_dirs(); for (let i = 0; i < systemDataDirs.length; i++) { dirPath = `${systemDataDirs[i]}/cinnamon/${folder}/${uuid}`; dir = Gio.file_new_for_path(dirPath); if (dir.query_file_type(Gio.FileQueryInfoFlags.NONE, null) === Gio.FileType.DIRECTORY) { return dir; } } return null; } function getMetadata(uuid, type) { return loadMetaData({ uuid, userDir: type.userDir, folder: type.folder }); } function maybeAddWindowAttentionHandlerRole(meta) { const keywords = ['window-list', 'windowlist', 'taskbar']; keywords.some(element => { if (meta.uuid.includes(element)) { if (!meta.role) { meta.role = "windowattentionhandler"; } else { if (!meta.role.includes("windowattentionhandler")) { meta.role += ",windowattentionhandler"; } } } }); } function loadMetaData({state, path, uuid, userDir, folder, force}) { return new Promise((resolve, reject) => { let dir = findExtensionDirectory(uuid, userDir, folder); let meta; let metadataFile = dir.get_child('metadata.json'); let oldState = state ? state : State.INITIALIZING; let oldPath = path ? path : dir.get_path(); ensureFileExists(metadataFile); metadataFile.load_contents_async(null, (object, result) => { try { let [success, json] = metadataFile.load_contents_finish(result); if (!success) { reject(); return; } meta = JSON.parse(ByteArray.toString(json)); maybeAddWindowAttentionHandlerRole(meta); } catch (e) { logError(`Failed to load/parse metadata.json`, uuid, e); meta = createMetaDummy(uuid, oldPath, State.ERROR); } // Store some additional crap here meta.state = oldState; meta.path = oldPath; meta.error = ''; meta.force_loaded = force; resolve(meta); }); }); } /** * findExtensionSubdirectory: * @dir (Gio.File): directory to search in * * For extensions that are shipped with multiple versions in different * directories, look for the largest available version that is less than or * equal to the current running version. If no such version is found, the * original directory is returned. * * Returns (Gio.File): directory object of the desired directory. */ function findExtensionSubdirectory(dir) { return new Promise(function(resolve, reject) { dir.enumerate_children_async( 'standard::*', Gio.FileQueryInfoFlags.NONE, GLib.PRIORITY_DEFAULT, null, function(obj, res) { try { let fileEnum = obj.enumerate_children_finish(res); let info; let largest = null; while ((info = fileEnum.next_file(null)) != null) { let fileType = info.get_file_type(); if (fileType !== Gio.FileType.DIRECTORY) { continue; } let name = info.get_name(); if (!name.match(/^[1-9][0-9]*\.[0-9]+(\.[0-9]+)?$/)) { continue; } if (versionLeq(name, Config.PACKAGE_VERSION) && (!largest || versionLeq(largest[0], name))) { largest = [name, fileEnum.get_child(info)]; } } fileEnum.close(null); resolve(largest ? largest[1] : dir); } catch (e) { logError(`Error looking for extension version for ${dir.get_basename()} in directory ${dir}`, 'findExtensionSubdirectory', e); resolve(dir) } }); }); } function get_max_instances (uuid, type) { let extension = getExtension(uuid); if (extension && extension.uuid) { if (extension.meta['max-instances']) { let i = extension.meta['max-instances']; return parseInt(i); } } return 1; }
Save
Cancel