mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-17 05:58:41 -09:00
first pass at plugins system
This commit is contained in:
parent
eb147d9b9a
commit
9f635021c9
4 changed files with 322 additions and 12 deletions
|
|
@ -2,8 +2,15 @@ import { render } from 'react-dom';
|
||||||
import HyperTerm from './hyperterm';
|
import HyperTerm from './hyperterm';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Config from './config';
|
import Config from './config';
|
||||||
|
import Plugins from './plugins';
|
||||||
|
|
||||||
require('./css/hyperterm.css');
|
require('./css/hyperterm.css');
|
||||||
require('./css/tabs.css');
|
require('./css/tabs.css');
|
||||||
|
|
||||||
render(<Config><HyperTerm /></Config>, document.getElementById('mount'));
|
const app = <Config>
|
||||||
|
<Plugins>
|
||||||
|
<HyperTerm />
|
||||||
|
</Plugins>
|
||||||
|
</Config>;
|
||||||
|
|
||||||
|
render(app, document.getElementById('mount'));
|
||||||
|
|
|
||||||
18
app/plugins.js
Normal file
18
app/plugins.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default class Plugins extends React.Component {
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const child = React.Children.only(this.props.children);
|
||||||
|
return React.cloneElement(child, this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
index.js
48
index.js
|
|
@ -143,16 +143,30 @@ app.on('ready', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// the window can be closed by the browser process itself
|
// expose internals to extension authors
|
||||||
win.on('close', () => {
|
|
||||||
rpc.destroy();
|
|
||||||
deleteSessions();
|
|
||||||
winCount--;
|
|
||||||
cfgUnsubscribe();
|
|
||||||
});
|
|
||||||
|
|
||||||
win.rpc = rpc;
|
win.rpc = rpc;
|
||||||
win.sessions = sessions;
|
win.sessions = sessions;
|
||||||
|
|
||||||
|
const load = () => {
|
||||||
|
plugins.onWindow(win);
|
||||||
|
};
|
||||||
|
|
||||||
|
// load plugins
|
||||||
|
load();
|
||||||
|
|
||||||
|
const pluginsUnsubscribe = plugins.subscribe(() => {
|
||||||
|
load();
|
||||||
|
win.webContents.send('plugins change');
|
||||||
|
});
|
||||||
|
|
||||||
|
// the window can be closed by the browser process itself
|
||||||
|
win.on('close', () => {
|
||||||
|
winCount--;
|
||||||
|
rpc.destroy();
|
||||||
|
deleteSessions();
|
||||||
|
cfgUnsubscribe();
|
||||||
|
pluginsUnsubscribe();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// when opening create a new window
|
// when opening create a new window
|
||||||
|
|
@ -167,9 +181,21 @@ app.on('ready', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// set menu
|
const setupMenu = () => {
|
||||||
const tpl = createMenu({ createWindow });
|
const tpl = plugins.decorateMenu(createMenu({
|
||||||
Menu.setApplicationMenu(Menu.buildFromTemplate(tpl));
|
createWindow,
|
||||||
|
updatePlugins: plugins.updatePlugins
|
||||||
|
}));
|
||||||
|
Menu.setApplicationMenu(Menu.buildFromTemplate(tpl));
|
||||||
|
};
|
||||||
|
|
||||||
|
const load = () => {
|
||||||
|
plugins.onApp(app);
|
||||||
|
setupMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
|
plugins.subscribe(load);
|
||||||
});
|
});
|
||||||
|
|
||||||
function initSession (opts, fn) {
|
function initSession (opts, fn) {
|
||||||
|
|
|
||||||
259
plugins.js
Normal file
259
plugins.js
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
const { dialog } = require('electron');
|
||||||
|
const { homedir } = require('os');
|
||||||
|
const { resolve, basename } = require('path');
|
||||||
|
const { writeFileSync } = require('fs');
|
||||||
|
const config = require('./config');
|
||||||
|
const { sync: mkdirpSync } = require('mkdirp');
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
const Config = require('electron-config');
|
||||||
|
const ms = require('ms');
|
||||||
|
const notify = require('./notify');
|
||||||
|
|
||||||
|
// local storage
|
||||||
|
const cache = new Config();
|
||||||
|
|
||||||
|
// modules path
|
||||||
|
const path = resolve(homedir(), '.hyperterm_modules');
|
||||||
|
const localPath = resolve(homedir(), '.hyperterm_local_modules');
|
||||||
|
|
||||||
|
// init plugin directories if not present
|
||||||
|
mkdirpSync(path);
|
||||||
|
mkdirpSync(localPath);
|
||||||
|
|
||||||
|
// caches
|
||||||
|
let plugins = config.getPlugins();
|
||||||
|
let paths = getPaths(plugins);
|
||||||
|
let id = getId(plugins);
|
||||||
|
let modules = requirePlugins();
|
||||||
|
|
||||||
|
function getId (plugins_) {
|
||||||
|
return JSON.stringify(plugins_);
|
||||||
|
}
|
||||||
|
|
||||||
|
const watchers = [];
|
||||||
|
|
||||||
|
// we listen on configuration updates to trigger
|
||||||
|
// plugin installation
|
||||||
|
config.subscribe(() => {
|
||||||
|
const plugins_ = config.getPlugins();
|
||||||
|
if (plugins !== plugins_) {
|
||||||
|
const id_ = getId(plugins_);
|
||||||
|
if (id !== id_) {
|
||||||
|
id = id_;
|
||||||
|
plugins = plugins_;
|
||||||
|
updatePlugins();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let updating = false;
|
||||||
|
|
||||||
|
function updatePlugins () {
|
||||||
|
if (updating) return notify('Plugin update in progress');
|
||||||
|
updating = true;
|
||||||
|
syncPackageJSON();
|
||||||
|
const id_ = id;
|
||||||
|
install((err) => {
|
||||||
|
updating = false;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
notify(
|
||||||
|
'Error updating plugins.',
|
||||||
|
'Check `~/.hyperterm_modules/npm-debug.log` for more information.'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// flag successful plugin update
|
||||||
|
cache.set('plugins', id_);
|
||||||
|
|
||||||
|
// cache paths
|
||||||
|
paths = getPaths(plugins);
|
||||||
|
|
||||||
|
// clear require cache
|
||||||
|
paths.plugins.forEach(clearCache);
|
||||||
|
paths.localPlugins.forEach(clearCache);
|
||||||
|
|
||||||
|
// cache modules
|
||||||
|
modules = requirePlugins();
|
||||||
|
|
||||||
|
// notify watchers
|
||||||
|
watchers.forEach((fn) => fn(err));
|
||||||
|
|
||||||
|
// we consider it a success if we loaded *all* modules
|
||||||
|
if ((paths.plugins.length + paths.localPlugins.length) === modules.length) {
|
||||||
|
notify('HyperTerm plugins updated!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCache (mod) {
|
||||||
|
for (const entry in require.cache) {
|
||||||
|
if (0 === entry.indexOf(mod + '/')) {
|
||||||
|
delete require.cache[entry];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.updatePlugins = updatePlugins;
|
||||||
|
|
||||||
|
// we schedule the initial plugins update
|
||||||
|
// a bit after the user launches the terminal
|
||||||
|
// to prevent slowness
|
||||||
|
if (cache.get('plugins') !== id) {
|
||||||
|
// install immediately if the user changed plugins
|
||||||
|
console.log('plugins have changed / not init, scheduling plugins installation');
|
||||||
|
setTimeout(() => {
|
||||||
|
updatePlugins();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise update plugins every 5 hours
|
||||||
|
setInterval(updatePlugins, ms('5h'));
|
||||||
|
|
||||||
|
function syncPackageJSON () {
|
||||||
|
const dependencies = toDependencies(plugins);
|
||||||
|
const pkg = {
|
||||||
|
name: 'hyperterm-plugins',
|
||||||
|
private: true,
|
||||||
|
version: '0.0.1',
|
||||||
|
homepage: 'https://hyperterm.org',
|
||||||
|
dependencies
|
||||||
|
};
|
||||||
|
|
||||||
|
const file = resolve(path, 'package.json');
|
||||||
|
try {
|
||||||
|
writeFileSync(file, JSON.stringify(pkg, null, 2));
|
||||||
|
} catch (err) {
|
||||||
|
alert(`An error occurred writing to ${file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function alert (message) {
|
||||||
|
dialog.showMessageBox({
|
||||||
|
message,
|
||||||
|
buttons: ['Ok']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDependencies (plugins) {
|
||||||
|
const obj = {};
|
||||||
|
plugins.plugins.forEach((plugin) => {
|
||||||
|
const pieces = plugin.split('#');
|
||||||
|
obj[pieces[0]] = null == pieces[1] ? 'latest' : pieces[1];
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function install (fn) {
|
||||||
|
exec('npm prune && npm install --production', {
|
||||||
|
cwd: path
|
||||||
|
}, (err, stdout, stderr) => {
|
||||||
|
if (err) {
|
||||||
|
if (/(command not found|not recognized as an)/.test(err.message)) {
|
||||||
|
if (plugins.plugins.length) {
|
||||||
|
alert('We found `plugins` in `.hyperterm.js`, but `npm` is ' +
|
||||||
|
'not installed or not in $PATH!\nPlease head to ' +
|
||||||
|
'https://nodejs.org and install the Node.js runtime.');
|
||||||
|
} else {
|
||||||
|
console.log('npm not found, but no plugins defined');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fn(err);
|
||||||
|
}
|
||||||
|
fn(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.subscribe = function (fn) {
|
||||||
|
watchers.push(fn);
|
||||||
|
return () => {
|
||||||
|
watchers.splice(watchers.indexOf(fn), 1);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function getPaths () {
|
||||||
|
return {
|
||||||
|
plugins: plugins.plugins.map((name) => {
|
||||||
|
return resolve(path, 'node_modules', name);
|
||||||
|
}),
|
||||||
|
localPlugins: plugins.localPlugins.map((name) => {
|
||||||
|
return resolve(localPath, name);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getPaths = getPaths;
|
||||||
|
|
||||||
|
function requirePlugins () {
|
||||||
|
const { plugins, localPlugins } = paths;
|
||||||
|
|
||||||
|
const load = (path) => {
|
||||||
|
let mod;
|
||||||
|
try {
|
||||||
|
mod = require(path);
|
||||||
|
|
||||||
|
if (!mod || (!mod.onApp && !mod.onWindow && !mod.onUnload &&
|
||||||
|
!mod.decorateConfig && !mod.decorateMenu &&
|
||||||
|
!mod.decorateTerm && !mod.decorateHyperTerm &&
|
||||||
|
!mod.decorateTabs && !mod.decorateConfig)) {
|
||||||
|
notify('Plugin error!', `Plugin "${basename(path)}" does not expose any ` +
|
||||||
|
'HyperTerm extension API methods');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return mod;
|
||||||
|
} catch (err) {
|
||||||
|
notify('Plugin error!', `Plugin "${basename(path)}" failed to load (${err.message})`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return plugins.map(load)
|
||||||
|
.concat(localPlugins.map(load))
|
||||||
|
.filter(v => !!v);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.onApp = function (app) {
|
||||||
|
modules.forEach((plugin) => {
|
||||||
|
if (plugin.onApp) {
|
||||||
|
plugin.onApp(app);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.onWindow = function (win, app) {
|
||||||
|
modules.forEach((plugin) => {
|
||||||
|
if (plugin.onWindow) {
|
||||||
|
plugin.onWindow(app);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.decorateTerm = function (Term) {
|
||||||
|
return Term;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.decorateTabs = function (Tabs) {
|
||||||
|
return Tabs;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.decorateHyperTerm = function (HyperTerm) {
|
||||||
|
return HyperTerm;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.decorateMenu = function (tpl) {
|
||||||
|
return tpl;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.decorateConfig = function (config) {
|
||||||
|
let decorated = config;
|
||||||
|
modules.forEach((plugin) => {
|
||||||
|
if (plugin.decorateConfig) {
|
||||||
|
const res = plugin.decorateConfig(decorated);
|
||||||
|
if (res) {
|
||||||
|
decorated = res;
|
||||||
|
} else {
|
||||||
|
console.error('incompatible response type for `decorateConfig`');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return decorated;
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue