Skip to content

Commit

Permalink
refactor terminal emulation structure - fix #45 fix #48 fix #55
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanjudis committed Aug 7, 2016
1 parent 36295c0 commit 287834b
Show file tree
Hide file tree
Showing 27 changed files with 690 additions and 244 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@ module.exports = {
'always'
],
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'no-console' : process.env.NODE_ENV === 'production' ? 2 : 0
}
};
177 changes: 145 additions & 32 deletions app/electron.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

const { app, ipcMain, BrowserWindow } = require( 'electron' );
const path = require( 'path' );
const menu = require( './main/menu' );
const fixPath = require( 'fix-path' );
const ElectronSettings = require( 'electron-settings' );
const windowStateKeeper = require( 'electron-window-state' );
const uuid = require( 'uuid' );
const GitHubApi = require( 'github' );
const menu = require( './main/menu' );
const Session = require( './main/session' );
const repoUtils = require( './main/repoUtils' );
const createRPC = require( './main/rpc' );
const github = new GitHubApi( {
protocol : 'https',
headers : {
Expand Down Expand Up @@ -53,16 +57,15 @@ const staticWindows = {
}
};

let mainWindows = [];
const windowSet = new Set( [] );
const rpcSet = new Set( [] );

let config = {};
let baseUrl = {
development : '',
production : `file://${ __dirname }/dist/index.html`
};

// fix path to guarantee that npm and node are available
fixPath();

// enable experimental feature to use context menus
app.commandLine.appendSwitch( '--enable-experimental-web-platform-features' );

Expand All @@ -76,21 +79,13 @@ if ( process.env.NODE_ENV === 'development' ) {
config.url = `${ baseUrl.production }`;
}

ipcMain.on( 'updatedAppSetting', ( event, key, value ) => {
if ( key === 'alwaysOnTop' ) {
mainWindows.forEach( window => {
window.setAlwaysOnTop( value );
} );
}
} );
ipcMain.on( 'openNewWindow', createWindow );

ipcMain.on( 'updatedRepos', ( event, reposString ) => {
mainWindows.forEach(
window => window.webContents.send( 'updatedRepos', reposString )
);
} );
ipcMain.on( 'shellWrite', () => console.log( arguments ) );

ipcMain.on( 'openNewWindow', createWindow );
let settings = new ElectronSettings( {
configFileName : 'npm-app'
} );


/**
Expand Down Expand Up @@ -134,6 +129,7 @@ function createWindow( event, hash ) {
x : mainWindowState.x,
y : mainWindowState.y,
backgroundColor : initialBgColor,
alwaysOnTop : settings.get( 'app.alwaysOnTop' ),
minWidth : 250,
titleBarStyle : 'hidden',
'web-preferences' : {
Expand All @@ -147,29 +143,138 @@ function createWindow( event, hash ) {

newWindow.loadURL( url );

windowSet.add( newWindow );

if ( process.env.NODE_ENV === 'development' ) {
newWindow.webContents.openDevTools( { mode : 'undocked' } );
}

newWindow.on( 'closed', () => {
mainWindows = mainWindows.reduce( ( windows, window ) => {
if ( window !== newWindow ) {
windows.push( window );
}

return windows;
}, [] );
} );

if ( config.devtron ) {
BrowserWindow.addDevToolsExtension( path.join( __dirname, '../node_modules/devtron' ) );
} else {
BrowserWindow.removeDevToolsExtension( 'devtron' );
}

mainWindows.push( newWindow );
const rpc = createRPC( newWindow );
const sessions = new Map();

rpcSet.add( rpc );

rpc.on( 'create session', () => {
initSession( { /* rows, cols, cwd, shell */ }, ( uid, session ) => {
sessions.set( uid, session );

console.log( 'sesstion created', uid );
rpc.emit( 'session set', {
uid,
shell : session.shell,
pid : session.pty.pid
} );

rpc.emit( 'settings loaded', settings.get( 'app' ) || {} );
rpc.emit( 'repos loaded', settings.get( 'repos' ) || [] );

session.on( 'data', ( data ) => {
rpc.emit( 'session data', { uid, data } );
} );

session.on( 'exit', () => {
rpc.emit( 'session exit', { uid } );
sessions.delete( uid );
} );
} );

rpc.on( 'data', ( { uid, data } ) => {
sessions.get( uid ).write( data );
} );

rpc.on( 'update app settings', ( { name, setting } ) => {
settings.set( `app.${ name }`, setting );

// TODO save on loop here
windowSet.forEach( ( window ) => {
if ( name === 'alwaysOnTop' ) {
window.setAlwaysOnTop( setting );
}
} );

rpcSet.forEach( rpc => rpc.emit( 'setting set', { name, setting } ) );
} );

