255 lines
7.3 KiB
JavaScript
Raw Normal View History

2024-07-08 22:46:35 +02:00
// 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';
import Plugin from '../plugin.js';
export const Metadata = {
label: _('Run Commands'),
id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.RunCommand',
description: _('Run commands on your paired device or let the device run predefined commands on this PC'),
incomingCapabilities: [
'kdeconnect.runcommand',
'kdeconnect.runcommand.request',
],
outgoingCapabilities: [
'kdeconnect.runcommand',
'kdeconnect.runcommand.request',
],
actions: {
commands: {
label: _('Commands'),
icon_name: 'system-run-symbolic',
parameter_type: new GLib.VariantType('s'),
incoming: ['kdeconnect.runcommand'],
outgoing: ['kdeconnect.runcommand.request'],
},
executeCommand: {
label: _('Commands'),
icon_name: 'system-run-symbolic',
parameter_type: new GLib.VariantType('s'),
incoming: ['kdeconnect.runcommand'],
outgoing: ['kdeconnect.runcommand.request'],
},
},
};
/**
* RunCommand Plugin
* https://github.com/KDE/kdeconnect-kde/tree/master/plugins/remotecommands
* https://github.com/KDE/kdeconnect-kde/tree/master/plugins/runcommand
*/
const RunCommandPlugin = GObject.registerClass({
GTypeName: 'GSConnectRunCommandPlugin',
Properties: {
'remote-commands': GObject.param_spec_variant(
'remote-commands',
'Remote Command List',
'A list of the device\'s remote commands',
new GLib.VariantType('a{sv}'),
null,
GObject.ParamFlags.READABLE
),
},
}, class RunCommandPlugin extends Plugin {
_init(device) {
super._init(device, 'runcommand');
// Local Commands
this._commandListChangedId = this.settings.connect(
'changed::command-list',
this._sendCommandList.bind(this)
);
// We cache remote commands so they can be used in the settings even
// when the device is offline.
this._remote_commands = {};
this.cacheProperties(['_remote_commands']);
}
get remote_commands() {
return this._remote_commands;
}
connected() {
super.connected();
this._sendCommandList();
this._requestCommandList();
this._handleCommandList(this.remote_commands);
}
clearCache() {
this._remote_commands = {};
this.notify('remote-commands');
}
cacheLoaded() {
if (!this.device.connected)
return;
this._sendCommandList();
this._requestCommandList();
this._handleCommandList(this.remote_commands);
}
handlePacket(packet) {
switch (packet.type) {
case 'kdeconnect.runcommand':
this._handleCommandList(packet.body.commandList);
break;
case 'kdeconnect.runcommand.request':
if (packet.body.hasOwnProperty('key'))
this._handleCommand(packet.body.key);
else if (packet.body.hasOwnProperty('requestCommandList'))
this._sendCommandList();
break;
}
}
/**
* Handle a request to execute the local command with the UUID @key
*
* @param {string} key - The UUID of the local command
*/
_handleCommand(key) {
try {
const commands = this.settings.get_value('command-list');
const commandList = commands.recursiveUnpack();
if (!commandList.hasOwnProperty(key)) {
throw new Gio.IOErrorEnum({
code: Gio.IOErrorEnum.PERMISSION_DENIED,
message: `Unknown command: ${key}`,
});
}
this.device.launchProcess([
'/bin/sh',
'-c',
commandList[key].command,
]);
} catch (e) {
logError(e, this.device.name);
}
}
/**
* Parse the response to a request for the remote command list. Remove the
* command menu if there are no commands, otherwise amend the menu.
*
* @param {string|Object[]} commandList - A list of remote commands
*/
_handleCommandList(commandList) {
// See: https://github.com/GSConnect/gnome-shell-extension-gsconnect/issues/1051
if (typeof commandList === 'string') {
try {
commandList = JSON.parse(commandList);
} catch (e) {
commandList = {};
}
}
this._remote_commands = commandList;
this.notify('remote-commands');
const commandEntries = Object.entries(this.remote_commands);
// If there are no commands, hide the menu by disabling the action
this.device.lookup_action('commands').enabled = (commandEntries.length > 0);
// Commands Submenu
const submenu = new Gio.Menu();
for (const [uuid, info] of commandEntries) {
const item = new Gio.MenuItem();
item.set_label(info.name);
item.set_icon(
new Gio.ThemedIcon({name: 'application-x-executable-symbolic'})
);
item.set_detailed_action(`device.executeCommand::${uuid}`);
submenu.append_item(item);
}
// Commands Item
const item = new Gio.MenuItem();
item.set_detailed_action('device.commands::menu');
item.set_attribute_value(
'hidden-when',
new GLib.Variant('s', 'action-disabled')
);
item.set_icon(new Gio.ThemedIcon({name: 'system-run-symbolic'}));
item.set_label(_('Commands'));
item.set_submenu(submenu);
// If the submenu item is already present it will be replaced
const menuActions = this.device.settings.get_strv('menu-actions');
const index = menuActions.indexOf('commands');
if (index > -1) {
this.device.removeMenuAction('device.commands');
this.device.addMenuItem(item, index);
}
}
/**
* Send a request for the remote command list
*/
_requestCommandList() {
this.device.sendPacket({
type: 'kdeconnect.runcommand.request',
body: {requestCommandList: true},
});
}
/**
* Send the local command list
*/
_sendCommandList() {
const commands = this.settings.get_value('command-list').recursiveUnpack();
const commandList = JSON.stringify(commands);
this.device.sendPacket({
type: 'kdeconnect.runcommand',
body: {commandList: commandList},
});
}
/**
* Placeholder function for command action
*/
commands() {}
/**
* Send a request to execute the remote command with the UUID @key
*
* @param {string} key - The UUID of the remote command
*/
executeCommand(key) {
this.device.sendPacket({
type: 'kdeconnect.runcommand.request',
body: {key: key},
});
}
destroy() {
this.settings.disconnect(this._commandListChangedId);
super.destroy();
}
});
export default RunCommandPlugin;