linux-presets/gui/gnome/autocustom-gnome-macos/res/extensions/gsconnect@andyholmes.github.io/utils/remote.js

517 lines
14 KiB
JavaScript
Executable File

// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
//
// SPDX-License-Identifier: GPL-2.0-or-later
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
const SERVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect';
const SERVICE_PATH = '/org/gnome/Shell/Extensions/GSConnect';
const DEVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect.Device';
const _PROPERTIES = {
'Connected': 'connected',
'EncryptionInfo': 'encryption-info',
'IconName': 'icon-name',
'Id': 'id',
'Name': 'name',
'Paired': 'paired',
'Type': 'type',
};
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);
}
}
);
});
}
/**
* A simple proxy wrapper for devices exported over DBus.
*/
export const 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: SERVICE_NAME,
g_object_path: object_path,
g_interface_name: DEVICE_NAME,
});
}
vfunc_g_properties_changed(changed, invalidated) {
try {
for (const name in changed.deepUnpack())
this.notify(_PROPERTIES[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 service() {
return this._service;
}
get type() {
return this._get('Type', 'desktop');
}
async start() {
try {
await _proxyInit(this);
// For GActions & GMenu we pass the service's name owner to avoid
// any mixup with instances.
this.action_group = Gio.DBusActionGroup.get(
this.g_connection,
this.service.g_name_owner,
this.g_object_path
);
this.menu = Gio.DBusMenuModel.get(
this.g_connection,
this.service.g_name_owner,
this.g_object_path
);
// Poke the GMenu to ensure it's ready for us
await new Promise((resolve, reject) => {
this.g_connection.call(
SERVICE_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() {
GObject.signal_handlers_destroy(this);
}
});
/**
* A simple proxy wrapper for the GSConnect service.
*/
export const 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: [Device.$gtype],
},
'device-removed': {
flags: GObject.SignalFlags.RUN_FIRST,
param_types: [Device.$gtype],
},
},
}, class Service extends Gio.DBusProxy {
_init() {
super._init({
g_bus_type: Gio.BusType.SESSION,
g_name: SERVICE_NAME,
g_object_path: SERVICE_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} interfaces - A dictionary 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
const 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[]} interfaces - 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
const 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() {
const objects = await new Promise((resolve, reject) => {
this.call(
'GetManagedObjects',
null,
Gio.DBusCallFlags.NONE,
-1,
null,
(proxy, res) => {
try {
const variant = proxy.call_finish(res);
resolve(variant.deepUnpack()[0]);
} catch (e) {
Gio.DBusError.strip_remote_error(e);
reject(e);
}
}
);
});
for (const [object_path, object] of Object.entries(objects))
await this._onInterfacesAdded(object_path, object);
}
_clearDevices() {
for (const [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(
SERVICE_NAME,
SERVICE_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 {
const paramArray = [];
if (parameter instanceof GLib.Variant)
paramArray[0] = parameter;
const connection = this.g_connection || Gio.DBus.session;
connection.call(
SERVICE_NAME,
SERVICE_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;
GObject.signal_handlers_destroy(this);
}
}
});