runcommand.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. import Gio from 'gi://Gio';
  5. import GLib from 'gi://GLib';
  6. import GObject from 'gi://GObject';
  7. import Plugin from '../plugin.js';
  8. export const Metadata = {
  9. label: _('Run Commands'),
  10. id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.RunCommand',
  11. description: _('Run commands on your paired device or let the device run predefined commands on this PC'),
  12. incomingCapabilities: [
  13. 'kdeconnect.runcommand',
  14. 'kdeconnect.runcommand.request',
  15. ],
  16. outgoingCapabilities: [
  17. 'kdeconnect.runcommand',
  18. 'kdeconnect.runcommand.request',
  19. ],
  20. actions: {
  21. commands: {
  22. label: _('Commands'),
  23. icon_name: 'system-run-symbolic',
  24. parameter_type: new GLib.VariantType('s'),
  25. incoming: ['kdeconnect.runcommand'],
  26. outgoing: ['kdeconnect.runcommand.request'],
  27. },
  28. executeCommand: {
  29. label: _('Commands'),
  30. icon_name: 'system-run-symbolic',
  31. parameter_type: new GLib.VariantType('s'),
  32. incoming: ['kdeconnect.runcommand'],
  33. outgoing: ['kdeconnect.runcommand.request'],
  34. },
  35. },
  36. };
  37. /**
  38. * RunCommand Plugin
  39. * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/remotecommands
  40. * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/runcommand
  41. */
  42. const RunCommandPlugin = GObject.registerClass({
  43. GTypeName: 'GSConnectRunCommandPlugin',
  44. Properties: {
  45. 'remote-commands': GObject.param_spec_variant(
  46. 'remote-commands',
  47. 'Remote Command List',
  48. 'A list of the device\'s remote commands',
  49. new GLib.VariantType('a{sv}'),
  50. null,
  51. GObject.ParamFlags.READABLE
  52. ),
  53. },
  54. }, class RunCommandPlugin extends Plugin {
  55. _init(device) {
  56. super._init(device, 'runcommand');
  57. // Local Commands
  58. this._commandListChangedId = this.settings.connect(
  59. 'changed::command-list',
  60. this._sendCommandList.bind(this)
  61. );
  62. // We cache remote commands so they can be used in the settings even
  63. // when the device is offline.
  64. this._remote_commands = {};
  65. this.cacheProperties(['_remote_commands']);
  66. }
  67. get remote_commands() {
  68. return this._remote_commands;
  69. }
  70. connected() {
  71. super.connected();
  72. this._sendCommandList();
  73. this._requestCommandList();
  74. this._handleCommandList(this.remote_commands);
  75. }
  76. clearCache() {
  77. this._remote_commands = {};
  78. this.notify('remote-commands');
  79. }
  80. cacheLoaded() {
  81. if (!this.device.connected)
  82. return;
  83. this._sendCommandList();
  84. this._requestCommandList();
  85. this._handleCommandList(this.remote_commands);
  86. }
  87. handlePacket(packet) {
  88. switch (packet.type) {
  89. case 'kdeconnect.runcommand':
  90. this._handleCommandList(packet.body.commandList);
  91. break;
  92. case 'kdeconnect.runcommand.request':
  93. if (packet.body.hasOwnProperty('key'))
  94. this._handleCommand(packet.body.key);
  95. else if (packet.body.hasOwnProperty('requestCommandList'))
  96. this._sendCommandList();
  97. break;
  98. }
  99. }
  100. /**
  101. * Handle a request to execute the local command with the UUID @key
  102. *
  103. * @param {string} key - The UUID of the local command
  104. */
  105. _handleCommand(key) {
  106. try {
  107. const commands = this.settings.get_value('command-list');
  108. const commandList = commands.recursiveUnpack();
  109. if (!commandList.hasOwnProperty(key)) {
  110. throw new Gio.IOErrorEnum({
  111. code: Gio.IOErrorEnum.PERMISSION_DENIED,
  112. message: `Unknown command: ${key}`,
  113. });
  114. }
  115. this.device.launchProcess([
  116. '/bin/sh',
  117. '-c',
  118. commandList[key].command,
  119. ]);
  120. } catch (e) {
  121. logError(e, this.device.name);
  122. }
  123. }
  124. /**
  125. * Parse the response to a request for the remote command list. Remove the
  126. * command menu if there are no commands, otherwise amend the menu.
  127. *
  128. * @param {string|Object[]} commandList - A list of remote commands
  129. */
  130. _handleCommandList(commandList) {
  131. // See: https://github.com/GSConnect/gnome-shell-extension-gsconnect/issues/1051
  132. if (typeof commandList === 'string') {
  133. try {
  134. commandList = JSON.parse(commandList);
  135. } catch (e) {
  136. commandList = {};
  137. }
  138. }
  139. this._remote_commands = commandList;
  140. this.notify('remote-commands');
  141. const commandEntries = Object.entries(this.remote_commands);
  142. // If there are no commands, hide the menu by disabling the action
  143. this.device.lookup_action('commands').enabled = (commandEntries.length > 0);
  144. // Commands Submenu
  145. const submenu = new Gio.Menu();
  146. for (const [uuid, info] of commandEntries) {
  147. const item = new Gio.MenuItem();
  148. item.set_label(info.name);
  149. item.set_icon(
  150. new Gio.ThemedIcon({name: 'application-x-executable-symbolic'})
  151. );
  152. item.set_detailed_action(`device.executeCommand::${uuid}`);
  153. submenu.append_item(item);
  154. }
  155. // Commands Item
  156. const item = new Gio.MenuItem();
  157. item.set_detailed_action('device.commands::menu');
  158. item.set_attribute_value(
  159. 'hidden-when',
  160. new GLib.Variant('s', 'action-disabled')
  161. );
  162. item.set_icon(new Gio.ThemedIcon({name: 'system-run-symbolic'}));
  163. item.set_label(_('Commands'));
  164. item.set_submenu(submenu);
  165. // If the submenu item is already present it will be replaced
  166. const menuActions = this.device.settings.get_strv('menu-actions');
  167. const index = menuActions.indexOf('commands');
  168. if (index > -1) {
  169. this.device.removeMenuAction('device.commands');
  170. this.device.addMenuItem(item, index);
  171. }
  172. }
  173. /**
  174. * Send a request for the remote command list
  175. */
  176. _requestCommandList() {
  177. this.device.sendPacket({
  178. type: 'kdeconnect.runcommand.request',
  179. body: {requestCommandList: true},
  180. });
  181. }
  182. /**
  183. * Send the local command list
  184. */
  185. _sendCommandList() {
  186. const commands = this.settings.get_value('command-list').recursiveUnpack();
  187. const commandList = JSON.stringify(commands);
  188. this.device.sendPacket({
  189. type: 'kdeconnect.runcommand',
  190. body: {commandList: commandList},
  191. });
  192. }
  193. /**
  194. * Placeholder function for command action
  195. */
  196. commands() {}
  197. /**
  198. * Send a request to execute the remote command with the UUID @key
  199. *
  200. * @param {string} key - The UUID of the remote command
  201. */
  202. executeCommand(key) {
  203. this.device.sendPacket({
  204. type: 'kdeconnect.runcommand.request',
  205. body: {key: key},
  206. });
  207. }
  208. destroy() {
  209. this.settings.disconnect(this._commandListChangedId);
  210. super.destroy();
  211. }
  212. });
  213. export default RunCommandPlugin;