├── .gitignore ├── .eslintrc ├── package.json ├── README.md └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-lets-move", 3 | "main": "src/index.js", 4 | "version": "0.0.5", 5 | "license": "MIT", 6 | "repository": "tommoor/electron-lets-move", 7 | "dependencies": { 8 | "sudo-prompt": "^6.2.0" 9 | }, 10 | "devDependencies": { 11 | "eslint": "^3.12.0", 12 | "eslint-config-airbnb": "^13.0.0", 13 | "eslint-plugin-import": "^2.2.0", 14 | "eslint-plugin-jsx-a11y": "^2.2.3", 15 | "eslint-plugin-react": "^6.8.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This functionality is now baked into Electron and the project is archived. I suggest using the [new native API](https://github.com/electron/electron/blob/c0e9dbcc0081aec773016cd08a759929536aaae4/docs/api/app.md#appisinapplicationsfolder-macos).** 2 | 3 | [![npm version](https://badge.fury.io/js/electron-lets-move.svg)](https://badge.fury.io/js/electron-lets-move) 4 | 5 | # Electron LetsMove 6 | 7 | A module that offers to automatically move your Electron app to the Applications 8 | folder if opened from another location. Inspired by [LetsMove](https://github.com/potionfactory/LetsMove) for MacOS. 9 | 10 | ![Electron LetsMove](https://cloud.githubusercontent.com/assets/380914/21082066/31881d30-bf88-11e6-8110-9526168eb95b.png) 11 | 12 | ## Requirements 13 | 14 | This module is designed to be used within Electron on macOS, it can be included in a cross platform Electron app and is a no-op on the Windows and Linux platforms. 15 | 16 | 17 | ## Installation 18 | 19 | `npm install --save electron-lets-move` 20 | 21 | 22 | ## Usage 23 | 24 | You should call the `moveToApplications` method as soon as possible after the app 25 | ready event in the main process. Ideally before the user has any chance to interact 26 | with the application. 27 | 28 | `moveToApplications` returns a promise that will resolve when the application is 29 | in the correct location, the user asked not to move or an error occurred. You can 30 | also provide an optional node-style callback as the only parameter. 31 | 32 | 33 | ### ES5 34 | ```javascript 35 | const {app} = require('electron'); 36 | const {moveToApplications} = require('electron-lets-move'); 37 | 38 | app.on('ready', function() { 39 | moveToApplications(function(err, moved) { 40 | if (err) { 41 | // log error, something went wrong whilst moving the app. 42 | } 43 | if (!moved) { 44 | // the user asked not to move the app, it's up to the parent application 45 | // to store this information and not hassle them again. 46 | } 47 | 48 | // do the rest of your application startup 49 | }); 50 | }); 51 | ``` 52 | 53 | ### ES6 54 | ```javascript 55 | import {app} from 'electron'; 56 | import {moveToApplications} from 'electron-lets-move'; 57 | 58 | app.on('ready', async () => { 59 | try { 60 | const moved = await moveToApplications(); 61 | if (!moved) { 62 | // the user asked not to move the app, it's up to the parent application 63 | // to store this information and not hassle them again. 64 | } 65 | } catch (err) { 66 | // log error, something went wrong whilst moving the app. 67 | } 68 | 69 | // do the rest of your application startup 70 | }); 71 | ``` 72 | 73 | ## License 74 | 75 | Electron LetsMove is released under the MIT license. It is simple and easy to understand and places almost no restrictions on what you can do with it. 76 | [More Information](http://en.wikipedia.org/wiki/MIT_License) 77 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { app, dialog, shell } = require('electron'); 2 | const os = require('os'); 3 | const cp = require('child_process'); 4 | const sudo = require('sudo-prompt'); 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | 8 | const exePath = app.getPath('exe'); 9 | const rootApplicationPath = '/Applications'; 10 | const userApplicationPath = path.join(app.getPath('home'), 'Applications'); 11 | 12 | function getBundlePath() { 13 | const bundleExtension = '.app'; 14 | const parts = exePath.split(bundleExtension); 15 | return `${parts[0]}${bundleExtension}`; 16 | } 17 | 18 | function canWrite(filePath, callback) { 19 | fs.access(filePath, fs.W_OK, (err) => { 20 | callback(null, !err); 21 | }); 22 | } 23 | 24 | function isInApplicationsFolder() { 25 | return exePath.startsWith(rootApplicationPath) || exePath.startsWith(userApplicationPath); 26 | } 27 | 28 | function isInDownloadsFolder() { 29 | const downloadsPath = app.getPath('downloads'); 30 | return exePath.startsWith(downloadsPath); 31 | } 32 | 33 | function preferredInstallLocation() { 34 | if (fs.existsSync(userApplicationPath)) { 35 | return userApplicationPath; 36 | } 37 | return rootApplicationPath; 38 | } 39 | 40 | function moveToTrash(directory) { 41 | if (!fs.existsSync(directory)) return true; 42 | return shell.moveItemToTrash(directory); 43 | } 44 | 45 | function getDialogMessage(needsAuthorization) { 46 | let detail; 47 | 48 | detail = 'I can move myself to the Applications folder if you\'d like.'; 49 | if (needsAuthorization) { 50 | detail += ' Note that this will require an administrator password.'; 51 | } else if (isInDownloadsFolder()) { 52 | detail += ' This will keep your Downloads folder uncluttered.'; 53 | } 54 | return detail; 55 | } 56 | 57 | function moveToApplications(callback) { 58 | let resolve; 59 | let reject; 60 | const bundlePath = getBundlePath(); 61 | const fileName = path.basename(bundlePath); 62 | const installLocation = path.join(preferredInstallLocation(), fileName); 63 | 64 | // We return a promise so that the parent application can await the result. 65 | // Also support an optional callback for those that prefer a callback style. 66 | const deferred = new Promise((res, rej) => { 67 | resolve = (response) => { 68 | if (callback) callback(null, response); 69 | res(response); 70 | }; 71 | reject = (error) => { 72 | if (callback) callback(error); 73 | rej(error); 74 | }; 75 | }); 76 | 77 | // If we're not on MacOS then we're done here. 78 | if (os.platform() !== 'darwin') { 79 | resolve(true); 80 | return deferred; 81 | } 82 | 83 | // Skip if the application is already in some Applications folder 84 | if (isInApplicationsFolder()) { 85 | resolve(true); 86 | return deferred; 87 | } 88 | 89 | // Check if the install location needs administrator permissions 90 | canWrite(installLocation, (err, isWritable) => { 91 | const needsAuthorization = !isWritable; 92 | 93 | // show dialog requesting to move 94 | const detail = getDialogMessage(needsAuthorization); 95 | const chosen = dialog.showMessageBox({ 96 | type: 'question', 97 | buttons: ['Move to Applications', 'Do Not Move'], 98 | message: 'Move to Applications folder?', 99 | detail, 100 | }); 101 | 102 | // user chose to do nothing 103 | if (chosen !== 0) { 104 | resolve(false); 105 | return; 106 | } 107 | 108 | const moved = (error) => { 109 | if (error) { 110 | reject(error); 111 | return; 112 | } 113 | 114 | // open the moved app 115 | const execName = path.basename(process.execPath); 116 | const execPath = path.join(installLocation, 'Contents', 'MacOS', execName); 117 | const child = cp.spawn(execPath, [], { 118 | detached: true, 119 | stdio: 'ignore', 120 | }); 121 | child.unref(); 122 | 123 | // quit the app immediately 124 | app.exit(); 125 | }; 126 | 127 | // move any existing application bundle to the trash 128 | if (!moveToTrash(installLocation)) { 129 | reject(new Error('Failed to move existing application to Trash, it may be in use.')); 130 | return; 131 | } 132 | 133 | // move the application bundle 134 | const command = `mv ${bundlePath} ${installLocation}`; 135 | if (needsAuthorization) { 136 | sudo.exec(command, { name: app.getName() }, moved); 137 | } else { 138 | cp.exec(command, moved); 139 | } 140 | }); 141 | 142 | return deferred; 143 | } 144 | 145 | module.exports = { 146 | moveToApplications, 147 | }; 148 | --------------------------------------------------------------------------------