├── .gitignore ├── LICENSE.md ├── README.md ├── apm ├── README.md └── package.json ├── build ├── .npmrc ├── Gruntfile.coffee ├── README.md ├── package.json └── tasks │ ├── build-task.coffee │ ├── clean-task.coffee │ ├── codesign-task.coffee │ ├── copy-info-plist-task.coffee │ ├── dump-symbols-task.coffee │ ├── install-task.coffee │ ├── mkdeb-task.coffee │ ├── mkrpm-task.coffee │ ├── nof-task.coffee │ ├── output-build-filetypes.coffee │ ├── output-disk-space.coffee │ ├── output-for-loop-returns.coffee │ ├── output-long-paths-task.coffee │ ├── output-module-counts.coffee │ ├── publish-build-task.coffee │ ├── set-exe-icon-task.coffee │ ├── set-version-task.coffee │ └── task-helpers.coffee ├── package.json ├── resources ├── mac │ ├── helper-Info.plist │ ├── messenger-Info.plist │ ├── messenger.icns │ └── speakeasy.pem ├── messenger.png └── win │ ├── loading.gif │ └── messenger.ico ├── script ├── bootstrap ├── bootstrap.cmd ├── build ├── build.cmd ├── cibuild ├── clean ├── clean.cmd ├── copy-folder.cmd ├── create-shortcut.cmd ├── grunt ├── grunt.cmd ├── set-version └── utils │ ├── child-process-wrapper.js │ ├── clean-merged-branches │ ├── clean-open-with-menu │ ├── compile-main-to-app │ ├── fix-author │ ├── translate-crash-log-addresses.coffee │ └── verify-requirements.js ├── src ├── main.js └── updates.coffee └── static ├── app.html ├── css └── app.css ├── images └── messenger.icns └── js ├── app.coffee └── menu.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | atom-shell 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2015 Marek Hrabe, http://marekhrabe.com/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Messenger for Mac 2 | 3 | This is a 5-minute experiment to make an **unofficial** Mac App for Messenger. 4 | 5 | > Works for me, may work for you :metal: 6 | 7 | MIT licensed, [Electron (formerly atom-shell)](https://github.com/atom/atom-shell) based app. 8 | 9 | # Download 10 | 11 | [](https://github.com/marekhrabe/messenger/releases) 12 | 13 | Requires OS X 10.8+ 14 | 15 | # Desktop notifications 16 | 17 | Go to Settings in Messenger and turn them on. They should just work 18 | 19 | ![](https://upx.cz/23K) 20 | -------------------------------------------------------------------------------- /apm/README.md: -------------------------------------------------------------------------------- 1 | This folder is where [apm](https://github.com/atom/apm) is installed to so that 2 | it is bundled with Atom. 3 | -------------------------------------------------------------------------------- /apm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-bundled-apm", 3 | "description": "Atom's bundled apm", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/atom/atom.git" 7 | }, 8 | "dependencies": { 9 | "atom-package-manager": "0.157.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /build/.npmrc: -------------------------------------------------------------------------------- 1 | cache = ~/.atom/.npm 2 | -------------------------------------------------------------------------------- /build/Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | os = require 'os' 4 | 5 | # Add support for obselete APIs of vm module so we can make some third-party 6 | # modules work under node v0.11.x. 7 | require 'vm-compatibility-layer' 8 | 9 | _ = require 'underscore-plus' 10 | 11 | packageJson = require '../package.json' 12 | 13 | # Shim harmony collections in case grunt was invoked without harmony 14 | # collections enabled 15 | _.extend(global, require('harmony-collections')) unless global.WeakMap? 16 | 17 | module.exports = (grunt) -> 18 | grunt.loadNpmTasks('grunt-contrib-coffee') 19 | grunt.loadNpmTasks('grunt-shell') 20 | grunt.loadNpmTasks('grunt-download-atom-shell') 21 | grunt.loadNpmTasks('grunt-atom-shell-installer') 22 | grunt.loadTasks('tasks') 23 | 24 | # This allows all subsequent paths to the relative to the root of the repo 25 | grunt.file.setBase(path.resolve('..')) 26 | 27 | if not grunt.option('verbose') 28 | grunt.log.writeln = (args...) -> grunt.log 29 | grunt.log.write = (args...) -> grunt.log 30 | 31 | [major, minor, patch] = packageJson.version.split('.') 32 | tmpDir = os.tmpdir() 33 | appName = if process.platform is 'darwin' then 'Messenger.app' else 'Messenger' 34 | buildDir = grunt.option('build-dir') ? path.join(tmpDir, 'atom-build') 35 | buildDir = path.resolve(buildDir) 36 | installDir = grunt.option('install-dir') 37 | 38 | home = if process.platform is 'win32' then process.env.USERPROFILE else process.env.HOME 39 | atomShellDownloadDir = path.join(home, '.atom', 'atom-shell') 40 | 41 | symbolsDir = path.join(buildDir, 'Atom.breakpad.syms') 42 | shellAppDir = path.join(buildDir, appName) 43 | if process.platform is 'win32' 44 | contentsDir = shellAppDir 45 | appDir = path.join(shellAppDir, 'resources', 'app') 46 | installDir ?= path.join(process.env.ProgramFiles, appName) 47 | killCommand = 'taskkill /F /IM messenger.exe' 48 | else if process.platform is 'darwin' 49 | contentsDir = path.join(shellAppDir, 'Contents') 50 | appDir = path.join(contentsDir, 'Resources', 'app') 51 | installDir ?= path.join('/Applications', appName) 52 | killCommand = 'pkill -9 Messenger' 53 | else 54 | contentsDir = shellAppDir 55 | appDir = path.join(shellAppDir, 'resources', 'app') 56 | installDir ?= process.env.INSTALL_PREFIX ? '/usr/local' 57 | killCommand ='pkill -9 messenger' 58 | 59 | installDir = path.resolve(installDir) 60 | 61 | coffeeConfig = 62 | glob_to_multiple: 63 | expand: true 64 | src: [ 65 | 'src/**/*.coffee' 66 | 'exports/**/*.coffee' 67 | 'static/**/*.coffee' 68 | ] 69 | dest: appDir 70 | ext: '.js' 71 | 72 | grunt.initConfig 73 | pkg: grunt.file.readJSON('package.json') 74 | 75 | atom: {appDir, appName, symbolsDir, buildDir, contentsDir, installDir, shellAppDir} 76 | 77 | docsOutputDir: 'docs/output' 78 | 79 | coffee: coffeeConfig 80 | 81 | 'download-atom-shell': 82 | version: packageJson.atomShellVersion 83 | outputDir: 'atom-shell' 84 | downloadDir: atomShellDownloadDir 85 | rebuild: true # rebuild native modules after atom-shell is updated 86 | token: process.env.ATOM_ACCESS_TOKEN 87 | 88 | 'create-windows-installer': 89 | appDirectory: shellAppDir 90 | outputDirectory: path.join(buildDir, 'installer') 91 | authors: 'Marek Hrabě' 92 | loadingGif: path.resolve(__dirname, '..', 'resources', 'win', 'loading.gif') 93 | iconUrl: 'https://raw.githubusercontent.com/marekhrabe/messenger/master/resources/win/messenger.ico' 94 | setupIcon: path.resolve(__dirname, '..', 'resources', 'win', 'messenger.ico') 95 | remoteReleases: 'https://atom.io/api/updates' 96 | 97 | shell: 98 | 'kill-atom': 99 | command: killCommand 100 | options: 101 | stdout: false 102 | stderr: false 103 | failOnError: false 104 | 105 | grunt.registerTask('compile', ['coffee']) 106 | 107 | ciTasks = ['output-disk-space', 'download-atom-shell', 'download-atom-shell-chromedriver', 'build'] 108 | ciTasks.push('dump-symbols') if process.platform isnt 'win32' 109 | ciTasks.push('set-version', 'check-licenses') 110 | ciTasks.push('mkdeb') if process.platform is 'linux' 111 | ciTasks.push('create-windows-installer') if process.platform is 'win32' 112 | ciTasks.push('codesign') 113 | ciTasks.push('publish-build') 114 | grunt.registerTask('ci', ciTasks) 115 | 116 | defaultTasks = ['download-atom-shell', 'download-atom-shell-chromedriver', 'build', 'set-version'] 117 | defaultTasks.push 'install' unless process.platform is 'linux' 118 | grunt.registerTask('default', defaultTasks) 119 | -------------------------------------------------------------------------------- /build/README.md: -------------------------------------------------------------------------------- 1 | # Atom Build 2 | 3 | This folder contains the grunt configuration and tasks to build Atom. 4 | 5 | It was moved from the root of the repository so that any native modules used 6 | would be compiled against node's v8 headers since anything stored in 7 | `node_modules` at the root of the repo is compiled against atom's v8 headers. 8 | 9 | New build dependencies should be added to the `package.json` file located in 10 | this folder. 11 | -------------------------------------------------------------------------------- /build/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-build", 3 | "description": "Atom build", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/atom/atom.git" 7 | }, 8 | "dependencies": { 9 | "async": "~0.2.9", 10 | "donna": "1.0.10", 11 | "formidable": "~1.0.14", 12 | "fs-plus": "2.x", 13 | "github-releases": "~0.2.0", 14 | "grunt": "~0.4.1", 15 | "grunt-atom-shell-installer": "^0.28.0", 16 | "grunt-cli": "~0.1.9", 17 | "grunt-coffeelint": "git+https://github.com/atom/grunt-coffeelint.git#cfb99aa99811d52687969532bd5a98011ed95bfe", 18 | "grunt-contrib-coffee": "~0.12.0", 19 | "grunt-contrib-csslint": "~0.1.2", 20 | "grunt-contrib-less": "~0.8.0", 21 | "grunt-cson": "0.14.0", 22 | "grunt-download-atom-shell": "~0.12.0", 23 | "grunt-lesslint": "0.13.0", 24 | "grunt-peg": "~1.1.0", 25 | "grunt-shell": "~0.3.1", 26 | "harmony-collections": "~0.3.8", 27 | "legal-eagle": "~0.9.0", 28 | "minidump": "~0.8", 29 | "npm": "2.5.1", 30 | "rcedit": "~0.3.0", 31 | "request": "~2.27.0", 32 | "rimraf": "~2.2.2", 33 | "runas": "^2", 34 | "tello": "1.0.4", 35 | "temp": "~0.8.1", 36 | "underscore-plus": "1.x", 37 | "unzip": "~0.1.9", 38 | "vm-compatibility-layer": "~0.1.0", 39 | "webdriverio": "^2.4.5" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /build/tasks/build-task.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | _ = require 'underscore-plus' 4 | 5 | module.exports = (grunt) -> 6 | {cp, isAtomPackage, mkdir, rm} = require('./task-helpers')(grunt) 7 | 8 | grunt.registerTask 'build', 'Build the application', -> 9 | shellAppDir = grunt.config.get('atom.shellAppDir') 10 | buildDir = grunt.config.get('atom.buildDir') 11 | appDir = grunt.config.get('atom.appDir') 12 | 13 | rm shellAppDir 14 | rm path.join(buildDir, 'installer') 15 | mkdir path.dirname(buildDir) 16 | 17 | if process.platform is 'darwin' 18 | cp 'atom-shell/Atom.app', shellAppDir, filter: /default_app/ 19 | else 20 | cp 'atom-shell', shellAppDir, filter: /default_app/ 21 | 22 | mkdir appDir 23 | 24 | cp 'package.json', path.join(appDir, 'package.json') 25 | 26 | packageDirectories = [] 27 | nonPackageDirectories = [ 28 | # 'vendor' 29 | # 'resources' 30 | ] 31 | 32 | {devDependencies} = grunt.file.readJSON('package.json') 33 | for child in fs.readdirSync('node_modules') 34 | directory = path.join('node_modules', child) 35 | if isAtomPackage(directory) 36 | packageDirectories.push(directory) 37 | else 38 | nonPackageDirectories.push(directory) 39 | 40 | # Put any paths here that shouldn't end up in the built Atom.app 41 | # so that it doesn't becomes larger than it needs to be. 42 | ignoredPaths = [ 43 | path.join('git-utils', 'deps') 44 | path.join('oniguruma', 'deps') 45 | path.join('less', 'dist') 46 | path.join('bootstrap', 'docs') 47 | path.join('bootstrap', '_config.yml') 48 | path.join('bootstrap', '_includes') 49 | path.join('bootstrap', '_layouts') 50 | path.join('npm', 'doc') 51 | path.join('npm', 'html') 52 | path.join('npm', 'man') 53 | path.join('npm', 'node_modules', '.bin', 'beep') 54 | path.join('npm', 'node_modules', '.bin', 'clear') 55 | path.join('npm', 'node_modules', '.bin', 'starwars') 56 | path.join('pegjs', 'examples') 57 | path.join('jasmine-reporters', 'ext') 58 | path.join('jasmine-node', 'node_modules', 'gaze') 59 | path.join('jasmine-node', 'spec') 60 | path.join('node_modules', 'nan') 61 | path.join('build', 'binding.Makefile') 62 | path.join('build', 'config.gypi') 63 | path.join('build', 'gyp-mac-tool') 64 | path.join('build', 'Makefile') 65 | path.join('build', 'Release', 'obj.target') 66 | path.join('build', 'Release', 'obj') 67 | path.join('build', 'Release', '.deps') 68 | path.join('vendor', 'apm') 69 | path.join('resources', 'linux') 70 | path.join('resources', 'mac') 71 | path.join('resources', 'win') 72 | 73 | # These are only require in dev mode when the grammar isn't precompiled 74 | path.join('atom-keymap', 'node_modules', 'loophole') 75 | path.join('atom-keymap', 'node_modules', 'pegjs') 76 | path.join('atom-keymap', 'node_modules', '.bin', 'pegjs') 77 | path.join('snippets', 'node_modules', 'loophole') 78 | path.join('snippets', 'node_modules', 'pegjs') 79 | path.join('snippets', 'node_modules', '.bin', 'pegjs') 80 | 81 | '.DS_Store' 82 | '.jshintrc' 83 | '.npmignore' 84 | '.pairs' 85 | '.travis.yml' 86 | ] 87 | ignoredPaths = ignoredPaths.map (ignoredPath) -> _.escapeRegExp(ignoredPath) 88 | 89 | # Add .* to avoid matching hunspell_dictionaries. 90 | ignoredPaths.push "#{_.escapeRegExp(path.join('spellchecker', 'vendor', 'hunspell') + path.sep)}.*" 91 | ignoredPaths.push "#{_.escapeRegExp(path.join('build', 'Release') + path.sep)}.*\\.pdb" 92 | 93 | # Ignore *.cc and *.h files from native modules 94 | ignoredPaths.push "#{_.escapeRegExp(path.join('ctags', 'src') + path.sep)}.*\\.(cc|h)*" 95 | ignoredPaths.push "#{_.escapeRegExp(path.join('git-utils', 'src') + path.sep)}.*\\.(cc|h)*" 96 | ignoredPaths.push "#{_.escapeRegExp(path.join('keytar', 'src') + path.sep)}.*\\.(cc|h)*" 97 | ignoredPaths.push "#{_.escapeRegExp(path.join('nslog', 'src') + path.sep)}.*\\.(cc|h)*" 98 | ignoredPaths.push "#{_.escapeRegExp(path.join('oniguruma', 'src') + path.sep)}.*\\.(cc|h)*" 99 | ignoredPaths.push "#{_.escapeRegExp(path.join('pathwatcher', 'src') + path.sep)}.*\\.(cc|h)*" 100 | ignoredPaths.push "#{_.escapeRegExp(path.join('runas', 'src') + path.sep)}.*\\.(cc|h)*" 101 | ignoredPaths.push "#{_.escapeRegExp(path.join('scrollbar-style', 'src') + path.sep)}.*\\.(cc|h)*" 102 | ignoredPaths.push "#{_.escapeRegExp(path.join('spellchecker', 'src') + path.sep)}.*\\.(cc|h)*" 103 | 104 | # Ignore build files 105 | ignoredPaths.push "#{_.escapeRegExp(path.sep)}binding\\.gyp$" 106 | ignoredPaths.push "#{_.escapeRegExp(path.sep)}.+\\.target.mk$" 107 | ignoredPaths.push "#{_.escapeRegExp(path.sep)}linker\\.lock$" 108 | ignoredPaths.push "#{_.escapeRegExp(path.join('build', 'Release') + path.sep)}.+\\.node\\.dSYM" 109 | 110 | # Hunspell dictionaries are only not needed on OS X. 111 | if process.platform is 'darwin' 112 | ignoredPaths.push path.join('spellchecker', 'vendor', 'hunspell_dictionaries') 113 | ignoredPaths = ignoredPaths.map (ignoredPath) -> "(#{ignoredPath})" 114 | 115 | testFolderPattern = new RegExp("#{_.escapeRegExp(path.sep)}te?sts?#{_.escapeRegExp(path.sep)}") 116 | exampleFolderPattern = new RegExp("#{_.escapeRegExp(path.sep)}examples?#{_.escapeRegExp(path.sep)}") 117 | benchmarkFolderPattern = new RegExp("#{_.escapeRegExp(path.sep)}benchmarks?#{_.escapeRegExp(path.sep)}") 118 | 119 | nodeModulesFilter = new RegExp(ignoredPaths.join('|')) 120 | filterNodeModule = (pathToCopy) -> 121 | return true if benchmarkFolderPattern.test(pathToCopy) 122 | 123 | pathToCopy = path.resolve(pathToCopy) 124 | nodeModulesFilter.test(pathToCopy) or testFolderPattern.test(pathToCopy) or exampleFolderPattern.test(pathToCopy) 125 | 126 | packageFilter = new RegExp("(#{ignoredPaths.join('|')})|(.+\\.(cson|coffee)$)") 127 | filterPackage = (pathToCopy) -> 128 | return true if benchmarkFolderPattern.test(pathToCopy) 129 | 130 | pathToCopy = path.resolve(pathToCopy) 131 | packageFilter.test(pathToCopy) or testFolderPattern.test(pathToCopy) or exampleFolderPattern.test(pathToCopy) 132 | 133 | for directory in nonPackageDirectories 134 | cp directory, path.join(appDir, directory), filter: filterNodeModule 135 | 136 | for directory in packageDirectories 137 | cp directory, path.join(appDir, directory), filter: filterPackage 138 | 139 | cp 'src', path.join(appDir, 'src'), filter: /.+\.(cson|coffee)$/ 140 | cp 'static', path.join(appDir, 'static') 141 | 142 | if process.platform is 'darwin' 143 | grunt.file.recurse path.join('resources', 'mac'), (sourcePath, rootDirectory, subDirectory='', filename) -> 144 | unless /.+\.plist/.test(sourcePath) 145 | grunt.file.copy(sourcePath, path.resolve(appDir, '..', subDirectory, filename)) 146 | 147 | dependencies = ['compile'] 148 | dependencies.push('copy-info-plist') if process.platform is 'darwin' 149 | dependencies.push('set-exe-icon') if process.platform is 'win32' 150 | grunt.task.run(dependencies...) 151 | -------------------------------------------------------------------------------- /build/tasks/clean-task.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | os = require 'os' 3 | 4 | module.exports = (grunt) -> 5 | {rm} = require('./task-helpers')(grunt) 6 | 7 | grunt.registerTask 'partial-clean', 'Delete some of the build files', -> 8 | tmpdir = os.tmpdir() 9 | 10 | rm grunt.config.get('atom.buildDir') 11 | rm require('../src/coffee-cache').cacheDir 12 | rm require('../src/less-compile-cache').cacheDir 13 | rm path.join(tmpdir, 'atom-cached-atom-shells') 14 | rm 'atom-shell' 15 | 16 | grunt.registerTask 'clean', 'Delete all the build files', -> 17 | homeDir = process.env[if process.platform is 'win32' then 'USERPROFILE' else 'HOME'] 18 | 19 | rm 'node_modules' 20 | rm path.join(homeDir, '.atom', '.node-gyp') 21 | grunt.task.run('partial-clean') 22 | -------------------------------------------------------------------------------- /build/tasks/codesign-task.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | fs = require 'fs-plus' 3 | 4 | module.exports = (grunt) -> 5 | {spawn} = require('./task-helpers')(grunt) 6 | 7 | grunt.registerTask 'codesign', 'Codesign the app', -> 8 | done = @async() 9 | 10 | if process.platform is 'darwin' and process.env.XCODE_KEYCHAIN 11 | unlockKeychain (error) -> 12 | if error? 13 | done(error) 14 | else 15 | signApp(done) 16 | else 17 | signApp(done) 18 | 19 | unlockKeychain = (callback) -> 20 | cmd = 'security' 21 | {XCODE_KEYCHAIN_PASSWORD, XCODE_KEYCHAIN} = process.env 22 | args = ['unlock-keychain', '-p', XCODE_KEYCHAIN_PASSWORD, XCODE_KEYCHAIN] 23 | spawn {cmd, args}, (error) -> callback(error) 24 | 25 | signApp = (callback) -> 26 | switch process.platform 27 | when 'darwin' 28 | cmd = 'codesign' 29 | args = ['--deep', '--force', '--verbose', '--sign', 'Marek Hrabě', grunt.config.get('atom.shellAppDir')] 30 | spawn {cmd, args}, (error) -> callback(error) 31 | when 'win32' 32 | spawn {cmd: 'taskkill', args: ['/F', '/IM', 'atom.exe']}, -> 33 | cmd = process.env.JANKY_SIGNTOOL ? 'signtool' 34 | args = [path.join(grunt.config.get('atom.shellAppDir'), 'atom.exe')] 35 | 36 | spawn {cmd, args}, (error) -> 37 | return callback(error) if error? 38 | 39 | setupExePath = path.resolve(grunt.config.get('atom.buildDir'), 'installer', 'AtomSetup.exe') 40 | if fs.isFileSync(setupExePath) 41 | args = [setupExePath] 42 | spawn {cmd, args}, (error) -> callback(error) 43 | else 44 | callback() 45 | else 46 | callback() 47 | -------------------------------------------------------------------------------- /build/tasks/copy-info-plist-task.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | module.exports = (grunt) -> 4 | {cp} = require('./task-helpers')(grunt) 5 | 6 | grunt.registerTask 'copy-info-plist', 'Copy plist', -> 7 | contentsDir = grunt.config.get('atom.contentsDir') 8 | plistPath = path.join(contentsDir, 'Info.plist') 9 | helperPlistPath = path.join(contentsDir, 'Frameworks/Atom Helper.app/Contents/Info.plist') 10 | 11 | # Copy custom plist files 12 | cp 'resources/mac/messenger-Info.plist', plistPath 13 | cp 'resources/mac/helper-Info.plist', helperPlistPath 14 | -------------------------------------------------------------------------------- /build/tasks/dump-symbols-task.coffee: -------------------------------------------------------------------------------- 1 | async = require 'async' 2 | fs = require 'fs-plus' 3 | path = require 'path' 4 | minidump = require 'minidump' 5 | 6 | module.exports = (grunt) -> 7 | {mkdir, rm} = require('./task-helpers')(grunt) 8 | 9 | dumpSymbolTo = (binaryPath, targetDirectory, callback) -> 10 | minidump.dumpSymbol binaryPath, (error, content) -> 11 | return callback(error) if error? 12 | 13 | moduleLine = /MODULE [^ ]+ [^ ]+ ([0-9A-F]+) (.*)\n/.exec(content) 14 | if moduleLine.length isnt 3 15 | return callback("Invalid output when dumping symbol for #{binaryPath}") 16 | 17 | filename = moduleLine[2] 18 | targetPathDirname = path.join(targetDirectory, filename, moduleLine[1]) 19 | mkdir targetPathDirname 20 | 21 | targetPath = path.join(targetPathDirname, "#{filename}.sym") 22 | fs.writeFile(targetPath, content, callback) 23 | 24 | grunt.registerTask 'dump-symbols', 'Dump symbols for native modules', -> 25 | done = @async() 26 | 27 | symbolsDir = grunt.config.get('atom.symbolsDir') 28 | rm symbolsDir 29 | mkdir symbolsDir 30 | 31 | tasks = [] 32 | onFile = (binaryPath) -> 33 | if /.*\.node$/.test(binaryPath) 34 | tasks.push(dumpSymbolTo.bind(this, binaryPath, symbolsDir)) 35 | onDirectory = -> 36 | true 37 | fs.traverseTreeSync 'node_modules', onFile, onDirectory 38 | 39 | async.parallel tasks, done 40 | -------------------------------------------------------------------------------- /build/tasks/install-task.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | _ = require 'underscore-plus' 3 | fs = require 'fs-plus' 4 | runas = null 5 | temp = require 'temp' 6 | 7 | module.exports = (grunt) -> 8 | {cp, mkdir, rm} = require('./task-helpers')(grunt) 9 | 10 | grunt.registerTask 'install', 'Install the built application', -> 11 | installDir = grunt.config.get('atom.installDir') 12 | shellAppDir = grunt.config.get('atom.shellAppDir') 13 | 14 | if process.platform is 'win32' 15 | runas ?= require 'runas' 16 | copyFolder = path.resolve 'script', 'copy-folder.cmd' 17 | if runas('cmd', ['/c', copyFolder, shellAppDir, installDir], admin: true) isnt 0 18 | grunt.log.error("Failed to copy #{shellAppDir} to #{installDir}") 19 | 20 | createShortcut = path.resolve 'script', 'create-shortcut.cmd' 21 | runas('cmd', ['/c', createShortcut, path.join(installDir, 'messenger.exe'), 'Messenger']) 22 | else if process.platform is 'darwin' 23 | rm installDir 24 | mkdir path.dirname(installDir) 25 | 26 | tempFolder = temp.path() 27 | mkdir tempFolder 28 | cp shellAppDir, tempFolder 29 | fs.renameSync(tempFolder, installDir) 30 | -------------------------------------------------------------------------------- /build/tasks/mkdeb-task.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | _ = require 'underscore-plus' 4 | 5 | module.exports = (grunt) -> 6 | {spawn} = require('./task-helpers')(grunt) 7 | 8 | fillTemplate = (filePath, data) -> 9 | template = _.template(String(fs.readFileSync("#{filePath}.in"))) 10 | filled = template(data) 11 | 12 | outputPath = path.join(grunt.config.get('atom.buildDir'), path.basename(filePath)) 13 | grunt.file.write(outputPath, filled) 14 | outputPath 15 | 16 | getInstalledSize = (buildDir, callback) -> 17 | cmd = 'du' 18 | args = ['-sk', path.join(buildDir, 'Atom')] 19 | spawn {cmd, args}, (error, {stdout}) -> 20 | installedSize = stdout.split(/\s+/)?[0] or '200000' # default to 200MB 21 | callback(null, installedSize) 22 | 23 | grunt.registerTask 'mkdeb', 'Create debian package', -> 24 | done = @async() 25 | buildDir = grunt.config.get('atom.buildDir') 26 | 27 | if process.arch is 'ia32' 28 | arch = 'i386' 29 | else if process.arch is 'x64' 30 | arch = 'amd64' 31 | else 32 | return done("Unsupported arch #{process.arch}") 33 | 34 | {name, version, description} = grunt.file.readJSON('package.json') 35 | section = 'devel' 36 | maintainer = 'GitHub ' 37 | installDir = '/usr' 38 | iconName = 'atom' 39 | executable = path.join(installDir, 'share', 'atom', 'atom') 40 | getInstalledSize buildDir, (error, installedSize) -> 41 | data = {name, version, description, section, arch, maintainer, installDir, iconName, installedSize, executable} 42 | controlFilePath = fillTemplate(path.join('resources', 'linux', 'debian', 'control'), data) 43 | desktopFilePath = fillTemplate(path.join('resources', 'linux', 'atom.desktop'), data) 44 | icon = path.join('resources', 'atom.png') 45 | 46 | cmd = path.join('script', 'mkdeb') 47 | args = [version, arch, controlFilePath, desktopFilePath, icon, buildDir] 48 | spawn {cmd, args}, (error) -> 49 | if error? 50 | done(error) 51 | else 52 | grunt.log.ok "Created #{buildDir}/atom-#{version}-#{arch}.deb" 53 | done() 54 | -------------------------------------------------------------------------------- /build/tasks/mkrpm-task.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | _ = require 'underscore-plus' 4 | 5 | module.exports = (grunt) -> 6 | {spawn, rm, mkdir} = require('./task-helpers')(grunt) 7 | 8 | fillTemplate = (filePath, data) -> 9 | template = _.template(String(fs.readFileSync("#{filePath}.in"))) 10 | filled = template(data) 11 | 12 | outputPath = path.join(grunt.config.get('atom.buildDir'), path.basename(filePath)) 13 | grunt.file.write(outputPath, filled) 14 | outputPath 15 | 16 | grunt.registerTask 'mkrpm', 'Create rpm package', -> 17 | done = @async() 18 | 19 | if process.arch is 'ia32' 20 | arch = 'i386' 21 | else if process.arch is 'x64' 22 | arch = 'amd64' 23 | else 24 | return done("Unsupported arch #{process.arch}") 25 | 26 | {name, version, description} = grunt.file.readJSON('package.json') 27 | buildDir = grunt.config.get('atom.buildDir') 28 | 29 | rpmDir = path.join(buildDir, 'rpm') 30 | rm rpmDir 31 | mkdir rpmDir 32 | 33 | installDir = grunt.config.get('atom.installDir') 34 | shareDir = path.join(installDir, 'share', 'atom') 35 | iconName = 'atom' 36 | executable = 'atom' 37 | 38 | data = {name, version, description, installDir, iconName, executable} 39 | specFilePath = fillTemplate(path.join('resources', 'linux', 'redhat', 'atom.spec'), data) 40 | desktopFilePath = fillTemplate(path.join('resources', 'linux', 'atom.desktop'), data) 41 | 42 | cmd = path.join('script', 'mkrpm') 43 | args = [specFilePath, desktopFilePath, buildDir] 44 | spawn {cmd, args}, (error) -> 45 | if error? 46 | done(error) 47 | else 48 | grunt.log.ok "Created rpm package in #{rpmDir}" 49 | done() 50 | -------------------------------------------------------------------------------- /build/tasks/nof-task.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | {spawn} = require('./task-helpers')(grunt) 3 | 4 | grunt.registerTask 'nof', 'Un-focus all specs', -> 5 | nof = require.resolve('.bin/nof') 6 | spawn({cmd: nof, args: ['spec']}, @async()) 7 | -------------------------------------------------------------------------------- /build/tasks/output-build-filetypes.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | module.exports = (grunt) -> 4 | grunt.registerTask 'output-build-filetypes', 'Log counts for each filetype in the built application', -> 5 | shellAppDir = grunt.config.get('atom.shellAppDir') 6 | 7 | types = {} 8 | grunt.file.recurse shellAppDir, (absolutePath, rootPath, relativePath, fileName) -> 9 | extension = path.extname(fileName) or fileName 10 | types[extension] ?= 0 11 | types[extension]++ 12 | 13 | extensions = Object.keys(types).sort (extension1, extension2) -> 14 | diff = types[extension2] - types[extension1] 15 | if diff is 0 16 | extension1.toLowerCase().localeCompare(extension2.toLowerCase()) 17 | else 18 | diff 19 | 20 | extensions.forEach (extension) -> 21 | grunt.log.error "#{extension}: #{types[extension]}" 22 | -------------------------------------------------------------------------------- /build/tasks/output-disk-space.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | {spawn} = require('./task-helpers')(grunt) 3 | 4 | grunt.registerTask 'output-disk-space', 'Print diskspace available', -> 5 | return unless process.platform is 'darwin' 6 | 7 | done = @async() 8 | 9 | cmd = 'df' 10 | args = ['-Hl'] 11 | spawn {cmd, args}, (error, result, code) -> 12 | return done(error) if error? 13 | 14 | lines = result.stdout.split("\n") 15 | 16 | for line in lines[1..] 17 | [filesystem, size, used, avail, capacity, extra] = line.split(/\s+/) 18 | capacity = parseInt(capacity) 19 | 20 | if capacity > 90 21 | grunt.log.error("#{filesystem} is at #{capacity}% capacity!") 22 | else if capacity > 80 23 | grunt.log.ok("#{filesystem} is at #{capacity}% capacity.") 24 | 25 | done() 26 | -------------------------------------------------------------------------------- /build/tasks/output-for-loop-returns.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | module.exports = (grunt) -> 4 | grunt.registerTask 'output-for-loop-returns', 'Log methods that end with a for loop', -> 5 | appDir = grunt.config.get('atom.appDir') 6 | 7 | jsPaths = [] 8 | grunt.file.recurse path.join(appDir, 'src'), (absolutePath, rootPath, relativePath, fileName) -> 9 | jsPaths.push(absolutePath) if path.extname(fileName) is '.js' 10 | 11 | jsPaths.forEach (jsPath) -> 12 | js = grunt.file.read(jsPath) 13 | method = null 14 | for line, index in js.split('\n') 15 | [match, className, methodName] = /^\s*([a-zA-Z]+)\.(?:prototype\.)?([a-zA-Z]+)\s*=\s*function\(/.exec(line) ? [] 16 | if className and methodName 17 | method = "#{className}::#{methodName}" 18 | else 19 | [match, ctorName] = /^\s*function\s+([a-zA-Z]+)\(/.exec(line) ? [] 20 | 21 | if /^\s*return\s+_results;\s*$/.test(line) 22 | console.log(method ? "#{path.basename(jsPath)}:#{index}") 23 | -------------------------------------------------------------------------------- /build/tasks/output-long-paths-task.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | module.exports = (grunt) -> 4 | grunt.registerTask 'output-long-paths', 'Log long paths in the built application', -> 5 | shellAppDir = grunt.config.get('atom.shellAppDir') 6 | 7 | longPaths = [] 8 | grunt.file.recurse shellAppDir, (absolutePath, rootPath, relativePath, fileName) -> 9 | if relativePath 10 | fullPath = path.join(relativePath, fileName) 11 | else 12 | fullPath = fileName 13 | longPaths.push(fullPath) if fullPath.length >= 175 14 | 15 | longPaths.sort (longPath1, longPath2) -> longPath2.length - longPath1.length 16 | 17 | longPaths.forEach (longPath) -> 18 | grunt.log.error "#{longPath.length} character path: #{longPath}" 19 | -------------------------------------------------------------------------------- /build/tasks/output-module-counts.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | module.exports = (grunt) -> 5 | grunt.registerTask 'output-module-counts', 'Log modules where more than one copy exists in node_modules', -> 6 | nodeModulesDir = path.resolve(__dirname, '..', '..', 'node_modules') 7 | 8 | otherModules = {} 9 | atomModules = {} 10 | 11 | sortModuleNames = (modules) -> 12 | Object.keys(modules).sort (name1, name2) -> 13 | diff = modules[name2].count - modules[name1].count 14 | diff = name1.localeCompare(name2) if diff is 0 15 | diff 16 | 17 | getAtomTotal = -> 18 | Object.keys(atomModules).length 19 | 20 | getOtherTotal = -> 21 | Object.keys(otherModules).length 22 | 23 | recurseHandler = (absolutePath, rootPath, relativePath, fileName) -> 24 | return if fileName isnt 'package.json' 25 | 26 | {name, version, repository} = grunt.file.readJSON(absolutePath) 27 | return unless name and version 28 | 29 | repository = repository.url if repository?.url 30 | 31 | if /.+\/atom\/.+/.test(repository) 32 | modules = atomModules 33 | else 34 | modules = otherModules 35 | 36 | modules[name] ?= {versions: {}, count: 0} 37 | modules[name].count++ 38 | modules[name].versions[version] = true 39 | 40 | walkNodeModuleDir = -> 41 | grunt.file.recurse(nodeModulesDir, recurseHandler) 42 | 43 | # Handle broken symlinks that grunt.file.recurse fails to handle 44 | loop 45 | try 46 | walkNodeModuleDir() 47 | break 48 | catch error 49 | if error.code is 'ENOENT' 50 | fs.unlinkSync(error.path) 51 | otherModules = {} 52 | atomModules = {} 53 | else 54 | break 55 | 56 | if getAtomTotal() > 0 57 | console.log "Atom Modules: #{getAtomTotal()}" 58 | sortModuleNames(atomModules).forEach (name) -> 59 | {count, versions, atom} = atomModules[name] 60 | grunt.log.error "#{name}: #{count} (#{Object.keys(versions).join(', ')})" if count > 1 61 | console.log() 62 | 63 | console.log "Other Modules: #{getOtherTotal()}" 64 | sortModuleNames(otherModules).forEach (name) -> 65 | {count, versions, atom} = otherModules[name] 66 | grunt.log.error "#{name}: #{count} (#{Object.keys(versions).join(', ')})" if count > 1 67 | -------------------------------------------------------------------------------- /build/tasks/publish-build-task.coffee: -------------------------------------------------------------------------------- 1 | child_process = require 'child_process' 2 | path = require 'path' 3 | 4 | _ = require 'underscore-plus' 5 | async = require 'async' 6 | fs = require 'fs-plus' 7 | GitHub = require 'github-releases' 8 | request = require 'request' 9 | 10 | grunt = null 11 | 12 | commitSha = process.env.JANKY_SHA1 13 | token = process.env.ATOM_ACCESS_TOKEN 14 | defaultHeaders = 15 | Authorization: "token #{token}" 16 | 'User-Agent': 'Atom' 17 | 18 | module.exports = (gruntObject) -> 19 | grunt = gruntObject 20 | {cp} = require('./task-helpers')(grunt) 21 | 22 | grunt.registerTask 'publish-build', 'Publish the built app', -> 23 | tasks = [] 24 | tasks.push('build-docs', 'prepare-docs') if process.platform is 'darwin' 25 | tasks.push('upload-assets') if process.env.JANKY_SHA1 and process.env.JANKY_BRANCH is 'master' 26 | grunt.task.run(tasks) 27 | 28 | grunt.registerTask 'prepare-docs', 'Move api.json to atom-api.json', -> 29 | docsOutputDir = grunt.config.get('docsOutputDir') 30 | buildDir = grunt.config.get('atom.buildDir') 31 | cp path.join(docsOutputDir, 'api.json'), path.join(buildDir, 'atom-api.json') 32 | 33 | grunt.registerTask 'upload-assets', 'Upload the assets to a GitHub release', -> 34 | doneCallback = @async() 35 | startTime = Date.now() 36 | done = (args...) -> 37 | elapsedTime = Math.round((Date.now() - startTime) / 100) / 10 38 | grunt.log.ok("Upload time: #{elapsedTime}s") 39 | doneCallback(args...) 40 | 41 | unless token 42 | return done(new Error('ATOM_ACCESS_TOKEN environment variable not set')) 43 | 44 | buildDir = grunt.config.get('atom.buildDir') 45 | assets = getAssets() 46 | 47 | zipAssets buildDir, assets, (error) -> 48 | return done(error) if error? 49 | getAtomDraftRelease (error, release) -> 50 | return done(error) if error? 51 | assetNames = (asset.assetName for asset in assets) 52 | deleteExistingAssets release, assetNames, (error) -> 53 | return done(error) if error? 54 | uploadAssets(release, buildDir, assets, done) 55 | 56 | getAssets = -> 57 | {cp} = require('./task-helpers')(grunt) 58 | 59 | {version} = grunt.file.readJSON('package.json') 60 | buildDir = grunt.config.get('atom.buildDir') 61 | 62 | switch process.platform 63 | when 'darwin' 64 | [ 65 | {assetName: 'atom-mac.zip', sourcePath: 'Atom.app'} 66 | {assetName: 'atom-mac-symbols.zip', sourcePath: 'Atom.breakpad.syms'} 67 | {assetName: 'atom-api.json', sourcePath: 'atom-api.json'} 68 | ] 69 | when 'win32' 70 | assets = [{assetName: 'atom-windows.zip', sourcePath: 'Atom'}] 71 | for squirrelAsset in ['AtomSetup.exe', 'RELEASES', "atom-#{version}-full.nupkg", "atom-#{version}-delta.nupkg"] 72 | cp path.join(buildDir, 'installer', squirrelAsset), path.join(buildDir, squirrelAsset) 73 | assets.push({assetName: squirrelAsset, sourcePath: assetName}) 74 | assets 75 | when 'linux' 76 | if process.arch is 'ia32' 77 | arch = 'i386' 78 | else 79 | arch = 'amd64' 80 | 81 | # Check for a Debian build 82 | sourcePath = "#{buildDir}/atom-#{version}-#{arch}.deb" 83 | assetName = "atom-#{arch}.deb" 84 | 85 | # Check for a Fedora build 86 | unless fs.isFileSync(sourcePath) 87 | rpmName = fs.readdirSync("#{buildDir}/rpm")[0] 88 | sourcePath = "#{buildDir}/rpm/#{rpmName}" 89 | if process.arch is 'ia32' 90 | arch = 'i386' 91 | else 92 | arch = 'x86_64' 93 | assetName = "atom.#{arch}.rpm" 94 | 95 | cp sourcePath, path.join(buildDir, assetName) 96 | 97 | [ 98 | {assetName, sourcePath} 99 | ] 100 | 101 | logError = (message, error, details) -> 102 | grunt.log.error(message) 103 | grunt.log.error(error.message ? error) if error? 104 | grunt.log.error(require('util').inspect(details)) if details 105 | 106 | zipAssets = (buildDir, assets, callback) -> 107 | zip = (directory, sourcePath, assetName, callback) -> 108 | if process.platform is 'win32' 109 | zipCommand = "C:/psmodules/7z.exe a -r #{assetName} #{sourcePath}" 110 | else 111 | zipCommand = "zip -r --symlinks #{assetName} #{sourcePath}" 112 | options = {cwd: directory, maxBuffer: Infinity} 113 | child_process.exec zipCommand, options, (error, stdout, stderr) -> 114 | logError("Zipping #{sourcePath} failed", error, stderr) if error? 115 | callback(error) 116 | 117 | tasks = [] 118 | for {assetName, sourcePath} in assets when path.extname(assetName) is '.zip' 119 | fs.removeSync(path.join(buildDir, assetName)) 120 | tasks.push(zip.bind(this, buildDir, sourcePath, assetName)) 121 | async.parallel(tasks, callback) 122 | 123 | getAtomDraftRelease = (callback) -> 124 | atomRepo = new GitHub({repo: 'atom/atom', token}) 125 | atomRepo.getReleases (error, releases=[]) -> 126 | if error? 127 | logError('Fetching atom/atom releases failed', error, releases) 128 | callback(error) 129 | else 130 | [firstDraft] = releases.filter ({draft}) -> draft 131 | if firstDraft? 132 | options = 133 | uri: firstDraft.assets_url 134 | method: 'GET' 135 | headers: defaultHeaders 136 | json: true 137 | request options, (error, response, assets=[]) -> 138 | if error? or response.statusCode isnt 200 139 | logError('Fetching draft release assets failed', error, assets) 140 | callback(error ? new Error(response.statusCode)) 141 | else 142 | firstDraft.assets = assets 143 | callback(null, firstDraft) 144 | else 145 | createAtomDraftRelease(callback) 146 | 147 | createAtomDraftRelease = (callback) -> 148 | {version} = require('../../package.json') 149 | options = 150 | uri: 'https://api.github.com/repos/atom/atom/releases' 151 | method: 'POST' 152 | headers: defaultHeaders 153 | json: 154 | tag_name: "v#{version}" 155 | name: version 156 | draft: true 157 | body: """ 158 | ### Notable Changes 159 | 160 | * Something new 161 | """ 162 | 163 | request options, (error, response, body='') -> 164 | if error? or response.statusCode isnt 201 165 | logError("Creating atom/atom draft release failed", error, body) 166 | callback(error ? new Error(response.statusCode)) 167 | else 168 | callback(null, body) 169 | 170 | deleteRelease = (release) -> 171 | options = 172 | uri: release.url 173 | method: 'DELETE' 174 | headers: defaultHeaders 175 | json: true 176 | request options, (error, response, body='') -> 177 | if error? or response.statusCode isnt 204 178 | logError('Deleting release failed', error, body) 179 | 180 | deleteExistingAssets = (release, assetNames, callback) -> 181 | [callback, assetNames] = [assetNames, callback] if not callback? 182 | 183 | deleteAsset = (url, callback) -> 184 | options = 185 | uri: url 186 | method: 'DELETE' 187 | headers: defaultHeaders 188 | request options, (error, response, body='') -> 189 | if error? or response.statusCode isnt 204 190 | logError('Deleting existing release asset failed', error, body) 191 | callback(error ? new Error(response.statusCode)) 192 | else 193 | callback() 194 | 195 | tasks = [] 196 | for asset in release.assets when not assetNames? or asset.name in assetNames 197 | tasks.push(deleteAsset.bind(this, asset.url)) 198 | async.parallel(tasks, callback) 199 | 200 | uploadAssets = (release, buildDir, assets, callback) -> 201 | upload = (release, assetName, assetPath, callback) -> 202 | options = 203 | uri: release.upload_url.replace(/\{.*$/, "?name=#{assetName}") 204 | method: 'POST' 205 | headers: _.extend({ 206 | 'Content-Type': 'application/zip' 207 | 'Content-Length': fs.getSizeSync(assetPath) 208 | }, defaultHeaders) 209 | 210 | assetRequest = request options, (error, response, body='') -> 211 | if error? or response.statusCode >= 400 212 | logError("Upload release asset #{assetName} failed", error, body) 213 | callback(error ? new Error(response.statusCode)) 214 | else 215 | callback(null, release) 216 | 217 | fs.createReadStream(assetPath).pipe(assetRequest) 218 | 219 | tasks = [] 220 | for {assetName} in assets 221 | assetPath = path.join(buildDir, assetName) 222 | tasks.push(upload.bind(this, release, assetName, assetPath)) 223 | async.parallel(tasks, callback) 224 | -------------------------------------------------------------------------------- /build/tasks/set-exe-icon-task.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | module.exports = (grunt) -> 4 | grunt.registerTask 'set-exe-icon', 'Set icon of the exe', -> 5 | done = @async() 6 | 7 | shellAppDir = grunt.config.get('atom.shellAppDir') 8 | shellExePath = path.join(shellAppDir, 'messenger.exe') 9 | iconPath = path.resolve('resources', 'win', 'messenger.ico') 10 | 11 | rcedit = require('rcedit') 12 | rcedit(shellExePath, {'icon': iconPath}, done) 13 | -------------------------------------------------------------------------------- /build/tasks/set-version-task.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | module.exports = (grunt) -> 5 | {spawn} = require('./task-helpers')(grunt) 6 | 7 | grunt.registerTask 'set-version', 'Set the version in the plist and package.json', -> 8 | done = @async() 9 | appDir = grunt.config.get('atom.appDir') 10 | {version} = require(path.join(appDir, 'package.json')) 11 | 12 | # Replace version field of package.json. 13 | packageJsonPath = path.join(appDir, 'package.json') 14 | packageJson = require(packageJsonPath) 15 | packageJson.version = version 16 | packageJsonString = JSON.stringify(packageJson) 17 | fs.writeFileSync(packageJsonPath, packageJsonString) 18 | 19 | if process.platform is 'darwin' 20 | cmd = 'script/set-version' 21 | args = [grunt.config.get('atom.buildDir'), version] 22 | spawn {cmd, args}, (error, result, code) -> done(error) 23 | else if process.platform is 'win32' 24 | shellAppDir = grunt.config.get('atom.shellAppDir') 25 | shellExePath = path.join(shellAppDir, 'messenger.exe') 26 | 27 | strings = 28 | CompanyName: 'Marek Hrabě' 29 | FileDescription: 'Messenger' 30 | LegalCopyright: 'Copyright (C) 2015 Marek Hrabě. All rights reserved' 31 | ProductName: 'Messenger' 32 | ProductVersion: version 33 | 34 | rcedit = require('rcedit') 35 | rcedit(shellExePath, {'version-string': strings}, done) 36 | else 37 | done() 38 | -------------------------------------------------------------------------------- /build/tasks/task-helpers.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs-plus' 2 | path = require 'path' 3 | 4 | module.exports = (grunt) -> 5 | cp: (source, destination, {filter}={}) -> 6 | unless grunt.file.exists(source) 7 | grunt.fatal("Cannot copy non-existent #{source.cyan} to #{destination.cyan}") 8 | 9 | copyFile = (sourcePath, destinationPath) -> 10 | return if filter?(sourcePath) or filter?.test?(sourcePath) 11 | 12 | stats = fs.lstatSync(sourcePath) 13 | if stats.isSymbolicLink() 14 | grunt.file.mkdir(path.dirname(destinationPath)) 15 | fs.symlinkSync(fs.readlinkSync(sourcePath), destinationPath) 16 | else if stats.isFile() 17 | grunt.file.copy(sourcePath, destinationPath) 18 | 19 | if grunt.file.exists(destinationPath) 20 | fs.chmodSync(destinationPath, fs.statSync(sourcePath).mode) 21 | 22 | if grunt.file.isFile(source) 23 | copyFile(source, destination) 24 | else 25 | try 26 | onFile = (sourcePath) -> 27 | destinationPath = path.join(destination, path.relative(source, sourcePath)) 28 | copyFile(sourcePath, destinationPath) 29 | onDirectory = (sourcePath) -> 30 | if fs.isSymbolicLinkSync(sourcePath) 31 | destinationPath = path.join(destination, path.relative(source, sourcePath)) 32 | copyFile(sourcePath, destinationPath) 33 | false 34 | else 35 | true 36 | fs.traverseTreeSync source, onFile, onDirectory 37 | catch error 38 | grunt.fatal(error) 39 | 40 | grunt.verbose.writeln("Copied #{source.cyan} to #{destination.cyan}.") 41 | 42 | mkdir: (args...) -> 43 | grunt.file.mkdir(args...) 44 | 45 | rm: (args...) -> 46 | grunt.file.delete(args..., force: true) if grunt.file.exists(args...) 47 | 48 | spawn: (options, callback) -> 49 | childProcess = require 'child_process' 50 | stdout = [] 51 | stderr = [] 52 | error = null 53 | proc = childProcess.spawn(options.cmd, options.args, options.opts) 54 | proc.stdout.on 'data', (data) -> stdout.push(data.toString()) 55 | proc.stderr.on 'data', (data) -> stderr.push(data.toString()) 56 | proc.on 'error', (processError) -> error ?= processError 57 | proc.on 'close', (exitCode, signal) -> 58 | error ?= new Error(signal) if exitCode isnt 0 59 | results = {stderr: stderr.join(''), stdout: stdout.join(''), code: exitCode} 60 | grunt.log.error results.stderr if exitCode isnt 0 61 | callback(error, results, exitCode) 62 | 63 | isAtomPackage: (packagePath) -> 64 | try 65 | {engines} = grunt.file.readJSON(path.join(packagePath, 'package.json')) 66 | engines?.atom? 67 | catch error 68 | false 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "messenger", 3 | "version": "0.2.1", 4 | "description": "Unofficial Facebook Messenger for Mac", 5 | "main": "src/main.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/marekhrabe/messenger" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/marekhrabe/messenger/issues" 12 | }, 13 | "author": "Marek Hrabe (https://github.com/marekhrabe)", 14 | "license": "MIT", 15 | "atomShellVersion": "0.22.3" 16 | } 17 | -------------------------------------------------------------------------------- /resources/mac/helper-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Messenger Helper 9 | CFBundleExecutable 10 | Atom Helper 11 | CFBundleIdentifier 12 | com.marekhrabe.messenger.helper 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | Messenger Helper 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | ${VERSION} 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | ${VERSION} 25 | LSMinimumSystemVersion 26 | 10.7.0 27 | LSUIElement 28 | 1 29 | NSSupportsAutomaticGraphicsSwitching 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /resources/mac/messenger-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleExecutable 6 | Atom 7 | CFBundleIconFile 8 | messenger.icns 9 | CFBundleIdentifier 10 | com.marekhrabe.messenger 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Messenger 15 | CFBundlePackageType 16 | APPL 17 | CFBundleDevelopmentRegion 18 | English 19 | CFBundleShortVersionString 20 | ${VERSION} 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | ${VERSION} 25 | LSApplicationCategoryType 26 | public.app-category.social-networking 27 | LSMinimumSystemVersion 28 | 10.8 29 | NSAppleScriptEnabled 30 | YES 31 | NSMainNibFile 32 | MainMenu 33 | NSPrincipalClass 34 | AtomApplication 35 | NSSupportsAutomaticGraphicsSwitching 36 | 37 | SUPublicDSAKeyFile 38 | speakeasy.pem 39 | SUScheduledCheckInterval 40 | 3600 41 | NSHumanReadableCopyright 42 | Copyright © 2015, Marek Hrabe (https://github.com/marekhrabe/messenger) 43 | 44 | 45 | -------------------------------------------------------------------------------- /resources/mac/messenger.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marekhrabe/messenger/efdbbf5adc78c5d6855a7335f5a9ad1329c7ffa3/resources/mac/messenger.icns -------------------------------------------------------------------------------- /resources/mac/speakeasy.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIDOjCCAi0GByqGSM44BAEwggIgAoIBAQCEh+j0nKWTw7soK6w3uk9PzPGVBksk 3 | wDIaA+d+1CHJY9qhjp7OjAlSOl6nrUlGHzU87DRmBlwYZONAzDZnYpLi7zmPVASg 4 | Xk+AmuqzqahTKtwodJp7R/Aq/lCbB2tXTXOxVo+Jya1BQbfd0wWXJFUlD/xTvrgu 5 | zrtw6VYBvaRu8jCjHAJNZn0CO80igj1ZNxRqmmz1Rkt1tT0KBBfGBTNzXeBmGKHN 6 | bVIKW7zImgfm+UQky+WFei1dqcfWOyfrHIYa3Qn1Nes48SBdrolvfvrChlSpqgEN 7 | wxFW9aoognS1UJTu350AQb2NwOOSQRsR++y3iJp+60nBSDZu7sjNN9etAhUAvqki 8 | JOjBjooRd2odMh7imICHQ3kCggEATwa6W0s2xrolPRpwWZS8ORUNDgEI4eOIvonq 9 | O2qZgwD21zUQOsFjLMbWn0cCtrORr7iM8pFg8Yn8dSccpqs+2cM4uFZAycKXf6w3 10 | jIvV6M3IPQuUSqVFZtqUVuteGTEuAHZKIrXE05P4aJXHLjqSC9JuaXNRm9q7OW7m 11 | rwsoAFyfkKqbtl5Ch+WZ21CE4J+ByTfVwVU4XLiOtce6NABSDWNJsF9fIoFCZCDc 12 | uumLllDJysD8S6aBNhOjNMHPmeIpZBXT23zHH5du/blcEyBbVF3a2ntgudfJmyln 13 | T178CIEUSSjcbz9JyAhhK7OfNlzKhRiO1c4Y3XaZIniLGjF5DwOCAQUAAoIBABGZ 14 | mfuHBW89ub19iICE//VbB91m2f0nUvHk8vE4vvAK8AdD91GODPJr4DU0kJM6ne8r 15 | ohvZgokgDRkGAEceX/nVoG0RLq9T15Xr2qedWVwAffpU10iV9mYwbhHqUKPtG8cj 16 | GW0cDdSI+0oG6UEyn8aQ5p93YEm5N6lq4rWKpxXb/gkrIla4sJJP8VHOOKmo6l1H 17 | AKVIfofiaNAQShu72WVCCurWaoVTUEliEBhy3WlcjuKXEuoL1lpNxyqkt7mf6w71 18 | 6y2+Nh+XUTiFoTIVhk/CH0z+BQTneWEALvfTFzDae+a42rPAisKlt+Gbe7zopnVA 19 | kcQwM0lLzgwx4T1DV3s= 20 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /resources/messenger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marekhrabe/messenger/efdbbf5adc78c5d6855a7335f5a9ad1329c7ffa3/resources/messenger.png -------------------------------------------------------------------------------- /resources/win/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marekhrabe/messenger/efdbbf5adc78c5d6855a7335f5a9ad1329c7ffa3/resources/win/loading.gif -------------------------------------------------------------------------------- /resources/win/messenger.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marekhrabe/messenger/efdbbf5adc78c5d6855a7335f5a9ad1329c7ffa3/resources/win/messenger.ico -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var verifyRequirements = require('./utils/verify-requirements'); 5 | var safeExec = require('./utils/child-process-wrapper.js').safeExec; 6 | var path = require('path'); 7 | 8 | // Executes an array of commands one by one. 9 | function executeCommands(commands, done, index) { 10 | index = (index == undefined ? 0 : index); 11 | if (index < commands.length) { 12 | var command = commands[index]; 13 | if (command.message) 14 | console.log(command.message); 15 | var options = null; 16 | if (typeof command !== 'string') { 17 | options = command.options; 18 | command = command.command; 19 | } 20 | safeExec(command, options, executeCommands.bind(this, commands, done, index + 1)); 21 | } 22 | else 23 | done(null); 24 | } 25 | 26 | function bootstrap() { 27 | var apmInstallPath = path.resolve(__dirname, '..', 'apm'); 28 | if (!fs.existsSync(apmInstallPath)) 29 | fs.mkdirSync(apmInstallPath); 30 | if (!fs.existsSync(path.join(apmInstallPath, 'node_modules'))) 31 | fs.mkdirSync(path.join(apmInstallPath, 'node_modules')); 32 | 33 | var apmPath = path.resolve(__dirname, '..', 'apm', 'node_modules', 'atom-package-manager', 'bin', 'apm') 34 | var apmFlags = process.env.JANKY_SHA1 || process.argv.indexOf('--no-color') !== -1 ? ' --no-color' : ''; 35 | 36 | var npmPath = path.resolve(__dirname, '..', 'build', 'node_modules', '.bin', 'npm'); 37 | var initialNpmCommand = fs.existsSync(npmPath) ? npmPath : 'npm'; 38 | var npmFlags = ' --userconfig=' + path.resolve(__dirname, '..', 'build', '.npmrc') + ' '; 39 | 40 | var packagesToDedupe = [ 41 | 'abbrev', 42 | 'amdefine', 43 | 'atom-space-pen-views', 44 | 'cheerio', 45 | 'domelementtype', 46 | 'fs-plus', 47 | 'grim', 48 | 'highlights', 49 | 'humanize-plus', 50 | 'iconv-lite', 51 | 'inherits', 52 | 'loophole', 53 | 'oniguruma', 54 | 'q', 55 | 'request', 56 | 'rimraf', 57 | 'roaster', 58 | 'season', 59 | 'sigmund', 60 | 'semver', 61 | 'through', 62 | 'temp' 63 | ]; 64 | 65 | var buildInstallCommand = initialNpmCommand + npmFlags + 'install'; 66 | var buildInstallOptions = {cwd: path.resolve(__dirname, '..', 'build')}; 67 | var apmInstallCommand = npmPath + npmFlags + '--target=0.10.35 ' + 'install'; 68 | var apmInstallOptions = {cwd: apmInstallPath}; 69 | var moduleInstallCommand = apmPath + ' install' + apmFlags; 70 | var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; 71 | 72 | if (process.argv.indexOf('--no-quiet') === -1) { 73 | buildInstallCommand += ' --loglevel error'; 74 | apmInstallCommand += ' --loglevel error'; 75 | moduleInstallCommand += ' --loglevel error'; 76 | dedupeApmCommand += ' --quiet'; 77 | buildInstallOptions.ignoreStdout = true; 78 | apmInstallOptions.ignoreStdout = true; 79 | } 80 | 81 | // apm ships with 32-bit node so make sure its native modules are compiled 82 | // for a 32-bit target architecture 83 | if (process.env.JANKY_SHA1 && process.platform === 'win32') 84 | apmInstallCommand += ' --arch=ia32'; 85 | 86 | var commands = [ 87 | { 88 | command: buildInstallCommand, 89 | message: 'Installing build modules...', 90 | options: buildInstallOptions 91 | }, 92 | { 93 | command: apmInstallCommand, 94 | message: 'Installing apm...', 95 | options: apmInstallOptions 96 | }, 97 | apmPath + ' clean' + apmFlags, 98 | moduleInstallCommand, 99 | dedupeApmCommand + ' ' + packagesToDedupe.join(' '), 100 | ]; 101 | 102 | process.chdir(path.dirname(__dirname)); 103 | executeCommands(commands, process.exit); 104 | } 105 | 106 | verifyRequirements(function(error, successMessage) { 107 | if (error) { 108 | console.log(error); 109 | process.exit(1); 110 | } 111 | 112 | console.log(successMessage); 113 | bootstrap(); 114 | }); 115 | -------------------------------------------------------------------------------- /script/bootstrap.cmd: -------------------------------------------------------------------------------- 1 | @IF EXIST "%~dp0\node.exe" ( 2 | "%~dp0\node.exe" "%~dp0\bootstrap" %* 3 | ) ELSE ( 4 | node "%~dp0\bootstrap" %* 5 | ) 6 | 7 | -------------------------------------------------------------------------------- /script/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var cp = require('./utils/child-process-wrapper.js'); 3 | var path = require('path'); 4 | 5 | process.chdir(path.dirname(__dirname)); 6 | 7 | cp.safeExec('node script/bootstrap', function() { 8 | // build/node_modules/.bin/grunt "$@" 9 | var gruntPath = path.join('build', 'node_modules', '.bin', 'grunt') + (process.platform === 'win32' ? '.cmd' : ''); 10 | var args = ['--gruntfile', path.resolve('build', 'Gruntfile.coffee')]; 11 | args = args.concat(process.argv.slice(2)); 12 | cp.safeSpawn(gruntPath, args, process.exit); 13 | }); 14 | -------------------------------------------------------------------------------- /script/build.cmd: -------------------------------------------------------------------------------- 1 | @IF EXIST "%~dp0\node.exe" ( 2 | "%~dp0\node.exe" "%~dp0\build" %* 3 | ) ELSE ( 4 | node "%~dp0\build" %* 5 | ) 6 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var cp = require('./utils/child-process-wrapper.js'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | 6 | process.chdir(path.dirname(__dirname)); 7 | 8 | var homeDir = process.platform == 'win32' ? process.env.USERPROFILE : process.env.HOME; 9 | 10 | function loadEnvironmentVariables(filePath) { 11 | try { 12 | var lines = fs.readFileSync(filePath, 'utf8').trim().split('\n'); 13 | for (i in lines) { 14 | var parts = lines[i].split('='); 15 | var key = parts[0].trim(); 16 | var value = parts[1].trim().substr(1, parts[1].length - 2); 17 | process.env[key] = value; 18 | } 19 | } catch(error) { 20 | console.error("Failed to load environment variables: " + filePath, error.code); 21 | } 22 | } 23 | 24 | function readEnvironmentVariables() { 25 | if (process.platform === 'win32') 26 | loadEnvironmentVariables(path.resolve('/jenkins/config/atomcredentials')); 27 | else if (process.platform === 'darwin') { 28 | loadEnvironmentVariables('/var/lib/jenkins/config/atomcredentials'); 29 | loadEnvironmentVariables('/var/lib/jenkins/config/xcodekeychain'); 30 | } 31 | } 32 | 33 | function removeNodeModules() { 34 | var fsPlus; 35 | try { 36 | fsPlus = require('fs-plus'); 37 | } catch (error) { 38 | return; 39 | } 40 | 41 | try { 42 | fsPlus.removeSync(path.resolve(__dirname, '..', 'node_modules')); 43 | } catch (error) { 44 | console.error(error.message); 45 | process.exit(1); 46 | } 47 | } 48 | 49 | function removeTempFolders() { 50 | var fsPlus; 51 | try { 52 | fsPlus = require('fs-plus'); 53 | } catch (error) { 54 | return; 55 | } 56 | 57 | var temp = require('os').tmpdir(); 58 | if (!fsPlus.isDirectorySync(temp)) 59 | return; 60 | 61 | var deletedFolders = 0; 62 | 63 | fsPlus.readdirSync(temp).filter(function(folderName) { 64 | return folderName.indexOf('npm-') === 0; 65 | }).forEach(function(folderName) { 66 | try { 67 | fsPlus.removeSync(path.join(temp, folderName)); 68 | deletedFolders++; 69 | } catch (error) { 70 | console.error("Failed to delete npm temp folder: " + error.message); 71 | } 72 | }); 73 | 74 | if (deletedFolders > 0) 75 | console.log("Deleted " + deletedFolders + " npm folders from temp directory"); 76 | } 77 | 78 | readEnvironmentVariables(); 79 | removeNodeModules(); 80 | removeTempFolders(); 81 | cp.safeExec.bind(global, 'npm install npm --loglevel error', {cwd: path.resolve(__dirname, '..', 'build')}, function() { 82 | cp.safeExec.bind(global, 'node script/bootstrap', function(error) { 83 | if (error) 84 | process.exit(1); 85 | require('fs-plus').removeSync.bind(global, path.join(homeDir, '.atom')) 86 | var async = require('async'); 87 | var gruntPath = path.join('build', 'node_modules', '.bin', 'grunt') + (process.platform === 'win32' ? '.cmd' : ''); 88 | var tasks = [ 89 | cp.safeExec.bind(global, 'git clean -dff'), 90 | cp.safeExec.bind(global, gruntPath + ' ci --gruntfile build/Gruntfile.coffee --stack --no-color'), 91 | ] 92 | async.series(tasks, function(error) { 93 | process.exit(error ? 1 : 0); 94 | }); 95 | })(); 96 | })(); 97 | -------------------------------------------------------------------------------- /script/clean: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var cp = require('./utils/child-process-wrapper.js'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var os = require('os'); 6 | 7 | var removeCommand = process.platform === 'win32' ? 'rmdir /S /Q ' : 'rm -rf '; 8 | var productName = require('../package.json').productName; 9 | 10 | process.chdir(path.dirname(__dirname)); 11 | var home = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; 12 | var tmpdir = os.tmpdir(); 13 | 14 | // Windows: Use START as a way to ignore error if Atom.exe isnt running 15 | var killatom = process.platform === 'win32' ? 'START taskkill /F /IM ' + productName + '.exe' : 'pkill -9 ' + productName + ' || true'; 16 | 17 | var commands = [ 18 | killatom, 19 | [__dirname, '..', 'node_modules'], 20 | [__dirname, '..', 'build', 'node_modules'], 21 | [__dirname, '..', 'apm', 'node_modules'], 22 | [__dirname, '..', 'atom-shell'], 23 | [home, '.atom', '.node-gyp'], 24 | [home, '.atom', 'storage'], 25 | [home, '.atom', '.apm'], 26 | [home, '.atom', '.npm'], 27 | [home, '.atom', 'compile-cache'], 28 | [home, '.atom', 'atom-shell'], 29 | [tmpdir, 'atom-build'], 30 | [tmpdir, 'atom-cached-atom-shells'], 31 | ]; 32 | var run = function() { 33 | var next = commands.shift(); 34 | if (!next) 35 | process.exit(0); 36 | 37 | if (Array.isArray(next)) { 38 | var pathToRemove = path.resolve.apply(path.resolve, next); 39 | if (fs.existsSync(pathToRemove)) 40 | next = removeCommand + pathToRemove; 41 | else 42 | return run(); 43 | } 44 | 45 | cp.safeExec(next, run); 46 | }; 47 | run(); 48 | -------------------------------------------------------------------------------- /script/clean.cmd: -------------------------------------------------------------------------------- 1 | @IF EXIST "%~dp0\node.exe" ( 2 | "%~dp0\node.exe" "%~dp0\clean" %* 3 | ) ELSE ( 4 | node "%~dp0\clean" %* 5 | ) 6 | -------------------------------------------------------------------------------- /script/copy-folder.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set USAGE=Usage: %0 source destination 4 | 5 | if [%1] == [] ( 6 | echo %USAGE% 7 | exit 1 8 | ) 9 | if [%2] == [] ( 10 | echo %USAGE% 11 | exit 2 12 | ) 13 | 14 | :: rm -rf %2 15 | if exist %2 rmdir %2 /s /q 16 | 17 | :: cp -rf %1 %2 18 | (robocopy %1 %2 /e) ^& IF %ERRORLEVEL% LEQ 1 exit 0 19 | -------------------------------------------------------------------------------- /script/create-shortcut.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set USAGE=Usage: %0 source name-on-desktop 4 | 5 | if [%1] == [] ( 6 | echo %USAGE% 7 | exit 1 8 | ) 9 | if [%2] == [] ( 10 | echo %USAGE% 11 | exit 2 12 | ) 13 | 14 | set SCRIPT="%TEMP%\%RANDOM%-%RANDOM%-%RANDOM%-%RANDOM%.vbs" 15 | 16 | echo Set oWS = WScript.CreateObject("WScript.Shell") >> %SCRIPT% 17 | echo sLinkFile = "%USERPROFILE%\Desktop\%2.lnk" >> %SCRIPT% 18 | echo Set oLink = oWS.CreateShortcut(sLinkFile) >> %SCRIPT% 19 | echo oLink.TargetPath = %1 >> %SCRIPT% 20 | echo oLink.Save >> %SCRIPT% 21 | 22 | cscript /nologo %SCRIPT% 23 | del %SCRIPT% 24 | -------------------------------------------------------------------------------- /script/grunt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var cp = require('./utils/child-process-wrapper.js'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | 6 | // node build/node_modules/.bin/grunt "$@" 7 | var gruntPath = path.resolve(__dirname, '..', 'build', 'node_modules', '.bin', 'grunt') + (process.platform === 'win32' ? '.cmd' : ''); 8 | 9 | if (!fs.existsSync(gruntPath)) { 10 | console.error('Grunt command does not exist at: ' + gruntPath); 11 | console.error('Run script/bootstrap to install Grunt'); 12 | process.exit(1); 13 | } 14 | 15 | var args = ['--gruntfile', path.resolve('build', 'Gruntfile.coffee')]; 16 | args = args.concat(process.argv.slice(2)); 17 | cp.safeSpawn(gruntPath, args, process.exit); 18 | -------------------------------------------------------------------------------- /script/grunt.cmd: -------------------------------------------------------------------------------- 1 | @IF EXIST "%~dp0\node.exe" ( 2 | "%~dp0\node.exe" "%~dp0\grunt" %* 3 | ) ELSE ( 4 | node "%~dp0\grunt" %* 5 | ) 6 | -------------------------------------------------------------------------------- /script/set-version: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | BUILT_PRODUCTS_DIR=$1 6 | VERSION=$2 7 | PLIST_PATH="$BUILT_PRODUCTS_DIR/Messenger.app/Contents/Info.plist" 8 | HELPER_PLIST_PATH="$BUILT_PRODUCTS_DIR/Messenger.app/Contents/Frameworks/Atom Helper.app/Contents/Info.plist" 9 | 10 | # Update version 11 | /usr/libexec/PlistBuddy -c "Set CFBundleShortVersionString $VERSION" "$PLIST_PATH" 12 | /usr/libexec/PlistBuddy -c "Set CFBundleVersion $VERSION" "$PLIST_PATH" 13 | /usr/libexec/PlistBuddy -c "Set CFBundleShortVersionString $VERSION" "$HELPER_PLIST_PATH" 14 | /usr/libexec/PlistBuddy -c "Set CFBundleVersion $VERSION" "$HELPER_PLIST_PATH" 15 | -------------------------------------------------------------------------------- /script/utils/child-process-wrapper.js: -------------------------------------------------------------------------------- 1 | var childProcess = require('child_process'); 2 | 3 | // Exit the process if the command failed and only call the callback if the 4 | // command succeed, output of the command would also be piped. 5 | exports.safeExec = function(command, options, callback) { 6 | if (!callback) { 7 | callback = options; 8 | options = {}; 9 | } 10 | if (!options) 11 | options = {}; 12 | 13 | // This needed to be increased for `apm test` runs that generate many failures 14 | // The default is 200KB. 15 | options.maxBuffer = 1024 * 1024; 16 | 17 | var child = childProcess.exec(command, options, function(error, stdout, stderr) { 18 | if (error) 19 | process.exit(error.code || 1); 20 | else 21 | callback(null); 22 | }); 23 | child.stderr.pipe(process.stderr); 24 | if (!options.ignoreStdout) 25 | child.stdout.pipe(process.stdout); 26 | } 27 | 28 | // Same with safeExec but call child_process.spawn instead. 29 | exports.safeSpawn = function(command, args, options, callback) { 30 | if (!callback) { 31 | callback = options; 32 | options = {}; 33 | } 34 | var child = childProcess.spawn(command, args, options); 35 | child.stderr.pipe(process.stderr); 36 | child.stdout.pipe(process.stdout); 37 | child.on('error', function(error) { 38 | console.error('Command \'' + command + '\' failed: ' + error.message); 39 | }); 40 | child.on('exit', function(code) { 41 | if (code != 0) 42 | process.exit(code); 43 | else 44 | callback(null); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /script/utils/clean-merged-branches: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #/ Usage: clean-merged-branches [-f] 3 | #/ Delete merged branches from the origin remote. 4 | #/ 5 | #/ Options: 6 | #/ -f Really delete the branches. Without this branches are shown 7 | #/ but nothing is deleted. 8 | 9 | set -e 10 | 11 | # show usage maybe 12 | [ "$1" = "--help" ] && { 13 | grep '^#/' <"$0"| cut -c4- 14 | exit 0 15 | } 16 | 17 | # fetch and prune remote branches 18 | git fetch origin --prune 19 | 20 | # grab list of merged branches 21 | branches=$( 22 | git branch -a --merged origin/master | 23 | grep remotes/origin/ | 24 | grep -v /master | 25 | sed 's@remotes/origin/@@' 26 | ) 27 | 28 | # bail out with no branches 29 | [ -z "$branches" ] && { 30 | echo "no merged branches detected" 1>&2 31 | exit 0 32 | } 33 | 34 | # delete the branches or just show what would be done without -f 35 | if [ "$1" = -f ]; then 36 | git push origin $(echo "$branches" | sed 's/^ */:/') 37 | else 38 | echo "These branches will be deleted:" 1>&2 39 | echo "$branches" 40 | echo "Run \`$0 -f' if you're sure." 41 | fi 42 | -------------------------------------------------------------------------------- /script/utils/clean-open-with-menu: -------------------------------------------------------------------------------- 1 | /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user 2 | killall Finder 3 | -------------------------------------------------------------------------------- /script/utils/compile-main-to-app: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | coffee -c -o /Applications/Atom.app/Contents/Resources/app/src/ src/main.coffee 4 | -------------------------------------------------------------------------------- /script/utils/fix-author: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | usage() { 4 | echo "usage: $0 sha name email" 5 | exit 1 6 | } 7 | 8 | if [ ! $3 ]; then 9 | usage 10 | fi 11 | 12 | git filter-branch -f --env-filter " 13 | export GIT_AUTHOR_NAME='$2' 14 | export GIT_AUTHOR_EMAIL='$3' 15 | export GIT_COMMITTER_NAME='$2' 16 | export GIT_COMMITTER_EMAIL='$3' 17 | " -- $1..HEAD -------------------------------------------------------------------------------- /script/utils/translate-crash-log-addresses.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | # Usage: 3 | # Copy the crash log into pasteboard and then run 4 | # pbpaste | ./script/translate-crash-log-addresses.coffee 5 | 6 | atos = (addresses, callback) -> 7 | path = require 'path' 8 | exec = require('child_process').exec 9 | 10 | cwd = path.join __dirname, '..' 11 | exec 'atos -o cef/Release/libcef.dylib -arch i386 '.concat(addresses...), cwd: cwd, (error, stdout, stderr) -> 12 | throw error if error? 13 | callback stdout.split('\n') 14 | 15 | parse_stack_trace = (raw) -> 16 | lines = {} 17 | addresses = [] 18 | for line in raw 19 | columns = line.split /\ +/ 20 | if columns[1] is 'libcef.dylib' and /0x[a-f0-9]+/.test columns[3] 21 | lines[columns[0]] = addresses.length 22 | addresses.push '0x' + parseInt(columns[5]).toString(16) + ' ' 23 | 24 | atos addresses, (parsed) -> 25 | for line in raw 26 | columns = line.split /\ +/ 27 | frame = columns[0] 28 | if lines[frame]? 29 | console.log frame, parsed[lines[frame]] 30 | else 31 | console.log line 32 | 33 | parse_log_file = (content) -> 34 | state = 'start' 35 | stack_trace = [] 36 | lines = content.split /\r?\n/ 37 | 38 | for line in lines 39 | if state is 'start' 40 | if /Thread \d+ Crashed::/.test line 41 | console.log line 42 | state = 'parse' 43 | else if state is 'parse' 44 | break if line is '' 45 | stack_trace.push line 46 | 47 | parse_stack_trace stack_trace 48 | 49 | input = '' 50 | process.stdin.resume() 51 | process.stdin.setEncoding 'utf8' 52 | process.stdin.on 'data', (chunk) -> 53 | input += chunk 54 | process.stdin.on 'end', -> 55 | parse_log_file input 56 | -------------------------------------------------------------------------------- /script/utils/verify-requirements.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var childProcess = require('child_process'); 4 | 5 | var pythonExecutable = process.env.PYTHON; 6 | 7 | module.exports = function(cb) { 8 | verifyNode(function(error, nodeSuccessMessage) { 9 | if (error) { 10 | cb(error); 11 | return; 12 | } 13 | 14 | verifyNpm(function(error, npmSuccessMessage) { 15 | if (error) { 16 | cb(error); 17 | return; 18 | } 19 | 20 | verifyPython27(function(error, pythonSuccessMessage) { 21 | cb(error, (nodeSuccessMessage + "\n" + npmSuccessMessage + "\n" + pythonSuccessMessage).trim()); 22 | }); 23 | }); 24 | }); 25 | 26 | }; 27 | 28 | function verifyNode(cb) { 29 | var nodeVersion = process.versions.node; 30 | var versionArray = nodeVersion.split('.'); 31 | var nodeMajorVersion = +versionArray[0]; 32 | var nodeMinorVersion = +versionArray[1]; 33 | if (nodeMajorVersion === 0 && nodeMinorVersion < 10) { 34 | error = "node v0.10 is required to build Atom, node " + nodeVersion + " is installed."; 35 | cb(error); 36 | } 37 | else { 38 | cb(null, "Node: v" + nodeVersion); 39 | } 40 | } 41 | 42 | function verifyNpm(cb) { 43 | var localNpmPath = path.resolve(__dirname, '..', '..', 'build', 'node_modules', '.bin', 'npm'); 44 | if (process.platform === 'win32') 45 | localNpmPath += ".cmd"; 46 | 47 | var npmCommand = fs.existsSync(localNpmPath) ? localNpmPath : 'npm'; 48 | if (npmCommand === 'npm' && process.platform === 'win32') 49 | npmCommand += ".cmd"; 50 | 51 | childProcess.execFile(npmCommand, ['-v'], { env: process.env }, function(err, stdout) { 52 | if (err) 53 | return cb("npm 1.4 is required to build Atom. An error (" + err + ") occured when checking the version."); 54 | 55 | var npmVersion = stdout ? stdout.trim() : ''; 56 | var versionArray = npmVersion.split('.'); 57 | var npmMajorVersion = +versionArray[0] || 0; 58 | var npmMinorVersion = +versionArray[1] || 0; 59 | if (npmMajorVersion === 1 && npmMinorVersion < 4) 60 | cb("npm v1.4+ is required to build Atom. Version " + npmVersion + " was detected."); 61 | else 62 | cb(null, "npm: v" + npmVersion); 63 | }); 64 | } 65 | 66 | 67 | function verifyPython27(cb) { 68 | if (process.platform == 'win32') { 69 | if (!pythonExecutable) { 70 | var systemDrive = process.env.SystemDrive || 'C:\\'; 71 | pythonExecutable = path.join(systemDrive, 'Python27', 'python.exe'); 72 | 73 | if (!fs.existsSync(pythonExecutable)) { 74 | pythonExecutable = 'python'; 75 | } 76 | } 77 | 78 | checkPythonVersion(pythonExecutable, cb); 79 | } 80 | else { 81 | cb(null, ''); 82 | } 83 | } 84 | 85 | function checkPythonVersion (python, cb) { 86 | var pythonHelpMessage = "Set the PYTHON env var to '/path/to/Python27/python.exe' if your python is installed in a non-default location."; 87 | 88 | childProcess.execFile(python, ['-c', 'import platform; print(platform.python_version());'], { env: process.env }, function (err, stdout) { 89 | if (err) { 90 | error = "Python 2.7 is required to build Atom. An error (" + err + ") occured when checking the version of '" + python + "'. "; 91 | error += pythonHelpMessage; 92 | cb(error); 93 | return; 94 | } 95 | 96 | var version = stdout.trim(); 97 | if (~version.indexOf('+')) { 98 | version = version.replace(/\+/g, ''); 99 | } 100 | if (~version.indexOf('rc')) { 101 | version = version.replace(/rc(.*)$/ig, ''); 102 | } 103 | 104 | // Atom requires python 2.7 or higher (but not python 3) for node-gyp 105 | var versionArray = version.split('.').map(function(num) { return +num; }); 106 | var goodPythonVersion = (versionArray[0] === 2 && versionArray[1] >= 7); 107 | if (!goodPythonVersion) { 108 | error = "Python 2.7 is required to build Atom. '" + python + "' returns version " + version + ". "; 109 | error += pythonHelpMessage; 110 | cb(error); 111 | return; 112 | } 113 | 114 | // Finally, if we've gotten this far, callback to resume the install process. 115 | cb(null, "Python: v" + version); 116 | }); 117 | } 118 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | var app = require('app'); 2 | var ipc = require('ipc'); 3 | var BrowserWindow = require('browser-window'); 4 | var path = require('path'); 5 | var AutoUpdateManager = require('./updates'); 6 | var dialog = require('dialog'); 7 | 8 | var staticPath = path.join(__dirname, '..', 'static'); 9 | 10 | var mainWindow = null; 11 | 12 | // App 13 | app.on('ready', function () { 14 | mainWindow = new BrowserWindow({ 15 | width: 800, 16 | height: 600, 17 | frame: false, 18 | resizable: false, 19 | 'web-preferences': { 20 | plugins: true 21 | }, 22 | title: 'Loading' 23 | }); 24 | 25 | mainWindow.loadUrl('file://' + staticPath + '/app.html'); 26 | }); 27 | 28 | // API 29 | ipc.on('do-native-action', function(event, action) { 30 | switch (action) { 31 | case 'close': 32 | app.quit(); 33 | break; 34 | 35 | case 'minimize': 36 | mainWindow.minimize(); 37 | break; 38 | } 39 | }); 40 | 41 | // Auto updater 42 | 43 | updater = new AutoUpdateManager(app.getVersion()) 44 | updater.on('state-changed', function () { 45 | if (updater.state === 'update-available') { 46 | dialog.showMessageBox({ 47 | type: 'info', 48 | buttons: ['Install and Relaunch', 'Not now'], 49 | message: 'Messenger Update Available', 50 | detail: "New version of Messenger is available and ready to be installed.", 51 | }, function (result) { 52 | if (result === 0) { 53 | updater.install(); 54 | } 55 | }) 56 | } 57 | }) 58 | -------------------------------------------------------------------------------- /src/updates.coffee: -------------------------------------------------------------------------------- 1 | autoUpdater = require 'auto-updater' 2 | dialog = require 'dialog' 3 | {EventEmitter} = require 'events' 4 | 5 | IDLE_STATE='idle' 6 | CHECKING_STATE='checking' 7 | DOWNLOADING_STATE='downloading' 8 | UPDATE_AVAILABLE_STATE='update-available' 9 | NO_UPDATE_AVAILABLE_STATE='no-update-available' 10 | ERROR_STATE='error' 11 | 12 | module.exports = 13 | class AutoUpdateManager extends EventEmitter 14 | 15 | constructor: (@version) -> 16 | @state = IDLE_STATE 17 | 18 | # Only released versions should check for updates. 19 | return if /\w{7}/.test(@version) 20 | 21 | autoUpdater.setFeedUrl "https://messenger-updates.herokuapp.com/api/updates?version=#{@version}" 22 | 23 | autoUpdater.on 'checking-for-update', => 24 | @setState(CHECKING_STATE) 25 | 26 | autoUpdater.on 'update-not-available', => 27 | @setState(NO_UPDATE_AVAILABLE_STATE) 28 | 29 | autoUpdater.on 'update-available', => 30 | @setState(DOWNLOADING_STATE) 31 | 32 | autoUpdater.on 'error', (event, message) => 33 | @setState(ERROR_STATE) 34 | console.error "Error Downloading Update: #{message}" 35 | 36 | autoUpdater.on 'update-downloaded', (event, @releaseNotes, @releaseVersion) => 37 | @setState(UPDATE_AVAILABLE_STATE) 38 | @emitUpdateAvailableEvent(@getWindows()...) 39 | 40 | @check(hidePopups: true) 41 | 42 | emitUpdateAvailableEvent: (windows...) -> 43 | return unless @releaseVersion? and @releaseNotes 44 | for atomWindow in windows 45 | atomWindow.sendCommand('window:update-available', [@releaseVersion, @releaseNotes]) 46 | 47 | setState: (state) -> 48 | return unless @state != state 49 | @state = state 50 | @emit 'state-changed', @state 51 | 52 | getState: -> 53 | @state 54 | 55 | check: ({hidePopups}={})-> 56 | unless hidePopups 57 | autoUpdater.once 'update-not-available', @onUpdateNotAvailable 58 | autoUpdater.once 'error', @onUpdateError 59 | 60 | autoUpdater.checkForUpdates() 61 | 62 | install: -> 63 | autoUpdater.quitAndInstall() 64 | 65 | onUpdateNotAvailable: => 66 | autoUpdater.removeListener 'error', @onUpdateError 67 | dialog.showMessageBox type: 'info', buttons: ['OK'], message: 'No update available.', detail: "Version #{@version} is the latest version." 68 | 69 | onUpdateError: (event, message) => 70 | autoUpdater.removeListener 'update-not-available', @onUpdateNotAvailable 71 | dialog.showMessageBox type: 'warning', buttons: ['OK'], message: 'There was an error checking for updates.', detail: message 72 | 73 | getWindows: -> 74 | return [] 75 | -------------------------------------------------------------------------------- /static/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Messenger 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 |
14 |
15 | Messenger 16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /static/css/app.css: -------------------------------------------------------------------------------- 1 | /* General styling */ 2 | * { 3 | padding: 0; 4 | margin: 0; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | overflow: hidden; 10 | -webkit-touch-callout: none; 11 | -webkit-user-select: none; 12 | cursor: default; 13 | } 14 | 15 | /* Top bar */ 16 | #top-bar { 17 | border-bottom: 1px solid rgba(0, 0, 0, 0); 18 | padding: 8px; 19 | height: 32px; 20 | width: 100%; 21 | -webkit-app-region: drag; 22 | text-align: center; 23 | } 24 | 25 | /* Action buttons */ 26 | #window-btns { 27 | -webkit-transform: translate3d(-60px, 0px, 0px); 28 | display: inline-block; 29 | position: absolute; 30 | top: 8px; 31 | left: 10px; 32 | z-index: 2; 33 | } 34 | 35 | #window-btns .btn { 36 | -webkit-app-region: no-drag!important; 37 | display: inline-block; 38 | width: 12px; 39 | height: 12px; 40 | text-align: center; 41 | border-radius: 6px; 42 | } 43 | 44 | #window-btns .btn + .btn { 45 | margin-left: 5px; 46 | } 47 | 48 | #window-btns .btn i { 49 | -webkit-transition: opacity cubic-bezier(0.645, 0.045, 0.355, 1) 0.1s; 50 | display: block; 51 | opacity: 0; 52 | } 53 | 54 | #window-btns .btn:hover i { 55 | opacity: 1; 56 | } 57 | 58 | #btn-close { 59 | background: #ed6555; 60 | } 61 | 62 | #btn-close i { 63 | width: 6px; 64 | height: 6px; 65 | margin: 3px auto 0; 66 | background-color: #b61402; 67 | border-radius: 3px; 68 | } 69 | 70 | #btn-minimize { 71 | background: #f4b860; 72 | } 73 | 74 | #btn-minimize i { 75 | width: 6px; 76 | height: 2px; 77 | margin: 5px auto 0; 78 | background-color: #ca8117; 79 | } 80 | 81 | /* Window Title */ 82 | #title { 83 | font: 14px/18px Helvetica, Arial, sans-serif; 84 | z-index: -1; 85 | -webkit-transform: translate3d(0px, -60px, 0px); 86 | } 87 | 88 | /* Loading animations */ 89 | .ready #top-bar { 90 | -webkit-transition: border-bottom-color 1.2s cubic-bezier(0.645, 0.045, 0.355, 1) 0.4s; 91 | border-bottom-color: rgba(0, 0, 0, 0.2); 92 | } 93 | 94 | .ready #window-btns, 95 | .ready #title { 96 | -webkit-transform: translate3d(0px, 0px, 0px); 97 | -webkit-transition: -webkit-transform 0.4s cubic-bezier(0.645, 0.045, 0.355, 1) 0.4s; 98 | } 99 | 100 | /* Messenger area */ 101 | #messenger { 102 | width: 800px; 103 | height: 612px; 104 | display: inline-block; 105 | margin: 0; 106 | z-index: 2; 107 | } 108 | -------------------------------------------------------------------------------- /static/images/messenger.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marekhrabe/messenger/efdbbf5adc78c5d6855a7335f5a9ad1329c7ffa3/static/images/messenger.icns -------------------------------------------------------------------------------- /static/js/app.coffee: -------------------------------------------------------------------------------- 1 | remote = require 'remote' 2 | app = remote.require 'app' 3 | ipc = require 'ipc' 4 | shell = require 'shell' 5 | 6 | # Window buttons 7 | addButtonListener = (action) -> 8 | document.querySelector("#btn-#{action}").addEventListener 'click', (e) -> 9 | e.preventDefault() 10 | ipc.send('do-native-action', action) 11 | 12 | addButtonListener(action) for action in ['close', 'minimize'] 13 | 14 | # Show window UI 15 | document.body.classList.add('ready') 16 | 17 | # Handle webview 18 | titleElement = document.querySelector('#title') 19 | 20 | webview = document.getElementById("messenger") 21 | webview.addEventListener 'did-finish-load', -> 22 | webview.focus() 23 | webview.addEventListener 'new-window', (e) -> 24 | e.preventDefault() 25 | shell.openExternal(e.url) 26 | 27 | # Autofocus Messenger webview 28 | window.addEventListener 'focus', -> 29 | webview.focus() 30 | 31 | # Handle changes 32 | lastTitle = null 33 | setInterval -> 34 | title = webview.getTitle() 35 | if title and title isnt lastTitle and title.indexOf('http') isnt 0 36 | titleElement.innerHTML = title 37 | 38 | badgeResult = (/(?:\(([0-9])\) )?messenger/ig).exec(title) 39 | if badgeResult 40 | app.dock.setBadge(badgeResult[1] or '') 41 | 42 | lastTitle = title 43 | , 300 44 | 45 | # Menus 46 | require('./js/menu') 47 | -------------------------------------------------------------------------------- /static/js/menu.coffee: -------------------------------------------------------------------------------- 1 | ipc = require 'ipc' 2 | remote = require 'remote' 3 | BrowserWindow = remote.require 'browser-window' 4 | Menu = remote.require 'menu' 5 | MenuItem = remote.require 'menu-item' 6 | 7 | template = [ 8 | { 9 | label: 'Messenger' 10 | submenu: [ 11 | { 12 | label: 'About Messenger' 13 | selector: 'orderFrontStandardAboutPanel:' 14 | } 15 | { type: 'separator' } 16 | { 17 | label: 'Services' 18 | submenu: [] 19 | } 20 | { type: 'separator' } 21 | { 22 | label: 'Hide Messenger' 23 | accelerator: 'Command+H' 24 | selector: 'hide:' 25 | } 26 | { 27 | label: 'Hide Others' 28 | accelerator: 'Command+Shift+H' 29 | selector: 'hideOtherApplications:' 30 | } 31 | { 32 | label: 'Show All' 33 | selector: 'unhideAllApplications:' 34 | } 35 | { type: 'separator' } 36 | { 37 | label: 'Quit' 38 | accelerator: 'Command+Q' 39 | click: -> ipc.send('do-native-action', 'close') 40 | } 41 | ] 42 | } 43 | { 44 | label: 'Edit' 45 | submenu: [ 46 | { 47 | label: 'Undo' 48 | accelerator: 'Command+Z' 49 | selector: 'undo:' 50 | } 51 | { 52 | label: 'Redo' 53 | accelerator: 'Shift+Command+Z' 54 | selector: 'redo:' 55 | } 56 | { type: 'separator' } 57 | { 58 | label: 'Cut' 59 | accelerator: 'Command+X' 60 | selector: 'cut:' 61 | } 62 | { 63 | label: 'Copy' 64 | accelerator: 'Command+C' 65 | selector: 'copy:' 66 | } 67 | { 68 | label: 'Paste' 69 | accelerator: 'Command+V' 70 | selector: 'paste:' 71 | } 72 | { 73 | label: 'Select All' 74 | accelerator: 'Command+A' 75 | selector: 'selectAll:' 76 | } 77 | ] 78 | } 79 | { 80 | label: 'View' 81 | submenu: [ 82 | { 83 | label: 'Reload' 84 | accelerator: 'Command+R' 85 | click: -> BrowserWindow.getFocusedWindow().reloadIgnoringCache() 86 | } 87 | { 88 | label: 'Toggle DevTools' 89 | accelerator: 'Alt+Command+I' 90 | click: -> BrowserWindow.getFocusedWindow().toggleDevTools() 91 | } 92 | ] 93 | } 94 | { 95 | label: 'Window' 96 | submenu: [ 97 | { 98 | label: 'Minimize' 99 | accelerator: 'Command+M' 100 | selector: 'performMiniaturize:' 101 | } 102 | { 103 | label: 'Close' 104 | accelerator: 'Command+W' 105 | selector: 'performClose:' 106 | } 107 | { type: 'separator' } 108 | { 109 | label: 'Bring All to Front' 110 | selector: 'arrangeInFront:' 111 | } 112 | ] 113 | } 114 | { 115 | label: 'Help' 116 | submenu: [] 117 | } 118 | ] 119 | 120 | appMenu = Menu.buildFromTemplate(template) 121 | Menu.setApplicationMenu appMenu 122 | --------------------------------------------------------------------------------