This commit is contained in:
Eugene Pankov 2016-12-26 23:21:50 +01:00
parent 4e451d0598
commit d7bae654eb
19 changed files with 326 additions and 71 deletions

View File

@ -13,7 +13,7 @@ setupWindowManagement = () => {
app.window.on('close', (e) => { app.window.on('close', (e) => {
windowConfig.set('windowBoundaries', app.window.getBounds()) windowConfig.set('windowBoundaries', app.window.getBounds())
if (!windowCloseable) { if (!windowCloseable) {
app.window.hide() app.window.minimize()
e.preventDefault() e.preventDefault()
} }
}) })
@ -31,6 +31,18 @@ setupWindowManagement = () => {
app.window.focus() app.window.focus()
}) })
electron.ipcMain.on('window-maximize', () => {
if (app.window.isMaximized()) {
app.window.unmaximize()
} else {
app.window.maximize()
}
})
electron.ipcMain.on('window-minimize', () => {
app.window.minimize()
})
app.on('before-quit', () => windowCloseable = true) app.on('before-quit', () => windowCloseable = true)
} }
@ -82,16 +94,15 @@ start = () => {
'web-preferences': {'web-security': false}, 'web-preferences': {'web-security': false},
//- background to avoid the flash of unstyled window //- background to avoid the flash of unstyled window
backgroundColor: '#1D272D', backgroundColor: '#1D272D',
frame: false,
} }
Object.assign(options, windowConfig.get('windowBoundaries')) Object.assign(options, windowConfig.get('windowBoundaries'))
if (platform == 'darwin') { if (platform == 'darwin') {
options.titleBarStyle = 'hidden' options.titleBarStyle = 'hidden'
} else {
options.frame = false
} }
app.commandLine.appendSwitch('--disable-http-cache') app.commandLine.appendSwitch('disable-http-cache')
app.window = new electron.BrowserWindow(options) app.window = new electron.BrowserWindow(options)
app.window.loadURL(`file://${app.getAppPath()}/assets/webpack/index.html`, {extraHeaders: "pragma: no-cache\n"}) app.window.loadURL(`file://${app.getAppPath()}/assets/webpack/index.html`, {extraHeaders: "pragma: no-cache\n"})

View File

