import GObject from 'gi://GObject'; import Gio from 'gi://Gio'; const Config = await import("resource:///org/gnome/shell/misc/config.js").catch(async () => await import("resource:///org/gnome/Shell/Extensions/js/misc/config.js") ); export function split(string, sep, maxsplit) { const splitted = string.split(sep); return maxsplit ? splitted.slice(0, maxsplit).concat([splitted.slice(maxsplit).join(sep)]) : splitted; } export function rsplit(string, sep, maxsplit) { const splitted = string.split(sep); return maxsplit ? [splitted.slice(0, -maxsplit).join(sep)].concat(splitted.slice(-maxsplit)) : splitted; } export function array_remove(array, item) { const index = array.indexOf(item); if (index > -1) { array.splice(index, 1); return true; } return false; } export function array_insert(array, index, ...items) { array.splice(index, 0, ...items); } export function get_stack() { return new Error().stack.split('\n').slice(1).map(l => l.trim()).filter(Boolean).map(frame => { const [func, remaining] = split(frame, '@', 1); const [file, line, column] = rsplit(remaining, ':', 2); return { func, file, line, column }; }); } export function get_extension_uuid() { const stack = get_stack(); for (const frame of stack.reverse()) { if (frame.file.includes('/gnome-shell/extensions/')) { const [left, right] = frame.file.split('@').slice(-2); return `${left.split('/').at(-1)}@${right.split('/')[0]}`; } } return undefined; } export function get_shell_version() { const [major, minor] = Config.PACKAGE_VERSION.split('.').map(s => Number(s)); return { major, minor }; } export function add_named_connections(patcher, object) { // this is used to debug things /*function _get_address(object) { return object.toString().slice(1, 15); }*/ function set_signal(object, source, signal, callback, id) { // Add the source map if (object._lp_connections === undefined) { object._lp_connections = new Map(); if (object instanceof GObject.Object) { object.connect('destroy', () => { /*console.log(`${object}: removing connections`, JSON.stringify(object._lp_connections, function simplify(key, value) { if (value instanceof Map) return Object.fromEntries(Array.from(value, ([k, v]) => [simplify(null, k), v])); else if (value instanceof GObject.Object) return _get_address(value); else if (value instanceof Function) return ""; else return value; }) );*/ object.disconnect_named(); }); } } const source_map = object._lp_connections; // Add the signal map if (!source_map.has(source)) { source_map.set(source, new Map()); source.connect('destroy', () => { //console.log(`REMOVING ${_get_address(source)} FROM ${_get_address(object)}`); source_map.delete(source); }); } const signal_map = source_map.get(source); // Add the callback map if (!signal_map.has(signal)) signal_map.set(signal, new Map()); const callback_map = signal_map.get(signal); //console.log(`CONNECT ${_get_address(source)}::${signal} -> ${_get_address(object)}::${id}`); //console.log(`Fake connections are ${Object.fromEntries(Object.entries(source?._signalConnections))}`); // Add the id callback_map.set(callback, id); return id + 100000; // this is just here to prevent any accidental usage of this id with normal disconnect } patcher.replace_method(object, function connect_named(_wrapped, source, signal, callback) { return set_signal(this, source, signal, callback, source.connect(signal, callback)); }); patcher.replace_method(object, function connect_after_named(_wrapped, source, signal, callback) { return set_signal(this, source, signal, callback, source.connect_after(signal, callback)); }); patcher.replace_method(object, function disconnect_named(_wrapped, source = undefined, signal = undefined, callback = undefined) { if (typeof source === 'number') { // The function was called with an id. const id_to_remove = source - 100000; const source_map = this._lp_connections; if (!source_map) return; for (const [source, signal_map] of source_map.entries()) { for (const [signal, callback_map] of signal_map.entries()) { for (const [callback, id] of callback_map.entries()) { if (id === id_to_remove) { this.disconnect_named(source, signal, callback); } } } } return; } if (callback !== undefined) { // Every argments have been provided const source_map = this._lp_connections; if (!source_map) return; const signal_map = source_map.get(source); if (!signal_map) return; const callback_map = signal_map.get(signal); if (!callback_map) return; const id = callback_map.get(callback); if (id === undefined) return; // console.log(`Disconnecting ${signal} on ${source} with id ${id}`); // console.log(`Fake connections are ${Object.fromEntries(Object.entries(source?._signalConnections))}`); if (source.signalHandlerIsConnected?.(id) || (source instanceof GObject.Object && GObject.signal_handler_is_connected(source, id))) source.disconnect(id); callback_map.delete(callback); } else if (signal !== undefined) { // Only source and signal have been provided // console.log(`Disconnecting ${signal} on ${source}`); const source_map = this._lp_connections; if (!source_map) return; const signal_map = source_map.get(source); if (!signal_map) return; const callback_map = signal_map.get(signal); if (!callback_map) return; for (const callback of callback_map.keys()) { this.disconnect_named(source, signal, callback); } signal_map.delete(signal); } else if (source !== undefined) { // Only source have been provided // console.log(`Disconnecting ${source}`); const source_map = this._lp_connections; if (!source_map) return; const signal_map = source_map.get(source); if (!signal_map) return; for (const signal of signal_map.keys()) { this.disconnect_named(source, signal); } source_map.delete(source); } else { // Nothing have been provided // console.log("Disconnecting everything"); const source_map = this._lp_connections; if (!source_map) return; for (const source of source_map.keys()) { this.disconnect_named(source); } this._lp_connections.clear(); } }); } export function find_panel(widget) { const panels = []; do { if (widget.is_grid_item) { panels.push(widget); } } while ((widget = widget.get_parent()) !== null); return panels.at(-1); } export function get_settings(path) { const [parent_path, file] = rsplit(path, '/', 1); const id = rsplit(file, '.', 2)[0]; const source = Gio.SettingsSchemaSource.new_from_directory( parent_path, Gio.SettingsSchemaSource.get_default(), false ); return new Gio.Settings({ settings_schema: source.lookup(id, true) }); } export function get_style(widget) { return widget.style ?.split(';') .map(x => { const [name, value] = split(x, ':', 1).map(x => x.trim()); return { name, value }; }) .filter(x => x.name !== '') || []; } export function set_style(widget, name, value) { let style = get_style(widget).filter(x => x.name !== name); if (value !== null) style.push({ name, value }); widget.style = style .map(({ name, value }) => `${name}: ${value}`) .join(';'); }