dot/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/utils/remote.js

540 lines
14 KiB
JavaScript

'use strict';
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const DBUS_NAME = 'org.gnome.Shell.Extensions.GSConnect';
const DBUS_PATH = '/org/gnome/Shell/Extensions/GSConnect';
function toHyphenCase(string) {
if (toHyphenCase.__cache === undefined) {
toHyphenCase.__cache = {};
}
if (!toHyphenCase.__cache[string]) {
toHyphenCase.__cache[string] = string.replace(/(?:[A-Z])/g, (c, i) => {
return (i > 0) ? '-' + c.toLowerCase() : c.toLowerCase();
}).replace(/[\s_]+/g, '');
}
return toHyphenCase.__cache[string];
}
function _proxyInit(proxy, cancellable = null) {
if (proxy.__initialized !== undefined) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
proxy.init_async(
GLib.PRIORITY_DEFAULT,
cancellable,
(proxy, res) => {
try {
proxy.init_finish(res);
proxy.__initialized = true;
resolve();
} catch (e) {
Gio.DBusError.strip_remote_error(e);
reject(e);
}
}
);
});
}
var Device = GObject.registerClass({
GTypeName: 'GSConnectRemoteDevice',
Implements: [Gio.DBusInterface],
Properties: {
'connected': GObject.ParamSpec.boolean(
'connected',
'Connected',
'Whether the device is connected',
GObject.ParamFlags.READABLE,
null
),
'encryption-info': GObject.ParamSpec.string(
'encryption-info',
'Encryption Info',
'A formatted string with the local and remote fingerprints',
GObject.ParamFlags.READABLE,
null
),
'icon-name': GObject.ParamSpec.string(
'icon-name',
'Icon Name',
'Icon name representing the device',
GObject.ParamFlags.READABLE,
null
),
'id': GObject.ParamSpec.string(
'id',
'deviceId',
'The device hostname or other unique id',
GObject.ParamFlags.READABLE,
''
),
'name': GObject.ParamSpec.string(
'name',
'deviceName',
'The device name',
GObject.ParamFlags.READABLE,
null
),
'paired': GObject.ParamSpec.boolean(
'paired',
'Paired',
'Whether the device is paired',
GObject.ParamFlags.READABLE,
null
),
'type': GObject.ParamSpec.string(
'type',
'deviceType',
'The device type',
GObject.ParamFlags.READABLE,
null
)
}
}, class Device extends Gio.DBusProxy {
_init(service, object_path) {
this._service = service;
super._init({
g_connection: service.g_connection,
g_name: DBUS_NAME,
g_object_path: object_path,
g_interface_name: 'org.gnome.Shell.Extensions.GSConnect.Device'
});
}
// Proxy GObject::notify signals
vfunc_g_properties_changed(changed, invalidated) {
try {
if (this.__disposed !== undefined)
return;
for (let name in changed.deepUnpack()) {
this.notify(toHyphenCase(name));
}
} catch (e) {
logError(e);
}
}
_get(name, fallback = null) {
try {
return this.get_cached_property(name).unpack();
} catch (e) {
return fallback;
}
}
get connected() {
return this._get('Connected', false);
}
get encryption_info() {
return this._get('EncryptionInfo', '');
}
get icon_name() {
return this._get('IconName', 'computer');
}
get id() {
return this._get('Id', '0');
}
get name() {
return this._get('Name', 'Unknown');
}
get paired() {
return this._get('Paired', false);
}
get settings() {
if (this._settings === undefined) {
this._settings = new Gio.Settings({
settings_schema: gsconnect.gschema.lookup(
this.g_interface_name,
true
),
path: `${this.g_object_path.toLowerCase()}/`
});
}
return this._settings;
}
get service() {
return this._service;
}
get type() {
return this._get('Type', 'desktop');
}
async start() {
try {
// Initialize the proxy
await _proxyInit(this);
// GActions
this.action_group = Gio.DBusActionGroup.get(
this.g_connection,
this.service.g_name_owner,
this.g_object_path
);
// GMenu
this.menu = Gio.DBusMenuModel.get(
this.g_connection,
this.service.g_name_owner,
this.g_object_path
);
// Subscribe to the GMenu
await new Promise((resolve, reject) => {
this.g_connection.call(
DBUS_NAME,
this.g_object_path,
'org.gtk.Menus',
'Start',
new GLib.Variant('(au)', [[0]]),
null,
Gio.DBusCallFlags.NONE,
-1,
null,
(proxy, res) => {
try {
resolve(proxy.call_finish(res));
} catch (e) {
Gio.DBusError.strip_remote_error(e);
reject(e);
}
}
);
});
} catch (e) {
this.destroy();
throw e;
}
}
destroy() {
if (this.__disposed === undefined) {
this.__disposed = true;
if (this._settings) {
this._settings.run_dispose();
this._settings = null;
}
this.run_dispose();
}
}
});
var Service = GObject.registerClass({
GTypeName: 'GSConnectRemoteService',
Implements: [Gio.DBusInterface],
Properties: {
'active': GObject.ParamSpec.boolean(
'active',
'Active',
'Whether the service is active',
GObject.ParamFlags.READABLE,
false
)
},
Signals: {
'device-added': {
flags: GObject.SignalFlags.RUN_FIRST,
param_types: [GObject.TYPE_OBJECT]
},
'device-removed': {
flags: GObject.SignalFlags.RUN_FIRST,
param_types: [GObject.TYPE_OBJECT]
}
}
}, class Service extends Gio.DBusProxy {
_init() {
super._init({
g_bus_type: Gio.BusType.SESSION,
g_name: DBUS_NAME,
g_object_path: DBUS_PATH,
g_interface_name: 'org.freedesktop.DBus.ObjectManager',
g_flags: Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION
});
this._active = false;
this._devices = new Map();
this._starting = false;
// Watch the service
this._nameOwnerChangedId = this.connect(
'notify::g-name-owner',
this._onNameOwnerChanged.bind(this)
);
}
get active() {
return this._active;
}
get devices() {
return Array.from(this._devices.values());
}
vfunc_g_signal(sender_name, signal_name, parameters) {
try {
// Don't emit signals until the ObjectManager has started
if (!this.active) return;
parameters = parameters.deepUnpack();
switch (true) {
case (signal_name === 'InterfacesAdded'):
this._onInterfacesAdded(...parameters);
break;
case (signal_name === 'InterfacesRemoved'):
this._onInterfacesRemoved(...parameters);
break;
}
} catch (e) {
logError(e);
}
}
/**
* org.freedesktop.DBus.ObjectManager.InterfacesAdded
*
* @param {string} object_path - Path interfaces have been added to
* @param {object[]} - list of interface objects
*/
async _onInterfacesAdded(object_path, interfaces) {
try {
// An empty list means only the object has been added
if (Object.values(interfaces).length === 0) return;
// Skip existing proxies
if (this._devices.has(object_path)) return;
// Create a proxy
let device = new Device(this, object_path);
await device.start();
// Hold the proxy and emit ::device-added
this._devices.set(object_path, device);
this.emit('device-added', device);
} catch (e) {
logError(e, object_path);
}
}
/**
* org.freedesktop.DBus.ObjectManager.InterfacesRemoved
*
* @param {string} object_path - Path interfaces have been removed from
* @param {string[]} - List of interface names removed
*/
_onInterfacesRemoved(object_path, interfaces) {
try {
// An empty interface list means the object is being removed
if (interfaces.length === 0) return;
// Get the proxy
let device = this._devices.get(object_path);
if (device === undefined) return;
// Release the proxy and emit ::device-removed
this._devices.delete(object_path);
this.emit('device-removed', device);
// Destroy the device and force disposal
device.destroy();
} catch (e) {
logError(e, object_path);
}
}
async _addDevices() {
let objects = await new Promise((resolve, reject) => {
this.call(
'GetManagedObjects',
null,
Gio.DBusCallFlags.NONE,
-1,
null,
(proxy, res) => {
try {
let variant = proxy.call_finish(res);
resolve(variant.deepUnpack()[0]);
} catch (e) {
Gio.DBusError.strip_remote_error(e);
reject(e);
}
}
);
});
for (let [object_path, object] of Object.entries(objects)) {
await this._onInterfacesAdded(object_path, object);
}
}
_clearDevices() {
for (let [object_path, device] of this._devices) {
this._devices.delete(object_path);
this.emit('device-removed', device);
device.destroy();
}
}
async _onNameOwnerChanged() {
try {
// If the service stopped, remove each device and mark it inactive
if (this.g_name_owner === null) {
this._clearDevices();
this._active = false;
this.notify('active');
// If the service started, mark it active and add each device
} else {
this._active = true;
this.notify('active');
await this._addDevices();
}
} catch (e) {
logError(e);
}
}
/**
* Reload all devices without affecting the remote service. This amounts to
* removing and adding each device while emitting the appropriate signals.
*/
async reload() {
try {
if (this._starting === false) {
this._starting = true;
this._clearDevices();
await _proxyInit(this);
await this._onNameOwnerChanged();
this._starting = false;
}
} catch (e) {
this._starting = false;
throw e;
}
}
/**
* Start the service
*/
async start() {
try {
if (this._starting === false && this.active === false) {
this._starting = true;
await _proxyInit(this);
await this._onNameOwnerChanged();
// Activate the service if it's not already running
if (!this.active) {
await new Promise((resolve, reject) => {
this.g_connection.call(
DBUS_NAME,
DBUS_PATH,
'org.freedesktop.Application',
'Activate',
GLib.Variant.new('(a{sv})', [{}]),
null,
Gio.DBusCallFlags.NONE,
-1,
null,
(proxy, res) => {
try {
resolve(proxy.call_finish(res));
} catch (e) {
Gio.DBusError.strip_remote_error(e);
reject(e);
}
}
);
});
}
this._starting = false;
}
} catch (e) {
this._starting = false;
throw e;
}
}
/**
* Stop the service
*/
stop() {
if (this.active) {
this.activate_action('quit');
}
}
activate_action(name, parameter = null) {
try {
let paramArray = [];
if (parameter instanceof GLib.Variant) {
paramArray[0] = parameter;
}
let connection = this.g_connection || Gio.DBus.session;
connection.call(
DBUS_NAME,
DBUS_PATH,
'org.freedesktop.Application',
'ActivateAction',
GLib.Variant.new('(sava{sv})', [name, paramArray, {}]),
null,
Gio.DBusCallFlags.NONE,
-1,
null,
null
);
} catch (e) {
logError(e);
}
}
destroy() {
if (this._nameOwnerChangedId > 0) {
this.disconnect(this._nameOwnerChangedId);
this._nameOwnerChangedId = 0;
this._clearDevices();
this._active = false;
this.run_dispose();
}
}
});