448 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			448 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| // This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
 | |
| //
 | |
| // This program is free software; you can redistribute it and/or
 | |
| // modify it under the terms of the GNU General Public License
 | |
| // as published by the Free Software Foundation; either version 2
 | |
| // of the License, or (at your option) any later version.
 | |
| //
 | |
| // This program is distributed in the hope that it will be useful,
 | |
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| // GNU General Public License for more details.
 | |
| //
 | |
| // You should have received a copy of the GNU General Public License
 | |
| // along with this program; if not, write to the Free Software
 | |
| // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 | |
| 
 | |
| import Gio from 'gi://Gio';
 | |
| import GLib from 'gi://GLib';
 | |
| import GObject from 'gi://GObject';
 | |
| import St from 'gi://St';
 | |
| 
 | |
| import * as Main from 'resource:///org/gnome/shell/ui/main.js';
 | |
| import * as Config from 'resource:///org/gnome/shell/misc/config.js';
 | |
| import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
 | |
| 
 | |
| import {BaseStatusIcon} from './indicatorStatusIcon.js';
 | |
| 
 | |
| export const BUS_ADDRESS_REGEX = /([a-zA-Z0-9._-]+\.[a-zA-Z0-9.-]+)|(:[0-9]+\.[0-9]+)$/;
 | |
| 
 | |
| Gio._promisify(Gio.DBusConnection.prototype, 'call');
 | |
| Gio._promisify(Gio._LocalFilePrototype, 'read');
 | |
| Gio._promisify(Gio.InputStream.prototype, 'read_bytes_async');
 | |
| 
 | |
| export function indicatorId(service, busName, objectPath) {
 | |
|     if (service !== busName && service?.match(BUS_ADDRESS_REGEX))
 | |
|         return service;
 | |
| 
 | |
|     return `${busName}@${objectPath}`;
 | |
| }
 | |
| 
 | |
| export async function getUniqueBusName(bus, name, cancellable) {
 | |
|     if (name[0] === ':')
 | |
|         return name;
 | |
| 
 | |
|     if (!bus)
 | |
|         bus = Gio.DBus.session;
 | |
| 
 | |
|     const variantName = new GLib.Variant('(s)', [name]);
 | |
|     const [unique] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
 | |
|         'GetNameOwner', variantName, new GLib.VariantType('(s)'),
 | |
|         Gio.DBusCallFlags.NONE, -1, cancellable)).deep_unpack();
 | |
| 
 | |
|     return unique;
 | |
| }
 | |
| 
 | |
| export async function getBusNames(bus, cancellable) {
 | |
|     if (!bus)
 | |
|         bus = Gio.DBus.session;
 | |
| 
 | |
|     const [names] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
 | |
|         'ListNames', null, new GLib.VariantType('(as)'), Gio.DBusCallFlags.NONE,
 | |
|         -1, cancellable)).deep_unpack();
 | |
| 
 | |
|     const uniqueNames = new Map();
 | |
|     const requests = names.map(name => getUniqueBusName(bus, name, cancellable));
 | |
|     const results = await Promise.allSettled(requests);
 | |
| 
 | |
