diff --git a/app/css/hyperterm.css b/app/css/hyperterm.css
index f4610ed9..39abf185 100644
--- a/app/css/hyperterm.css
+++ b/app/css/hyperterm.css
@@ -63,3 +63,23 @@ header {
.resize-indicator.showing {
opacity: 1;
}
+
+.update-indicator {
+ background: rgba(255, 51, 76, .8);
+ padding: 6px 14px;
+ color: #fff;
+ font: 11px Menlo;
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ opacity: 0;
+ transition: opacity 150ms ease-in;
+}
+
+.update-indicator a {
+ color: #fff;
+}
+
+.update-indicator.showing {
+ opacity: 1;
+}
diff --git a/app/hyperterm.js b/app/hyperterm.js
index b44b24ed..4d2d3fa8 100644
--- a/app/hyperterm.js
+++ b/app/hyperterm.js
@@ -6,6 +6,7 @@ import classes from 'classnames';
import getTextMetrics from './text-metrics';
import shallowCompare from 'react-addons-shallow-compare';
import React, { Component } from 'react';
+import UpdateChecker from './update-checker';
export default class HyperTerm extends Component {
constructor () {
@@ -19,7 +20,8 @@ export default class HyperTerm extends Component {
active: null,
activeMarkers: [],
mac: /Mac/.test(navigator.userAgent),
- resizeIndicatorShowing: false
+ resizeIndicatorShowing: false,
+ updateVersion: null
};
// we set this to true when the first tab
@@ -34,6 +36,7 @@ export default class HyperTerm extends Component {
this.onResize = this.onResize.bind(this);
this.onChange = this.onChange.bind(this);
+ this.openExternal = this.openExternal.bind(this);
this.focusActive = this.focusActive.bind(this);
this.onHeaderMouseDown = this.onHeaderMouseDown.bind(this);
@@ -80,9 +83,19 @@ export default class HyperTerm extends Component {
{ this.state.cols }x{ this.state.rows }
+
+ Update available (
{ this.state.updateVersion }).
+ {' '}
+
Download
+
;
}
+ openExternal (ev) {
+ ev.preventDefault();
+ this.rpc.emit('open external', { url: ev.target.href });
+ }
+
requestTab () {
this.rpc.emit('new', this.getDimensions());
}
@@ -156,6 +169,7 @@ export default class HyperTerm extends Component {
componentDidMount () {
this.rpc = new RPC();
+ this.updateChecker = new UpdateChecker(this.onUpdateAvailable.bind(this));
this.setState(this.getDimensions());
// open a new tab upon mounting
@@ -218,6 +232,10 @@ export default class HyperTerm extends Component {
Mousetrap.bind('command+alt+right', this.moveRight);
}
+ onUpdateAvailable (updateVersion) {
+ this.setState({ updateVersion });
+ }
+
moveTo (n) {
if (this.state.sessions[n]) {
this.setActive(n);
@@ -359,5 +377,6 @@ export default class HyperTerm extends Component {
this.rpc.destroy();
clearTimeout(this.resizeIndicatorTimeout);
Mousetrap.reset();
+ this.updateChecker.destroy();
}
}
diff --git a/app/package.json b/app/package.json
index b1e51117..7f9c90f8 100644
--- a/app/package.json
+++ b/app/package.json
@@ -7,7 +7,9 @@
"mousetrap": "1.6.0",
"classnames": "2.2.5",
"react": "15.1.0",
- "react-dom": "15.1.0"
+ "react-dom": "15.1.0",
+ "semver-compare": "^1.0.0",
+ "json-loader": "^0.5.4"
},
"devDependencies": {
"eslint": "2.13.1",
@@ -30,10 +32,19 @@
],
"rules": {
"yoda": 0,
- "semi": [2, "always"],
+ "semi": [
+ 2,
+ "always"
+ ],
"no-unused-vars": 2,
"no-extra-semi": 2,
- "semi-spacing": [2, { "before": false, "after": true }],
+ "semi-spacing": [
+ 2,
+ {
+ "before": false,
+ "after": true
+ }
+ ],
"react/jsx-uses-react": 1,
"react/jsx-uses-vars": 1
},
@@ -54,4 +65,4 @@
"lint": "eslint *.js",
"build": "NODE_ENV=production webpack"
}
-}
+}
\ No newline at end of file
diff --git a/app/update-checker.js b/app/update-checker.js
new file mode 100644
index 00000000..0e8a907b
--- /dev/null
+++ b/app/update-checker.js
@@ -0,0 +1,58 @@
+/*global fetch:false*/
+import { version as currentVersion } from '../package';
+import compare from 'semver-compare';
+
+export default class UpdateChecker {
+
+ constructor (fn, { interval = 5000 } = {}) {
+ this.callback = fn;
+ this.interval = interval;
+ this.check();
+ this.lastKnown = null;
+ }
+
+ check () {
+ const done = () => {
+ this.checkTimer = setTimeout(() => {
+ this.check();
+ }, this.interval);
+ };
+
+ console.log('checking for update');
+ fetch('https://hyperterm.now.sh/data.json')
+ .then((res) => {
+ if (200 !== res.status) {
+ console.error('Update check error. Status (%d)', res.status);
+ return done();
+ }
+
+ res.json()
+ .then(({ version }) => {
+ if (this.lastKnown !== version) {
+ this.lastKnown = version;
+
+ if (1 === compare(version, currentVersion)) {
+ console.log('update found');
+ this.callback(version);
+ } else {
+ console.log('no update. latest:', version);
+ }
+ }
+ done();
+ })
+ .catch((err) => {
+ console.error('Update JSON parse error', err.stack);
+ done();
+ });
+ }).catch((err) => {
+ console.error('Update check error', err.stack);
+ done();
+ });
+ }
+
+ destroy () {
+ this.aborted = true;
+ clearTimeout(this.checkTimer);
+ }
+
+}
diff --git a/app/webpack.config.js b/app/webpack.config.js
index 8b427534..fa00d3c4 100644
--- a/app/webpack.config.js
+++ b/app/webpack.config.js
@@ -20,6 +20,10 @@ module.exports = {
'babel-loader'
]
},
+ {
+ test: /\.json/,
+ loader: 'json-loader'
+ },
{
test: /\.css$/,
loader: 'style-loader!css-loader'
diff --git a/index.js b/index.js
index f29d80f9..59f4e77e 100644
--- a/index.js
+++ b/index.js
@@ -1,4 +1,4 @@
-const { app, BrowserWindow, Menu } = require('electron');
+const { app, BrowserWindow, shell, Menu } = require('electron');
const createRPC = require('./rpc');
const Session = require('./session');
const genUid = require('uid2');
@@ -81,6 +81,10 @@ app.on('ready', () => {
sessions.get(uid).write(data);
});
+ rpc.on('open external', ({ url }) => {
+ shell.openExternal(url);
+ });
+
const deleteSessions = () => {
sessions.forEach((session, key) => {
session.removeAllListeners();