AtomWindow = require './atom-window' ApplicationMenu = require './application-menu' AtomProtocolHandler = require './atom-protocol-handler' AutoUpdateManager = require './auto-update-manager' BrowserWindow = require 'browser-window' Menu = require 'menu' app = require 'app' fs = require 'fs' ipc = require 'ipc' path = require 'path' os = require 'os' net = require 'net' url = require 'url' {EventEmitter} = require 'events' _ = require 'underscore-plus' socketPath = if process.platform is 'win32' '\\\\.\\pipe\\atom-sock' else path.join(os.tmpdir(), 'atom.sock') # The application's singleton class. # # It's the entry point into the Atom application and maintains the global state # of the application. # module.exports = class AtomApplication _.extend @prototype, EventEmitter.prototype # Public: The entry point into the Atom application. @open: (options) -> createAtomApplication = -> new AtomApplication(options) # FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely # take a few seconds to trigger 'error' event, it could be a bug of node # or atom-shell, before it's fixed we check the existence of socketPath to # speedup startup. if (process.platform isnt 'win32' and not fs.existsSync socketPath) or options.test createAtomApplication() return client = net.connect {path: socketPath}, -> client.write JSON.stringify(options), -> client.end() app.terminate() client.on 'error', createAtomApplication windows: null applicationMenu: null atomProtocolHandler: null resourcePath: null version: null exit: (status) -> app.exit(status) constructor: (options) -> {@resourcePath, @version, @devMode, @safeMode} = options # Normalize to make sure drive letter case is consistent on Windows @resourcePath = path.normalize(@resourcePath) if @resourcePath global.atomApplication = this @pidsToOpenWindows = {} @pathsToOpen ?= [] @windows = [] @autoUpdateManager = new AutoUpdateManager(@version) @applicationMenu = new ApplicationMenu(@version) @atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode) @listenForArgumentsFromNewProcess() @setupJavaScriptArguments() @handleEvents() @openWithOptions(options) # Opens a new window based on the options provided. openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory, logFile}) -> if test @runSpecs({exitWhenDone: true, @resourcePath, specDirectory, logFile}) else if pathsToOpen.length > 0 @openPaths({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode}) else if urlsToOpen.length > 0 @openUrl({urlToOpen, devMode, safeMode}) for urlToOpen in urlsToOpen else @openPath({pidToKillWhenClosed, newWindow, devMode, safeMode}) # Always open a editor window if this is the first instance of Atom. # Public: Removes the {AtomWindow} from the global window list. removeWindow: (window) -> @windows.splice @windows.indexOf(window), 1 @applicationMenu?.enableWindowSpecificItems(false) if @windows.length == 0 # Public: Adds the {AtomWindow} to the global window list. addWindow: (window) -> @windows.push window @applicationMenu?.addWindow(window.browserWindow) window.once 'window:loaded', => @autoUpdateManager.emitUpdateAvailableEvent(window) unless window.isSpec focusHandler = => @lastFocusedWindow = window window.browserWindow.on 'focus', focusHandler window.browserWindow.once 'closed', => @lastFocusedWindow = null if window is @lastFocusedWindow window.browserWindow.removeListener 'focus', focusHandler # Creates server to listen for additional atom application launches. # # You can run the atom command multiple times, but after the first launch # the other launches will just pass their information to this server and then # close immediately. listenForArgumentsFromNewProcess: -> @deleteSocketFile() server = net.createServer (connection) => connection.on 'data', (data) => @openWithOptions(JSON.parse(data)) server.listen socketPath server.on 'error', (error) -> console.error 'Application server failed', error deleteSocketFile: -> return if process.platform is 'win32' if fs.existsSync(socketPath) try fs.unlinkSync(socketPath) catch error # Ignore ENOENT errors in case the file was deleted between the exists # check and the call to unlink sync. This occurred occasionally on CI # which is why this check is here. throw error unless error.code is 'ENOENT' # Configures required javascript environment flags. setupJavaScriptArguments: -> app.commandLine.appendSwitch 'js-flags', '--harmony' # Registers basic application commands, non-idempotent. handleEvents: -> @on 'application:run-all-specs', -> @runSpecs(exitWhenDone: false, resourcePath: global.devResourcePath, safeMode: @focusedWindow()?.safeMode) @on 'application:run-benchmarks', -> @runBenchmarks() @on 'application:quit', -> app.quit() @on 'application:new-window', -> @openPath(windowDimensions: @focusedWindow()?.getDimensions()) @on 'application:new-file', -> (@focusedWindow() ? this).openPath() @on 'application:open', -> @promptForPath(type: 'all') @on 'application:open-file', -> @promptForPath(type: 'file') @on 'application:open-folder', -> @promptForPath(type: 'folder') @on 'application:open-dev', -> @promptForPath(devMode: true) @on 'application:open-safe', -> @promptForPath(safeMode: true) @on 'application:inspect', ({x,y, atomWindow}) -> atomWindow ?= @focusedWindow() atomWindow?.browserWindow.inspectElement(x, y) @on 'application:open-documentation', -> require('shell').openExternal('https://atom.io/docs/latest/?app') @on 'application:open-discussions', -> require('shell').openExternal('https://discuss.atom.io') @on 'application:open-roadmap', -> require('shell').openExternal('https://atom.io/roadmap?app') @on 'application:open-faq', -> require('shell').openExternal('https://atom.io/faq') @on 'application:open-terms-of-use', -> require('shell').openExternal('https://atom.io/terms') @on 'application:report-issue', -> require('shell').openExternal('https://github.com/atom/atom/issues/new') @on 'application:search-issues', -> require('shell').openExternal('https://github.com/issues?q=+is%3Aissue+user%3Aatom') @on 'application:install-update', -> @autoUpdateManager.install() @on 'application:check-for-update', => @autoUpdateManager.check() if process.platform is 'darwin' @on 'application:about', -> Menu.sendActionToFirstResponder('orderFrontStandardAboutPanel:') @on 'application:bring-all-windows-to-front', -> Menu.sendActionToFirstResponder('arrangeInFront:') @on 'application:hide', -> Menu.sendActionToFirstResponder('hide:') @on 'application:hide-other-applications', -> Menu.sendActionToFirstResponder('hideOtherApplications:') @on 'application:minimize', -> Menu.sendActionToFirstResponder('performMiniaturize:') @on 'application:unhide-all-applications', -> Menu.sendActionToFirstResponder('unhideAllApplications:') @on 'application:zoom', -> Menu.sendActionToFirstResponder('zoom:') else @on 'application:minimize', -> @focusedWindow()?.minimize() @on 'application:zoom', -> @focusedWindow()?.maximize() @openPathOnEvent('application:show-settings', 'atom://config') @openPathOnEvent('application:open-your-config', 'atom://.atom/config') @openPathOnEvent('application:open-your-init-script', 'atom://.atom/init-script') @openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap') @openPathOnEvent('application:open-your-snippets', 'atom://.atom/snippets') @openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet') @openPathOnEvent('application:open-license', path.join(@resourcePath, 'LICENSE.md')) app.on 'window-all-closed', -> app.quit() if process.platform in ['win32', 'linux'] app.on 'will-quit', => @killAllProcesses() @deleteSocketFile() app.on 'will-exit', => @killAllProcesses() @deleteSocketFile() app.on 'open-file', (event, pathToOpen) => event.preventDefault() @openPath({pathToOpen}) app.on 'open-url', (event, urlToOpen) => event.preventDefault() @openUrl({urlToOpen, @devMode, @safeMode}) app.on 'activate-with-no-open-windows', (event) => event.preventDefault() @emit('application:new-window') # A request from the associated render process to open a new render process. ipc.on 'open', (event, options) => window = @windowForEvent(event) if options? if options.pathsToOpen?.length > 0 options.window = window @openPaths(options) else new AtomWindow(options) else @promptForPath({window}) ipc.on 'update-application-menu', (event, template, keystrokesByCommand) => win = BrowserWindow.fromWebContents(event.sender) @applicationMenu.update(win, template, keystrokesByCommand) ipc.on 'run-package-specs', (event, specDirectory) => @runSpecs({resourcePath: global.devResourcePath, specDirectory: specDirectory, exitWhenDone: false}) ipc.on 'command', (event, command) => @emit(command) ipc.on 'window-command', (event, command, args...) -> win = BrowserWindow.fromWebContents(event.sender) win.emit(command, args...) ipc.on 'call-window-method', (event, method, args...) -> win = BrowserWindow.fromWebContents(event.sender) win[method](args...) # Public: Executes the given command. # # If it isn't handled globally, delegate to the currently focused window. # # command - The string representing the command. # args - The optional arguments to pass along. sendCommand: (command, args...) -> unless @emit(command, args...) focusedWindow = @focusedWindow() if focusedWindow? focusedWindow.sendCommand(command, args...) else @sendCommandToFirstResponder(command) # Public: Executes the given command on the given window. # # command - The string representing the command. # atomWindow - The {AtomWindow} to send the command to. # args - The optional arguments to pass along. sendCommandToWindow: (command, atomWindow, args...) -> unless @emit(command, args...) if atomWindow? atomWindow.sendCommand(command, args...) else @sendCommandToFirstResponder(command) # Translates the command into OS X action and sends it to application's first # responder. sendCommandToFirstResponder: (command) -> return false unless process.platform is 'darwin' switch command when 'core:undo' then Menu.sendActionToFirstResponder('undo:') when 'core:redo' then Menu.sendActionToFirstResponder('redo:') when 'core:copy' then Menu.sendActionToFirstResponder('copy:') when 'core:cut' then Menu.sendActionToFirstResponder('cut:') when 'core:paste' then Menu.sendActionToFirstResponder('paste:') when 'core:select-all' then Menu.sendActionToFirstResponder('selectAll:') else return false true # Public: Open the given path in the focused window when the event is # triggered. # # A new window will be created if there is no currently focused window. # # eventName - The event to listen for. # pathToOpen - The path to open when the event is triggered. openPathOnEvent: (eventName, pathToOpen) -> @on eventName, -> if window = @focusedWindow() window.openPath(pathToOpen) else @openPath({pathToOpen}) # Returns the {AtomWindow} for the given path. windowForPath: (pathToOpen) -> _.find @windows, (atomWindow) -> atomWindow.containsPath(pathToOpen) # Returns the {AtomWindow} for the given ipc event. windowForEvent: ({sender}) -> window = BrowserWindow.fromWebContents(sender) _.find @windows, ({browserWindow}) -> window is browserWindow # Public: Returns the currently focused {AtomWindow} or undefined if none. focusedWindow: -> _.find @windows, (atomWindow) -> atomWindow.isFocused() # Public: Opens multiple paths, in existing windows if possible. # # options - # :pathsToOpen - The array of file paths to open # :pidToKillWhenClosed - The integer of the pid to kill # :newWindow - Boolean of whether this should be opened in a new window. # :devMode - Boolean to control the opened window's dev mode. # :safeMode - Boolean to control the opened window's safe mode. # :window - {AtomWindow} to open file paths in. openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, window}) -> for pathToOpen in pathsToOpen ? [] @openPath({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, window}) # Public: Opens a single path, in an existing window if possible. # # options - # :pathToOpen - The file path to open # :pidToKillWhenClosed - The integer of the pid to kill # :newWindow - Boolean of whether this should be opened in a new window. # :devMode - Boolean to control the opened window's dev mode. # :safeMode - Boolean to control the opened window's safe mode. # :windowDimensions - Object with height and width keys. # :window - {AtomWindow} to open file paths in. openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, window}={}) -> {pathToOpen, initialLine, initialColumn} = @locationForPathToOpen(pathToOpen) unless pidToKillWhenClosed or newWindow pathToOpenStat = fs.statSyncNoException(pathToOpen) # Default to using the specified window or the last focused window currentWindow = window ? @lastFocusedWindow if pathToOpenStat.isFile?() # Open the file in the current window existingWindow = currentWindow else if pathToOpenStat.isDirectory?() # Open the folder in the current window if it doesn't have a path existingWindow = currentWindow unless currentWindow?.hasProjectPath() # Don't reuse windows in dev mode existingWindow ?= @windowForPath(pathToOpen) unless devMode if existingWindow? openedWindow = existingWindow openedWindow.openPath(pathToOpen, initialLine) if openedWindow.isMinimized() openedWindow.restore() else openedWindow.focus() else if devMode try bootstrapScript = require.resolve(path.join(global.devResourcePath, 'src', 'window-bootstrap')) resourcePath = global.devResourcePath bootstrapScript ?= require.resolve('../window-bootstrap') resourcePath ?= @resourcePath openedWindow = new AtomWindow({pathToOpen, initialLine, initialColumn, bootstrapScript, resourcePath, devMode, safeMode, windowDimensions}) if pidToKillWhenClosed? @pidsToOpenWindows[pidToKillWhenClosed] = openedWindow openedWindow.browserWindow.once 'closed', => @killProcessForWindow(openedWindow) # Kill all processes associated with opened windows. killAllProcesses: -> @killProcess(pid) for pid of @pidsToOpenWindows # Kill process associated with the given opened window. killProcessForWindow: (openedWindow) -> for pid, trackedWindow of @pidsToOpenWindows @killProcess(pid) if trackedWindow is openedWindow # Kill the process with the given pid. killProcess: (pid) -> try parsedPid = parseInt(pid) process.kill(parsedPid) if isFinite(parsedPid) catch error if error.code isnt 'ESRCH' console.log("Killing process #{pid} failed: #{error.code ? error.message}") delete @pidsToOpenWindows[pid] # Open an atom:// url. # # The host of the URL being opened is assumed to be the package name # responsible for opening the URL. A new window will be created with # that package's `urlMain` as the bootstrap script. # # options - # :urlToOpen - The atom:// url to open. # :devMode - Boolean to control the opened window's dev mode. # :safeMode - Boolean to control the opened window's safe mode. openUrl: ({urlToOpen, devMode, safeMode}) -> unless @packages? PackageManager = require '../package-manager' fs = require 'fs-plus' @packages = new PackageManager configDirPath: fs.absolute('~/.atom') devMode: devMode resourcePath: @resourcePath packageName = url.parse(urlToOpen).host pack = _.find @packages.getAvailablePackageMetadata(), ({name}) -> name is packageName if pack? if pack.urlMain packagePath = @packages.resolvePackagePath(packageName) bootstrapScript = path.resolve(packagePath, pack.urlMain) windowDimensions = @focusedWindow()?.getDimensions() new AtomWindow({bootstrapScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions}) else console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}" else console.log "Opening unknown url: #{urlToOpen}" # Opens up a new {AtomWindow} to run specs within. # # options - # :exitWhenDone - A Boolean that, if true, will close the window upon # completion. # :resourcePath - The path to include specs from. # :specPath - The directory to load specs from. # :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages # and ~/.atom/dev/packages, defaults to false. runSpecs: ({exitWhenDone, resourcePath, specDirectory, logFile, safeMode}) -> if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath) resourcePath = @resourcePath try bootstrapScript = require.resolve(path.resolve(global.devResourcePath, 'spec', 'spec-bootstrap')) catch error bootstrapScript = require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'spec-bootstrap')) isSpec = true devMode = true safeMode ?= false new AtomWindow({bootstrapScript, resourcePath, exitWhenDone, isSpec, devMode, specDirectory, logFile, safeMode}) runBenchmarks: ({exitWhenDone, specDirectory}={}) -> try bootstrapScript = require.resolve(path.resolve(global.devResourcePath, 'benchmark', 'benchmark-bootstrap')) catch error bootstrapScript = require.resolve(path.resolve(__dirname, '..', '..', 'benchmark', 'benchmark-bootstrap')) specDirectory ?= path.dirname(bootstrapScript) isSpec = true devMode = true new AtomWindow({bootstrapScript, @resourcePath, exitWhenDone, isSpec, specDirectory, devMode}) locationForPathToOpen: (pathToOpen) -> return {pathToOpen} unless pathToOpen return {pathToOpen} if fs.existsSync(pathToOpen) [fileToOpen, initialLine, initialColumn] = path.basename(pathToOpen).split(':') return {pathToOpen} unless initialLine return {pathToOpen} unless parseInt(initialLine) > 0 # Convert line numbers to a base of 0 initialLine -= 1 if initialLine initialColumn -= 1 if initialColumn pathToOpen = path.join(path.dirname(pathToOpen), fileToOpen) {pathToOpen, initialLine, initialColumn} # Opens a native dialog to prompt the user for a path. # # Once paths are selected, they're opened in a new or existing {AtomWindow}s. # # options - # :type - A String which specifies the type of the dialog, could be 'file', # 'folder' or 'all'. The 'all' is only available on OS X. # :devMode - A Boolean which controls whether any newly opened windows # should be in dev mode or not. # :safeMode - A Boolean which controls whether any newly opened windows # should be in safe mode or not. # :window - An {AtomWindow} to use for opening a selected file path. promptForPath: ({type, devMode, safeMode, window}={}) -> type ?= 'all' properties = switch type when 'file' then ['openFile'] when 'folder' then ['openDirectory'] when 'all' then ['openFile', 'openDirectory'] else throw new Error("#{type} is an invalid type for promptForPath") # Show the open dialog as child window on Windows and Linux, and as # independent dialog on OS X. This matches most native apps. parentWindow = if process.platform is 'darwin' null else BrowserWindow.getFocusedWindow() openOptions = properties: properties.concat(['multiSelections', 'createDirectory']) title: 'Open' if process.platform is 'linux' if projectPath = @lastFocusedWindow?.projectPath openOptions.defaultPath = projectPath dialog = require 'dialog' dialog.showOpenDialog parentWindow, openOptions, (pathsToOpen) => @openPaths({pathsToOpen, devMode, safeMode, window})