diff --git a/lib/connection.coffee b/lib/connection.coffee index 56a9e4b7..cc82903f 100644 --- a/lib/connection.coffee +++ b/lib/connection.coffee @@ -24,17 +24,33 @@ module.exports = consumeInk: (ink) -> @IPC.consumeInk ink + @ink = ink consumeTerminal: (term) -> @terminal.consumeTerminal term - boot: -> + consumeGetServerConfig: (getconf) -> + @local.consumeGetServerConfig(getconf) + + consumeGetServerName: (name) -> + @local.consumeGetServerName(name) + + _boot: (provider) -> if not @client.isActive() and not @booting @booting = true - p = @local.start() + p = @local.start(provider) + @client.setBootMode(provider) + if @ink? + @ink.Opener.allowRemoteFiles(provider == 'Remote') p.then => @booting = false p.catch => @booting = false time "Julia Boot", @client.import('ping')().then => metrics() + + bootRemote: -> + @_boot('Remote') + + boot: -> + @_boot(atom.config.get('julia-client.juliaOptions.bootMode')) diff --git a/lib/connection/client.coffee b/lib/connection/client.coffee index 67bd079b..f6260610 100644 --- a/lib/connection/client.coffee +++ b/lib/connection/client.coffee @@ -28,6 +28,8 @@ module.exports = @emitter = new Emitter + @bootMode = atom.config.get('julia-client.juliaOptions.bootMode') + @ipc.writeMsg = (msg) => if @isActive() and @conn.ready?() isnt false @conn.message msg @@ -55,6 +57,25 @@ module.exports = @onDetached => plotpane?.dispose() + @onBoot (proc) => + @remoteConfig = proc.config + + setBootMode: (@bootMode) -> + + editorPath: (ed) -> + if not ed? then return ed + if @bootMode is 'Remote' and @remoteConfig? + path = ed.getPath() + if not path? then return path + ind = path.indexOf(@remoteConfig.host) + if ind > -1 + path = path.slice(ind + @remoteConfig.host.length, path.length) + path = path.replace(/\\/g, '/') + return path + else + return path + else + return ed.getPath() deactivate: -> @emitter.dispose() @@ -126,6 +147,10 @@ module.exports = else if atom.config.get('julia-client.consoleOptions.consoleStyle') is 'REPL-based' @clientCall 'interrupts', 'interruptREPL' + disconnect: -> + if @isActive() + @clientCall 'disconnecting', 'disconnect' + kill: -> if @isActive() if not @isWorking() @@ -150,7 +175,7 @@ module.exports = connectedError: (action = 'do that') -> if @isActive() atom.notifications.addError "Can't #{action} with a Julia client running.", - detail: "Stop the current client with Packages → Julia → Stop Julia." + description: "Stop the current client with Packages → Julia → Stop Julia." true else false @@ -158,7 +183,7 @@ module.exports = notConnectedError: (action = 'do that') -> if not @isActive() atom.notifications.addError "Can't #{action} without a Julia client running.", - detail: "Start Julia using Packages → Julia → Start Julia." + description: "Start Julia using Packages → Julia → Start Julia." true else false diff --git a/lib/connection/local.coffee b/lib/connection/local.coffee index 2136d213..5f91c038 100644 --- a/lib/connection/local.coffee +++ b/lib/connection/local.coffee @@ -6,20 +6,32 @@ cd = client.import 'cd', false # legacy basic = require './process/basic' + cycler = require './process/cycler' +ssh = require './process/remote' # server = require './process/server' # new console basic2 = require './process/basic2' module.exports = - # server: server + consumeGetServerConfig: (getconf) -> + ssh.consumeGetServerConfig(getconf) + + consumeGetServerName: (name) -> + ssh.consumeGetServerName(name) - provider: -> - switch atom.config.get 'julia-client.juliaOptions.bootMode' + provider: (p) -> + @bootMode = undefined + if p? + @bootMode = p + else + @bootMode = atom.config.get('julia-client.juliaOptions.bootMode') + switch @bootMode when 'Cycler' then cycler + when 'Remote' then ssh when 'Basic' - switch atom.config.get 'julia-client.consoleOptions.consoleStyle' + switch atom.config.get('julia-client.consoleOptions.consoleStyle') when 'REPL-based' then basic2 when 'Legacy' then basic @@ -28,7 +40,7 @@ module.exports = process.env.JULIA_EDITOR = "\"#{process.execPath}\" -a" else process.env.JULIA_EDITOR = "atom -a" - + paths.getVersion() .then => @provider().start? paths.jlpath(), client.clargs() @@ -73,16 +85,15 @@ module.exports = client.flush() proc - start: -> + start: (provider) -> [path, args] = [paths.jlpath(), client.clargs()] - paths.projectDir().then (dir) -> cd dir check = paths.getVersion() check.catch (err) => messages.jlNotFound paths.jlpath(), err proc = check - .then => @spawnJulia(path, args) + .then => @spawnJulia(path, args, provider) .then (proc) => if proc.ty? then @monitor2(proc) else @monitor(proc) proc .then (proc) => @@ -92,7 +103,12 @@ module.exports = .catch (e) -> client.detach() console.error("Julia exited with #{e}.") + .then => + if @bootMode is 'Remote' + ssh.withRemoteConfig((conf) -> cd conf.remote).catch -> + else + paths.projectDir().then (dir) -> cd dir proc - spawnJulia: (path, args) -> - @provider().get(path, args) + spawnJulia: (path, args, provider) -> + @provider(provider).get(path, args) diff --git a/lib/connection/messages.coffee b/lib/connection/messages.coffee index 16c25725..4279cb3e 100644 --- a/lib/connection/messages.coffee +++ b/lib/connection/messages.coffee @@ -92,4 +92,4 @@ module.exports = client.onceAttached -> if not msg.isDismissed() msg.dismiss() - atom.notifications.addSuccess "Julia is connected." + atom.notifications.addSuccess "Julia is connected." diff --git a/lib/connection/process/basic2.js b/lib/connection/process/basic2.js index 86dac81b..c43c1e18 100644 --- a/lib/connection/process/basic2.js +++ b/lib/connection/process/basic2.js @@ -36,7 +36,7 @@ export function get_ (path, args) { function getUnix (path, args, env) { return new Promise((resolve, reject) => { - tcp.listen().then((port, server) => { + tcp.listen().then((port) => { paths.fullPath(path).then((path) => { paths.projectDir().then((cwd) => { // space before port needed for pty.js on windows: @@ -76,7 +76,7 @@ function getWindows (path, args, env) { return getUnix(path, args, env) } return new Promise((resolve, reject) => { - tcp.listen().then((port, server) => { + tcp.listen().then((port) => { freePort().then((wrapPort) => { paths.fullPath("powershell").then((psPath) => { paths.projectDir().then((cwd) => { diff --git a/lib/connection/process/remote.js b/lib/connection/process/remote.js new file mode 100644 index 00000000..353ae436 --- /dev/null +++ b/lib/connection/process/remote.js @@ -0,0 +1,249 @@ +'use babel' + +import tcp from './tcp' +import * as pty from 'node-pty-prebuilt' +import net from 'net' +import { paths, mutex } from '../../misc' +import * as ssh from 'ssh2' +import fs from 'fs' + +export var lock = mutex() + +let getRemoteConf = undefined +let getRemoteName = undefined +let serversettings = {} +let currentServer = undefined + +export function get (path, args) { + return lock((release) => { + let p = get_(path, args) + release(p.then(({socket}) => socket)) + return p + }) +} + +function getConnectionSettings () { + return new Promise((resolve, reject) => { + if (getRemoteConf) { + let conf = getRemoteConf('Juno requests access to your server configuration to open a terminal.') + conf.then(conf => resolve(conf)).catch(reason => reject(reason)) + } else { + reject('nopackage') + } + }) +} + +export function withRemoteConfig (f) { + return new Promise((resolve, reject) => { + if (getRemoteName === undefined) { + reject() + } else { + getRemoteName().then(name => { + name = name.toString() + let cachedSettings = serversettings[name] + if (cachedSettings) { + resolve(f(maybe_add_agent(cachedSettings))) + } else { + getConnectionSettings().then(conf => { + serversettings[name] = conf + resolve(f(maybe_add_agent(conf))) + }).catch(reason => { + showRemoteError(reason) + reject() + }) + } + }).catch(reason => { + showRemoteError(reason) + reject() + }) + } + }) +} + +function maybe_add_agent (conf) { + if (conf && !conf.agent && atom.config.get('julia-client.remoteOptions.agentAuth')) { + let sshsock = ssh_socket() + if (sshsock) { + conf.agent = sshsock + conf.agentForward = atom.config.get('julia-client.remoteOptions.forwardAgent') + } + } + return conf +} + +function ssh_socket () { + let sock = process.env['SSH_AUTH_SOCK'] + if (sock) { + return sock + } else { + if (process.platform == 'win32') { + return 'pageant' + } else { + return '' + } + } +} + +const storageKey = 'juno_remote_server_exec_key' + +function setRemoteExec (server, command) { + let store = getRemoteStore() + store[server] = command + setRemoteStore(store) +} + +function getRemoteExec (server) { + let store = getRemoteStore() + return store[server] +} + +function setRemoteStore (store) { + localStorage[storageKey] = JSON.stringify(store) +} + +function getRemoteStore () { + let store = localStorage[storageKey] + if (store == undefined) { + store = [] + } else { + store = JSON.parse(store) + } + return store +} + +function showRemoteError (reason) { + if (reason == 'nopackage') { + atom.notifications.addInfo('ftp-remote-edit not installed') + } else if (reason == 'noservers') { + let notif = atom.notifications.addInfo('Please select a project', { + description: `Connect to a server in the ftp-remote-edit tree view.`, + dismissable: true, + buttons: [ + { + text: 'Togle Remote Tree View', + onDidClick: () => { + let edview = atom.views.getView(atom.workspace) + atom.commands.dispatch(edview, 'ftp-remote-edit:toggle') + notif.dismiss() + } + } + ] + }) + } else { + atom.notifications.addError('Remote Connection Failed', { + details: `Unknown Error: \n\n ${reason}` + }) + } +} + +export function consumeGetServerConfig (getconf) { + getRemoteConf = getconf +} + +export function consumeGetServerName (name) { + getRemoteName = name +} + +export function get_ (path, args) { + return withRemoteConfig(conf => { + let execs = getRemoteExec(conf.name) + if (!execs) { + console.log("open a dialog and get config here") + } + return new Promise((resolve, reject) => { + tcp.listen().then((port) => { + let conn = new ssh.Client() + + conn.on('ready', () => { + conn.forwardIn('127.0.0.1', port, err => { + if (err) { + console.error(`Error while forwarding remote connection from ${port}: ${err}`) + atom.notifications.addError(`Port in use`, { + description: `Port ${port} on the remote server already in use. + Try again with another port.` + }) + reject() + } + }) + let jlpath = atom.config.get('julia-client.remoteOptions.remoteJulia') + let exec = '' + if (atom.config.get('julia-client.remoteOptions.tmux')) { + let sessionName = atom.config.get('julia-client.remoteOptions.tmuxName') + exec = exec + `tmux new -s ${sessionName} ` + exec = exec + jlpath + exec = exec + ' ' + args.join(' ') + ' -e \'' + exec = exec + fs.readFileSync(paths.script('boot_repl.jl')) + exec = exec + '\' ' + port + exec = exec + ` || tmux send-keys -t ${sessionName}.{left} ^A ^K \'Juno.connect(${port})\' ENTER` + exec = exec + ` && tmux attach -t ${sessionName} ` + } else { + exec = exec + jlpath + ' ' + args.join(' ') + ' -e \'' + exec = exec + fs.readFileSync(paths.script('boot_repl.jl')) + exec = exec + '\' ' + port + } + + conn.exec(exec, { pty: { term: "xterm-256color" } }, (err, stream) => { + if (err) console.error(`Error while executing command \n\`${exec}\`\n on remote server.`) + + stream.on('close', () => { + conn.end() + }) + + let sock = socket(stream) + + // forward resize handling + stream.resize = (cols, rows) => stream.setWindow(rows, cols, 999, 999) + let proc = { + ty: stream, + kill: () => stream.signal('KILL'), + disconnect: () => stream.close(), + interrupt: () => stream.write('\x03'), // signal handling doesn't seem to work :/ + interruptREPL: () => stream.write('\x03'), + socket: sock, + onExit: (f) => stream.on('close', f), + onStderr: (f) => stream.stderr.on('data', data => f(data.toString())), + onStdout: (f) => stream.on('data', data => f(data.toString())), + config: conf + } + resolve(proc) + }) + }).on('tcp connection', (info, accept, reject) => { + let stream = accept() // connect to forwarded connection + stream.on('close', () => { + conn.end() + }) + stream.on('error', () => { + conn.end() + }) + stream.on('finish', () => { + conn.end() + }) + // start server that the julia server can connect to + let sock = net.createConnection({ port: port }, () => { + stream.pipe(sock).pipe(stream) + }) + sock.on('close', () => { + conn.end() + }) + sock.on('error', () => { + conn.end() + }) + sock.on('finish', () => { + conn.end() + }) + }).connect(conf) + }) + }) + }) +} + +function socket (stream) { + conn = tcp.next() + failure = new Promise((resolve, reject) => { + stream.on('close', (err) => { + conn.dispose() + reject(err) + }) + }) + return Promise.race([conn, failure]) +} diff --git a/lib/connection/process/tcp.coffee b/lib/connection/process/tcp.coffee index a9344a57..1e625858 100644 --- a/lib/connection/process/tcp.coffee +++ b/lib/connection/process/tcp.coffee @@ -31,13 +31,13 @@ module.exports = listen: -> return Promise.resolve(@port) if @port? - externalPort = atom.config.get('julia-client.juliaOptions.externalProcessPort') - if externalPort == 'random' - port = 0 - else - port = parseInt(externalPort) new Promise (resolve) => - @server = net.createServer (c) => @handle c + externalPort = atom.config.get('julia-client.juliaOptions.externalProcessPort') + if externalPort == 'random' + port = 0 + else + port = parseInt(externalPort) + @server = net.createServer((sock) => @handle(sock)) @server.listen port, '127.0.0.1', => @port = @server.address().port - resolve @port + resolve(@port) diff --git a/lib/julia-client.coffee b/lib/julia-client.coffee index ad205abd..2b27540c 100644 --- a/lib/julia-client.coffee +++ b/lib/julia-client.coffee @@ -50,6 +50,10 @@ module.exports = JuliaClient = consumeToolBar: (bar) -> toolbar.consumeToolBar bar + consumeGetServerConfig: (conf) -> @connection.consumeGetServerConfig(conf) + + consumeGetServerName: (name) -> @connection.consumeGetServerName(name) + provideClient: -> @connection.client provideHyperclick: -> @runtime.provideHyperclick() diff --git a/lib/package/commands.coffee b/lib/package/commands.coffee index b44a1f07..ac9bb85c 100644 --- a/lib/package/commands.coffee +++ b/lib/package/commands.coffee @@ -78,8 +78,10 @@ module.exports = @subs.add atom.commands.add 'atom-workspace', 'julia-client:open-a-repl': -> juno.connection.terminal.repl() 'julia-client:start-julia': -> disrequireClient 'boot Julia', -> boot() + 'julia-client:start-remote-julia-process': -> disrequireClient 'boot a remote Julia process', -> juno.connection.bootRemote() 'julia-client:kill-julia': -> juno.connection.client.kill() 'julia-client:interrupt-julia': => requireClient 'interrupt Julia', -> juno.connection.client.interrupt() + 'julia-client:disconnect-julia': => requireClient 'disconnect Julia', -> juno.connection.client.disconnect() # 'julia-client:reset-julia-server': -> juno.connection.local.server.reset() # server mode not functional 'julia-client:connect-external-process': -> disrequireClient -> juno.connection.messages.connectExternal() 'julia-client:connect-platformio-terminal': -> disrequireClient -> juno.connection.terminal.runPlatformIOTerm() diff --git a/lib/package/config.coffee b/lib/package/config.coffee index 9fadddb6..68dabdbf 100644 --- a/lib/package/config.coffee +++ b/lib/package/config.coffee @@ -13,7 +13,7 @@ config = bootMode: title: 'Boot Mode' type: 'string' - enum: ['Basic', 'Cycler'] + enum: ['Basic', 'Cycler', 'Remote'] default: 'Cycler' order: 1 arguments: @@ -183,6 +183,37 @@ config = default: false description: 'Enable this if you\'re experiencing slowdowns in the built-in terminals.' order: 9 + remoteOptions: + type: 'object' + order: 5 + properties: + remoteJulia: + title: 'Command to execute Julia on the remote server' + type: 'string' + default: 'julia' + order: 1 + tmux: + title: 'Use a persistent tmux session' + description: 'Requires tmux to be installed on the server you\'re connecting to.' + type: 'boolean' + default: false + order: 2 + tmuxName: + title: 'tmux session name' + type: 'string' + default: 'juno_tmux_session' + order: 3 + agentAuth: + title: 'Use SSH agent' + description: 'Requires `$SSH_AUTH_SOCKET` to be set. Defaults to putty\'s pageant on Windows' + type: 'boolean' + default: true + order: 4 + forwardAgent: + title: 'Forward SSH agent' + type: 'boolean' + default: true + order: 5 firstBoot: type: 'boolean' diff --git a/lib/package/toolbar.coffee b/lib/package/toolbar.coffee index 643119db..0c615916 100644 --- a/lib/package/toolbar.coffee +++ b/lib/package/toolbar.coffee @@ -28,6 +28,14 @@ module.exports = @bar.addSpacer() + @bar.addButton + icon: 'planet' + callback: 'julia-client:start-remote-julia-process' + tooltip: 'Start Remote Julia Process' + iconset: 'ion' + + @bar.addSpacer() + # Windows & Panes @bar.addButton diff --git a/lib/runtime/console2.js b/lib/runtime/console2.js index 1ac35be9..678c3f78 100644 --- a/lib/runtime/console2.js +++ b/lib/runtime/console2.js @@ -8,6 +8,8 @@ import modules from './modules' import * as pty from 'node-pty-prebuilt' import { debounce, once } from 'underscore-plus' import { customSelector } from '../ui' +import { withRemoteConfig } from '../connection/process/remote' +import * as ssh from 'ssh2' var { changeprompt, changemodule, resetprompt, validatepath, fullpath } = client.import({msg: ['changeprompt', 'changemodule', 'resetprompt'], rpc: ['validatepath', 'fullpath']}) @@ -103,6 +105,9 @@ export function activate (ink) { client.onDetached(() => { terminal.detach() + // make sure to switch to the normal termbuffer, otherweise there might be + // issues when leaving an xterm session: + terminal.write('\x1b[?1049l') terminal.write('\n\r\x1b[1m\r\x1b[31mJulia has exited.\x1b[0m Press Enter to start a new session.\n\r') terminal.terminal.deregisterLinkMatcher(linkHandler) if (promptObserver) promptObserver.dispose() @@ -136,6 +141,21 @@ export function activate (ink) { }).catch((e) => console.error(e)) })) + subs.add(atom.commands.add('atom-workspace', 'julia-client:new-remote-terminal', () => { + let term = ink.InkTerminal.fromId(`terminal-remote-julia-${Math.floor(Math.random()*10000000)}`, { + scrollback: atom.config.get('julia-client.consoleOptions.maximumConsoleSize'), + cursorStyle: atom.config.get('julia-client.consoleOptions.cursorStyle'), + rendererType: atom.config.get('julia-client.consoleOptions.rendererType') ? 'dom' : 'canvas' + }) + term.attachCustomKeyEventHandler(handleWhitelistedKeybindingTerminal) + remotePty().then(({pty, cwd, conf}) => { + term.attach(pty, true, cwd) + term.setTitle(`Terminal @ ${conf.name}`) + term.open().then(() => term.show()) + pty.on('close', () => term.detach()) + }).catch((e) => console.error(e)) + })) + subs.add(atom.workspace.onDidStopChangingActivePaneItem(item => { if (item && item.id && item.name === 'InkTerminal' && item.element.initialized) { let rt = atom.config.get('julia-client.consoleOptions.rendererType') @@ -147,19 +167,20 @@ export function activate (ink) { })) // handle deserialized terminals - forEachTerminalPane(item => { + forEachPane(item => { if (!item.ty) { item.attachCustomKeyEventHandler(handleWhitelistedKeybindingTerminal) shellPty(item.persistentState.cwd) .then(({pty, cwd}) => item.attach(pty, true, cwd)) .catch(() => {}) } - }) + }, /terminal\-julia\-\d+/) + forEachPane(item => item.close(), /terminal\-remote\-julia\-\d+/) } -function forEachTerminalPane (f) { +function forEachPane (f, id = /terminal\-julia\-\d+/) { atom.workspace.getPaneItems().forEach((item) => { - if (item.id && item.name === 'InkTerminal' && item.id.match(/terminal\-julia\-\d+/)) { + if (item.id && item.name === 'InkTerminal' && item.id.match(id)) { f(item) } }) @@ -172,6 +193,28 @@ function handleWhitelistedKeybindingTerminal (e) { return e } +function remotePty () { + return withRemoteConfig(conf => { + return new Promise((resolve, reject) => { + let conn = new ssh.Client() + conn.on('ready', () => { + conn.shell({ term: "xterm-256color" }, (err, stream) => { + if (err) console.error(`Error while starting remote shell.`) + + stream.on('close', () => { + conn.end() + }) + + // forward resize handling + stream.resize = (cols, rows) => stream.setWindow(rows, cols, 999, 999) + + resolve({pty: stream, cwd: '~', conf: conf}) + }) + }).connect(conf) + }) + }) +} + function shellPty (cwd) { return new Promise((resolve, reject) => { let pr @@ -202,11 +245,12 @@ function shellPty (cwd) { export function deactivate () { // detach node-pty process from ink terminals; necessary for updates to work cleanly - atom.workspace.getPaneItems().forEach((item) => { - if (item.id && item.name === 'InkTerminal' && item.id.match(/terminal\-julia\-\d+/)) { - item.detach() - } - }) + forEachPane(item => item.detach(), /terminal\-julia\-\d+/) + // remote terminals shouldn't be serialized + forEachPane(item => { + item.detach() + item.close() + }, /terminal\-remote\-julia\-\d+/) terminal.detach() if (subs) subs.dispose() diff --git a/lib/runtime/evaluation.coffee b/lib/runtime/evaluation.coffee index bf7b1b74..a99cb12d 100644 --- a/lib/runtime/evaluation.coffee +++ b/lib/runtime/evaluation.coffee @@ -15,7 +15,7 @@ module.exports = currentContext: -> editor = atom.workspace.getActiveTextEditor() mod = modules.current() ? 'Main' - edpath = editor.getPath() || 'untitled-' + editor.getBuffer().id + edpath = client.editorPath(editor) || 'untitled-' + editor.getBuffer().id {editor, mod, edpath} eval: ({move, cell}={}) -> @@ -124,7 +124,7 @@ module.exports = # Working Directory cdHere: -> - file = atom.workspace.getActiveTextEditor()?.getPath() + file = client.editorPath(atom.workspace.getActiveTextEditor()?) if !file then atom.notifications.addError 'This file has no path.' cd path.dirname(file) diff --git a/lib/runtime/modules.coffee b/lib/runtime/modules.coffee index 476ad118..eeb2888c 100644 --- a/lib/runtime/modules.coffee +++ b/lib/runtime/modules.coffee @@ -98,7 +98,7 @@ module.exports = sels = ed.getSelections() {row, column} = sels[sels.length - 1].getBufferRange().end data = - path: ed.getPath() + path: client.editorPath(ed) code: ed.getText() row: row+1, column: column+1 module: ed.juliaModule diff --git a/package.json b/package.json index a1d53cc8..d0437351 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "coffee-script": "*", "physical-cpu-count": "*", "node-pty-prebuilt": "0.7.6", + "ssh2": "^0.6.0", + "fuzzaldrin-plus": "^0.6.0", "etch": "*" }, "consumedServices": { @@ -38,6 +40,16 @@ "versions": { "*": "consumeTerminal" } + }, + "ftp-remote.getCurrentServerConfig": { + "versions": { + "0.1.0": "consumeGetServerConfig" + } + }, + "ftp-remote.getCurrentServerName": { + "versions": { + "0.1.0": "consumeGetServerName" + } } }, "providedServices": { diff --git a/script/boot_repl.jl b/script/boot_repl.jl index c4112a26..bd3194a2 100644 --- a/script/boot_repl.jl +++ b/script/boot_repl.jl @@ -12,22 +12,7 @@ junorc = abspath(normpath(expanduser(junorc))) if (VERSION > v"0.7-" ? Base.find_package("Atom") : Base.find_in_path("Atom")) == nothing p = VERSION > v"0.7-" ? (x) -> printstyled(x, color=:cyan, bold=true) : (x) -> print_with_color(:cyan, x, bold=true) - p("\nHold on tight while we're installing some packages for you.\nThis should only take a few seconds...\n\n") - - if VERSION > v"0.7-" - using Pkg - Pkg.activate() - end - - Pkg.add("Atom") - Pkg.add("Juno") - - println() -end - -if (VERSION > v"0.7-" ? Base.find_package("Atom") : Base.find_in_path("Atom")) == nothing - p = VERSION > v"0.7-" ? (x) -> printstyled(x, color=:cyan, bold=true) : (x) -> print_with_color(:cyan, x, bold=true) - p("\nHold on tight while we're installing some packages for you.\nThis should only take a few seconds...\n\n") + p("\nHold on tight while we are installing some packages for you.\nThis should only take a few seconds...\n\n") if VERSION > v"0.7-" using Pkg