├── .env.example ├── .gitignore ├── LICENSE.md ├── README.md ├── build └── .gitkeep ├── gulpfile.js ├── keymaps └── learn-ide.json ├── lib ├── airbrake.js ├── apm.js ├── application-metadata.js ├── atom-helper.js ├── auth.js ├── colors.js ├── command-log.js ├── config.js ├── event-bus.js ├── fetch.js ├── learn-ide.js ├── learn-open.js ├── local-storage.js ├── logout.js ├── notifications │ └── submission.js ├── notifier.js ├── popout-emulator.html ├── popout-emulator.js ├── post.js ├── protocol.js ├── remote-notification.js ├── terminal-view.js ├── terminal.js ├── token.js ├── updater.js ├── url-handler.js ├── username.js └── views │ └── status.coffee ├── menus └── learn-ide.cson ├── package.json ├── resources ├── app-icons │ ├── atom.icns │ ├── atom.ico │ └── png │ │ ├── 1024.png │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 24.png │ │ ├── 256.png │ │ ├── 32.png │ │ ├── 48.png │ │ ├── 512.png │ │ ├── 64.png │ │ └── 96.png ├── script-replacements │ └── code-sign-on-mac.js └── win │ └── loading.gif ├── scripts └── airbrake_deploy ├── static └── images │ ├── fail.png │ └── pass.png ├── styles ├── learn-ide.less └── terminal-colors.css └── utils └── child-process-wrapper.js /.env.example: -------------------------------------------------------------------------------- 1 | # AIRBRAKE_ENABLED=false 2 | # AIRBRAKE_PROJECT_ID=12345 3 | # AIRBRAKE_PROJECT_KEY=ABCDEF123456 4 | 5 | # IDE_LEARN_CO=https://learn.co 6 | 7 | # IDE_WS_HOST=ile.learn.co 8 | # IDE_WS_PORT=443 9 | # IDE_WS_TERM_PATH=v2/terminal 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | .tags* 5 | .env 6 | build/* 7 | !build/.gitkeep 8 | styles/terminal-colors.css 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Flatiron School 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 | # :warning: This package is deprecated 2 | 3 | ...but checkout its replacement! https://github.com/learn-co/learn-ide-3 4 | -------------------------------------------------------------------------------- /build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/build/.gitkeep -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({silent: true}); 2 | const _ = require('underscore-plus'); 3 | const gulp = require('gulp'); 4 | const gutil = require('gulp-util'); 5 | const shell = require('shelljs'); 6 | const Client = require('ssh2').Client; 7 | const fs = require('fs'); 8 | const os = require('os'); 9 | const path = require('path'); 10 | const decompress = require('decompress'); 11 | const request = require('request'); 12 | const del = require('del'); 13 | const runSequence = require('run-sequence'); 14 | const cp = require('./utils/child-process-wrapper'); 15 | const pkg = require('./package.json') 16 | 17 | var buildBeta; 18 | 19 | var buildDir = path.join(__dirname, 'build') 20 | console.log('build directory', buildDir) 21 | 22 | function productName() { 23 | var name = 'Learn IDE'; 24 | 25 | if (buildBeta) { 26 | name += ' Beta'; 27 | } 28 | 29 | return name; 30 | } 31 | 32 | function executableName() { 33 | var name = productName().toLowerCase(); 34 | return name.replace(/ /g, '_'); 35 | } 36 | 37 | function windowsInstallerName() { 38 | return productName().replace(/ /g, '') + 'Setup.exe'; 39 | } 40 | 41 | gulp.task('setup', function() { 42 | shell.cp('./.env.example', './.env'); 43 | }); 44 | 45 | gulp.task('download-atom', function(done) { 46 | var tarballURL = `https://github.com/atom/atom/archive/v${ pkg.atomVersion }.tar.gz` 47 | console.log(`Downloading Atom from ${ tarballURL }`) 48 | var tarballPath = path.join(buildDir, 'atom.tar.gz') 49 | 50 | var r = request(tarballURL) 51 | 52 | r.on('end', function() { 53 | decompress(tarballPath, buildDir, {strip: 1}).then(function(files) { 54 | fs.unlinkSync(tarballPath) 55 | done() 56 | }).catch(function(err) { 57 | console.error(err) 58 | }) 59 | }) 60 | 61 | r.pipe(fs.createWriteStream(tarballPath)) 62 | }) 63 | 64 | gulp.task('build-atom', function(done) { 65 | process.chdir(buildDir) 66 | 67 | var cmd = path.join(buildDir, 'script', 'build') 68 | var args = [] 69 | 70 | switch (process.platform) { 71 | case 'win32': 72 | args.push('--create-windows-installer'); 73 | break; 74 | 75 | case 'darwin': 76 | args.push('--compress-artifacts'); 77 | args.push('--code-sign'); 78 | break; 79 | 80 | case 'linux': 81 | args.push('--create-rpm-package'); 82 | args.push('--create-debian-package'); 83 | break; 84 | } 85 | 86 | if (process.platform == 'win32') { 87 | args = ['/s', '/c', cmd].concat(args); 88 | cmd = 'cmd'; 89 | } 90 | 91 | console.log('running command: ' + cmd + ' ' + args.join(' ')) 92 | cp.safeSpawn(cmd, args, function() { 93 | done() 94 | }) 95 | }) 96 | 97 | gulp.task('reset', function() { 98 | del.sync(['build/**/*', '!build/.gitkeep'], {dot: true}) 99 | }) 100 | 101 | gulp.task('sleep', function(done) { 102 | setTimeout(function() { done() }, 1000 * 60) 103 | }) 104 | 105 | gulp.task('inject-packages', function() { 106 | function rmPackage(name) { 107 | var packageJSON = path.join(buildDir, 'package.json') 108 | var packages = JSON.parse(fs.readFileSync(packageJSON)) 109 | delete packages.packageDependencies[name] 110 | fs.writeFileSync(packageJSON, JSON.stringify(packages, null, ' ')) 111 | } 112 | 113 | function injectPackage(name, version) { 114 | var packageJSON = path.join(buildDir, 'package.json') 115 | var packages = JSON.parse(fs.readFileSync(packageJSON)) 116 | packages.packageDependencies[name] = version 117 | fs.writeFileSync(packageJSON, JSON.stringify(packages, null, ' ')) 118 | } 119 | 120 | var pkg = require('./package.json') 121 | rmPackage('welcome') 122 | rmPackage('tree-view') 123 | rmPackage('about') 124 | rmPackage('notifications') 125 | injectPackage(pkg.name, pkg.version) 126 | _.each(pkg.packageDependencies, (version, name) => { 127 | injectPackage(name, version) 128 | }) 129 | }) 130 | 131 | gulp.task('replace-files', function() { 132 | var iconSrc = path.join('resources', 'app-icons', '**', '*'); 133 | var iconDest = path.join(buildDir, 'resources', 'app-icons', 'stable') 134 | 135 | gulp.src([iconSrc]).pipe(gulp.dest(iconDest)); 136 | 137 | var winSrc = path.join('resources', 'win', '**', '*'); 138 | var winDest = path.join(buildDir, 'resources', 'win'); 139 | 140 | gulp.src([winSrc]).pipe(gulp.dest(winDest)); 141 | 142 | var scriptSrc = path.join('resources', 'script-replacements', '**', '*'); 143 | var scriptDest = path.join(buildDir, 'script', 'lib') 144 | 145 | gulp.src([scriptSrc]).pipe(gulp.dest(scriptDest)); 146 | }) 147 | 148 | gulp.task('alter-files', function() { 149 | function replaceInFile(filepath, replaceArgs) { 150 | var data = fs.readFileSync(filepath, 'utf8'); 151 | 152 | replaceArgs.forEach(function(args) { 153 | data = data.replace(args[0], args[1]); 154 | }); 155 | 156 | fs.writeFileSync(filepath, data) 157 | } 158 | 159 | replaceInFile(path.join(buildDir, 'script', 'lib', 'create-windows-installer.js'), [ 160 | [ 161 | 'https://raw.githubusercontent.com/atom/atom/master/resources/app-icons/${CONFIG.channel}/atom.ico', 162 | 'https://raw.githubusercontent.com/learn-co/learn-ide/master/resources/app-icons/atom.ico' 163 | ] 164 | ]) 165 | 166 | replaceInFile(path.join(buildDir, 'script', 'lib', 'create-rpm-package.js'), [ 167 | ['atom.${generatedArch}.rpm', executableName() + '.${generatedArch}.rpm'], 168 | [/'Atom Beta' : 'Atom'/g, "'" + productName() + "' : '" + productName() + "'"] 169 | ]); 170 | 171 | replaceInFile(path.join(buildDir, 'script', 'lib', 'create-debian-package.js'), [ 172 | ['atom-${arch}.deb', executableName() + '-${arch}.deb'], 173 | [/'Atom Beta' : 'Atom'/g, "'" + productName() + "' : '" + productName() + "'"] 174 | ]); 175 | 176 | replaceInFile(path.join(buildDir, 'script', 'lib', 'package-application.js'), [ 177 | [/'Atom Beta' : 'Atom'/g, "'" + productName() + "' : '" + productName() + "'"] 178 | ]); 179 | 180 | replaceInFile(path.join(buildDir, 'script', 'lib', 'package-application.js'), [ 181 | [/'Atom'/g, `'${productName()}'`] 182 | ]); 183 | 184 | if (process.platform != 'linux') { 185 | replaceInFile(path.join(buildDir, 'script', 'lib', 'package-application.js'), [ 186 | [/return 'atom'/, "return '" + executableName() + "'"], 187 | [/'atom-beta' : 'atom'/g, "'" + executableName() + "' : '" + executableName() + "'"] 188 | ]); 189 | } 190 | 191 | replaceInFile(path.join(buildDir, 'script', 'lib', 'compress-artifacts.js'), [ 192 | [/atom-/g, executableName() + '-'] 193 | ]); 194 | 195 | replaceInFile(path.join(buildDir, 'src', 'main-process', 'atom-application.coffee'), [ 196 | [ 197 | /options.socketPath = "\\\\\\\\.\\\\pipe\\\\atom-#{options.version}-#{userNameSafe}-#{process.arch}-sock"/, 198 | 'options.socketPath = "\\\\\\\\.\\\\pipe\\\\' + executableName() + '-#{options.version}-#{userNameSafe}-#{process.arch}-sock"' 199 | ], 200 | [ 201 | 'options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.env.USER}.sock")', 202 | 'options.socketPath = path.join(os.tmpdir(), "' + executableName() + '-#{options.version}-#{process.env.USER}.sock")' 203 | ] 204 | ]); 205 | 206 | replaceInFile(path.join(buildDir, 'resources', 'mac', 'atom-Info.plist'), [ 207 | [ 208 | /(CFBundleURLSchemes.+\n.+\n.+)(atom)(.+)/, 209 | '$1learn-ide$3' 210 | ] 211 | ]); 212 | 213 | replaceInFile(path.join(buildDir, 'src', 'main-process', 'atom-protocol-handler.coffee'), [ 214 | [ 215 | /(registerFileProtocol.+)(atom)(.+)/, 216 | '$1learn-ide$3' 217 | ] 218 | ]); 219 | 220 | replaceInFile(path.join(buildDir, 'src', 'main-process', 'parse-command-line.js'), [ 221 | [ 222 | /(urlsToOpen.+)/, 223 | "$1\n if (args['url-to-open']) { urlsToOpen.push(args['url-to-open']) }\n" 224 | ], 225 | [ 226 | /(const args)/, 227 | "options.string('url-to-open')\n $1" 228 | ] 229 | ]); 230 | 231 | replaceInFile(path.join(buildDir, 'menus', 'darwin.cson'), [ 232 | [ 233 | "{ label: 'Check for Update', command: 'application:check-for-update', visible: false}", 234 | "{ label: 'Check for Update', command: 'learn-ide:update-check'}" 235 | ], 236 | [ 237 | "{ label: 'VERSION', enabled: false }\n { label: 'Restart and Install Update', command: 'application:install-update', visible: false}", 238 | "{ label: 'View Version', command: 'learn-ide:view-version'}" 239 | ], 240 | [/About Atom/, 'About'], 241 | [/application:about/, 'learn-ide:about'], 242 | [/application:open-faq/, 'learn-ide:faq'], 243 | [/application:report-issue/, 'learn-ide:report-issue'], 244 | ["\n { label: 'Search Issues', command: 'application:search-issues' }", ""], 245 | ]); 246 | 247 | replaceInFile(path.join(buildDir, 'menus', 'win32.cson'), [ 248 | [ 249 | "{ label: 'Check for Update', command: 'application:check-for-update', visible: false}", 250 | "{ label: 'Check for Update', command: 'learn-ide:update-check'}" 251 | ], 252 | [ 253 | "{ label: 'VERSION', enabled: false }\n { label: 'Restart and Install Update', command: 'application:install-update', visible: false}", 254 | "{ label: 'View Version', command: 'learn-ide:view-version'}" 255 | ], 256 | [ 257 | "\n { label: 'Checking for Update', enabled: false, visible: false}\n { label: 'Downloading Update', enabled: false, visible: false}", 258 | '' 259 | ], 260 | [/About Atom/, 'About'], 261 | [/application:about/, 'learn-ide:about'], 262 | [/application:open-faq/, 'learn-ide:faq'], 263 | [/application:report-issue/, 'learn-ide:report-issue'], 264 | ["\n { label: 'Search Issues', command: 'application:search-issues' }", ""], 265 | ]); 266 | 267 | replaceInFile(path.join(buildDir, 'menus', 'linux.cson'), [ 268 | [/About Atom/, 'About'], 269 | [ 270 | '{ label: "VERSION", enabled: false }', 271 | "{ label: 'View Version', command: 'learn-ide:view-version'}" 272 | ], 273 | [/application:about/, 'learn-ide:about'], 274 | [/application:open-faq/, 'learn-ide:faq'], 275 | [/application:report-issue/, 'learn-ide:report-issue'], 276 | ["\n { label: 'Search Issues', command: 'application:search-issues' }", ""], 277 | ]); 278 | 279 | replaceInFile(path.join(buildDir, 'src', 'config-schema.js'), [ 280 | [ 281 | "automaticallyUpdate: {\n description: 'Automatically update Atom when a new release is available.',\n type: 'boolean',\n default: true\n }", 282 | "automaticallyUpdate: {\n description: 'Automatically update Atom when a new release is available.',\n type: 'boolean',\n default: false\n }", 283 | ], 284 | [ 285 | "openEmptyEditorOnStart: {\n description: 'When checked opens an untitled editor when loading a blank environment (such as with _File > New Window_ or when \"Restore Previous Windows On Start\" is unchecked); otherwise no editor is opened when loading a blank environment. This setting has no effect when restoring a previous state.',\n type: 'boolean',\n default: true", 286 | "openEmptyEditorOnStart: {\n description: 'When checked opens an untitled editor when loading a blank environment (such as with _File > New Window_ or when \"Restore Previous Windows On Start\" is unchecked); otherwise no editor is opened when loading a blank environment. This setting has no effect when restoring a previous state.',\n type: 'boolean',\n default: false" 287 | ], 288 | [ 289 | "restorePreviousWindowsOnStart: {\n description: 'When checked restores the last state of all Atom windows when started from the icon or `atom` by itself from the command line; otherwise a blank environment is loaded.',\n type: 'boolean',\n default: true", 290 | "restorePreviousWindowsOnStart: {\n description: 'When checked restores the last state of all Atom windows when started from the icon or `atom` by itself from the command line; otherwise a blank environment is loaded.',\n type: 'boolean',\n default: false" 291 | ], 292 | [ 293 | "['one-dark-ui', 'one-dark-syntax']", "['learn-ide-material-ui', 'atom-material-syntax']" 294 | ] 295 | ]); 296 | }) 297 | 298 | gulp.task('update-package-json', function() { 299 | var packageJSON = path.join(buildDir, 'package.json') 300 | var atomPkg = JSON.parse(fs.readFileSync(packageJSON)) 301 | var learnPkg = require('./package.json') 302 | 303 | atomPkg.name = executableName() 304 | atomPkg.productName = productName() 305 | atomPkg.version = learnPkg.version 306 | atomPkg.description = learnPkg.description 307 | 308 | fs.writeFileSync(packageJSON, JSON.stringify(atomPkg, null, ' ')) 309 | }) 310 | 311 | gulp.task('rename-installer', function(done) { 312 | var src = path.join(buildDir, 'out', productName() + 'Setup.exe'); 313 | var des = path.join(buildDir, 'out', windowsInstallerName()); 314 | 315 | fs.rename(src, des, function (err) { 316 | if (err) { 317 | console.log('error while renaming: ', err.message) 318 | } 319 | 320 | done() 321 | }) 322 | }) 323 | 324 | gulp.task('sign-installer', function() { 325 | var certPath = process.env.FLATIRON_P12KEY_PATH; 326 | var password = process.env.FLATIRON_P12KEY_PASSWORD; 327 | 328 | if (!certPath || !password) { 329 | console.log('unable to sign installer, must provide FLATIRON_P12KEY_PATH and FLATIRON_P12KEY_PASSWORD environment variables') 330 | return 331 | } 332 | 333 | var cmd = path.join(buildDir, 'script', 'node_modules', 'electron-winstaller', 'vendor', 'signtool.exe') 334 | var installer = path.join(buildDir, 'out', windowsInstallerName()); 335 | args = ['sign', '/a', '/f', certPath, '/p', "'" + password + "'", installer] 336 | 337 | console.log('running command: ' + cmd + ' ' + args.join(' ')) 338 | cp.safeSpawn(cmd, args, function() { 339 | done() 340 | }) 341 | }) 342 | 343 | gulp.task('cleanup', function(done) { 344 | switch (process.platform) { 345 | case 'win32': 346 | runSequence('rename-installer', 'sign-installer', done) 347 | break; 348 | 349 | case 'darwin': 350 | done() 351 | break; 352 | 353 | case 'linux': 354 | done() 355 | break; 356 | } 357 | }) 358 | 359 | gulp.task('prep-build', function(done) { 360 | runSequence( 361 | 'inject-packages', 362 | 'replace-files', 363 | 'alter-files', 364 | 'update-package-json', 365 | done 366 | ) 367 | }) 368 | 369 | gulp.task('build', function(done) { 370 | var pkg = require('./package.json') 371 | if (pkg.version.match(/beta/)) { buildBeta = true } 372 | 373 | runSequence( 374 | 'reset', 375 | 'download-atom', 376 | 'prep-build', 377 | 'build-atom', 378 | 'cleanup', 379 | done 380 | ) 381 | }) 382 | 383 | gulp.task('mastermind', function(done) { 384 | // update package.json 385 | var pkg = require('./package.json') 386 | pkg.name = 'mastermind' 387 | pkg.description = 'The Learn IDE\'s evil twin that we use for testing' 388 | pkg.packageDependencies['mirage'] = 'learn-co/mirage#master' 389 | pkg.repository = pkg.repository.replace('learn-ide', 'mastermind') 390 | delete pkg.packageDependencies['learn-ide-tree'] 391 | fs.writeFileSync('./package.json', JSON.stringify(pkg, null, ' ')) 392 | 393 | // update gulpfile 394 | var gf = fs.readFileSync('./gulpfile.js', 'utf-8') 395 | var updated = gf.replace('Learn IDE', 'Mastermind IDE') 396 | fs.writeFileSync('./gulpfile.js', updated) 397 | 398 | // update menus 399 | var menu = fs.readFileSync('./menus/learn-ide.cson', 'utf-8') 400 | var updated = menu.replace(/Learn IDE/g, 'Mastermind') 401 | fs.writeFileSync('./menus/learn-ide.cson', updated) 402 | }) 403 | 404 | -------------------------------------------------------------------------------- /keymaps/learn-ide.json: -------------------------------------------------------------------------------- 1 | { 2 | ".platform-darwin": { 3 | "cmd-i": "learn-ide:toggle-terminal", 4 | "cmd-;": "learn-ide:toggle-focus", 5 | "cmd-:": "learn-ide:toggle-popout" 6 | }, 7 | ".platform-win32, .platform-linux": { 8 | "ctrl-U": "learn-ide:toggle-terminal", 9 | "ctrl-;": "learn-ide:toggle-focus", 10 | "ctrl-:": "learn-ide:toggle-popout" 11 | }, 12 | ".platform-darwin .terminal": { 13 | "cmd-=": "learn-ide:increase-font-size", 14 | "cmd--": "learn-ide:decrease-font-size", 15 | "cmd-0": "learn-ide:reset-font-size", 16 | "cmd-up": "learn-ide:scroll-up", 17 | "cmd-down": "learn-ide:scroll-down", 18 | "ctrl-alt-up": "learn-ide:scroll-up", 19 | "ctrl-alt-down": "learn-ide:scroll-down" 20 | }, 21 | ".platform-win32 .terminal, .platform-linux .terminal": { 22 | "ctrl-=": "learn-ide:increase-font-size", 23 | "ctrl--": "learn-ide:decrease-font-size", 24 | "ctrl-0": "learn-ide:reset-font-size", 25 | "ctrl-C": "core:copy", 26 | "ctrl-V": "core:paste", 27 | "ctrl-up": "learn-ide:scroll-up", 28 | "ctrl-down": "learn-ide:scroll-down" 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /lib/airbrake.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import commandLog from './command-log' 4 | import path from 'path' 5 | import post from './post' 6 | import remote from 'remote' 7 | import token from './token' 8 | import {learnCo, airbrakeEnabled} from './config' 9 | import {name, version} from './application-metadata' 10 | import {parse} from 'stacktrace-parser' 11 | 12 | const fs = remote.require('fs-plus') 13 | 14 | const util = { 15 | pkgPath() { 16 | return path.resolve(__dirname, '..') 17 | }, 18 | 19 | shouldNotify() { 20 | if (airbrakeEnabled != null) { return airbrakeEnabled } 21 | 22 | // package is symlinked to ~/.atom/packages, likely for dev purposes 23 | var isProbablyDevelopmentPackage = fs.isSymbolicLinkSync(this.pkgPath()) 24 | 25 | return !isProbablyDevelopmentPackage 26 | }, 27 | 28 | appVersion() { 29 | return name.includes('atom') ? version : window.LEARN_IDE_VERSION 30 | }, 31 | 32 | backtrace(stack='') { 33 | return parse(stack).map((entry) => { 34 | return { 35 | file: entry.file, 36 | line: entry.lineNumber, 37 | column: entry.column, 38 | function: entry.methodName 39 | }; 40 | }); 41 | }, 42 | 43 | rootDirectory(stack) { 44 | if (stack.match(this.pkgPath()) === null) { return } 45 | 46 | return this.pkgPath(); 47 | }, 48 | 49 | payload(err) { 50 | return { 51 | error: { 52 | message: err.message, 53 | type: err.name, 54 | backtrace: this.backtrace(err.stack) 55 | }, 56 | context: { 57 | environment: name, 58 | os: process.platform, 59 | version: this.appVersion(), 60 | rootDirectory: this.rootDirectory(err.stack) 61 | }, 62 | additional: { 63 | commands: commandLog.get(), 64 | core_app_version: version, 65 | package_version: window.LEARN_IDE_VERSION, 66 | occurred_at: Date.now(), 67 | os_detail: navigator.platform, 68 | token: token.get() 69 | } 70 | } 71 | } 72 | } 73 | 74 | export default { 75 | notify(err) { 76 | var url = `${learnCo}/api/v1/learn_ide_airbrake`; 77 | 78 | if (!util.shouldNotify()) { 79 | console.warn(`*Airbrake notification will not be sent for "${err.message}"`) 80 | return Promise.resolve() 81 | } 82 | 83 | return post(url, util.payload(err), {'Authorization': `Bearer ${token.get()}`}); 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /lib/apm.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import {BufferedProcess} from 'atom'; 4 | 5 | function run(args) { 6 | return new Promise(function(resolve) { 7 | var command = atom.packages.getApmPath(); 8 | 9 | var log = ''; 10 | var stdout = data => log += `${data}`; 11 | var stderr = data => log += `${data}`; 12 | 13 | var exit = code => resolve({log, code}); 14 | 15 | new BufferedProcess({command, args, stdout, stderr, exit}); 16 | }); 17 | } 18 | 19 | function fullname(name, version) { 20 | return (version != null) ? `${name}@${version}` : name 21 | }; 22 | 23 | function parseDependencies(dependencies) { 24 | return Object.keys(dependencies).map((name) => { 25 | return fullname(name, dependencies[name]) 26 | }) 27 | }; 28 | 29 | export default { 30 | install(name, version) { 31 | var nameIsObject = typeof name === 'object'; 32 | 33 | var args = nameIsObject ? parseDependencies(name) : [fullname(name, version)]; 34 | 35 | return run(['install', '--compatible', '--no-confirm', '--no-color'].concat(args)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/application-metadata.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | export default require(`${atom.packages.resourcePathWithTrailingSlash}package.json`); 4 | -------------------------------------------------------------------------------- /lib/atom-helper.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import localStorage from './local-storage'; 4 | import { name } from '../package.json'; 5 | 6 | export default { 7 | isLastFocusedWindow() { 8 | return parseInt(localStorage.get('lastFocusedWindow')) === process.pid; 9 | }, 10 | 11 | setLastFocusedWindow() { 12 | localStorage.set('lastFocusedWindow', process.pid); 13 | }, 14 | 15 | trackFocusedWindow() { 16 | this.setLastFocusedWindow(); 17 | window.onfocus = this.setLastFocusedWindow; 18 | }, 19 | 20 | cleanup() { 21 | if (this.isLastFocusedWindow()) { 22 | localStorage.delete('lastFocusedWindow'); 23 | } 24 | }, 25 | 26 | emit(key, detail) { 27 | atom.emitter.emit(key, detail); 28 | }, 29 | 30 | on(key, callback) { 31 | return atom.emitter.on(key, callback); 32 | }, 33 | 34 | closePaneItems() { 35 | atom.workspace.getPanes().forEach(pane => pane.close()); 36 | }, 37 | 38 | resetPackage() { 39 | atom.packages.deactivatePackage(name); 40 | 41 | atom.packages.activatePackage(name).then(() => 42 | atom.menu.sortPackagesMenu() 43 | ); 44 | }, 45 | 46 | reloadStylesheets() { 47 | var pkg = atom.packages.getActivePackage(name); 48 | pkg.reloadStylesheets(); 49 | }, 50 | 51 | addStylesheet(css) { 52 | atom.styles.addStyleSheet(css); 53 | } 54 | }; 55 | 56 | -------------------------------------------------------------------------------- /lib/auth.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import _token from './token'; 4 | import _url from 'url'; 5 | import fetch from './fetch'; 6 | import localStorage from './local-storage'; 7 | import shell from 'shell'; 8 | import {BrowserWindow} from 'remote'; 9 | import {learnCo} from './config'; 10 | import {version} from '../package.json'; 11 | 12 | var authUrl = `${learnCo}/api/v1/users/me?ile_version=${version}`; 13 | 14 | function confirmOauthToken(token) { 15 | var headers = new Headers({'Authorization': `Bearer ${token}`}); 16 | 17 | return fetch(authUrl, {headers}).then(function(data) { 18 | return (data.email != null) ? data : false 19 | }); 20 | } 21 | 22 | function githubLogin() { 23 | return new Promise((resolve, reject) => { 24 | var win = new BrowserWindow({autoHideMenuBar: true, show: false, width: 440, height: 660, resizable: false}); 25 | var { webContents } = win; 26 | 27 | win.setSkipTaskbar(true); 28 | win.setMenuBarVisibility(false); 29 | win.setTitle('Sign in to Github to get started with the Learn IDE'); 30 | 31 | // show window only if login is required 32 | webContents.on('did-finish-load', () => win.show()); 33 | 34 | // hide window immediately after login 35 | webContents.on('will-navigate', (e, url) => { 36 | if (url.match(`${learnCo}/users/auth/github/callback`)) { return win.hide(); } 37 | }); 38 | 39 | webContents.on('did-get-redirect-request', (e, oldURL, newURL) => { 40 | if (!newURL.match(/ide_token/)) { return; } 41 | 42 | var token = _url.parse(newURL, true).query.ide_token; 43 | 44 | confirmOauthToken(token).then((res) => { 45 | if (res == null) { return; } 46 | 47 | localStorage.set('didCompleteGithubLogin'); 48 | _token.set(token); 49 | win.destroy(); 50 | resolve(); 51 | }); 52 | }); 53 | 54 | if (!win.loadURL(`${learnCo}/ide/token?ide_config=true`)) { 55 | atom.notifications.warning('Learn IDE: connectivity issue', { 56 | detail: `The editor is unable to connect to ${learnCo}. Are you connected to the internet?`, 57 | buttons: [ 58 | {text: 'Try again', onDidClick() { learnSignIn(); }} 59 | ] 60 | }); 61 | }}) 62 | }; 63 | 64 | function learnSignIn() { 65 | return new Promise((resolve, reject) => { 66 | var win = new BrowserWindow({autoHideMenuBar: true, show: false, width: 400, height: 600, resizable: false}); 67 | var {webContents} = win; 68 | 69 | win.setSkipTaskbar(true); 70 | win.setMenuBarVisibility(false); 71 | win.setTitle('Welcome to the Learn IDE'); 72 | 73 | webContents.on('did-finish-load', () => win.show()); 74 | 75 | webContents.on('new-window', (e, url) => { 76 | e.preventDefault(); 77 | win.destroy(); 78 | shell.openExternal(url); 79 | }); 80 | 81 | webContents.on('will-navigate', (e, url) => { 82 | if (url.match(/github_sign_in/)) { 83 | win.destroy(); 84 | githubLogin().then(resolve); 85 | } 86 | }); 87 | 88 | webContents.on('did-get-redirect-request', (e, oldURL, newURL) => { 89 | if (newURL.match(/ide_token/)) { 90 | var token = _url.parse(newURL, true).query.ide_token; 91 | 92 | if (token != null && token.length) { 93 | confirmOauthToken(token).then((res) => { 94 | if (!res) { return; } 95 | _token.set(token); 96 | resolve(); 97 | }); 98 | } 99 | } 100 | 101 | if (newURL.match(/github_sign_in/)) { 102 | win.destroy(); 103 | githubLogin().then(resolve); 104 | } 105 | }); 106 | 107 | if (!win.loadURL(`${learnCo}/ide/sign_in?ide_onboard=true`)) { 108 | win.destroy(); 109 | githubLogin.then(resolve); 110 | } 111 | }) 112 | } 113 | 114 | export default function() { 115 | var existingToken = _token.get(); 116 | return (!existingToken) ? learnSignIn() : confirmOauthToken(existingToken) 117 | } 118 | -------------------------------------------------------------------------------- /lib/colors.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import atomHelper from './atom-helper'; 6 | import { name } from '../package.json'; 7 | 8 | const stylesheetPath = path.join(__dirname, '..', 'styles', 'terminal-colors.css'); 9 | 10 | const helper = { 11 | convertLegacyConfig() { 12 | var text = atom.config.get(`${name}.terminalFontColor`); 13 | atom.config.unset(`${name}.terminalFontColor`); 14 | 15 | if (text != null) { 16 | atom.config.set(`${name}.terminalColors.basic.foreground`, text); 17 | } 18 | 19 | var background = atom.config.get(`${name}.terminalBackgroundColor`); 20 | atom.config.unset(`${name}.terminalBackgroundColor`); 21 | 22 | if (background != null) { 23 | atom.config.set(`${name}.terminalColors.basic.background`, background); 24 | } 25 | }, 26 | 27 | ansiObjectToArray(ansiColorsObject) { 28 | var colorArray = []; 29 | 30 | for (var indexish in ansiColorsObject) { 31 | var color = ansiColorsObject[indexish]; 32 | var index = parseInt(indexish); 33 | 34 | colorArray[index] = color.toRGBAString(); 35 | } 36 | 37 | return colorArray; 38 | }, 39 | 40 | ansiArrayToObject(ansiColorsArray) { 41 | var colorObject = {}; 42 | 43 | ansiColorsArray.forEach((color, index) => colorObject[index] = color); 44 | 45 | return colorObject; 46 | }, 47 | 48 | buildCSS({foreground, background, ansiColors}) { 49 | var context = '.terminal.xterm' 50 | 51 | var css = `${context} {color: ${foreground}; background-color: ${background}}\ 52 | .bottom .tool-panel .terminal-resizer, ${context} .xterm-rows, ${context} .xterm-viewport {background-color: ${background}}\n`; 53 | 54 | ansiColors.forEach((color, index) => { 55 | css += `${context} .xterm-color-${index} {color: ${color}}\ 56 | ${context} .xterm-bg-color-${index} {background-color: ${color}}\n` 57 | }); 58 | 59 | return css; 60 | }, 61 | 62 | addStylesheet(css) { 63 | return new Promise((resolve, reject) => { 64 | fs.writeFile(stylesheetPath, css, (err) => { 65 | if (err != null) { 66 | console.warn('unable to write colors to file:', err); 67 | atomHelper.addStylesheet(css); 68 | } 69 | 70 | resolve(); 71 | }); 72 | }); 73 | } 74 | }; 75 | 76 | const colors = { 77 | apply() { 78 | helper.convertLegacyConfig(); 79 | 80 | var css = this.getCSS(); 81 | 82 | helper.addStylesheet(css).then(() => 83 | atomHelper.reloadStylesheets() 84 | ); 85 | }, 86 | 87 | getCSS() { 88 | var foreground = atom.config.get(`${name}.terminalColors.basic.foreground`).toRGBAString(); 89 | var background = atom.config.get(`${name}.terminalColors.basic.background`).toRGBAString(); 90 | var ansiColors = helper.ansiObjectToArray(atom.config.get(`${name}.terminalColors.ansi`)); 91 | 92 | return helper.buildCSS({foreground, background, ansiColors}); 93 | }, 94 | 95 | parseJSON(jsonString) { 96 | var scheme; 97 | 98 | if ((jsonString == null) || !jsonString.length) { 99 | return; 100 | } 101 | 102 | try { 103 | scheme = JSON.parse(jsonString); 104 | } catch (err) { 105 | atom.notifications.addWarning('Learn IDE: Unable to parse color scheme!', { 106 | description: 'The scheme you\'ve entered is invalid JSON. Did you export the complete JSON from [terminal.sexy](https://terminal.sexy)?' 107 | }); 108 | return; 109 | } 110 | 111 | var {color, foreground, background} = scheme; 112 | var itemIsMissing = [color, foreground, background].some((i) => i == null) 113 | 114 | if (itemIsMissing) { 115 | atom.notifications.addWarning('Learn IDE: Unable to parse color scheme!', { 116 | description: 'The scheme you\'ve entered is incomplete. Be sure to export the complete JSON from [terminal.sexy](https://terminal.sexy)?' 117 | }); 118 | return; 119 | } 120 | 121 | var ansiColorsObject = helper.ansiArrayToObject(color); 122 | 123 | atom.config.set(`${name}.terminalColors.ansi`, ansiColorsObject); 124 | atom.config.set(`${name}.terminalColors.basic`, {foreground, background}); 125 | } 126 | }; 127 | 128 | export default colors; 129 | 130 | -------------------------------------------------------------------------------- /lib/command-log.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import localStorage from './local-storage' 4 | 5 | const key = 'learn-ide:recent-commands'; 6 | 7 | export default { 8 | get() { 9 | var commands = JSON.parse(localStorage.get(key)) || []; 10 | 11 | return commands; 12 | }, 13 | 14 | add(command) { 15 | var commands = this.get(); 16 | 17 | if (commands.unshift(command) > 5) { commands.pop() } 18 | 19 | localStorage.set(key, JSON.stringify(commands)) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import path from 'path' 4 | import dotenv from 'dotenv' 5 | 6 | dotenv.config({ 7 | path: path.join(__dirname, '..', '.env'), 8 | silent: true 9 | }); 10 | 11 | dotenv.config({ 12 | path: path.join(atom.getConfigDirPath(), '.env'), 13 | silent: true 14 | }); 15 | 16 | const util = { 17 | defaultConfig: { 18 | host: 'ile.learn.co', 19 | port: 443, 20 | path: 'environment', 21 | learnCo: 'https://learn.co' 22 | }, 23 | 24 | envConfig() { 25 | return this.clean({ 26 | host: process.env['IDE_WS_HOST'], 27 | port: process.env['IDE_WS_PORT'], 28 | path: process.env['IDE_WS_TERM_PATH'], 29 | learnCo: process.env['IDE_LEARN_CO'], 30 | airbrakeEnabled: this.airbrakeEnabled() 31 | }) 32 | }, 33 | 34 | airbrakeEnabled() { 35 | if (process.env['AIRBRAKE_ENABLED'] === 'true') { return true } 36 | if (process.env['AIRBRAKE_ENABLED'] === 'false') { return false } 37 | }, 38 | 39 | clean(obj) { 40 | var cleanObj = {}; 41 | 42 | Object.keys(obj).forEach((key) => { 43 | if (obj[key] != null) { cleanObj[key] = obj[key] } 44 | }) 45 | 46 | return cleanObj; 47 | } 48 | } 49 | 50 | export default { ...util.defaultConfig, ...util.envConfig() } 51 | 52 | -------------------------------------------------------------------------------- /lib/event-bus.js: -------------------------------------------------------------------------------- 1 | var pageBus = require('page-bus'); 2 | 3 | module.exports = pageBus({key: 'learn-ide'}); 4 | 5 | -------------------------------------------------------------------------------- /lib/fetch.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | export default function(url, init) { 4 | return fetch(url, init).then(response => response.text()).then(body => JSON.parse(body)) 5 | } 6 | -------------------------------------------------------------------------------- /lib/learn-ide.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import Notifier from './notifier' 4 | import StatusView from './views/status' 5 | import Terminal from './terminal' 6 | import TerminalView from './terminal-view' 7 | import airbrake from './airbrake' 8 | import atomHelper from './atom-helper' 9 | import auth from './auth' 10 | import bus from './event-bus' 11 | import colors from './colors' 12 | import config from './config' 13 | import localStorage from './local-storage' 14 | import logout from './logout' 15 | import remoteNotification from './remote-notification' 16 | import token from './token' 17 | import updater from './updater' 18 | import {CompositeDisposable} from 'atom' 19 | import {name, version} from '../package.json' 20 | import {shell} from 'electron' 21 | 22 | window.LEARN_IDE_VERSION = version; 23 | 24 | const ABOUT_URL = `${config.learnCo}/ide/about`; 25 | const FAQ_URL = `${config.learnCo}/ide/faq`; 26 | const REPORT_ISSUE_URL = `${config.learnCo}/ide/report-issue`; 27 | 28 | export default { 29 | token, 30 | 31 | activate(state) { 32 | this.subscriptions = new CompositeDisposable; 33 | 34 | this.activateMonitor(); 35 | this.registerWindowsProtocol(); 36 | this.disableFormerPackage(); 37 | 38 | colors.apply(); 39 | 40 | this.subscribeToLogin(); 41 | 42 | this.waitForAuth = auth().then(() => { 43 | this.activateIDE(state); 44 | }).catch(() => { 45 | this.activateIDE(state); 46 | }); 47 | }, 48 | 49 | activateIDE(state) { 50 | this.isRestartAfterUpdate = localStorage.remove('restartingForUpdate') === 'true'; 51 | 52 | if (this.isRestartAfterUpdate) { 53 | updater.didRestartAfterUpdate(); 54 | } 55 | 56 | this.activateTerminal(); 57 | this.activateStatusView(state); 58 | this.activateEventHandlers(); 59 | this.activateSubscriptions(); 60 | this.activateNotifier(); 61 | this.activateUpdater(); 62 | this.activateRemoteNotification(); 63 | }, 64 | 65 | activateTerminal() { 66 | this.term = new Terminal({ 67 | host: config.host, 68 | port: config.port, 69 | path: config.path, 70 | token: this.token.get() 71 | }); 72 | 73 | this.termView = new TerminalView(this.term); 74 | }, 75 | 76 | activateStatusView(state) { 77 | this.statusView = new StatusView(state, this.term); 78 | }, 79 | 80 | activateEventHandlers() { 81 | atomHelper.trackFocusedWindow(); 82 | 83 | // listen for learn:open event from other render processes (url handler) 84 | bus.on('learn:open', lab => { 85 | this.learnOpen(lab.slug); 86 | atom.getCurrentWindow().focus(); 87 | }); 88 | 89 | // tidy up when the window closes 90 | atom.getCurrentWindow().on('close', () => this.cleanup()); 91 | }, 92 | 93 | activateSubscriptions() { 94 | this.subscriptions.add(atom.commands.add('atom-workspace', { 95 | 'learn-ide:open': e => this.learnOpen(e.detail.path), 96 | 'learn-ide:toggle-terminal': () => this.termView.toggle(), 97 | 'learn-ide:toggle-popout': () => this.termView.focusPopoutEmulator(), 98 | 'learn-ide:toggle-focus': () => this.termView.toggleFocus(), 99 | 'learn-ide:focus': () => this.termView.focusEmulator(), 100 | 'learn-ide:toggle:debugger': () => this.term.toggleDebugger(), 101 | 'learn-ide:reset-connection': () => this.term.reset(), 102 | 'learn-ide:view-version': () => this.viewVersion(), 103 | 'learn-ide:update-check': () => updater.checkForUpdate(), 104 | 'learn-ide:about': () => this.about(), 105 | 'learn-ide:faq': () => this.faq(), 106 | 'learn-ide:report-issue': () => this.reportIssue() 107 | })); 108 | 109 | this.subscriptions.add(atom.commands.add('.terminal', { 110 | 'core:copy': () => this.termView.clipboardCopy(), 111 | 'core:paste': () => this.termView.clipboardPaste(), 112 | 'learn-ide:reset-font-size': () => this.termView.resetFontSize(), 113 | 'learn-ide:increase-font-size': () => this.termView.increaseFontSize(), 114 | 'learn-ide:decrease-font-size': () => this.termView.decreaseFontSize(), 115 | 'learn-ide:scroll-up': () => this.termView.scrollUp(), 116 | 'learn-ide:scroll-down': () => this.termView.scrollDown(), 117 | 'learn-ide:clear-terminal': () => this.term.send(' ') 118 | })); 119 | 120 | this.subscriptions.add( 121 | atom.config.onDidChange(`${name}.bleedingUpdates`, ({newValue}) => { 122 | var key = 'learn-ide:shouldRollback'; 123 | var didSubscribe = newValue; 124 | 125 | didSubscribe ? localStorage.delete(key) : localStorage.set(key, Date.now()) 126 | 127 | updater.checkForUpdate() 128 | }) 129 | ) 130 | 131 | this.subscriptions.add( 132 | atom.config.onDidChange(`${name}.notifier`, ({newValue}) => { 133 | newValue ? this.activateNotifier() : this.notifier.deactivate() 134 | }) 135 | ) 136 | 137 | this.subscriptions.add( 138 | atom.config.observe(`${name}.fontFamily`, (font) => { 139 | this.termView.setFontFamily(font) 140 | }) 141 | ) 142 | 143 | this.subscriptions.add( 144 | atom.config.observe(`${name}.fontSize`, (size) => { 145 | this.termView.setFontSize(size) 146 | }) 147 | ) 148 | 149 | this.subscriptions.add( 150 | atom.config.onDidChange(`${name}.terminalColors.basic`, () => colors.apply()) 151 | ) 152 | 153 | this.subscriptions.add( 154 | atom.config.onDidChange(`${name}.terminalColors.ansi`, () => colors.apply()) 155 | ) 156 | 157 | this.subscriptions.add( 158 | atom.config.onDidChange(`${name}.terminalColors.json`, ({newValue}) => { 159 | colors.parseJSON(newValue); 160 | }) 161 | ) 162 | 163 | var openPath = localStorage.get('learnOpenLabOnActivation'); 164 | if (openPath) { 165 | localStorage.delete('learnOpenLabOnActivation'); 166 | this.learnOpen(openPath); 167 | } 168 | }, 169 | 170 | activateNotifier() { 171 | if (atom.config.get(`${name}.notifier`)) { 172 | this.notifier = new Notifier(this.token.get()); 173 | this.notifier.activate(); 174 | } 175 | }, 176 | 177 | activateUpdater() { 178 | if (!this.isRestartAfterUpdate) { 179 | return updater.autoCheck(); 180 | } 181 | }, 182 | 183 | activateMonitor() { 184 | this.subscriptions.add(atom.onWillThrowError(err => { 185 | airbrake.notify(err.originalError); 186 | })) 187 | }, 188 | 189 | activateRemoteNotification() { 190 | remoteNotification(); 191 | }, 192 | 193 | deactivate() { 194 | localStorage.delete('disableTreeView'); 195 | localStorage.delete('terminalOut'); 196 | this.termView = null; 197 | this.statusView = null; 198 | this.subscriptions.dispose(); 199 | this.term.emitter.removeAllListeners(); 200 | }, 201 | 202 | subscribeToLogin() { 203 | this.subscriptions.add(atom.commands.add('atom-workspace', 204 | {'learn-ide:log-in-out': () => this.logInOrOut()}) 205 | ); 206 | }, 207 | 208 | cleanup() { 209 | atomHelper.cleanup(); 210 | }, 211 | 212 | waitForTerminalConnection() { 213 | return new Promise((resolve, reject) => { 214 | this.waitForAuth.then(() => { 215 | this.term.waitForSocket.then(resolve).catch(reject) 216 | }).catch(reject) 217 | }) 218 | }, 219 | 220 | consumeStatusBar(statusBar) { 221 | this.waitForAuth.then(() => this.addLearnToStatusBar(statusBar)); 222 | }, 223 | 224 | logInOrOut() { 225 | (this.token.get() == null) ? atomHelper.resetPackage() : logout() 226 | }, 227 | 228 | registerWindowsProtocol() { 229 | if (process.platform === 'win32') { require('./protocol') } 230 | }, 231 | 232 | disableFormerPackage() { 233 | var pkgName = 'integrated-learn-environment'; 234 | 235 | if (!atom.packages.isPackageDisabled(pkgName)) { 236 | atom.packages.disablePackage(pkgName); 237 | } 238 | }, 239 | 240 | addLearnToStatusBar(statusBar) { 241 | var leftTiles = Array.from(statusBar.getLeftTiles()); 242 | var rightTiles = Array.from(statusBar.getRightTiles()); 243 | var rightMostTile = rightTiles[rightTiles.length - 1]; 244 | 245 | var priority = ((rightMostTile != null ? rightMostTile.priority : undefined) || 0) - 1; 246 | statusBar.addRightTile({item: this.statusView, priority}); 247 | }, 248 | 249 | learnOpen(labSlug) { 250 | if (labSlug != null) { 251 | this.term.send(`learn open ${labSlug.toString()}\r`); 252 | } 253 | }, 254 | 255 | about() { 256 | shell.openExternal(ABOUT_URL); 257 | }, 258 | 259 | faq() { 260 | shell.openExternal(FAQ_URL); 261 | }, 262 | 263 | reportIssue() { 264 | shell.openExternal(REPORT_ISSUE_URL); 265 | }, 266 | 267 | viewVersion() { 268 | atom.notifications.addInfo(`Learn IDE: v${version}`); 269 | } 270 | }; 271 | -------------------------------------------------------------------------------- /lib/learn-open.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import localStorage from './local-storage' 4 | 5 | let openPath = localStorage.get('learnOpenLabOnActivation'); 6 | localStorage.delete('learnOpenLabOnActivation'); 7 | 8 | export default { 9 | getLabSlug () { 10 | return openPath || null 11 | } 12 | } -------------------------------------------------------------------------------- /lib/local-storage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | get(key) { 3 | return localStorage.getItem(key) 4 | }, 5 | 6 | set(key, value) { 7 | localStorage.setItem(key, value) 8 | }, 9 | 10 | delete(key) { 11 | localStorage.removeItem(key) 12 | }, 13 | 14 | remove(key) { 15 | var item = localStorage.getItem(key) 16 | localStorage.removeItem(key) 17 | return item 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /lib/logout.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import atomHelper from './atom-helper' 4 | import localStorage from './local-storage' 5 | import token from './token' 6 | import {BrowserWindow} from 'remote' 7 | import {learnCo} from './config' 8 | 9 | function logOutOfLearn() { 10 | var win = new BrowserWindow({show: false}); 11 | 12 | return new Promise((resolve) => { 13 | win.once('ready-to-show', resolve); 14 | win.loadURL(`${learnCo}/sign_out`); 15 | }); 16 | } 17 | 18 | function logOutOfGithub() { 19 | if (localStorage.remove('didCompleteGithubLogin') === null) { 20 | return Promise.resolve() 21 | } 22 | 23 | var win = new BrowserWindow({autoHideMenuBar: true, show: false}); 24 | 25 | return new Promise((resolve) => { 26 | win.once('ready-to-show', () => win.show()); 27 | 28 | win.webContents.on('will-navigate', () => win.hide()); 29 | 30 | win.webContents.on('did-navigate', (e, url) => { 31 | if (url.endsWith('github.com/')) { resolve() } 32 | }); 33 | 34 | win.loadURL('https://github.com/logout'); 35 | }); 36 | } 37 | 38 | export default function logout() { 39 | token.unset(); 40 | 41 | learn = logOutOfLearn() 42 | github = logOutOfGithub() 43 | 44 | return Promise.all([learn, github]).then(() => { 45 | atomHelper.emit('learn-ide:logout'); 46 | atomHelper.closePaneItems(); 47 | atom.restartApplication(); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /lib/notifications/submission.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import path from 'path' 4 | import token from '../token' 5 | import fetch from '../fetch' 6 | import {learnCo} from '../config' 7 | 8 | var submissionRegistry = []; 9 | var cachedLessonTitles = {}; 10 | 11 | function getLessonTitle(lessonID) { 12 | var title = cachedLessonTitles[lessonID]; 13 | 14 | if (title != null) { return Promise.resolve(title) } 15 | 16 | var lessonEndpoint = `${learnCo}/api/v1/lessons/${lessonID}`; 17 | var headers = new Headers({'Authorization': `Bearer ${token.get()}`}); 18 | 19 | return fetch(lessonEndpoint, {headers}).then(({title}) => { 20 | cachedLessonTitles[lessonID] = title || 'Learn IDE'; 21 | return title; 22 | }); 23 | }; 24 | 25 | function icon(passing) { 26 | var pass = path.resolve(__dirname, '..', '..', 'static', 'images', 'pass.png'); 27 | var fail = path.resolve(__dirname, '..', '..', 'static', 'images', 'fail.png'); 28 | 29 | return (passing === 'true') ? pass : fail 30 | }; 31 | 32 | export default function({submission_id, lesson_id, passing, message}) { 33 | if (submissionRegistry.includes(submission_id)) { return } 34 | 35 | submissionRegistry.push(submission_id); 36 | 37 | getLessonTitle(lesson_id).then((title) => { 38 | var notif = new Notification(title, {body: message, icon: icon(passing)}); 39 | notif.onclick = () => notif.close(); 40 | }); 41 | } 42 | 43 | -------------------------------------------------------------------------------- /lib/notifier.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import AtomSocket from 'atom-socket' 4 | import atomHelper from './atom-helper' 5 | import fetch from './fetch' 6 | import querystring from 'querystring' 7 | import {learnCo} from './config' 8 | 9 | import submission from './notifications/submission' 10 | 11 | var notificationStrategies = {submission} 12 | 13 | export default class Notifier { 14 | constructor(token) { 15 | this.token = token; 16 | } 17 | 18 | activate() { 19 | return this.authenticate().then(({id}) => { 20 | this.connect(id); 21 | }); 22 | } 23 | 24 | authenticate() { 25 | var headers = new Headers({'Authorization': `Bearer ${this.token}`}); 26 | return fetch(`${learnCo}/api/v1/users/me`, {headers}) 27 | } 28 | 29 | connect(userID) { 30 | this.ws = new AtomSocket('notif', `wss://push.flatironschool.com:9443/ws/fis-user-${userID}`) 31 | 32 | this.ws.on('message', msg => this.parseMessage(JSON.parse(msg))) 33 | } 34 | 35 | parseMessage({text}) { 36 | if (atomHelper.isLastFocusedWindow()) { 37 | var data = querystring.parse(text); 38 | 39 | var strategy = notificationStrategies[data.type]; 40 | var strategyIsDefined = typeof strategy === 'function'; 41 | 42 | return strategyIsDefined ? strategy(data) : undefined 43 | } 44 | } 45 | 46 | deactivate() { 47 | this.ws.close(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/popout-emulator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/popout-emulator.js: -------------------------------------------------------------------------------- 1 | var bus = require('./event-bus') 2 | var localStore = require('./local-storage') 3 | var TerminalEmulator = require('xterm') 4 | var {clipboard} = require('electron') 5 | 6 | TerminalEmulator.loadAddon('fit') 7 | TerminalEmulator.loadAddon('fullscreen') 8 | 9 | class PopoutEmulator { 10 | constructor() { 11 | this.style = document.createElement('style') 12 | this.emulator = new TerminalEmulator({cursorBlink: true}) 13 | 14 | this.attach() 15 | this.subscribe() 16 | this.clearScreen() 17 | } 18 | 19 | attach() { 20 | let css = localStore.remove('popout-emulator:css') + 21 | localStore.remove('popout-emulator:xterm-css') + 22 | localStore.remove('popout-emulator:fullscreen-css'); 23 | 24 | this.style.innerHTML = css 25 | document.head.appendChild(this.style) 26 | 27 | document.body.style.height = `${window.innerHeight}px` 28 | document.body.style.fontSize = `${localStore.get('popout-emulator:font-size')}px` 29 | this.emulator.open(document.body, true) 30 | 31 | this.emulator.toggleFullscreen(true) 32 | this.setFontFamily(localStore.get('popout-emulator:font-family')) 33 | } 34 | 35 | get container() { 36 | return this.emulator.parent 37 | } 38 | 39 | subscribe() { 40 | this.emulator.attachCustomKeydownHandler((e) => { 41 | var callbackName = this.callbackNameForKeyEvent(e); 42 | 43 | if (callbackName != null) { 44 | e.preventDefault() 45 | this[callbackName](e) 46 | return false 47 | } 48 | }) 49 | 50 | this.emulator.on('data', (data) => { 51 | this.sendToTerminal(data) 52 | }) 53 | 54 | bus.on('popout-emulator:write', (text) => { 55 | this.emulator.write(text) 56 | }) 57 | 58 | window.onresize = () => { 59 | this.container.style.height = `${window.innerHeight}px` 60 | this.emulator.fit() 61 | } 62 | } 63 | 64 | clearScreen() { 65 | this.sendToTerminal(' ') 66 | } 67 | 68 | sendToTerminal(data) { 69 | bus.emit('popout-emulator:data', data) 70 | } 71 | 72 | callbackNameForKeyEvent({keyCode, metaKey, shiftKey, ctrlKey}) { 73 | var keyCodeCallbacks = { 74 | // Mac only 75 | cmd: { 76 | 187: 'increaseFontSize', // cmd-= 77 | 189: 'decreaseFontSize', // cmd-- 78 | 48: 'resetFontSize', // cmd-0 79 | 38: 'scrollUp', // cmd-up 80 | 40: 'scrollDown', // cmd-down 81 | 67: 'clipboardCopy', // cmd-c 82 | 86: 'clipboardPaste' // cmd-v 83 | }, 84 | // Windows & Linux only 85 | ctrl: { 86 | 187: 'increaseFontSize', // ctrl-= 87 | 189: 'decreaseFontSize', // ctrl-- 88 | 48: 'resetFontSize', // ctrl-0 89 | 38: 'scrollUp', // ctrl-up 90 | 40: 'scrollDown' // ctrl-down 91 | }, 92 | ctrlShift: { 93 | 67: 'clipboardCopy', // ctrl-C 94 | 86: 'clipboardPaste' // ctrl-V 95 | } 96 | } 97 | 98 | var isMac = process.platform === 'darwin'; 99 | 100 | var callbackGroup; 101 | if (isMac && metaKey) { 102 | callbackGroup = keyCodeCallbacks.cmd 103 | } else if (!isMac && ctrlKey) { 104 | callbackGroup = shiftKey ? keyCodeCallbacks.ctrlShift : keyCodeCallbacks.ctrl 105 | } 106 | 107 | if (!callbackGroup) { return } 108 | 109 | return callbackGroup[keyCode] 110 | } 111 | 112 | scrollUp() { 113 | this.emulator.scrollDisp(-1); 114 | } 115 | 116 | scrollDown() { 117 | this.emulator.scrollDisp(1); 118 | } 119 | 120 | clipboardCopy() { 121 | var selection = document.getSelection(); 122 | var rawText = selection.toString(); 123 | var preparedText = rawText.replace(/\u00A0/g, ' ').replace(/\s+(\n)?$/gm, '$1'); 124 | 125 | clipboard.writeText(preparedText); 126 | } 127 | 128 | clipboardPaste() { 129 | var rawText = clipboard.readText(); 130 | var preparedText = rawText.replace(/\n/g, '\r'); 131 | 132 | this.sendToTerminal(preparedText) 133 | } 134 | 135 | fontSize() { 136 | var style = window.getComputedStyle(this.container), 137 | size = parseInt(style.fontSize); 138 | 139 | if (this.initialFontSize == null) { this.initialFontSize = size } 140 | 141 | return size 142 | } 143 | 144 | increaseFontSize() { 145 | this.setFontSize(this.fontSize() + 2) 146 | } 147 | 148 | decreaseFontSize() { 149 | var next = this.fontSize() - 2; 150 | if (next < 2) { return } 151 | this.setFontSize(next) 152 | } 153 | 154 | resetFontSize() { 155 | if (this.initialFontSize == null) { return } 156 | this.setFontSize(this.initialFontSize) 157 | } 158 | 159 | setFontSize(sizeInt) { 160 | this.container.style.fontSize = `${sizeInt}px` 161 | this.fit() 162 | } 163 | 164 | setFontFamily(fontFamily) { 165 | if (fontFamily && fontFamily.length) { 166 | this.emulator.element.style.fontFamily = fontFamily 167 | this.fit() 168 | } 169 | } 170 | 171 | fit() { 172 | // Two calls are necessary to properly fit 173 | this.emulator.fit() 174 | this.emulator.fit() 175 | } 176 | } 177 | 178 | var popout = new PopoutEmulator() 179 | 180 | -------------------------------------------------------------------------------- /lib/post.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | function httpRequest(method, path, body, headers={}) { 4 | return new Promise((resolve, reject) => { 5 | var xmlhttp = new XMLHttpRequest(); 6 | 7 | xmlhttp.open(method, path); 8 | xmlhttp.responseType = 'json'; 9 | 10 | xmlhttp.setRequestHeader('Accept', 'application/json'); 11 | xmlhttp.setRequestHeader('Content-Type', 'application/json'); 12 | 13 | Object.keys(headers).forEach((key) => { 14 | xmlhttp.setRequestHeader(key, headers[key]); 15 | }); 16 | 17 | xmlhttp.addEventListener('load', (e) => resolve(xmlhttp.response, e)) 18 | xmlhttp.addEventListener('error', (e) => reject(xmlhttp.response, e)) 19 | 20 | xmlhttp.send(JSON.stringify(body)); 21 | }); 22 | } 23 | 24 | export default function post(url, body, headers) { 25 | return httpRequest('POST', url, body, headers) 26 | } 27 | 28 | -------------------------------------------------------------------------------- /lib/protocol.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import protocol from 'register-protocol-win32' 4 | 5 | protocol.install('learn-ide', `${process.execPath} --url-to-open=\"%1\"`) 6 | -------------------------------------------------------------------------------- /lib/remote-notification.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import localStorage from './local-storage' 4 | import fetch from './fetch' 5 | import {learnCo} from './config' 6 | import {shell} from 'electron' 7 | 8 | const key = 'learn-ide:remote-notification-id' 9 | 10 | function checkForRemoteNotification() { 11 | var url = `${learnCo}/api/v1/learn_ide/notification` 12 | fetch(url).then(handleRemoteNotification) 13 | } 14 | 15 | function handleRemoteNotification(remoteNotification) { 16 | var recentId = parseInt(localStorage.get(key)); 17 | if (recentId === remoteNotification.id) { return } 18 | 19 | if (!remoteNotification.active) { return } 20 | 21 | var expiration = Date.parse(remoteNotification.expires) 22 | if (Date.now() > expiration) { return } 23 | 24 | localStorage.set(key, remoteNotification.id) 25 | createNotification(remoteNotification.notification) 26 | } 27 | 28 | function createNotification(notification) { 29 | var {description, detail, dismissable, icon, buttons} = notification.options; 30 | 31 | buttons = buttons.map((button) => { 32 | if (button.onDidClick != null) { 33 | var url = button.onDidClick; 34 | button.onDidClick = () => shell.openExternal(url) 35 | } 36 | return button 37 | }) 38 | 39 | atom.notifications.addInfo(notification.message, { 40 | description, 41 | detail, 42 | dismissable, 43 | icon, 44 | buttons 45 | }); 46 | } 47 | 48 | export default function remoteNotification() { 49 | checkForRemoteNotification(); 50 | 51 | var oneMinute = 60000; 52 | setInterval(checkForRemoteNotification, oneMinute * 20); 53 | } 54 | -------------------------------------------------------------------------------- /lib/terminal-view.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import TerminalEmulator from 'xterm'; 4 | import path from 'path'; 5 | import { $, View } from 'atom-space-pen-views'; 6 | import { BrowserWindow } from 'remote'; 7 | import { clipboard } from 'electron'; 8 | 9 | import bus from './event-bus'; 10 | import colors from './colors'; 11 | import localStorage from './local-storage'; 12 | import { name } from '../package.json'; 13 | import fs from 'fs' 14 | import { app } from 'remote' 15 | 16 | TerminalEmulator.loadAddon('fit'); 17 | 18 | const popoutEmulatorFile = path.resolve(__dirname, 'popout-emulator.html'); 19 | 20 | const heightKey = 'learn-ide:currentTerminalHeight' 21 | 22 | const verticalLimit = 100; 23 | const defaultHeight = 275; 24 | 25 | class TerminalView extends View { 26 | static content() { 27 | return this.div({class: 'terminal-resizer tool-panel'}, () => { 28 | this.div({class: 'terminal-resize-handle', outlet: 'resizeHandle'}); 29 | }); 30 | } 31 | 32 | initialize(terminal) { 33 | this.terminal = terminal; 34 | this.emulator = new TerminalEmulator({cursorBlink: true, rows: 16}); 35 | 36 | this.attach(); 37 | this.subscribe(); 38 | this.resizeAfterDrag = this.resizeAfterDrag.bind(this) 39 | } 40 | 41 | attach() { 42 | atom.workspace.addBottomPanel({item: this}); 43 | this.emulator.open(this.element, true); 44 | this.restoreHeight() 45 | } 46 | 47 | subscribe() { 48 | this.emulator.attachCustomKeydownHandler((e) => { 49 | if (this.isAttemptToScroll(e) || this.isAttemptToSave(e)) { 50 | e.preventDefault() 51 | return false 52 | } 53 | }) 54 | 55 | this.emulator.on('data', (data) => { 56 | this.sendToTerminal(data, event); 57 | }); 58 | 59 | this.terminal.on('message', (msg) => { 60 | this.writeToEmulator(msg); 61 | }); 62 | 63 | bus.on('popout-emulator:data', (data) => { 64 | this.sendToTerminal(data); 65 | }); 66 | 67 | this.on('mousedown', '.terminal-resize-handle', (e) => { 68 | this.resizeByDragStarted(e); 69 | }); 70 | 71 | this.on('mouseup', '.terminal-resize-handle', (e) => { 72 | this.resizeByDragStopped(e); 73 | }); 74 | } 75 | 76 | sendToTerminal(data) { 77 | this.terminal.send(data) 78 | } 79 | 80 | writeToEmulator(text) { 81 | this.emulator.write(text); 82 | 83 | if (this.hasPopoutEmulator()) { 84 | bus.emit('popout-emulator:write', text); 85 | } 86 | } 87 | 88 | isAttemptToSave({keyCode, ctrlKey}) { 89 | // ctrl-s on windows and linux 90 | return ctrlKey && (keyCode === 83) && (process.platform !== 'darwin') 91 | } 92 | 93 | isAttemptToScroll({keyCode, ctrlKey, metaKey, altKey}) { 94 | var isUpOrDown = [38, 40].includes(keyCode); 95 | 96 | if (!isUpOrDown) { return false } 97 | 98 | // ctrl-up/down on windows and linux 99 | if (process.platform !== 'darwin') { return ctrlKey } 100 | 101 | // cmd-up/down or ctrl-alt-up/down on mac 102 | return metaKey || (ctrlKey && altKey) 103 | } 104 | 105 | loadPopoutEmulator() { 106 | return new Promise((resolve) => { 107 | localStorage.set('popout-emulator:css', colors.getCSS()); 108 | localStorage.set('popout-emulator:font-size', this.currentFontSize()) 109 | localStorage.set('popout-emulator:font-family', $(this.emulator.element).css('font-family')) 110 | 111 | let xterm = path.join(app.getAppPath(), 'node_modules', 'xterm', 'dist', 'xterm.css') 112 | let fullscreen = path.join(app.getAppPath(), 'node_modules', 'xterm', 'dist', 'addons', 'fullscreen', 'fullscreen.css') 113 | localStorage.set('popout-emulator:xterm-css', fs.readFileSync(xterm)) 114 | localStorage.set('popout-emulator:fullscreen-css', fs.readFileSync(fullscreen)) 115 | 116 | this.popout = new BrowserWindow({title: 'Learn IDE Terminal', show: false, autoHideMenuBar: true}); 117 | this.popout.loadURL(`file://${popoutEmulatorFile}`); 118 | 119 | this.popout.once('ready-to-show', () => resolve(this.popout)); 120 | this.popout.on('closed', () => this.show()); 121 | }); 122 | } 123 | 124 | hasPopoutEmulator() { 125 | return (this.popout != null) && !this.popout.isDestroyed(); 126 | } 127 | 128 | focusPopoutEmulator() { 129 | if (this.hasPopoutEmulator()) { 130 | this.hide(); 131 | this.popout.focus(); 132 | return; 133 | } 134 | 135 | this.loadPopoutEmulator().then(() => { 136 | this.hide(); 137 | this.popout.show(); 138 | }); 139 | } 140 | 141 | clipboardCopy() { 142 | var selection = document.getSelection(); 143 | var rawText = selection.toString(); 144 | var preparedText = rawText.replace(/\u00A0/g, ' ').replace(/\s+(\n)?$/gm, '$1'); 145 | 146 | clipboard.writeText(preparedText); 147 | } 148 | 149 | clipboardPaste() { 150 | var rawText = clipboard.readText(); 151 | var preparedText = rawText.replace(/\n/g, '\r'); 152 | 153 | this.sendToTerminal(preparedText); 154 | } 155 | 156 | toggleFocus() { 157 | var hasFocus = document.activeElement === this.emulator.textarea; 158 | 159 | hasFocus ? this.transferFocus() : this.focusEmulator() 160 | } 161 | 162 | transferFocus() { 163 | atom.workspace.getActivePane().activate(); 164 | } 165 | 166 | focusEmulator() { 167 | this.emulator.focus(); 168 | } 169 | 170 | scrollUp() { 171 | this.emulator.scrollDisp(-1); 172 | } 173 | 174 | scrollDown() { 175 | this.emulator.scrollDisp(1); 176 | } 177 | 178 | setFontFamily(fontFamily) { 179 | var value = 'courier-new, courier, monospace'; 180 | 181 | if (fontFamily.length) { value = `${fontFamily}, ${value}` } 182 | 183 | $(this.emulator.element).css('font-family', value) 184 | 185 | this.fit() 186 | } 187 | 188 | setFontSize(size) { 189 | this.css('font-size', size); 190 | this.fit() 191 | } 192 | 193 | currentFontSize() { 194 | return atom.config.get(`${name}.fontSize`); 195 | } 196 | 197 | increaseFontSize() { 198 | atom.config.set(`${name}.fontSize`, this.currentFontSize() + 2); 199 | } 200 | 201 | decreaseFontSize() { 202 | atom.config.set(`${name}.fontSize`, this.currentFontSize() - 2); 203 | } 204 | 205 | resetFontSize() { 206 | atom.config.unset(`${name}.fontSize`) 207 | } 208 | 209 | restoreHeight() { 210 | var height = localStorage.get(heightKey) || defaultHeight; 211 | this.setHeightWithFit(height) 212 | } 213 | 214 | setHeightWithFit(desiredHeight) { 215 | this.height(desiredHeight) 216 | this.fit() 217 | } 218 | 219 | resizeByDragStarted({target}) { 220 | $(document).on('mousemove', this.resizeAfterDrag) 221 | } 222 | 223 | resizeByDragStopped() { 224 | $(document).off('mousemove', this.resizeAfterDrag) 225 | } 226 | 227 | resizeAfterDrag({pageY, which}) { 228 | if (which !== 1) { 229 | this.resizeByDragStopped() 230 | return 231 | } 232 | 233 | var availableHeight = this.height() + this.offset().top; 234 | var proposedHeight = availableHeight - pageY; 235 | 236 | var tooLarge = pageY < verticalLimit; 237 | var tooSmall = proposedHeight < verticalLimit; 238 | 239 | if (tooLarge) { proposedHeight = availableHeight - verticalLimit } 240 | if (tooSmall) { proposedHeight = verticalLimit } 241 | 242 | this.setHeightWithFit(proposedHeight) 243 | } 244 | 245 | fit() { 246 | // Two calls are necessary to properly fit after the font-size has changed 247 | this.emulator.fit() 248 | this.emulator.fit() 249 | 250 | var newHeight = this.getHeightForFit(); 251 | 252 | this.height(newHeight) 253 | localStorage.set(heightKey, newHeight) 254 | } 255 | 256 | getHeightForFit() { 257 | var rowCount = this.emulator.rows; 258 | var rowHeight = this.emulator.viewport.currentRowHeight; 259 | var emulatorHeight = rowCount * rowHeight; 260 | 261 | var paddingTop = parseInt(this.css('padding-top')); 262 | var paddingBottom = parseInt(this.css('padding-bottom')); 263 | var paddingHeight = paddingTop + paddingBottom; 264 | 265 | return emulatorHeight + paddingHeight 266 | } 267 | } 268 | 269 | export default TerminalView 270 | 271 | -------------------------------------------------------------------------------- /lib/terminal.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import AtomSocket from 'atom-socket' 4 | import {EventEmitter} from 'events' 5 | 6 | export default class Terminal { 7 | constructor(args={}) { 8 | this.emitter = new EventEmitter(); 9 | 10 | this.host = args.host; 11 | this.port = args.port; 12 | this.path = args.path; 13 | this.token = args.token; 14 | 15 | this.hasFailed = false; 16 | 17 | this.connect(); 18 | } 19 | 20 | connect(token) { 21 | this.socket = new AtomSocket('environment', this.url()); 22 | 23 | this.waitForSocket = new Promise(((resolve, reject) => { 24 | this.socket.on('open', e => { 25 | this.emit('open', e) 26 | resolve() 27 | }) 28 | 29 | this.socket.on('open:cached', e => { 30 | this.emit('open', e) 31 | resolve() 32 | }) 33 | 34 | this.socket.on('message', (msg) => { 35 | if (msg.slice(2,10) !== 'terminal') { return } 36 | 37 | try { 38 | var {terminal} = JSON.parse(msg) 39 | } catch ({message}) { 40 | console.error(`terminal parse error: ${message}`) 41 | return 42 | } 43 | 44 | var decoded = new Buffer(terminal, 'base64').toString() 45 | this.emit('message', decoded) 46 | }) 47 | 48 | this.socket.on('close', e => this.emit('close', e)) 49 | 50 | this.socket.on('error', e => this.emit('error', e)) 51 | })); 52 | 53 | return this.waitForSocket 54 | } 55 | 56 | emit() { 57 | return this.emitter.emit.apply(this.emitter, arguments); 58 | } 59 | 60 | on() { 61 | return this.emitter.on.apply(this.emitter, arguments); 62 | } 63 | 64 | url() { 65 | var {version} = require('../package.json'); 66 | var protocol = (this.port === 443) ? 'wss' : 'ws'; 67 | 68 | return `${protocol}://${this.host}:${this.port}/${this.path}?token=${this.token}&version=${version}`; 69 | } 70 | 71 | reset() { 72 | return this.socket.reset(); 73 | } 74 | 75 | send(msg) { 76 | var encoded = new Buffer(msg).toString('base64') 77 | var preparedMessage = JSON.stringify({terminal: encoded}) 78 | if (this.waitForSocket != null ) { 79 | this.socket.send(preparedMessage) 80 | return 81 | } 82 | 83 | this.waitForSocket.then(() => { 84 | this.waitForSocket = null; 85 | this.socket.send(preparedMessage); 86 | }); 87 | } 88 | 89 | toggleDebugger() { 90 | this.socket.toggleDebugger(); 91 | } 92 | 93 | debugInfo() { 94 | return { 95 | host: this.host, 96 | port: this.port, 97 | path: this.path, 98 | token: this.token, 99 | socket: this.socket 100 | }; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/token.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import localStorage from './local-storage' 4 | import bus from './event-bus' 5 | 6 | var tokenKey = 'learn-ide:token'; 7 | 8 | export default { 9 | get() { 10 | return localStorage.get(tokenKey); 11 | }, 12 | 13 | set(value) { 14 | localStorage.set(tokenKey, value); 15 | bus.emit(tokenKey, value); 16 | }, 17 | 18 | unset() { 19 | localStorage.delete(tokenKey); 20 | bus.emit(tokenKey, undefined); 21 | }, 22 | 23 | observe(callback) { 24 | callback(this.get()); 25 | bus.on(tokenKey, callback); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/updater.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import fetch from './fetch' 4 | import fs from 'fs' 5 | import localStorage from './local-storage' 6 | import path from 'path' 7 | import semver from 'semver' 8 | import {install} from './apm' 9 | import {learnCo} from './config' 10 | import {name} from '../package.json' 11 | 12 | var helpCenterUrl = `${learnCo}/ide/faq`; 13 | 14 | function latestVersionUrl() { 15 | var bleedingUpdates = atom.config.get(`${name}.bleedingUpdates`); 16 | return bleedingUpdates ? `${learnCo}/api/v1/learn_ide/bleeding_version` : `${learnCo}/api/v1/learn_ide/stable_version` 17 | } 18 | 19 | export default { 20 | autoCheck() { 21 | if (this._shouldSkipCheck()) { return } 22 | 23 | this._fetchLatestVersionData().then(({version, detail}) => { 24 | this._setCheckDate(); 25 | 26 | if (this._shouldUpdate(version)) { this._addUpdateNotification(detail) } 27 | }); 28 | }, 29 | 30 | checkForUpdate() { 31 | this._fetchLatestVersionData().then(({version, detail}) => { 32 | this._setCheckDate(); 33 | 34 | if (this._shouldUpdate(version)) { 35 | this._addUpdateNotification(detail); 36 | } else { 37 | this._addUpToDateNotification(); 38 | } 39 | }); 40 | }, 41 | 42 | update() { 43 | localStorage.set('restartingForUpdate', true); 44 | 45 | if (this.updateNotification != null) { 46 | this.updateNotification.dismiss(); 47 | } 48 | 49 | var waitNotification = 50 | atom.notifications.addInfo('Please wait while the update is installed...', { 51 | description: 'This may take a few minutes. Please **do not** close the editor.', 52 | dismissable: true 53 | }); 54 | 55 | this._updatePackage().then(pkgResult => { 56 | this._installDependencies().then(depResult => { 57 | var log = '', 58 | code = 0; 59 | 60 | if (pkgResult != null) { 61 | log += `Learn IDE:\n---\n${pkgResult.log}`; 62 | code += pkgResult.code; 63 | } 64 | 65 | if (depResult != null) { 66 | log += `\nDependencies:\n---\n${depResult.log}`; 67 | code += depResult.code; 68 | } 69 | 70 | if (code !== 0) { 71 | waitNotification.dismiss(); 72 | localStorage.delete('restartingForUpdate'); 73 | this._updateFailed(log); 74 | return; 75 | } 76 | 77 | localStorage.set('updateLog', log); 78 | atom.restartApplication(); 79 | }); 80 | }); 81 | }, 82 | 83 | didRestartAfterUpdate() { 84 | var log = localStorage.remove('updateLog'); 85 | var target = localStorage.remove('targetedUpdateVersion'); 86 | 87 | this._shouldUpdate(target) ? this._updateFailed(log) : this._updateSucceeded() 88 | }, 89 | 90 | _fetchLatestVersionData() { 91 | return fetch(latestVersionUrl()).then(latestVersionData => { 92 | this.latestVersionData = latestVersionData; 93 | return this.latestVersionData; 94 | }); 95 | }, 96 | 97 | _getLatestVersion() { 98 | if ((this.latestVersionData != null) && (this.latestVersionData.version != null)) { 99 | return Promise.resolve(this.latestVersionData.version); 100 | } 101 | 102 | return this._fetchLatestVersionData().then(({version}) => version); 103 | }, 104 | 105 | _setCheckDate() { 106 | localStorage.set('updateCheckDate', Date.now()); 107 | }, 108 | 109 | _shouldUpdate(latestVersion) { 110 | return this._shouldUpdatePackage(latestVersion) || this._shouldUpdateDependencies(); 111 | }, 112 | 113 | _shouldUpdatePackage(latestVersion) { 114 | var {version} = require('../package.json'); 115 | 116 | if (this._shouldRollback()) { 117 | return !semver.eq(latestVersion, version) 118 | } 119 | 120 | return semver.gt(latestVersion, version) 121 | }, 122 | 123 | _shouldRollback() { 124 | var rollback = parseInt(localStorage.get('learn-ide:shouldRollback')); 125 | 126 | if (!rollback) { return false } 127 | 128 | var twelveHours = 12 * 60 * 60; 129 | var rollbackExpires = rollback + twelveHours; 130 | 131 | return rollbackExpires > Date.now() 132 | }, 133 | 134 | _shouldUpdateDependencies() { 135 | var {packageDependencies} = require('../package.json'); 136 | 137 | return Object.keys(packageDependencies).some(pkg => { 138 | var version = packageDependencies[pkg]; 139 | return this._shouldInstallDependency(pkg, version) 140 | }); 141 | }, 142 | 143 | _shouldInstallDependency(pkgName, latestVersion) { 144 | var pkg = atom.packages.loadPackage(pkgName); 145 | var currentVersion = (pkg === null) ? undefined : pkg.metadata.version; 146 | 147 | return !semver.satisfies(currentVersion, latestVersion); 148 | }, 149 | 150 | _shouldSkipCheck() { 151 | var twelveHours = 12 * 60 * 60; 152 | return this._lastCheckedAgo() < twelveHours; 153 | }, 154 | 155 | _lastCheckedAgo() { 156 | var checked = parseInt(localStorage.get('updateCheckDate')); 157 | return Date.now() - checked; 158 | }, 159 | 160 | _addUpdateNotification(detail) { 161 | this.updateNotification = 162 | atom.notifications.addInfo('Learn IDE: update available!', { 163 | detail, 164 | description: 'Just click below to get the sweet, sweet newness.', 165 | dismissable: true, 166 | buttons: [{ 167 | text: 'Install update & restart editor', 168 | onDidClick: () => this.update() 169 | }] 170 | }); 171 | }, 172 | 173 | _addUpToDateNotification() { 174 | atom.notifications.addSuccess('Learn IDE: up-to-date!'); 175 | }, 176 | 177 | _updatePackage() { 178 | return this._getLatestVersion().then(version => { 179 | localStorage.set('targetedUpdateVersion', version); 180 | if (!this._shouldUpdatePackage(version)) { return } 181 | return install(name, version); 182 | }); 183 | }, 184 | 185 | _installDependencies() { 186 | return this._getDependenciesToInstall().then(dependencies => { 187 | if (dependencies == null) { return } 188 | if (Object.keys(dependencies).length <= 0) { return } 189 | 190 | return install(dependencies); 191 | }); 192 | }, 193 | 194 | _getDependenciesToInstall() { 195 | return this._getUpdatedDependencies().then(dependencies => { 196 | var packagesToUpdate = {}; 197 | 198 | Object.keys(dependencies).forEach(pkg => { 199 | var version = dependencies[pkg]; 200 | if (this._shouldInstallDependency(pkg, version)) { 201 | packagesToUpdate[pkg] = version; 202 | } 203 | }) 204 | 205 | return packagesToUpdate; 206 | }); 207 | }, 208 | 209 | _getUpdatedDependencies() { 210 | return this._getDependenciesFromPackagesDir().catch(() => { 211 | return this._getDependenciesFromCurrentPackage(); 212 | }); 213 | }, 214 | 215 | _getDependenciesFromPackagesDir() { 216 | var pkg = path.join(atom.getConfigDirPath(), 'packages', name, 'package.json'); 217 | return this._getDependenciesFromPath(pkg); 218 | }, 219 | 220 | _getDependenciesFromCurrentPackage() { 221 | var pkgJSON = path.resolve(__dirname, '..', 'package.json'); 222 | return this._getDependenciesFromPath(pkgJSON); 223 | }, 224 | 225 | _getDependenciesFromPath(pkgJSON) { 226 | return new Promise((resolve, reject) => { 227 | fs.readFile(pkgJSON, 'utf-8', (err, data) => { 228 | if (err != null) { 229 | reject(err) 230 | return 231 | } 232 | 233 | try { 234 | var pkg = JSON.parse(data); 235 | } catch (e) { 236 | console.error(`Unable to parse ${pkgJSON}:`, e); 237 | reject(e) 238 | return 239 | } 240 | 241 | var dependenciesObj = pkg.packageDependencies; 242 | resolve(dependenciesObj) 243 | }); 244 | }); 245 | }, 246 | 247 | _updateFailed(detail) { 248 | var {shell, clipboard} = require('electron'); 249 | 250 | var description = 'The installation seems to have been interrupted.'; 251 | var buttons = [ 252 | { 253 | text: 'Retry', 254 | onDidClick: () => this.update() 255 | }, 256 | { 257 | text: 'Visit help center', 258 | onDidClick() { shell.openExternal(helpCenterUrl) } 259 | } 260 | ]; 261 | 262 | if (detail != null) { 263 | description = 'Please include this information when contacting the Learn support team about the issue.'; 264 | buttons.push({ 265 | text: 'Copy this log', 266 | onDidClick() { clipboard.writeText(detail) } 267 | }); 268 | } 269 | 270 | this.updateNotification = 271 | atom.notifications.addWarning('Learn IDE: update failed!', {detail, description, buttons, dismissable: true}); 272 | }, 273 | 274 | _updateSucceeded() { 275 | atom.notifications.addSuccess('Learn IDE: update successful!'); 276 | } 277 | }; 278 | 279 | -------------------------------------------------------------------------------- /lib/url-handler.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import url from 'url' 4 | import {ipcRenderer} from 'electron' 5 | import localStorage from './local-storage' 6 | import bus from './event-bus' 7 | 8 | function getLabSlug() { 9 | var {urlToOpen} = JSON.parse(decodeURIComponent(location.hash.substr(1))); 10 | return url.parse(urlToOpen).pathname.substring(1); 11 | }; 12 | 13 | function openInNewWindow() { 14 | localStorage.set('learnOpenLabOnActivation', getLabSlug()); 15 | ipcRenderer.send('command', 'application:new-window'); 16 | }; 17 | 18 | function openInExistingWindow() { 19 | bus.emit('learn:open', {timestamp: Date.now(), slug: getLabSlug()}) 20 | } 21 | 22 | function windowOpen() { 23 | return localStorage.get('lastFocusedWindow') 24 | } 25 | 26 | function onWindows() { 27 | return process.platform === 'win32' 28 | } 29 | 30 | export default function() { 31 | if (!windowOpen() || onWindows()) { 32 | openInNewWindow(); 33 | } else { 34 | openInExistingWindow(); 35 | } 36 | 37 | return Promise.resolve(); 38 | }; 39 | -------------------------------------------------------------------------------- /lib/username.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import localStorage from './local-storage' 4 | 5 | var tokenKey = 'learn-ide:username'; 6 | 7 | export default { 8 | get() { 9 | return localStorage.get(tokenKey); 10 | }, 11 | 12 | set(value) { 13 | localStorage.set(tokenKey, value); 14 | }, 15 | 16 | unset() { 17 | localStorage.delete(tokenKey); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/views/status.coffee: -------------------------------------------------------------------------------- 1 | {View} = require 'atom-space-pen-views' 2 | {ipcRenderer} = require 'electron' 3 | {EventEmitter} = require 'events' 4 | 5 | localStorage = require '../local-storage' 6 | 7 | module.exports = 8 | class StatusView extends View 9 | @content: -> 10 | @div class: 'learn-synced-fs-status inline-block inline-block-tight', => 11 | @div class: 'learn-status-icon inline-block inline-block-tight icon icon-terminal', id: 'learn-status-icon', ' Learn' 12 | @div class: 'learn-popout-terminal-icon inline-block inline-block-tight icon icon-link-external', id: 'learn-popout-terminal-icon' 13 | 14 | constructor: (state, termSocket) -> 15 | super 16 | @socket = termSocket 17 | @activateEventHandlers() 18 | @activatePopoutIcon() 19 | 20 | on: -> 21 | @emitter || (@emitter = new EventEmitter) 22 | @emitter.on.apply(@emitter, arguments) 23 | 24 | activateEventHandlers: -> 25 | @socket.on 'open', => 26 | icon = @statusIcon() 27 | icon.textContent = ' Learn' 28 | icon.dataset.status = 'good' 29 | 30 | @socket.on 'close', => 31 | @displayDisconnected() 32 | 33 | @socket.on 'error', => 34 | @displayDisconnected() 35 | 36 | @statusIcon().addEventListener 'click', (e) => 37 | # TODO: have this based on the socket state itself instead of the view state 38 | if e.target.dataset.status is 'bad' 39 | view = atom.views.getView(atom.workspace) 40 | atom.commands.dispatch view, 'learn-ide:reset-connection' 41 | 42 | displayDisconnected: -> 43 | icon = @statusIcon() 44 | icon.textContent = ' Learn ...reconnect?' 45 | icon.dataset.status = 'bad' 46 | 47 | activatePopoutIcon: -> 48 | @popoutIcon().addEventListener 'click', => 49 | @popoutTerminal() 50 | 51 | popoutTerminal: -> 52 | view = atom.views.getView(atom.workspace) 53 | atom.commands.dispatch(view, 'learn-ide:toggle-popout') 54 | 55 | onTerminalPopIn: -> 56 | @showPopoutIcon() 57 | 58 | # ui elements 59 | 60 | statusIcon: -> 61 | @element.getElementsByClassName('learn-status-icon')[0] 62 | 63 | popoutIcon: -> 64 | @element.getElementsByClassName('learn-popout-terminal-icon')[0] 65 | 66 | showPopoutIcon: -> 67 | @popoutIcon().classList.remove('inactive') 68 | @popoutIcon().classList.add('active') 69 | 70 | hidePopoutIcon: -> 71 | @popoutIcon().classList.remove('active') 72 | @popoutIcon().classList.add('inactive') 73 | -------------------------------------------------------------------------------- /menus/learn-ide.cson: -------------------------------------------------------------------------------- 1 | 'context-menu': 2 | 'atom-text-editor': [ 3 | { 4 | 'label': 'Toggle Learn IDE Terminal' 5 | 'command': 'learn-ide:toggle-terminal' 6 | } 7 | ] 8 | '.terminal': [ 9 | { 10 | 'label': 'Copy' 11 | 'command': 'core:copy' 12 | } 13 | { 14 | 'label': 'Paste' 15 | 'command': 'core:paste' 16 | } 17 | { 18 | 'label': 'Clear terminal' 19 | 'command': 'learn-ide:clear-terminal' 20 | } 21 | { 22 | 'label': 'Hide Learn IDE Terminal' 23 | 'command': 'learn-ide:toggle-terminal' 24 | } 25 | ] 26 | 'menu': [ 27 | { 28 | 'label': 'Packages' 29 | 'submenu': [ 30 | 'label': 'Learn IDE' 31 | 'submenu': [ 32 | { 33 | 'label': 'Reconnect' 34 | 'command': 'learn-ide:reset-connection' 35 | } 36 | { 37 | 'label': 'Log In/Out' 38 | 'command': 'learn-ide:log-in-out' 39 | } 40 | { 41 | 'label': 'Focus Terminal' 42 | 'command': 'learn-ide:toggle-focus' 43 | } 44 | { 45 | 'label': 'Toggle Terminal' 46 | 'command': 'learn-ide:toggle-terminal' 47 | } 48 | { 49 | 'label': 'Toggle Debugger' 50 | 'command': 'learn-ide:toggle:debugger' 51 | } 52 | { 53 | 'label': 'Check for Update' 54 | 'command': 'learn-ide:update-check' 55 | } 56 | ] 57 | ] 58 | } 59 | ] 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-ide", 3 | "version": "2.5.6", 4 | "atomVersion": "1.14.4", 5 | "main": "./lib/learn-ide", 6 | "urlMain": "./lib/url-handler", 7 | "description": "An integrated development environment for Learn.co", 8 | "scripts": { 9 | "postpublish": "./scripts/airbrake_deploy" 10 | }, 11 | "keywords": [], 12 | "repository": "https://github.com/learn-co/learn-ide", 13 | "license": "MIT", 14 | "engines": { 15 | "atom": ">=1.0.0 <2.0.0" 16 | }, 17 | "consumedServices": { 18 | "status-bar": { 19 | "versions": { 20 | "^1.0.0": "consumeStatusBar" 21 | } 22 | } 23 | }, 24 | "dependencies": { 25 | "atom-socket": "0.0.9", 26 | "atom-space-pen-views": "2.1.0", 27 | "dotenv": "2.0.0", 28 | "page-bus": "3.0.1", 29 | "register-protocol-win32": "1.0.0", 30 | "semver": "5.3.0", 31 | "stacktrace-parser": "0.1.4", 32 | "xterm": "2.6.0" 33 | }, 34 | "packageDependencies": { 35 | "atom-material-syntax": "1.0.2", 36 | "atom-material-syntax-dark": "0.2.7", 37 | "atom-material-syntax-light": "0.4.6", 38 | "learn-ide-material-ui": "1.3.15", 39 | "learn-ide-notifications": "0.66.5", 40 | "learn-ide-tree": "1.0.24" 41 | }, 42 | "devDependencies": { 43 | "decompress": "4.0.0", 44 | "del": "2.2.2", 45 | "gulp": "3.9.1", 46 | "gulp-util": "3.0.7", 47 | "request": "2.75.0", 48 | "run-sequence": "1.2.2", 49 | "shelljs": "0.7.3", 50 | "ssh2": "0.5.0", 51 | "underscore-plus": "^1.6.6" 52 | }, 53 | "configSchema": { 54 | "bleedingUpdates": { 55 | "order": 0, 56 | "type": "boolean", 57 | "default": false, 58 | "title": "Subscribe to Bleeding Updates", 59 | "description": "Recieve update notifications as beta and early releases of the Learn IDE become available. Learn more on the [help center](https://learn.co/ide/bleeding)." 60 | }, 61 | "notifier": { 62 | "order": 1, 63 | "type": "boolean", 64 | "default": true, 65 | "title": "Learn Status Notifications", 66 | "description": "Receive desktop notifications that correspond to the lights on Learn.co." 67 | }, 68 | "terminalColors": { 69 | "order": 2, 70 | "type": "object", 71 | "properties": { 72 | "basic": { 73 | "order": 1, 74 | "title": "Basic Colors", 75 | "type": "object", 76 | "properties": { 77 | "foreground": { 78 | "order": 0, 79 | "type": "color", 80 | "default": "#7ea2b4", 81 | "title": "Foreground" 82 | }, 83 | "background": { 84 | "order": 1, 85 | "type": "color", 86 | "default": "#1A2226", 87 | "title": "Background" 88 | } 89 | } 90 | }, 91 | "ansi": { 92 | "order": 2, 93 | "title": "ANSI Colors", 94 | "type": "object", 95 | "properties": { 96 | "0": { 97 | "order": 0, 98 | "type": "color", 99 | "default": "#161b1d", 100 | "title": "Black" 101 | }, 102 | "1": { 103 | "order": 1, 104 | "type": "color", 105 | "default": "#d22d72", 106 | "title": "Red" 107 | }, 108 | "2": { 109 | "order": 2, 110 | "type": "color", 111 | "default": "#568c3b", 112 | "title": "Green" 113 | }, 114 | "3": { 115 | "order": 3, 116 | "type": "color", 117 | "default": "#8a8a0f", 118 | "title": "Yellow" 119 | }, 120 | "4": { 121 | "order": 4, 122 | "type": "color", 123 | "default": "#257fad", 124 | "title": "Blue" 125 | }, 126 | "5": { 127 | "order": 5, 128 | "type": "color", 129 | "default": "#5d5db1", 130 | "title": "Magenta" 131 | }, 132 | "6": { 133 | "order": 6, 134 | "type": "color", 135 | "default": "#2d8f6f", 136 | "title": "Cyan" 137 | }, 138 | "7": { 139 | "order": 7, 140 | "type": "color", 141 | "default": "#7ea2b4", 142 | "title": "White" 143 | }, 144 | "8": { 145 | "order": 8, 146 | "type": "color", 147 | "default": "#5a7b8c", 148 | "title": "Bright Black" 149 | }, 150 | "9": { 151 | "order": 9, 152 | "type": "color", 153 | "default": "#d22d72", 154 | "title": "Bright Red" 155 | }, 156 | "10": { 157 | "order": 10, 158 | "type": "color", 159 | "default": "#568c3b", 160 | "title": "Bright Green" 161 | }, 162 | "11": { 163 | "order": 11, 164 | "type": "color", 165 | "default": "#8a8a0f", 166 | "title": "Bright Yellow" 167 | }, 168 | "12": { 169 | "order": 12, 170 | "type": "color", 171 | "default": "#257fad", 172 | "title": "Bright Blue" 173 | }, 174 | "13": { 175 | "order": 13, 176 | "type": "color", 177 | "default": "#5d5db1", 178 | "title": "Bright Magenta" 179 | }, 180 | "14": { 181 | "order": 14, 182 | "type": "color", 183 | "default": "#2d8f6f", 184 | "title": "Bright Cyan" 185 | }, 186 | "15": { 187 | "order": 15, 188 | "type": "color", 189 | "default": "#ebf8ff", 190 | "title": "Bright White" 191 | } 192 | } 193 | }, 194 | "json": { 195 | "title": "JSON Color Scheme", 196 | "order": 3, 197 | "type": "string", 198 | "default": "", 199 | "description": "Import complete and customizable color schemes from [terminal.sexy](https://terminal.sexy/) by pasting a JSON export here. For more information, visit the [help center](http://help.learn.co/)." 200 | } 201 | } 202 | }, 203 | "fontFamily": { 204 | "title": "Font Family", 205 | "order": 3, 206 | "type": "string", 207 | "default": "", 208 | "description": "The name of the font family used in the terminal. Be sure to choose a monospaced font!" 209 | }, 210 | "fontSize": { 211 | "title": "Font Size", 212 | "order": 4, 213 | "type": "number", 214 | "default": 14, 215 | "minimum": 2, 216 | "description": "The name of the font size used in the terminal." 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /resources/app-icons/atom.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/resources/app-icons/atom.icns -------------------------------------------------------------------------------- /resources/app-icons/atom.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/resources/app-icons/atom.ico -------------------------------------------------------------------------------- /resources/app-icons/png/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/resources/app-icons/png/1024.png -------------------------------------------------------------------------------- /resources/app-icons/png/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/resources/app-icons/png/128.png -------------------------------------------------------------------------------- /resources/app-icons/png/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/resources/app-icons/png/16.png -------------------------------------------------------------------------------- /resources/app-icons/png/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/resources/app-icons/png/24.png -------------------------------------------------------------------------------- /resources/app-icons/png/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/resources/app-icons/png/256.png -------------------------------------------------------------------------------- /resources/app-icons/png/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/resources/app-icons/png/32.png -------------------------------------------------------------------------------- /resources/app-icons/png/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/resources/app-icons/png/48.png -------------------------------------------------------------------------------- /resources/app-icons/png/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/resources/app-icons/png/512.png -------------------------------------------------------------------------------- /resources/app-icons/png/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/resources/app-icons/png/64.png -------------------------------------------------------------------------------- /resources/app-icons/png/96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/resources/app-icons/png/96.png -------------------------------------------------------------------------------- /resources/script-replacements/code-sign-on-mac.js: -------------------------------------------------------------------------------- 1 | const spawnSync = require('./spawn-sync') 2 | 3 | module.exports = function (packagedAppPath) { 4 | var certificates = spawnSync('security', ['find-identity', '-p', 'codesigning', '-v']).stdout.toString(); 5 | var hasFlatironCert = certificates.match('Developer ID Application: Flatiron School, Inc'); 6 | 7 | if (!hasFlatironCert) { 8 | console.log('Skipping code signing because the Flatiron School dev certificate is missing'.gray) 9 | return 10 | } 11 | 12 | console.log(`Code-signing application at ${packagedAppPath}`) 13 | spawnSync('codesign', [ 14 | '--deep', '--force', '--verbose', 15 | '--sign', 'Developer ID Application: Flatiron School, Inc', packagedAppPath 16 | ], {stdio: 'inherit'}) 17 | } 18 | -------------------------------------------------------------------------------- /resources/win/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/resources/win/loading.gif -------------------------------------------------------------------------------- /scripts/airbrake_deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./.env 4 | 5 | ENVIRONMENT=$(jq -r .name ./package.json) 6 | REPOSITORY=$(jq -r .repository ./package.json) 7 | REVISION=$(git rev-parse HEAD) 8 | USERNAME=$(whoami) 9 | VERSION=v$(jq -r .version ./package.json) 10 | 11 | PAYLOAD="{'environment':'${ENVIRONMENT}','version':'${VERSION}','username':'${USERNAME}','repository':'${REPOSITORY}','revision':'${REVISION}'}" 12 | 13 | echo $PAYLOAD 14 | echo $AIRBRAKE_PROJECT_ID 15 | echo $AIRBRAKE_PROJECT_KEY 16 | 17 | curl -X POST \ 18 | -H "Content-Type: application/json" \ 19 | -d PAYLOAD \ 20 | "https://airbrake.io/api/v4/projects/${AIRBRAKE_PROJECT_ID}/deploys?key=${AIRBRAKE_PROJECT_KEY}" 21 | -------------------------------------------------------------------------------- /static/images/fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/static/images/fail.png -------------------------------------------------------------------------------- /static/images/pass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co/learn-ide/43358dccca0d0066f3f55f2afbebd748807a47cf/static/images/pass.png -------------------------------------------------------------------------------- /styles/learn-ide.less: -------------------------------------------------------------------------------- 1 | @import (less) "../node_modules/xterm/dist/xterm.css"; 2 | 3 | @red: #d92626; 4 | @green: #73c990; 5 | 6 | .terminal-resizer { 7 | z-index: 2; 8 | padding: 3px 3px 0; 9 | 10 | .terminal-resize-handle { 11 | position: absolute; 12 | height: 10px; 13 | top: 0; 14 | left: 0; 15 | right: 0; 16 | cursor: row-resize; 17 | z-index: 3; 18 | } 19 | } 20 | 21 | .platform-win32 { 22 | .terminal-resizer { 23 | .terminal-resize-handle { 24 | cursor: ns-resize; 25 | } 26 | } 27 | } 28 | 29 | .learn-synced-fs-status { 30 | &:hover { 31 | cursor: default; 32 | } 33 | } 34 | 35 | .learn-popout-terminal-icon { 36 | margin-left: 12px !important; 37 | 38 | &.active { 39 | &:hover { 40 | cursor: pointer; 41 | } 42 | } 43 | 44 | &.inactive { 45 | &:hover { 46 | cursor: default; 47 | } 48 | display: none; 49 | } 50 | } 51 | 52 | .learn-status-icon { 53 | &[data-status='good'] { 54 | color: @green; 55 | } 56 | 57 | &[data-status='bad'] { 58 | color: @red; 59 | text-decoration: underline; 60 | 61 | &:hover { 62 | cursor: pointer; 63 | } 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /styles/terminal-colors.css: -------------------------------------------------------------------------------- 1 | .terminal.xterm {color: rgba(126, 162, 180, 1); background-color: rgba(26, 34, 38, 1)} .bottom .tool-panel .terminal-resizer, .terminal.xterm .xterm-rows, .terminal.xterm .xterm-viewport {background-color: rgba(26, 34, 38, 1)} 2 | .terminal.xterm .xterm-color-0 {color: rgba(22, 27, 29, 1)} .terminal.xterm .xterm-bg-color-0 {background-color: rgba(22, 27, 29, 1)} 3 | .terminal.xterm .xterm-color-1 {color: rgba(210, 45, 114, 1)} .terminal.xterm .xterm-bg-color-1 {background-color: rgba(210, 45, 114, 1)} 4 | .terminal.xterm .xterm-color-2 {color: rgba(86, 140, 59, 1)} .terminal.xterm .xterm-bg-color-2 {background-color: rgba(86, 140, 59, 1)} 5 | .terminal.xterm .xterm-color-3 {color: rgba(138, 138, 15, 1)} .terminal.xterm .xterm-bg-color-3 {background-color: rgba(138, 138, 15, 1)} 6 | .terminal.xterm .xterm-color-4 {color: rgba(37, 127, 173, 1)} .terminal.xterm .xterm-bg-color-4 {background-color: rgba(37, 127, 173, 1)} 7 | .terminal.xterm .xterm-color-5 {color: rgba(93, 93, 177, 1)} .terminal.xterm .xterm-bg-color-5 {background-color: rgba(93, 93, 177, 1)} 8 | .terminal.xterm .xterm-color-6 {color: rgba(45, 143, 111, 1)} .terminal.xterm .xterm-bg-color-6 {background-color: rgba(45, 143, 111, 1)} 9 | .terminal.xterm .xterm-color-7 {color: rgba(126, 162, 180, 1)} .terminal.xterm .xterm-bg-color-7 {background-color: rgba(126, 162, 180, 1)} 10 | .terminal.xterm .xterm-color-8 {color: rgba(90, 123, 140, 1)} .terminal.xterm .xterm-bg-color-8 {background-color: rgba(90, 123, 140, 1)} 11 | .terminal.xterm .xterm-color-9 {color: rgba(210, 45, 114, 1)} .terminal.xterm .xterm-bg-color-9 {background-color: rgba(210, 45, 114, 1)} 12 | .terminal.xterm .xterm-color-10 {color: rgba(86, 140, 59, 1)} .terminal.xterm .xterm-bg-color-10 {background-color: rgba(86, 140, 59, 1)} 13 | .terminal.xterm .xterm-color-11 {color: rgba(138, 138, 15, 1)} .terminal.xterm .xterm-bg-color-11 {background-color: rgba(138, 138, 15, 1)} 14 | .terminal.xterm .xterm-color-12 {color: rgba(37, 127, 173, 1)} .terminal.xterm .xterm-bg-color-12 {background-color: rgba(37, 127, 173, 1)} 15 | .terminal.xterm .xterm-color-13 {color: rgba(93, 93, 177, 1)} .terminal.xterm .xterm-bg-color-13 {background-color: rgba(93, 93, 177, 1)} 16 | .terminal.xterm .xterm-color-14 {color: rgba(45, 143, 111, 1)} .terminal.xterm .xterm-bg-color-14 {background-color: rgba(45, 143, 111, 1)} 17 | .terminal.xterm .xterm-color-15 {color: rgba(235, 248, 255, 1)} .terminal.xterm .xterm-bg-color-15 {background-color: rgba(235, 248, 255, 1)} 18 | -------------------------------------------------------------------------------- /utils/child-process-wrapper.js: -------------------------------------------------------------------------------- 1 | // special thanks to [particle dev](https://github.com/spark/particle-dev-app) for this 2 | 3 | var childProcess = require('child_process'); 4 | 5 | // Exit the process if the command failed and only call the callback if the 6 | // command succeed, output of the command would also be piped. 7 | exports.safeExec = function(command, options, callback) { 8 | if (!callback) { 9 | callback = options; 10 | options = {}; 11 | } 12 | if (!options) 13 | options = {}; 14 | 15 | // This needed to be increased for `apm test` runs that generate many failures 16 | // The default is 200KB. 17 | options.maxBuffer = 1024 * 1024; 18 | 19 | var child = childProcess.exec(command, options, function(error, stdout, stderr) { 20 | if (error) 21 | process.exit(error.code || 1); 22 | else 23 | callback(null); 24 | }); 25 | child.stderr.pipe(process.stderr); 26 | if (!options.ignoreStdout) 27 | child.stdout.pipe(process.stdout); 28 | } 29 | 30 | // Same with safeExec but call child_process.spawn instead. 31 | exports.safeSpawn = function(command, args, options, callback) { 32 | if (!callback) { 33 | callback = options; 34 | options = {}; 35 | } 36 | var child = childProcess.spawn(command, args, options); 37 | child.stderr.pipe(process.stderr); 38 | child.stdout.pipe(process.stdout); 39 | child.on('exit', function(code) { 40 | if (code != 0) 41 | process.exit(code); 42 | else 43 | callback(null); 44 | }); 45 | } 46 | --------------------------------------------------------------------------------