mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-13 12:38:39 -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 React from 'react';
|
||||
import Config from './config';
|
||||
import Plugins from './plugins';
|
||||
|
||||
require('./css/hyperterm.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
|
||||
win.on('close', () => {
|
||||
rpc.destroy();
|
||||
deleteSessions();
|
||||
winCount--;
|
||||
cfgUnsubscribe();
|
||||
});
|
||||
|
||||
// expose internals to extension authors
|
||||
win.rpc = rpc;
|
||||
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
|
||||
|
|
@ -167,9 +181,21 @@ app.on('ready', () => {
|
|||
}
|
||||
});
|
||||
|
||||
// set menu
|
||||
const tpl = createMenu({ createWindow });
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(tpl));
|
||||
const setupMenu = () => {
|
||||
const tpl = plugins.decorateMenu(createMenu({
|
||||
createWindow,
|
||||
updatePlugins: plugins.updatePlugins
|
||||
}));
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(tpl));
|
||||
};
|
||||
|
||||
const load = () => {
|
||||
plugins.onApp(app);
|
||||
setupMenu();
|
||||
};
|
||||
|
||||
load();
|
||||
plugins.subscribe(load);
|
||||
});
|
||||
|
||||
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