├── .gitignore ├── LICENSE.md ├── README.md ├── lib ├── atom-xcode.js ├── constants.js ├── ios │ └── parse-simulators-list.js ├── ui │ ├── base-view.js │ ├── bottom-panel-action.js │ ├── bottom-panel.js │ ├── build-view.js │ ├── selected-simulator.js │ └── simulator-dropdown.js └── utils.js ├── package.json └── styles └── atom-xcode.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | atom-xcode 2 | ======= 3 | 4 | > Native iOS developers have amazing Xcode - let's bridge the gap and make Atom our to go choice 5 | 6 | ### Instalation 7 | 8 | ```bash 9 | $ apm install atom-xcode 10 | ``` 11 | ### Guide 12 | 13 | After successful installation, open up your react-native project and check the bottom section (just where the atom-linter is located). Clicking the device name will open up another view allowing you to choose the device to run the app on. 14 | 15 | 16 | 17 | The terminal will automatically disappear when CLI finishes without issues, otherwise it stays opened and will highlight the error. You can close it with "ESC" key. 18 | 19 | ### Roadmap 20 | 21 | The current version is a proof of concept to demonstrate we can really hack Atom to fit our needs in less than a day. Below is the list of features I've been thinking about implementing so far. 22 | 23 | - [ ] Add ability to select build targets 24 | - [ ] Add `Xcode` menu item with all options available 25 | - [ ] Add build command 26 | - [ ] Improve terminal colors 27 | - [ ] Improve project detection and support Android as well 28 | - [ ] Add settings and make this configureable 29 | - [ ] Decouple from RN cli and ideally ship with own library 30 | - [ ] Support multiple projects opened in Atom (for now we take the first one) 31 | 32 | Please feel free to submit your ideas by making a new issue. 33 | 34 | ### FAQ 35 | 36 | #### Command finishes w/o errors, but no Simulator has started 37 | 38 | Try choosing different device. This error will be gone when either https://github.com/facebook/react-native/pull/5978 gets merged, or when we move the `run-ios` into this package (or `rnpm`, who knows) 39 | -------------------------------------------------------------------------------- /lib/atom-xcode.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | const BottomPanel = require('./ui/bottom-panel'); 4 | 5 | class AtomXcode { 6 | bottomPanel = null; 7 | 8 | activate = (state) => { 9 | this.bottomPanel = BottomPanel.create(state); 10 | }; 11 | 12 | consumeStatusBar = (statusBar) => { 13 | statusBar.addLeftTile({ 14 | item: this.bottomPanel, 15 | priority: -1000, 16 | }); 17 | }; 18 | 19 | }; 20 | 21 | module.exports = new AtomXcode(); 22 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | ACTION_RUN: 'icon-triangle-right', 5 | ACTION_STOP: 'icon-primitive-square', 6 | CLI_PATH: path.join(__dirname, '../', 'node_modules', '.bin', 'react-native'), 7 | }; 8 | -------------------------------------------------------------------------------- /lib/ios/parse-simulators-list.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /** 4 | * https://github.com/facebook/react-native/blob/master/local-cli/runIOS/parseIOSSimulatorsList.js 5 | */ 6 | 7 | /** 8 | * Copyright (c) 2015-present, Facebook, Inc. 9 | * All rights reserved. 10 | * 11 | * This source code is licensed under the BSD-style license found in the 12 | * LICENSE file in the root directory of this source tree. An additional grant 13 | * of patent rights can be found in the PATENTS file in the same directory. 14 | * 15 | * @flow 16 | */ 17 | type IOSSimulatorInfo = { 18 | name: string; 19 | udid: string; 20 | version: string; 21 | } 22 | 23 | /** 24 | * Parses the output of `xcrun simctl list devices` command 25 | */ 26 | function parseIOSSimulatorsList(text: string): Array { 27 | const devices = []; 28 | var currentOS = null; 29 | 30 | text.split('\n').forEach((line) => { 31 | var section = line.match(/^-- (.+) --$/); 32 | if (section) { 33 | var header = section[1].match(/^iOS (.+)$/); 34 | 35 | if (header) { 36 | currentOS = header[1]; 37 | } else { 38 | currentOS = null; 39 | } 40 | 41 | return; 42 | } 43 | 44 | const device = line.match(/^[ ]*([^()]+) \(([^()]+)\)/); 45 | if (device && currentOS) { 46 | var name = device[1]; 47 | var udid = device[2]; 48 | devices.push({udid, name, version: currentOS}); 49 | } 50 | }); 51 | 52 | return devices; 53 | } 54 | 55 | module.exports = parseIOSSimulatorsList; 56 | -------------------------------------------------------------------------------- /lib/ui/base-view.js: -------------------------------------------------------------------------------- 1 | module.exports = function (htmlElement, name) { 2 | htmlElement.create = (...args) => { 3 | const el = document.createElement(name); 4 | if (typeof el.prepare === 'function') { 5 | el.prepare(...args); 6 | } 7 | return el; 8 | }; 9 | 10 | return document.registerElement(name, { 11 | prototype: htmlElement.prototype 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/ui/bottom-panel-action.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | const { Disposable } = require('atom'); 4 | const { ACTION_RUN, ACTION_STOP } = require('../constants'); 5 | const baseView = require('./base-view'); 6 | 7 | /** 8 | * Bottom panel action 9 | */ 10 | class BottomPanelAction extends HTMLElement { 11 | 12 | /** Click subscription **/ 13 | clickSubscription = null; 14 | 15 | prepare({ type, handler }) { 16 | this.classList.add('btn', 'btn-sm', 'icon', type); 17 | 18 | this.addEventListener('click', handler); 19 | this.clickSubscription = new Disposable(() => this.removeEventListener('click', handler)); 20 | } 21 | 22 | destroy() { 23 | if (this.clickSubscription) { 24 | this.clickSubscription.dispose(); 25 | } 26 | } 27 | }; 28 | 29 | module.exports = baseView(BottomPanelAction, 'xcatom-bottom-panel-action'); 30 | -------------------------------------------------------------------------------- /lib/ui/bottom-panel.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | const { ACTION_RUN, CLI_PATH, ACTION_STOP } = require('../constants'); 4 | const BottomPanelAction = require('./bottom-panel-action'); 5 | const baseView = require('./base-view'); 6 | const { getPath } = require('consistent-path'); 7 | const { getRootFolder, isRNProject } = require('../utils'); 8 | const kill = require('tree-kill'); 9 | const child_process = require('child_process'); 10 | const SelectedSimulator = require('./selected-simulator'); 11 | const BuildView = require('./build-view'); 12 | const { CompositeDisposable } = require('atom'); 13 | 14 | /** 15 | * Bottom panel container 16 | * 17 | * Contains 3 components - start/stop actions and an array of 18 | * available simulators 19 | */ 20 | class BottomPanel extends HTMLElement { 21 | disposables = null; 22 | 23 | selectedSimulator = null; 24 | 25 | /** Build view with the current stdout stderr **/ 26 | buildView = null; 27 | 28 | /** Current child_process */ 29 | ps = null; 30 | 31 | createdCallback() { 32 | this.disposables = new CompositeDisposable(); 33 | 34 | this.disposables.add(atom.project.onDidChangePaths(() => this.maybeRender())); 35 | 36 | this.maybeRender(); 37 | } 38 | 39 | maybeRender() { 40 | if (!isRNProject()) { 41 | return; 42 | } 43 | 44 | const runProject = BottomPanelAction.create({ 45 | type: ACTION_RUN, 46 | handler: () => this.runProject() 47 | }); 48 | 49 | const stopProject = BottomPanelAction.create({ 50 | type: ACTION_STOP, 51 | handler: () => this.stopProject() 52 | }); 53 | 54 | this.buildView = BuildView.create(); 55 | 56 | this.selectedSimulator = SelectedSimulator.create(); 57 | 58 | this.appendChild(runProject); 59 | this.appendChild(stopProject); 60 | this.appendChild(this.selectedSimulator); 61 | } 62 | 63 | stopProject() { 64 | if (!this.ps) return; 65 | kill(this.ps.pid); 66 | } 67 | 68 | destroy() { 69 | this.disposables.dispose(); 70 | } 71 | 72 | /** 73 | * Runs Xcode project 74 | * 75 | * Some targets might not be running (e.g. iPhone 6s), see: https://github.com/facebook/react-native/pull/5978 76 | */ 77 | runProject() { 78 | if (this.ps) return; 79 | 80 | const simulator = this.selectedSimulator.getSelectedItem(); 81 | 82 | const args = ['run-ios', '--simulator', simulator.name]; 83 | 84 | const ps = this.ps = child_process.spawn(CLI_PATH, args, { 85 | encoding: 'utf8', 86 | stdio: 'pipe', 87 | env: Object.assign({}, process.env, { PATH: getPath() }), 88 | cwd: getRootFolder() 89 | }); 90 | 91 | this.buildView.attach(); 92 | 93 | ps.stdout.on('data', data => this.buildView.append(data)); 94 | ps.stderr.on('data', data => this.buildView.append(data, true)); 95 | 96 | ps.on('close', () => { 97 | if (this.buildView.hasErrors()) { 98 | this.buildView.detachOnEsc(); 99 | } else { 100 | this.buildView.detach(); 101 | } 102 | this.ps = null; 103 | }); 104 | } 105 | }; 106 | 107 | module.exports = baseView(BottomPanel, 'xcatom-bottom-panel'); 108 | -------------------------------------------------------------------------------- /lib/ui/build-view.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | const { Disposable } = require('atom'); 4 | const baseView = require('./base-view'); 5 | 6 | /** Xcode build errors always contain BUILD FAILED string **/ 7 | const ERROR_MARKER = 'BUILD FAILED'; 8 | 9 | /** 10 | * BuildView showing real-time progress of any build task 11 | */ 12 | class BuildView extends HTMLElement { 13 | /** Key press subscription - set only when build contains errors **/ 14 | keyPressSubscription = null; 15 | 16 | /** Current panel **/ 17 | panel = null; 18 | 19 | createdCallback() { 20 | this.classList.add('tool-panel', 'panel-bottom'); 21 | 22 | this.stdout = document.createElement('div'); 23 | this.stdout.classList.add('output', 'panel-body'); 24 | 25 | this.appendChild(this.stdout); 26 | } 27 | 28 | /** 29 | * Attaches view to current panel and cleans up stdout 30 | */ 31 | attach() { 32 | if (this.panel) { 33 | this.panel.destroy(); 34 | } 35 | 36 | this.stdout.innerHTML = ''; 37 | 38 | this.panel = atom.workspace.addBottomPanel({ item: this }); 39 | 40 | this.focus(); 41 | } 42 | 43 | hasErrors() { 44 | return this.stdout.innerHTML.indexOf(``) !== -1; 45 | } 46 | 47 | highlightError(line) { 48 | return `${line}`; 49 | } 50 | 51 | convertLineBreakes(line) { 52 | return line.replace(/\r?\n/g, '
'); 53 | } 54 | 55 | /** 56 | * Appends buffer to stdout 57 | */ 58 | append(buffer, isError) { 59 | const line = this.convertLineBreakes(buffer.toString('utf8')); 60 | this.stdout.innerHTML += isError ? this.highlightError(line) : line; 61 | this.stdout.scrollTop = this.stdout.scrollHeight; 62 | } 63 | 64 | /** 65 | * Attaches keyUp listener that will detach the preview on ESC press 66 | */ 67 | detachOnEsc() { 68 | if (this.keyPressSubscription) return; 69 | 70 | const handler = this.onKeyUp.bind(this); 71 | document.addEventListener('keyup', handler); 72 | this.keyPressSubscription = new Disposable(() => document.removeEventListener('keyup', handler)); 73 | } 74 | 75 | onKeyUp({ keyCode }) { 76 | if (keyCode === 27) { 77 | this.detach(); 78 | } 79 | } 80 | 81 | /** 82 | * Detaches the window 83 | */ 84 | detach() { 85 | const currentView = atom.views.getView(atom.workspace); 86 | 87 | if (currentView) { 88 | currentView.focus(); 89 | } 90 | 91 | this.panel.destroy(); 92 | this.panel = null; 93 | 94 | if (this.keyPressSubscription) { 95 | this.keyPressSubscription.dispose(); 96 | } 97 | } 98 | 99 | } 100 | 101 | module.exports = baseView(BuildView, 'xcatom-build-view'); 102 | -------------------------------------------------------------------------------- /lib/ui/selected-simulator.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | const { Disposable } = require('atom'); 4 | const baseView = require('./base-view'); 5 | const child_process = require('child_process'); 6 | const parseIOSSimulatorsList = require('../ios/parse-simulators-list'); 7 | const SimulatorDropdown = require('./simulator-dropdown'); 8 | 9 | /** 10 | * Component displaying selected simulator 11 | */ 12 | class SelectedSimulator extends HTMLElement { 13 | 14 | /** Disposable click subscription **/ 15 | clickSubscription = null; 16 | 17 | /** Selected simulator **/ 18 | selectedItem = null; 19 | 20 | /** with current simulator **/ 21 | simulatorNode = null; 22 | 23 | /** dropdown **/ 24 | dropdown = null; 25 | 26 | /** list of devices **/ 27 | list = null; 28 | 29 | /** 30 | * Gets available simulators and returns them asynchronously 31 | */ 32 | getSimulators(cb) { 33 | child_process.execFile('xcrun', ['simctl', 'list', 'devices'], {encoding: 'utf8'}, (err, stdout) => { 34 | if (err) { 35 | cb(err, null); 36 | } else { 37 | cb(null, parseIOSSimulatorsList(stdout)); 38 | } 39 | }); 40 | } 41 | 42 | /** 43 | * Returns selected simulator - which is either the default RN device 44 | * (when first run), or previously selected device matched by the UUID 45 | */ 46 | getSelectedItem(list = this.list) { 47 | return list.find(device => this.selectedItem 48 | ? device.udid === this.selectedItem.udid 49 | : device.name === 'iPhone 6' 50 | ); 51 | } 52 | 53 | /** 54 | * Sets selected item and updates the markup 55 | */ 56 | setSelectedItem(item) { 57 | this.selectedItem = item; 58 | this.simulatorNode.textContent = item.name; 59 | } 60 | 61 | /** 62 | * When this item is created, we asynchronously load 63 | * available devices and initialize SimulatorDropdown 64 | */ 65 | createdCallback() { 66 | this.dropdown = new SimulatorDropdown({ 67 | onSelected: (i) => this.setSelectedItem(i) 68 | }); 69 | 70 | this.simulatorNode = document.createElement('a'); 71 | 72 | this.getSimulators((err, list) => { 73 | if (err) return; 74 | 75 | this.list = list; 76 | 77 | this.setSelectedItem(this.getSelectedItem(list)); 78 | }); 79 | 80 | this.appendChild(this.simulatorNode); 81 | 82 | this.setupEvents(); 83 | } 84 | 85 | /** 86 | * Attach listeners & events, most importantly, display a dropdown 87 | * on every click 88 | */ 89 | setupEvents() { 90 | const handler = () => { 91 | this.dropdown.setItems(this.list); 92 | this.dropdown.show(); 93 | } 94 | 95 | this.simulatorNode.addEventListener('click', handler); 96 | this.clickSubscription = new Disposable( 97 | () => this.simulatorNode.removeEventListener('click', handler) 98 | ); 99 | } 100 | 101 | /** 102 | * Remove subscriptions 103 | */ 104 | destroy() { 105 | if (this.clickSubscription) { 106 | this.clickSubscription.dispose(); 107 | } 108 | } 109 | }; 110 | 111 | module.exports = baseView(SelectedSimulator, 'xcatom-selected-simulator'); 112 | -------------------------------------------------------------------------------- /lib/ui/simulator-dropdown.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | const { SelectListView } = require('atom-space-pen-views'); 4 | 5 | class SimulatorDropdown extends SelectListView { 6 | panel = null; 7 | 8 | constructor(opts) { 9 | super(); 10 | this.opts = opts; 11 | } 12 | 13 | show() { 14 | this.storeFocusedElement(); 15 | this.panel = atom.workspace.addModalPanel({ item: this }); 16 | this.focusFilterEditor(); 17 | } 18 | 19 | getFilterKey(item) { 20 | return "name"; 21 | } 22 | 23 | viewForItem(item) { 24 | return `
  • ${item.name}
  • ` 25 | } 26 | 27 | getEmptyMessage() { 28 | return "No Simulators available"; 29 | } 30 | 31 | cancelled() { 32 | this.panel.destroy(); 33 | } 34 | 35 | confirmed(item) { 36 | const { onSelected } = this.opts; 37 | if (onSelected) onSelected(item); 38 | this.cancel() 39 | } 40 | 41 | } 42 | 43 | module.exports = SimulatorDropdown; 44 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | /** 7 | * Gets first root folder opened in Atom 8 | * 9 | * @todo Support multiple root folders etc. 10 | */ 11 | exports.getRootFolder = () => atom.project.getPaths()[0]; 12 | 13 | /** 14 | * Returns true if current project is RN 15 | */ 16 | exports.isRNProject = () => { 17 | const rootFolder = exports.getRootFolder(); 18 | if (!rootFolder) return false; 19 | return fs.existsSync(path.join(rootFolder, 'node_modules', 'react-native')); 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-xcode", 3 | "main": "./lib/atom-xcode", 4 | "version": "0.1.3", 5 | "description": "Native iOS developers have amazing Xcode - let's bridge the gap and make Atom our to go choice", 6 | "keywords": [ 7 | "react-native", 8 | "xcode", 9 | "ios", 10 | "rnpm" 11 | ], 12 | "repository": "https://github.com/grabbou/atom-xcode", 13 | "license": "MIT", 14 | "engines": { 15 | "atom": ">=1.0.0 <2.0.0" 16 | }, 17 | "dependencies": { 18 | "atom-space-pen-views": "^2.0.0", 19 | "consistent-path": "^1.1.1", 20 | "react-native-cli": "^0.1.10", 21 | "tree-kill": "^1.0.0" 22 | }, 23 | "consumedServices": { 24 | "status-bar": { 25 | "versions": { 26 | "^1.0.0": "consumeStatusBar" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /styles/atom-xcode.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | xcatom-bottom-panel { 8 | margin-right: 20px; 9 | } 10 | 11 | xcatom-bottom-panel-action { 12 | font-size: 1.2em!important; 13 | line-height: 1.2em!important; 14 | padding: 0 0.2em!important; 15 | height: 1.5em!important; 16 | 17 | &:not(:last-child) { 18 | margin-right: 0.2em; 19 | } 20 | 21 | &:active { 22 | background: transparent; 23 | } 24 | 25 | &.active { 26 | color: @text-color-highlight; 27 | background: @button-background-color; 28 | } 29 | } 30 | 31 | xcatom-selected-simulator { 32 | margin-left: 5px; 33 | } 34 | 35 | xcatom-build-view { 36 | height: 150px; 37 | padding: 10px; 38 | 39 | .output { 40 | width: 100%; 41 | color: white; 42 | height: 100%; 43 | overflow-y: scroll; 44 | background: #000; 45 | padding: 20px; 46 | } 47 | } 48 | --------------------------------------------------------------------------------