From c9bb24a82000488505df78912e77d728cb2380f4 Mon Sep 17 00:00:00 2001
From: Fabio Manganiello <blacklight86@gmail.com>
Date: Fri, 19 Jun 2020 00:26:39 +0200
Subject: [PATCH] Support for custom scripts

---
 package-lock.json   | 101 +++++++++++++++++++++++++++++++++++++++++++-
 package.json        |   2 +
 src/options/Run.vue |  62 ++++++++++++++++-----------
 src/utils.js        |  18 ++++++++
 4 files changed, 156 insertions(+), 27 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index e73265d..6fea28c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2333,6 +2333,17 @@
       "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
       "dev": true
     },
+    "clipboard": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz",
+      "integrity": "sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==",
+      "optional": true,
+      "requires": {
+        "good-listener": "^1.2.2",
+        "select": "^1.1.2",
+        "tiny-emitter": "^2.0.0"
+      }
+    },
     "cliui": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
@@ -2450,6 +2461,16 @@
       "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
       "dev": true
     },
+    "component-props": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/component-props/-/component-props-1.1.1.tgz",
+      "integrity": "sha1-+bffm5kntubZfJvScqqGdnDzSUQ="
+    },
+    "component-xor": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/component-xor/-/component-xor-0.0.4.tgz",
+      "integrity": "sha1-xV2DzMG5TNUImk6T+niRxyY+Wao="
+    },
     "compress-commons": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz",
@@ -2952,6 +2973,12 @@
       "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
       "dev": true
     },
+    "delegate": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
+      "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
+      "optional": true
+    },
     "delegates": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -3011,6 +3038,15 @@
         "esutils": "^2.0.2"
       }
     },
+    "dom-iterator": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/dom-iterator/-/dom-iterator-1.0.0.tgz",
+      "integrity": "sha512-7dsMOQI07EMU98gQM8NSB3GsAiIeBYIPKpnxR3c9xOvdvBjChAcOM0iJ222I3p5xyiZO9e5oggkNaCusuTdYig==",
+      "requires": {
+        "component-props": "1.1.1",
+        "component-xor": "0.0.4"
+      }
+    },
     "domain-browser": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
@@ -3198,6 +3234,11 @@
         "is-symbol": "^1.0.2"
       }
     },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+    },
     "escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -4330,6 +4371,15 @@
         "minimatch": "~3.0.2"
       }
     },
+    "good-listener": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
+      "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
+      "optional": true,
+      "requires": {
+        "delegate": "^3.1.2"
+      }
+    },
     "graceful-fs": {
       "version": "4.2.4",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
@@ -4987,8 +5037,7 @@
     "is-extendable": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
-      "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
-      "dev": true
+      "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik="
     },
     "is-extglob": {
       "version": "2.1.1",
@@ -6833,6 +6882,14 @@
         }
       }
     },
+    "prismjs": {
+      "version": "1.20.0",
+      "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.20.0.tgz",
+      "integrity": "sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ==",
+      "requires": {
+        "clipboard": "^2.0.0"
+      }
+    },
     "private": {
       "version": "0.1.8",
       "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
@@ -7422,6 +7479,12 @@
         }
       }
     },
+    "select": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
+      "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=",
+      "optional": true
+    },
     "semver": {
       "version": "5.7.1",
       "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -8226,6 +8289,12 @@
         "setimmediate": "^1.0.4"
       }
     },
+    "tiny-emitter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
+      "optional": true
+    },
     "tmp": {
       "version": "0.0.33",
       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@@ -8385,6 +8454,24 @@
       "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
       "dev": true
     },
+    "unescape": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/unescape/-/unescape-1.0.1.tgz",
+      "integrity": "sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==",
+      "requires": {
+        "extend-shallow": "^2.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
     "unicode-canonical-property-names-ecmascript": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@@ -8686,6 +8773,16 @@
         "vue-style-loader": "^4.1.0"
       }
     },
+    "vue-prism-editor": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/vue-prism-editor/-/vue-prism-editor-0.6.1.tgz",
+      "integrity": "sha512-UyFLZ242eAplU0C1Tx/ZHSKFTPODQDMBuW9qqgMJyZqHFL2iuIbfT8EWmKtoNUn8w9VWS9IIicPs2odz2eni4Q==",
+      "requires": {
+        "dom-iterator": "^1.0.0",
+        "escape-html": "^1.0.3",
+        "unescape": "^1.0.1"
+      }
+    },
     "vue-style-loader": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",
