├── .gitignore ├── README.md ├── build.sh ├── devtoolrc.sh ├── index.js ├── package.json ├── server.js ├── src ├── assets │ ├── dropdown.svg │ └── fonts │ │ └── woff │ │ ├── hack-bold-webfont.woff │ │ ├── hack-regular-webfont.woff │ │ └── latin │ │ ├── hack-bold-latin-webfont.woff │ │ └── hack-regular-latin-webfont.woff ├── browser │ ├── app.js │ ├── cmd.js │ ├── dev_index.html │ ├── inputs.js │ ├── prod_index.html │ ├── rpc.js │ └── scss │ │ └── main.scss └── electron │ ├── main.js │ └── rpc.js ├── start.sh ├── webpack.config.js └── webpack.config.prod.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.DS_Store 3 | 4 | dist 5 | built 6 | 7 | *.gz -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dev Tool 2 | 3 | This is a basic boilerplate for building an Electron app with React, HMR, SCSS, and basic commandline rpc features. My intentions with this project is to build simple GUI's for different projects of mine. 4 | 5 | Here's an example Dev Tool I built for [Shindig](https://medium.com/@chetcorcos/shindig-an-event-discovery-app-built-with-meteor-js-react-js-and-neo4j-602afb483ae6#.aoy41qgml). 6 | 7 | ![](https://s3.amazonaws.com/uploads.hipchat.com/51605/2692734/vCp2jqdoR1f14e9/upload.png) 8 | 9 | # Getting Started 10 | 11 | First off, you need to be on a Mac using iTerm2. You can install iTerm2 using `brew cask install iterm2`. 12 | 13 | Then clone and install some dependencies: 14 | 15 | ```bash 16 | git clone https://github.com/ccorcos/dev-tool 17 | cd dev-tool 18 | npm install 19 | ``` 20 | 21 | To start developing: 22 | 23 | ```bash 24 | ./start.sh 25 | ``` 26 | 27 | To build a distributable standalone app: 28 | 29 | ```bash 30 | ./build.sh 31 | ``` 32 | 33 | Some references you might want to checkout that are used in this project: 34 | 35 | - [Ramda.js](http://ramdajs.com/0.19.0/docs/) is a functional programming utility library 36 | - [react-hyperscript](https://github.com/mlmorg/react-hyperscript) is a nice way of writing React code without JSX. 37 | 38 | 39 | ## UI Components 40 | 41 | This project comes with a set of UI components that make it easy to build your own functionality in `src/browser/app.js`. This project uses React and Webpack with hot-module-replacement so your code will update almost instantly with no refresh! 42 | 43 | There are 6 main UI elements for building the UI. All of them are demonstrated in this repo, and an image is provided below: 44 | 45 | ![](https://s3.amazonaws.com/uploads.hipchat.com/51605/2692734/wdglOJdrtZYyIBz/upload.png) 46 | 47 | ### CLI Components 48 | 49 | Three components are in charge of interfacing with Terminal. 50 | 51 | #### `RunBtn` 52 | 53 | `RunBtn` creates a button that runs a shell command in a new tab. For this to work, you must have iTerm open. For example: 54 | 55 | ```js 56 | RunBtn("Node REPL", "cd ~; node;") 57 | ``` 58 | 59 | #### `ExecBtn` 60 | 61 | `ExecBtn` is like `RunBtn` but doesn't create a new tab. For example: 62 | 63 | ```js 64 | ExecBtn("say hi", "say hello `whoami`") 65 | ``` 66 | 67 | #### `Exec` 68 | 69 | `Exec` is a higher-order function for running a command and rendering something with the result: 70 | 71 | ```js 72 | Exec("ls -1 ~/Desktop", (result) => { 73 | const files = R.pipe( 74 | R.trim, 75 | R.split('\n'), 76 | R.map(R.trim) 77 | )(lt) 78 | return h('div', [ 79 | "The following files are on your desktop:", 80 | h('ul', R.map(file => h('li', file), files)) 81 | ]) 82 | }) 83 | ``` 84 | 85 | ### Input Components 86 | 87 | There are also a number of input higher-order functions that compose together well. 88 | 89 | #### `Text` 90 | 91 | Text` renders a text input and lets you render something with the result. For example, the following will run a command typed into the text input: 92 | 93 | ```js 94 | Text({placeholder: 'shell command'}, (cmd) => { 95 | return RunBtn('Run', cmd) 96 | }) 97 | ``` 98 | 99 | #### `Path` 100 | 101 | If you're specifying paths, the `Path` component provides tab completion. The following will open a file: 102 | 103 | ```js 104 | Path({placeholder: '~/path/to/file'}, (path) => { 105 | return ExecBtn('open', `open ${path}`) 106 | }) 107 | ``` 108 | 109 | #### `Select` 110 | 111 | If you want to list things in a dropdown, you can use the `Select` component. This example will open either your Desktop or Documents folders: 112 | 113 | ```js 114 | Select(['Desktop', 'Documents'], (selection) => { 115 | return ExecBtn('Open Folder', `open ~/${selection}`) 116 | }) 117 | ``` 118 | 119 | ### Composing Components 120 | 121 | The most important thing about all these functions is that they compose beautifully. For example, suppose you want to open any file on your Desktop: 122 | 123 | ```js 124 | Exec('ls -1 ~/Desktop/', (result) => { 125 | const files = R.pipe( 126 | R.trim, 127 | R.split('\n'), 128 | R.map(R.trim) 129 | )(result) 130 | return Select(files, (selected) => { 131 | return ExecBtn("Open", `open ~/Desktop/${selected}`) 132 | }) 133 | }) 134 | ``` 135 | 136 | # To Do 137 | 138 | - icon.icns for distributable app 139 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # build js and css files 4 | trash dist 5 | NODE_ENV=production ./node_modules/.bin/webpack -p --config webpack.config.prod.js 6 | 7 | # build mac app 8 | trash built 9 | ./node_modules/.bin/electron-packager . "Dev Tool" \ 10 | --platform=darwin --arch=x64 --version=0.35.6 \ 11 | --prune \ 12 | --app-bundle-id "com.dev-tool.app" --app-version 0.1.0 \ 13 | --out ./built 14 | # --icon ./src/assets/icon.icns \ 15 | 16 | # compress it 17 | cd built 18 | tar -czf DevTool.tar.gz -C ./Dev\ Tool-darwin-x64/ Dev\ Tool.app 19 | cd .. 20 | 21 | # open it 22 | open built/Dev\ Tool-darwin-x64/Dev\ Tool.app -------------------------------------------------------------------------------- /devtoolrc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Open new Terminal tabs from the command line 4 | # 5 | # Author: Justin Hileman (http://justinhileman.com) 6 | # 7 | # Installation: 8 | # Add the following function to your `.bashrc` or `.bash_profile`, 9 | # or save it somewhere (e.g. `~/.tab.bash`) and source it in `.bashrc` 10 | # 11 | # Usage: 12 | # tab Opens the current directory in a new tab 13 | # tab [PATH] Open PATH in a new tab 14 | # tab [CMD] Open a new tab and execute CMD 15 | # tab [PATH] [CMD] ... You can prob'ly guess 16 | 17 | # Only for teh Mac users 18 | [ `uname -s` != "Darwin" ] && return 19 | 20 | function tab () { 21 | local cmd="" 22 | local cdto="$PWD" 23 | local args="$@" 24 | 25 | if [ -d "$1" ]; then 26 | cdto=`cd "$1"; pwd` 27 | args="${@:2}" 28 | fi 29 | 30 | if [ -n "$args" ]; then 31 | cmd="; $args" 32 | fi 33 | 34 | osascript &>/dev/null < 2 | 3 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/fonts/woff/hack-bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccorcos/dev-tool/7a8f0ab77ccfac8f01ac4ff25a0151ab3630c25c/src/assets/fonts/woff/hack-bold-webfont.woff -------------------------------------------------------------------------------- /src/assets/fonts/woff/hack-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccorcos/dev-tool/7a8f0ab77ccfac8f01ac4ff25a0151ab3630c25c/src/assets/fonts/woff/hack-regular-webfont.woff -------------------------------------------------------------------------------- /src/assets/fonts/woff/latin/hack-bold-latin-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccorcos/dev-tool/7a8f0ab77ccfac8f01ac4ff25a0151ab3630c25c/src/assets/fonts/woff/latin/hack-bold-latin-webfont.woff -------------------------------------------------------------------------------- /src/assets/fonts/woff/latin/hack-regular-latin-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccorcos/dev-tool/7a8f0ab77ccfac8f01ac4ff25a0151ab3630c25c/src/assets/fonts/woff/latin/hack-regular-latin-webfont.woff -------------------------------------------------------------------------------- /src/browser/app.js: -------------------------------------------------------------------------------- 1 | require('scss/main.scss') 2 | 3 | import React from "react" 4 | import ReactDOM from "react-dom" 5 | import h from "react-hyperscript" 6 | import R from "ramda" 7 | import electron from "electron" 8 | import rpc from './rpc' 9 | import {RunBtn, ExecBtn, Exec} from './cmd' 10 | import {Text, Path, Select} from './inputs' 11 | 12 | // globals for debugging 13 | window.h = h 14 | window.R = R 15 | window.rpc = rpc 16 | window.Exec = Exec 17 | window.React = React 18 | window.electron = electron 19 | 20 | const Section = (name, children) => { 21 | return h('section.section', [ 22 | h('h3.section__title', name), 23 | h('div.section__content', children) 24 | ]) 25 | } 26 | 27 | const Header = Exec("whoami", (hostname) => { 28 | return h('header.header', [ 29 | h("div.header__logo"), 30 | h("h4.header__title", "Dev Tool Demo"), 31 | h("h4.header__user", hostname) 32 | ]) 33 | }) 34 | 35 | const Row = (children) => h('div.section__row', children) 36 | 37 | const App = React.createClass({ 38 | render() { 39 | return h('div.app', [ 40 | Header, 41 | Section('Demo', [ 42 | Row([ 43 | "Here's a demo of some of the various UI components." 44 | ]) 45 | ]), 46 | Section('RunBtn', [ 47 | Row([ 48 | RunBtn("Node REPL", "cd ~; node;") 49 | ]) 50 | ]), 51 | Section('ExecBtn', [ 52 | Row([ 53 | ExecBtn("Say Hello", "say hello `whoami`") 54 | ]) 55 | ]), 56 | Section('Exec', [ 57 | Row([ 58 | Exec("ls -1 ~/Desktop", (result) => { 59 | const files = R.pipe( 60 | R.trim, 61 | R.split('\n'), 62 | R.map(R.trim) 63 | )(result) 64 | return h('div', [ 65 | "The following files are the first 10 items on your desktop:", 66 | h('ul', R.map(file => h('li', file), files.slice(0,10))) 67 | ]) 68 | }) 69 | ]) 70 | ]), 71 | Section('Text', [ 72 | Row([ 73 | Text({placeholder: 'shell command'}, (cmd) => { 74 | return RunBtn('Run', cmd) 75 | }) 76 | ]) 77 | ]), 78 | Section('Path', [ 79 | Row([ 80 | Path({placeholder: '~/path/to/file'}, (path) => { 81 | return ExecBtn('Open', `open ${path}`) 82 | }) 83 | ]) 84 | ]), 85 | Section('Select', [ 86 | Row([ 87 | Select(['Desktop', 'Documents'], (selection) => { 88 | return ExecBtn('Open Folder', `open ~/${selection}`) 89 | }) 90 | ]) 91 | ]), 92 | Section('Composing Components', [ 93 | Row([ 94 | Exec('ls -1 ~/Desktop/', (result) => { 95 | const files = R.pipe( 96 | R.trim, 97 | R.split('\n'), 98 | R.map(R.trim) 99 | )(result) 100 | return Select(files, (selected) => { 101 | return ExecBtn("Open", `open ~/Desktop/${selected}`) 102 | }) 103 | }) 104 | ]) 105 | ]) 106 | ]) 107 | } 108 | }) 109 | 110 | ReactDOM.render( 111 | h(App, {}), 112 | document.getElementById('root') 113 | ) -------------------------------------------------------------------------------- /src/browser/cmd.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import h from "react-hyperscript" 3 | import rpc from './rpc' 4 | 5 | const RunButton = React.createClass({ 6 | getInitialState() { 7 | return {running: false} 8 | }, 9 | run() { 10 | this.setState({running: true}) 11 | rpc('run', this.props.cmd, (result) => { 12 | this.setState({running: false}) 13 | }) 14 | }, 15 | render() { 16 | return h('button.btn' + (this.state.running ? ".running" : ""), {onClick: this.run}, this.props.name) 17 | } 18 | }) 19 | 20 | // run command in a new tab 21 | const RunBtn = (name, cmd) => { 22 | return h(RunButton, {name, cmd}) 23 | } 24 | 25 | const ExecButton = React.createClass({ 26 | getInitialState() { 27 | return {running: false} 28 | }, 29 | exec() { 30 | this.setState({running: true}) 31 | rpc('exec', this.props.cmd, (result) => { 32 | this.setState({running: false}) 33 | }) 34 | }, 35 | render() { 36 | return h('button.btn' + (this.state.running ? ".running" : ""), {onClick: this.exec}, this.props.name) 37 | } 38 | }) 39 | 40 | // execute a command 41 | const ExecBtn = (name, cmd) => { 42 | return h(ExecButton, {name, cmd}) 43 | } 44 | 45 | const ExecData = React.createClass({ 46 | getInitialState() { 47 | return {data: undefined} 48 | }, 49 | componentWillMount() { 50 | rpc("exec", this.props.cmd, (data) => { 51 | this.setState({data}) 52 | }) 53 | }, 54 | render() { 55 | return (this.state.data === undefined) ? false : this.props.render(this.state.data) 56 | } 57 | }) 58 | 59 | // execute command and render result 60 | const Exec = (cmd, render) => { 61 | return h(ExecData, {cmd, render}) 62 | } 63 | 64 | export {RunBtn, ExecBtn, Exec} -------------------------------------------------------------------------------- /src/browser/dev_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dev Tool Demo 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/browser/inputs.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import h from "react-hyperscript" 3 | import R from 'ramda' 4 | import rpc from './rpc' 5 | 6 | const TextInput = React.createClass({ 7 | getInitialState() { 8 | return {value: this.props.initial || ''} 9 | }, 10 | onChange(e) { 11 | this.setState({value: e.target.value}) 12 | }, 13 | render() { 14 | return h('div', [ 15 | h('input', { 16 | type: 'text', 17 | value: this.state.value, 18 | onChange: this.onChange, 19 | placeholder: this.props.placeholder 20 | }), 21 | this.props.render(this.state.value) 22 | ]) 23 | } 24 | }) 25 | 26 | const Text = ({initial, placeholder}, render) => { 27 | return h(TextInput, {initial, placeholder, render}) 28 | } 29 | 30 | const PathInput = React.createClass({ 31 | getInitialState() { 32 | return {value: this.props.initial || '', valid: true} 33 | }, 34 | onChange(e) { 35 | this.setState({value: e.target.value}) 36 | }, 37 | validate() { 38 | rpc("exec", `ls -d ${this.state.value}`, (result) => { 39 | if (result) { 40 | this.setState({valid: true}) 41 | } else { 42 | this.setState({valid: false}) 43 | } 44 | }) 45 | }, 46 | componentWillMount() { 47 | this.validate() 48 | }, 49 | onBlur() { 50 | this.validate() 51 | }, 52 | onKeyDown(e) { 53 | // if the user presses tab 54 | if (e.keyCode == 9) { 55 | e.preventDefault() 56 | // break up into pieces 57 | const pieces = R.pipe( 58 | R.trim, 59 | R.split('/') 60 | )(this.state.value) 61 | // get the latest directory 62 | const dir = R.pipe( 63 | R.init, 64 | R.join('/') 65 | )(pieces) 66 | // and get the ending 67 | const ending = R.last(pieces) 68 | if (ending === "") { return } 69 | // see if anything matches in that directory 70 | rpc('exec', `ls ${dir}`, (value) => { 71 | // first with hard constrain on beginning 72 | const re = RegExp("^" + ending, "ig") 73 | const files = R.pipe( 74 | R.trim, 75 | R.split('\n'), 76 | R.map(R.trim) 77 | )(value) 78 | const matches = R.filter(R.pipe( 79 | R.match(re), 80 | R.length 81 | ), files) 82 | if (matches.length) { 83 | this.setState({valid: true, value: dir + "/" + matches[0]}) 84 | } else { 85 | // compare with fuzzy matching 86 | const re = RegExp(ending, "ig") 87 | const matches = R.filter(R.pipe( 88 | R.match(re), 89 | R.length 90 | ), files) 91 | if (matches.length) { 92 | this.setState({valid: true, value: dir + "/" + matches[0]}) 93 | } else { 94 | this.setState({valid: false}) 95 | } 96 | } 97 | }) 98 | } 99 | }, 100 | render() { 101 | return h('div', [ 102 | h('input' + (this.state.valid ? '' : '.invalid'), { 103 | type: 'text', 104 | value: this.state.value, 105 | onChange: this.onChange, 106 | onBlur: this.onBlur, 107 | onKeyDown: this.onKeyDown, 108 | placeholder: this.props.placeholder 109 | }), 110 | this.props.render(this.state.value) 111 | ]) 112 | } 113 | }) 114 | 115 | const Path = ({initial, placeholder}, render) => { 116 | return h(PathInput, {initial, placeholder, render}) 117 | } 118 | 119 | const SelectInput = React.createClass({ 120 | getInitialState() { 121 | return {selected: this.props.options[0]} 122 | }, 123 | onChange(e) { 124 | this.setState({selected: e.target.value}) 125 | }, 126 | render() { 127 | return h('span.select-wrapper', [ 128 | h('select', {value: this.state.selected, onChange: this.onChange}, 129 | this.props.options.map(option => { 130 | return h('option', {value: option}, option) 131 | }) 132 | ), 133 | this.props.render(this.state.selected) 134 | ]) 135 | } 136 | }) 137 | 138 | const Select = (options, render) => { 139 | return h(SelectInput, {options, render}) 140 | } 141 | 142 | export {Text, Path, Select} -------------------------------------------------------------------------------- /src/browser/prod_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dev Tool Demo 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/browser/rpc.js: -------------------------------------------------------------------------------- 1 | import electron from "electron" 2 | 3 | const makeId = () => String(Math.round(Math.random()*Math.pow(10, 10))) 4 | 5 | const rpc = (name, arg, callback) => { 6 | const id = makeId() 7 | const listener = (event, result) => { 8 | callback && callback(result) 9 | electron.ipcRenderer.removeListener(channel, listener) 10 | } 11 | const channel = `${name}-${id}` 12 | electron.ipcRenderer.on(channel, listener) 13 | electron.ipcRenderer.send(name, id, arg) 14 | } 15 | 16 | export default rpc -------------------------------------------------------------------------------- /src/browser/scss/main.scss: -------------------------------------------------------------------------------- 1 | // Variables 2 | 3 | $black: #23292F; 4 | $white: #fff; 5 | 6 | $blue: #00C8E5; 7 | 8 | $time: 0.2s; 9 | $curve: ease-in-out; 10 | 11 | 12 | // Fonts 13 | 14 | @font-face { 15 | font-family: 'Hack'; 16 | src: url('../../assets/fonts/woff/latin/hack-regular-latin-webfont.woff') format('woff'); 17 | font-weight: 400; 18 | font-style: normal; 19 | } 20 | 21 | @font-face { 22 | font-family: 'Hack'; 23 | src: url('../../assets/fonts/woff/latin/hack-bold-latin-webfont.woff') format('woff'); 24 | font-weight: 700; 25 | font-style: normal; 26 | } 27 | 28 | 29 | // Reset 30 | 31 | body { 32 | margin: 0; 33 | padding: 0; 34 | } 35 | 36 | 37 | // Base styles 38 | 39 | html, 40 | body { 41 | min-height: 100%; 42 | } 43 | 44 | html { 45 | font-size: 14px; 46 | } 47 | 48 | body { 49 | font-family: "Hack"; 50 | color: $white; 51 | background-color: $black; 52 | } 53 | 54 | a { 55 | text-decoration: none; 56 | color: $white; 57 | } 58 | 59 | 60 | // Typography 61 | 62 | h1, h2, h3, h4 { 63 | margin-top: 0.5em; 64 | margin-bottom: 0.5em; 65 | 66 | &:first-child { 67 | margin-top: 0; 68 | } 69 | 70 | &:last-child { 71 | margin-bottom: 0; 72 | } 73 | } 74 | 75 | p { 76 | margin: 1rem 0; 77 | 78 | a { 79 | border-bottom: 2px solid rgba($white, 0.2); 80 | padding-bottom: 2px; 81 | } 82 | 83 | &:first-child { 84 | margin-top: 0; 85 | } 86 | 87 | &:last-child { 88 | margin-bottom: 0; 89 | } 90 | } 91 | 92 | h4 { 93 | font-weight: 700; 94 | text-transform: uppercase; 95 | font-size: 13px; 96 | letter-spacing: 1px; 97 | } 98 | 99 | 100 | // Header 101 | 102 | .header { 103 | border-bottom: 1px solid rgba($white, 0.1); 104 | height: 80px; 105 | padding: 0 2.5rem; 106 | margin-bottom: 2.5rem; 107 | display: flex; 108 | align-items: center; 109 | } 110 | 111 | .header__logo, 112 | .header__title, 113 | .header__user { 114 | margin: 0; 115 | } 116 | 117 | .header__logo { 118 | // width: 30px; 119 | // height: 30px; 120 | // margin-right: 1.5rem; 121 | // background-image: url('../../assets/logo.svg'); 122 | background-position: center center; 123 | background-size: 30px; 124 | background-repeat: no-repeat; 125 | flex-shrink: 0; 126 | } 127 | 128 | .header__user { 129 | color: rgba($white, 0.5); 130 | margin-left: auto; 131 | } 132 | 133 | 134 | // Section 135 | 136 | .section { 137 | padding-left: 2.5rem; 138 | padding-right: 2.5rem; 139 | margin-bottom: 3rem; 140 | } 141 | 142 | .section__title { 143 | font-size: 22px; 144 | margin-bottom: 1rem; 145 | } 146 | 147 | .section__row { 148 | margin-bottom: 0.5rem; 149 | *:not(ul,li) { 150 | display: inline !important; 151 | } 152 | } 153 | 154 | 155 | // Buttons 156 | 157 | .btn { 158 | font-family: inherit; 159 | font-size: inherit; 160 | display: inline-block; 161 | border: 0; 162 | outline: 0; 163 | color: inherit; 164 | cursor: pointer; 165 | background: rgba($white, 0.1); 166 | box-shadow: 0px 1px 2px 0px rgba(#000, 0.1); 167 | border-radius: 4px; 168 | padding: 0.85rem 1rem; 169 | transition: background $time $curve; 170 | 171 | &:hover { 172 | background: rgba($white, 0.15); 173 | } 174 | } 175 | 176 | button, input, select { 177 | margin-right: 0.5rem; 178 | margin-bottom: 0.5rem; 179 | } 180 | 181 | .btn--primary { 182 | background-color: $blue; 183 | 184 | &:hover { 185 | background: mix($blue, $white, 90%); 186 | } 187 | } 188 | 189 | 190 | // Input 191 | 192 | input { 193 | font-family: inherit; 194 | font-size: inherit; 195 | display: inline-block; 196 | border: 0; 197 | outline: 0; 198 | color: $black; 199 | background: $white; 200 | border-radius: 4px; 201 | padding: 0.85rem 1rem; 202 | transition: background $time $curve; 203 | } 204 | 205 | 206 | // Select 207 | 208 | select { 209 | font-family: inherit; 210 | font-size: inherit; 211 | padding: 0.85rem 1rem; 212 | padding-right: 2.5rem; 213 | border-radius: 4px; 214 | background-color: $white; 215 | color: $black; 216 | -webkit-appearance: none; 217 | border: 0; 218 | outline: 0; 219 | background-image: url('../../assets/dropdown.svg'); 220 | background-position: right 1rem center; 221 | background-size: 10px; 222 | background-repeat: no-repeat; 223 | } 224 | 225 | .select-wrapper { 226 | // display: block; 227 | 228 | // select { 229 | // margin-right: 0.5rem; 230 | // } 231 | 232 | .btn { 233 | @extend .btn--primary; 234 | } 235 | } -------------------------------------------------------------------------------- /src/electron/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const r = require('ramda'); 4 | 5 | const electron = require('electron'); 6 | // Module to control application life. 7 | const app = electron.app; 8 | // Module to create native browser window. 9 | const BrowserWindow = electron.BrowserWindow; 10 | // Debug console and utils 11 | const Debug = require('electron-debug') 12 | // Send crash reports to a remote server 13 | const crashReporter = electron.crashReporter; 14 | 15 | // crashReporter.start({ 16 | // productName: 'DevTool', 17 | // companyName: 'YourCompany', 18 | // submitURL: 'https://your-domain.com/url-to-submit', 19 | // autoSubmit: true 20 | // }); 21 | 22 | Debug({ 23 | showDevTools: (process.env.NODE_ENV === "development") 24 | }); 25 | 26 | // Keep a global reference of the window object, if you don't, the window will 27 | // be closed automatically when the JavaScript object is garbage collected. 28 | let mainWindow; 29 | 30 | function createWindow () { 31 | // Create the browser window. 32 | mainWindow = new BrowserWindow({width: 800, height: 600}); 33 | 34 | // and load the index.html of the app. 35 | if (process.env.HOT) { 36 | mainWindow.loadURL('file://' + __dirname + '/../browser/dev_index.html'); 37 | } else { 38 | mainWindow.loadURL('file://' + __dirname + '/../browser/prod_index.html'); 39 | } 40 | 41 | // Emitted when the window is closed. 42 | mainWindow.on('closed', function() { 43 | // Dereference the window object, usually you would store windows 44 | // in an array if your app supports multi windows, this is the time 45 | // when you should delete the corresponding element. 46 | mainWindow = null; 47 | }); 48 | } 49 | 50 | // This method will be called when Electron has finished 51 | // initialization and is ready to create browser windows. 52 | app.on('ready', createWindow); 53 | 54 | // Quit when all windows are closed. 55 | app.on('window-all-closed', function () { 56 | // On OS X it is common for applications and their menu bar 57 | // to stay active until the user quits explicitly with Cmd + Q 58 | if (process.platform !== 'darwin') { 59 | app.quit(); 60 | } 61 | }); 62 | 63 | app.on('activate', function () { 64 | // On OS X it's common to re-create a window in the app when the 65 | // dock icon is clicked and there are no other windows open. 66 | if (mainWindow === null) { 67 | createWindow(); 68 | } 69 | }); 70 | 71 | const rpc = require('./rpc'); 72 | const shell = require('shelljs'); 73 | 74 | const withRc = (cmd) => "DIR=\"" + __dirname + "\"; source \"$DIR\"/../../devtoolrc.sh; " + cmd 75 | const inNewTab = (cmd) => withRc('tab "' + cmd + '"') 76 | 77 | // run command in a new tab 78 | rpc('run', function(cb, cmd) { 79 | shell.exec(inNewTab(cmd), (code, stdout, stderr) => { 80 | cb(stdout.trim() || stderr) 81 | }) 82 | }) 83 | 84 | // simply execute a command 85 | rpc('exec', function(cb, cmd) { 86 | shell.exec(withRc(cmd), (code, stdout, stderr) => { 87 | cb(stdout.trim() || stderr) 88 | }) 89 | }) 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/electron/rpc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const electron = require("electron"); 4 | 5 | const rpc = (name, func) => { 6 | const listener = function(event, id, arg) { 7 | const channel = `${name}-${id}` 8 | const callback = (result) => event.sender.send(channel, result) 9 | func(callback, arg) 10 | } 11 | electron.ipcMain.on(name, listener) 12 | return () => electron.ipcMain.removeListener(name, listener) 13 | } 14 | 15 | module.exports = rpc -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | source devtoolrc.sh 2 | 3 | # tab 1: start-server 4 | tab "node server.js" 5 | # tab 2: start-electron 6 | tab "HOT=1 NODE_ENV=development node_modules/.bin/electron src/electron/main.js --debug" 7 | # tab 3: main process debug console 8 | tab "node debug localhost:5858" 9 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | entry: { 6 | app: [ 7 | 'webpack-hot-middleware/client?path=http://localhost:8080/__webpack_hmr', 8 | './src/browser/app.js' 9 | ], 10 | }, 11 | output: { 12 | path: path.join(__dirname, 'dist'), 13 | filename: 'bundle.js', 14 | publicPath: 'http://localhost:8080/' 15 | }, 16 | module: { 17 | loaders: [ 18 | { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/, query: {presets: ['es2015', 'react', 'react-hmre']}}, 19 | { test: /\.css$/, loader: 'style-loader!css-loader' }, 20 | { test: /\.scss$/, loader: 'style-loader!css-loader!sass-loader'}, 21 | { test: /\.(svg|gif|png|jpe?g|ttf|woff2?|eot)$/, loader: 'url?limit=8182' }, 22 | ] 23 | }, 24 | plugins: [ 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoErrorsPlugin(), 27 | new webpack.ExternalsPlugin('commonjs', ['electron']) 28 | ], 29 | resolve: { 30 | root: [ 31 | path.resolve('src/browser'), 32 | path.resolve('src/assets'), 33 | ], 34 | modulesDirectories: [ 35 | 'node_modules' 36 | ] 37 | } 38 | } -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: { 7 | app: [ 8 | './src/browser/app.js' 9 | ], 10 | }, 11 | output: { 12 | path: path.join(__dirname, 'dist'), 13 | filename: 'bundle.js', 14 | publicPath: '' 15 | }, 16 | module: { 17 | loaders: [ 18 | { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/, query: {presets: ['es2015', 'react']}}, 19 | { test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader','css-loader') }, 20 | { test: /\.scss$/, loader: ExtractTextPlugin.extract('style-loader','css-loader!sass-loader') }, 21 | { test: /\.(svg|gif|png|jpe?g|ttf |woff2?|eot)$/, loader: 'url?limit=8182' }, 22 | ] 23 | }, 24 | plugins: [ 25 | new webpack.optimize.OccurenceOrderPlugin(), 26 | new webpack.ExternalsPlugin('commonjs', ['electron']), 27 | new webpack.optimize.UglifyJsPlugin({compressor: {screw_ie8: true, warnings: false }}), 28 | new ExtractTextPlugin('style.css', { allChunks: true }), 29 | ], 30 | resolve: { 31 | root: [ 32 | path.resolve('src/browser'), 33 | ], 34 | modulesDirectories: [ 35 | 'node_modules' 36 | ] 37 | } 38 | } --------------------------------------------------------------------------------