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