.dotfiles/.local/share/gnome-shell/extensions/appindicatorsupport@rgcjona.../util.js

435 lines
14 KiB
JavaScript

// 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';
const ByteArray = imports.byteArray;
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);
return ByteArray.toString(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 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();
}
});