@ -3,7 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"main": "main.js", "main": "main.js",
"dependencies": { "dependencies": {
"child-process-promise": "^2.1.3", "child-process-promise": "^2.2.0",
"devtron": "^1.4.0", "devtron": "^1.4.0",
"electron-config": "^0.2.1", "electron-config": "^0.2.1",
"electron-debug": "^1.0.1", "electron-debug": "^1.0.1",

View File

@ -9,6 +9,7 @@ import { ConfigService } from 'services/config'
import { ElectronService } from 'services/electron' import { ElectronService } from 'services/electron'
import { HostAppService } from 'services/hostApp' import { HostAppService } from 'services/hostApp'
import { LogService } from 'services/log' import { LogService } from 'services/log'
import { HotkeysService } from 'services/hotkeys'
import { ModalService } from 'services/modal' import { ModalService } from 'services/modal'
import { NotifyService } from 'services/notify' import { NotifyService } from 'services/notify'
import { QuitterService } from 'services/quitter' import { QuitterService } from 'services/quitter'
@ -33,6 +34,7 @@ import { TerminalComponent } from 'components/terminal'
ConfigService, ConfigService,
ElectronService, ElectronService,
HostAppService, HostAppService,
HotkeysService,
LogService, LogService,
ModalService, ModalService,
NotifyService, NotifyService,

View File

@ -3,20 +3,65 @@
:host { :host {
display: flex; display: flex;
width: 100vw; width: ~"calc(100vw - 2px)";
height: 100vh; height: ~"calc(100vh - 2px)";
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
-webkit-user-select: none; -webkit-user-select: none;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
cursor: default;
background: @body-bg; background: @body-bg;
} }
@titlebar-height: 35px;
@tabs-height: 40px; @tabs-height: 40px;
@tab-border-radius: 3px;
.button-states() {
transition: 0.125s all;
&:hover:not(.active) {
background: rgba(255, 255, 255, .033);
}
&:active:not(.active) {
background: rgba(0, 0, 0, .1);
}
}
.titlebar {
height: @titlebar-height;
background: #141c23;
flex: none;
display: flex;
flex-direction: row;
.title {
flex: auto;
padding-left: 15px;
line-height: @titlebar-height;
-webkit-app-region: drag;
}
.btn-minimize, .btn-maximize, .btn-close {
flex: none;
line-height: @titlebar-height - 2px;
padding: 0 15px;
font-size: 8px;
.button-states();
cursor: pointer;
}
.btn-close {
font-size: 12px;
}
}
.tabs { .tabs {
flex: none; flex: none;
height: @tabs-height; height: @tabs-height;
background: #141c23;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -41,13 +86,7 @@
margin-right: 10px; margin-right: 10px;
} }
&:hover { .button-states();
background: rgba(255, 255, 255, .1);
}
&:active {
background: rgba(0, 0, 0, .1);
}
} }
.tab { .tab {
@ -55,24 +94,41 @@
flex-basis: 0; flex-basis: 0;
flex-grow: 1; flex-grow: 1;
background: @body-bg;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
overflow: hidden;
min-width: 0;
div { &.pre-selected, &:nth-last-child(2) {
border-top-right-radius: @tab-border-radius;
}
&.post-selected {
border-top-left-radius: @tab-border-radius;
}
div.index {
flex: none;
padding: 0 0 0 15px;
font-weight: bold;
color: #444;
}
div.name {
flex: auto; flex: auto;
padding: 0 15px; margin: 0 15px 0 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
min-width: 0;
} }
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
transition: 0.25s all; transition: 0.25s all;
&:hover:not(.active) { .button-states();
background: rgba(255, 255, 255, .05);
}
&:active {
background: rgba(0, 0, 0, .1);
}
&.active { &.active {
background: #141c23; background: #141c23;

View File

@ -1,6 +1,22 @@
.titlebar
.title((dblclick)='hostApp.maximizeWindow()') Term
.btn-minimize((click)='hostApp.minimizeWindow()')
i.fa.fa-window-minimize
.btn-maximize((click)='hostApp.maximizeWindow()')
i.fa.fa-window-maximize
.btn-close((click)='hostApp.quit()')
i.fa.fa-close
.tabs .tabs
.tab(*ngFor='let tab of tabs; trackBy: tab?.id', (click)='selectTab(tab)', [class.active]='tab == activeTab') .tab(
div {{tab.name}} *ngFor='let tab of tabs; let idx = index; trackBy: tab?.id',
(click)='selectTab(tab)',
[class.active]='tab == activeTab',
[class.pre-selected]='tabs[idx + 1] == activeTab',
[class.post-selected]='tabs[idx - 1] == activeTab',
)
div.index {{idx + 1}}
div.name {{tab.name || 'Terminal'}}
button((click)='closeTab(tab)') × button((click)='closeTab(tab)') ×
.btn-new-tab((click)='newTab()') .btn-new-tab((click)='newTab()')
i.fa.fa-plus i.fa.fa-plus

View File

@ -1,7 +1,8 @@
import { Component } from '@angular/core' import { Component, ElementRef } from '@angular/core'
import { ModalService } from 'services/modal' import { ModalService } from 'services/modal'
import { ElectronService } from 'services/electron' import { ElectronService } from 'services/electron'
import { HostAppService } from 'services/hostApp' import { HostAppService } from 'services/hostApp'
import { HotkeysService } from 'services/hotkeys'
import { LogService } from 'services/log' import { LogService } from 'services/log'
import { QuitterService } from 'services/quitter' import { QuitterService } from 'services/quitter'
import { ToasterConfig } from 'angular2-toaster' import { ToasterConfig } from 'angular2-toaster'
@ -31,11 +32,13 @@ class Tab {
}) })
export class AppComponent { export class AppComponent {
constructor( constructor(
private hostApp: HostAppService,
private modal: ModalService, private modal: ModalService,
private electron: ElectronService, private elementRef: ElementRef,
private sessions: SessionsService, private sessions: SessionsService,
public hostApp: HostAppService,
public hotkeys: HotkeysService,
log: LogService, log: LogService,
electron: ElectronService,
_quitter: QuitterService, _quitter: QuitterService,
) { ) {
console.timeStamp('AppComponent ctor') console.timeStamp('AppComponent ctor')
@ -48,6 +51,22 @@ export class AppComponent {
preventDuplicates: true, preventDuplicates: true,
timeout: 4000, timeout: 4000,
}) })
this.hotkeys.key.subscribe((key) => {
if (key.event == 'keydown') {
if (key.alt && key.key >= '1' && key.key <= '9') {
let index = key.key.charCodeAt(0) - '0'.charCodeAt(0) - 1
if (index < this.tabs.length) {
this.selectTab(this.tabs[index])
}
}
if (key.alt && key.key == '0') {
if (this.tabs.length >= 10) {
this.selectTab(this.tabs[9])
}
}
}
})
} }
toasterConfig: ToasterConfig toasterConfig: ToasterConfig
@ -55,23 +74,40 @@ export class AppComponent {
activeTab: Tab activeTab: Tab
newTab () { newTab () {
const tab = new Tab(this.sessions.createSession({command: 'bash'})) this.addSessionTab(this.sessions.createNewSession({command: 'bash'}))
}
addSessionTab (session) {
let tab = new Tab(session)
this.tabs.push(tab) this.tabs.push(tab)
this.selectTab(tab) this.selectTab(tab)
} }
selectTab (tab) { selectTab (tab) {
this.activeTab = tab this.activeTab = tab
setImmediate(() => {
this.elementRef.nativeElement.querySelector(':scope .tab.active iframe').focus()
})
} }
closeTab (tab) { closeTab (tab) {
tab.session.destroy() tab.session.gracefullyDestroy()
this.tabs = this.tabs.filter((x) => x != tab) this.tabs = this.tabs.filter((x) => x != tab)
this.selectTab(this.tabs[0]) if (tab == this.activeTab) {
this.selectTab(this.tabs[0])
}
} }
ngOnInit () { ngOnInit () {
this.newTab() this.sessions.recoverAll().then((recoveredSessions) => {
if (recoveredSessions.length > 0) {
recoveredSessions.forEach((session) => {
this.addSessionTab(session)
})
} else {
this.newTab()
}
})
} }
ngOnDestroy () { ngOnDestroy () {

View File

@ -2,11 +2,8 @@ import { Component } from '@angular/core'
import { ElectronService } from 'services/electron' import { ElectronService } from 'services/electron'
import { HostAppService, PLATFORM_WINDOWS, PLATFORM_LINUX, PLATFORM_MAC } from 'services/hostApp' import { HostAppService, PLATFORM_WINDOWS, PLATFORM_LINUX, PLATFORM_MAC } from 'services/hostApp'
import { ConfigService } from 'services/config' import { ConfigService } from 'services/config'
import { QuitterService } from 'services/quitter'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import * as os from 'os'
@Component({ @Component({
selector: 'settings-modal', selector: 'settings-modal',
@ -16,10 +13,9 @@ import * as os from 'os'
export class SettingsModalComponent { export class SettingsModalComponent {
constructor( constructor(
private modalInstance: NgbActiveModal, private modalInstance: NgbActiveModal,
private hostApp: HostAppService,
private electron: ElectronService,
private quitter: QuitterService,
public config: ConfigService, public config: ConfigService,
hostApp: HostAppService,
electron: ElectronService,
) { ) {
this.isWindows = hostApp.platform == PLATFORM_WINDOWS this.isWindows = hostApp.platform == PLATFORM_WINDOWS
this.isMac = hostApp.platform == PLATFORM_MAC this.isMac = hostApp.platform == PLATFORM_MAC

View File

@ -1,5 +1,4 @@
import { Component, NgZone, Input, Output, EventEmitter, ElementRef } from '@angular/core' import { Component, NgZone, Input, Output, EventEmitter, ElementRef } from '@angular/core'
import { ElectronService } from 'services/electron'
import { ConfigService } from 'services/config' import { ConfigService } from 'services/config'
import { Session } from 'services/sessions' import { Session } from 'services/sessions'
@ -10,16 +9,25 @@ const hterm = require('hterm-commonjs')
hterm.hterm.VT.ESC['k'] = function(parseState) { hterm.hterm.VT.ESC['k'] = function(parseState) {
parseState.resetArguments(); parseState.resetArguments();
function parseOSC(parseState) { function parseOSC(ps) {
if (!this.parseUntilStringTerminator_(parseState) || parseState.func == parseOSC) { if (!this.parseUntilStringTerminator_(ps) || ps.func == parseOSC) {
return return
} }
this.terminal.setWindowTitle(parseState.args[0]) this.terminal.setWindowTitle(ps.args[0])
} }
parseState.func = parseOSC parseState.func = parseOSC
} }
hterm.hterm.defaultStorage = new hterm.lib.Storage.Memory()
hterm.hterm.PreferenceManager.defaultPreferences['user-css'] = ``
const oldDecorate = hterm.hterm.ScrollPort.prototype.decorate
hterm.hterm.ScrollPort.prototype.decorate = function (...args) {
oldDecorate.bind(this)(...args)
this.screen_.style.cssText += `; padding-right: ${this.screen_.offsetWidth - this.screen_.clientWidth}px;`
}
@Component({ @Component({
selector: 'terminal', selector: 'terminal',
template: '', template: '',
@ -33,7 +41,6 @@ export class TerminalComponent {
constructor( constructor(
private zone: NgZone, private zone: NgZone,
private electron: ElectronService,
private elementRef: ElementRef, private elementRef: ElementRef,
public config: ConfigService, public config: ConfigService,
) { ) {
@ -41,7 +48,6 @@ export class TerminalComponent {
ngOnInit () { ngOnInit () {
let io let io
hterm.hterm.defaultStorage = new hterm.lib.Storage.Memory()
this.terminal = new hterm.hterm.Terminal() this.terminal = new hterm.hterm.Terminal()
this.terminal.setWindowTitle = (title) => { this.terminal.setWindowTitle = (title) => {
this.zone.run(() => { this.zone.run(() => {
@ -72,5 +78,6 @@ export class TerminalComponent {
} }
ngOnDestroy () { ngOnDestroy () {
;
} }
} }

View File

@ -19,7 +19,7 @@ if (nodeRequire('electron-is-dev')) {
} }
console.timeStamp('angular bootstrap started') console.timeStamp('angular bootstrap started')
platformBrowserDynamic().bootstrapModule(AppModule) platformBrowserDynamic().bootstrapModule(AppModule);
process.emitWarning = function () { console.log(arguments) } (<any>process).emitWarning = function () { console.log(arguments) }

View File

@ -62,7 +62,15 @@ export class HostAppService {
this.electron.ipcRenderer.send('window-focus') this.electron.ipcRenderer.send('window-focus')
} }
quit() { minimizeWindow () {
this.electron.ipcRenderer.send('window-minimize')
}
maximizeWindow () {
this.electron.ipcRenderer.send('window-maximize')
}
quit () {
this.logger.info('Quitting') this.logger.info('Quitting')
this.electron.app.quit() this.electron.app.quit()
} }

View File

@ -0,0 +1,60 @@
import { Injectable, NgZone, EventEmitter } from '@angular/core'
const hterm = require('hterm-commonjs')
export interface Key {
event: string,
alt: boolean,
ctrl: boolean,
cmd: boolean,
shift: boolean,
key: string
}
@Injectable()
export class HotkeysService {
key = new EventEmitter<Key>()
constructor(private zone: NgZone) {
let events = [
{
name: 'keydown',
htermHandler: 'onKeyDown_',
},
{
name: 'keypress',
htermHandler: 'onKeyPress_',
},
{
name: 'keyup',
htermHandler: 'onKeyUp_',
},
]
events.forEach((event) => {
document.addEventListener(event.name, (nativeEvent) => {
this.emitNativeEvent(event.name, nativeEvent)
})
let oldHandler = hterm.hterm.Keyboard.prototype[event.htermHandler]
const __this = this
hterm.hterm.Keyboard.prototype[event.htermHandler] = function (nativeEvent) {
__this.emitNativeEvent(event.name, nativeEvent)
oldHandler.bind(this)(nativeEvent)
}
})
}
emitNativeEvent (name, nativeEvent) {
console.debug('Key', nativeEvent)
this.zone.run(() => {
this.key.emit({
event: name,
alt: nativeEvent.altKey,
shift: nativeEvent.shiftKey,
cmd: nativeEvent.metaKey,
ctrl: nativeEvent.ctrlKey,
key: nativeEvent.key,
})
})
}
}

View File

@ -1,11 +1,10 @@
import { Injectable, NgZone } from '@angular/core'; import { Injectable } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@Injectable() @Injectable()
export class ModalService { export class ModalService {
constructor( constructor(
private zone: NgZone,
private ngbModal: NgbModal, private ngbModal: NgbModal,
) {} ) {}

View File

@ -1,13 +1,11 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { ToasterService } from 'angular2-toaster' import { ToasterService } from 'angular2-toaster'
import { LogService } from 'services/log'
@Injectable() @Injectable()
export class NotifyService { export class NotifyService {
constructor( constructor(
private toaster: ToasterService, private toaster: ToasterService,
private log: LogService,
) {} ) {}
pop(options) { pop(options) {

View File

@ -1,12 +1,10 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { HostAppService } from 'services/hostApp' import { HostAppService } from 'services/hostApp'
import { ElectronService } from 'services/electron'
@Injectable() @Injectable()
export class QuitterService { export class QuitterService {
constructor( constructor(
private electron: ElectronService,
private hostApp: HostAppService, private hostApp: HostAppService,
) { ) {
hostApp.quitRequested.subscribe(() => { hostApp.quitRequested.subscribe(() => {

View File

@ -1,8 +1,52 @@
import { Injectable, NgZone, EventEmitter } from '@angular/core' import { Injectable, NgZone, EventEmitter } from '@angular/core'
import { Logger, LogService } from 'services/log' import { Logger, LogService } from 'services/log'
const exec = require('child-process-promise').exec
import * as crypto from 'crypto'
import * as ptyjs from 'pty.js' import * as ptyjs from 'pty.js'
export interface SessionRecoveryProvider {
list(): Promise<any[]>
getRecoveryCommand(item: any): string
getNewSessionCommand(command: string): string
}
export class NullSessionRecoveryProvider implements SessionRecoveryProvider {
list(): Promise<any[]> {
return Promise.resolve([])
}
getRecoveryCommand(_: any): string {
return null
}
getNewSessionCommand(command: string) {
return command
}
}
export class ScreenSessionRecoveryProvider implements SessionRecoveryProvider {
list(): Promise<any[]> {
return exec('screen -ls').then((result) => {
return result.stdout.split('\n')
.filter((line) => /\bterm-tab-/.exec(line))
.map((line) => line.trim().split('.')[0])
}).catch(() => {
return []
})
}
getRecoveryCommand(item: any): string {
return `screen -r ${item}`
}
getNewSessionCommand(command: string): string {
const id = crypto.randomBytes(8).toString('hex')
return `screen -U -S term-tab-${id} -- ${command}`
}
}
export interface SessionOptions { export interface SessionOptions {
name?: string, name?: string,
command: string, command: string,
@ -20,9 +64,10 @@ export class Session {
constructor (options: SessionOptions) { constructor (options: SessionOptions) {
this.name = options.name this.name = options.name
console.log('Spawning', options.command)
this.pty = ptyjs.spawn('sh', ['-c', options.command], { this.pty = ptyjs.spawn('sh', ['-c', options.command], {
name: 'xterm-color', //name: 'xterm-color',
//name: 'screen-256color', name: 'xterm-256color',
cols: 80, cols: 80,
rows: 30, rows: 30,
cwd: options.cwd || process.env.HOME, cwd: options.cwd || process.env.HOME,
@ -62,7 +107,7 @@ export class Session {
gracefullyDestroy () { gracefullyDestroy () {
return new Promise((resolve) => { return new Promise((resolve) => {
this.sendSignal('SIGTERM') this.sendSignal('SIGTERM')
if (!open) { if (!this.open) {
resolve() resolve()
this.destroy() this.destroy()
} else { } else {
@ -91,11 +136,20 @@ export class SessionsService {
sessions: {[id: string]: Session} = {} sessions: {[id: string]: Session} = {}
logger: Logger logger: Logger
private lastID = 0 private lastID = 0
recoveryProvider: SessionRecoveryProvider
constructor( constructor(
private zone: NgZone,
log: LogService, log: LogService,
) { ) {
this.logger = log.create('sessions') this.logger = log.create('sessions')
this.recoveryProvider = new ScreenSessionRecoveryProvider()
//this.recoveryProvider = new NullSessionRecoveryProvider()
}
createNewSession (options: SessionOptions) : Session {
options.command = this.recoveryProvider.getNewSessionCommand(options.command)
return this.createSession(options)
} }
createSession (options: SessionOptions) : Session { createSession (options: SessionOptions) : Session {
@ -109,4 +163,15 @@ export class SessionsService {
this.sessions[session.name] = session this.sessions[session.name] = session
return session return session
} }
recoverAll () : Promise<Session[]> {
return <Promise<Session[]>>(this.recoveryProvider.list().then((items) => {
return this.zone.run(() => {
return items.map((item) => {
const command = this.recoveryProvider.getRecoveryCommand(item)
return this.createSession({command})
})
})
}))
}
} }

View File

@ -20,7 +20,7 @@
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"style-loader": "^0.13.1", "style-loader": "^0.13.1",
"to-string-loader": "^1.1.5", "to-string-loader": "^1.1.5",
"tslint": "4.0.2", "tslint": "4.2.0",
"typescript": "2.1.1", "typescript": "2.1.1",
"typings": "2.0.0", "typings": "2.0.0",
"url-loader": "^0.5.7", "url-loader": "^0.5.7",

View File

@ -11,17 +11,20 @@
"sourceMap": true, "sourceMap": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,
"noUnusedLocals": true
}, },
"compileOnSave": false, "compileOnSave": false,
"exclude": [ "exclude": [
"node_modules", "node_modules",
"platforms", "platforms"
], ],
"files": [ "filesGlob" : [
"app/src/app.d.ts", "app/src/*.ts",
"app/src/entry.ts", "app/src/**/*.ts",
"typings/index.d.ts", "!node_modules/**",
"!app/node_modules/**",
"node_modules/rxjs/Rx.d.ts" "node_modules/rxjs/Rx.d.ts"
] ]
} }

View File

@ -5,16 +5,13 @@
"semicolon": false, "semicolon": false,
"no-inferrable-types": [true, "ignore-params"], "no-inferrable-types": [true, "ignore-params"],
"curly": true, "curly": true,
"no-duplicate-key": true,
"no-duplicate-variable": true, "no-duplicate-variable": true,
"no-empty": true, "no-empty": true,
"no-eval": true, "no-eval": true,
"no-invalid-this": true, "no-invalid-this": true,
"no-shadowed-variable": true, "no-shadowed-variable": true,
"no-unreachable": true,
"no-unused-expression": true, "no-unused-expression": true,
"no-unused-new": true, "no-unused-new": true,
"no-unused-variable": true,
"no-use-before-declare": true, "no-use-before-declare": true,
"no-var-keyword": true, "no-var-keyword": true,
"new-parens": true "new-parens": true

View File

@ -67,16 +67,19 @@ module.exports = {
] ]
}, },
externals: { externals: {
'electron': 'require("electron")', 'fs': 'require("fs")',
'buffer': 'require("buffer")',
'system': '{}',
'file': '{}',
'net': 'require("net")', 'net': 'require("net")',
'electron': 'require("electron")',
'remote': 'require("remote")', 'remote': 'require("remote")',
'shell': 'require("shell")', 'shell': 'require("shell")',
'ipc': 'require("ipc")', 'ipc': 'require("ipc")',
'fs': 'require("fs")', 'crypto': 'require("crypto")',
'buffer': 'require("buffer")',
'pty.js': 'require("pty.js")', 'pty.js': 'require("pty.js")',
'system': '{}', 'child-process-promise': 'require("child-process-promise")',
'file': '{}'
}, },
plugins: [ plugins: [
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({