diff --git a/lib/apm.coffee b/lib/apm.coffee deleted file mode 100644 index 27c2200..0000000 --- a/lib/apm.coffee +++ /dev/null @@ -1,41 +0,0 @@ -{BufferedProcess} = require 'atom' - -run = (args) -> - new Promise (resolve) -> - command = atom.packages.getApmPath() - - log = '' - - stdout = (data) -> - log += "#{data}" - - stderr = (data) -> - log += "#{data}" - - exit = (code) -> - resolve({log, code}) - - new BufferedProcess({command, args, stdout, stderr, exit}) - -fullname = (name, version) -> - if version? then "#{name}@#{version}" else name - -parseNameAndVersionObject = (namesAndVersions) -> - fullnames = [] - - for name, version of namesAndVersions - fullnames.push(fullname(name, version)) - - fullnames - -module.exports = apm = - install: (name, version) -> - args = - if typeof name is 'object' - parseNameAndVersionObject(name) - else - [fullname(name, version)] - - run(['install', '--compatible', '--no-confirm', '--no-color'].concat(args)) - - diff --git a/lib/apm.js b/lib/apm.js new file mode 100644 index 0000000..0667381 --- /dev/null +++ b/lib/apm.js @@ -0,0 +1,37 @@ +'use babel' + +import {BufferedProcess} from 'atom'; + +function run(args) { + return new Promise(function(resolve) { + var command = atom.packages.getApmPath(); + + var log = ''; + var stdout = data => log += `${data}`; + var stderr = data => log += `${data}`; + + var exit = code => resolve({log, code}); + + new BufferedProcess({command, args, stdout, stderr, exit}); + }); +} + +function fullname(name, version) { + return (version != null) ? `${name}@${version}` : name +}; + +function parseDependencies(dependencies) { + return Object.keys(dependencies).map((name) => { + return fullname(name, dependencies[name]) + }) +}; + +export default { + install(name, version) { + var nameIsObject = typeof name === 'object'; + + var args = nameIsObject ? parseDependencies(name) : [fullname(name, version)]; + + return run(['install', '--compatible', '--no-confirm', '--no-color'].concat(args)); + } +} diff --git a/lib/atom-helper.coffee b/lib/atom-helper.coffee deleted file mode 100644 index 6d7f69b..0000000 --- a/lib/atom-helper.coffee +++ /dev/null @@ -1,43 +0,0 @@ -localStorage = require './local-storage' - -packageName = -> - pkg = require '../package.json' - pkg.name - -module.exports = - isLastFocusedWindow: -> - parseInt(localStorage.get('lastFocusedWindow')) == process.pid - - setLastFocusedWindow: -> - localStorage.set('lastFocusedWindow', process.pid) - - trackFocusedWindow: -> - @setLastFocusedWindow() - window.onfocus = @setLastFocusedWindow - - cleanup: -> - if @isLastFocusedWindow() - localStorage.delete('lastFocusedWindow') - - emit: (key, detail) -> - atom.emitter.emit(key, detail) - - on: (key, callback) -> - atom.emitter.on(key, callback) - - closePaneItems: -> - atom.workspace.getPanes().forEach (pane) -> - pane.close() - - resetPackage: -> - atom.packages.deactivatePackage(packageName()) - atom.packages.activatePackage(packageName()).then -> - atom.menu.sortPackagesMenu() - - reloadStylesheets: -> - pkg = atom.packages.getActivePackage(packageName()) - pkg.reloadStylesheets() - - addStylesheet: (css) -> - atom.styles.addStyleSheet(css) - diff --git a/lib/atom-helper.js b/lib/atom-helper.js new file mode 100644 index 0000000..7d2cb89 --- /dev/null +++ b/lib/atom-helper.js @@ -0,0 +1,55 @@ +'use babel' + +import localStorage from './local-storage'; +import { name } from '../package.json'; + +export default { + isLastFocusedWindow() { + return parseInt(localStorage.get('lastFocusedWindow')) === process.pid; + }, + + setLastFocusedWindow() { + localStorage.set('lastFocusedWindow', process.pid); + }, + + trackFocusedWindow() { + this.setLastFocusedWindow(); + window.onfocus = this.setLastFocusedWindow; + }, + + cleanup() { + if (this.isLastFocusedWindow()) { + localStorage.delete('lastFocusedWindow'); + } + }, + + emit(key, detail) { + atom.emitter.emit(key, detail); + }, + + on(key, callback) { + return atom.emitter.on(key, callback); + }, + + closePaneItems() { + atom.workspace.getPanes().forEach(pane => pane.close()); + }, + + resetPackage() { + atom.packages.deactivatePackage(name); + + atom.packages.activatePackage(name).then(() => + atom.menu.sortPackagesMenu() + ); + }, + + reloadStylesheets() { + var pkg = atom.packages.getActivePackage(name); + pkg.reloadStylesheets(); + }, + + addStylesheet(css) { + atom.styles.addStyleSheet(css); + } +}; + diff --git a/lib/auth.coffee b/lib/auth.coffee deleted file mode 100644 index ba5fee7..0000000 --- a/lib/auth.coffee +++ /dev/null @@ -1,95 +0,0 @@ -url = require 'url' -shell = require 'shell' -path = require 'path' -version = require './version' -fetch = require './fetch' -_token = require './token' -localStorage = require './local-storage' -{learnCo} = require './config' -{BrowserWindow} = require 'remote' - -AUTH_URL = "#{learnCo}/api/v1/users/me?ile_version=#{version}" - -confirmOauthToken = (token) -> - headers = new Headers({'Authorization': "Bearer #{token}"}) - - fetch(AUTH_URL, {headers}).then (data) -> - if data.email? then data else false - -githubLogin = () -> - new Promise (resolve, reject) -> - win = new BrowserWindow(autoHideMenuBar: true, show: false, width: 440, height: 660, resizable: false) - webContents = win.webContents - - win.setSkipTaskbar(true) - win.setMenuBarVisibility(false) - win.setTitle('Sign in to Github to get started with the Learn IDE') - - # show window only if login is required - webContents.on 'did-finish-load', -> win.show() - - # hide window immediately after login - webContents.on 'will-navigate', (e, url) -> - win.hide() if url.match("#{learnCo}/users/auth/github/callback") - - webContents.on 'did-get-redirect-request', (e, oldURL, newURL) -> - return unless newURL.match(/ide_token/) - token = url.parse(newURL, true).query.ide_token - confirmOauthToken(token).then (res) -> - return unless res? - localStorage.set('didCompleteGithubLogin') - _token.set(token) - win.destroy() - resolve() - - if not win.loadURL("#{learnCo}/ide/token?ide_config=true") - atom.notifications.warning 'Learn IDE: connectivity issue', - detail: "The editor is unable to connect to #{learnCo}. Are you connected to the internet?" - buttons: [ - {text: 'Try again', onDidClick: -> learnSignIn()} - ] - -learnSignIn = () -> - new Promise (resolve, reject) -> - win = new BrowserWindow(autoHideMenuBar: true, show: false, width: 400, height: 600, resizable: false) - {webContents} = win - - win.setSkipTaskbar(true) - win.setMenuBarVisibility(false) - win.setTitle('Welcome to the Learn IDE') - - webContents.on 'did-finish-load', -> win.show() - - webContents.on 'new-window', (e, url) -> - e.preventDefault() - win.destroy() - shell.openExternal(url) - - webContents.on 'will-navigate', (e, url) -> - if url.match(/github_sign_in/) - win.destroy() - githubLogin().then(resolve) - - webContents.on 'did-get-redirect-request', (e, oldURL, newURL) -> - if newURL.match(/ide_token/) - token = url.parse(newURL, true).query.ide_token - if token?.length - confirmOauthToken(token).then (res) -> - return unless res - _token.set(token) - resolve() - if newURL.match(/github_sign_in/) - win.destroy() - githubLogin().then(resolve) - - if not win.loadURL("#{learnCo}/ide/sign_in?ide_onboard=true") - win.destroy() - githubLogin.then(resolve) - -module.exports = -> - existingToken = _token.get() - - if !existingToken - learnSignIn() - else - confirmOauthToken(existingToken) diff --git a/lib/auth.js b/lib/auth.js new file mode 100644 index 0000000..1ca58d9 --- /dev/null +++ b/lib/auth.js @@ -0,0 +1,117 @@ +'use babel' + +import _token from './token'; +import _url from 'url'; +import fetch from './fetch'; +import localStorage from './local-storage'; +import shell from 'shell'; +import {BrowserWindow} from 'remote'; +import {learnCo} from './config'; +import {version} from '../package.json'; + +var authUrl = `${learnCo}/api/v1/users/me?ile_version=${version}`; + +function confirmOauthToken(token) { + var headers = new Headers({'Authorization': `Bearer ${token}`}); + + return fetch(authUrl, {headers}).then(function(data) { + return (data.email != null) ? data : false + }); +} + +function githubLogin() { + return new Promise((resolve, reject) => { + var win = new BrowserWindow({autoHideMenuBar: true, show: false, width: 440, height: 660, resizable: false}); + var { webContents } = win; + + win.setSkipTaskbar(true); + win.setMenuBarVisibility(false); + win.setTitle('Sign in to Github to get started with the Learn IDE'); + + // show window only if login is required + webContents.on('did-finish-load', () => win.show()); + + // hide window immediately after login + webContents.on('will-navigate', (e, url) => { + if (url.match(`${learnCo}/users/auth/github/callback`)) { return win.hide(); } + }); + + webContents.on('did-get-redirect-request', (e, oldURL, newURL) => { + if (!newURL.match(/ide_token/)) { return; } + + var token = _url.parse(newURL, true).query.ide_token; + + confirmOauthToken(token).then((res) => { + if (res == null) { return; } + + localStorage.set('didCompleteGithubLogin'); + _token.set(token); + win.destroy(); + resolve(); + }); + }); + + if (!win.loadURL(`${learnCo}/ide/token?ide_config=true`)) { + atom.notifications.warning('Learn IDE: connectivity issue', { + detail: `The editor is unable to connect to ${learnCo}. Are you connected to the internet?`, + buttons: [ + {text: 'Try again', onDidClick() { learnSignIn(); }} + ] + }); + }}) +}; + +function learnSignIn() { + return new Promise((resolve, reject) => { + var win = new BrowserWindow({autoHideMenuBar: true, show: false, width: 400, height: 600, resizable: false}); + var {webContents} = win; + + win.setSkipTaskbar(true); + win.setMenuBarVisibility(false); + win.setTitle('Welcome to the Learn IDE'); + + webContents.on('did-finish-load', () => win.show()); + + webContents.on('new-window', (e, url) => { + e.preventDefault(); + win.destroy(); + shell.openExternal(url); + }); + + webContents.on('will-navigate', (e, url) => { + if (url.match(/github_sign_in/)) { + win.destroy(); + githubLogin().then(resolve); + } + }); + + webContents.on('did-get-redirect-request', (e, oldURL, newURL) => { + if (newURL.match(/ide_token/)) { + var token = _url.parse(newURL, true).query.ide_token; + + if (token != null && token.length) { + confirmOauthToken(token).then((res) => { + if (!res) { return; } + _token.set(token); + resolve(); + }); + } + } + + if (newURL.match(/github_sign_in/)) { + win.destroy(); + githubLogin().then(resolve); + } + }); + + if (!win.loadURL(`${learnCo}/ide/sign_in?ide_onboard=true`)) { + win.destroy(); + githubLogin.then(resolve); + } + }) +} + +export default function() { + var existingToken = _token.get(); + return (!existingToken) ? learnSignIn() : confirmOauthToken(existingToken) +} diff --git a/lib/config.coffee b/lib/config.coffee deleted file mode 100644 index 05a4090..0000000 --- a/lib/config.coffee +++ /dev/null @@ -1,22 +0,0 @@ -path = require 'path' -_ = require 'underscore-plus' - -require('dotenv').config - path: path.join(__dirname, '..', '.env') - silent: true - -require('dotenv').config - path: path.join(atom.getConfigDirPath(), '.env') - silent: true - -module.exports = _.defaults - host: process.env['IDE_WS_HOST'] - port: process.env['IDE_WS_PORT'] - path: process.env['IDE_WS_TERM_PATH'] - learnCo: process.env['IDE_LEARN_CO'] -, - host: 'ile.learn.co' - port: 443 - path: 'v2/terminal' - learnCo: 'https://learn.co' - diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..76b327b --- /dev/null +++ b/lib/config.js @@ -0,0 +1,40 @@ +'use babel' + +import path from 'path' +import dotenv from 'dotenv' + +dotenv.config({ + path: path.join(__dirname, '..', '.env'), + silent: true +}); + +dotenv.config({ + path: path.join(atom.getConfigDirPath(), '.env'), + silent: true +}); + +var defaultConfig = { + host: 'ile.learn.co', + port: 443, + path: 'v2/terminal', + learnCo: 'https://learn.co' +} + +var envConfig = { + host: process.env['IDE_WS_HOST'], + port: process.env['IDE_WS_PORT'], + path: process.env['IDE_WS_TERM_PATH'], + learnCo: process.env['IDE_LEARN_CO'] +} + +export default Object.assign({}, defaultConfig, clean(envConfig)) + +function clean(obj) { + var cleanObj = {}; + + Object.keys(obj).forEach((key) => { + if (obj[key] != null) { cleanObj[key] = obj[key] } + }) + + return cleanObj; +} diff --git a/lib/fetch.coffee b/lib/fetch.coffee deleted file mode 100644 index 45aab1e..0000000 --- a/lib/fetch.coffee +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = fetchWrapper = (url, init) -> - fetch(url, init) - .then (response) -> response.text() - .then (body) -> JSON.parse(body) - diff --git a/lib/fetch.js b/lib/fetch.js new file mode 100644 index 0000000..1983b3a --- /dev/null +++ b/lib/fetch.js @@ -0,0 +1,5 @@ +'use babel' + +export default function(url, init) { + return fetch(url, init).then(response => response.text()).then(body => JSON.parse(body)) +} diff --git a/lib/learn-ide.coffee b/lib/learn-ide.coffee deleted file mode 100644 index a6fffad..0000000 --- a/lib/learn-ide.coffee +++ /dev/null @@ -1,189 +0,0 @@ -localStorage = require './local-storage' -{CompositeDisposable} = require 'atom' -Terminal = require './terminal' -TerminalView = require './terminal-view' -StatusView = require './views/status' -Notifier = require './notifier' -airbrake = require './airbrake' -atomHelper = require './atom-helper' -auth = require './auth' -bus = require './event-bus' -config = require './config' -{shell} = require 'electron' -updater = require './updater' -version = require './version' -remoteNotification = require './remote-notification' -{name} = require '../package.json' -colors = require './colors' -logout = require './logout' - -ABOUT_URL = "#{config.learnCo}/ide/about" - -module.exports = - token: require('./token') - - activate: (state) -> - @subscriptions = new CompositeDisposable - - @activateMonitor() - @checkForV1WindowsInstall() - @registerWindowsProtocol() - @disableFormerPackage() - colors.apply() - - @subscribeToLogin() - - @waitForAuth = auth().then => - @activateIDE(state) - console.log('successfully authenticated') - .catch => - @activateIDE(state) - console.error('failed to authenticate') - - activateIDE: (state) -> - @isRestartAfterUpdate = (localStorage.get('restartingForUpdate') is 'true') - if @isRestartAfterUpdate - updater.didRestartAfterUpdate() - localStorage.delete('restartingForUpdate') - - @activateTerminal() - @activateStatusView(state) - @activateEventHandlers() - @activateSubscriptions() - @activateNotifier() - @activateUpdater() - @activateRemoteNotification() - - activateTerminal: -> - @term = new Terminal - host: config.host - port: config.port - path: config.path - token: @token.get() - - @termView = new TerminalView(@term) - - activateStatusView: (state) -> - @statusView = new StatusView(state, @term) - - activateEventHandlers: -> - atomHelper.trackFocusedWindow() - - # listen for learn:open event from other render processes (url handler) - bus.on 'learn:open', (lab) => - @learnOpen(lab.slug) - atom.getCurrentWindow().focus() - - # tidy up when the window closes - atom.getCurrentWindow().on 'close', => - @cleanup() - - activateSubscriptions: -> - @subscriptions.add atom.commands.add 'atom-workspace', - 'learn-ide:open': (e) => @learnOpen(e.detail.path) - 'learn-ide:toggle-terminal': () => @termView.toggle() - 'learn-ide:toggle-popout': () => @termView.focusPopoutEmulator() - 'learn-ide:toggle-focus': => @termView.toggleFocus() - 'learn-ide:focus': => @termView.focusEmulator() - 'learn-ide:toggle:debugger': => @term.toggleDebugger() - 'learn-ide:reset-connection': => @term.reset() - 'learn-ide:view-version': => @viewVersion() - 'learn-ide:update-check': -> updater.checkForUpdate() - 'learn-ide:about': => @about() - - @subscriptions.add atom.commands.add '.terminal', - 'core:copy': => @termView.clipboardCopy() - 'core:paste': => @termView.clipboardPaste() - 'learn-ide:reset-font-size': => @termView.resetFontSize() - 'learn-ide:increase-font-size': => @termView.increaseFontSize() - 'learn-ide:decrease-font-size': => @termView.decreaseFontSize() - 'learn-ide:clear-terminal': => @term.send(' ') - - atom.config.onDidChange "#{name}.terminalColors.basic", => - colors.apply() - - atom.config.onDidChange "#{name}.terminalColors.ansi", => - colors.apply() - - atom.config.onDidChange "#{name}.terminalColors.json", ({newValue}) => - colors.parseJSON(newValue) - - atom.config.onDidChange "#{name}.notifier", ({newValue}) => - if newValue then @activateNotifier() else @notifier.deactivate() - - openPath = localStorage.get('learnOpenLabOnActivation') - if openPath - localStorage.delete('learnOpenLabOnActivation') - @learnOpen(openPath) - - activateNotifier: -> - if atom.config.get("#{name}.notifier") - @notifier = new Notifier(@token.get()) - @notifier.activate() - - activateUpdater: -> - if not @isRestartAfterUpdate - updater.autoCheck() - - activateMonitor: -> - @subscriptions.add atom.onWillThrowError ({originalError}) => - airbrake.notify(originalError) - - activateRemoteNotification: -> - remoteNotification() - - deactivate: -> - localStorage.delete('disableTreeView') - localStorage.delete('terminalOut') - @termView = null - @statusView = null - @subscriptions.dispose() - @term.removeAllListeners() - - subscribeToLogin: -> - @subscriptions.add atom.commands.add 'atom-workspace', - 'learn-ide:log-in-out': => @logInOrOut() - - cleanup: -> - atomHelper.cleanup() - - consumeStatusBar: (statusBar) -> - @waitForAuth.then => @addLearnToStatusBar(statusBar) - - logInOrOut: -> - if @token.get()? - logout() - else - atomHelper.resetPackage() - - checkForV1WindowsInstall: -> - require('./windows') - - registerWindowsProtocol: -> - if process.platform == 'win32' - require('./protocol') - - disableFormerPackage: -> - pkgName = 'integrated-learn-environment' - - if not atom.packages.isPackageDisabled(pkgName) - atom.packages.disablePackage(pkgName) - - addLearnToStatusBar: (statusBar) -> - leftTiles = Array.from(statusBar.getLeftTiles()) - rightTiles = Array.from(statusBar.getRightTiles()) - rightMostTile = rightTiles[rightTiles.length - 1] - - priority = (rightMostTile?.priority || 0) - 1 - statusBar.addRightTile({item: @statusView, priority}) - - learnOpen: (labSlug) -> - if labSlug? - @term.send("learn open #{labSlug.toString()}\r") - - about: -> - shell.openExternal(ABOUT_URL) - - viewVersion: -> - atom.notifications.addInfo("Learn IDE: v#{version}") - diff --git a/lib/learn-ide.js b/lib/learn-ide.js new file mode 100644 index 0000000..a89b58c --- /dev/null +++ b/lib/learn-ide.js @@ -0,0 +1,223 @@ +'use babel' + +import Notifier from './notifier' +import StatusView from './views/status' +import Terminal from './terminal' +import TerminalView from './terminal-view' +import airbrake from './airbrake' +import atomHelper from './atom-helper' +import auth from './auth' +import bus from './event-bus' +import colors from './colors' +import config from './config' +import localStorage from './local-storage' +import logout from './logout' +import remoteNotification from './remote-notification' +import token from './token' +import updater from './updater' +import {CompositeDisposable} from 'atom' +import {name, version} from '../package.json' +import {shell} from 'electron' + +window.LEARN_IDE_VERSION = version; + +var ABOUT_URL = `${config.learnCo}/ide/about`; + +export default { + token, + + activate(state) { + this.subscriptions = new CompositeDisposable; + + this.activateMonitor(); + this.checkForV1WindowsInstall(); + this.registerWindowsProtocol(); + this.disableFormerPackage(); + + colors.apply(); + + this.subscribeToLogin(); + + this.waitForAuth = auth().then(() => { + this.activateIDE(state); + }).catch(() => { + this.activateIDE(state); + }); + }, + + activateIDE(state) { + this.isRestartAfterUpdate = (localStorage.get('restartingForUpdate') === 'true'); + + if (this.isRestartAfterUpdate) { + updater.didRestartAfterUpdate(); + localStorage.delete('restartingForUpdate'); + } + + this.activateTerminal(); + this.activateStatusView(state); + this.activateEventHandlers(); + this.activateSubscriptions(); + this.activateNotifier(); + this.activateUpdater(); + this.activateRemoteNotification(); + }, + + activateTerminal() { + this.term = new Terminal({ + host: config.host, + port: config.port, + path: config.path, + token: this.token.get() + }); + + this.termView = new TerminalView(this.term); + }, + + activateStatusView(state) { + this.statusView = new StatusView(state, this.term); + }, + + activateEventHandlers() { + atomHelper.trackFocusedWindow(); + + // listen for learn:open event from other render processes (url handler) + bus.on('learn:open', lab => { + this.learnOpen(lab.slug); + atom.getCurrentWindow().focus(); + }); + + // tidy up when the window closes + atom.getCurrentWindow().on('close', () => this.cleanup()); + }, + + activateSubscriptions() { + this.subscriptions.add(atom.commands.add('atom-workspace', { + 'learn-ide:open': e => this.learnOpen(e.detail.path), + 'learn-ide:toggle-terminal': () => this.termView.toggle(), + 'learn-ide:toggle-popout': () => this.termView.focusPopoutEmulator(), + 'learn-ide:toggle-focus': () => this.termView.toggleFocus(), + 'learn-ide:focus': () => this.termView.focusEmulator(), + 'learn-ide:toggle:debugger': () => this.term.toggleDebugger(), + 'learn-ide:reset-connection': () => this.term.reset(), + 'learn-ide:view-version': () => this.viewVersion(), + 'learn-ide:update-check': () => updater.checkForUpdate(), + 'learn-ide:about': () => this.about() + })); + + this.subscriptions.add(atom.commands.add('.terminal', { + 'core:copy': () => this.termView.clipboardCopy(), + 'core:paste': () => this.termView.clipboardPaste(), + 'learn-ide:reset-font-size': () => this.termView.resetFontSize(), + 'learn-ide:increase-font-size': () => this.termView.increaseFontSize(), + 'learn-ide:decrease-font-size': () => this.termView.decreaseFontSize(), + 'learn-ide:clear-terminal': () => this.term.send(' ') + })); + + atom.config.onDidChange(`${name}.terminalColors.basic`, () => colors.apply()) + + atom.config.onDidChange(`${name}.terminalColors.ansi`, () => colors.apply()) + + atom.config.onDidChange(`${name}.terminalColors.json`, ({newValue}) => { + colors.parseJSON(newValue); + }); + + atom.config.onDidChange(`${name}.notifier`, ({newValue}) => { + newValue ? this.activateNotifier() : this.notifier.deactivate() + }); + + var openPath = localStorage.get('learnOpenLabOnActivation'); + if (openPath) { + localStorage.delete('learnOpenLabOnActivation'); + this.learnOpen(openPath); + } + }, + + activateNotifier() { + if (atom.config.get(`${name}.notifier`)) { + this.notifier = new Notifier(this.token.get()); + this.notifier.activate(); + } + }, + + activateUpdater() { + if (!this.isRestartAfterUpdate) { + return updater.autoCheck(); + } + }, + + activateMonitor() { + this.subscriptions.add(atom.onWillThrowError(err => { + airbrake.notify(err.originalError); + })) + }, + + activateRemoteNotification() { + remoteNotification(); + }, + + deactivate() { + localStorage.delete('disableTreeView'); + localStorage.delete('terminalOut'); + this.termView = null; + this.statusView = null; + this.subscriptions.dispose(); + this.term.emitter.removeAllListeners(); + }, + + subscribeToLogin() { + this.subscriptions.add(atom.commands.add('atom-workspace', + {'learn-ide:log-in-out': () => this.logInOrOut()}) + ); + }, + + cleanup() { + atomHelper.cleanup(); + }, + + consumeStatusBar(statusBar) { + this.waitForAuth.then(() => this.addLearnToStatusBar(statusBar)); + }, + + logInOrOut() { + (this.token.get() == null) ? atomHelper.resetPackage() : logout() + }, + + checkForV1WindowsInstall() { + require('./windows'); + }, + + registerWindowsProtocol() { + if (process.platform === 'win32') { require('./protocol') } + }, + + disableFormerPackage() { + var pkgName = 'integrated-learn-environment'; + + if (!atom.packages.isPackageDisabled(pkgName)) { + atom.packages.disablePackage(pkgName); + } + }, + + addLearnToStatusBar(statusBar) { + var leftTiles = Array.from(statusBar.getLeftTiles()); + var rightTiles = Array.from(statusBar.getRightTiles()); + var rightMostTile = rightTiles[rightTiles.length - 1]; + + var priority = ((rightMostTile != null ? rightMostTile.priority : undefined) || 0) - 1; + statusBar.addRightTile({item: this.statusView, priority}); + }, + + learnOpen(labSlug) { + if (labSlug != null) { + this.term.send(`learn open ${labSlug.toString()}\r`); + } + }, + + about() { + shell.openExternal(ABOUT_URL); + }, + + viewVersion() { + atom.notifications.addInfo(`Learn IDE: v${version}`); + } +}; diff --git a/lib/notifications/submission.coffee b/lib/notifications/submission.coffee deleted file mode 100644 index 0df90d8..0000000 --- a/lib/notifications/submission.coffee +++ /dev/null @@ -1,29 +0,0 @@ -path = require 'path' -fetch = require '../fetch' -{learnCo} = require '../config' -submissionRegistry = [] -cachedLessonTitles = {} - -getLessonTitle = (lessonID) -> - title = cachedLessonTitles[lessonID] - - if title? - return Promise.resolve(title) - - fetch("#{learnCo}/api/v1/lessons/#{lessonID}").then ({title}) => - cachedLessonTitles[lessonID] = title || 'Learn IDE' - return title - -icon = (passing) -> - pass = path.resolve(__dirname, '..', '..', 'static', 'images', 'pass.png') - fail = path.resolve(__dirname, '..', '..', 'static', 'images', 'fail.png') - if passing is 'true' then pass else fail - -module.exports = ({submission_id, lesson_id, passing, message}) -> - if not submissionRegistry.includes(submission_id) - submissionRegistry.push(submission_id) - - getLessonTitle(lesson_id).then (title) => - notif = new Notification(title, {body: message, icon: icon(passing)}) - notif.onclick = -> notif.close() - diff --git a/lib/notifications/submission.js b/lib/notifications/submission.js new file mode 100644 index 0000000..3659a2f --- /dev/null +++ b/lib/notifications/submission.js @@ -0,0 +1,38 @@ +'use babel' + +import path from 'path' +import fetch from '../fetch' +import {learnCo} from '../config' + +var submissionRegistry = []; +var cachedLessonTitles = {}; + +function getLessonTitle(lessonID) { + var title = cachedLessonTitles[lessonID]; + + if (title != null) { return Promise.resolve(title) } + + return fetch(`${learnCo}/api/v1/lessons/${lessonID}`).then(({title}) => { + cachedLessonTitles[lessonID] = title || 'Learn IDE'; + return title; + }); +}; + +function icon(passing) { + var pass = path.resolve(__dirname, '..', '..', 'static', 'images', 'pass.png'); + var fail = path.resolve(__dirname, '..', '..', 'static', 'images', 'fail.png'); + + return (passing === 'true') ? pass : fail +}; + +export default function({submission_id, lesson_id, passing, message}) { + if (submissionRegistry.includes(submission_id)) { return } + + submissionRegistry.push(submission_id); + + getLessonTitle(lesson_id).then((title) => { + var notif = new Notification(title, {body: message, icon: icon(passing)}); + notif.onclick = () => notif.close(); + }); +} + diff --git a/lib/notifier.coffee b/lib/notifier.coffee deleted file mode 100644 index 6a5e69a..0000000 --- a/lib/notifier.coffee +++ /dev/null @@ -1,37 +0,0 @@ -querystring = require 'querystring' -AtomSocket = require 'atom-socket' -atomHelper = require './atom-helper' -fetch = require './fetch' -{learnCo} = require './config' - -notificationStrategies = - submission: require('./notifications/submission') - -module.exports = -class Notifier - constructor: (token) -> - @token = token - - activate: -> - @authenticate().then ({id}) => - @connect(id) - - authenticate: -> - headers = new Headers({'Authorization': "Bearer #{@token}"}) - fetch("#{learnCo}/api/v1/users/me", {headers}) - - connect: (userID) -> - @ws = new AtomSocket('notif', "wss://push.flatironschool.com:9443/ws/fis-user-#{userID}") - - @ws.on 'message', (msg) => - @parseMessage(JSON.parse(msg)) - - parseMessage: ({text}) -> - if atomHelper.isLastFocusedWindow() - data = querystring.parse(text) - callback = notificationStrategies[data.type] - callback?(data) - - deactivate: -> - @ws.close() - diff --git a/lib/notifier.js b/lib/notifier.js new file mode 100644 index 0000000..66fa572 --- /dev/null +++ b/lib/notifier.js @@ -0,0 +1,49 @@ +'use babel' + +import AtomSocket from 'atom-socket' +import atomHelper from './atom-helper' +import fetch from './fetch' +import querystring from 'querystring' +import {learnCo} from './config' + +import submission from './notifications/submission' + +var notificationStrategies = {submission} + +export default class Notifier { + constructor(token) { + this.token = token; + } + + activate() { + return this.authenticate().then(({id}) => { + this.connect(id); + }); + } + + authenticate() { + var headers = new Headers({'Authorization': `Bearer ${this.token}`}); + return fetch(`${learnCo}/api/v1/users/me`, {headers}) + } + + connect(userID) { + this.ws = new AtomSocket('notif', `wss://push.flatironschool.com:9443/ws/fis-user-${userID}`) + + this.ws.on('message', msg => this.parseMessage(JSON.parse(msg))) + } + + parseMessage({text}) { + if (atomHelper.isLastFocusedWindow()) { + var data = querystring.parse(text); + + var strategy = notificationStrategies[data.type]; + var strategyIsDefined = typeof strategy === 'function'; + + return strategyIsDefined ? strategy(data) : undefined + } + } + + deactivate() { + this.ws.close(); + } +} diff --git a/lib/protocol.coffee b/lib/protocol.coffee deleted file mode 100644 index 5db98e4..0000000 --- a/lib/protocol.coffee +++ /dev/null @@ -1,3 +0,0 @@ -protocol = require 'register-protocol-win32' - -protocol.install('learn-ide', "#{process.execPath} --url-to-open=\"%1\"") \ No newline at end of file diff --git a/lib/protocol.js b/lib/protocol.js new file mode 100644 index 0000000..7dadc0e --- /dev/null +++ b/lib/protocol.js @@ -0,0 +1,5 @@ +'use babel' + +import protocol from 'register-protocol-win32' + +protocol.install('learn-ide', `${process.execPath} --url-to-open=\"%1\"`) diff --git a/lib/terminal.coffee b/lib/terminal.coffee deleted file mode 100644 index 30441e2..0000000 --- a/lib/terminal.coffee +++ /dev/null @@ -1,68 +0,0 @@ -{EventEmitter} = require 'events' -atomHelper = require './atom-helper' -path = require 'path' -bus = require './event-bus' -AtomSocket = require('atom-socket') - -module.exports = class Terminal extends EventEmitter - constructor: (args) -> - args || (args = {}) - - @host = args.host - @port = args.port - @path = args.path - @token = args.token - - @hasFailed = false - - @connect() - - connect: (token) -> - @socket = new AtomSocket('term', @url()) - - @waitForSocket = new Promise (resolve, reject) => - @socket.on 'open', (e) => - @emit 'open', e - resolve() - - @socket.on 'open:cached', (e) => - @emit 'open', e - resolve() - - @socket.on 'message', (message) => - decoded = new Buffer(message or '', 'base64').toString() - @emit('message', decoded) - - @socket.on 'close', (e) => - @emit 'close', e - - @socket.on 'error', (e) => - @emit 'error', e - - url: -> - version = require './version' - protocol = if @port == 443 then 'wss' else 'ws' - "#{protocol}://#{@host}:#{@port}/#{@path}?token=#{@token}&version=#{version}" - - reset: -> - @socket.reset() - - send: (msg) -> - if @waitForSocket - @waitForSocket.then => - @waitForSocket = null - @socket.send(msg) - else - @socket.send(msg) - - toggleDebugger: () -> - @socket.toggleDebugger() - - debugInfo: -> - { - host: @host, - port: @port, - path: @path, - token: @token, - socket: @socket - } diff --git a/lib/terminal.js b/lib/terminal.js new file mode 100644 index 0000000..49cde22 --- /dev/null +++ b/lib/terminal.js @@ -0,0 +1,91 @@ +'use babel' + +import AtomSocket from 'atom-socket' +import {EventEmitter} from 'events' + +export default class Terminal { + constructor(args={}) { + this.emitter = new EventEmitter(); + + this.host = args.host; + this.port = args.port; + this.path = args.path; + this.token = args.token; + + this.hasFailed = false; + + this.connect(); + } + + connect(token) { + this.socket = new AtomSocket('term', this.url()); + + this.waitForSocket = new Promise(((resolve, reject) => { + this.socket.on('open', e => { + this.emit('open', e) + resolve() + }) + + this.socket.on('open:cached', e => { + this.emit('open', e) + resolve() + }) + + this.socket.on('message', (msg='') => { + var decoded = new Buffer(msg, 'base64').toString(); + this.emit('message', decoded) + }) + + this.socket.on('close', e => this.emit('close', e)) + + this.socket.on('error', e => this.emit('error', e)) + })); + + return this.waitForSocket + } + + emit() { + return this.emitter.emit.apply(this, arguments); + } + + on() { + return this.emitter.on.apply(this, arguments); + } + + url() { + var {version} = require('../package.json'); + var protocol = (this.port === 443) ? 'wss' : 'ws'; + + return `${protocol}://${this.host}:${this.port}/${this.path}?token=${this.token}&version=${version}`; + } + + reset() { + return this.socket.reset(); + } + + send(msg) { + if (this.waitForSocket === null ) { + this.socket.send(msg) + return + } + + this.waitForSocket.then(() => { + this.waitForSocket = null; + this.socket.send(msg); + }); + } + + toggleDebugger() { + this.socket.toggleDebugger(); + } + + debugInfo() { + return { + host: this.host, + port: this.port, + path: this.path, + token: this.token, + socket: this.socket + }; + } +} diff --git a/lib/token.coffee b/lib/token.coffee deleted file mode 100644 index 2541a96..0000000 --- a/lib/token.coffee +++ /dev/null @@ -1,21 +0,0 @@ -localStorage = require './local-storage' -bus = require './event-bus' - -TOKEN_KEY = 'learn-ide:token' - -module.exports = token = { - get: -> - localStorage.get(TOKEN_KEY) - - set: (value) -> - localStorage.set(TOKEN_KEY, value) - bus.emit(TOKEN_KEY, value) - - unset: -> - localStorage.delete(TOKEN_KEY) - bus.emit(TOKEN_KEY, undefined) - - observe: (callback) -> - callback(token.get()) - bus.on(TOKEN_KEY, callback) -} diff --git a/lib/token.js b/lib/token.js new file mode 100644 index 0000000..7f82c9c --- /dev/null +++ b/lib/token.js @@ -0,0 +1,27 @@ +'use babel' + +import localStorage from './local-storage' +import bus from './event-bus' + +var tokenKey = 'learn-ide:token'; + +export default { + get() { + return localStorage.get(tokenKey); + }, + + set(value) { + localStorage.set(tokenKey, value); + bus.emit(tokenKey, value); + }, + + unset() { + localStorage.delete(tokenKey); + bus.emit(tokenKey, undefined); + }, + + observe(callback) { + callback(this.get()); + bus.on(tokenKey, callback); + } +} diff --git a/lib/updater.coffee b/lib/updater.coffee deleted file mode 100644 index a081606..0000000 --- a/lib/updater.coffee +++ /dev/null @@ -1,202 +0,0 @@ -fs = require 'fs' -path = require 'path' -semver = require 'semver' -{learnCo} = require './config' -fetch = require './fetch' -{install} = require './apm' -localStorage = require './local-storage' -{name} = require '../package.json' - -HELP_CENTER_URL = "#{learnCo}/ide/faq" -LATEST_VERSION_URL = "#{learnCo}/api/v1/learn_ide/latest_version" - -module.exports = - autoCheck: -> - if not @_shouldSkipCheck() - @_fetchLatestVersionData().then ({version, detail}) => - @_setCheckDate() - - if @_shouldUpdate(version) - @_addUpdateNotification(detail) - - checkForUpdate: -> - @_fetchLatestVersionData().then ({version, detail}) => - @_setCheckDate() - - if @_shouldUpdate(version) - @_addUpdateNotification(detail) - else - @_addUpToDateNotification() - - update: -> - localStorage.set('restartingForUpdate', true) - @updateNotification?.dismiss() - - waitNotification = - atom.notifications.addInfo 'Please wait while the update is installed...', - description: 'This may take a few minutes. Please **do not** close the editor.' - dismissable: true - - @_updatePackage().then (pkgResult) => - @_installDependencies().then (depResult) => - log = "Learn IDE:\n---\n#{pkgResult.log}" - code = pkgResult.code - - if depResult? - log += "\nDependencies:\n---\n#{depResult.log}" - code += depResult.code - - if code isnt 0 - waitNotification.dismiss() - localStorage.delete('restartingForUpdate') - @_updateFailed(log) - return - - localStorage.set('updateLog', log) - atom.restartApplication() - - didRestartAfterUpdate: -> - log = localStorage.remove('updateLog') - target = localStorage.remove('targetedUpdateVersion') - - if @_shouldUpdate(target) then @_updateFailed(log) else @_updateSucceeded() - - _fetchLatestVersionData: -> - fetch(LATEST_VERSION_URL).then (@latestVersionData) => - return @latestVersionData - - _getLatestVersion: -> - if @latestVersionData? and @latestVersionData.version? - return Promise.resolve(@latestVersionData.version) - - @_fetchLatestVersionData().then ({version}) -> - return version - - _setCheckDate: -> - localStorage.set('updateCheckDate', Date.now()) - - _shouldUpdate: (latestVersion) -> - currentVersion = require './version' - - if semver.gt(latestVersion, currentVersion) - return true - - return @_someDependencyIsMismatched() - - _shouldSkipCheck: -> - twelveHours = 12 * 60 * 60 - @_lastCheckedAgo() < twelveHours - - _lastCheckedAgo: -> - checked = parseInt(localStorage.get('updateCheckDate')) - Date.now() - checked - - _addUpdateNotification: (detail) -> - @updateNotification = - atom.notifications.addInfo 'Learn IDE: update available!', - detail: detail - description: 'Just click below to get the sweet, sweet newness.' - dismissable: true - buttons: [ - text: 'Install update & restart editor' - onDidClick: => @update() - ] - - _addUpToDateNotification: -> - atom.notifications.addSuccess 'Learn IDE: up-to-date!' - - _updatePackage: -> - @_getLatestVersion().then (version) -> - localStorage.set('targetedUpdateVersion', version) - install(name, version) - - _installDependencies: -> - @_getDependenciesToInstall().then (dependencies) => - if not dependencies? - return - - install(dependencies) - - _getDependenciesToInstall: -> - @_getUpdatedDependencies().then (dependencies) => - packagesToUpdate = null - - for pkg, version of dependencies - if @_shouldInstallDependency(pkg, version) - packagesToUpdate ?= {} - packagesToUpdate[pkg] = version - - packagesToUpdate - - _getUpdatedDependencies: -> - @_getDependenciesFromPackagesDir().catch => - @_getDependenciesFromCurrentPackage() - - _getDependenciesFromPackagesDir: -> - pkg = path.join(atom.getConfigDirPath(), 'packages', name, 'package.json') - @_getDependenciesFromPath(pkg) - - _getDependenciesFromCurrentPackage: -> - pkgJSON = path.resolve(__dirname, '..', 'package.json') - @_getDependenciesFromPath(pkgJSON) - - _getDependenciesFromPath: (pkgJSON) -> - new Promise (resolve, reject) -> - fs.readFile pkgJSON, 'utf-8', (err, data) -> - if err? - reject(err) - - try - pkg = JSON.parse(data) - catch e - console.error("Unable to parse #{pkgJSON}:", e) - return reject(e) - - dependenciesObj = pkg.packageDependencies - resolve(dependenciesObj) - - _shouldInstallDependency: (pkgName, latestVersion) -> - pkg = atom.packages.loadPackage(pkgName) - currentVersion = pkg?.metadata.version - - not semver.satisfies(currentVersion, latestVersion) - - _someDependencyIsMismatched: -> - {packageDependencies} = require('../package.json') - - for pkg, version of packageDependencies - if @_shouldInstallDependency(pkg, version) - return true - - false - - _updateFailed: (detail) -> - {shell, clipboard} = require 'electron' - - description = 'The installation seems to have been interrupted.' - buttons = [ - { - text: 'Retry' - onDidClick: => - @update() - } - { - text: 'Visit help center' - onDidClick: -> - shell.openExternal(HELP_CENTER_URL) - } - ] - - if detail? - description = 'Please include this information when contacting the Learn support team about the issue.' - buttons.push - text: 'Copy this log' - onDidClick: -> - clipboard.writeText(detail) - - @updateNotification = - atom.notifications.addWarning('Learn IDE: update failed!', {detail, description, buttons, dismissable: true}) - - _updateSucceeded: -> - atom.notifications.addSuccess('Learn IDE: update successful!') - diff --git a/lib/updater.js b/lib/updater.js new file mode 100644 index 0000000..6f1fb04 --- /dev/null +++ b/lib/updater.js @@ -0,0 +1,253 @@ +'use babel' + +import fetch from './fetch' +import fs from 'fs' +import localStorage from './local-storage' +import path from 'path' +import semver from 'semver' +import {install} from './apm' +import {learnCo} from './config' +import {name} from '../package.json' + +var helpCenterUrl = `${learnCo}/ide/faq`; +var latestVersionUrl = `${learnCo}/api/v1/learn_ide/latest_version`; + +export default { + autoCheck() { + if (this._shouldSkipCheck()) { return } + + this._fetchLatestVersionData().then(({version, detail}) => { + this._setCheckDate(); + + if (this._shouldUpdate(version)) { this._addUpdateNotification(detail) } + }); + }, + + checkForUpdate() { + this._fetchLatestVersionData().then(({version, detail}) => { + this._setCheckDate(); + + if (this._shouldUpdate(version)) { + this._addUpdateNotification(detail); + } else { + this._addUpToDateNotification(); + } + }); + }, + + update() { + localStorage.set('restartingForUpdate', true); + + if (this.updateNotification != null) { + this.updateNotification.dismiss(); + } + + var waitNotification = + atom.notifications.addInfo('Please wait while the update is installed...', { + description: 'This may take a few minutes. Please **do not** close the editor.', + dismissable: true + }); + + this._updatePackage().then(pkgResult => { + this._installDependencies().then(depResult => { + var log = `Learn IDE:\n---\n${pkgResult.log}`; + var { code } = pkgResult; + + if (depResult != null) { + log += `\nDependencies:\n---\n${depResult.log}`; + code += depResult.code; + } + + if (code !== 0) { + waitNotification.dismiss(); + localStorage.delete('restartingForUpdate'); + this._updateFailed(log); + return; + } + + localStorage.set('updateLog', log); + atom.restartApplication(); + }); + }); + }, + + didRestartAfterUpdate() { + var log = localStorage.remove('updateLog'); + var target = localStorage.remove('targetedUpdateVersion'); + + this._shouldUpdate(target) ? this._updateFailed(log) : this._updateSucceeded() + }, + + _fetchLatestVersionData() { + return fetch(latestVersionUrl).then(latestVersionData => { + this.latestVersionData = latestVersionData; + return this.latestVersionData; + }); + }, + + _getLatestVersion() { + if ((this.latestVersionData != null) && (this.latestVersionData.version != null)) { + return Promise.resolve(this.latestVersionData.version); + } + + return this._fetchLatestVersionData().then(({version}) => version); + }, + + _setCheckDate() { + localStorage.set('updateCheckDate', Date.now()); + }, + + _shouldUpdate(latestVersion) { + var {version} = require('../package.json'); + + if (semver.gt(latestVersion, version)) { + return true; + } + + return this._someDependencyIsMismatched(); + }, + + _shouldSkipCheck() { + var twelveHours = 12 * 60 * 60; + return this._lastCheckedAgo() < twelveHours; + }, + + _lastCheckedAgo() { + var checked = parseInt(localStorage.get('updateCheckDate')); + return Date.now() - checked; + }, + + _addUpdateNotification(detail) { + this.updateNotification = + atom.notifications.addInfo('Learn IDE: update available!', { + detail, + description: 'Just click below to get the sweet, sweet newness.', + dismissable: true, + buttons: [{ + text: 'Install update & restart editor', + onDidClick: () => this.update() + }] + }); + }, + + _addUpToDateNotification() { + atom.notifications.addSuccess('Learn IDE: up-to-date!'); + }, + + _updatePackage() { + return this._getLatestVersion().then(version => { + localStorage.set('targetedUpdateVersion', version); + return install(name, version); + }); + }, + + _installDependencies() { + return this._getDependenciesToInstall().then(dependencies => { + if (dependencies == null) { return } + if (Object.keys(dependencies).length <= 0) { return } + + return install(dependencies); + }); + }, + + _getDependenciesToInstall() { + return this._getUpdatedDependencies().then(dependencies => { + var packagesToUpdate = {}; + + Object.keys(dependencies).forEach(pkg => { + var version = dependencies[pkg]; + if (this._shouldInstallDependency(pkg, version)) { + packagesToUpdate[pkg] = version; + } + }) + + return packagesToUpdate; + }); + }, + + _getUpdatedDependencies() { + return this._getDependenciesFromPackagesDir().catch(() => { + return this._getDependenciesFromCurrentPackage(); + }); + }, + + _getDependenciesFromPackagesDir() { + var pkg = path.join(atom.getConfigDirPath(), 'packages', name, 'package.json'); + return this._getDependenciesFromPath(pkg); + }, + + _getDependenciesFromCurrentPackage() { + var pkgJSON = path.resolve(__dirname, '..', 'package.json'); + return this._getDependenciesFromPath(pkgJSON); + }, + + _getDependenciesFromPath(pkgJSON) { + return new Promise((resolve, reject) => { + fs.readFile(pkgJSON, 'utf-8', (err, data) => { + if (err != null) { + reject(err) + return + } + + try { + var pkg = JSON.parse(data); + } catch (e) { + console.error(`Unable to parse ${pkgJSON}:`, e); + reject(e) + return + } + + var dependenciesObj = pkg.packageDependencies; + resolve(dependenciesObj) + }); + }); + }, + + _shouldInstallDependency(pkgName, latestVersion) { + var pkg = atom.packages.loadPackage(pkgName); + var currentVersion = (pkg === null) ? undefined : pkg.metadata.version; + + return !semver.satisfies(currentVersion, latestVersion); + }, + + _someDependencyIsMismatched() { + var {packageDependencies} = require('../package.json'); + + return Object.keys(packageDependencies).some(pkg => { + var version = packageDependencies[pkg]; + return this._shouldInstallDependency(pkg, version) + }); + }, + + _updateFailed(detail) { + var {shell, clipboard} = require('electron'); + + var description = 'The installation seems to have been interrupted.'; + var buttons = [ + { + text: 'Retry', + onDidClick: () => this.update() + }, + { + text: 'Visit help center', + onDidClick() { shell.openExternal(helpCenterUrl) } + } + ]; + + if (detail != null) { + description = 'Please include this information when contacting the Learn support team about the issue.'; + buttons.push({ + text: 'Copy this log', + onDidClick() { clipboard.writeText(detail) } + }); + } + + this.updateNotification = + atom.notifications.addWarning('Learn IDE: update failed!', {detail, description, buttons, dismissable: true}); + }, + + _updateSucceeded() { + atom.notifications.addSuccess('Learn IDE: update successful!'); + } +}; + diff --git a/lib/url-handler.coffee b/lib/url-handler.coffee deleted file mode 100644 index 99a1beb..0000000 --- a/lib/url-handler.coffee +++ /dev/null @@ -1,29 +0,0 @@ -url = require 'url' -{ipcRenderer} = require 'electron' -localStorage = require './local-storage' -bus = require './event-bus' - -getLabSlug = -> - {urlToOpen} = JSON.parse(decodeURIComponent(location.hash.substr(1))) - url.parse(urlToOpen).pathname.substring(1) - -openInNewWindow = -> - localStorage.set('learnOpenLabOnActivation', getLabSlug()) - ipcRenderer.send('command', 'application:new-window') - -openInExistingWindow = -> - bus.emit('learn:open', {timestamp: Date.now(), slug: getLabSlug()}) - -windowOpen = -> - localStorage.get('lastFocusedWindow') - -onWindows = -> - process.platform == 'win32' - -module.exports = ({blobStore}) -> - if !windowOpen() || onWindows() - openInNewWindow() - else - openInExistingWindow() - - Promise.resolve() diff --git a/lib/url-handler.js b/lib/url-handler.js new file mode 100644 index 0000000..3241996 --- /dev/null +++ b/lib/url-handler.js @@ -0,0 +1,38 @@ +'use babel' + +import url from 'url' +import {ipcRenderer} from 'electron' +import localStorage from './local-storage' +import bus from './event-bus' + +function getLabSlug() { + var {urlToOpen} = JSON.parse(decodeURIComponent(location.hash.substr(1))); + return url.parse(urlToOpen).pathname.substring(1); +}; + +function openInNewWindow() { + localStorage.set('learnOpenLabOnActivation', getLabSlug()); + ipcRenderer.send('command', 'application:new-window'); +}; + +function openInExistingWindow() { + bus.emit('learn:open', {timestamp: Date.now(), slug: getLabSlug()}) +} + +function windowOpen() { + return localStorage.get('lastFocusedWindow') +} + +function onWindows() { + return process.platform === 'win32' +} + +export default function() { + if (!windowOpen() || onWindows()) { + openInNewWindow(); + } else { + openInExistingWindow(); + } + + return Promise.resolve(); +}; diff --git a/lib/version.coffee b/lib/version.coffee deleted file mode 100644 index f635607..0000000 --- a/lib/version.coffee +++ /dev/null @@ -1 +0,0 @@ -module.exports = window.LEARN_IDE_VERSION = require('../package.json').version diff --git a/package.json b/package.json index a1d744b..93ea875 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "register-protocol-win32": "1.0.0", "semver": "^5.3.0", "stacktrace-parser": "^0.1.4", - "underscore-plus": "1.6.6", "xterm": "^2.4.0" }, "packageDependencies": { @@ -48,7 +47,8 @@ "request": "2.75.0", "run-sequence": "1.2.2", "shelljs": "0.7.3", - "ssh2": "0.5.0" + "ssh2": "0.5.0", + "underscore-plus": "^1.6.6" }, "configSchema": { "notifier": { @@ -61,7 +61,7 @@ "terminalColors": { "order": 1, "type": "object", - "properties" : { + "properties": { "basic": { "order": 1, "title": "Basic Colors",