|     for (let i = 0; i < results.length; i++) {
 | |
|         const result = results[i];
 | |
|         if (result.status === 'fulfilled') {
 | |
|             let namesForBus = uniqueNames.get(result.value);
 | |
|             if (!namesForBus) {
 | |
|                 namesForBus = new Set();
 | |
|                 uniqueNames.set(result.value, namesForBus);
 | |
|             }
 | |
|             namesForBus.add(result.value !== names[i] ? names[i] : null);
 | |
|         } else if (!result.reason.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
 | |
|             Logger.debug(`Impossible to get the unique name of ${names[i]}: ${result.reason}`);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return uniqueNames;
 | |
| }
 | |
| 
 | |
| async function getProcessId(connectionName, cancellable = null, bus = Gio.DBus.session) {
 | |
|     const res = await bus.call('org.freedesktop.DBus', '/',
 | |
|         'org.freedesktop.DBus', 'GetConnectionUnixProcessID',
 | |
|         new GLib.Variant('(s)', [connectionName]),
 | |
|         new GLib.VariantType('(u)'),
 | |
|         Gio.DBusCallFlags.NONE,
 | |
|         -1,
 | |
|         cancellable);
 | |
|     const [pid] = res.deepUnpack();
 | |
|     return pid;
 | |
| }
 | |
| 
 | |
| export async function getProcessName(connectionName, cancellable = null,
 | |
|     priority = GLib.PRIORITY_DEFAULT, bus = Gio.DBus.session) {
 | |
|     const pid = await getProcessId(connectionName, cancellable, bus);
 | |
|     const cmdFile = Gio.File.new_for_path(`/proc/${pid}/cmdline`);
 | |
|     const inputStream = await cmdFile.read_async(priority, cancellable);
 | |
|     const bytes = await inputStream.read_bytes_async(2048, priority, cancellable);
 | |
|     const textDecoder = new TextDecoder();
 | |
|     return textDecoder.decode(bytes.toArray().map(v => !v ? 0x20 : v));
 | |
| }
 | |
| 
 | |
| export async function* introspectBusObject(bus, name, cancellable,
 | |
|     interfaces = undefined, path = undefined) {
 | |
|     if (!path)
 | |
|         path = '/';
 | |
| 
 | |
|     const [introspection] = (await bus.call(name, path, 'org.freedesktop.DBus.Introspectable',
 | |
|         'Introspect', null, new GLib.VariantType('(s)'), Gio.DBusCallFlags.NONE,
 | |
|         5000, cancellable)).deep_unpack();
 | |
| 
 | |
|     const nodeInfo = Gio.DBusNodeInfo.new_for_xml(introspection);
 | |
| 
 | |
|     if (!interfaces || dbusNodeImplementsInterfaces(nodeInfo, interfaces))
 | |
|         yield {nodeInfo, path};
 | |
| 
 | |
|     if (path === '/')
 | |
|         path = '';
 | |
| 
 | |
|     for (const subNodeInfo of nodeInfo.nodes) {
 | |
|         const subPath = `${path}/${subNodeInfo.path}`;
 | |
|         yield* introspectBusObject(bus, name, cancellable, interfaces, subPath);
 | |
|     }
 | |
| }
 | |
| 
 | |
| function dbusNodeImplementsInterfaces(nodeInfo, interfaces) {
 | |
|     if (!(nodeInfo instanceof Gio.DBusNodeInfo) || !Array.isArray(interfaces))
 | |
|         return false;
 | |
| 
 | |
|     return interfaces.some(iface => nodeInfo.lookup_interface(iface));
 | |
| }
 | |
| 
 | |
| export class NameWatcher extends Signals.EventEmitter {
 | |
|     constructor(name) {
 | |
|         super();
 | |
| 
 | |
|         this._watcherId = Gio.DBus.session.watch_name(name,
 | |
|             Gio.BusNameWatcherFlags.NONE, () => {
 | |
|                 this._nameOnBus = true;
 | |
|                 Logger.debug(`Name ${name} appeared`);
 | |
|                 this.emit('changed');
 | |
|                 this.emit('appeared');
 | |
|             }, () => {
 | |
|                 this._nameOnBus = false;
 | |
|                 Logger.debug(`Name ${name} vanished`);
 | |
|                 this.emit('changed');
 | |
|                 this.emit('vanished');
 | |
|             });
 | |
|     }
 | |
| 
 | |
|     destroy() {
 | |
|         this.emit('destroy');
 | |
| 
 | |
|         Gio.DBus.session.unwatch_name(this._watcherId);
 | |
|         delete this._watcherId;
 | |
|     }
 | |
| 
 | |
|     get nameOnBus() {
 | |
|         return !!this._nameOnBus;
 | |
|     }
 | |
| }
 | |
| 
 | |
| function connectSmart3A(src, signal, handler) {
 | |
|     const id = src.connect(signal, handler);
 | |
|     let destroyId = 0;
 | |
| 
 | |
|     if (src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src))) {
 | |
|         destroyId = src.connect('destroy', () => {
 | |
|             src.disconnect(id);
 | |
|             src.disconnect(destroyId);
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     return [id, destroyId];
 | |
| }
 | |
| 
 | |
| function connectSmart4A(src, signal, target, method) {
 | |
|     if (typeof method !== 'function')
 | |
|         throw new TypeError('Unsupported function');
 | |
| 
 | |
|     method = method.bind(target);
 | |
|     const signalId = src.connect(signal, method);
 | |
|     const onDestroy = () => {
 | |
|         src.disconnect(signalId);
 | |
|         if (srcDestroyId)
 | |
|             src.disconnect(srcDestroyId);
 | |
|         if (tgtDestroyId)
 | |
|             target.disconnect(tgtDestroyId);
 | |
|     };
 | |
| 
 | |
|     // GObject classes might or might not have a destroy signal
 | |
|     // JS Classes will not complain when connecting to non-existent signals
 | |
|     const srcDestroyId = src.connect && (!(src instanceof GObject.Object) ||
 | |
|         GObject.signal_lookup('destroy', src)) ? src.connect('destroy', onDestroy) : 0;
 | |
|     const tgtDestroyId = target.connect && (!(target instanceof GObject.Object) ||
 | |
|         GObject.signal_lookup('destroy', target)) ? target.connect('destroy', onDestroy) : 0;
 | |
| 
 | |
|     return [signalId, srcDestroyId, tgtDestroyId];
 | |
| }
 | |
| 
 | |
| // eslint-disable-next-line valid-jsdoc
 | |
| /**
 | |
|  * Connect signals to slots, and remove the connection when either source or
 | |
|  * target are destroyed
 | |
|  *
 | |
|  * Usage:
 | |
|  *      Util.connectSmart(srcOb, 'signal', tgtObj, 'handler')
 | |
|  * or
 | |
|  *      Util.connectSmart(srcOb, 'signal', () => { ... })
 | |
|  */
 | |
| export function connectSmart(...args) {
 | |
|     if (arguments.length === 4)
 | |
|         return connectSmart4A(...args);
 | |
|     else
 | |
|         return connectSmart3A(...args);
 | |
| }
 | |
| 
 | |
| function disconnectSmart3A(src, signalIds) {
 | |
|     const [id, destroyId] = signalIds;
 | |
|     src.disconnect(id);
 | |
| 
 | |
|     if (destroyId)
 | |
|         src.disconnect(destroyId);
 | |
| }
 | |
| 
 | |
| function disconnectSmart4A(src, tgt, signalIds) {
 | |
|     const [signalId, srcDestroyId, tgtDestroyId] = signalIds;
 | |
| 
 | |
|     disconnectSmart3A(src, [signalId, srcDestroyId]);
 | |
| 
 | |
|     if (tgtDestroyId)
 | |
|         tgt.disconnect(tgtDestroyId);
 | |
| }
 | |
| 
 | |
| export function disconnectSmart(...args) {
 | |
|     if (arguments.length === 2)
 | |
|         return disconnectSmart3A(...args);
 | |
|     else if (arguments.length === 3)
 | |
|         return disconnectSmart4A(...args);
 | |
| 
 | |
|     throw new TypeError('Unexpected number of arguments');
 | |
| }
 | |
| 
 | |
| let _defaultTheme;
 | |
| export function getDefaultTheme() {
 | |
|     if (_defaultTheme)
 | |
|         return _defaultTheme;
 | |
| 
 | |
|     _defaultTheme = new St.IconTheme();
 | |
|     return _defaultTheme;
 | |
| }
 | |
| 
 | |
| export function destroyDefaultTheme() {
 | |
|     _defaultTheme = null;
 | |
| }
 | |
| 
 | |
| // eslint-disable-next-line valid-jsdoc
 | |
| /**
 | |
|  * Helper function to wait for the system startup to be completed.
 | |
|  * Adding widgets before the desktop is ready to accept them can result in errors.
 | |
|  */
 | |
| export async function waitForStartupCompletion(cancellable) {
 | |
|     if (Main.layoutManager._startingUp)
 | |
|         await Main.layoutManager.connect_once('startup-complete', cancellable);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Helper class for logging stuff
 | |
|  */
 | |
| export class Logger {
 | |
|     static _logStructured(logLevel, message, extraFields = {}) {
 | |
|         if (!Object.values(GLib.LogLevelFlags).includes(logLevel)) {
 | |
|             Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING,
 | |
|                 'logLevel is not a valid GLib.LogLevelFlags');
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if (!Logger._levels.includes(logLevel))
 | |
|             return;
 | |
| 
 | |
|         let fields = {
 | |
|             'SYSLOG_IDENTIFIER': this.uuid,
 | |
|             'MESSAGE': `${message}`,
 | |
|         };
 | |
| 
 | |
|         let thisFile = null;
 | |
|         const {stack} = new Error();
 | |
|         for (let stackLine of stack.split('\n')) {
 | |
|             stackLine = stackLine.replace('resource:///org/gnome/Shell/', '');
 | |
|             const [code, line] = stackLine.split(':');
 | |
|             const [func, file] = code.split(/@(.+)/);
 | |
| 
 | |
|             if (!thisFile || thisFile === file) {
 | |
|                 thisFile = file;
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             fields = Object.assign(fields, {
 | |
|                 'CODE_FILE': file || '',
 | |
|                 'CODE_LINE': line || '',
 | |
|                 'CODE_FUNC': func || '',
 | |
|             });
 | |
| 
 | |
|             break;
 | |
|         }
 | |
| 
 | |
|         GLib.log_structured(Logger._domain, logLevel, Object.assign(fields, extraFields));
 | |
|     }
 | |
| 
 | |
|     static init(extension) {
 | |
|         if (Logger._domain)
 | |
|             return;
 | |
| 
 | |
|         const allLevels = Object.values(GLib.LogLevelFlags);
 | |
|         const domains = GLib.getenv('G_MESSAGES_DEBUG');
 | |
|         const {name: domain} = extension.metadata;
 | |
|         this.uuid = extension.metadata.uuid;
 | |
|         Logger._domain = domain.replaceAll(' ', '-');
 | |
| 
 | |
|         if (domains === 'all' || (domains && domains.split(' ').includes(Logger._domain))) {
 | |
|             Logger._levels = allLevels;
 | |
|         } else {
 | |
|             Logger._levels = allLevels.filter(
 | |
|                 l => l <= GLib.LogLevelFlags.LEVEL_WARNING);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     static debug(message) {
 | |
|         Logger._logStructured(GLib.LogLevelFlags.LEVEL_DEBUG, message);
 | |
|     }
 | |
| 
 | |
|     static message(message) {
 | |
|         Logger._logStructured(GLib.LogLevelFlags.LEVEL_MESSAGE, message);
 | |
|     }
 | |
| 
 | |
|     static warn(message) {
 | |
|         Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING, message);
 | |
|     }
 | |
| 
 | |
|     static error(message) {
 | |
|         Logger._logStructured(GLib.LogLevelFlags.LEVEL_ERROR, message);
 | |
|     }
 | |
| 
 | |
|     static critical(message) {
 | |
|         Logger._logStructured(GLib.LogLevelFlags.LEVEL_CRITICAL, message);
 | |
|     }
 | |
| }
 | |
| 
 | |
| export function versionCheck(required) {
 | |
|     const current = Config.PACKAGE_VERSION;
 | |
|     const currentArray = current.split('.');
 | |
|     const [major] = currentArray;
 | |
|     return major >= required;
 | |
| }
 | |
| 
 | |
| export function tryCleanupOldIndicators() {
 | |
|     const indicatorType = BaseStatusIcon;
 | |
|     const indicators = Object.values(Main.panel.statusArea).filter(i => i instanceof indicatorType);
 | |
| 
 | |
|     try {
 | |
|         const panelBoxes = [
 | |
|             Main.panel._leftBox, Main.panel._centerBox, Main.panel._rightBox,
 | |
|         ];
 | |
| 
 | |
|         panelBoxes.forEach(box =>
 | |
|             indicators.push(...box.get_children().filter(i => i instanceof indicatorType)));
 | |
|     } catch (e) {
 | |
|         logError(e);
 | |
|     }
 | |
| 
 | |
|     new Set(indicators).forEach(i => i.destroy());
 | |
| }
 | |
| 
 | |
| export function addActor(obj, actor) {
 | |
|     if (obj.add_actor)
 | |
|         obj.add_actor(actor);
 | |
|     else
 | |
|         obj.add_child(actor);
 | |
| }
 | |
| 
 | |
| export function removeActor(obj, actor) {
 | |
|     if (obj.remove_actor)
 | |
|         obj.remove_actor(actor);
 | |
|     else
 | |
|         obj.remove_child(actor);
 | |
| }
 | |
| 
 | |
| export const CancellableChild = GObject.registerClass({
 | |
|     Properties: {
 | |
|         'parent': GObject.ParamSpec.object(
 | |
|             'parent', 'parent', 'parent',
 | |
|             GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
 | |
|             Gio.Cancellable.$gtype),
 | |
|     },
 | |
| },
 | |
| class CancellableChild extends Gio.Cancellable {
 | |
|     _init(parent) {
 | |
|         if (parent && !(parent instanceof Gio.Cancellable))
 | |
|             throw TypeError('Not a valid cancellable');
 | |
| 
 | |
|         super._init({parent});
 | |
| 
 | |
|         if (parent) {
 | |
|             if (parent.is_cancelled()) {
 | |
|                 this.cancel();
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             this._connectToParent();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     _connectToParent() {
 | |
|         this._connectId = this.parent.connect(() => {
 | |
|             this._realCancel();
 | |
| 
 | |
|             if (this._disconnectIdle)
 | |
|                 return;
 | |
| 
 | |
|             this._disconnectIdle = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
 | |
|                 delete this._disconnectIdle;
 | |
|                 this._disconnectFromParent();
 | |
|                 return GLib.SOURCE_REMOVE;
 | |
|             });
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     _disconnectFromParent() {
 | |
|         if (this._connectId && !this._disconnectIdle) {
 | |
|             this.parent.disconnect(this._connectId);
 | |
|             delete this._connectId;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     _realCancel() {
 | |
|         Gio.Cancellable.prototype.cancel.call(this);
 | |
|     }
 | |
| 
 | |
|     cancel() {
 | |
|         this._disconnectFromParent();
 | |
|         this._realCancel();
 | |
|     }
 | |
| });
 |