import GLib from 'gi://GLib'; const Signals = imports.signals; function new_id(prefix) { return `${prefix}_${GLib.uuid_string_random()}`; } function clone_effect(effect) { return { ...effect, params: Object.fromEntries( Object.entries(effect.params ?? {}).map(([key, value]) => [ key, Array.isArray(value) ? [...value] : value, ]) ), }; } /// The `PipelinesManager` object permits to store the list of pipelines and their effects in /// memory. It is meant to *always* be in sync with the `org.gnome.shell.extensions.blur-my-shell`'s /// `pipelines` gschema. However, we do not want to re-create every effect each time this schema is /// changed, so the pipelines manager handles it, and dispatches the updates with targeted signals. /// /// It is only connected to ONE signal (the pipelines schema being changed), and emits numerous /// which are connected to by both the different `Pipeline` objects in the extension, and by the /// different pages of the extension preferences. /// It emits three different types of signals: /// /// - general changes to the pipelines list, connected to by the extension preferences: /// - `pipeline-list-changed`, when the list of pipelines has changed (by creation or deletion) /// - `pipeline-names-changed`, when the name of a pipeline is changed /// /// - signals that are targeted towards a given pipeline, with `pipeline_id` being its unique id: /// - `'pipeline_id'::pipeline-updated`, handing a new pipeline descriptor object, when the /// pipeline has been changed quite a bit (added/destroyed/reordered the effects) /// - `'pipeline_id'::pipeline-destroyed`, when the pipeline has been destroyed /// - `'pipeline_id'::pipeline-renamed`, handing the new name, when the pipeline has been /// renamed, which is only important for the preferences /// /// - signals that are targeted towards a given effect, with `effect_id` being its unique id, and /// `pipeline_id` the unique id of the pipeline it is attached to: /// - `'pipeline_id'::effect-'effect_id'-key-removed`, handing the key that was removed /// - `'pipeline_id'::effect-'effect_id'-key-updated`, handing the key that was changed and its /// new value /// - `'pipeline_id'::effect-'effect_id'-key-added`, handing the key that was added and its /// value export class PipelinesManager { constructor(settings) { this.settings = settings; this.pipelines = this.settings.PIPELINES; this.settings.PIPELINES_changed(_ => this.on_pipeline_update()); } create_pipeline(name, effects = []) { const id = new_id('pipeline'); effects.forEach(effect => effect.id = new_id('effect')); this.pipelines[id] = { name, effects }; this.settings.PIPELINES = this.pipelines; this._emit('pipeline-created', id, this.pipelines[id]); this._emit('pipeline-list-changed'); return id; } duplicate_pipeline(id) { if (!(id in this.pipelines)) { this._warn(`could not duplicate pipeline, id ${id} does not exist`); return; } const pipeline = this.pipelines[id]; this.create_pipeline( `${pipeline.name} - duplicate`, pipeline.effects.map(clone_effect) ); } delete_pipeline(id) { if (!(id in this.pipelines)) { this._warn(`could not delete pipeline, id ${id} does not exist`); return; } if (id == "pipeline_default") { this._warn(`could not delete pipeline "pipeline_default" as it is immutable`); return; } delete this.pipelines[id]; this.settings.PIPELINES = this.pipelines; this._emit(id + '::pipeline-destroyed'); this._emit('pipeline-list-changed'); } update_pipeline_effects(id, effects, emit_update_signal = true) { if (!(id in this.pipelines)) { this._warn(`could not update pipeline effects, id ${id} does not exist`); return; } this.pipelines[id].effects = [...effects]; this.settings.PIPELINES = this.pipelines; if (emit_update_signal) this._emit(id + '::pipeline-updated'); } rename_pipeline(id, name) { if (!(id in this.pipelines)) { this._warn(`could not rename pipeline, id ${id} does not exist`); return; } this.pipelines[id].name = name; this.settings.PIPELINES = this.pipelines; this._emit(id + '::pipeline-renamed', name); this._emit('pipeline-names-changed'); } on_pipeline_update() { const old_pipelines = this.pipelines; this.pipelines = this.settings.PIPELINES; const old_ids = Object.keys(old_pipelines); const new_ids = Object.keys(this.pipelines); const list_changed = old_ids.length !== new_ids.length || old_ids.some(id => !(id in this.pipelines)); let names_changed = false; for (const pipeline_id of new_ids) { if (!(pipeline_id in old_pipelines)) this._emit('pipeline-created', pipeline_id, this.pipelines[pipeline_id]); } for (const pipeline_id of old_ids) { // if we find a pipeline that does not exist anymore, signal it if (!(pipeline_id in this.pipelines)) { this._emit(pipeline_id + '::pipeline-destroyed'); continue; } const old_pipeline = old_pipelines[pipeline_id]; const new_pipeline = this.pipelines[pipeline_id]; if (old_pipeline.name !== new_pipeline.name) { this._emit(pipeline_id + '::pipeline-renamed', new_pipeline.name); names_changed = true; } // verify if both pipelines have effects in the same order // if they have, then check for their parameters if ( old_pipeline.effects.length == new_pipeline.effects.length && old_pipeline.effects.every((effect, i) => effect.id === new_pipeline.effects[i].id) ) { for (let i = 0; i < old_pipeline.effects.length; i++) { const old_effect = old_pipeline.effects[i]; const new_effect = new_pipeline.effects[i]; const id = old_effect.id; for (let key in old_effect.params) { // if a key was removed, we emit to tell the effect to use the default value if (!(key in new_effect.params)) this._emit( pipeline_id + '::effect-' + id + '-key-removed', key ); // if a key was updated, we emit to tell the effect to change its value else if (old_effect.params[key] != new_effect.params[key]) this._emit( pipeline_id + '::effect-' + id + '-key-updated', key, new_effect.params[key] ); } for (let key in new_effect.params) { // if a key was added, we emit to tell the effect the key and its value if (!(key in old_effect.params)) this._emit( pipeline_id + '::effect-' + id + '-key-added', key, new_effect.params[key] ); } } } // if either the order has changed, or there are new effects, then rebuild it else this._emit(pipeline_id + '::pipeline-updated', new_pipeline); } if (list_changed) this._emit('pipeline-list-changed'); if (names_changed) this._emit('pipeline-names-changed'); } destroy() { this.settings.PIPELINES_disconnect(); } _emit(signal, ...args) { this.emit(signal, ...args); this._log(`signal: '${signal}', arguments: ${args}`); } _log(str) { if (this.settings.DEBUG) console.log(`[Blur my Shell > pipelines] ${str}`); } _warn(str) { console.warn(`[Blur my Shell > pipelines] ${str}`); } } Signals.addSignalMethods(PipelinesManager.prototype);