rpc.on( 'add repo', ( repoPath ) => {
repoUtils.readRepoData( repoPath )
.then( repo => {
const repos = [ ...settings.get( 'repos' ), repo ];
settings.set( 'repos', repos );

emitAll( 'repos updated', repos );
} )
.catch( () => {
// TODO put error handling here
} );
} );

rpc.on( 'update repo', ( repoPath ) => {
Promise.all( settings.get( 'repos' ).map( ( repo ) => {
if ( repo.path === repoPath ) {
return repoUtils.readRepoData( repoPath );
}

return repo;
} ) )
.then( repos => {
settings.set( 'repos', repos );

emitAll( 'repos updated', repos );
} )
.catch( () => {
// TODO put error handling here
} );
} );

rpc.on( 'remove repo', ( repoPath ) => {
const repos = settings.get( 'repos' ).reduce( ( repos, storedRepo ) => {
if ( storedRepo.path !== repoPath ) {
repos.push( storedRepo );
}

return repos;
}, [] );

settings.set( 'repos', repos );

emitAll( 'repos updated', repos );
} );
} );


if ( mainWindows.length === 1 ) {
const deleteSessions = () => {
sessions.forEach( ( session, key ) => {
rpc.removeAllListeners( 'data' );
rpc.removeAllListeners( 'update app settings' );
rpc.removeAllListeners( 'add repo' );
rpc.removeAllListeners( 'update repo' );
rpc.removeAllListeners( 'remove repo' );

session.removeAllListeners();
session.destroy();
sessions.delete( key );
} );
};

newWindow.on( 'close', () => {
windowSet.delete( newWindow );
rpcSet.delete( rpc );

rpc.destroy();
deleteSessions();
} );

// we reset the rpc channel only upon
// subsequent refreshes (ie: F5)
newWindow.webContents.on( 'did-navigate', deleteSessions );

if ( windowSet.size === 1 ) {
menu.init( {
createWindow,
openStaticWindow
Expand All @@ -180,6 +285,14 @@ function createWindow( event, hash ) {
console.log( 'window opened' );
}

function emitAll() {
rpcSet.forEach( rpc => rpc.emit.apply( rpc, arguments ) );
}

function initSession( opts, fn ) {
fn( uuid.v4(), new Session( opts ) );
}

if ( process.env.NODE_ENV !== 'development' ) {
github.repos.getReleases(
{ user : 'stefanjudis', repo : 'forrest' },
Expand All @@ -204,7 +317,7 @@ app.on( 'window-all-closed', () => {
} );

app.on( 'activate', () => {
if ( ! mainWindows.length ) {
if ( ! windowSet.size ) {
createWindow();
}
} );
10 changes: 6 additions & 4 deletions app/src/modules/RepoUtils.js → app/main/repoUtils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { remote } from 'electron';

const fs = remote.require( 'fs' );
const fs = require( 'fs' );

/**
* Evaluate repo url from a read package data
Expand Down Expand Up @@ -33,7 +31,7 @@ function getRepoUrl( repoData ) {
*
* @returns {Promise}
*/
export const readRepoData = function( repoPath ) {
function readRepoData( repoPath ) {
return new Promise( ( resolve, reject ) => {
fs.readFile( `${ repoPath }/package.json`, ( error, data ) => {
if ( error ) {
Expand All @@ -57,3 +55,7 @@ export const readRepoData = function( repoPath ) {
} );
} );
};

module.exports = {
readRepoData
};
57 changes: 57 additions & 0 deletions app/main/rpc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const { EventEmitter } = require( 'events' );
const { ipcMain } = require( 'electron' );
const uuid = require( 'uuid' );

class Server extends EventEmitter {

constructor ( win ) {
super();
this.win = win;
this.ipcListener = this.ipcListener.bind( this );

if ( this.destroyed ) {
return;
}

const uid = uuid.v4();
this.id = uid;

ipcMain.on( uid, this.ipcListener );

// we intentionally subscribe to `on` instead of `once`
// to support reloading the window and re-initializing
// the channel
this.wc.on( 'did-finish-load', () => {
this.wc.send( 'init', uid );
} );
}

get wc () {
return this.win.webContents;
}

ipcListener ( event, { ev, data } ) {
super.emit( ev, data );
}

emit ( ch, data ) {
this.wc.send( this.id, { ch, data } );
}

destroy () {
this.removeAllListeners();
this.wc.removeAllListeners();

if ( this.id ) {
ipcMain.removeListener( this.id, this.ipcListener );
} else {
// mark for `genUid` in constructor
this.destroyed = true;
}
}

}

module.exports = function createRPC ( win ) {
return new Server( win );
};
Loading

0 comments on commit 287834b

Please # to comment.