├── .gitattributes ├── .gitignore ├── gatsby-config.js ├── templates └── base │ └── example │ ├── src │ ├── index.css │ ├── index.js │ ├── ConfigView.js │ └── SDApi.js │ ├── public │ ├── action │ │ ├── images.sketch │ │ └── images │ │ │ ├── action.png │ │ │ ├── action@2x.png │ │ │ ├── actionimage.png │ │ │ └── actionimage@2x.png │ ├── propertyinspector │ │ ├── css │ │ │ ├── check.png │ │ │ ├── rcheck.svg │ │ │ ├── caret.svg │ │ │ ├── check.svg │ │ │ ├── elg_calendar_inv.svg │ │ │ ├── elg_calendar.svg │ │ │ ├── g_d8d8d8.svg │ │ │ └── sdpi.css │ │ └── index.html │ ├── en.json │ ├── de.json │ ├── index.html │ ├── code.html │ ├── manifest.json │ ├── externalWindow.html │ ├── app.js │ └── common.js │ ├── Release │ └── README.md │ ├── config │ ├── jest │ │ ├── fileTransform.js │ │ └── cssTransform.js │ ├── polyfills.js │ ├── paths.js │ ├── env.js │ ├── webpackDevServer.config.js │ ├── webpack.config.dev.js │ ├── webpack.config.export.js │ └── webpack.config.prod.js │ ├── scripts │ ├── test.js │ ├── dev.js │ ├── export.js │ ├── start.js │ └── build.js │ └── package.json ├── src └── gatsby-theme-docz │ └── Logo │ ├── console-icon.png │ ├── index.js │ └── logo.svg ├── docs-src ├── gatsby-theme-docz │ └── Logo │ │ ├── console-icon.png │ │ ├── index.js │ │ └── logo.svg ├── development.mdx ├── features.mdx ├── home.mdx ├── deployment.mdx └── guide.mdx ├── .editorconfig ├── cli.js ├── test.js ├── readme.md ├── doczrc.js ├── package.json └── ui.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .docz 3 | docs 4 | node_modules 5 | **/node_modules 6 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [], 3 | pathPrefix: "/create-streamdeck-plugin" 4 | }; 5 | -------------------------------------------------------------------------------- /templates/base/example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/gatsby-theme-docz/Logo/console-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmgriffing/create-streamdeck-plugin/HEAD/src/gatsby-theme-docz/Logo/console-icon.png -------------------------------------------------------------------------------- /docs-src/gatsby-theme-docz/Logo/console-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmgriffing/create-streamdeck-plugin/HEAD/docs-src/gatsby-theme-docz/Logo/console-icon.png -------------------------------------------------------------------------------- /templates/base/example/public/action/images.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmgriffing/create-streamdeck-plugin/HEAD/templates/base/example/public/action/images.sketch -------------------------------------------------------------------------------- /templates/base/example/public/action/images/action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmgriffing/create-streamdeck-plugin/HEAD/templates/base/example/public/action/images/action.png -------------------------------------------------------------------------------- /templates/base/example/public/action/images/action@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmgriffing/create-streamdeck-plugin/HEAD/templates/base/example/public/action/images/action@2x.png -------------------------------------------------------------------------------- /templates/base/example/public/action/images/actionimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmgriffing/create-streamdeck-plugin/HEAD/templates/base/example/public/action/images/actionimage.png -------------------------------------------------------------------------------- /templates/base/example/public/propertyinspector/css/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmgriffing/create-streamdeck-plugin/HEAD/templates/base/example/public/propertyinspector/css/check.png -------------------------------------------------------------------------------- /templates/base/example/public/action/images/actionimage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmgriffing/create-streamdeck-plugin/HEAD/templates/base/example/public/action/images/actionimage@2x.png -------------------------------------------------------------------------------- /src/gatsby-theme-docz/Logo/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import logo from "./logo.svg"; 3 | 4 | export const Logo = () => ( 5 | create-streamdeck-plugin logo 6 | ); 7 | -------------------------------------------------------------------------------- /templates/base/example/public/propertyinspector/css/rcheck.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs-src/gatsby-theme-docz/Logo/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import logo from "./logo.svg"; 3 | 4 | export const Logo = () => ( 5 | create-streamdeck-plugin logo 6 | ); 7 | -------------------------------------------------------------------------------- /templates/base/example/public/propertyinspector/css/caret.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /templates/base/example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import "./index.css"; 5 | import ConfigView from "./ConfigView"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | -------------------------------------------------------------------------------- /templates/base/example/public/propertyinspector/css/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /templates/base/example/Release/README.md: -------------------------------------------------------------------------------- 1 | # Releases 2 | 3 | By default, `yarn export` creates a `.streamDeckPlugin` file from your source. This folder is the output of that process. 4 | 5 | The plugin file is a single self contained way of delivering your plugin to consumers. All they do is download it, and then double click it to install it within the Streamdeck Plugins folder. 6 | -------------------------------------------------------------------------------- /templates/base/example/config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /templates/base/example/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | const React = require("react"); 4 | const importJsx = require("import-jsx"); 5 | const { render } = require("ink"); 6 | const meow = require("meow"); 7 | 8 | const ui = importJsx("./ui"); 9 | 10 | const cli = meow(` 11 | Usage 12 | $ create-streamdeck-plugin 13 | 14 | Examples 15 | $ create-streamdeck-plugin --name=Jane 16 | Hello, Jane 17 | `); 18 | 19 | render(React.createElement(ui, cli.flags)); 20 | -------------------------------------------------------------------------------- /docs-src/development.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚧 Development 3 | route: /development 4 | --- 5 | 6 | # 🚧 Development 7 | 8 | Developing with CSDP is fairly simple. Simply run: 9 | 10 | ``` 11 | yarn dev:watch 12 | ``` 13 | 14 | This will listen for changes to the `public` and `src` folders. It then compiles the app using development settings for a faster iteration cycle. After that, it copies the results to your platform-specific Elgato StreamDeck plugins folder. 15 | -------------------------------------------------------------------------------- /templates/base/example/public/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Use this to create your own plugin", 3 | "Name": "Stream Deck Template", 4 | "Category": "Templates", 5 | "com.elgato.template.action": { 6 | "Name": "Action", 7 | "Tooltip": "This is the only action in this plugin" 8 | }, 9 | "Localization": { 10 | "More info": "More info", 11 | "Message": "Message", 12 | "Click Me": "Click Me", 13 | "Button": "Button" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /templates/base/example/public/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Nimm diese Vorlage für dein erstes Plugin", 3 | "Name": "Stream Deck Template", 4 | "Category": "Templates", 5 | "com.elgato.template.action": { 6 | "Name": "Action", 7 | "Tooltip": "Dies ist die einzige 'Action' in diesem Template" 8 | }, 9 | "Localization": { 10 | "More info": "Mehr Infos", 11 | "Message": "Nachricht", 12 | "Click Me": "Klicke mich", 13 | "Button": "Taste" 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import chalk from 'chalk'; 3 | import test from 'ava'; 4 | import {render} from 'ink-testing-library'; 5 | import App from './ui'; 6 | 7 | test('greet unknown user', t => { 8 | const {lastFrame} = render(); 9 | 10 | t.is(lastFrame(), chalk`Hello, {green Stranger}`); 11 | }); 12 | 13 | test('greet user with a name', t => { 14 | const {lastFrame} = render(); 15 | 16 | t.is(lastFrame(), chalk`Hello, {green Jane}`); 17 | }); 18 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # create-streamdeck-plugin 2 | 3 | > This readme is automatically generated by [create-ink-app](https://github.com/vadimdemedes/create-ink-app) 4 | 5 | 6 | ## Install 7 | 8 | ```bash 9 | $ npm install --global create-streamdeck-plugin 10 | ``` 11 | 12 | 13 | ## CLI 14 | 15 | ``` 16 | $ create-streamdeck-plugin --help 17 | 18 | Usage 19 | $ create-streamdeck-plugin 20 | 21 | Options 22 | --name Your name 23 | 24 | Examples 25 | $ create-streamdeck-plugin --name=Jane 26 | Hello, Jane 27 | ``` 28 | -------------------------------------------------------------------------------- /templates/base/example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | react-streamdeck 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /doczrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: "create-streamdeck-plugin", 3 | dest: "docs", 4 | src: "./docs-src", 5 | typescript: false, 6 | themeConfig: { 7 | initialColorMode: "dark", 8 | colors: { 9 | modes: { 10 | dark: { 11 | primary: "#6678FF", 12 | header: { 13 | bg: "#070f49", 14 | text: "#FFFFFF" 15 | }, 16 | sidebar: { 17 | bg: "#2a2a2a", 18 | navGroup: "#ACAEBD", 19 | navLink: "#ACAEBD", 20 | navLinkActive: "#EBECF3", 21 | tocLink: "#ACAEBD", 22 | tocLinkActive: "#EBECF3" 23 | }, 24 | background: "#282828", 25 | text: "#EFF0F3", 26 | muted: "#ACAEBD", 27 | link: "#6678FF", 28 | props: { 29 | highlight: "#6678FF" 30 | } 31 | } 32 | } 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /templates/base/example/public/propertyinspector/css/elg_calendar_inv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /templates/base/example/public/code.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | react-streamdeck 14 | 15 | 16 | 17 | 18 | <%% #extraFeatures.obs %%> 19 | 20 | 21 | <%% /extraFeatures.obs %%> 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /templates/base/example/scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | let argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /docs-src/features.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚏 Features 3 | route: /features 4 | --- 5 | 6 | # 🚏 Features 7 | 8 | CSDP has several feature options available when scaffolding out your plugin. 9 | 10 | These cover any necessary additional build commands and boilerplate. 11 | 12 | ## OBS 13 | 14 | By selecting this feature, CSDP will scaffold out some some of the events and code for connecting to obs-websocket. 15 | 16 | You must have obs-websocket installed as a plugin into OBS. You can find the relevant Releases here on Github: 17 | 18 | [https://github.com/Palakis/obs-websocket/releases](https://github.com/Palakis/obs-websocket/releases) 19 | 20 | ## Spotify (in progress) 21 | 22 | There is currently a feature option for Spotify being worked on. It is not ready for release yet, but some of its references may be found in the cli tool and code (during the alpha testing of this CSDP). 23 | -------------------------------------------------------------------------------- /templates/base/example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "Actions": [ 3 | { 4 | "Icon": "action/images/actionimage", 5 | "Name": "<%% camelizedProjectName %%>", 6 | "States": [ 7 | { 8 | "Image": "action/images/action" 9 | } 10 | ], 11 | "Tooltip": "This is an example tooltip", 12 | "UUID": "com.<%% projectNamespace %%>.<%% camelizedProjectName %%>.action" 13 | } 14 | ], 15 | "SDKVersion": 2, 16 | "Author": "Elgato Systems", 17 | "CodePath": "code.html", 18 | "PropertyInspectorPath": "propertyinspector/index.html", 19 | "Description": "Example description", 20 | "Name": "<%% camelizedProjectName %%>", 21 | "Icon": "action/images/action", 22 | "URL": "", 23 | "Version": "1.0.0", 24 | "OS": [ 25 | { 26 | "Platform": "mac", 27 | "MinimumVersion": "10.11" 28 | }, 29 | { 30 | "Platform": "windows", 31 | "MinimumVersion": "10" 32 | } 33 | ], 34 | "Software": { 35 | "MinimumVersion": "4.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /templates/base/example/config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | 18 | // In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet. 19 | // We don't polyfill it in the browser--this is user's responsibility. 20 | if (process.env.NODE_ENV === 'test') { 21 | require('raf').polyfill(global); 22 | } 23 | -------------------------------------------------------------------------------- /docs-src/home.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🏠 About 3 | route: / 4 | --- 5 | 6 | 16 | 17 | # ![create-streamdeck-plugin logo](./gatsby-theme-docz/Logo/logo.svg) create-streamdeck-plugin 18 | 19 | This CLI tool scaffolds out a starter plugin for the Elgato StreamDeck. Throughout these docs you will find it referenced as CSDP (C ~~reate-~~ S ~~tream~~ D ~~eck-~~ P ~~lugin~~). 20 | 21 | ## Why? 22 | 23 | Copying and pasting or cloning the plugin example from Github does not give you the full process needed to build, test, and deploy a custom plugin. 24 | 25 | ## There Be ~~Dragons~~ React 26 | 27 | This tool scaffold the plugin using a React based set of components and build processes. Those components can be found in this repo: 28 | 29 | [https://cmgriffing.github.io/react-streamdeck](https://cmgriffing.github.io/react-streamdeck) 30 | -------------------------------------------------------------------------------- /src/gatsby-theme-docz/Logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs-src/gatsby-theme-docz/Logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /templates/base/example/public/propertyinspector/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | com.<%% projectNamespace %%>.<%% projectName %%> Property Inspector 12 | 13 | 14 | 15 |
16 | 17 | <%% #extraFeatures.obs %%> 18 | 19 | <%% #shouldShowVerboseExampleComments %%> 20 | 21 | <%% /shouldShowVerboseExampleComments %%> 22 | 23 | <%% /extraFeatures.obs %%> 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /templates/base/example/scripts/dev.js: -------------------------------------------------------------------------------- 1 | const os = require("os"); 2 | const child_process = require("child_process"); 3 | const fs = require("fs-extra"); 4 | 5 | const platforms = { 6 | win32: { 7 | pluginsFolderPath: `${os.homedir()}\\AppData\\Roaming\\Elgato\\StreamDeck\\Plugins\\`, 8 | }, 9 | darwin: { 10 | pluginsFolderPath: `${os.homedir()}/Library/Application\\ Support/com.elgato.StreamDeck/Plugins/`, 11 | }, 12 | }; 13 | 14 | const currentPlatform = platforms[os.platform()]; 15 | 16 | if (!currentPlatform) { 17 | console.error( 18 | "Current Platform not supported. Supported platforms are: 'win32', 'darwin'" 19 | ); 20 | process.exit(-1); 21 | } 22 | switch (os.platform()) { 23 | case "darwin": 24 | child_process.execSync( 25 | `cp -R build/com.<%%projectNamespace%%>.<%%camelizedProjectName%%>.sdPlugin ${currentPlatform.pluginsFolderPath}/` 26 | ); 27 | break; 28 | case "win32": 29 | fs.copySync( 30 | "build/com.<%%projectNamespace%%>.<%%camelizedProjectName%%>.sdPlugin", 31 | `${currentPlatform.pluginsFolderPath}\\com.<%%projectNamespace%%>.<%%camelizedProjectName%%>.sdPlugin` 32 | ); 33 | break; 34 | 35 | default: 36 | console.error( 37 | "Current Platform not supported. Supported platforms are: 'win32', 'darwin'" 38 | ); 39 | process.exit(-1); 40 | } 41 | -------------------------------------------------------------------------------- /templates/base/example/public/propertyinspector/css/elg_calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs-src/deployment.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🛥️ Distribution 3 | route: /deployment 4 | --- 5 | 6 | # 🛥️ Distribution 7 | 8 | To distribute the aplpication, you can run: 9 | 10 | ``` 11 | yarn export 12 | ``` 13 | 14 | This runs a production build and uses the official Streamdeck `DistributionTool` for your platform. 15 | 16 | You can find these tools here as well if you would like to use them manually: 17 | 18 | [https://developer.elgato.com/documentation/stream-deck/sdk/exporting-your-plugin/](https://developer.elgato.com/documentation/stream-deck/sdk/exporting-your-plugin/) 19 | 20 | ## Distribution On the Store 21 | 22 | Elgato has an internal "store" within the StreamDeck software. 23 | 24 | There is currently no Developer Portal for submitting to the store in an automated way. See here to stay up to date with Elgato's changes on this front: 25 | 26 | [https://developer.elgato.com/documentation/stream-deck/sdk/distribution-on-the-store/](https://developer.elgato.com/documentation/stream-deck/sdk/distribution-on-the-store/) 27 | 28 | ## Distribution On Your Website 29 | 30 | You are welcome to distribute the `.streamDeckPlugin` file on your own site. 31 | 32 | [https://developer.elgato.com/documentation/stream-deck/sdk/distribution-on-your-website/](https://developer.elgato.com/documentation/stream-deck/sdk/distribution-on-your-website/) 33 | 34 | ## Unofficial Index 35 | 36 | There is an unofficial collection of streamdeck plugins available here: 37 | 38 | [https://streamdeck-plugins.com/](https://streamdeck-plugins.com/) 39 | 40 | Instructions for adding your plugin to the unofficial list can be found here: 41 | 42 | [https://streamdeck-plugins.com/developers](https://streamdeck-plugins.com/developers) 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-streamdeck-plugin", 3 | "version": "0.0.18", 4 | "license": "MIT", 5 | "bin": "cli.js", 6 | "repository": "cmgriffing/create-streamdeck-plugin", 7 | "engines": { 8 | "node": ">=8" 9 | }, 10 | "scripts": { 11 | "test": "xo && ava", 12 | "docz:dev": "docz dev", 13 | "docz:build": "docz build", 14 | "docz:serve": "docz build && docz serve", 15 | "docz:deploy": "yarn docz:build && gh-pages -d docs" 16 | }, 17 | "files": [ 18 | "cli.js", 19 | "ui.js", 20 | "templates", 21 | "utils" 22 | ], 23 | "dependencies": { 24 | "camelize": "^1.0.0", 25 | "cli-spinners": "^2.2.0", 26 | "fs-extra": "^8.1.0", 27 | "glob": "^7.1.6", 28 | "import-jsx": "^3.1.0", 29 | "ink": "2.6.0", 30 | "ink-big-text": "^1.0.1", 31 | "ink-box": "^1.0.0", 32 | "ink-gradient": "^1.0.0", 33 | "ink-multi-select": "^1.1.2", 34 | "ink-select-input": "^3.1.2", 35 | "ink-spinner": "^3.0.1", 36 | "ink-text-input": "^3.2.2", 37 | "meow": "^6.0.1", 38 | "mustache": "^4.0.0", 39 | "prop-types": "^15.7.2", 40 | "react": "^16.13.1" 41 | }, 42 | "devDependencies": { 43 | "@babel/preset-react": "^7.8.3", 44 | "@babel/register": "^7.8.6", 45 | "ava": "^3.5.0", 46 | "chalk": "^3.0.0", 47 | "docz": "^2.3.1", 48 | "eslint-config-xo-react": "^0.23.0", 49 | "eslint-plugin-react": "^7.19.0", 50 | "eslint-plugin-react-hooks": "^2.5.0", 51 | "gh-pages": "^1.2.0", 52 | "ink-testing-library": "^1.0.3", 53 | "react-dom": "^16.13.1", 54 | "xo": "^0.28.0" 55 | }, 56 | "ava": { 57 | "require": [ 58 | "@babel/register" 59 | ] 60 | }, 61 | "babel": { 62 | "presets": [ 63 | "@babel/preset-react" 64 | ] 65 | }, 66 | "xo": { 67 | "extends": "xo-react" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docs-src/guide.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🗺️ Getting Started 3 | route: /getting-started 4 | --- 5 | 6 | # 🗺️ Getting Started 7 | 8 | Here we will help guide you through the usage of the CLI tool. 9 | 10 | ## Running the command 11 | 12 | To run the command we recommend using `npx`: 13 | 14 | ``` 15 | npx create-streamdeck-plugin 16 | ``` 17 | 18 | This will ensure that you are always running the latest version. 19 | 20 | ### Or Global 21 | 22 | You can also install it globally, if you insist: 23 | 24 | ``` 25 | yarn global create-streamdeck-plugin 26 | ``` 27 | 28 | or 29 | 30 | ``` 31 | npm install -g create-streamdeck-plugin 32 | ``` 33 | 34 | Then just run it: 35 | 36 | ``` 37 | create-streamdeck-plugin 38 | ``` 39 | 40 | ## The Interactive Steps 41 | 42 | When running the command this tool currently only supports a guided creation process. In the future, it will allow an automated workflow via cli arguments. 43 | 44 | ### Step 1: Project Name 45 | 46 | This value is used in various places. We handle various cases as needed for injecting the name within the app. eg: dasherized, camelCased, and PascalCased. 47 | 48 | ### Step 2: Project Path 49 | 50 | The default value is dasherized form your project name as a subfolder of your current path. You can modify this value as you see fit. eg: Pointing at the current directory would be `./` 51 | 52 | ### Step 3: Project Features 53 | 54 | At this step you can select some of the boilerplate that can be autofilled for you. OBS, Spotify, and more on the way. 55 | 56 | ### Step 4: Scaffolding Finished 57 | 58 | Hooray! You are done. Navigate to the path you installed into in [Step 2](#step-2-project-path) and open your favorite editor. 59 | 60 | See [Development](./development) for the next steps. 61 | -------------------------------------------------------------------------------- /templates/base/example/config/paths.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const fs = require("fs"); 5 | const url = require("url"); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith("/"); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = (appPackageJson) => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /templates/base/example/public/app.js: -------------------------------------------------------------------------------- 1 | /* global $CC, Utils, $SD, OBSWebSocket, OBSWebSocket */ 2 | 3 | <%% #shouldShowVerboseExampleComments %%> 4 | /** 5 | * Here are a couple of wrappers we created to help you quickly setup 6 | * your plugin and subscribe to events sent by Stream Deck to your plugin. 7 | */ 8 | 9 | /** 10 | * The 'connected' event is sent to your plugin, after the plugin's instance 11 | * is registered with Stream Deck software. It carries the current websocket 12 | * and other information about the current environmet in a JSON object 13 | * You can use it to subscribe to events you want to use in your plugin. 14 | */ 15 | <%% /shouldShowVerboseExampleComments %%> 16 | 17 | $SD.on("connected", jsonObj => connected(jsonObj)); 18 | $SD.on("deviceDidConnect", jsonObj => console.log("deviceDidConnect", jsonObj)); 19 | 20 | const obs = new OBSWebSocket(); 21 | obs.connect(); 22 | 23 | async function connected(jsn) { 24 | /** subscribe to the willAppear and other events */ 25 | $SD.on( 26 | "com.<%% projectNamespace %%>.<%% camelizedProjectName %%>.action.willAppear", 27 | jsonObj => action.onWillAppear(jsonObj) 28 | ); 29 | $SD.on( 30 | "com.<%% projectNamespace %%>.<%% camelizedProjectName %%>.action.keyUp", 31 | jsonObj => action.onKeyUp(jsonObj) 32 | ); 33 | $SD.on( 34 | "com.<%% projectNamespace %%>.<%% camelizedProjectName %%>.action.sendToPlugin", 35 | jsonObj => action.onSendToPlugin(jsonObj) 36 | ); 37 | $SD.on( 38 | "com.<%% projectNamespace %%>.<%% camelizedProjectName %%>.action.didReceiveSettings", 39 | jsonObj => action.onDidReceiveSettings(jsonObj) 40 | ); 41 | } 42 | 43 | /** ACTIONS */ 44 | 45 | const action = { 46 | settings: {}, 47 | onDidReceiveSettings: function(jsn) { 48 | console.log( 49 | "%c%s", 50 | "color: white; background: red; font-size: 15px;", 51 | "[app.js]onDidReceiveSettings:" 52 | ); 53 | this.settings[jsonObj.context] = Utils.getProp(jsn, "payload.settings", {}); 54 | this.doSomeThing(this.settings, "onDidReceiveSettings", "orange"); 55 | }, 56 | 57 | <%% #shouldShowVerboseExampleComments %%> 58 | /** 59 | * The 'willAppear' event is the first event a key will receive, right before it gets 60 | * showed on your Stream Deck and/or in Stream Deck software. 61 | * This event is a good place to setup your plugin and look at current settings (if any), 62 | * which are embedded in the events payload. 63 | */ 64 | <%% /shouldShowVerboseExampleComments %%> 65 | 66 | onWillAppear: function(jsn) { 67 | console.log( 68 | "You can cache your settings in 'onWillAppear'", 69 | jsn.payload.settings 70 | ); 71 | 72 | <%% #shouldShowVerboseExampleComments %%> 73 | /** 74 | * "The willAppear event carries your saved settings (if any). You can use these settings 75 | * to setup your plugin or save the settings for later use. 76 | * If you want to request settings at a later time, you can do so using the 77 | * 'getSettings' event, which will tell Stream Deck to send your data 78 | * (in the 'didReceiveSettings above) 79 | * 80 | * $SD.api.getSettings(jsn.context); 81 | */ 82 | <%% /shouldShowVerboseExampleComments %%> 83 | this.settings[jsonObj.context] = jsn.payload.settings; 84 | }, 85 | 86 | onKeyUp: async function(jsn) { 87 | console.log("onKeyUp", jsn); 88 | }, 89 | 90 | onSendToPlugin: function(jsn) { 91 | <%% #shouldShowVerboseExampleComments %%> 92 | /** 93 | * this is a message sent directly from the Property Inspector 94 | * (e.g. some value, which is not saved to settings) 95 | * You can send this event from Property Inspector (see there for an example) 96 | */ 97 | <%% /shouldShowVerboseExampleComments %%> 98 | 99 | const sdpi_collection = Utils.getProp(jsn, "payload.sdpi_collection", {}); 100 | if (sdpi_collection.value && sdpi_collection.value !== undefined) { 101 | this.doSomeThing( 102 | { [sdpi_collection.key]: sdpi_collection.value }, 103 | "onSendToPlugin", 104 | "fuchsia" 105 | ); 106 | } 107 | }, 108 | 109 | <%% #shouldShowVerboseExampleComments %%> 110 | /** 111 | * Here's a quick demo-wrapper to show how you could change a key's title based on what you 112 | * stored in settings. 113 | * If you enter something into Property Inspector's name field (in this demo), 114 | * it will get the title of your key. 115 | * 116 | * @param {JSON} jsn // the JSON object passed from Stream Deck to the plugin, which contains the plugin's context 117 | * 118 | */ 119 | <%% /shouldShowVerboseExampleComments %%> 120 | 121 | setTitle: function(jsn) { 122 | if ( 123 | this.settings[jsonObj.context] && 124 | this.settings[jsonObj.context].hasOwnProperty("mynameinput") 125 | ) { 126 | console.log( 127 | "watch the key on your StreamDeck - it got a new title...", 128 | this.settings[jsonObj.context].mynameinput 129 | ); 130 | $SD.api.setTitle(jsn.context, this.settings[jsonObj.context].mynameinput); 131 | } 132 | }, 133 | 134 | <%% #shouldShowVerboseExampleComments %%> 135 | /** 136 | * Finally here's a method which gets called from various events above. 137 | * This is just an idea how you can act on receiving some interesting message 138 | * from Stream Deck. 139 | */ 140 | <%% /shouldShowVerboseExampleComments %%> 141 | 142 | doSomeThing: function(inJsonData, caller, tagColor) { 143 | console.log( 144 | "%c%s", 145 | `color: white; background: ${tagColor || "grey"}; font-size: 15px;`, 146 | `[app.js]doSomeThing from: ${caller}` 147 | ); 148 | console.log({ inJsonData }); 149 | } 150 | }; 151 | -------------------------------------------------------------------------------- /templates/base/example/config/webpackDevServer.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware'); 4 | const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware'); 5 | const ignoredFiles = require('react-dev-utils/ignoredFiles'); 6 | const config = require('./webpack.config.dev'); 7 | const paths = require('./paths'); 8 | 9 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 10 | const host = process.env.HOST || '0.0.0.0'; 11 | 12 | module.exports = function(proxy, allowedHost) { 13 | return { 14 | // WebpackDevServer 2.4.3 introduced a security fix that prevents remote 15 | // websites from potentially accessing local content through DNS rebinding: 16 | // https://github.com/webpack/webpack-dev-server/issues/887 17 | // https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a 18 | // However, it made several existing use cases such as development in cloud 19 | // environment or subdomains in development significantly more complicated: 20 | // https://github.com/facebookincubator/create-react-app/issues/2271 21 | // https://github.com/facebookincubator/create-react-app/issues/2233 22 | // While we're investigating better solutions, for now we will take a 23 | // compromise. Since our WDS configuration only serves files in the `public` 24 | // folder we won't consider accessing them a vulnerability. However, if you 25 | // use the `proxy` feature, it gets more dangerous because it can expose 26 | // remote code execution vulnerabilities in backends like Django and Rails. 27 | // So we will disable the host check normally, but enable it if you have 28 | // specified the `proxy` setting. Finally, we let you override it if you 29 | // really know what you're doing with a special environment variable. 30 | disableHostCheck: 31 | !proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true', 32 | // Enable gzip compression of generated files. 33 | compress: true, 34 | // Silence WebpackDevServer's own logs since they're generally not useful. 35 | // It will still show compile warnings and errors with this setting. 36 | clientLogLevel: 'none', 37 | // By default WebpackDevServer serves physical files from current directory 38 | // in addition to all the virtual build products that it serves from memory. 39 | // This is confusing because those files won’t automatically be available in 40 | // production build folder unless we copy them. However, copying the whole 41 | // project directory is dangerous because we may expose sensitive files. 42 | // Instead, we establish a convention that only files in `public` directory 43 | // get served. Our build script will copy `public` into the `build` folder. 44 | // In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%: 45 | // 46 | // In JavaScript code, you can access it with `process.env.PUBLIC_URL`. 47 | // Note that we only recommend to use `public` folder as an escape hatch 48 | // for files like `favicon.ico`, `manifest.json`, and libraries that are 49 | // for some reason broken when imported through Webpack. If you just want to 50 | // use an image, put it in `src` and `import` it from JavaScript instead. 51 | contentBase: paths.appPublic, 52 | // By default files from `contentBase` will not trigger a page reload. 53 | watchContentBase: true, 54 | // Enable hot reloading server. It will provide /sockjs-node/ endpoint 55 | // for the WebpackDevServer client so it can learn when the files were 56 | // updated. The WebpackDevServer client is included as an entry point 57 | // in the Webpack development configuration. Note that only changes 58 | // to CSS are currently hot reloaded. JS changes will refresh the browser. 59 | hot: true, 60 | // It is important to tell WebpackDevServer to use the same "root" path 61 | // as we specified in the config. In development, we always serve from /. 62 | publicPath: config.output.publicPath, 63 | // WebpackDevServer is noisy by default so we emit custom message instead 64 | // by listening to the compiler events with `compiler.plugin` calls above. 65 | quiet: true, 66 | // Reportedly, this avoids CPU overload on some systems. 67 | // https://github.com/facebookincubator/create-react-app/issues/293 68 | // src/node_modules is not ignored to support absolute imports 69 | // https://github.com/facebookincubator/create-react-app/issues/1065 70 | watchOptions: { 71 | ignored: ignoredFiles(paths.appSrc), 72 | }, 73 | // Enable HTTPS if the HTTPS environment variable is set to 'true' 74 | https: protocol === 'https', 75 | host: host, 76 | overlay: false, 77 | historyApiFallback: { 78 | // Paths with dots should still use the history fallback. 79 | // See https://github.com/facebookincubator/create-react-app/issues/387. 80 | disableDotRule: true, 81 | }, 82 | public: allowedHost, 83 | proxy, 84 | before(app) { 85 | // This lets us open files from the runtime error overlay. 86 | app.use(errorOverlayMiddleware()); 87 | // This service worker file is effectively a 'no-op' that will reset any 88 | // previous service worker registered for the same host:port combination. 89 | // We do this in development to avoid hitting the production cache if 90 | // it used the same host and port. 91 | // https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432 92 | app.use(noopServiceWorkerMiddleware()); 93 | }, 94 | }; 95 | }; 96 | -------------------------------------------------------------------------------- /templates/base/example/src/ConfigView.js: -------------------------------------------------------------------------------- 1 | /* global $SD, OBSWebSocket, lox */ 2 | import React, { useState, useEffect, useReducer } from "react"; 3 | 4 | import { 5 | createUseSDAction, 6 | SDButton, 7 | SDNumberInput, 8 | SDTextInput, 9 | SDSelectInput, 10 | SDList, 11 | SDListSelect, 12 | SDListMultiSelect, 13 | createUsePluginSettings, 14 | createUseGlobalSettings 15 | } from "react-streamdeck"; 16 | 17 | // Slightly modified sdpi.css file. Adds 'data-' prefixes where needed. 18 | import "react-streamdeck/dist/css/sdpi.css"; 19 | 20 | const createGetSettings = _sd => () => { 21 | if (_sd.api.getSettings) { 22 | _sd.api.getSettings(_sd.uuid); 23 | } else { 24 | _sd.api.common.getSettings(_sd.uuid); 25 | } 26 | }; 27 | 28 | const useSDAction = createUseSDAction({ 29 | useState, 30 | useEffect 31 | }); 32 | 33 | export default function ConfigView() { 34 | const getSettings = createGetSettings($SD); 35 | useEffect(getSettings, []); 36 | 37 | const connectedResult = useSDAction("connected"); 38 | 39 | const [settings, setSettings] = createUsePluginSettings({ 40 | useState, 41 | useEffect, 42 | useReducer 43 | })( 44 | { 45 | buttonState: "", 46 | textState: "", 47 | numberState: 0, 48 | selectState: "", 49 | selectedListState: [] 50 | }, 51 | connectedResult 52 | ); 53 | 54 | <%% #extraFeatures.obs %%> 55 | 56 | const [obs, setOBS] = useState(null); 57 | 58 | useEffect(() => { 59 | const obsInstance = new OBSWebSocket(); 60 | obsInstance.connect().then(result => { 61 | setOBS(obsInstance); 62 | }); 63 | }, []); 64 | 65 | useEffect(() => { 66 | if (obs) { 67 | obs.on("SwitchScenes", (err, rawEvent) => { 68 | console.log({ err, rawEvent }); 69 | }); 70 | } else { 71 | console.log("OBS as not connected yet"); 72 | } 73 | }, [obs]); 74 | 75 | const handleClick = event => { 76 | obs.sendCallback("GetCurrentScene", (err, currentScene) => { 77 | console.log("OBS:sendCallback", { currentScene, err }); 78 | }); 79 | obs.send("GetCurrentScene").then(currentScene => { 80 | console.log("OBS:send", { currentScene }); 81 | }); 82 | }; 83 | 84 | <%% /extraFeatures.obs %%> 85 | 86 | console.log({ 87 | connectedResult, 88 | settings 89 | }); 90 | 91 | return ( 92 |
93 | <%% #extraFeatures.obs %%> 94 | 95 | <%% /extraFeatures.obs %%> 96 | 97 | { 100 | const newState = { 101 | ...settings, 102 | buttonState: `testing ${Date.now()}` 103 | }; 104 | setSettings(newState); 105 | }} 106 | /> 107 | { 111 | const newState = { 112 | ...settings, 113 | textState: event.target.value 114 | }; 115 | setSettings(newState); 116 | }} 117 | /> 118 | { 122 | const newState = { 123 | ...settings, 124 | numberState: event.target.value 125 | }; 126 | setSettings(newState); 127 | }} 128 | /> 129 | { 147 | const newState = { 148 | ...settings, 149 | selectState: event 150 | }; 151 | setSettings(newState); 152 | }} 153 | /> 154 | 155 | 172 | 173 | { 191 | const newState = { 192 | ...settings, 193 | selectedListState: event 194 | }; 195 | setSettings(newState); 196 | }} 197 | /> 198 | 199 | { 217 | const newState = { 218 | ...settings, 219 | selectedListState: event 220 | }; 221 | setSettings(newState); 222 | }} 223 | /> 224 |
225 | ); 226 | } 227 | -------------------------------------------------------------------------------- /templates/base/example/src/SDApi.js: -------------------------------------------------------------------------------- 1 | /* global $SD */ 2 | 3 | const DestinationEnum = Object.freeze({ 4 | HARDWARE_AND_SOFTWARE: 0, 5 | HARDWARE_ONLY: 1, 6 | SOFTWARE_ONLY: 2 7 | }); 8 | 9 | /** SDApi 10 | * This ist the main API to communicate between plugin, property inspector and 11 | * application host. 12 | * Internal functions: 13 | * - setContext: sets the context of the current plugin 14 | * - exec: prepare the correct JSON structure and send 15 | * 16 | * Methods exposed in the $SD.api alias 17 | * Messages send from the plugin 18 | * ----------------------------- 19 | * - showAlert 20 | * - showOK 21 | * - setSettings 22 | * - setTitle 23 | * - setImage 24 | * - sendToPropertyInspector 25 | * 26 | * Messages send from Property Inspector 27 | * ------------------------------------- 28 | * - sendToPlugin 29 | * 30 | * Messages received in the plugin 31 | * ------------------------------- 32 | * willAppear 33 | * willDisappear 34 | * keyDown 35 | * keyUp 36 | */ 37 | 38 | const SDApi = { 39 | send: function(context, fn, payload, debug) { 40 | /** Combine the passed JSON with the name of the event and it's context 41 | * If the payload contains 'event' or 'context' keys, it will overwrite existing 'event' or 'context'. 42 | * This function is non-mutating and thereby creates a new object containing 43 | * all keys of the original JSON objects. 44 | */ 45 | const pl = Object.assign({}, { event: fn, context: context }, payload); 46 | 47 | /** Check, if we have a connection, and if, send the JSON payload */ 48 | if (debug) { 49 | console.log("-----SDApi.send-----"); 50 | console.log("context", context); 51 | console.log(pl); 52 | console.log(payload.payload); 53 | console.log(JSON.stringify(payload.payload)); 54 | console.log("-------"); 55 | } 56 | $SD.connection && $SD.connection.sendJSON(pl); 57 | 58 | /** 59 | * DEBUG-Utility to quickly show the current payload in the Property Inspector. 60 | */ 61 | 62 | if ( 63 | $SD.connection && 64 | ["sendToPropertyInspector", "showOK", "showAlert", "setSettings"].indexOf( 65 | fn 66 | ) === -1 67 | ) { 68 | // console.log("send.sendToPropertyInspector", payload); 69 | // this.sendToPropertyInspector(context, typeof payload.payload==='object' ? JSON.stringify(payload.payload) : JSON.stringify({'payload':payload.payload}), pl['action']); 70 | } 71 | }, 72 | 73 | registerPlugin: { 74 | /** Messages send from the plugin */ 75 | showAlert: function(context) { 76 | SDApi.send(context, "showAlert", {}); 77 | }, 78 | 79 | showOk: function(context) { 80 | SDApi.send(context, "showOk", {}); 81 | }, 82 | 83 | setState: function(context, payload) { 84 | SDApi.send(context, "setState", { 85 | payload: { 86 | state: 1 - Number(payload === 0) 87 | } 88 | }); 89 | }, 90 | 91 | setTitle: function(context, title, target) { 92 | SDApi.send(context, "setTitle", { 93 | payload: { 94 | title: "" + title || "", 95 | target: target || DestinationEnum.HARDWARE_AND_SOFTWARE 96 | } 97 | }); 98 | }, 99 | 100 | setImage: function(context, img, target) { 101 | SDApi.send(context, "setImage", { 102 | payload: { 103 | image: img || "", 104 | target: target || DestinationEnum.HARDWARE_AND_SOFTWARE 105 | } 106 | }); 107 | }, 108 | 109 | sendToPropertyInspector: function(context, payload, action) { 110 | SDApi.send(context, "sendToPropertyInspector", { 111 | action: action, 112 | payload: payload 113 | }); 114 | }, 115 | 116 | showUrl2: function(context, urlToOpen) { 117 | SDApi.send(context, "openUrl", { 118 | payload: { 119 | url: urlToOpen 120 | } 121 | }); 122 | } 123 | }, 124 | 125 | /** Messages send from Property Inspector */ 126 | 127 | registerPropertyInspector: { 128 | sendToPlugin: function(piUUID, action, payload) { 129 | SDApi.send( 130 | piUUID, 131 | "sendToPlugin", 132 | { 133 | action: action, 134 | payload: payload || {} 135 | }, 136 | false 137 | ); 138 | } 139 | }, 140 | 141 | /** COMMON */ 142 | 143 | common: { 144 | getSettings: function(context, payload) { 145 | SDApi.send(context, "getSettings", {}); 146 | }, 147 | 148 | setSettings: function(context, payload) { 149 | SDApi.send(context, "setSettings", { 150 | payload: payload 151 | }); 152 | }, 153 | 154 | getGlobalSettings: function(context, payload) { 155 | SDApi.send(context, "getGlobalSettings", {}); 156 | }, 157 | 158 | setGlobalSettings: function(context, payload) { 159 | SDApi.send(context, "setGlobalSettings", { 160 | payload: payload 161 | }); 162 | }, 163 | 164 | logMessage: function() { 165 | /** 166 | * for logMessage we don't need a context, so we allow both 167 | * logMessage(unneededContext, 'message') 168 | * and 169 | * logMessage('message') 170 | */ 171 | 172 | let payload = arguments.length > 1 ? arguments[1] : arguments[0]; 173 | 174 | SDApi.send(null, "logMessage", { 175 | payload: { 176 | message: payload 177 | } 178 | }); 179 | }, 180 | 181 | openUrl: function(context, urlToOpen) { 182 | SDApi.send(context, "openUrl", { 183 | payload: { 184 | url: urlToOpen 185 | } 186 | }); 187 | }, 188 | 189 | test: function() { 190 | console.log(this); 191 | console.log(SDApi); 192 | }, 193 | 194 | debugPrint: function(context, inString) { 195 | // console.log("------------ DEBUGPRINT"); 196 | // console.log([].slice.apply(arguments).join()); 197 | // console.log("------------ DEBUGPRINT"); 198 | SDApi.send(context, "debugPrint", { 199 | payload: [].slice.apply(arguments).join(".") || "" 200 | }); 201 | }, 202 | 203 | dbgSend: function(fn, context) { 204 | /** lookup if an appropriate function exists */ 205 | if ($SD.connection && this[fn] && typeof this[fn] === "function") { 206 | /** verify if type of payload is an object/json */ 207 | const payload = this[fn](); 208 | if (typeof payload === "object") { 209 | Object.assign({ event: fn, context: context }, payload); 210 | $SD.connection && $SD.connection.sendJSON(payload); 211 | } 212 | } 213 | console.log(this, fn, typeof this[fn], this[fn]()); 214 | } 215 | } 216 | }; 217 | -------------------------------------------------------------------------------- /ui.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Node deps 3 | const path = require("path"); 4 | const fs = require("fs-extra"); 5 | const mustache = require("mustache"); 6 | const glob = require("glob"); 7 | const camelize = require("camelize"); 8 | 9 | // React deps 10 | const React = require("react"); 11 | const { useState } = React; 12 | const PropTypes = require("prop-types"); 13 | 14 | // Ink deps 15 | const { Text, Color, Static, Box } = require("ink"); 16 | const Gradient = require("ink-gradient"); 17 | const BigText = require("ink-big-text"); 18 | const SelectInput = require("ink-select-input").default; 19 | const MultiSelect = require("ink-multi-select").default; 20 | const InkBox = require("ink-box"); 21 | const TextInput = require("ink-text-input").UncontrolledTextInput; 22 | 23 | const Spinner = require("ink-spinner").default; 24 | 25 | // -------------------------------------------------- 26 | // Select Random Spinner Key for each invocation 27 | const cliSpinners = require("cli-spinners"); 28 | const cliSpinnerKeys = Object.keys(cliSpinners); 29 | const currentSpinner = 30 | cliSpinnerKeys[Math.floor(Math.random() * cliSpinnerKeys.length)]; 31 | // -------------------------------------------------- 32 | 33 | mustache.templateCache = undefined; 34 | 35 | /* 36 | 37 | - plugin name 38 | - plugin namespace (usually company name) 39 | - plugin path 40 | 41 | eventual options: 42 | - include OBS? 43 | - interactive component scaffolding based 44 | 45 | */ 46 | 47 | const booleanOptions = [ 48 | { label: "Yes", value: true }, 49 | { label: "No", value: false }, 50 | ]; 51 | 52 | const availableOptionsMap = { 53 | obs: "OBS", 54 | spotify: "Spotify", 55 | }; 56 | 57 | const availableOptions = Object.keys(availableOptionsMap).map((optionKey) => { 58 | return { 59 | label: availableOptionsMap[optionKey], 60 | value: optionKey, 61 | }; 62 | }); 63 | 64 | const App = ({ name }) => { 65 | const [finished, setFinished] = useState(false); 66 | const [projectName, setProjectName] = useState(""); 67 | const [projectNameSelected, setProjectNameSelected] = useState(false); 68 | const [projectNamespace, setProjectNamespace] = useState(""); 69 | const [projectNamespaceSelected, setProjectNamespaceSelected] = useState( 70 | false 71 | ); 72 | const [projectPath, setProjectPath] = useState(""); 73 | const [projectPathSelected, setProjectPathSelected] = useState(false); 74 | 75 | const [ 76 | shouldShowVerboseExampleComments, 77 | setShouldShowVerboseExampleComments, 78 | ] = useState(true); 79 | const [ 80 | shouldShowVerboseExampleCommentsSelected, 81 | setShouldShowVerboseExampleCommentsSelected, 82 | ] = useState(false); 83 | 84 | const [extraFeatures, setExtraFeatures] = useState([]); 85 | const [extraFeaturesSelected, setExtraFeaturesSelected] = useState(false); 86 | 87 | const defaultProjectName = "my-streamdeck-plugin"; 88 | const defaultProjectNamespace = "acme"; 89 | const defaultProjectPath = `./${projectName}`; 90 | return ( 91 | <> 92 | 93 | 94 | 99 | 100 | 106 | Use: 107 | "create-streamdeck-plugin --help" for 108 | instructions. 109 | 110 | 111 | 112 | {!projectNameSelected && ( 113 | 114 | Project Name: 115 | { 119 | setProjectName(name || defaultProjectName); 120 | setProjectNameSelected(true); 121 | }} 122 | /> 123 | 124 | )} 125 | 126 | {projectNameSelected && ( 127 | 128 | Project Name: 129 | 130 | {projectName} 131 | 132 | 133 | )} 134 | 135 | {projectNameSelected && !projectPathSelected && ( 136 | 137 | Project Path: 138 | { 142 | setProjectPath(path || defaultProjectPath); 143 | setProjectPathSelected(true); 144 | }} 145 | /> 146 | 147 | )} 148 | {projectPathSelected && ( 149 | 150 | Project Path: 151 | 152 | {projectPath} 153 | 154 | 155 | )} 156 | 157 | {projectNameSelected && projectPathSelected && !projectNamespaceSelected && ( 158 | 159 | Project Namespace: 160 | { 164 | setProjectNamespace(namespace || defaultProjectNamespace); 165 | setProjectNamespaceSelected(true); 166 | }} 167 | /> 168 | 169 | )} 170 | 171 | {projectNamespaceSelected && ( 172 | 173 | Project Namespace: 174 | 175 | {projectNamespace} 176 | 177 | 178 | )} 179 | 180 | {projectNamespaceSelected && !shouldShowVerboseExampleCommentsSelected && ( 181 | 182 | Show Verbose Example Comments: 183 | { 186 | setShouldShowVerboseExampleComments(selection.value); 187 | setShouldShowVerboseExampleCommentsSelected(true); 188 | }} 189 | /> 190 | 191 | )} 192 | 193 | {shouldShowVerboseExampleCommentsSelected && ( 194 | 195 | Show Verbose Example Comments: 196 | 197 | 198 | {shouldShowVerboseExampleComments ? "Yes" : "No"} 199 | 200 | 201 | 202 | )} 203 | 204 | {shouldShowVerboseExampleCommentsSelected && !extraFeaturesSelected && ( 205 | <> 206 | { 209 | setExtraFeaturesSelected(true); 210 | 211 | setExtraFeatures(result.map((feature) => feature.label)); 212 | 213 | const newExtraFeaturesMap = {}; 214 | result.map((feature) => { 215 | newExtraFeaturesMap[feature.value] = true; 216 | }); 217 | 218 | startScaffoldingFileStructure({ 219 | projectNamespace: projectNamespace || defaultProjectNamespace, 220 | projectName, 221 | camelizedProjectName: camelize(projectName), 222 | projectPath, 223 | extraFeatures: newExtraFeaturesMap, 224 | shouldShowVerboseExampleComments, 225 | }); 226 | setTimeout(() => { 227 | setFinished(true); 228 | }, 3000); 229 | }} 230 | /> 231 | 232 | )} 233 | 234 | {extraFeaturesSelected && ( 235 | 236 | Extra Features: 237 | 238 | {extraFeatures.join(", ")} 239 | 240 | 241 | )} 242 | 243 | {extraFeaturesSelected && !finished && ( 244 | <> 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | Scaffolding Project 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | )} 274 | 275 | {finished && ( 276 | <> 277 | All done. :) 278 | 279 | )} 280 | 281 | ); 282 | }; 283 | 284 | App.propTypes = { 285 | name: PropTypes.string, 286 | }; 287 | 288 | App.defaultProps = { 289 | name: "Stranger", 290 | }; 291 | 292 | module.exports = App; 293 | 294 | async function startScaffoldingFileStructure(inputResults) { 295 | // coerce projectPath 296 | const projectPath = path.resolve(inputResults.projectPath); 297 | 298 | // get plugin path 299 | const templatePath = path.resolve(__dirname, `templates/base/example`); 300 | 301 | // read files from plugin path 302 | return Promise.all( 303 | glob.sync(`${templatePath}/**/*`, { nodir: true }).map(async (filePath) => { 304 | const relativeFilePath = filePath.slice(templatePath.length); 305 | const newFilePath = `${projectPath}${relativeFilePath}`; 306 | await fs.ensureFile(newFilePath); 307 | 308 | if ( 309 | ![ 310 | ".gif", 311 | ".jpg", 312 | ".jpeg", 313 | ".bmp", 314 | ".png", 315 | ".eot", 316 | ".ttf", 317 | ".woff", 318 | ".woff2", 319 | ".wav", 320 | ".pdf", 321 | ].includes(path.extname(filePath)) 322 | ) { 323 | const interpolatedFileContents = mustache.render( 324 | await fs.readFile(filePath, { encoding: "utf-8" }), 325 | inputResults, 326 | {}, 327 | ["<%%", "%%>"] 328 | ); 329 | await fs.writeFile(newFilePath, interpolatedFileContents); 330 | } else { 331 | const fileContents = await fs.readFile(filePath); 332 | await fs.writeFile(newFilePath, fileContents); 333 | } 334 | }) 335 | ); 336 | } 337 | -------------------------------------------------------------------------------- /templates/base/example/config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const autoprefixer = require('autoprefixer'); 4 | const path = require('path'); 5 | const webpack = require('webpack'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); 8 | const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); 9 | const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); 10 | const eslintFormatter = require('react-dev-utils/eslintFormatter'); 11 | const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); 12 | const getClientEnvironment = require('./env'); 13 | const paths = require('./paths'); 14 | 15 | // Webpack uses `publicPath` to determine where the app is being served from. 16 | // In development, we always serve from the root. This makes config easier. 17 | const publicPath = '/'; 18 | // `publicUrl` is just like `publicPath`, but we will provide it to our app 19 | // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. 20 | // Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. 21 | const publicUrl = ''; 22 | // Get environment variables to inject into our app. 23 | const env = getClientEnvironment(publicUrl); 24 | 25 | // This is the development configuration. 26 | // It is focused on developer experience and fast rebuilds. 27 | // The production configuration is different and lives in a separate file. 28 | module.exports = { 29 | // You may want 'eval' instead if you prefer to see the compiled output in DevTools. 30 | // See the discussion in https://github.com/facebookincubator/create-react-app/issues/343. 31 | devtool: 'cheap-module-source-map', 32 | // These are the "entry points" to our application. 33 | // This means they will be the "root" imports that are included in JS bundle. 34 | // The first two entry points enable "hot" CSS and auto-refreshes for JS. 35 | entry: [ 36 | // We ship a few polyfills by default: 37 | require.resolve('./polyfills'), 38 | // Include an alternative client for WebpackDevServer. A client's job is to 39 | // connect to WebpackDevServer by a socket and get notified about changes. 40 | // When you save a file, the client will either apply hot updates (in case 41 | // of CSS changes), or refresh the page (in case of JS changes). When you 42 | // make a syntax error, this client will display a syntax error overlay. 43 | // Note: instead of the default WebpackDevServer client, we use a custom one 44 | // to bring better experience for Create React App users. You can replace 45 | // the line below with these two lines if you prefer the stock client: 46 | // require.resolve('webpack-dev-server/client') + '?/', 47 | // require.resolve('webpack/hot/dev-server'), 48 | require.resolve('react-dev-utils/webpackHotDevClient'), 49 | // Finally, this is your app's code: 50 | paths.appIndexJs, 51 | // We include the app code last so that if there is a runtime error during 52 | // initialization, it doesn't blow up the WebpackDevServer client, and 53 | // changing JS code would still trigger a refresh. 54 | ], 55 | output: { 56 | // Add /* filename */ comments to generated require()s in the output. 57 | pathinfo: true, 58 | // This does not produce a real file. It's just the virtual path that is 59 | // served by WebpackDevServer in development. This is the JS bundle 60 | // containing code from all our entry points, and the Webpack runtime. 61 | filename: 'static/js/bundle.js', 62 | // There are also additional JS chunk files if you use code splitting. 63 | chunkFilename: 'static/js/[name].chunk.js', 64 | // This is the URL that app is served from. We use "/" in development. 65 | publicPath: publicPath, 66 | // Point sourcemap entries to original disk location (format as URL on Windows) 67 | devtoolModuleFilenameTemplate: info => 68 | path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'), 69 | }, 70 | resolve: { 71 | // This allows you to set a fallback for where Webpack should look for modules. 72 | // We placed these paths second because we want `node_modules` to "win" 73 | // if there are any conflicts. This matches Node resolution mechanism. 74 | // https://github.com/facebookincubator/create-react-app/issues/253 75 | modules: ['node_modules', paths.appNodeModules].concat( 76 | // It is guaranteed to exist because we tweak it in `env.js` 77 | process.env.NODE_PATH.split(path.delimiter).filter(Boolean) 78 | ), 79 | // These are the reasonable defaults supported by the Node ecosystem. 80 | // We also include JSX as a common component filename extension to support 81 | // some tools, although we do not recommend using it, see: 82 | // https://github.com/facebookincubator/create-react-app/issues/290 83 | // `web` extension prefixes have been added for better support 84 | // for React Native Web. 85 | extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx'], 86 | alias: { 87 | 88 | // Support React Native Web 89 | // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ 90 | 'react-native': 'react-native-web', 91 | }, 92 | plugins: [ 93 | // Prevents users from importing files from outside of src/ (or node_modules/). 94 | // This often causes confusion because we only process files within src/ with babel. 95 | // To fix this, we prevent you from importing files out of src/ -- if you'd like to, 96 | // please link the files into your node_modules/ and let module-resolution kick in. 97 | // Make sure your source files are compiled, as they will not be processed in any way. 98 | new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]), 99 | ], 100 | }, 101 | module: { 102 | strictExportPresence: true, 103 | rules: [ 104 | // TODO: Disable require.ensure as it's not a standard language feature. 105 | // We are waiting for https://github.com/facebookincubator/create-react-app/issues/2176. 106 | // { parser: { requireEnsure: false } }, 107 | 108 | // First, run the linter. 109 | // It's important to do this before Babel processes the JS. 110 | { 111 | test: /\.(js|jsx|mjs)$/, 112 | enforce: 'pre', 113 | use: [ 114 | { 115 | options: { 116 | formatter: eslintFormatter, 117 | eslintPath: require.resolve('eslint'), 118 | 119 | }, 120 | loader: require.resolve('eslint-loader'), 121 | }, 122 | ], 123 | include: paths.appSrc, 124 | }, 125 | { 126 | // "oneOf" will traverse all following loaders until one will 127 | // match the requirements. When no loader matches it will fall 128 | // back to the "file" loader at the end of the loader list. 129 | oneOf: [ 130 | // "url" loader works like "file" loader except that it embeds assets 131 | // smaller than specified limit in bytes as data URLs to avoid requests. 132 | // A missing `test` is equivalent to a match. 133 | { 134 | test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], 135 | loader: require.resolve('url-loader'), 136 | options: { 137 | limit: 10000, 138 | name: 'static/media/[name].[hash:8].[ext]', 139 | }, 140 | }, 141 | // Process JS with Babel. 142 | { 143 | test: /\.(js|jsx|mjs)$/, 144 | include: paths.appSrc, 145 | loader: require.resolve('babel-loader'), 146 | options: { 147 | 148 | // This is a feature of `babel-loader` for webpack (not Babel itself). 149 | // It enables caching results in ./node_modules/.cache/babel-loader/ 150 | // directory for faster rebuilds. 151 | cacheDirectory: true, 152 | }, 153 | }, 154 | // "postcss" loader applies autoprefixer to our CSS. 155 | // "css" loader resolves paths in CSS and adds assets as dependencies. 156 | // "style" loader turns CSS into JS modules that inject