[WIP] Use XO instead of Standard (#723)

* Bump `eslint-plugin-react`

* Add `eslint-config-xo-react`

* Add XO

* Remove eslint-related dependencies, add XO config and use XO as the linter

* Code style: Standard => XO 

* Use xo property to ignore files

* Fix remaining errors
This commit is contained in:
Matheus Fernandes 2016-09-21 11:27:11 -03:00 committed by Leo Lamprecht
parent 32a665d7c0
commit 1866104d03
49 changed files with 910 additions and 784 deletions

View file

@ -1,12 +0,0 @@
# build output
dist
build
app/dist
# dependencies
node_modules
app/node_modules
# other
app/static
assets

View file

@ -1,17 +1,18 @@
const { autoUpdater } = require('electron'); const {autoUpdater} = require('electron');
const { version } = require('./package');
const notify = require('./notify'); // eslint-disable-line no-unused-vars
const ms = require('ms'); const ms = require('ms');
const notify = require('./notify'); // eslint-disable-line no-unused-vars
const {version} = require('./package');
// accepted values: `osx`, `win32` // accepted values: `osx`, `win32`
// https://nuts.gitbook.com/update-windows.html // https://nuts.gitbook.com/update-windows.html
const platform = 'darwin' === process.platform const platform = process.platform === 'darwin' ?
? 'osx' 'osx' :
: process.platform; process.platform;
const FEED_URL = `https://hyperterm-updates.now.sh/update/${platform}`; const FEED_URL = `https://hyperterm-updates.now.sh/update/${platform}`;
let isInit = false; let isInit = false;
function init () { function init() {
autoUpdater.on('error', (err, msg) => { autoUpdater.on('error', (err, msg) => {
console.error('Error fetching updates', msg + ' (' + err.stack + ')'); console.error('Error fetching updates', msg + ' (' + err.stack + ')');
}); });
@ -30,12 +31,14 @@ function init () {
} }
module.exports = function (win) { module.exports = function (win) {
if (!isInit) init(); if (!isInit) {
init();
}
const { rpc } = win; const {rpc} = win;
const onupdate = (ev, releaseNotes, releaseName) => { const onupdate = (ev, releaseNotes, releaseName) => {
rpc.emit('update available', { releaseNotes, releaseName }); rpc.emit('update available', {releaseNotes, releaseName});
}; };
autoUpdater.on('update-downloaded', onupdate); autoUpdater.on('update-downloaded', onupdate);

View file

@ -1,9 +1,10 @@
const { dialog } = require('electron'); const {homedir} = require('os');
const { homedir } = require('os'); const {readFileSync, writeFileSync} = require('fs');
const { resolve } = require('path'); const {resolve} = require('path');
const { readFileSync, writeFileSync } = require('fs');
const gaze = require('gaze');
const vm = require('vm'); const vm = require('vm');
const {dialog} = require('electron');
const gaze = require('gaze');
const notify = require('./notify'); const notify = require('./notify');
const path = resolve(homedir(), '.hyperterm.js'); const path = resolve(homedir(), '.hyperterm.js');
@ -11,14 +12,16 @@ const watchers = [];
let cfg = {}; let cfg = {};
function watch () { function watch() {
gaze(path, function (err) { gaze(path, function (err) {
if (err) throw err; if (err) {
throw err;
}
this.on('changed', () => { this.on('changed', () => {
try { try {
if (exec(readFileSync(path, 'utf8'))) { if (exec(readFileSync(path, 'utf8'))) {
notify('HyperTerm configuration reloaded!'); notify('HyperTerm configuration reloaded!');
watchers.forEach((fn) => fn()); watchers.forEach(fn => fn());
} }
} catch (err) { } catch (err) {
dialog.showMessageBox({ dialog.showMessageBox({
@ -31,12 +34,14 @@ function watch () {
} }
let _str; // last script let _str; // last script
function exec (str) { function exec(str) {
if (str === _str) return false; if (str === _str) {
return false;
}
_str = str; _str = str;
const script = new vm.Script(str); const script = new vm.Script(str);
const module = {}; const module = {};
script.runInNewContext({ module }); script.runInNewContext({module});
if (!module.exports) { if (!module.exports) {
throw new Error('Error reading configuration: `module.exports` not set'); throw new Error('Error reading configuration: `module.exports` not set');
} }

View file

@ -1,21 +1,28 @@
const { app, BrowserWindow, shell, Menu } = require('electron'); // Native
const createRPC = require('./rpc'); const {resolve} = require('path');
const createMenu = require('./menu');
// Packages
const {parse: parseUrl} = require('url');
const {gitDescribe} = require('git-describe');
const {app, BrowserWindow, shell, Menu} = require('electron');
const uuid = require('uuid'); const uuid = require('uuid');
const { resolve } = require('path');
const { parse: parseUrl } = require('url');
const fileUriToPath = require('file-uri-to-path'); const fileUriToPath = require('file-uri-to-path');
const isDev = require('electron-is-dev'); const isDev = require('electron-is-dev');
// Ours
const AutoUpdater = require('./auto-updater'); const AutoUpdater = require('./auto-updater');
const toElectronBackgroundColor = require('./utils/to-electron-background-color'); const toElectronBackgroundColor = require('./utils/to-electron-background-color');
const createMenu = require('./menu');
const createRPC = require('./rpc');
const notify = require('./notify'); const notify = require('./notify');
const { gitDescribe } = require('git-describe');
app.commandLine.appendSwitch('js-flags', '--harmony'); app.commandLine.appendSwitch('js-flags', '--harmony');
// set up config // set up config
const config = require('./config'); const config = require('./config');
config.init(); config.init();
const plugins = require('./plugins'); const plugins = require('./plugins');
const Session = require('./session'); const Session = require('./session');
@ -29,7 +36,9 @@ app.getWindows = () => new Set([...windowSet]); // return a clone
// function to retrive the last focused window in windowSet; // function to retrive the last focused window in windowSet;
// added to app object in order to expose it to plugins. // added to app object in order to expose it to plugins.
app.getLastFocusedWindow = () => { app.getLastFocusedWindow = () => {
if (!windowSet.size) return null; if (!windowSet.size) {
return null;
}
return Array.from(windowSet).reduce((lastWindow, win) => { return Array.from(windowSet).reduce((lastWindow, win) => {
return win.focusTime > lastWindow.focusTime ? win : lastWindow; return win.focusTime > lastWindow.focusTime ? win : lastWindow;
}); });
@ -40,7 +49,9 @@ if (isDev) {
// Overide default appVersion which is set from package.json // Overide default appVersion which is set from package.json
gitDescribe({customArguments: ['--tags']}, (error, gitInfo) => { gitDescribe({customArguments: ['--tags']}, (error, gitInfo) => {
if (!error) app.setVersion(gitInfo.raw); if (!error) {
app.setVersion(gitInfo.raw);
}
}); });
} else { } else {
console.log('running in prod mode'); console.log('running in prod mode');
@ -54,11 +65,11 @@ const url = 'file://' + resolve(
console.log('electron will open', url); console.log('electron will open', url);
app.on('ready', () => { app.on('ready', () => {
function createWindow (fn) { function createWindow(fn) {
let cfg = plugins.getDecoratedConfig(); let cfg = plugins.getDecoratedConfig();
const [width, height] = cfg.windowSize || [540, 380]; const [width, height] = cfg.windowSize || [540, 380];
const { screen } = require('electron'); const {screen} = require('electron');
let startX = 50; let startX = 50;
let startY = 50; let startY = 50;
@ -69,7 +80,7 @@ app.on('ready', () => {
const focusedWindow = BrowserWindow.getFocusedWindow() || app.getLastFocusedWindow(); const focusedWindow = BrowserWindow.getFocusedWindow() || app.getLastFocusedWindow();
if (focusedWindow) { if (focusedWindow) {
const points = focusedWindow.getPosition(); const points = focusedWindow.getPosition();
const currentScreen = screen.getDisplayNearestPoint({ x: points[0], y: points[1] }); const currentScreen = screen.getDisplayNearestPoint({x: points[0], y: points[1]});
const biggestX = ((points[0] + 100 + width) - currentScreen.bounds.x); const biggestX = ((points[0] + 100 + width) - currentScreen.bounds.x);
const biggestY = ((points[1] + 100 + height) - currentScreen.bounds.y); const biggestY = ((points[1] + 100 + height) - currentScreen.bounds.y);
@ -138,7 +149,9 @@ app.on('ready', () => {
// If no callback is passed to createWindow, // If no callback is passed to createWindow,
// a new session will be created by default. // a new session will be created by default.
if (!fn) fn = (win) => win.rpc.emit('session add req'); if (!fn) {
fn = win => win.rpc.emit('session add req');
}
// app.windowCallback is the createWindow callback // app.windowCallback is the createWindow callback
// that can be setted before the 'ready' app event // that can be setted before the 'ready' app event
@ -155,11 +168,11 @@ app.on('ready', () => {
} }
}); });
rpc.on('new', ({ rows = 40, cols = 100, cwd = process.env.HOME }) => { rpc.on('new', ({rows = 40, cols = 100, cwd = process.env.HOME}) => {
const shell = cfg.shell; const shell = cfg.shell;
const shellArgs = cfg.shellArgs && Array.from(cfg.shellArgs); const shellArgs = cfg.shellArgs && Array.from(cfg.shellArgs);
initSession({ rows, cols, cwd, shell, shellArgs }, (uid, session) => { initSession({rows, cols, cwd, shell, shellArgs}, (uid, session) => {
sessions.set(uid, session); sessions.set(uid, session);
rpc.emit('session add', { rpc.emit('session add', {
uid, uid,
@ -167,17 +180,17 @@ app.on('ready', () => {
pid: session.pty.pid pid: session.pty.pid
}); });
session.on('data', (data) => { session.on('data', data => {
rpc.emit('session data', { uid, data }); rpc.emit('session data', {uid, data});
}); });
session.on('title', (title) => { session.on('title', title => {
win.setTitle(title); win.setTitle(title);
rpc.emit('session title', { uid, title }); rpc.emit('session title', {uid, title});
}); });
session.on('exit', () => { session.on('exit', () => {
rpc.emit('session exit', { uid }); rpc.emit('session exit', {uid});
sessions.delete(uid); sessions.delete(uid);
}); });
}); });
@ -186,7 +199,7 @@ app.on('ready', () => {
// TODO: this goes away when we are able to poll // TODO: this goes away when we are able to poll
// for the title ourseleves, instead of relying // for the title ourseleves, instead of relying
// on Session and focus/blur to subscribe // on Session and focus/blur to subscribe
rpc.on('focus', ({ uid }) => { rpc.on('focus', ({uid}) => {
const session = sessions.get(uid); const session = sessions.get(uid);
if (typeof session !== 'undefined' && typeof session.lastTitle !== 'undefined') { if (typeof session !== 'undefined' && typeof session.lastTitle !== 'undefined') {
win.setTitle(session.lastTitle); win.setTitle(session.lastTitle);
@ -197,7 +210,7 @@ app.on('ready', () => {
console.log('session not found by', uid); console.log('session not found by', uid);
} }
}); });
rpc.on('blur', ({ uid }) => { rpc.on('blur', ({uid}) => {
const session = sessions.get(uid); const session = sessions.get(uid);
if (session) { if (session) {
@ -207,7 +220,7 @@ app.on('ready', () => {
} }
}); });
rpc.on('exit', ({ uid }) => { rpc.on('exit', ({uid}) => {
const session = sessions.get(uid); const session = sessions.get(uid);
if (session) { if (session) {
@ -225,17 +238,17 @@ app.on('ready', () => {
win.maximize(); win.maximize();
}); });
rpc.on('resize', ({ cols, rows }) => { rpc.on('resize', ({cols, rows}) => {
sessions.forEach((session) => { sessions.forEach(session => {
session.resize({ cols, rows }); session.resize({cols, rows});
}); });
}); });
rpc.on('data', ({ uid, data }) => { rpc.on('data', ({uid, data}) => {
sessions.get(uid).write(data); sessions.get(uid).write(data);
}); });
rpc.on('open external', ({ url }) => { rpc.on('open external', ({url}) => {
shell.openExternal(url); shell.openExternal(url);
}); });
@ -263,11 +276,11 @@ app.on('ready', () => {
// If file is dropped onto the terminal window, navigate event is prevented // If file is dropped onto the terminal window, navigate event is prevented
// and his path is added to active session. // and his path is added to active session.
win.webContents.on('will-navigate', (event, url) => { win.webContents.on('will-navigate', (event, url) => {
var protocol = typeof url === 'string' && parseUrl(url).protocol; const protocol = typeof url === 'string' && parseUrl(url).protocol;
if (protocol === 'file:') { if (protocol === 'file:') {
event.preventDefault(); event.preventDefault();
let path = fileUriToPath(url).replace(/ /g, '\\ '); const path = fileUriToPath(url).replace(/ /g, '\\ ');
rpc.emit('session data send', { data: path }); rpc.emit('session data send', {data: path});
} }
}); });
@ -282,7 +295,7 @@ app.on('ready', () => {
// load plugins // load plugins
load(); load();
const pluginsUnsubscribe = plugins.subscribe((err) => { const pluginsUnsubscribe = plugins.subscribe(err => {
if (!err) { if (!err) {
load(); load();
win.webContents.send('plugins change'); win.webContents.send('plugins change');
@ -337,16 +350,18 @@ app.on('ready', () => {
const tpl = plugins.decorateMenu(createMenu({ const tpl = plugins.decorateMenu(createMenu({
createWindow, createWindow,
updatePlugins: () => { updatePlugins: () => {
plugins.updatePlugins({ force: true }); plugins.updatePlugins({force: true});
} }
})); }));
// If we're on Mac make a Dock Menu // If we're on Mac make a Dock Menu
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const { app, Menu } = require('electron'); const dockMenu = Menu.buildFromTemplate([{
const dockMenu = Menu.buildFromTemplate([ label: 'New Window',
{label: 'New Window', click () { createWindow(); }} click() {
]); createWindow();
}
}]);
app.dock.setMenu(dockMenu); app.dock.setMenu(dockMenu);
} }
@ -362,16 +377,16 @@ app.on('ready', () => {
plugins.subscribe(load); plugins.subscribe(load);
}); });
function initSession (opts, fn) { function initSession(opts, fn) {
fn(uuid.v4(), new Session(opts)); fn(uuid.v4(), new Session(opts));
} }
app.on('open-file', (event, path) => { app.on('open-file', (event, path) => {
const lastWindow = app.getLastFocusedWindow(); const lastWindow = app.getLastFocusedWindow();
const callback = win => win.rpc.emit('open file', { path }); const callback = win => win.rpc.emit('open file', {path});
if (lastWindow) { if (lastWindow) {
callback(lastWindow); callback(lastWindow);
} else if (!lastWindow && app.hasOwnProperty('createWindow')) { } else if (!lastWindow && {}.hasOwnProperty.call(app, 'createWindow')) {
app.createWindow(callback); app.createWindow(callback);
} else { } else {
// if createWindow not exists yet ('ready' event was not fired), // if createWindow not exists yet ('ready' event was not fired),

View file

@ -1,12 +1,13 @@
const os = require('os'); const os = require('os');
const path = require('path'); const path = require('path');
const { app, shell, dialog } = require('electron'); const {app, shell, dialog} = require('electron');
const appName = app.getName(); const appName = app.getName();
// based on and inspired by // based on and inspired by
// https://github.com/sindresorhus/anatine/blob/master/menu.js // https://github.com/sindresorhus/anatine/blob/master/menu.js
module.exports = function createMenu ({ createWindow, updatePlugins }) { module.exports = function createMenu({createWindow, updatePlugins}) {
return [ return [
{ {
label: 'Application', label: 'Application',
@ -20,7 +21,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
{ {
label: 'Preferences...', label: 'Preferences...',
accelerator: 'Cmd+,', accelerator: 'Cmd+,',
click (item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('preferences'); focusedWindow.rpc.emit('preferences');
} else { } else {
@ -61,14 +62,14 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
{ {
label: 'New Window', label: 'New Window',
accelerator: 'CmdOrCtrl+N', accelerator: 'CmdOrCtrl+N',
click (item, focusedWindow) { click() {
createWindow(); createWindow();
} }
}, },
{ {
label: 'New Tab', label: 'New Tab',
accelerator: 'CmdOrCtrl+T', accelerator: 'CmdOrCtrl+T',
click (item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('session add req'); focusedWindow.rpc.emit('session add req');
} else { } else {
@ -82,7 +83,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
{ {
label: 'Close', label: 'Close',
accelerator: 'CmdOrCtrl+W', accelerator: 'CmdOrCtrl+W',
click (item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('session close req'); focusedWindow.rpc.emit('session close req');
} }
@ -125,7 +126,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
{ {
label: 'Clear', label: 'Clear',
accelerator: 'CmdOrCtrl+K', accelerator: 'CmdOrCtrl+K',
click (item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('session clear req'); focusedWindow.rpc.emit('session clear req');
} }
@ -139,7 +140,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
{ {
label: 'Reload', label: 'Reload',
accelerator: 'CmdOrCtrl+R', accelerator: 'CmdOrCtrl+R',
click (item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('reload'); focusedWindow.rpc.emit('reload');
} }
@ -148,7 +149,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
{ {
label: 'Full Reload', label: 'Full Reload',
accelerator: 'CmdOrCtrl+Shift+R', accelerator: 'CmdOrCtrl+Shift+R',
click (item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.reload(); focusedWindow.reload();
} }
@ -157,7 +158,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
{ {
label: 'Toggle Developer Tools', label: 'Toggle Developer Tools',
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
click (item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.webContents.toggleDevTools(); focusedWindow.webContents.toggleDevTools();
} }
@ -169,7 +170,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
{ {
label: 'Reset Zoom Level', label: 'Reset Zoom Level',
accelerator: 'CmdOrCtrl+0', accelerator: 'CmdOrCtrl+0',
click (item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('reset fontSize req'); focusedWindow.rpc.emit('reset fontSize req');
} }
@ -178,7 +179,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
{ {
label: 'Zoom In', label: 'Zoom In',
accelerator: 'CmdOrCtrl+plus', accelerator: 'CmdOrCtrl+plus',
click (item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('increase fontSize req'); focusedWindow.rpc.emit('increase fontSize req');
} }
@ -187,7 +188,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
{ {
label: 'Zoom Out', label: 'Zoom Out',
accelerator: 'CmdOrCtrl+-', accelerator: 'CmdOrCtrl+-',
click (item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('decrease fontSize req'); focusedWindow.rpc.emit('decrease fontSize req');
} }
@ -201,7 +202,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
{ {
label: 'Update All Now', label: 'Update All Now',
accelerator: 'CmdOrCtrl+Shift+U', accelerator: 'CmdOrCtrl+Shift+U',
click () { click() {
updatePlugins(); updatePlugins();
} }
} }
@ -222,7 +223,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
{ {
label: 'Show Previous Tab', label: 'Show Previous Tab',
accelerator: 'CmdOrCtrl+Option+Left', accelerator: 'CmdOrCtrl+Option+Left',
click (item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('move left req'); focusedWindow.rpc.emit('move left req');
} }
@ -231,7 +232,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
{ {
label: 'Show Next Tab', label: 'Show Next Tab',
accelerator: 'CmdOrCtrl+Option+Right', accelerator: 'CmdOrCtrl+Option+Right',
click (item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('move right req'); focusedWindow.rpc.emit('move right req');
} }
@ -253,13 +254,13 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
submenu: [ submenu: [
{ {
label: `${appName} Website`, label: `${appName} Website`,
click () { click() {
shell.openExternal('https://hyperterm.now.sh'); shell.openExternal('https://hyperterm.now.sh');
} }
}, },
{ {
label: 'Report an Issue...', label: 'Report an Issue...',
click () { click() {
const body = ` const body = `
<!-- Please succinctly describe your issue and steps to reproduce it. --> <!-- Please succinctly describe your issue and steps to reproduce it. -->
@ -273,12 +274,13 @@ ${process.platform} ${process.arch} ${os.release()}`;
} }
}, },
...( ...(
'darwin' !== process.platform process.platform === 'darwin' ?
? [ [] :
{ type: 'separator' }, [
{type: 'separator'},
{ {
role: 'about', role: 'about',
click () { click() {
dialog.showMessageBox({ dialog.showMessageBox({
title: `About ${appName}`, title: `About ${appName}`,
message: `${appName} ${app.getVersion()}`, message: `${appName} ${app.getVersion()}`,
@ -289,7 +291,6 @@ ${process.platform} ${process.arch} ${os.release()}`;
} }
} }
] ]
: []
) )
] ]
} }

View file

@ -1,6 +1,7 @@
const { app, BrowserWindow } = require('electron'); const {resolve} = require('path');
const {app, BrowserWindow} = require('electron');
const isDev = require('electron-is-dev'); const isDev = require('electron-is-dev');
const { resolve } = require('path');
let win; let win;
@ -29,10 +30,10 @@ app.on('ready', () => {
}); });
}); });
function notify (title, body) { function notify(title, body) {
console.log(`[Notification] ${title}: ${body}`); console.log(`[Notification] ${title}: ${body}`);
if (win) { if (win) {
win.webContents.send('notification', { title, body }); win.webContents.send('notification', {title, body});
} else { } else {
buffer.push([title, body]); buffer.push([title, body]);
} }

View file

@ -1,15 +1,17 @@
const { app, dialog } = require('electron'); const {exec} = require('child_process');
const { homedir } = require('os'); const {homedir} = require('os');
const { resolve, basename } = require('path'); const {resolve, basename} = require('path');
const { writeFileSync } = require('fs'); const {writeFileSync} = require('fs');
const config = require('./config');
const { sync: mkdirpSync } = require('mkdirp'); const {app, dialog} = require('electron');
const { exec } = require('child_process'); const {sync: mkdirpSync} = require('mkdirp');
const Config = require('electron-config'); const Config = require('electron-config');
const ms = require('ms'); const ms = require('ms');
const notify = require('./notify');
const shellEnv = require('shell-env'); const shellEnv = require('shell-env');
const config = require('./config');
const notify = require('./notify');
// local storage // local storage
const cache = new Config(); const cache = new Config();
@ -34,7 +36,7 @@ let paths = getPaths(plugins);
let id = getId(plugins); let id = getId(plugins);
let modules = requirePlugins(); let modules = requirePlugins();
function getId (plugins_) { function getId(plugins_) {
return JSON.stringify(plugins_); return JSON.stringify(plugins_);
} }
@ -56,12 +58,14 @@ config.subscribe(() => {
let updating = false; let updating = false;
function updatePlugins ({ force = false } = {}) { function updatePlugins({force = false} = {}) {
if (updating) return notify('Plugin update in progress'); if (updating) {
return notify('Plugin update in progress');
}
updating = true; updating = true;
syncPackageJSON(); syncPackageJSON();
const id_ = id; const id_ = id;
install((err) => { install(err => {
updating = false; updating = false;
if (err) { if (err) {
@ -109,15 +113,15 @@ function updatePlugins ({ force = false } = {}) {
'No changes!' 'No changes!'
); );
} }
watchers.forEach((fn) => fn(err, { force })); watchers.forEach(fn => fn(err, {force}));
} }
} }
}); });
} }
function getPluginVersions () { function getPluginVersions() {
const paths_ = paths.plugins.concat(paths.localPlugins); const paths_ = paths.plugins.concat(paths.localPlugins);
return paths_.map((path) => { return paths_.map(path => {
let version = null; let version = null;
try { try {
version = require(resolve(path, 'package.json')).version; version = require(resolve(path, 'package.json')).version;
@ -129,10 +133,12 @@ function getPluginVersions () {
}); });
} }
function clearCache (mod) { function clearCache() {
// trigger unload hooks // trigger unload hooks
modules.forEach((mod) => { modules.forEach(mod => {
if (mod.onUnload) mod.onUnload(app); if (mod.onUnload) {
mod.onUnload(app);
}
}); });
// clear require cache // clear require cache
@ -159,7 +165,7 @@ if (cache.get('hyperterm.plugins') !== id || process.env.HYPERTERM_FORCE_UPDATE)
// otherwise update plugins every 5 hours // otherwise update plugins every 5 hours
setInterval(updatePlugins, ms('5h')); setInterval(updatePlugins, ms('5h'));
function syncPackageJSON () { function syncPackageJSON() {
const dependencies = toDependencies(plugins); const dependencies = toDependencies(plugins);
const pkg = { const pkg = {
name: 'hyperterm-plugins', name: 'hyperterm-plugins',
@ -180,16 +186,16 @@ function syncPackageJSON () {
} }
} }
function alert (message) { function alert(message) {
dialog.showMessageBox({ dialog.showMessageBox({
message, message,
buttons: ['Ok'] buttons: ['Ok']
}); });
} }
function toDependencies (plugins) { function toDependencies(plugins) {
const obj = {}; const obj = {};
plugins.plugins.forEach((plugin) => { plugins.plugins.forEach(plugin => {
const regex = /.(@|#)/; const regex = /.(@|#)/;
const match = regex.exec(plugin); const match = regex.exec(plugin);
@ -207,22 +213,28 @@ function toDependencies (plugins) {
return obj; return obj;
} }
function install (fn) { function install(fn) {
const { shell: cfgShell, npmRegistry } = exports.getDecoratedConfig(); const {shell: cfgShell, npmRegistry} = exports.getDecoratedConfig();
const shell = cfgShell && cfgShell !== '' ? cfgShell : undefined; const shell = cfgShell && cfgShell !== '' ? cfgShell : undefined;
shellEnv(shell).then((env) => { shellEnv(shell).then(env => {
if (npmRegistry) env.NPM_CONFIG_REGISTRY = npmRegistry; if (npmRegistry) {
env.NPM_CONFIG_REGISTRY = npmRegistry;
}
/* eslint-disable camelcase */
env.npm_config_runtime = 'electron'; env.npm_config_runtime = 'electron';
env.npm_config_target = '1.3.0'; env.npm_config_target = '1.3.0';
env.npm_config_disturl = 'https://atom.io/download/atom-shell'; env.npm_config_disturl = 'https://atom.io/download/atom-shell';
/* eslint-enable camelcase */
exec('npm prune; npm install --production', { exec('npm prune; npm install --production', {
cwd: path, cwd: path,
env, env,
shell shell
}, (err, stdout, stderr) => { }, err => {
if (err) return fn(err); if (err) {
return fn(err);
}
fn(null); fn(null);
}); });
}).catch(fn); }).catch(fn);
@ -235,12 +247,12 @@ exports.subscribe = function (fn) {
}; };
}; };
function getPaths () { function getPaths() {
return { return {
plugins: plugins.plugins.map((name) => { plugins: plugins.plugins.map(name => {
return resolve(path, 'node_modules', name.split('#')[0]); return resolve(path, 'node_modules', name.split('#')[0]);
}), }),
localPlugins: plugins.localPlugins.map((name) => { localPlugins: plugins.localPlugins.map(name => {
return resolve(localPath, name); return resolve(localPath, name);
}) })
}; };
@ -251,13 +263,13 @@ exports.getPaths = getPaths;
// get paths from renderer // get paths from renderer
exports.getBasePaths = function () { exports.getBasePaths = function () {
return { path, localPath }; return {path, localPath};
}; };
function requirePlugins () { function requirePlugins() {
const { plugins, localPlugins } = paths; const {plugins, localPlugins} = paths;
const load = (path) => { const load = path => {
let mod; let mod;
try { try {
mod = require(path); mod = require(path);
@ -279,11 +291,11 @@ function requirePlugins () {
return plugins.map(load) return plugins.map(load)
.concat(localPlugins.map(load)) .concat(localPlugins.map(load))
.filter(v => !!v); .filter(v => Boolean(v));
} }
exports.onApp = function (app) { exports.onApp = function (app) {
modules.forEach((plugin) => { modules.forEach(plugin => {
if (plugin.onApp) { if (plugin.onApp) {
plugin.onApp(app); plugin.onApp(app);
} }
@ -291,7 +303,7 @@ exports.onApp = function (app) {
}; };
exports.onWindow = function (win) { exports.onWindow = function (win) {
modules.forEach((plugin) => { modules.forEach(plugin => {
if (plugin.onWindow) { if (plugin.onWindow) {
plugin.onWindow(win); plugin.onWindow(win);
} }
@ -300,12 +312,12 @@ exports.onWindow = function (win) {
// decorates the base object by calling plugin[key] // decorates the base object by calling plugin[key]
// for all the available plugins // for all the available plugins
function decorateObject (base, key) { function decorateObject(base, key) {
let decorated = base; let decorated = base;
modules.forEach((plugin) => { modules.forEach(plugin => {
if (plugin[key]) { if (plugin[key]) {
const res = plugin[key](decorated); const res = plugin[key](decorated);
if (res && 'object' === typeof res) { if (res && typeof res === 'object') {
decorated = res; decorated = res;
} else { } else {
notify('Plugin error!', `"${plugin._name}": invalid return type for \`${key}\``); notify('Plugin error!', `"${plugin._name}": invalid return type for \`${key}\``);

View file

@ -1,15 +1,17 @@
const { EventEmitter } = require('events'); const {EventEmitter} = require('events');
const { ipcMain } = require('electron'); const {ipcMain} = require('electron');
const uuid = require('uuid'); const uuid = require('uuid');
class Server extends EventEmitter { class Server extends EventEmitter {
constructor (win) { constructor(win) {
super(); super();
this.win = win; this.win = win;
this.ipcListener = this.ipcListener.bind(this); this.ipcListener = this.ipcListener.bind(this);
if (this.destroyed) return; if (this.destroyed) {
return;
}
const uid = uuid.v4(); const uid = uuid.v4();
this.id = uid; this.id = uid;
@ -24,19 +26,19 @@ class Server extends EventEmitter {
}); });
} }
get wc () { get wc() {
return this.win.webContents; return this.win.webContents;
} }
ipcListener (event, { ev, data }) { ipcListener(event, {ev, data}) {
super.emit(ev, data); super.emit(ev, data);
} }
emit (ch, data) { emit(ch, data) {
this.wc.send(this.id, { ch, data }); this.wc.send(this.id, {ch, data});
} }
destroy () { destroy() {
this.removeAllListeners(); this.removeAllListeners();
this.wc.removeAllListeners(); this.wc.removeAllListeners();
if (this.id) { if (this.id) {
@ -49,6 +51,6 @@ class Server extends EventEmitter {
} }
module.exports = function createRPC (win) { module.exports = function createRPC(win) {
return new Server(win); return new Server(win);
}; };

View file

@ -1,9 +1,11 @@
const { app } = require('electron'); const {exec} = require('child_process');
const { EventEmitter } = require('events'); const {EventEmitter} = require('events');
const { exec } = require('child_process');
const {app} = require('electron');
const defaultShell = require('default-shell'); const defaultShell = require('default-shell');
const { getDecoratedEnv } = require('./plugins');
const { productName, version } = require('./package'); const {getDecoratedEnv} = require('./plugins');
const {productName, version} = require('./package');
const config = require('./config'); const config = require('./config');
let spawn; let spawn;
@ -24,7 +26,7 @@ const envFromConfig = config.getConfig().env || {};
module.exports = class Session extends EventEmitter { module.exports = class Session extends EventEmitter {
constructor ({ rows, cols: columns, cwd, shell, shellArgs }) { constructor({rows, cols: columns, cwd, shell, shellArgs}) {
super(); super();
const baseEnv = Object.assign({}, process.env, { const baseEnv = Object.assign({}, process.env, {
LANG: app.getLocale().replace('-', '_') + '.UTF-8', LANG: app.getLocale().replace('-', '_') + '.UTF-8',
@ -42,7 +44,7 @@ module.exports = class Session extends EventEmitter {
env: getDecoratedEnv(baseEnv) env: getDecoratedEnv(baseEnv)
}); });
this.pty.stdout.on('data', (data) => { this.pty.stdout.on('data', data => {
if (this.ended) { if (this.ended) {
return; return;
} }
@ -60,19 +62,24 @@ module.exports = class Session extends EventEmitter {
this.getTitle(); this.getTitle();
} }
focus () { focus() {
this.subscribed = true; this.subscribed = true;
this.getTitle(); this.getTitle();
} }
blur () { blur() {
this.subscribed = false; this.subscribed = false;
clearTimeout(this.titlePoll); clearTimeout(this.titlePoll);
} }
getTitle () { getTitle() {
if ('win32' === process.platform) return; if (process.platform === 'win32') {
if (this.fetching) return; return;
}
if (this.fetching) {
return;
}
this.fetching = true; this.fetching = true;
let tty = this.pty.stdout.ttyname; let tty = this.pty.stdout.ttyname;
@ -86,8 +93,12 @@ module.exports = class Session extends EventEmitter {
// TODO: only tested on mac // TODO: only tested on mac
exec(`ps uxac | grep ${tty} | head -n 1`, (err, out) => { exec(`ps uxac | grep ${tty} | head -n 1`, (err, out) => {
this.fetching = false; this.fetching = false;
if (this.ended) return; if (this.ended) {
if (err) return; return;
}
if (err) {
return;
}
let title = out.split(' ').pop(); let title = out.split(' ').pop();
if (title) { if (title) {
title = title.replace(/^\(/, ''); title = title.replace(/^\(/, '');
@ -104,23 +115,23 @@ module.exports = class Session extends EventEmitter {
}); });
} }
exit () { exit() {
this.destroy(); this.destroy();
} }
write (data) { write(data) {
this.pty.stdin.write(data); this.pty.stdin.write(data);
} }
resize ({ cols: columns, rows }) { resize({cols: columns, rows}) {
try { try {
this.pty.stdout.resize({ columns, rows }); this.pty.stdout.resize({columns, rows});
} catch (err) { } catch (err) {
console.error(err.stack); console.error(err.stack);
} }
} }
destroy () { destroy() {
try { try {
this.pty.kill('SIGHUP'); this.pty.kill('SIGHUP');
} catch (err) { } catch (err) {

View file

@ -3,12 +3,11 @@ const Color = require('color');
// returns a background color that's in hex // returns a background color that's in hex
// format including the alpha channel (e.g.: `#00000050`) // format including the alpha channel (e.g.: `#00000050`)
// input can be any css value (rgb, hsl, string…) // input can be any css value (rgb, hsl, string…)
module.exports = function toElectronBackgroundColor (bgColor) { module.exports = function toElectronBackgroundColor(bgColor) {
const color = Color(bgColor); const color = Color(bgColor);
if (1 !== color.alpha()) { if (color.alpha() === 1) {
// (╯°□°)╯︵ ┻━┻
return '#' + Math.floor(color.alpha() * 100) + color.hexString().substr(1);
} else {
return color.hexString(); return color.hexString();
} }
// (╯°□°)╯︵ ┻━┻
return '#' + Math.floor(color.alpha() * 100) + color.hexString().substr(1);
}; };

View file

@ -1,13 +1,13 @@
import { CONFIG_LOAD, CONFIG_RELOAD } from '../constants/config'; import {CONFIG_LOAD, CONFIG_RELOAD} from '../constants/config';
export function loadConfig (config) { export function loadConfig(config) {
return { return {
type: CONFIG_LOAD, type: CONFIG_LOAD,
config config
}; };
} }
export function reloadConfig (config) { export function reloadConfig(config) {
return { return {
type: CONFIG_RELOAD, type: CONFIG_RELOAD,
config config

View file

@ -1,48 +1,49 @@
import { CLOSE_TAB, CHANGE_TAB } from '../constants/tabs'; import {CLOSE_TAB, CHANGE_TAB} from '../constants/tabs';
import { UI_WINDOW_MAXIMIZE, UI_WINDOW_UNMAXIMIZE } from '../constants/ui'; import {UI_WINDOW_MAXIMIZE, UI_WINDOW_UNMAXIMIZE} from '../constants/ui';
import { userExitSession, setActiveSession } from './sessions';
import rpc from '../rpc'; import rpc from '../rpc';
export function closeTab (uid) { import {userExitSession, setActiveSession} from './sessions';
return (dispatch, getState) => {
export function closeTab(uid) {
return dispatch => {
dispatch({ dispatch({
type: CLOSE_TAB, type: CLOSE_TAB,
uid, uid,
effect () { effect() {
dispatch(userExitSession(uid)); dispatch(userExitSession(uid));
} }
}); });
}; };
} }
export function changeTab (uid) { export function changeTab(uid) {
return (dispatch, getState) => { return dispatch => {
dispatch({ dispatch({
type: CHANGE_TAB, type: CHANGE_TAB,
uid, uid,
effect () { effect() {
dispatch(setActiveSession(uid)); dispatch(setActiveSession(uid));
} }
}); });
}; };
} }
export function maximize () { export function maximize() {
return (dispatch, getState) => { return dispatch => {
dispatch({ dispatch({
type: UI_WINDOW_MAXIMIZE, type: UI_WINDOW_MAXIMIZE,
effect () { effect() {
rpc.emit('maximize'); rpc.emit('maximize');
} }
}); });
}; };
} }
export function unmaximize () { export function unmaximize() {
return (dispatch, getState) => { return dispatch => {
dispatch({ dispatch({
type: UI_WINDOW_UNMAXIMIZE, type: UI_WINDOW_UNMAXIMIZE,
effect () { effect() {
rpc.emit('unmaximize'); rpc.emit('unmaximize');
} }
}); });

View file

@ -1,5 +1,5 @@
import rpc from '../rpc'; import rpc from '../rpc';
export function init () { export function init() {
rpc.emit('init'); rpc.emit('init');
} }

View file

@ -1,6 +1,6 @@
import { NOTIFICATION_DISMISS } from '../constants/notifications'; import {NOTIFICATION_DISMISS} from '../constants/notifications';
export function dismissNotification (id) { export function dismissNotification(id) {
return { return {
type: NOTIFICATION_DISMISS, type: NOTIFICATION_DISMISS,
id id

View file

@ -1,6 +1,6 @@
import rpc from '../rpc'; import rpc from '../rpc';
import getURL from '../utils/url-command'; import getURL from '../utils/url-command';
import { keys } from '../utils/object'; import {keys} from '../utils/object';
import { import {
SESSION_ADD, SESSION_ADD,
SESSION_RESIZE, SESSION_RESIZE,
@ -19,8 +19,8 @@ import {
SESSION_SET_PROCESS_TITLE SESSION_SET_PROCESS_TITLE
} from '../constants/sessions'; } from '../constants/sessions';
export function addSession (uid, shell, pid) { export function addSession(uid, shell, pid) {
return (dispatch, getState) => { return dispatch => {
dispatch({ dispatch({
type: SESSION_ADD, type: SESSION_ADD,
uid, uid,
@ -30,26 +30,26 @@ export function addSession (uid, shell, pid) {
}; };
} }
export function requestSession (uid) { export function requestSession() {
return (dispatch, getState) => { return (dispatch, getState) => {
const { ui } = getState(); const {ui} = getState();
const { cols, rows, cwd } = ui; const {cols, rows, cwd} = ui;
dispatch({ dispatch({
type: SESSION_REQUEST, type: SESSION_REQUEST,
effect: () => { effect: () => {
rpc.emit('new', { cols, rows, cwd }); rpc.emit('new', {cols, rows, cwd});
} }
}); });
}; };
} }
export function addSessionData (uid, data) { export function addSessionData(uid, data) {
return function (dispatch, getState) { return function (dispatch, getState) {
dispatch({ dispatch({
type: SESSION_ADD_DATA, type: SESSION_ADD_DATA,
data, data,
effect () { effect() {
const { shell } = getState().sessions.sessions[uid]; const {shell} = getState().sessions.sessions[uid];
const enterKey = Boolean(data.match(/\n/)); const enterKey = Boolean(data.match(/\n/));
const url = enterKey ? getURL(shell, data) : null; const url = enterKey ? getURL(shell, data) : null;
@ -72,12 +72,12 @@ export function addSessionData (uid, data) {
}; };
} }
export function sessionExit (uid) { export function sessionExit(uid) {
return (dispatch, getState) => { return (dispatch, getState) => {
return dispatch({ return dispatch({
type: SESSION_PTY_EXIT, type: SESSION_PTY_EXIT,
uid, uid,
effect () { effect() {
// we reiterate the same logic as below // we reiterate the same logic as below
// for SESSION_USER_EXIT since the exit // for SESSION_USER_EXIT since the exit
// could happen pty side or optimistic // could happen pty side or optimistic
@ -92,13 +92,13 @@ export function sessionExit (uid) {
// we want to distinguish an exit // we want to distinguish an exit
// that's UI initiated vs pty initiated // that's UI initiated vs pty initiated
export function userExitSession (uid) { export function userExitSession(uid) {
return (dispatch, getState) => { return (dispatch, getState) => {
return dispatch({ return dispatch({
type: SESSION_USER_EXIT, type: SESSION_USER_EXIT,
uid, uid,
effect () { effect() {
rpc.emit('exit', { uid }); rpc.emit('exit', {uid});
const sessions = keys(getState().sessions.sessions); const sessions = keys(getState().sessions.sessions);
if (!sessions.length) { if (!sessions.length) {
window.close(); window.close();
@ -108,11 +108,11 @@ export function userExitSession (uid) {
}; };
} }
export function userExitActiveSession () { export function userExitActiveSession() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({
type: SESSION_USER_EXIT_ACTIVE, type: SESSION_USER_EXIT_ACTIVE,
effect () { effect() {
const uid = getState().sessions.activeUid; const uid = getState().sessions.activeUid;
dispatch(userExitSession(uid)); dispatch(userExitSession(uid));
} }
@ -120,31 +120,33 @@ export function userExitActiveSession () {
}; };
} }
export function setActiveSession (uid) { export function setActiveSession(uid) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const prevUid = state.sessions.activeUid; const prevUid = state.sessions.activeUid;
dispatch({ dispatch({
type: SESSION_SET_ACTIVE, type: SESSION_SET_ACTIVE,
uid, uid,
effect () { effect() {
// TODO: this goes away when we are able to poll // TODO: this goes away when we are able to poll
// for the title ourseleves, instead of relying // for the title ourseleves, instead of relying
// on Session and focus/blur to subscribe // on Session and focus/blur to subscribe
if (prevUid) rpc.emit('blur', { uid: prevUid }); if (prevUid) {
rpc.emit('focus', { uid }); rpc.emit('blur', {uid: prevUid});
}
rpc.emit('focus', {uid});
} }
}); });
}; };
} }
export function clearActiveSession () { export function clearActiveSession() {
return { return {
type: SESSION_CLEAR_ACTIVE type: SESSION_CLEAR_ACTIVE
}; };
} }
export function setSessionProcessTitle (uid, title) { export function setSessionProcessTitle(uid, title) {
return { return {
type: SESSION_SET_PROCESS_TITLE, type: SESSION_SET_PROCESS_TITLE,
uid, uid,
@ -152,7 +154,7 @@ export function setSessionProcessTitle (uid, title) {
}; };
} }
export function setSessionXtermTitle (uid, title) { export function setSessionXtermTitle(uid, title) {
return { return {
type: SESSION_SET_XTERM_TITLE, type: SESSION_SET_XTERM_TITLE,
uid, uid,
@ -160,32 +162,32 @@ export function setSessionXtermTitle (uid, title) {
}; };
} }
export function resizeSession (uid, cols, rows) { export function resizeSession(uid, cols, rows) {
return { return {
type: SESSION_RESIZE, type: SESSION_RESIZE,
cols, cols,
rows, rows,
effect () { effect() {
rpc.emit('resize', { cols, rows }); rpc.emit('resize', {cols, rows});
} }
}; };
} }
export function sendSessionData (uid, data) { export function sendSessionData(uid, data) {
return function (dispatch, getState) { return function (dispatch, getState) {
dispatch({ dispatch({
type: SESSION_USER_DATA, type: SESSION_USER_DATA,
data, data,
effect () { effect() {
// If no uid is passed, data is sended to the active session. // If no uid is passed, data is sended to the active session.
const targetUid = uid || getState().sessions.activeUid; const targetUid = uid || getState().sessions.activeUid;
rpc.emit('data', { uid: targetUid, data }); rpc.emit('data', {uid: targetUid, data});
} }
}); });
}; };
} }
export function exitSessionBrowser (uid) { export function exitSessionBrowser(uid) {
return { return {
type: SESSION_URL_UNSET, type: SESSION_URL_UNSET,
uid uid

View file

@ -1,8 +1,8 @@
import * as shellEscape from 'php-escape-shell'; import * as shellEscape from 'php-escape-shell';
import { setActiveSession } from './sessions';
import { keys } from '../utils/object'; import {keys} from '../utils/object';
import { last } from '../utils/array'; import {last} from '../utils/array';
import { isExecutable } from '../utils/file'; import {isExecutable} from '../utils/file';
import notify from '../utils/notify'; import notify from '../utils/notify';
import rpc from '../rpc'; import rpc from '../rpc';
import { import {
@ -23,13 +23,15 @@ import {
UI_OPEN_FILE UI_OPEN_FILE
} from '../constants/ui'; } from '../constants/ui';
const { stat } = window.require('fs'); import {setActiveSession} from './sessions';
export function increaseFontSize () { const {stat} = window.require('fs');
export function increaseFontSize() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({
type: UI_FONT_SIZE_INCR, type: UI_FONT_SIZE_INCR,
effect () { effect() {
const state = getState(); const state = getState();
const old = state.ui.fontSizeOverride || state.ui.fontSize; const old = state.ui.fontSizeOverride || state.ui.fontSize;
const value = old + 1; const value = old + 1;
@ -42,11 +44,11 @@ export function increaseFontSize () {
}; };
} }
export function decreaseFontSize () { export function decreaseFontSize() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({
type: UI_FONT_SIZE_DECR, type: UI_FONT_SIZE_DECR,
effect () { effect() {
const state = getState(); const state = getState();
const old = state.ui.fontSizeOverride || state.ui.fontSize; const old = state.ui.fontSizeOverride || state.ui.fontSize;
const value = old - 1; const value = old - 1;
@ -59,19 +61,19 @@ export function decreaseFontSize () {
}; };
} }
export function resetFontSize () { export function resetFontSize() {
return { return {
type: UI_FONT_SIZE_RESET type: UI_FONT_SIZE_RESET
}; };
} }
export function setFontSmoothing () { export function setFontSmoothing() {
return (dispatch) => { return dispatch => {
setTimeout(() => { setTimeout(() => {
const devicePixelRatio = window.devicePixelRatio; const devicePixelRatio = window.devicePixelRatio;
const fontSmoothing = devicePixelRatio < 2 const fontSmoothing = devicePixelRatio < 2 ?
? 'subpixel-antialiased' 'subpixel-antialiased' :
: 'antialiased'; 'antialiased';
dispatch({ dispatch({
type: UI_FONT_SMOOTHING_SET, type: UI_FONT_SMOOTHING_SET,
@ -81,12 +83,12 @@ export function setFontSmoothing () {
}; };
} }
export function moveLeft () { export function moveLeft() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({
type: UI_MOVE_LEFT, type: UI_MOVE_LEFT,
effect () { effect() {
const { sessions } = getState(); const {sessions} = getState();
const uid = sessions.activeUid; const uid = sessions.activeUid;
const sessionUids = keys(sessions.sessions); const sessionUids = keys(sessions.sessions);
const index = sessionUids.indexOf(uid); const index = sessionUids.indexOf(uid);
@ -101,12 +103,12 @@ export function moveLeft () {
}; };
} }
export function moveRight () { export function moveRight() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({
type: UI_MOVE_RIGHT, type: UI_MOVE_RIGHT,
effect () { effect() {
const { sessions } = getState(); const {sessions} = getState();
const uid = sessions.activeUid; const uid = sessions.activeUid;
const sessionUids = keys(sessions.sessions); const sessionUids = keys(sessions.sessions);
const index = sessionUids.indexOf(uid); const index = sessionUids.indexOf(uid);
@ -121,36 +123,36 @@ export function moveRight () {
}; };
} }
export function moveTo (i) { export function moveTo(i) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({
type: UI_MOVE_TO, type: UI_MOVE_TO,
index: i, index: i,
effect () { effect() {
const { sessions } = getState(); const {sessions} = getState();
const uid = sessions.activeUid; const uid = sessions.activeUid;
const sessionUids = keys(sessions.sessions); const sessionUids = keys(sessions.sessions);
if (uid === sessionUids[i]) { if (uid === sessionUids[i]) {
console.log('ignoring same uid'); console.log('ignoring same uid');
} else if (null != sessionUids[i]) { } else if (sessionUids[i] === null) {
dispatch(setActiveSession(sessionUids[i]));
} else {
console.log('ignoring inexistent index', i); console.log('ignoring inexistent index', i);
} else {
dispatch(setActiveSession(sessionUids[i]));
} }
} }
}); });
}; };
} }
export function showPreferences () { export function showPreferences() {
const editorFallback = process.platform === 'win32' ? 'notepad' : 'nano'; const editorFallback = process.platform === 'win32' ? 'notepad' : 'nano';
return (dispatch, getState) => { return dispatch => {
dispatch({ dispatch({
type: UI_SHOW_PREFERENCES, type: UI_SHOW_PREFERENCES,
effect () { effect() {
dispatch(requestSession()); dispatch(requestSession());
// TODO: replace this hack with an async action // TODO: replace this hack with an async action
rpc.once('session add', ({ uid }) => { rpc.once('session add', ({uid}) => {
rpc.once('session data', () => { rpc.once('session data', () => {
dispatch(sendSessionData( dispatch(sendSessionData(
uid, uid,
@ -168,22 +170,22 @@ export function showPreferences () {
}; };
} }
export function windowMove () { export function windowMove() {
return (dispatch) => { return dispatch => {
dispatch({ dispatch({
type: UI_WINDOW_MOVE, type: UI_WINDOW_MOVE,
effect () { effect() {
dispatch(setFontSmoothing()); dispatch(setFontSmoothing());
} }
}); });
}; };
} }
export function openFile (path) { export function openFile(path) {
return (dispatch, getState) => { return dispatch => {
dispatch({ dispatch({
type: UI_OPEN_FILE, type: UI_OPEN_FILE,
effect () { effect() {
stat(path, (err, stats) => { stat(path, (err, stats) => {
if (err) { if (err) {
console.error(err.stack); console.error(err.stack);
@ -198,7 +200,7 @@ export function openFile (path) {
} else if (stats.isFile() && isExecutable(stats)) { } else if (stats.isFile() && isExecutable(stats)) {
command += '\n'; command += '\n';
} }
rpc.once('session add', ({ uid }) => { rpc.once('session add', ({uid}) => {
rpc.once('session data', () => { rpc.once('session data', () => {
dispatch(sendSessionData(uid, command)); dispatch(sendSessionData(uid, command));
}); });

View file

@ -4,7 +4,7 @@ import {
} from '../constants/updater'; } from '../constants/updater';
import rpc from '../rpc'; import rpc from '../rpc';
export function installUpdate () { export function installUpdate() {
return { return {
type: UPDATE_INSTALL, type: UPDATE_INSTALL,
effect: () => { effect: () => {
@ -13,7 +13,7 @@ export function installUpdate () {
}; };
} }
export function updateAvailable (version, notes) { export function updateAvailable(version, notes) {
return { return {
type: UPDATE_AVAILABLE, type: UPDATE_AVAILABLE,
version, version,

View file

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { StyleSheet, css } from 'aphrodite-simple'; import {StyleSheet, css} from 'aphrodite-simple';
import { shouldComponentUpdate } from 'react-addons-pure-render-mixin'; import {shouldComponentUpdate} from 'react-addons-pure-render-mixin';
export default class Component extends React.Component { export default class Component extends React.Component {
constructor () { constructor() {
super(); super();
this.styles_ = this.createStyleSheet(); this.styles_ = this.createStyleSheet();
this.cssHelper = this.cssHelper.bind(this); this.cssHelper = this.cssHelper.bind(this);
@ -13,14 +13,14 @@ export default class Component extends React.Component {
} }
} }
createStyleSheet () { createStyleSheet() {
if (!this.styles) { if (!this.styles) {
return {}; return {};
} }
const styles = this.styles(); const styles = this.styles();
if ('object' !== typeof styles) { if (typeof styles !== 'object') {
throw new TypeError('Component `styles` returns a non-object'); throw new TypeError('Component `styles` returns a non-object');
} }
@ -34,9 +34,9 @@ export default class Component extends React.Component {
// or user agent extensions // or user agent extensions
// - the user doesn't need to keep track of both `css` // - the user doesn't need to keep track of both `css`
// and `style`, and we make that whole ordeal easier // and `style`, and we make that whole ordeal easier
cssHelper (...args) { cssHelper(...args) {
const classes = args const classes = args
.map((c) => { .map(c => {
if (c) { if (c) {
// we compute the global name from the given // we compute the global name from the given
// css class and we prepend the component name // css class and we prepend the component name
@ -48,19 +48,20 @@ export default class Component extends React.Component {
const globalName = `${component}_${c}`; const globalName = `${component}_${c}`;
return [globalName, css(this.styles_[c])]; return [globalName, css(this.styles_[c])];
} }
return null;
}) })
// skip nulls // skip nulls
.filter((v) => !!v) .filter(v => Boolean(v))
// flatten // flatten
.reduce((a, b) => a.concat(b)); .reduce((a, b) => a.concat(b));
return classes.length ? classes.join(' ') : null; return classes.length ? classes.join(' ') : null;
} }
render () { render() {
// convert static objects from `babel-plugin-transform-jsx` // convert static objects from `babel-plugin-transform-jsx`
// to `React.Element`. // to `React.Element`.
if (!this.template) { if (!this.template) {
throw new TypeError("Component doesn't define `template`"); throw new TypeError('Component doesn\'t define `template`');
} }
// invoke the template creator passing our css helper // invoke the template creator passing our css helper

View file

@ -1,20 +1,22 @@
import React from 'react'; import React from 'react';
import Tabs_ from './tabs';
import Component from '../component'; import Component from '../component';
import { decorate, getTabsProps } from '../utils/plugins'; import {decorate, getTabsProps} from '../utils/plugins';
import Tabs_ from './tabs';
const Tabs = decorate(Tabs_, 'Tabs'); const Tabs = decorate(Tabs_, 'Tabs');
export default class Header extends Component { export default class Header extends Component {
constructor () { constructor() {
super(); super();
this.onChangeIntent = this.onChangeIntent.bind(this); this.onChangeIntent = this.onChangeIntent.bind(this);
this.onHeaderClick = this.onHeaderClick.bind(this); this.handleHeaderClick = this.handleHeaderClick.bind(this);
this.onHeaderMouseDown = this.onHeaderMouseDown.bind(this); this.handleHeaderMouseDown = this.handleHeaderMouseDown.bind(this);
} }
onChangeIntent (active) { onChangeIntent(active) {
// we ignore clicks if they're a byproduct of a drag // we ignore clicks if they're a byproduct of a drag
// motion to move the window // motion to move the window
if (window.screenX !== this.headerMouseDownWindowX || if (window.screenX !== this.headerMouseDownWindowX ||
@ -25,12 +27,12 @@ export default class Header extends Component {
this.props.onChangeTab(active); this.props.onChangeTab(active);
} }
onHeaderMouseDown () { handleHeaderMouseDown() {
this.headerMouseDownWindowX = window.screenX; this.headerMouseDownWindowX = window.screenX;
this.headerMouseDownWindowY = window.screenY; this.headerMouseDownWindowY = window.screenY;
} }
onHeaderClick (event) { handleHeaderClick(event) {
this.clicks = this.clicks || 0; this.clicks = this.clicks || 0;
// Reset clicks if mouse moved between clicks // Reset clicks if mouse moved between clicks
@ -59,30 +61,31 @@ export default class Header extends Component {
} }
} }
componentWillUnmount () { componentWillUnmount() {
delete this.clicks; delete this.clicks;
clearTimeout(this.clickTimer); clearTimeout(this.clickTimer);
} }
template (css) { template(css) {
const { isMac } = this.props; const {isMac} = this.props;
const props = getTabsProps(this.props, { const props = getTabsProps(this.props, {
tabs: this.props.tabs, tabs: this.props.tabs,
borderColor: this.props.borderColor, borderColor: this.props.borderColor,
onClose: this.props.onCloseTab, onClose: this.props.onCloseTab,
onChange: this.onChangeIntent onChange: this.onChangeIntent
}); });
return <header return (<header
className={ css('header', isMac && 'headerRounded') } className={css('header', isMac && 'headerRounded')}
onClick={ this.onHeaderClick } onClick={this.handleHeaderClick}
onMouseDown={ this.onHeaderMouseDown }> onMouseDown={this.handleHeaderMouseDown}
{ this.props.customChildrenBefore } >
<Tabs {...props} /> { this.props.customChildrenBefore }
{ this.props.customChildren } <Tabs {...props}/>
</header>; { this.props.customChildren }
</header>);
} }
styles () { styles() {
return { return {
header: { header: {
position: 'fixed', position: 'fixed',

View file

@ -3,22 +3,22 @@ import Component from '../component';
export default class Notification extends Component { export default class Notification extends Component {
constructor () { constructor() {
super(); super();
this.state = { this.state = {
dismissing: false dismissing: false
}; };
this.dismiss = this.dismiss.bind(this); this.handleDismiss = this.handleDismiss.bind(this);
this.onElement = this.onElement.bind(this); this.onElement = this.onElement.bind(this);
} }
componentDidMount () { componentDidMount() {
if (this.props.dismissAfter) { if (this.props.dismissAfter) {
this.setDismissTimer(); this.setDismissTimer();
} }
} }
componentWillReceiveProps (next) { componentWillReceiveProps(next) {
// if we have a timer going and the notification text // if we have a timer going and the notification text
// changed we reset the timer // changed we reset the timer
if (next.text !== this.props.text) { if (next.text !== this.props.text) {
@ -26,23 +26,23 @@ export default class Notification extends Component {
this.resetDismissTimer(); this.resetDismissTimer();
} }
if (this.state.dismissing) { if (this.state.dismissing) {
this.setState({ dismissing: false }); this.setState({dismissing: false});
} }
} }
} }
dismiss () { handleDismiss() {
this.setState({ dismissing: true }); this.setState({dismissing: true});
} }
onElement (el) { onElement(el) {
if (el) { if (el) {
el.addEventListener('webkitTransitionEnd', () => { el.addEventListener('webkitTransitionEnd', () => {
if (this.state.dismissing) { if (this.state.dismissing) {
this.props.onDismiss(); this.props.onDismiss();
} }
}); });
const { backgroundColor } = this.props; const {backgroundColor} = this.props;
if (backgroundColor) { if (backgroundColor) {
el.style.setProperty( el.style.setProperty(
'background-color', 'background-color',
@ -53,42 +53,44 @@ export default class Notification extends Component {
} }
} }
setDismissTimer (after) { setDismissTimer() {
this.dismissTimer = setTimeout(() => { this.dismissTimer = setTimeout(() => {
this.dismiss(); this.handleDismiss();
}, this.props.dismissAfter); }, this.props.dismissAfter);
} }
resetDismissTimer (after) { resetDismissTimer() {
clearTimeout(this.dismissTimer); clearTimeout(this.dismissTimer);
this.setDismissTimer(); this.setDismissTimer();
} }
componentWillUnmount () { componentWillUnmount() {
clearTimeout(this.dismissTimer); clearTimeout(this.dismissTimer);
} }
template (css) { template(css) {
const { backgroundColor } = this.props; const {backgroundColor} = this.props;
const opacity = this.state.dismissing ? 0 : 1; const opacity = this.state.dismissing ? 0 : 1;
return <div return (<div
style={{ opacity, backgroundColor }} style={{opacity, backgroundColor}}
ref={ this.onElement } ref={this.onElement}
className={ css('indicator') }> className={css('indicator')}
>
{ this.props.customChildrenBefore } { this.props.customChildrenBefore }
{ this.props.children || this.props.text } { this.props.children || this.props.text }
{ {
this.props.userDismissable this.props.userDismissable ?
? <a <a
className={ css('dismissLink') } className={css('dismissLink')}
onClick={ this.dismiss }>[x]</a> onClick={this.handleDismiss}
: null >[x]</a> :
null
} }
{ this.props.customChildren } { this.props.customChildren }
</div>; </div>);
} }
styles () { styles() {
return { return {
indicator: { indicator: {
display: 'inline-block', display: 'inline-block',

View file

@ -1,68 +1,80 @@
import React from 'react'; import React from 'react';
import Component from '../component'; import Component from '../component';
import {decorate} from '../utils/plugins';
import Notification_ from './notification'; import Notification_ from './notification';
import { decorate } from '../utils/plugins';
const Notification = decorate(Notification_); const Notification = decorate(Notification_);
export default class Notifications extends Component { export default class Notifications extends Component {
template (css) { template(css) {
return <div className={ css('view') }> return (<div className={css('view')}>
{ this.props.customChildrenBefore } { this.props.customChildrenBefore }
{ {
this.props.fontShowing && this.props.fontShowing &&
<Notification <Notification
key='font' key="font"
backgroundColor='rgba(255, 255, 255, .2)' backgroundColor="rgba(255, 255, 255, .2)"
text={`${this.props.fontSize}px`} text={`${this.props.fontSize}px`}
userDismissable={ false } userDismissable={false}
onDismiss={ this.props.onDismissFont } onDismiss={this.props.onDismissFont}
dismissAfter={ 1000 } /> dismissAfter={1000}
/>
} }
{ {
this.props.resizeShowing && this.props.resizeShowing &&
<Notification <Notification
key='resize' key="resize"
backgroundColor='rgba(255, 255, 255, .2)' backgroundColor="rgba(255, 255, 255, .2)"
text={`${this.props.cols}x${this.props.rows}`} text={`${this.props.cols}x${this.props.rows}`}
userDismissable={ false } userDismissable={false}
onDismiss={ this.props.onDismissResize } onDismiss={this.props.onDismissResize}
dismissAfter={ 1000 } /> dismissAfter={1000}
/>
} }
{ {
this.props.updateShowing && this.props.updateShowing &&
<Notification <Notification
key='update' key="update"
backgroundColor='#7ED321' backgroundColor="#7ED321"
text={`Version ${this.props.updateVersion} ready`} text={`Version ${this.props.updateVersion} ready`}
onDismiss={ this.props.onDismissUpdate } onDismiss={this.props.onDismissUpdate}
userDismissable={ true }> userDismissable
>
Version <b>{ this.props.updateVersion}</b> ready. Version <b>{ this.props.updateVersion}</b> ready.
{ this.props.updateNote && ` ${this.props.updateNote.trim().replace(/\.$/, '')}` } { this.props.updateNote && ` ${this.props.updateNote.trim().replace(/\.$/, '')}` }
{ ' ' } { ' ' }
(<a (<a
style={{ color: '#fff' }} style={{color: '#fff'}}
onClick={ (ev) => { window.require('electron').shell.openExternal(ev.target.href); ev.preventDefault(); } } onClick={ev => {
href={`https://github.com/zeit/hyperterm/releases/tag/${this.props.updateVersion}`}>notes</a>). window.require('electron').shell.openExternal(ev.target.href);
ev.preventDefault();
}}
href={`https://github.com/zeit/hyperterm/releases/tag/${this.props.updateVersion}`}
>notes</a>).
{ ' ' } { ' ' }
<a style={{ <a
cursor: 'pointer', style={{
textDecoration: 'underline', cursor: 'pointer',
fontWeight: 'bold' }} textDecoration: 'underline',
onClick={ this.props.onUpdateInstall }> fontWeight: 'bold'
}}
onClick={this.props.onUpdateInstall}
>
Restart Restart
</a>. </a>.
{ ' ' } { ' ' }
</Notification> </Notification>
} }
{ this.props.customChildren } { this.props.customChildren }
</div>; </div>);
} }
styles () { styles() {
return { return {
view: { view: {
position: 'fixed', position: 'fixed',

View file

@ -2,78 +2,93 @@ import React from 'react';
import Component from '../component'; import Component from '../component';
export default class Tab extends Component { export default class Tab extends Component {
constructor () { constructor() {
super(); super();
this.hover = this.hover.bind(this);
this.blur = this.blur.bind(this); this.handleHover = this.handleHover.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.handleClick = this.handleClick.bind(this); this.handleClick = this.handleClick.bind(this);
this.state = { this.state = {
hovered: false hovered: false
}; };
} }
hover () { shouldComponentUpdate() {
this.setState({ hovered: true }); return true;
} }
blur () { handleHover() {
this.setState({ hovered: false }); this.setState({
hovered: true
});
} }
handleClick (event) { handleBlur() {
this.setState({
hovered: false
});
}
handleClick(event) {
const isLeftClick = event.nativeEvent.which === 1; const isLeftClick = event.nativeEvent.which === 1;
const isMiddleClick = event.nativeEvent.which === 2; const isMiddleClick = event.nativeEvent.which === 2;
if (isLeftClick) { if (isLeftClick) {
this.props.isActive ? null : this.props.onSelect(); if (this.props.isActive === null) {
this.props.onSelect();
}
} else if (isMiddleClick) { } else if (isMiddleClick) {
this.props.onClose(); this.props.onClose();
} }
} }
template (css) { template(css) {
const { isActive, isFirst, isLast, borderColor, hasActivity } = this.props; const {isActive, isFirst, isLast, borderColor, hasActivity} = this.props;
const { hovered } = this.state; const {hovered} = this.state;
return <li return (<li
onMouseEnter={ this.hover } onMouseEnter={this.handleHover}
onMouseLeave={ this.blur } onMouseLeave={this.handleBlur}
onClick={ this.props.onClick } onClick={this.props.onClick}
style={{ borderColor }} style={{borderColor}}
className={ css( className={css(
'tab', 'tab',
isFirst && 'first', isFirst && 'first',
isActive && 'active', isActive && 'active',
isFirst && isActive && 'firstActive', isFirst && isActive && 'firstActive',
hasActivity && 'hasActivity' hasActivity && 'hasActivity'
) }> )}
{ this.props.customChildrenBefore } >
<span { this.props.customChildrenBefore }
className={ css( <span
'text', className={css(
isLast && 'textLast', 'text',
isActive && 'textActive' isLast && 'textLast',
) } isActive && 'textActive'
onClick={ this.handleClick }> )}
<span className={ css('textInner') }> onClick={this.handleClick}
{ this.props.text } >
</span> <span className={css('textInner')}>
{ this.props.text }
</span> </span>
<i </span>
className={ css( <i
'icon', className={css(
hovered && 'iconHovered' 'icon',
) } hovered && 'iconHovered'
onClick={ this.props.onClose }> )}
<svg className={ css('shape') }> onClick={this.props.onClose}
<use xlinkHref='./dist/assets/icons.svg#close'></use> >
</svg> <svg className={css('shape')}>
</i> <use xlinkHref="./dist/assets/icons.svg#close"/>
{ this.props.customChildren } </svg>
</li>; </i>
{ this.props.customChildren }
</li>);
} }
styles () { styles() {
return { return {
tab: { tab: {
color: '#ccc', color: '#ccc',

View file

@ -1,14 +1,16 @@
import Tab_ from './tab';
import React from 'react'; import React from 'react';
import Component from '../component'; import Component from '../component';
import { decorate, getTabProps } from '../utils/plugins'; import {decorate, getTabProps} from '../utils/plugins';
import Tab_ from './tab';
const Tab = decorate(Tab_, 'Tab'); const Tab = decorate(Tab_, 'Tab');
const isMac = /Mac/.test(navigator.userAgent); const isMac = /Mac/.test(navigator.userAgent);
export default class Tabs extends Component { export default class Tabs extends Component {
template (css) { template(css) {
const { const {
tabs = [], tabs = [],
borderColor, borderColor,
@ -16,43 +18,45 @@ export default class Tabs extends Component {
onClose onClose
} = this.props; } = this.props;
return <nav className={ css('nav') }> return (<nav className={css('nav')}>
{ this.props.customChildrenBefore } { this.props.customChildrenBefore }
{ {
tabs.length tabs.length ?
? 1 === tabs.length tabs.length === 1 ?
? <div className={ css('title') }>{ tabs[0].title }</div> <div className={css('title')}>{tabs[0].title}</div> :
: [ [
<ul <ul
className={ css('list') }> className={css('list')}
>
{ {
tabs.map((tab, i) => { tabs.map((tab, i) => {
const { uid, title, isActive, hasActivity } = tab; const {uid, title, isActive, hasActivity} = tab;
const props = getTabProps(tab, this.props, { const props = getTabProps(tab, this.props, {
text: '' === title ? 'Shell' : title, text: title === '' ? 'Shell' : title,
isFirst: 0 === i, isFirst: i === 0,
isLast: tabs.length - 1 === i, isLast: tabs.length - 1 === i,
borderColor: borderColor, borderColor,
isActive, isActive,
hasActivity, hasActivity,
onSelect: onChange.bind(null, uid), onSelect: onChange.bind(null, uid),
onClose: onClose.bind(null, uid) onClose: onClose.bind(null, uid)
}); });
return <Tab key={`tab-${uid}`} {...props} />; return <Tab key={`tab-${uid}`} {...props}/>;
}) })
} }
</ul>, </ul>,
isMac && <div isMac && <div
style={{ borderColor }} style={{borderColor}}
className={ css('borderShim') }></div> className={css('borderShim')}
] />
: null ] :
null
} }
{ this.props.customChildren } { this.props.customChildren }
</nav>; </nav>);
} }
styles () { styles() {
return { return {
nav: { nav: {
fontSize: '12px', fontSize: '12px',

View file

@ -3,21 +3,21 @@ import React from 'react';
import Color from 'color'; import Color from 'color';
import hterm from '../hterm'; import hterm from '../hterm';
import Component from '../component'; import Component from '../component';
import { getColorList } from '../utils/colors'; import {getColorList} from '../utils/colors';
import notify from '../utils/notify'; import notify from '../utils/notify';
export default class Term extends Component { export default class Term extends Component {
constructor (props) { constructor(props) {
super(props); super(props);
this.onWheel = this.onWheel.bind(this); this.onWheel = this.onWheel.bind(this);
this.onScrollEnter = this.onScrollEnter.bind(this); this.handleScrollEnter = this.handleScrollEnter.bind(this);
this.onScrollLeave = this.onScrollLeave.bind(this); this.handleScrollLeave = this.handleScrollLeave.bind(this);
props.ref_(this); props.ref_(this);
} }
componentDidMount () { componentDidMount() {
const { props } = this; const {props} = this;
this.term = new hterm.Terminal(); this.term = new hterm.Terminal();
// the first term that's created has unknown size // the first term that's created has unknown size
@ -66,13 +66,15 @@ export default class Term extends Component {
}; };
this.term.decorate(this.refs.term); this.term.decorate(this.refs.term);
this.term.installKeyboard(); this.term.installKeyboard();
if (this.props.onTerminal) this.props.onTerminal(this.term); if (this.props.onTerminal) {
this.props.onTerminal(this.term);
}
const iframeWindow = this.getTermDocument().defaultView; const iframeWindow = this.getTermDocument().defaultView;
iframeWindow.addEventListener('wheel', this.onWheel); iframeWindow.addEventListener('wheel', this.onWheel);
} }
onWheel (e) { onWheel(e) {
if (this.props.onWheel) { if (this.props.onWheel) {
this.props.onWheel(e); this.props.onWheel(e);
} }
@ -85,28 +87,28 @@ export default class Term extends Component {
} }
} }
onScrollEnter () { handleScrollEnter() {
clearTimeout(this.scrollbarsHideTimer); clearTimeout(this.scrollbarsHideTimer);
this.term.prefs_.set('scrollbar-visible', true); this.term.prefs_.set('scrollbar-visible', true);
this.scrollMouseEnter = true; this.scrollMouseEnter = true;
} }
onScrollLeave () { handleScrollLeave() {
this.term.prefs_.set('scrollbar-visible', false); this.term.prefs_.set('scrollbar-visible', false);
this.scrollMouseEnter = false; this.scrollMouseEnter = false;
} }
write (data) { write(data) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.term.io.writeUTF8(data); this.term.io.writeUTF8(data);
}); });
} }
focus () { focus() {
this.term.focus(); this.term.focus();
} }
clear () { clear() {
this.term.clearPreserveCursorRow(); this.term.clearPreserveCursorRow();
// If cursor is still not at the top, a command is probably // If cursor is still not at the top, a command is probably
@ -117,53 +119,53 @@ export default class Term extends Component {
} }
} }
moveWordLeft () { moveWordLeft() {
this.term.onVTKeystroke('\x1bb'); this.term.onVTKeystroke('\x1bb');
} }
moveWordRight () { moveWordRight() {
this.term.onVTKeystroke('\x1bf'); this.term.onVTKeystroke('\x1bf');
} }
deleteWordLeft () { deleteWordLeft() {
this.term.onVTKeystroke('\x1b\x7f'); this.term.onVTKeystroke('\x1b\x7f');
} }
deleteWordRight () { deleteWordRight() {
this.term.onVTKeystroke('\x1bd'); this.term.onVTKeystroke('\x1bd');
} }
deleteLine () { deleteLine() {
this.term.onVTKeystroke('\x1bw'); this.term.onVTKeystroke('\x1bw');
} }
moveToStart () { moveToStart() {
this.term.onVTKeystroke('\x01'); this.term.onVTKeystroke('\x01');
} }
moveToEnd () { moveToEnd() {
this.term.onVTKeystroke('\x05'); this.term.onVTKeystroke('\x05');
} }
selectAll () { selectAll() {
this.term.selectAll(); this.term.selectAll();
} }
getTermDocument () { getTermDocument() {
return this.term.document_; return this.term.document_;
} }
getStylesheet (css) { getStylesheet(css) {
const blob = new Blob([` const blob = new Blob([`
.cursor-node[focus="false"] { .cursor-node[focus="false"] {
border-width: 1px !important; border-width: 1px !important;
} }
${css} ${css}
`], { type: 'text/css' }); `], {type: 'text/css'});
return URL.createObjectURL(blob); return URL.createObjectURL(blob);
} }
validateColor (color, alternative = 'rgb(255,255,255)') { validateColor(color, alternative = 'rgb(255,255,255)') {
try { try {
return Color(color).rgbString(); return Color(color).rgbString();
} catch (err) { } catch (err) {
@ -172,7 +174,7 @@ export default class Term extends Component {
return alternative; return alternative;
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps(nextProps) {
if (this.props.url !== nextProps.url) { if (this.props.url !== nextProps.url) {
// when the url prop changes, we make sure // when the url prop changes, we make sure
// the terminal starts or stops ignoring // the terminal starts or stops ignoring
@ -180,8 +182,8 @@ export default class Term extends Component {
// with the <webview> // with the <webview>
if (nextProps.url) { if (nextProps.url) {
const io = this.term.io.push(); const io = this.term.io.push();
io.onVTKeystroke = io.sendString = (str) => { io.onVTKeystroke = io.sendString = str => {
if (1 === str.length && 3 === str.charCodeAt(0) /* Ctrl + C */) { if (str.length === 1 && str.charCodeAt(0) === 3 /* Ctrl + C */) {
this.props.onURLAbort(); this.props.onURLAbort();
} }
}; };
@ -239,37 +241,39 @@ export default class Term extends Component {
} }
} }
componentWillUnmount () { componentWillUnmount() {
clearTimeout(this.scrollbarsHideTimer); clearTimeout(this.scrollbarsHideTimer);
this.props.ref_(null); this.props.ref_(null);
} }
template (css) { template(css) {
return <div className={ css('fit') }> return (<div className={css('fit')}>
{ this.props.customChildrenBefore } { this.props.customChildrenBefore }
<div ref='term' className={ css('fit', 'term') } /> <div ref="term" className={css('fit', 'term')}/>
{ this.props.url { this.props.url ?
? <webview <webview
src={this.props.url} src={this.props.url}
style={{ style={{
background: '#000', background: '#000',
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
display: 'inline-flex', display: 'inline-flex',
width: '100%', width: '100%',
height: '100%' height: '100%'
}}></webview> }}
: <div /> :
className={ css('scrollbarShim') } <div
onMouseEnter={ this.onScrollEnter } className={css('scrollbarShim')}
onMouseLeave={ this.onScrollLeave } /> onMouseEnter={this.handleScrollEnter}
onMouseLeave={this.handleScrollLeave}
/>
} }
{ this.props.customChildren } { this.props.customChildren }
</div>; </div>);
} }
styles () { styles() {
return { return {
fit: { fit: {
width: '100%', width: '100%',

View file

@ -1,14 +1,16 @@
import React from 'react'; import React from 'react';
import Term_ from './term';
import Component from '../component'; import Component from '../component';
import { last } from '../utils/array'; import {last} from '../utils/array';
import { decorate, getTermProps } from '../utils/plugins'; import {decorate, getTermProps} from '../utils/plugins';
import Term_ from './term';
const Term = decorate(Term_, 'Term'); const Term = decorate(Term_, 'Term');
export default class Terms extends Component { export default class Terms extends Component {
constructor (props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.terms = {}; this.terms = {};
this.bound = new WeakMap(); this.bound = new WeakMap();
@ -16,8 +18,8 @@ export default class Terms extends Component {
props.ref_(this); props.ref_(this);
} }
componentWillReceiveProps (next) { componentWillReceiveProps(next) {
const { write } = next; const {write} = next;
if (write && this.props.write !== write) { if (write && this.props.write !== write) {
this.getTermByUid(write.uid).write(write.data); this.getTermByUid(write.uid).write(write.data);
} }
@ -42,7 +44,7 @@ export default class Terms extends Component {
const curActive = this.props.activeSession; const curActive = this.props.activeSession;
// if we closed an item that wasn't focused, nothing changes // if we closed an item that wasn't focused, nothing changes
if (~newUids.indexOf(curActive)) { if (newUids.indexOf(curActive) !== -1) {
return; return;
} }
@ -58,15 +60,19 @@ export default class Terms extends Component {
} }
} }
shouldComponentUpdate (nextProps) { shouldComponentUpdate(nextProps) {
for (const i in nextProps) { for (const i in nextProps) {
if ('write' === i) continue; if (i === 'write') {
continue;
}
if (this.props[i] !== nextProps[i]) { if (this.props[i] !== nextProps[i]) {
return true; return true;
} }
} }
for (const i in this.props) { for (const i in this.props) {
if ('write' === i) continue; if (i === 'write') {
continue;
}
if (this.props[i] !== nextProps[i]) { if (this.props[i] !== nextProps[i]) {
return true; return true;
} }
@ -74,7 +80,7 @@ export default class Terms extends Component {
return false; return false;
} }
onRef (uid, term) { onRef(uid, term) {
if (term) { if (term) {
this.terms[uid] = term; this.terms[uid] = term;
} else { } else {
@ -82,19 +88,19 @@ export default class Terms extends Component {
} }
} }
getTermByUid (uid) { getTermByUid(uid) {
return this.terms[uid]; return this.terms[uid];
} }
getActiveTerm () { getActiveTerm() {
return this.getTermByUid(this.props.activeSession); return this.getTermByUid(this.props.activeSession);
} }
getLastTermIndex () { getLastTermIndex() {
return this.props.sessions.length - 1; return this.props.sessions.length - 1;
} }
bind (fn, thisObj, uid) { bind(fn, thisObj, uid) {
if (!this.bound.has(fn)) { if (!this.bound.has(fn)) {
this.bound.set(fn, {}); this.bound.set(fn, {});
} }
@ -105,25 +111,26 @@ export default class Terms extends Component {
return map[uid]; return map[uid];
} }
getTermProps (uid) { getTermProps(uid) {
return getTermProps(uid, this.props); return getTermProps(uid, this.props);
} }
onTerminal (uid, term) { onTerminal(uid, term) {
this.terms[uid] = term; this.terms[uid] = term;
} }
componentWillUnmount () { componentWillUnmount() {
this.props.ref_(null); this.props.ref_(null);
} }
template (css) { template(css) {
return <div return (<div
style={{ padding: this.props.padding }} style={{padding: this.props.padding}}
className={ css('terms') }> className={css('terms')}
>
{ this.props.customChildrenBefore } { this.props.customChildrenBefore }
{ {
this.props.sessions.map((session) => { this.props.sessions.map(session => {
const uid = session.uid; const uid = session.uid;
const isActive = uid === this.props.activeSession; const isActive = uid === this.props.activeSession;
const props = getTermProps(uid, this.props, { const props = getTermProps(uid, this.props, {
@ -147,21 +154,23 @@ export default class Terms extends Component {
bellSoundURL: this.props.bellSoundURL, bellSoundURL: this.props.bellSoundURL,
copyOnSelect: this.props.copyOnSelect copyOnSelect: this.props.copyOnSelect
}); });
return <div return (<div
key={`d${uid}`} key={`d${uid}`}
className={css('term', isActive && 'termActive')}> className={css('term', isActive && 'termActive')}
<Term >
key={uid} <Term
ref_={this.bind(this.onRef, this, uid)} key={uid}
{...props} /> ref_={this.bind(this.onRef, this, uid)}
</div>; {...props}
/>
</div>);
}) })
} }
{ this.props.customChildren } { this.props.customChildren }
</div>; </div>);
} }
styles () { styles() {
return { return {
terms: { terms: {
position: 'absolute', position: 'absolute',
@ -188,9 +197,9 @@ export default class Terms extends Component {
} }
// little memoized helper to compute a map of uids // little memoized helper to compute a map of uids
function uids (sessions) { function uids(sessions) {
if (!sessions._uids) { if (!sessions._uids) {
sessions._uids = sessions.map((s) => s.uid); sessions._uids = sessions.map(s => s.uid);
} }
return sessions._uids; return sessions._uids;
} }

View file

@ -1,17 +1,18 @@
import {createSelector} from 'reselect';
import Header from '../components/header'; import Header from '../components/header';
import { closeTab, changeTab, maximize, unmaximize } from '../actions/header'; import {closeTab, changeTab, maximize, unmaximize} from '../actions/header';
import { values } from '../utils/object'; import {values} from '../utils/object';
import { createSelector } from 'reselect'; import {connect} from '../utils/plugins';
import { connect } from '../utils/plugins';
const isMac = /Mac/.test(navigator.userAgent); const isMac = /Mac/.test(navigator.userAgent);
const getSessions = (sessions) => sessions.sessions; const getSessions = sessions => sessions.sessions;
const getActiveUid = (sessions) => sessions.activeUid; const getActiveUid = sessions => sessions.activeUid;
const getActivityMarkers = (sessions, ui) => ui.activityMarkers; const getActivityMarkers = (sessions, ui) => ui.activityMarkers;
const getTabs = createSelector( const getTabs = createSelector(
[getSessions, getActiveUid, getActivityMarkers], [getSessions, getActiveUid, getActivityMarkers],
(sessions, activeUid, activityMarkers) => values(sessions).map((s) => { (sessions, activeUid, activityMarkers) => values(sessions).map(s => {
return { return {
uid: s.uid, uid: s.uid,
title: s.title, title: s.title,
@ -22,7 +23,7 @@ const getTabs = createSelector(
); );
const HeaderContainer = connect( const HeaderContainer = connect(
(state) => { state => {
return { return {
// active is an index // active is an index
isMac, isMac,
@ -33,13 +34,13 @@ const HeaderContainer = connect(
maximized: state.ui.maximized maximized: state.ui.maximized
}; };
}, },
(dispatch) => { dispatch => {
return { return {
onCloseTab: (i) => { onCloseTab: i => {
dispatch(closeTab(i)); dispatch(closeTab(i));
}, },
onChangeTab: (i) => { onChangeTab: i => {
dispatch(changeTab(i)); dispatch(changeTab(i));
}, },

View file

@ -1,22 +1,24 @@
import Mousetrap from 'mousetrap';
import React from 'react'; import React from 'react';
import Component from '../component';
import {connect} from '../utils/plugins';
import * as uiActions from '../actions/ui';
import HeaderContainer from './header'; import HeaderContainer from './header';
import TermsContainer from './terms'; import TermsContainer from './terms';
import NotificationsContainer from './notifications'; import NotificationsContainer from './notifications';
import Component from '../component';
import Mousetrap from 'mousetrap';
import * as uiActions from '../actions/ui';
import { connect } from '../utils/plugins';
const isMac = /Mac/.test(navigator.userAgent); const isMac = /Mac/.test(navigator.userAgent);
class HyperTerm extends Component { class HyperTerm extends Component {
constructor (props) { constructor(props) {
super(props); super(props);
this.focusActive = this.focusActive.bind(this); this.handleFocusActive = this.handleFocusActive.bind(this);
this.onTermsRef = this.onTermsRef.bind(this); this.onTermsRef = this.onTermsRef.bind(this);
} }
componentWillReceiveProps (next) { componentWillReceiveProps(next) {
if (this.props.backgroundColor !== next.backgroundColor) { if (this.props.backgroundColor !== next.backgroundColor) {
// this can be removed when `setBackgroundColor` in electron // this can be removed when `setBackgroundColor` in electron
// starts working again // starts working again
@ -24,15 +26,19 @@ class HyperTerm extends Component {
} }
} }
focusActive () { handleFocusActive() {
const term = this.terms.getActiveTerm(); const term = this.terms.getActiveTerm();
if (term) term.focus(); if (term) {
term.focus();
}
} }
attachKeyListeners () { attachKeyListeners() {
const { moveTo, moveLeft, moveRight } = this.props; const {moveTo, moveLeft, moveRight} = this.props;
const term = this.terms.getActiveTerm(); const term = this.terms.getActiveTerm();
if (!term) return; if (!term) {
return;
}
const lastIndex = this.terms.getLastTermIndex(); const lastIndex = this.terms.getLastTermIndex();
const document = term.getTermDocument(); const document = term.getTermDocument();
const keys = new Mousetrap(document); const keys = new Mousetrap(document);
@ -55,7 +61,7 @@ class HyperTerm extends Component {
keys.bind('ctrl+shift+tab', moveLeft); keys.bind('ctrl+shift+tab', moveLeft);
keys.bind('ctrl+tab', moveRight); keys.bind('ctrl+tab', moveRight);
const bound = method => { return term[method].bind(term); }; const bound = method => term[method].bind(term);
keys.bind('alt+left', bound('moveWordLeft')); keys.bind('alt+left', bound('moveWordLeft'));
keys.bind('alt+right', bound('moveWordRight')); keys.bind('alt+right', bound('moveWordRight'));
keys.bind('alt+backspace', bound('deleteWordLeft')); keys.bind('alt+backspace', bound('deleteWordLeft'));
@ -67,40 +73,45 @@ class HyperTerm extends Component {
this.keys = keys; this.keys = keys;
} }
onTermsRef (terms) { onTermsRef(terms) {
this.terms = terms; this.terms = terms;
} }
componentDidUpdate (prev) { componentDidUpdate(prev) {
if (prev.activeSession !== this.props.activeSession) { if (prev.activeSession !== this.props.activeSession) {
if (this.keys) this.keys.reset(); if (this.keys) {
this.focusActive(); this.keys.reset();
}
this.handleFocusActive();
this.attachKeyListeners(); this.attachKeyListeners();
} }
} }
componentWillUnmount () { componentWillUnmount() {
if (this.keys) this.keys.reset(); if (this.keys) {
this.keys.reset();
}
document.body.style.backgroundColor = 'inherit'; document.body.style.backgroundColor = 'inherit';
} }
template (css) { template(css) {
const { isMac, customCSS, borderColor } = this.props; const {isMac, customCSS, borderColor} = this.props;
return <div onClick={ this.focusActive }> return (<div onClick={this.handleFocusActive}>
<div <div
style={{ borderColor }} style={{borderColor}}
className={ css('main', isMac && 'mainRounded') }> className={css('main', isMac && 'mainRounded')}
<HeaderContainer /> >
<TermsContainer ref_={this.onTermsRef} /> <HeaderContainer/>
<TermsContainer ref_={this.onTermsRef}/>
</div> </div>
<NotificationsContainer /> <NotificationsContainer/>
<style dangerouslySetInnerHTML={{ __html: customCSS }} /> <style dangerouslySetInnerHTML={{__html: customCSS}}/>
{ this.props.customChildren } { this.props.customChildren }
</div>; </div>);
} }
styles () { styles() {
return { return {
main: { main: {
position: 'fixed', position: 'fixed',
@ -120,7 +131,7 @@ class HyperTerm extends Component {
} }
const HyperTermContainer = connect( const HyperTermContainer = connect(
(state) => { state => {
return { return {
isMac, isMac,
customCSS: state.ui.css, customCSS: state.ui.css,
@ -129,9 +140,9 @@ const HyperTermContainer = connect(
backgroundColor: state.ui.backgroundColor backgroundColor: state.ui.backgroundColor
}; };
}, },
(dispatch) => { dispatch => {
return { return {
moveTo: (i) => { moveTo: i => {
dispatch(uiActions.moveTo(i)); dispatch(uiActions.moveTo(i));
}, },
@ -145,7 +156,7 @@ const HyperTermContainer = connect(
}; };
}, },
null, null,
{ withRef: true } {withRef: true}
)(HyperTerm, 'HyperTerm'); )(HyperTerm, 'HyperTerm');
export default HyperTermContainer; export default HyperTermContainer;

View file

@ -1,12 +1,12 @@
import Notifications from '../components/notifications'; import Notifications from '../components/notifications';
import { installUpdate } from '../actions/updater'; import {installUpdate} from '../actions/updater';
import { connect } from '../utils/plugins'; import {connect} from '../utils/plugins';
import { dismissNotification } from '../actions/notifications'; import {dismissNotification} from '../actions/notifications';
const NotificationsContainer = connect( const NotificationsContainer = connect(
(state) => { state => {
const { ui } = state; const {ui} = state;
const { notifications } = ui; const {notifications} = ui;
const state_ = {}; const state_ = {};
if (notifications.font) { if (notifications.font) {
@ -40,7 +40,7 @@ const NotificationsContainer = connect(
return state_; return state_;
}, },
(dispatch) => { dispatch => {
return { return {
onDismissFont: () => { onDismissFont: () => {
dispatch(dismissNotification('font')); dispatch(dismissNotification('font'));

View file

@ -1,6 +1,6 @@
import Terms from '../components/terms'; import Terms from '../components/terms';
import { values } from '../utils/object'; import {values} from '../utils/object';
import { connect } from '../utils/plugins'; import {connect} from '../utils/plugins';
import { import {
resizeSession, resizeSession,
sendSessionData, sendSessionData,
@ -10,7 +10,7 @@ import {
} from '../actions/sessions'; } from '../actions/sessions';
const TermsContainer = connect( const TermsContainer = connect(
(state) => { state => {
const sessions = state.sessions.sessions; const sessions = state.sessions.sessions;
return { return {
cols: state.ui.cols, cols: state.ui.cols,
@ -19,9 +19,9 @@ const TermsContainer = connect(
activeSession: state.sessions.activeUid, activeSession: state.sessions.activeUid,
customCSS: state.ui.termCSS, customCSS: state.ui.termCSS,
write: state.sessions.write, write: state.sessions.write,
fontSize: state.ui.fontSizeOverride fontSize: state.ui.fontSizeOverride ?
? state.ui.fontSizeOverride state.ui.fontSizeOverride :
: state.ui.fontSize, state.ui.fontSize,
fontFamily: state.ui.fontFamily, fontFamily: state.ui.fontFamily,
fontSmoothing: state.ui.fontSmoothingOverride, fontSmoothing: state.ui.fontSmoothingOverride,
padding: state.ui.padding, padding: state.ui.padding,
@ -36,31 +36,31 @@ const TermsContainer = connect(
copyOnSelect: state.ui.copyOnSelect copyOnSelect: state.ui.copyOnSelect
}; };
}, },
(dispatch) => { dispatch => {
return { return {
onData (uid, data) { onData(uid, data) {
dispatch(sendSessionData(uid, data)); dispatch(sendSessionData(uid, data));
}, },
onTitle (uid, title) { onTitle(uid, title) {
dispatch(setSessionXtermTitle(uid, title)); dispatch(setSessionXtermTitle(uid, title));
}, },
onResize (uid, cols, rows) { onResize(uid, cols, rows) {
dispatch(resizeSession(uid, cols, rows)); dispatch(resizeSession(uid, cols, rows));
}, },
onURLAbort (uid) { onURLAbort(uid) {
dispatch(exitSessionBrowser(uid)); dispatch(exitSessionBrowser(uid));
}, },
onActive (uid) { onActive(uid) {
dispatch(setActiveSession(uid)); dispatch(setActiveSession(uid));
} }
}; };
}, },
null, null,
{ withRef: true } {withRef: true}
)(Terms, 'Terms'); )(Terms, 'Terms');
export default TermsContainer; export default TermsContainer;

View file

@ -1,4 +1,5 @@
import { hterm, lib } from 'hterm-umdjs'; import {hterm, lib} from 'hterm-umdjs';
const selection = require('./utils/selection'); const selection = require('./utils/selection');
hterm.defaultStorage = new lib.Storage.Memory(); hterm.defaultStorage = new lib.Storage.Memory();
@ -13,7 +14,7 @@ hterm.Terminal.prototype.selectAll = function () {
// override double click behavior to copy // override double click behavior to copy
const oldMouse = hterm.Terminal.prototype.onMouse_; const oldMouse = hterm.Terminal.prototype.onMouse_;
hterm.Terminal.prototype.onMouse_ = function (e) { hterm.Terminal.prototype.onMouse_ = function (e) {
if ('dblclick' === e.type) { if (e.type === 'dblclick') {
selection.extend(this); selection.extend(this);
console.log('[hyperterm+hterm] ignore double click'); console.log('[hyperterm+hterm] ignore double click');
return; return;
@ -28,8 +29,8 @@ hterm.Terminal.prototype.overlaySize = function () {};
// a non-collapsed selection whose text is '', and results // a non-collapsed selection whose text is '', and results
// in an infinite copy loop // in an infinite copy loop
hterm.Terminal.prototype.copySelectionToClipboard = function () { hterm.Terminal.prototype.copySelectionToClipboard = function () {
var text = this.getSelectionText(); const text = this.getSelectionText();
if (text != null && text !== '') { if (text !== null && text !== '') {
this.copyStringToClipboard(text); this.copyStringToClipboard(text);
} }
}; };
@ -45,7 +46,7 @@ hterm.Keyboard.prototype.onKeyDown_ = function (e) {
*/ */
if (e.key === 'Dead') { if (e.key === 'Dead') {
if (e.code === 'Quote' && e.shiftKey === false) { if (e.code === 'Quote' && e.shiftKey === false) {
this.terminal.onVTKeystroke("'"); this.terminal.onVTKeystroke('\'');
return; return;
} }
if (e.code === 'Quote' && e.shiftKey === true) { if (e.code === 'Quote' && e.shiftKey === true) {
@ -103,11 +104,10 @@ hterm.Keyboard.prototype.onKeyDown_ = function (e) {
if (e.metaKey || e.altKey || (e.ctrlKey && e.code === 'Tab')) { if (e.metaKey || e.altKey || (e.ctrlKey && e.code === 'Tab')) {
return; return;
} else { }
if ((!e.ctrlKey || e.code !== 'ControlLeft') && !e.shiftKey && e.code !== 'CapsLock') {
// Test for valid keys in order to clear the terminal selection // Test for valid keys in order to clear the terminal selection
if ((!e.ctrlKey || e.code !== 'ControlLeft') && !e.shiftKey && e.code !== 'CapsLock') { selection.clear(this.terminal);
selection.clear(this.terminal);
}
} }
return oldKeyDown.call(this, e); return oldKeyDown.call(this, e);
}; };
@ -116,9 +116,8 @@ const oldKeyPress = hterm.Keyboard.prototype.onKeyPress_;
hterm.Keyboard.prototype.onKeyPress_ = function (e) { hterm.Keyboard.prototype.onKeyPress_ = function (e) {
if (e.metaKey) { if (e.metaKey) {
return; return;
} else {
selection.clear(this.terminal);
} }
selection.clear(this.terminal);
return oldKeyPress.call(this, e); return oldKeyPress.call(this, e);
}; };
@ -130,7 +129,7 @@ hterm.Terminal.prototype.clearPreserveCursorRow = function () {
this.scrollbackRows_.length = 0; this.scrollbackRows_.length = 0;
this.scrollPort_.resetCache(); this.scrollPort_.resetCache();
[this.primaryScreen_, this.alternateScreen_].forEach((screen) => { [this.primaryScreen_, this.alternateScreen_].forEach(screen => {
const bottom = screen.getHeight(); const bottom = screen.getHeight();
if (bottom > 0) { if (bottom > 0) {
this.renumberRows_(0, bottom); this.renumberRows_(0, bottom);
@ -168,17 +167,19 @@ hterm.Terminal.prototype.clearPreserveCursorRow = function () {
// fixes a bug in hterm, where the shorthand hex // fixes a bug in hterm, where the shorthand hex
// is not properly converted to rgb // is not properly converted to rgb
lib.colors.hexToRGB = function (arg) { lib.colors.hexToRGB = function (arg) {
var hex16 = lib.colors.re_.hex16; const hex16 = lib.colors.re_.hex16;
var hex24 = lib.colors.re_.hex24; const hex24 = lib.colors.re_.hex24;
function convert (hex) { function convert(hex) {
if (hex.length === 4) { if (hex.length === 4) {
hex = hex.replace(hex16, function (h, r, g, b) { hex = hex.replace(hex16, (h, r, g, b) => {
return '#' + r + r + g + g + b + b; return '#' + r + r + g + g + b + b;
}); });
} }
var ary = hex.match(hex24); const ary = hex.match(hex24);
if (!ary) return null; if (!ary) {
return null;
}
return 'rgb(' + return 'rgb(' +
parseInt(ary[1], 16) + ', ' + parseInt(ary[1], 16) + ', ' +
@ -188,7 +189,7 @@ lib.colors.hexToRGB = function (arg) {
} }
if (arg instanceof Array) { if (arg instanceof Array) {
for (var i = 0; i < arg.length; i++) { for (let i = 0; i < arg.length; i++) {
arg[i] = convert(arg[i]); arg[i] = convert(arg[i]);
} }
} else { } else {
@ -199,4 +200,4 @@ lib.colors.hexToRGB = function (arg) {
}; };
export default hterm; export default hterm;
export { lib }; export {lib};

View file

@ -1,21 +1,22 @@
import rpc from './rpc'; import {createStore, applyMiddleware} from 'redux';
import forceUpdate from 'react-deep-force-update';
import {Provider} from 'react-redux';
import React from 'react'; import React from 'react';
import { render } from 'react-dom'; import {render} from 'react-dom';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { Provider } from 'react-redux'; import {webFrame} from 'electron';
import { init } from './actions/index';
import rpc from './rpc';
import {init} from './actions/index';
import effects from './utils/effects'; import effects from './utils/effects';
import * as config from './utils/config'; import * as config from './utils/config';
import rootReducer from './reducers/index'; import rootReducer from './reducers/index';
import * as plugins from './utils/plugins'; import * as plugins from './utils/plugins';
import * as uiActions from './actions/ui'; import * as uiActions from './actions/ui';
import forceUpdate from 'react-deep-force-update';
import * as updaterActions from './actions/updater'; import * as updaterActions from './actions/updater';
import * as sessionActions from './actions/sessions'; import * as sessionActions from './actions/sessions';
import { createStore, applyMiddleware } from 'redux';
import HyperTermContainer from './containers/hyperterm'; import HyperTermContainer from './containers/hyperterm';
import { loadConfig, reloadConfig } from './actions/config'; import {loadConfig, reloadConfig} from './actions/config';
import { webFrame } from 'electron';
// Disable pinch zoom // Disable pinch zoom
webFrame.setZoomLevelLimits(1, 1); webFrame.setZoomLevelLimits(1, 1);
@ -48,23 +49,23 @@ rpc.on('ready', () => {
store_.dispatch(uiActions.setFontSmoothing()); store_.dispatch(uiActions.setFontSmoothing());
}); });
rpc.on('session add', ({ uid, shell, pid }) => { rpc.on('session add', ({uid, shell, pid}) => {
store_.dispatch(sessionActions.addSession(uid, shell, pid)); store_.dispatch(sessionActions.addSession(uid, shell, pid));
}); });
rpc.on('session data', ({ uid, data }) => { rpc.on('session data', ({uid, data}) => {
store_.dispatch(sessionActions.addSessionData(uid, data)); store_.dispatch(sessionActions.addSessionData(uid, data));
}); });
rpc.on('session data send', ({ uid, data }) => { rpc.on('session data send', ({uid, data}) => {
store_.dispatch(sessionActions.sendSessionData(uid, data)); store_.dispatch(sessionActions.sendSessionData(uid, data));
}); });
rpc.on('session title', ({ uid, title }) => { rpc.on('session title', ({uid, title}) => {
store_.dispatch(sessionActions.setSessionProcessTitle(uid, title)); store_.dispatch(sessionActions.setSessionProcessTitle(uid, title));
}); });
rpc.on('session exit', ({ uid }) => { rpc.on('session exit', ({uid}) => {
store_.dispatch(sessionActions.sessionExit(uid)); store_.dispatch(sessionActions.sessionExit(uid));
}); });
@ -104,11 +105,11 @@ rpc.on('preferences', () => {
store_.dispatch(uiActions.showPreferences()); store_.dispatch(uiActions.showPreferences());
}); });
rpc.on('open file', ({ path }) => { rpc.on('open file', ({path}) => {
store_.dispatch(uiActions.openFile(path)); store_.dispatch(uiActions.openFile(path));
}); });
rpc.on('update available', ({ releaseName, releaseNotes }) => { rpc.on('update available', ({releaseName, releaseNotes}) => {
store_.dispatch(updaterActions.updateAvailable(releaseName, releaseNotes)); store_.dispatch(updaterActions.updateAvailable(releaseName, releaseNotes));
}); });
@ -117,8 +118,8 @@ rpc.on('move', () => {
}); });
const app = render( const app = render(
<Provider store={ store_ }> <Provider store={store_}>
<HyperTermContainer /> <HyperTermContainer/>
</Provider>, </Provider>,
document.getElementById('mount') document.getElementById('mount')
); );

View file

@ -1,4 +1,4 @@
import { combineReducers } from 'redux'; import {combineReducers} from 'redux';
import ui from './ui'; import ui from './ui';
import sessions from './sessions'; import sessions from './sessions';

View file

@ -1,5 +1,5 @@
import Immutable from 'seamless-immutable'; import Immutable from 'seamless-immutable';
import { decorateSessionsReducer } from '../utils/plugins'; import {decorateSessionsReducer} from '../utils/plugins';
import { import {
SESSION_ADD, SESSION_ADD,
SESSION_PTY_EXIT, SESSION_PTY_EXIT,
@ -19,7 +19,7 @@ const initialState = Immutable({
activeUid: null activeUid: null
}); });
function Session (obj) { function Session(obj) {
return Immutable({ return Immutable({
uid: '', uid: '',
title: '', title: '',
@ -31,7 +31,7 @@ function Session (obj) {
}).merge(obj); }).merge(obj);
} }
function Write (obj) { function Write(obj) {
return Immutable({ return Immutable({
uid: '', uid: '',
data: '' data: ''
@ -63,7 +63,7 @@ const reducer = (state = initialState, action) => {
cleared: true cleared: true
} }
} }
}, { deep: true }); }, {deep: true});
case SESSION_PTY_DATA: case SESSION_PTY_DATA:
return state return state
@ -74,15 +74,14 @@ const reducer = (state = initialState, action) => {
cleared: false cleared: false
} }
} }
}, { deep: true }); }, {deep: true});
case SESSION_PTY_EXIT: case SESSION_PTY_EXIT:
if (state.sessions[action.uid]) { if (state.sessions[action.uid]) {
return deleteSession(state, action.uid); return deleteSession(state, action.uid);
} else {
console.log('ignore pty exit: session removed by user');
return state;
} }
console.log('ignore pty exit: session removed by user');
return state;
case SESSION_USER_EXIT: case SESSION_USER_EXIT:
return deleteSession(state, action.uid); return deleteSession(state, action.uid);
@ -98,8 +97,8 @@ const reducer = (state = initialState, action) => {
export default decorateSessionsReducer(reducer); export default decorateSessionsReducer(reducer);
function deleteSession (state, uid) { function deleteSession(state, uid) {
return state.updateIn(['sessions'], (sessions) => { return state.updateIn(['sessions'], sessions => {
const sessions_ = sessions.asMutable(); const sessions_ = sessions.asMutable();
delete sessions_[uid]; delete sessions_[uid];
return sessions_; return sessions_;

View file

@ -1,6 +1,6 @@
import Immutable from 'seamless-immutable'; import Immutable from 'seamless-immutable';
import { decorateUIReducer } from '../utils/plugins'; import {decorateUIReducer} from '../utils/plugins';
import { CONFIG_LOAD, CONFIG_RELOAD } from '../constants/config'; import {CONFIG_LOAD, CONFIG_RELOAD} from '../constants/config';
import { import {
UI_FONT_SIZE_SET, UI_FONT_SIZE_SET,
UI_FONT_SIZE_RESET, UI_FONT_SIZE_RESET,
@ -8,7 +8,7 @@ import {
UI_WINDOW_MAXIMIZE, UI_WINDOW_MAXIMIZE,
UI_WINDOW_UNMAXIMIZE UI_WINDOW_UNMAXIMIZE
} from '../constants/ui'; } from '../constants/ui';
import { NOTIFICATION_DISMISS } from '../constants/notifications'; import {NOTIFICATION_DISMISS} from '../constants/notifications';
import { import {
SESSION_ADD, SESSION_ADD,
SESSION_RESIZE, SESSION_RESIZE,
@ -17,8 +17,8 @@ import {
SESSION_SET_ACTIVE, SESSION_SET_ACTIVE,
SESSION_SET_CWD SESSION_SET_CWD
} from '../constants/sessions'; } from '../constants/sessions';
import { UPDATE_AVAILABLE } from '../constants/updater'; import {UPDATE_AVAILABLE} from '../constants/updater';
import { values } from '../utils/object'; import {values} from '../utils/object';
const allowedCursorShapes = new Set(['BEAM', 'BLOCK', 'UNDERLINE']); const allowedCursorShapes = new Set(['BEAM', 'BLOCK', 'UNDERLINE']);
const allowedBells = new Set(['SOUND', false]); const allowedBells = new Set(['SOUND', false]);
@ -77,10 +77,10 @@ const initial = Immutable({
const reducer = (state = initial, action) => { const reducer = (state = initial, action) => {
let state_ = state; let state_ = state;
switch (action.type) { switch (action.type) { // eslint-disable-line default-case
case CONFIG_LOAD: case CONFIG_LOAD:
case CONFIG_RELOAD: case CONFIG_RELOAD: // eslint-disable-line no-case-declarations
const { config } = action; const {config} = action;
state_ = state state_ = state
// we unset the user font size override if the // we unset the user font size override if the
// font size changed from the config // font size changed from the config
@ -91,15 +91,15 @@ const reducer = (state = initial, action) => {
ret.fontSizeOverride = null; ret.fontSizeOverride = null;
} }
if (null != config.fontSize) { if (config.fontSize !== null) {
ret.fontSize = config.fontSize; ret.fontSize = config.fontSize;
} }
if (null != config.fontFamily) { if (config.fontFamily !== null) {
ret.fontFamily = config.fontFamily; ret.fontFamily = config.fontFamily;
} }
if (null != config.cursorColor) { if (config.cursorColor !== null) {
ret.cursorColor = config.cursorColor; ret.cursorColor = config.cursorColor;
} }
@ -107,27 +107,27 @@ const reducer = (state = initial, action) => {
ret.cursorShape = config.cursorShape; ret.cursorShape = config.cursorShape;
} }
if (null != config.borderColor) { if (config.borderColor !== null) {
ret.borderColor = config.borderColor; ret.borderColor = config.borderColor;
} }
if (null != config.padding) { if (config.padding !== null) {
ret.padding = config.padding; ret.padding = config.padding;
} }
if (null != config.foregroundColor) { if (config.foregroundColor !== null) {
ret.foregroundColor = config.foregroundColor; ret.foregroundColor = config.foregroundColor;
} }
if (null != config.backgroundColor) { if (config.backgroundColor !== null) {
ret.backgroundColor = config.backgroundColor; ret.backgroundColor = config.backgroundColor;
} }
if (null != config.css) { if (config.css !== null) {
ret.css = config.css; ret.css = config.css;
} }
if (null != config.termCSS) { if (config.termCSS !== null) {
ret.termCSS = config.termCSS; ret.termCSS = config.termCSS;
} }
@ -135,27 +135,25 @@ const reducer = (state = initial, action) => {
ret.bell = config.bell; ret.bell = config.bell;
} }
if (null !== config.bellSoundURL) { if (config.bellSoundURL !== null) {
ret.bellSoundURL = config.bellSoundURL || initial.bellSoundURL; ret.bellSoundURL = config.bellSoundURL || initial.bellSoundURL;
} }
if (null !== config.copyOnSelect) { if (config.copyOnSelect !== null) {
ret.copyOnSelect = config.copyOnSelect; ret.copyOnSelect = config.copyOnSelect;
} }
if (null != config.colors) { if (config.colors !== null) {
if (Array.isArray(config.colors)) { if (Array.isArray(config.colors)) {
const stateColors = Array.isArray(state.colors) const stateColors = Array.isArray(state.colors) ?
? state.colors state.colors :
: values(state.colors); values(state.colors);
if (stateColors.toString() !== config.colors.toString()) { if (stateColors.toString() !== config.colors.toString()) {
ret.colors = config.colors; ret.colors = config.colors;
} }
} else { } else if (JSON.stringify(state.colors) !== JSON.stringify(config.colors)) {
if (JSON.stringify(state.colors) !== JSON.stringify(config.colors)) { ret.colors = config.colors;
ret.colors = config.colors;
}
} }
} }
@ -168,7 +166,7 @@ const reducer = (state = initial, action) => {
openAt: { openAt: {
[action.uid]: Date.now() [action.uid]: Date.now()
} }
}, { deep: true }); }, {deep: true});
break; break;
case SESSION_RESIZE: case SESSION_RESIZE:
@ -181,12 +179,12 @@ const reducer = (state = initial, action) => {
case SESSION_PTY_EXIT: case SESSION_PTY_EXIT:
state_ = state state_ = state
.updateIn(['openAt'], (times) => { .updateIn(['openAt'], times => {
const times_ = times.asMutable(); const times_ = times.asMutable();
delete times_[action.uid]; delete times_[action.uid];
return times_; return times_;
}) })
.updateIn(['activityMarkers'], (markers) => { .updateIn(['activityMarkers'], markers => {
const markers_ = markers.asMutable(); const markers_ = markers.asMutable();
delete markers_[action.uid]; delete markers_[action.uid];
return markers_; return markers_;
@ -199,18 +197,22 @@ const reducer = (state = initial, action) => {
activityMarkers: { activityMarkers: {
[action.uid]: false [action.uid]: false
} }
}, { deep: true }); }, {deep: true});
break; break;
case SESSION_PTY_DATA: case SESSION_PTY_DATA: // eslint-disable-line no-case-declarations
// ignore activity markers for current tab // ignore activity markers for current tab
if (action.uid === state.activeUid) break; if (action.uid === state.activeUid) {
break;
}
// current time for comparisons // current time for comparisons
let now = Date.now(); const now = Date.now();
// if first data events after open, ignore // if first data events after open, ignore
if (now - state.openAt[action.uid] < 1000) break; if (now - state.openAt[action.uid] < 1000) {
break;
}
// we ignore activity markers that are within // we ignore activity markers that are within
// proximity of a resize event, since we // proximity of a resize event, since we
@ -221,7 +223,7 @@ const reducer = (state = initial, action) => {
activityMarkers: { activityMarkers: {
[action.uid]: true [action.uid]: true
} }
}, { deep: true }); }, {deep: true});
} }
break; break;
@ -254,7 +256,7 @@ const reducer = (state = initial, action) => {
notifications: { notifications: {
[action.id]: false [action.id]: false
} }
}, { deep: true }); }, {deep: true});
break; break;
case UPDATE_AVAILABLE: case UPDATE_AVAILABLE:
@ -270,18 +272,18 @@ const reducer = (state = initial, action) => {
if (CONFIG_LOAD !== action.type) { if (CONFIG_LOAD !== action.type) {
if (state_.fontSize !== state.fontSize || if (state_.fontSize !== state.fontSize ||
state_.fontSizeOverride !== state.fontSizeOverride) { state_.fontSizeOverride !== state.fontSizeOverride) {
state_ = state_.merge({ notifications: { font: true } }, { deep: true }); state_ = state_.merge({notifications: {font: true}}, {deep: true});
} }
} }
if (null != state.cols && null != state.rows && if (state.cols !== null && state.rows !== null &&
(state.rows !== state_.rows || (state.rows !== state_.rows ||
state.cols !== state_.cols)) { state.cols !== state_.cols)) {
state_ = state_.merge({ notifications: { resize: true } }, { deep: true }); state_ = state_.merge({notifications: {resize: true}}, {deep: true});
} }
if (state.updateVersion !== state_.updateVersion) { if (state.updateVersion !== state_.updateVersion) {
state_ = state_.merge({ notifications: { updates: true } }, { deep: true }); state_ = state_.merge({notifications: {updates: true}}, {deep: true});
} }
return state_; return state_;

View file

@ -1,2 +1,3 @@
import RPC from './utils/rpc'; import RPC from './utils/rpc';
export default new RPC(); export default new RPC();

View file

@ -1,3 +1,3 @@
export function last (arr) { export function last(arr) {
return arr[arr.length - 1]; return arr[arr.length - 1];
} }

View file

@ -19,13 +19,13 @@ const colorList = [
'grayscale' 'grayscale'
]; ];
export function getColorList (colors) { export function getColorList(colors) {
// For backwards compatibility, return early if it's already an array // For backwards compatibility, return early if it's already an array
if (Array.isArray(colors)) { if (Array.isArray(colors)) {
return colors; return colors;
} }
return colorList.map((colorName) => { return colorList.map(colorName => {
return colors[colorName]; return colors[colorName];
}); });
} }

View file

@ -1,11 +1,12 @@
import { ipcRenderer, remote } from 'electron'; import {ipcRenderer, remote} from 'electron';
const plugins = remote.require('./plugins'); const plugins = remote.require('./plugins');
export function getConfig () { export function getConfig() {
return plugins.getDecoratedConfig(); return plugins.getDecoratedConfig();
} }
export function subscribe (fn) { export function subscribe(fn) {
ipcRenderer.on('config change', fn); ipcRenderer.on('config change', fn);
ipcRenderer.on('plugins change', fn); ipcRenderer.on('plugins change', fn);
return () => { return () => {

View file

@ -5,7 +5,7 @@
// defer or add to existing side effects at will // defer or add to existing side effects at will
// as the result of an action being triggered // as the result of an action being triggered
export default (store) => (next) => (action) => { export default () => next => action => {
const ret = next(action); const ret = next(action);
if (action.effect) { if (action.effect) {
action.effect(); action.effect();

View file

@ -6,12 +6,14 @@
// Issue: https://github.com/kevva/executable/issues/9 // Issue: https://github.com/kevva/executable/issues/9
// PR: https://github.com/kevva/executable/pull/10 // PR: https://github.com/kevva/executable/pull/10
export function isExecutable (fileStat) { export function isExecutable(fileStat) {
if (process.platform === 'win32') return true; if (process.platform === 'win32') {
return true;
}
return Boolean( return Boolean(
(fileStat['mode'] & parseInt('0001', 8)) || (fileStat.mode & parseInt('0001', 8)) ||
(fileStat['mode'] & parseInt('0010', 8)) || (fileStat.mode & parseInt('0010', 8)) ||
(fileStat['mode'] & parseInt('0100', 8)) (fileStat.mode & parseInt('0100', 8))
); );
} }

View file

@ -1,6 +1,6 @@
/* global Notification */ /* global Notification */
/* eslint no-new:0 */ /* eslint no-new:0 */
export default function notify (title, body) { export default function notify(title, body) {
console.log(`[Notification] ${title}: ${body}`); console.log(`[Notification] ${title}: ${body}`);
new Notification(title, { body }); new Notification(title, {body});
} }

View file

@ -1,7 +1,7 @@
import vals from 'object-values'; import vals from 'object-values';
const valsCache = new WeakMap(); const valsCache = new WeakMap();
export function values (imm) { export function values(imm) {
if (!valsCache.has(imm)) { if (!valsCache.has(imm)) {
valsCache.set(imm, vals(imm)); valsCache.set(imm, vals(imm));
} }
@ -9,7 +9,7 @@ export function values (imm) {
} }
const keysCache = new WeakMap(); const keysCache = new WeakMap();
export function keys (imm) { export function keys(imm) {
if (!keysCache.has(imm)) { if (!keysCache.has(imm)) {
keysCache.set(imm, Object.keys(imm)); keysCache.set(imm, Object.keys(imm));
} }

View file

@ -1,13 +1,13 @@
import { remote } from 'electron'; import {remote} from 'electron';
import { connect as reduxConnect } from 'react-redux'; import {connect as reduxConnect} from 'react-redux';
// we expose these two deps to component decorators // we expose these two deps to component decorators
import React from 'react'; import React from 'react';
import Notification from '../components/notification'; import Notification from '../components/notification';
import notify from './notify'; import notify from './notify';
var Module = require('module'); const Module = require('module'); // eslint-disable-line import/newline-after-import
var originalLoad = Module._load; const originalLoad = Module._load;
Module._load = function (path) { Module._load = function (path) {
if (path === 'react') { if (path === 'react') {
return React; return React;
@ -16,7 +16,7 @@ Module._load = function (path) {
}; };
// remote interface to `../plugins` // remote interface to `../plugins`
let plugins = remote.require('./plugins'); const plugins = remote.require('./plugins');
// `require`d modules // `require`d modules
let modules; let modules;
@ -34,12 +34,14 @@ let tabsPropsDecorators;
let termPropsDecorators; let termPropsDecorators;
// the fs locations where usr plugins are stored // the fs locations where usr plugins are stored
const { path, localPath } = plugins.getBasePaths(); const {path, localPath} = plugins.getBasePaths();
const clearModulesCache = () => { const clearModulesCache = () => {
// trigger unload hooks // trigger unload hooks
modules.forEach((mod) => { modules.forEach(mod => {
if (mod.onRendererUnload) mod.onRendererUnload(window); if (mod.onRendererUnload) {
mod.onRendererUnload(window);
}
}); });
// clear require cache // clear require cache
@ -51,7 +53,7 @@ const clearModulesCache = () => {
} }
}; };
const getPluginName = (path) => window.require('path').basename(path); const getPluginName = path => window.require('path').basename(path);
const loadModules = () => { const loadModules = () => {
console.log('(re)loading renderer plugins'); console.log('(re)loading renderer plugins');
@ -59,10 +61,10 @@ const loadModules = () => {
// initialize cache that we populate with extension methods // initialize cache that we populate with extension methods
connectors = { connectors = {
Terms: { state: [], dispatch: [] }, Terms: {state: [], dispatch: []},
Header: { state: [], dispatch: [] }, Header: {state: [], dispatch: []},
HyperTerm: { state: [], dispatch: [] }, HyperTerm: {state: [], dispatch: []},
Notifications: { state: [], dispatch: [] } Notifications: {state: [], dispatch: []}
}; };
uiReducers = []; uiReducers = [];
middlewares = []; middlewares = [];
@ -72,7 +74,7 @@ const loadModules = () => {
termPropsDecorators = []; termPropsDecorators = [];
modules = paths.plugins.concat(paths.localPlugins) modules = paths.plugins.concat(paths.localPlugins)
.map((path) => { .map(path => {
let mod; let mod;
const pluginName = getPluginName(path); const pluginName = getPluginName(path);
@ -83,11 +85,13 @@ const loadModules = () => {
} catch (err) { } catch (err) {
console.error(err.stack); console.error(err.stack);
notify('Plugin load error', `"${pluginName}" failed to load in the renderer process. Check Developer Tools for details.`); notify('Plugin load error', `"${pluginName}" failed to load in the renderer process. Check Developer Tools for details.`);
return; return undefined;
} }
for (const i in mod) { for (const i in mod) {
mod[i]._pluginName = pluginName; if ({}.hasOwnProperty.call(mod, i)) {
mod[i]._pluginName = pluginName;
}
} }
if (mod.middleware) { if (mod.middleware) {
@ -152,13 +156,13 @@ const loadModules = () => {
return mod; return mod;
}) })
.filter((mod) => !!mod); .filter(mod => Boolean(mod));
}; };
// load modules for initial decoration // load modules for initial decoration
loadModules(); loadModules();
export function reload () { export function reload() {
clearModulesCache(); clearModulesCache();
loadModules(); loadModules();
// trigger re-decoration when components // trigger re-decoration when components
@ -166,13 +170,15 @@ export function reload () {
decorated = {}; decorated = {};
} }
export function getTermProps (uid, parentProps, props) { export function getTermProps(uid, parentProps, props) {
let props_; let props_;
termPropsDecorators.forEach((fn) => { termPropsDecorators.forEach(fn => {
let ret_; let ret_;
if (!props_) props_ = Object.assign({}, props); if (!props_) {
props_ = Object.assign({}, props);
}
try { try {
ret_ = fn(uid, parentProps, props_); ret_ = fn(uid, parentProps, props_);
@ -182,7 +188,7 @@ export function getTermProps (uid, parentProps, props) {
return; return;
} }
if (!ret_ || 'object' !== typeof ret_) { if (!ret_ || typeof ret_ !== 'object') {
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`getTermProps\` (object expected).`); notify('Plugin error', `${fn._pluginName}: Invalid return value of \`getTermProps\` (object expected).`);
return; return;
} }
@ -193,13 +199,15 @@ export function getTermProps (uid, parentProps, props) {
return props_ || props; return props_ || props;
} }
export function getTabsProps (parentProps, props) { export function getTabsProps(parentProps, props) {
let props_; let props_;
tabsPropsDecorators.forEach((fn) => { tabsPropsDecorators.forEach(fn => {
let ret_; let ret_;
if (!props_) props_ = Object.assign({}, props); if (!props_) {
props_ = Object.assign({}, props);
}
try { try {
ret_ = fn(parentProps, props_); ret_ = fn(parentProps, props_);
@ -209,7 +217,7 @@ export function getTabsProps (parentProps, props) {
return; return;
} }
if (!ret_ || 'object' !== typeof ret_) { if (!ret_ || typeof ret_ !== 'object') {
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`getTabsProps\` (object expected).`); notify('Plugin error', `${fn._pluginName}: Invalid return value of \`getTabsProps\` (object expected).`);
return; return;
} }
@ -220,13 +228,15 @@ export function getTabsProps (parentProps, props) {
return props_ || props; return props_ || props;
} }
export function getTabProps (tab, parentProps, props) { export function getTabProps(tab, parentProps, props) {
let props_; let props_;
tabPropsDecorators.forEach((fn) => { tabPropsDecorators.forEach(fn => {
let ret_; let ret_;
if (!props_) props_ = Object.assign({}, props); if (!props_) {
props_ = Object.assign({}, props);
}
try { try {
ret_ = fn(tab, parentProps, props_); ret_ = fn(tab, parentProps, props_);
@ -236,7 +246,7 @@ export function getTabProps (tab, parentProps, props) {
return; return;
} }
if (!ret_ || 'object' !== typeof ret_) { if (!ret_ || typeof ret_ !== 'object') {
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`getTabProps\` (object expected).`); notify('Plugin error', `${fn._pluginName}: Invalid return value of \`getTabProps\` (object expected).`);
return; return;
} }
@ -250,12 +260,12 @@ export function getTabProps (tab, parentProps, props) {
// connects + decorates a class // connects + decorates a class
// plugins can override mapToState, dispatchToProps // plugins can override mapToState, dispatchToProps
// and the class gets decorated (proxied) // and the class gets decorated (proxied)
export function connect (stateFn, dispatchFn, c, d = {}) { export function connect(stateFn, dispatchFn, c, d = {}) {
return function (Class, name) { return (Class, name) => {
return reduxConnect( return reduxConnect(
function (state) { state => {
let ret = stateFn(state); let ret = stateFn(state);
connectors[name].state.forEach((fn) => { connectors[name].state.forEach(fn => {
let ret_; let ret_;
try { try {
@ -266,7 +276,7 @@ export function connect (stateFn, dispatchFn, c, d = {}) {
return; return;
} }
if (!ret_ || 'object' !== typeof ret_) { if (!ret_ || typeof ret_ !== 'object') {
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`map${name}State\` (object expected).`); notify('Plugin error', `${fn._pluginName}: Invalid return value of \`map${name}State\` (object expected).`);
return; return;
} }
@ -275,9 +285,9 @@ export function connect (stateFn, dispatchFn, c, d = {}) {
}); });
return ret; return ret;
}, },
function (dispatch) { dispatch => {
let ret = dispatchFn(dispatch); let ret = dispatchFn(dispatch);
connectors[name].dispatch.forEach((fn) => { connectors[name].dispatch.forEach(fn => {
let ret_; let ret_;
try { try {
@ -288,7 +298,7 @@ export function connect (stateFn, dispatchFn, c, d = {}) {
return; return;
} }
if (!ret_ || 'object' !== typeof ret_) { if (!ret_ || typeof ret_ !== 'object') {
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`map${name}Dispatch\` (object expected).`); notify('Plugin error', `${fn._pluginName}: Invalid return value of \`map${name}Dispatch\` (object expected).`);
return; return;
} }
@ -303,11 +313,11 @@ export function connect (stateFn, dispatchFn, c, d = {}) {
}; };
} }
export function decorateUIReducer (fn) { export function decorateUIReducer(fn) {
return (state, action) => { return (state, action) => {
let state_ = fn(state, action); let state_ = fn(state, action);
uiReducers.forEach((pluginReducer) => { uiReducers.forEach(pluginReducer => {
let state__; let state__;
try { try {
@ -318,7 +328,7 @@ export function decorateUIReducer (fn) {
return; return;
} }
if (!state__ || 'object' !== typeof state__) { if (!state__ || typeof state__ !== 'object') {
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`reduceUI\`.`); notify('Plugin error', `${fn._pluginName}: Invalid return value of \`reduceUI\`.`);
return; return;
} }
@ -330,11 +340,11 @@ export function decorateUIReducer (fn) {
}; };
} }
export function decorateSessionsReducer (fn) { export function decorateSessionsReducer(fn) {
return (state, action) => { return (state, action) => {
let state_ = fn(state, action); let state_ = fn(state, action);
sessionsReducers.forEach((pluginReducer) => { sessionsReducers.forEach(pluginReducer => {
let state__; let state__;
try { try {
@ -345,7 +355,7 @@ export function decorateSessionsReducer (fn) {
return; return;
} }
if (!state__ || 'object' !== typeof state__) { if (!state__ || typeof state__ !== 'object') {
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`reduceSessions\`.`); notify('Plugin error', `${fn._pluginName}: Invalid return value of \`reduceSessions\`.`);
return; return;
} }
@ -358,18 +368,18 @@ export function decorateSessionsReducer (fn) {
} }
// redux middleware generator // redux middleware generator
export const middleware = (store) => (next) => (action) => { export const middleware = store => next => action => {
const nextMiddleware = remaining => action => remaining.length const nextMiddleware = remaining => action => remaining.length ?
? remaining[0](store)(nextMiddleware(remaining.slice(1)))(action) remaining[0](store)(nextMiddleware(remaining.slice(1)))(action) :
: next(action); next(action);
nextMiddleware(middlewares)(action); nextMiddleware(middlewares)(action);
}; };
function getDecorated (parent, name) { function getDecorated(parent, name) {
if (!decorated[name]) { if (!decorated[name]) {
let class_ = parent; let class_ = parent;
modules.forEach((mod) => { modules.forEach(mod => {
const method = 'decorate' + name; const method = 'decorate' + name;
const fn = mod[method]; const fn = mod[method];
@ -377,14 +387,14 @@ function getDecorated (parent, name) {
let class__; let class__;
try { try {
class__ = fn(class_, { React, Notification, notify }); class__ = fn(class_, {React, Notification, notify});
} catch (err) { } catch (err) {
console.error(err.stack); console.error(err.stack);
notify('Plugin error', `${fn._pluginName}: Error occurred in \`${method}\`. Check Developer Tools for details`); notify('Plugin error', `${fn._pluginName}: Error occurred in \`${method}\`. Check Developer Tools for details`);
return; return;
} }
if (!class__ || 'function' !== typeof class__.prototype.render) { if (!class__ || typeof class__.prototype.render !== 'function') {
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`${method}\`. No \`render\` method found. Please return a \`React.Component\`.`); notify('Plugin error', `${fn._pluginName}: Invalid return value of \`${method}\`. No \`render\` method found. Please return a \`React.Component\`.`);
return; return;
} }
@ -402,11 +412,11 @@ function getDecorated (parent, name) {
// for each component, we return a higher-order component // for each component, we return a higher-order component
// that wraps with the higher-order components // that wraps with the higher-order components
// exposed by plugins // exposed by plugins
export function decorate (Component, name) { export function decorate(Component, name) {
return class extends React.Component { return class extends React.Component {
render () { render() {
const Sub = getDecorated(Component, name); const Sub = getDecorated(Component, name);
return <Sub {...this.props} />; return <Sub {...this.props}/>;
} }
}; };
} }

View file

@ -1,6 +1,6 @@
export default class Client { export default class Client {
constructor () { constructor() {
const electron = window.require('electron'); const electron = window.require('electron');
const EventEmitter = window.require('events'); const EventEmitter = window.require('events');
this.emitter = new EventEmitter(); this.emitter = new EventEmitter();
@ -25,32 +25,34 @@ export default class Client {
} }
} }
ipcListener (event, { ch, data }) { ipcListener(event, {ch, data}) {
this.emitter.emit(ch, data); this.emitter.emit(ch, data);
} }
on (ev, fn) { on(ev, fn) {
this.emitter.on(ev, fn); this.emitter.on(ev, fn);
} }
once (ev, fn) { once(ev, fn) {
this.emitter.once(ev, fn); this.emitter.once(ev, fn);
} }
emit (ev, data) { emit(ev, data) {
if (!this.id) throw new Error('Not ready'); if (!this.id) {
this.ipc.send(this.id, { ev, data }); throw new Error('Not ready');
}
this.ipc.send(this.id, {ev, data});
} }
removeListener (ev, fn) { removeListener(ev, fn) {
this.emitter.removeListener(ev, fn); this.emitter.removeListener(ev, fn);
} }
removeAllListeners () { removeAllListeners() {
this.emitter.removeAllListeners(); this.emitter.removeAllListeners();
} }
destroy () { destroy() {
this.removeAllListeners(); this.removeAllListeners();
this.ipc.removeAllListeners(); this.ipc.removeAllListeners();
} }

View file

@ -6,7 +6,7 @@ exports.clear = function (terminal) {
// Use selection extend upon dblclick // Use selection extend upon dblclick
exports.extend = function (terminal) { exports.extend = function (terminal) {
let sel = terminal.document_.getSelection(); const sel = terminal.document_.getSelection();
// Test if focusNode exist and nodeName is #text // Test if focusNode exist and nodeName is #text
if (sel.focusNode && sel.focusNode.nodeName === '#text') { if (sel.focusNode && sel.focusNode.nodeName === '#text') {
@ -17,10 +17,13 @@ exports.extend = function (terminal) {
// Fix a bug in ScrollPort selectAll behavior // Fix a bug in ScrollPort selectAll behavior
// Select all rows in the viewport // Select all rows in the viewport
exports.all = function (terminal) { exports.all = function (terminal) {
let scrollPort = terminal.scrollPort_; const scrollPort = terminal.scrollPort_;
let firstRow, lastRowIndex, lastRow; let firstRow;
let lastRow;
if (scrollPort.topFold_.nextSibling.rowIndex !== 0) { if (scrollPort.topFold_.nextSibling.rowIndex === 0) {
firstRow = scrollPort.topFold_.nextSibling;
} else {
while (scrollPort.topFold_.previousSibling) { while (scrollPort.topFold_.previousSibling) {
scrollPort.rowNodes_.removeChild(scrollPort.topFold_.previousSibling); scrollPort.rowNodes_.removeChild(scrollPort.topFold_.previousSibling);
} }
@ -28,21 +31,19 @@ exports.all = function (terminal) {
firstRow = scrollPort.fetchRowNode_(0); firstRow = scrollPort.fetchRowNode_(0);
scrollPort.rowNodes_.insertBefore(firstRow, scrollPort.topFold_); scrollPort.rowNodes_.insertBefore(firstRow, scrollPort.topFold_);
scrollPort.syncRowNodesDimensions_(); scrollPort.syncRowNodesDimensions_();
} else {
firstRow = scrollPort.topFold_.nextSibling;
} }
lastRowIndex = scrollPort.rowProvider_.getRowCount() - 1; const lastRowIndex = scrollPort.rowProvider_.getRowCount() - 1;
if (scrollPort.bottomFold_.previousSibling.rowIndex !== lastRowIndex) { if (scrollPort.bottomFold_.previousSibling.rowIndex === lastRowIndex) {
lastRow = scrollPort.bottomFold_.previousSibling.rowIndex;
} else {
while (scrollPort.bottomFold_.nextSibling) { while (scrollPort.bottomFold_.nextSibling) {
scrollPort.rowNodes_.removeChild(scrollPort.bottomFold_.nextSibling); scrollPort.rowNodes_.removeChild(scrollPort.bottomFold_.nextSibling);
} }
lastRow = scrollPort.fetchRowNode_(lastRowIndex); lastRow = scrollPort.fetchRowNode_(lastRowIndex);
scrollPort.rowNodes_.appendChild(lastRow); scrollPort.rowNodes_.appendChild(lastRow);
} else {
lastRow = scrollPort.bottomFold_.previousSibling.rowIndex;
} }
scrollPort.selection.sync(); scrollPort.selection.sync();

View file

@ -2,12 +2,16 @@ import * as regex from './url-regex';
export const domainRegex = /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b|^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*:)*?:?0*1$/; export const domainRegex = /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b|^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*:)*?:?0*1$/;
export default function isUrlCommand (shell, data) { export default function isUrlCommand(shell, data) {
const matcher = regex[shell]; const matcher = regex[shell]; // eslint-disable-line import/namespace
if (undefined === matcher || !data) return null; if (undefined === matcher || !data) {
return null;
}
const match = data.match(matcher); const match = data.match(matcher);
if (!match) return null; if (!match) {
return null;
}
const protocol = match[1]; const protocol = match[1];
const path = match[2]; const path = match[2];

View file

@ -36,51 +36,47 @@
"devDependencies": { "devDependencies": {
"babel-cli": "^6.11.4", "babel-cli": "^6.11.4",
"babel-core": "^6.11.4", "babel-core": "^6.11.4",
"babel-eslint": "^6.1.2",
"babel-loader": "^6.2.4", "babel-loader": "^6.2.4",
"babel-preset-react": "^6.11.1", "babel-preset-react": "^6.11.1",
"copy-webpack-plugin": "^3.0.1", "copy-webpack-plugin": "^3.0.1",
"electron-builder": "^7.0.1", "electron-builder": "^7.0.1",
"electron": "1.4.0", "electron": "1.4.0",
"eslint": "^3.2.0", "eslint-config-xo-react": "^0.9.0",
"eslint-config-standard": "^5.3.5", "eslint-plugin-react": "^6.2.2",
"eslint-plugin-promise": "^2.0.0",
"eslint-plugin-react": "^6.0.0",
"eslint-plugin-standard": "^2.0.0",
"husky": "^0.11.6", "husky": "^0.11.6",
"webpack": "^2.1.0-beta.15" "webpack": "^2.1.0-beta.15",
"xo": "^0.16.0"
}, },
"eslintConfig": { "xo": {
"extends": "standard", "extends": "xo-react",
"plugins": [ "esnext": true,
"react" "space": true,
"env": [
"browser",
"node",
"mocha"
], ],
"rules": { "rules": {
"yoda": "off", "react/jsx-filename-extension": 0,
"semi": [ "react/prop-types": 0,
"error", "babel/new-cap": 0,
"always" "quote-props": 0,
], "import/no-extraneous-dependencies": 0,
"no-unused-vars": "error", "no-warning-comments": 0,
"no-extra-semi": "error", "complexity": 0,
"semi-spacing": [ "react/no-danger": 0,
"error", "react/no-string-refs": 0,
{ "react/jsx-key": 0,
"before": false, "no-nested-ternary": 0,
"after": true "react/jsx-no-bind": 0
}
],
"react/jsx-uses-react": "warn",
"react/jsx-uses-vars": "warn"
}, },
"parserOptions": { "ignore": [
"ecmaFeatures": { "dist",
"jsx": true "build",
} "app/dist",
}, "app/static",
"env": { "assets"
"mocha": true ]
}
}, },
"babel": { "babel": {
"presets": [ "presets": [
@ -106,7 +102,7 @@
}, },
"scripts": { "scripts": {
"dev": "webpack --watch", "dev": "webpack --watch",
"lint": "eslint .", "lint": "xo",
"build": "NODE_ENV=production webpack", "build": "NODE_ENV=production webpack",
"test": "npm run lint && electron-mocha test/*", "test": "npm run lint && electron-mocha test/*",
"start": "electron app", "start": "electron app",

View file

@ -1,3 +1,4 @@
/* eslint-disable prefer-arrow-callback */
require('../setup'); require('../setup');
const {_toDependencies} = require('../../app/plugins'); const {_toDependencies} = require('../../app/plugins');
@ -9,7 +10,7 @@ describe('plugins', function () {
'@org1/project4#1.0.0', '@org2/project5@alpha', '@org1/project4#1.0.0', '@org2/project5@alpha',
'@org3/project6']; '@org3/project6'];
_toDependencies({plugins: plugins}).should.be.eql({ _toDependencies({plugins}).should.be.eql({
'project1': 'latest', 'project1': 'latest',
'project2': '1.0.0', 'project2': '1.0.0',
'project3': 'beta', 'project3': 'beta',

View file

@ -1,6 +1,7 @@
const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const Copy = require('copy-webpack-plugin'); const Copy = require('copy-webpack-plugin');
const path = require('path');
const nodeEnv = process.env.NODE_ENV || 'development'; const nodeEnv = process.env.NODE_ENV || 'development';
const isProd = nodeEnv === 'production'; const isProd = nodeEnv === 'production';
@ -27,7 +28,7 @@ module.exports = {
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': { 'process.env': { // eslint-disable-line quote-props
'NODE_ENV': JSON.stringify(nodeEnv) 'NODE_ENV': JSON.stringify(nodeEnv)
} }
}), }),