diff --git a/package.json b/package.json
index edebf9b..a105c61 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,9 @@
   },
   "dependencies": {
     "axios": "^0.19.0",
+    "prismjs": "^1.20.0",
     "vue": "^2.6.10",
+    "vue-prism-editor": "^0.6.1",
     "webextension-polyfill": "^0.3.1"
   },
   "devDependencies": {
diff --git a/src/options/Run.vue b/src/options/Run.vue
index 7707957..bd50f46 100644
--- a/src/options/Run.vue
+++ b/src/options/Run.vue
@@ -71,21 +71,19 @@
     </div>
 
     <div v-else>
-      <form ref="runForm" @submit.prevent="runScript">
-        <textarea v-model="script" />
+      <form ref="runForm" @submit.prevent="runAction">
+        <PrismEditor v-model="script" language="js" />
 
         <div class="row buttons">
-          <button type="button" @click="saveMode = true" :disabled="loading || !(action.name && action.name.length && action.name in actions)" v-if="!saveMode">
-            <i class="fas fa-save" /> &nbsp; Save Action
-          </button>
+          <button type="button" @click="saveMode = true" :disabled="loading || script === scriptTemplate" v-if="!saveMode"><i class="fas fa-save" /> &nbsp; Save Script</button>
           <button type="submit" :disabled="loading"><i class="fas fa-play" /> &nbsp; Run</button>
         </div>
       </form>
     </div>
 
-    <form class="save-form" ref="scriptForm" @submit.prevent="storeAction" v-if="saveMode">
+    <form class="save-form" ref="scriptForm" @submit.prevent="storeScript" v-if="saveMode">
       <div class="row">
-        <input type="text" name="displayName" placeholder="Action display name" />
+        <input type="text" name="displayName" placeholder="Script display name" />
       </div>
 
       <div class="row">
@@ -94,14 +92,14 @@
 
       <div class="row multiple-host-selector">
         <div class="desc">
-          Install action on these devices
+          Install script on these devices
         </div>
 
         <MultipleHostSelector :hosts="hosts" :selected="[host.name]" />
       </div>
 
       <div class="row buttons">
-        <button type="submit" :disabled="loading"><i class="fas fa-save" /> &nbsp; Save Action</button>
+        <button type="submit" :disabled="loading"><i class="fas fa-save" /> &nbsp; Save Script</button>
         <button type="button" @click="saveMode = false" :disabled="loading"><i class="fas fa-times" /> &nbsp; Cancel</button>
       </div>
     </form>
@@ -112,6 +110,10 @@
 </template>
 
 <script>
+import 'prismjs';
+import 'prismjs/themes/prism.css';
+import PrismEditor from 'vue-prism-editor';
+
 import mixins from '../utils';
 import Autocomplete from './Autocomplete';
 import MultipleHostSelector from './MultipleHostSelector';
@@ -121,11 +123,25 @@ export default {
   mixins: [mixins],
   props: {
     host: Object,
+    scriptTemplate: {
+      type: String,
+      default: `async (app, host, browser, window) => {
+  // Run some action on the host
+  const status = await app.run({ name: 'music.mpd.pause' }, host);
+
+  // Send notifications to the browser
+  app.notify(status.state, 'Music status changed');
+
+  // Return values back to the app
+  return status;
+}`,
+    },
   },
 
   components: {
     Autocomplete,
     MultipleHostSelector,
+    PrismEditor,
   },
 
   data() {
@@ -137,13 +153,10 @@ export default {
       actionResponse: null,
       actionError: null,
       hosts: {},
-      script: `(browser, window, document) => {
-  // Do something
-}`,
+      script: this.scriptTemplate,
       actionMode: 'request',
       action: {
         name: null,
-        script: null,
         args: [],
       },
     };
@@ -212,13 +225,17 @@ export default {
       this.loading = true;
 
       try {
-        this.actionResponse = await this.run(
-          {
-            name: this.action.name,
-            args: this.getActionArgs(),
-          },
-          this.host
-        );
+        if (this.actionMode === 'request') {
+          this.actionResponse = await this.run(
+            {
+              name: this.action.name,
+              args: this.getActionArgs(),
+            },
+            this.host
+          );
+        } else {
+          this.actionResponse = await this.runScript(this.script, this.host);
+        }
 
         this.actionError = null;
       } catch (e) {
@@ -229,11 +246,6 @@ export default {
       }
     },
 
-    async runScript() {
-      this.loading = true;
-      console.log(this.script);
-    },
-
     addActionArgument() {
       this.action.args.push({
         name: '',
diff --git a/src/utils.js b/src/utils.js
index b99648b..c8393bd 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -65,6 +65,24 @@ export default {
       }
     },
 
+    async runScript(script, host) {
+      this.loading = true;
+
+      try {
+        if (typeof script === 'string') {
+          /* eslint no-eval: "off" */
+          script = eval(this.script);
+        }
+
+        return await script(this, host, browser, window);
+      } catch (e) {
+        this.notify(e.message, 'Script error');
+        throw e;
+      } finally {
+        this.loading = false;
+      }
+    },
+
     async getHosts() {
       this.loading = true;