├── .gitattributes ├── .github └── workflows │ ├── docs.yml │ └── lint.yml ├── .gitignore ├── LICENSE ├── README.md ├── package.json └── src ├── actions ├── append-variable.js ├── combine.js ├── dictionary.js ├── get-clipboard.js ├── get-device-details.js ├── get-ip-address.js ├── list.js ├── math.js ├── nothing.js ├── number.js ├── random.js ├── replace.js ├── set-appearance.js ├── set-brightness.js ├── set-clipboard.js ├── set-variable.js ├── set-volume.js ├── url-encode.js └── wait.js ├── index.js ├── types ├── actions │ ├── action.js │ └── passthrough.js ├── context.js └── executor.js └── utils ├── debug.js └── get-actions.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Generate Docs 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | docs: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2.4.0 12 | - name: Install 13 | run: npm install 14 | - name: Generate Docs 15 | run: npm run docs 16 | - name: Deploy 17 | uses: JamesIves/github-pages-deploy-action@v4.2.5 18 | with: 19 | branch: gh-pages 20 | folder: docs -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2.4.0 11 | - name: Install 12 | run: npm install 13 | - name: Lint 14 | run: npm run lint -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # JSDoc output 107 | docs/ 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 haykam821 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cutana 2 | An emulator for shortcuts. 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cutana", 3 | "version": "1.0.0", 4 | "description": "An emulator for shortcuts.", 5 | "main": "./src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "eslint \"./**/*.js\" --ignore-path .gitignore", 9 | "docs": "jsdoc ./src --recurse --destination ./docs --template ./node_modules/minami" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/haykam821/Cutana.git" 14 | }, 15 | "keywords": [ 16 | "apple", 17 | "shortcuts", 18 | "evaluator" 19 | ], 20 | "author": "haykam821", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/haykam821/Cutana/issues" 24 | }, 25 | "homepage": "https://github.com/haykam821/Cutana#readme", 26 | "dependencies": { 27 | "brightness": "^3.0.0", 28 | "chance": "^1.1.4", 29 | "clipboardy": "^2.3.0", 30 | "computer-name": "^0.1.0", 31 | "dark-mode": "^3.0.0", 32 | "debug": "^4.1.1", 33 | "escape-string-regexp": "^4.0.0", 34 | "ipify": "^4.0.0", 35 | "require-all": "^3.0.0", 36 | "systeminformation": "^4.24.0", 37 | "vol": "^3.0.0" 38 | }, 39 | "devDependencies": { 40 | "eslint": "^8.10.0", 41 | "eslint-config-haykam": "^1.16.1", 42 | "jsdoc": "^3.6.4", 43 | "minami": "^1.2.3" 44 | }, 45 | "eslintConfig": { 46 | "extends": "eslint-config-haykam" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/actions/append-variable.js: -------------------------------------------------------------------------------- 1 | const Action = require("../types/actions/action.js"); 2 | 3 | class AppendVariableAction extends Action { 4 | run() { 5 | // Create variable if it doesn't exist yet 6 | if (!this.context.variables[this.parameters.variableName]) { 7 | return this.context.variables[this.parameters.variableName] = this.parameters.input; 8 | } 9 | 10 | return this.context.variables[this.parameters.variableName] += this.parameters.input; 11 | } 12 | } 13 | AppendVariableAction.identifier = "is.workflow.actions.appendvariable"; 14 | 15 | module.exports = AppendVariableAction; 16 | -------------------------------------------------------------------------------- /src/actions/combine.js: -------------------------------------------------------------------------------- 1 | const Action = require("../types/actions/action.js"); 2 | 3 | class CombineAction extends Action { 4 | getSeparator() { 5 | switch (this.parameters.textSeparator) { 6 | case "New Lines": 7 | default: 8 | return "\n"; 9 | case "Spaces": 10 | return " "; 11 | case "Custom": 12 | return this.parameters.textCustomSeparator || ""; 13 | } 14 | } 15 | 16 | run() { 17 | if (!Array.isArray(this.parameters.text)) { 18 | return this.parameters.text; 19 | } 20 | 21 | const separator = this.getSeparator(); 22 | return this.parameters.text.join(separator); 23 | } 24 | } 25 | CombineAction.identifier = "is.workflow.actions.text.combine"; 26 | 27 | module.exports = CombineAction; 28 | -------------------------------------------------------------------------------- /src/actions/dictionary.js: -------------------------------------------------------------------------------- 1 | const Action = require("../types/actions/action.js"); 2 | 3 | class DictionaryAction extends Action { 4 | run() { 5 | return this.parameters.items; 6 | } 7 | } 8 | DictionaryAction.identifier = "is.workflow.actions.dictionary"; 9 | 10 | module.exports = DictionaryAction; 11 | -------------------------------------------------------------------------------- /src/actions/get-clipboard.js: -------------------------------------------------------------------------------- 1 | const Action = require("../types/actions/action.js"); 2 | 3 | const clipboard = require("clipboardy"); 4 | 5 | class GetClipboard extends Action { 6 | run() { 7 | return clipboard.read(); 8 | } 9 | } 10 | GetClipboard.identifier = "is.workflow.actions.getclipboard"; 11 | 12 | module.exports = GetClipboard; 13 | -------------------------------------------------------------------------------- /src/actions/get-device-details.js: -------------------------------------------------------------------------------- 1 | const Action = require("../types/actions/action.js"); 2 | 3 | const systemInfo = require("systeminformation"); 4 | const computerName = require("computer-name"); 5 | 6 | const brightness = require("brightness"); 7 | const volume = require("vol"); 8 | 9 | class GetDeviceDetailsAction extends Action { 10 | getOSInfo(type) { 11 | return systemInfo.osInfo().then(os => { 12 | return os[type]; 13 | }); 14 | } 15 | 16 | getDeviceModel() { 17 | return systemInfo.system().then(system => { 18 | const modelMatch = system.model.match(/[A-Za-z]+/); 19 | if (modelMatch) { 20 | return modelMatch[0]; 21 | } else { 22 | return this.getOSInfo("platform"); 23 | } 24 | }); 25 | } 26 | 27 | getDisplayInfo(type) { 28 | return systemInfo.graphics().then(graphics => { 29 | if (!graphics || !graphics.displays || !graphics.displays[0]) { 30 | return null; 31 | } 32 | 33 | return graphics.displays[0][type]; 34 | }); 35 | } 36 | 37 | run() { 38 | switch (this.parameters.deviceDetail) { 39 | case "Device Name": 40 | default: 41 | return computerName(); 42 | case "Device Model": 43 | return this.getDeviceModel(); 44 | case "System Version": 45 | return this.getOSInfo("release"); 46 | case "Screen Width": 47 | return this.getDisplayInfo("currentResX"); 48 | case "Screen Height": 49 | return this.getDisplayInfo("currentResY"); 50 | case "Current Volume": 51 | return volume.get(); 52 | case "Current Brightness": 53 | return brightness.get(); 54 | } 55 | } 56 | } 57 | GetDeviceDetailsAction.identifier = "is.workflow.actions.getdevicedetails"; 58 | 59 | module.exports = GetDeviceDetailsAction; 60 | -------------------------------------------------------------------------------- /src/actions/get-ip-address.js: -------------------------------------------------------------------------------- 1 | const Action = require("../types/actions/action.js"); 2 | 3 | const externalIP = require("ipify"); 4 | const systemInfo = require("systeminformation"); 5 | 6 | class GetIPAddressAction extends Action { 7 | async getDefaultInterface() { 8 | const defaultInterface = await systemInfo.networkInterfaceDefault(); 9 | const interfaces = await systemInfo.networkInterfaces(); 10 | 11 | return interfaces.find(networkInterface => { 12 | return networkInterface.iface === defaultInterface; 13 | }); 14 | } 15 | 16 | async getLocalIP(ipv6 = false) { 17 | const networkInterface = await this.getDefaultInterface(); 18 | return ipv6 ? networkInterface.ip6 : networkInterface.ip4; 19 | } 20 | 21 | getExternalIP(ipv6 = false) { 22 | return ipv6 ? externalIP() : externalIP({ 23 | useIPv6: false, 24 | }); 25 | } 26 | 27 | run() { 28 | const ipv6 = this.parameters.iPAddressTypeOption === "IPv6"; 29 | 30 | if (this.parameters.iPAddressSourceOption === "External") { 31 | return this.getExternalIP(ipv6); 32 | } else { 33 | return this.getLocalIP(ipv6); 34 | } 35 | } 36 | } 37 | GetIPAddressAction.identifier = "is.workflow.actions.getipaddress"; 38 | 39 | module.exports = GetIPAddressAction; 40 | -------------------------------------------------------------------------------- /src/actions/list.js: -------------------------------------------------------------------------------- 1 | const Action = require("../types/actions/action.js"); 2 | 3 | class ListAction extends Action { 4 | run() { 5 | return this.parameters.items; 6 | } 7 | } 8 | ListAction.identifier = "is.workflow.actions.list"; 9 | 10 | module.exports = ListAction; 11 | -------------------------------------------------------------------------------- /src/actions/math.js: -------------------------------------------------------------------------------- 1 | const Action = require("../types/actions/action.js"); 2 | 3 | class MathAction extends Action { 4 | /** 5 | * Gets the operation of the action. 6 | * @returns {string} The operation of the action. 7 | */ 8 | getOperation() { 9 | if (this.parameters.mathOperation === "…") { 10 | return this.parameters.scientificMathOperation; 11 | } 12 | return this.parameters.mathOperation; 13 | } 14 | 15 | /** 16 | * Gets the second operand of the action. 17 | * @returns {number} The second operand of the action. 18 | */ 19 | getOperand() { 20 | return parseInt(this.parameters.mathOperand || this.parameters.scientificMathOperand); 21 | } 22 | 23 | /** 24 | * Performs an operation. 25 | * @param {string} operation The operation to perform. 26 | * @param {number} operandA The first operand. 27 | * @param {number} operandB The second operand. 28 | * @returns {number} The result of the operation. 29 | */ 30 | performOperation(operation, operandA, operandB) { 31 | switch (operation) { 32 | case undefined: 33 | return operandA + operandB; 34 | case "-": 35 | return operandA - operandB; 36 | case "×": 37 | return operandA * operandB; 38 | case "÷": 39 | return operandA / operandB; 40 | case "Modulus": 41 | return operandA % operandB; 42 | case "x^2": 43 | return operandA ** 2; 44 | case "x^3": 45 | return operandA ** 3; 46 | case "x^y": 47 | return operandA ** operandB; 48 | case "e^x": 49 | return Math.E ** operandA; 50 | case "10^x": 51 | return 10 ** operandA; 52 | case "ln(x)": 53 | return Math.log(operandA); 54 | case "log(x)": 55 | return Math.log10(operandA); 56 | case "√x": 57 | return Math.sqrt(operandA); 58 | case "∛x": 59 | return Math.cbrt(operandA); 60 | case "x!": 61 | for (let index = operandA - 1; index > 0; index--) { 62 | operandA *= index; 63 | } 64 | return operandA; 65 | case "sin(x)": 66 | return Math.sin(operandA); 67 | case "cos(x)": 68 | return Math.cos(operandA); 69 | case "tan(x)": 70 | return Math.tan(operandA); 71 | case "abs(x)": 72 | return Math.abs(operandA); 73 | } 74 | 75 | this.log("could not find the '%s' operation", operation); 76 | return null; 77 | } 78 | 79 | run() { 80 | const operation = this.getOperation(); 81 | return this.performOperation(operation, parseInt(this.parameters.input), this.getOperand()); 82 | } 83 | } 84 | MathAction.identifier = "is.workflow.actions.math"; 85 | 86 | module.exports = MathAction; 87 | -------------------------------------------------------------------------------- /src/actions/nothing.js: -------------------------------------------------------------------------------- 1 | const Action = require("../types/actions/action.js"); 2 | 3 | class NothingAction extends Action { 4 | run() { 5 | return null; 6 | } 7 | } 8 | NothingAction.identifier = "is.workflow.actions.nothing"; 9 | 10 | module.exports = NothingAction; 11 | -------------------------------------------------------------------------------- /src/actions/number.js: -------------------------------------------------------------------------------- 1 | const Action = require("../types/actions/action.js"); 2 | 3 | class NumberAction extends Action { 4 | run() { 5 | const parsedNumber = parseInt(this.parameters.numberActionNumber); 6 | return isNaN(parsedNumber) ? 0 : parsedNumber; 7 | } 8 | } 9 | NumberAction.identifier = "is.workflow.actions.number"; 10 | 11 | module.exports = NumberAction; 12 | -------------------------------------------------------------------------------- /src/actions/random.js: -------------------------------------------------------------------------------- 1 | const Action = require("../types/actions/action.js"); 2 | 3 | const chance = require("chance"); 4 | const random = new chance.Chance(); 5 | 6 | class RandomAction extends Action { 7 | run() { 8 | const min = Math.floor(parseFloat(this.parameters.randomNumberMinimum)); 9 | const max = Math.floor(parseFloat(this.parameters.randomNumberMaximum)); 10 | 11 | if (min > max) { 12 | return null; 13 | } 14 | 15 | return random.integer({ 16 | max, 17 | min, 18 | }); 19 | } 20 | } 21 | RandomAction.identifier = "is.workflow.actions.number.random"; 22 | 23 | module.exports = RandomAction; 24 | -------------------------------------------------------------------------------- /src/actions/replace.js: -------------------------------------------------------------------------------- 1 | const Action = require("../types/actions/action.js"); 2 | 3 | const escape = require("escape-string-regexp"); 4 | 5 | class ReplaceAction extends Action { 6 | run() { 7 | if (this.parameters.replaceTextRegularExpression) { 8 | const expression = new RegExp(this.parameters.replaceTextFind, "g"); 9 | return this.parameters.input.replace(expression, this.parameters.replaceTextReplace); 10 | } else if (!this.parameters.replaceTextCaseSensitive) { 11 | const expression = new RegExp(escape(this.parameters.replaceTextFind), "i"); 12 | return this.parameters.input.split(expression).join(this.parameters.replaceTextReplace); 13 | } else { 14 | return this.parameters.input.split(this.parameters.replaceTextFind).join(this.parameters.replaceTextReplace); 15 | } 16 | } 17 | } 18 | ReplaceAction.identifier = "is.workflow.actions.text.replace"; 19 | 20 | module.exports = ReplaceAction; 21 | -------------------------------------------------------------------------------- /src/actions/set-appearance.js: -------------------------------------------------------------------------------- 1 | const PassthroughAction = require("../types/actions/passthrough.js"); 2 | 3 | const darkMode = require("dark-mode"); 4 | 5 | class SetAppearanceAction extends PassthroughAction { 6 | async run() { 7 | if (this.parameters.style === "light") { 8 | await darkMode.disable(); 9 | } else { 10 | await darkMode.enable(); 11 | } 12 | } 13 | } 14 | SetAppearanceAction.identifier = "is.workflow.actions.appearance"; 15 | 16 | module.exports = SetAppearanceAction; 17 | -------------------------------------------------------------------------------- /src/actions/set-brightness.js: -------------------------------------------------------------------------------- 1 | const PassthroughAction = require("../types/actions/passthrough.js"); 2 | 3 | const brightness = require("brightness"); 4 | 5 | class SetBrightnessAction extends PassthroughAction { 6 | async run() { 7 | if (typeof this.parameters.brightness === "number") { 8 | await brightness.set(this.parameters.brightness); 9 | } else { 10 | await brightness.set(0.5); 11 | } 12 | } 13 | } 14 | SetBrightnessAction.identifier = "is.workflow.actions.setbrightness"; 15 | 16 | module.exports = SetBrightnessAction; 17 | -------------------------------------------------------------------------------- /src/actions/set-clipboard.js: -------------------------------------------------------------------------------- 1 | const PassthroughAction = require("../types/actions/passthrough.js"); 2 | 3 | const clipboard = require("clipboardy"); 4 | 5 | class SetClipboard extends PassthroughAction { 6 | async run() { 7 | await clipboard.write(this.parameters.input); 8 | } 9 | } 10 | SetClipboard.identifier = "is.workflow.actions.setclipboard"; 11 | 12 | module.exports = SetClipboard; 13 | -------------------------------------------------------------------------------- /src/actions/set-variable.js: -------------------------------------------------------------------------------- 1 | const PassthroughAction = require("../types/actions/passthrough.js"); 2 | 3 | class SetVariableAction extends PassthroughAction { 4 | run() { 5 | this.context.variables[this.parameters.variableName] = this.parameters.input; 6 | } 7 | } 8 | SetVariableAction.identifier = "is.workflow.actions.setvariable"; 9 | 10 | module.exports = SetVariableAction; 11 | -------------------------------------------------------------------------------- /src/actions/set-volume.js: -------------------------------------------------------------------------------- 1 | const PassthroughAction = require("../types/actions/passthrough.js"); 2 | 3 | const volume = require("vol"); 4 | 5 | class SetVolumeAction extends PassthroughAction { 6 | async run() { 7 | if (typeof this.parameters.volume === "number") { 8 | await volume.set(this.parameters.volume); 9 | } else { 10 | await volume.set(0.5); 11 | } 12 | } 13 | } 14 | SetVolumeAction.identifier = "is.workflow.actions.setvolume"; 15 | 16 | module.exports = SetVolumeAction; 17 | -------------------------------------------------------------------------------- /src/actions/url-encode.js: -------------------------------------------------------------------------------- 1 | const Action = require("../types/actions/action.js"); 2 | 3 | class URLEncodeAction extends Action { 4 | run() { 5 | if (this.parameters.encodeMode === "Decode") { 6 | return decodeURI(this.parameters.input); 7 | } else { 8 | return encodeURI(this.parameters.input); 9 | } 10 | } 11 | } 12 | URLEncodeAction.identifier = "is.workflow.actions.urlencode"; 13 | 14 | module.exports = URLEncodeAction; 15 | -------------------------------------------------------------------------------- /src/actions/wait.js: -------------------------------------------------------------------------------- 1 | const PassthroughAction = require("../types/actions/passthrough.js"); 2 | 3 | class WaitAction extends PassthroughAction { 4 | run() { 5 | return new Promise(resolve => { 6 | setTimeout(() => { 7 | resolve(); 8 | }, (this.parameters.delayTime || 1) * 1000); 9 | }); 10 | } 11 | } 12 | WaitAction.identifier = "is.workflow.actions.delay"; 13 | 14 | module.exports = WaitAction; 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const ShortcutExecutor = require("./types/executor.js"); 2 | module.exports.ShortcutExecutor = ShortcutExecutor; 3 | -------------------------------------------------------------------------------- /src/types/actions/action.js: -------------------------------------------------------------------------------- 1 | const clipboard = require("clipboardy"); 2 | 3 | const { actions: log } = require("../../utils/debug.js"); 4 | 5 | const rangePattern = /{(\d+), (\d+)}/; 6 | 7 | class Action { 8 | constructor(executor, context, rawAction) { 9 | /** 10 | * @type {ShortcutExecutor} 11 | */ 12 | this.executor = executor; 13 | this.context = context; 14 | 15 | const identifier = this.constructor.identifier.split("."); 16 | this.log = log.extend(identifier.at(-1)); 17 | 18 | this.rawAction = rawAction; 19 | this.parameters = this.getParameters(rawAction.parameters); 20 | } 21 | 22 | getParameters(parameters) { 23 | return Object.fromEntries(Object.entries(parameters).map(([ key, value ]) => { 24 | return [ 25 | this.getParameterKey(key), 26 | this.getParameterValue(value), 27 | ]; 28 | })); 29 | } 30 | 31 | getParameterKey(key) { 32 | if (key.startsWith("WF")) { 33 | key = key[2].toLowerCase() + key.slice(3); 34 | } 35 | return key; 36 | } 37 | 38 | ensureType(value, type) { 39 | return type === 3 ? parseInt(value) : value; 40 | } 41 | 42 | /** 43 | * Gets start and end indexes from a range string. 44 | * @param {string} rangeString The range string to get indexes from. 45 | * @returns {Object?} An object containing the start and end indexes. 46 | */ 47 | getAttachmentRange(rangeString) { 48 | const range = rangePattern.exec(rangeString); 49 | if (range === null) return null; 50 | 51 | return { 52 | end: parseInt(range[2]), 53 | start: parseInt(range[1]), 54 | }; 55 | } 56 | 57 | /** 58 | * Resolves attachments to a string. 59 | * @param {Object} base The base string. 60 | * @param {Object[]} attachments The attachments to apply to the base string. 61 | * @returns {string} The string with attachments resolved. 62 | */ 63 | resolveAttachments(base = "", attachments) { 64 | for (const [ rangeString, value ] of attachments) { 65 | const range = this.getAttachmentRange(rangeString); 66 | if (range === null) continue; 67 | 68 | base = base.slice(0, Math.max(0, range.start)) + this.getSpecialValueType(value) + base.slice(range.end + base.length); 69 | } 70 | return base; 71 | } 72 | 73 | getDictionaryValue(value) { 74 | return this.getParameterValue(value) || (value.Value && this.getDictionaryValue(value.Value)); 75 | } 76 | 77 | getDictionary(entries = []) { 78 | return Object.fromEntries(entries.map(entry => { 79 | const value = this.getDictionaryValue(entry.WFValue); 80 | return [ 81 | this.getParameterValue(entry.WFKey), 82 | this.ensureType(value, entry.WFItemType), 83 | ]; 84 | })); 85 | } 86 | 87 | /** 88 | * Resolves a special value type. 89 | * @param {Object} value The value type to resolve. 90 | * @returns {*} The resolved value. 91 | */ 92 | getSpecialValueType(value) { 93 | switch (value.Type) { 94 | case "ExtensionInput": 95 | return this.executor.initialInput; 96 | case "ActionOutput": 97 | return this.context.variables[value.OutputUUID]; 98 | case "Clipboard": 99 | return clipboard.readSync(); 100 | case "Ask": 101 | return "no"; 102 | } 103 | 104 | this.log("could not get value for value type '%s': %o", value.Type, value); 105 | return null; 106 | } 107 | 108 | getParameterValue(value) { 109 | if (Array.isArray(value)) { 110 | return value.map(subValue => { 111 | const parameterValue = this.getParameterValue(subValue.WFValue || subValue); 112 | return this.ensureType(parameterValue, subValue.WFItemType); 113 | }); 114 | } else if (value.Value === undefined) { 115 | return value; 116 | } else if (typeof value.Value !== "object" || value.Value === null) { 117 | return value.Value; 118 | } else if (value.Value.Type) { 119 | return this.getSpecialValueType(value.Value); 120 | } else if (value.Value.WFDictionaryFieldValueItems || (value.Value.Value && value.Value.Value.WFDictionaryFieldValueItems)) { 121 | return this.getDictionary(value.Value.WFDictionaryFieldValueItems || value.Value.Value.WFDictionaryFieldValueItems); 122 | } else if (value.Value.attachmentsByRange) { 123 | return this.resolveAttachments(value.Value.string, Object.entries(value.Value.attachmentsByRange)); 124 | } 125 | } 126 | 127 | /** 128 | * @abstract 129 | * @returns {*} The output of the action. 130 | */ 131 | run() { 132 | return; 133 | } 134 | 135 | /** 136 | * @returns {*} The output of the action. 137 | */ 138 | execute() { 139 | return this.run(); 140 | } 141 | } 142 | module.exports = Action; 143 | -------------------------------------------------------------------------------- /src/types/actions/passthrough.js: -------------------------------------------------------------------------------- 1 | const Action = require("./action.js"); 2 | 3 | class PassthroughAction extends Action { 4 | async execute() { 5 | await super.execute(); 6 | return this.context.input; 7 | } 8 | } 9 | module.exports = PassthroughAction; 10 | -------------------------------------------------------------------------------- /src/types/context.js: -------------------------------------------------------------------------------- 1 | class Context { 2 | constructor(input) { 3 | this.input = input; 4 | this.variables = {}; 5 | } 6 | } 7 | module.exports = Context; 8 | -------------------------------------------------------------------------------- /src/types/executor.js: -------------------------------------------------------------------------------- 1 | const { main: log } = require("../utils/debug.js"); 2 | const defaultActions = require("../utils/get-actions.js")(); 3 | 4 | const Context = require("./context.js"); 5 | const Action = require("../types/actions/action.js"); 6 | 7 | class ShortcutExecutor { 8 | /** 9 | * @param {Object} options Options for the executor. 10 | */ 11 | constructor(options) { 12 | this.actions = {}; 13 | this.registerActions(defaultActions); 14 | 15 | this.initialInput = options.initialInput || options.input; 16 | this.blacklistedActions = Array.isArray(options.blacklistedActions) ? options.blacklistedActions : []; 17 | this.skipUnsupportedActions = options.skipUnsupportedActions; 18 | } 19 | 20 | /** 21 | * Registers actions. 22 | * @param {Action[]} actions The actions to register. 23 | */ 24 | registerActions(actions) { 25 | if (!Array.isArray(actions)) { 26 | throw new TypeError("The actions to register must be an array."); 27 | } 28 | 29 | for (const action of actions) { 30 | this.registerAction(action); 31 | } 32 | } 33 | 34 | /** 35 | * Registers an action. 36 | * @param {Action} action The action to register. 37 | */ 38 | registerAction(action) { 39 | if (!(action.prototype instanceof Action)) { 40 | throw new TypeError("Actions must extend the Action class"); 41 | } 42 | if (!action.identifier) { 43 | throw new Error("Actions must have an identifier"); 44 | } 45 | 46 | this.actions[action.identifier] = action; 47 | } 48 | 49 | /** 50 | * Gets an action instance from a raw action. 51 | * @param {ShortcutAction} rawAction The raw action to get an action instance of. 52 | * @param {Context} context The context of the action. 53 | * @returns {Action} The action instance. 54 | */ 55 | getActionInstance(rawAction, context) { 56 | const action = this.actions[rawAction.identifier]; 57 | if (!action) { 58 | if (this.skipUnsupportedActions) { 59 | log("skipping action with identifier '%s' as it is unsupported", rawAction.identifier); 60 | return; 61 | } 62 | 63 | throw new Error("Unsupported action: " + rawAction.identifier); 64 | } 65 | 66 | const actionInstance = new action(this, context, rawAction); 67 | return actionInstance; 68 | } 69 | 70 | /** 71 | * Executes an action. 72 | * @param {ShortcutAction} rawAction The raw action to evaluate. 73 | * @param {Context} context The context of the action. 74 | */ 75 | async executeAction(rawAction, context) { 76 | if (Array.isArray(this.blacklistedActions) && this.blacklistedActions.includes(rawAction.identifier)) { 77 | throw new Error("Blacklisted action: " + rawAction.identifier); 78 | } 79 | 80 | const actionInstance = this.getActionInstance(rawAction, context); 81 | if (actionInstance) { 82 | actionInstance.log("evaluating action with identifier '%s' with input of '%s' and parameters: %O", rawAction.identifier, context.input, actionInstance.parameters); 83 | 84 | const actionOutput = await actionInstance.execute(); 85 | context.input = actionOutput; 86 | if (actionInstance.parameters.UUID) { 87 | context.variables[actionInstance.parameters.UUID] = actionOutput; 88 | } 89 | 90 | actionInstance.log("resulting context of previous: %O", context); 91 | } 92 | } 93 | 94 | /** 95 | * Executes a shortcut. 96 | * @param {ShortcutMetadata} metadata The shortcut metadata. 97 | * @returns {*} The result of the final action. 98 | */ 99 | async execute(metadata) { 100 | if (!metadata || !metadata.actions) { 101 | throw new TypeError("Shortcut metadata is required"); 102 | } 103 | 104 | log("evaluating shortcut with %d actions", metadata.actions.length); 105 | 106 | const context = new Context(this.initialInput); 107 | 108 | for await (const rawAction of metadata.actions) { 109 | await this.executeAction(rawAction, context); 110 | if (rawAction.parameters.UUID) { 111 | context.variables[rawAction.parameters.UUID] = context.input; 112 | } 113 | } 114 | 115 | return context.input; 116 | } 117 | } 118 | module.exports = ShortcutExecutor; 119 | -------------------------------------------------------------------------------- /src/utils/debug.js: -------------------------------------------------------------------------------- 1 | const debug = require("debug"); 2 | 3 | module.exports = { 4 | actions: debug("cutana:actions"), 5 | main: debug("cutana:main"), 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/get-actions.js: -------------------------------------------------------------------------------- 1 | const requireAll = require("require-all"); 2 | const path = require("path"); 3 | 4 | /** 5 | * Gets all actions. 6 | * @returns {Action[]} 7 | */ 8 | function getActions() { 9 | const actions = requireAll({ 10 | dirname: path.resolve(__dirname, "../actions"), 11 | filter: /(.+)\.js$/, 12 | }); 13 | return Object.values(actions); 14 | } 15 | module.exports = getActions; 16 | --------------------------------------------------------------------------------