Implements Commands Key mapping (#1876)

Keymaps part 2
This commit is contained in:
Philippe Potvin 2017-06-02 20:03:47 -04:00 committed by GitHub
parent 342580e730
commit 93b2229ff5
24 changed files with 441 additions and 329 deletions

View file

@ -1,136 +0,0 @@
const platform = process.platform;
const isMac = platform === 'darwin';
const prefix = isMac ? 'Cmd' : 'Ctrl';
const applicationMenu = { // app/menu.js
preferences: ',',
quit: isMac ? 'Q' : '',
// Shell/File menu
newWindow: 'N',
newTab: 'T',
splitVertically: isMac ? 'D' : 'Shift+E',
splitHorizontally: isMac ? 'Shift+D' : 'Shift+O',
closeSession: 'W',
closeWindow: 'Shift+W',
// Edit menu
undo: 'Z',
redo: 'Shift+Z',
cut: 'X',
copy: isMac ? 'C' : 'Shift+C',
paste: 'V',
selectAll: 'A',
clear: 'K',
emojis: isMac ? 'Ctrl+Cmd+Space' : '',
// View menu
reload: 'R',
fullReload: 'Shift+R',
toggleDevTools: isMac ? 'Alt+I' : 'Shift+I',
resetZoom: '0',
zoomIn: 'plus',
zoomOut: '-',
// Plugins menu
updatePlugins: 'Shift+U',
// Window menu
minimize: 'M',
showPreviousTab: 'Alt+Left',
showNextTab: 'Alt+Right',
selectNextPane: 'Ctrl+Alt+Tab',
selectPreviousPane: 'Ctrl+Shift+Alt+Tab',
enterFullScreen: isMac ? 'Ctrl+Cmd+F' : 'F11'
};
const mousetrap = { // lib/containers/hyper.js
moveTo1: '1',
moveTo2: '2',
moveTo3: '3',
moveTo4: '4',
moveTo5: '5',
moveTo6: '6',
moveTo7: '7',
moveTo8: '8',
moveToLast: '9',
// here `1`, `2` etc are used to "emulate" something like `moveLeft: ['...', '...', etc]`
moveLeft1: 'Shift+Left',
moveRight1: 'Shift+Right',
moveLeft2: 'Shift+{',
moveRight2: 'Shift+}',
moveLeft3: 'Alt+Left',
moveRight3: 'Alt+Right',
moveLeft4: 'Ctrl+Shift+Tab',
moveRight4: 'Ctrl+Tab',
// here we add `+` at the beginning to prevent the prefix from being added
moveWordLeft: '+Alt+Left',
moveWordRight: '+Alt+Right',
deleteWordLeft: '+Alt+Backspace',
deleteWordRight: '+Alt+Delete',
deleteLine: 'Backspace',
moveToStart: 'Left',
moveToEnd: 'Right',
selectAll: 'A'
};
const allAccelerators = Object.assign({}, applicationMenu, mousetrap);
const cache = [];
// ^ here we store the shortcuts so we don't need to
// look into the `allAccelerators` everytime
for (const key in allAccelerators) {
if ({}.hasOwnProperty.call(allAccelerators, key)) {
let value = allAccelerators[key];
if (value) {
if (value.startsWith('+')) {
// we don't need to add the prefix to accelerators starting with `+`
value = value.slice(1);
} else if (!value.startsWith('Ctrl')) { // nor to the ones starting with `Ctrl`
value = `${prefix}+${value}`;
}
cache.push(value.toLowerCase());
allAccelerators[key] = value;
}
}
}
// decides if a keybard event is a Hyper Accelerator
function isAccelerator(e) {
let keys = [];
if (!e.ctrlKey && !e.metaKey && !e.altKey) {
// all accelerators needs Ctrl or Cmd or Alt
return false;
}
if (e.ctrlKey) {
keys.push('ctrl');
}
if (e.metaKey && isMac) {
keys.push('cmd');
}
if (e.shiftKey) {
keys.push('shift');
}
if (e.altKey) {
keys.push('alt');
}
if (e.key === ' ') {
keys.push('space');
} else {
// we need `toLowerCase` for when the shortcut has `shift`
// we need to replace `arrow` when the shortcut uses the arrow keys
keys.push(e.key.toLowerCase().replace('arrow', ''));
}
keys = keys.join('+');
return cache.includes(keys);
}
module.exports.isAccelerator = isAccelerator;
module.exports.accelerators = allAccelerators;

View file

@ -1,82 +1,31 @@
const {statSync, renameSync, readFileSync, writeFileSync} = require('fs');
const vm = require('vm');
const {dialog} = require('electron');
const gaze = require('gaze');
const Config = require('electron-config');
const notify = require('./notify');
const _paths = require('./config/paths');
const _import = require('./config/import');
const _openConfig = require('./config/open');
// local storage
const winCfg = new Config({
defaults: {
windowPosition: [50, 50],
windowSize: [540, 380]
}
});
const path = _paths.confPath;
const pathLegacy = _paths.pathLegacy;
const win = require('./config/windows');
const {cfgPath, cfgDir} = require('./config/paths');
const watchers = [];
// watch for changes on config every 2s on windows
// https://github.com/zeit/hyper/pull/1772
const watchCfg = process.platform === 'win32' ? {interval: 2000} : {};
let cfg = {};
function watch() {
// watch for changes on config every 2s
// windows interval: https://github.com/zeit/hyper/pull/1772
gaze(path, process.platform === 'win32' ? {interval: 2000} : {}, function (err) {
const _watch = function () {
gaze(cfgPath, watchCfg, function (err) {
if (err) {
throw err;
}
this.on('changed', () => {
try {
if (exec(readFileSync(path, 'utf8'))) {
notify('Hyper configuration reloaded!');
watchers.forEach(fn => fn());
}
} catch (err) {
dialog.showMessageBox({
message: `An error occurred loading your configuration (${path}): ${err.message}`,
buttons: ['Ok']
});
}
cfg = _import();
notify('Configuration updated', 'Hyper configuration reloaded!');
watchers.forEach(fn => fn());
});
this.on('error', () => {
// Ignore file watching errors
});
});
}
let _str; // last script
function exec(str) {
if (str === _str) {
return false;
}
_str = str;
const script = new vm.Script(str);
const module = {};
script.runInNewContext({module});
if (!module.exports) {
throw new Error('Error reading configuration: `module.exports` not set');
}
const _cfg = module.exports;
if (!_cfg.config) {
throw new Error('Error reading configuration: `config` key is missing');
}
_cfg.plugins = _cfg.plugins || [];
_cfg.localPlugins = _cfg.localPlugins || [];
cfg = _cfg;
return true;
}
// This method will take text formatted as Unix line endings and transform it
// to text formatted with DOS line endings. We do this because the default
// text editor on Windows (notepad) doesn't Deal with LF files. Still. In 2017.
function crlfify(str) {
return str.replace(/\r?\n/g, '\r\n');
}
};
exports.subscribe = function (fn) {
watchers.push(fn);
@ -85,39 +34,9 @@ exports.subscribe = function (fn) {
};
};
exports.init = function () {
// for backwards compatibility with hyperterm
// (prior to the rename), we try to rename
// on behalf of the user
try {
statSync(pathLegacy);
renameSync(pathLegacy, path);
} catch (err) {
// ignore
}
try {
exec(readFileSync(path, 'utf8'));
} catch (err) {
console.log('read error', path, err.message);
const defaultConfig = readFileSync(_paths.defaultConfig);
try {
console.log('attempting to write default config to', path);
exec(defaultConfig);
writeFileSync(
path,
process.platform === 'win32' ? crlfify(defaultConfig.toString()) : defaultConfig);
} catch (err) {
throw new Error(`Failed to write config to ${path}: ${err.message}`);
}
}
watch();
};
exports.getConfigDir = function () {
// expose config directory to load plugin from the right place
return _paths.confDir;
return cfgDir;
};
exports.getConfig = function () {
@ -135,14 +54,20 @@ exports.getPlugins = function () {
};
};
exports.window = {
get() {
const position = winCfg.get('windowPosition');
const size = winCfg.get('windowSize');
return {position, size};
},
recordState(win) {
winCfg.set('windowPosition', win.getPosition());
winCfg.set('windowSize', win.getSize());
exports.getKeymaps = function () {
return cfg.keymaps;
};
exports.extendKeymaps = function (keymaps) {
if (keymaps) {
cfg.keymaps = keymaps;
}
};
exports.setup = function () {
cfg = _import();
_watch();
};
exports.getWin = win.get;
exports.winRecord = win.recordState;

41
app/config/import.js Normal file
View file

@ -0,0 +1,41 @@
const {writeFileSync, readFileSync} = require('fs');
const {defaultCfg, cfgPath} = require('./paths');
const _init = require('./init');
const _keymaps = require('./keymaps');
const _write = function (path, data) {
// This method will take text formatted as Unix line endings and transform it
// to text formatted with DOS line endings. We do this because the default
// text editor on Windows (notepad) doesn't Deal with LF files. Still. In 2017.
const crlfify = function (str) {
return str.replace(/\r?\n/g, '\r\n');
};
const format = process.platform === 'win32' ? crlfify(data.toString()) : data;
writeFileSync(path, format, 'utf8');
};
const _importConf = function () {
try {
const _defaultCfg = readFileSync(defaultCfg, 'utf8');
try {
const _cfgPath = readFileSync(cfgPath, 'utf8');
return {userCfg: _cfgPath, defaultCfg: _defaultCfg};
} catch (err) {
_write(cfgPath, defaultCfg);
return {userCfg: {}, defaultCfg: _defaultCfg};
}
} catch (err) {
console.log(err);
}
};
const _import = function () {
const cfg = _init(_importConf());
if (cfg) {
cfg.keymaps = _keymaps.import(cfg.keymaps);
}
return cfg;
};
module.exports = _import;

44
app/config/init.js Normal file
View file

@ -0,0 +1,44 @@
const vm = require('vm');
const notify = require('../notify');
const _extract = function (script) {
const module = {};
script.runInNewContext({module});
if (!module.exports) {
throw new Error('Error reading configuration: `module.exports` not set');
}
return module.exports;
};
const _syntaxValidation = function (cfg) {
try {
return new vm.Script(cfg, {filename: '.hyper.js', displayErrors: true});
} catch (err) {
notify(`Error loading config: ${err.name}, see DevTools for more info`);
console.error('Error loading config:', err);
return;
}
};
const _extractDefault = function (cfg) {
return _extract(_syntaxValidation(cfg));
};
// init config
const _init = function (cfg) {
const script = _syntaxValidation(cfg.userCfg);
if (script) {
const _cfg = _extract(script);
if (!_cfg.config) {
_cfg.plugins = _cfg.plugins || [];
_cfg.localPlugins = _cfg.localPlugins || [];
_cfg.keymaps = _cfg.keymaps || {};
notify('Error reading configuration: `config` key is missing');
return _extractDefault(cfg.defaultCfg);
}
return _cfg;
}
return _extractDefault(cfg.defaultCfg);
};
module.exports = _init;

49
app/config/keymaps.js Normal file
View file

@ -0,0 +1,49 @@
const {readFileSync} = require('fs');
const {defaultPlatformKeyPath} = require('./paths');
const commands = {};
const keys = {};
const _setKeysForCommands = function (keymap) {
for (const command in keymap) {
if (command) {
commands[command] = keymap[command].toLowerCase();
}
}
};
const _setCommandsForKeys = function (commands) {
for (const command in commands) {
if (command) {
keys[commands[command]] = command;
}
}
};
const _import = function (customsKeys) {
try {
const mapping = JSON.parse(readFileSync(defaultPlatformKeyPath()));
_setKeysForCommands(mapping);
_setKeysForCommands(customsKeys);
_setCommandsForKeys(commands);
return {commands, keys};
} catch (err) {}
};
const _extend = function (customsKeys) {
if (customsKeys) {
for (const command in customsKeys) {
if (command) {
commands[command] = customsKeys[command];
keys[customsKeys[command]] = command;
}
}
}
return {commands, keys};
};
module.exports = {
import: _import,
extend: _extend
};

View file

@ -1,7 +1,7 @@
const {shell} = require('electron');
const {confPath} = require('./paths');
const {cfgPath} = require('./paths');
module.exports = () => Promise.resolve(shell.openItem(confPath));
module.exports = () => Promise.resolve(shell.openItem(cfgPath));
if (process.platform === 'win32') {
const exec = require('child_process').exec;
@ -28,6 +28,6 @@ if (process.platform === 'win32') {
});
module.exports = () => canOpenNative()
.then(() => shell.openItem(confPath))
.catch(() => openNotepad(confPath));
.then(() => shell.openItem(cfgPath))
.catch(() => openNotepad(cfgPath));
}

View file

@ -1,36 +1,48 @@
// This module exports paths, names, and other metadata that is referenced
const {homedir} = require('os');
const {statSync} = require('fs');
const {resolve} = require('path');
const {resolve, join} = require('path');
const isDev = require('electron-is-dev');
const conf = '.hyper.js';
const defaultConf = 'config-default.js';
const legacyConf = '.hyperterm.js';
const cfgFile = '.hyper.js';
const defaultCfgFile = 'config-default.js';
const homeDir = homedir();
let confPath = resolve(homeDir, conf);
let confDir = homeDir;
let cfgPath = join(homeDir, cfgFile);
let cfgDir = homeDir;
const devDir = resolve(__dirname, '../..');
const devConfig = resolve(devDir, conf);
const defaultConfig = resolve(__dirname, defaultConf);
const pathLegacy = resolve(homeDir, legacyConf);
const devCfg = join(devDir, cfgFile);
const defaultCfg = resolve(__dirname, defaultCfgFile);
const icon = resolve(__dirname, '../static/icon.png');
const keymapPath = resolve(__dirname, '../keymaps');
const darwinKeys = join(keymapPath, 'darwin.json');
const win32Keys = join(keymapPath, 'win32.json');
const linuxKeys = join(keymapPath, 'linux.json');
const defaultPlatformKeyPath = () => {
switch (process.platform) {
case 'darwin': return darwinKeys;
case 'win32': return win32Keys;
case 'linux': return linuxKeys;
default: return darwinKeys;
}
};
if (isDev) {
// if a local config file exists, use it
try {
statSync(devConfig);
confPath = devConfig;
confDir = devDir;
console.log('using config file:', confPath);
statSync(devCfg);
cfgPath = devCfg;
cfgDir = devDir;
console.log('using config file:', cfgPath);
} catch (err) {
// ignore
}
}
module.exports = {
pathLegacy, confDir, confPath, conf, defaultConfig, defaultConf, icon
cfgDir, cfgPath, cfgFile, defaultCfg, icon, defaultPlatformKeyPath
};

21
app/config/windows.js Normal file
View file

@ -0,0 +1,21 @@
const Config = require('electron-config');
// local storage
const cfg = new Config({
defaults: {
windowPosition: [50, 50],
windowSize: [540, 380]
}
});
module.exports = {
get() {
const position = cfg.get('windowPosition');
const size = cfg.get('windowSize');
return {position, size};
},
recordState(win) {
cfg.set('windowPosition', win.getPosition());
cfg.set('windowSize', win.getSize());
}
};

View file

@ -53,13 +53,12 @@ const AppMenu = require('./menus/menu');
const createRPC = require('./rpc');
const notify = require('./notify');
const fetchNotifications = require('./notifications');
const config = require('./config');
app.commandLine.appendSwitch('js-flags', '--harmony-async-await');
// set up config
const config = require('./config');
config.init();
config.setup();
const plugins = require('./plugins');
const Session = require('./session');
@ -106,7 +105,7 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
function createWindow(fn, options = {}) {
let cfg = plugins.getDecoratedConfig();
const winSet = app.config.window.get();
const winSet = config.getWin();
let [startX, startY] = winSet.position;
const [width, height] = options.size ? options.size : (cfg.windowSize || winSet.size);
@ -353,7 +352,7 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
// the window can be closed by the browser process itself
win.on('close', () => {
app.config.window.recordState(win);
config.winRecord(win);
windowSet.delete(win);
rpc.destroy();
deleteSessions();
@ -415,6 +414,7 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
const load = () => {
plugins.onApp(app);
plugins.extendKeymaps();
makeMenu();
};

31
app/keymaps/darwin.json Normal file
View file

@ -0,0 +1,31 @@
{
"window:devtools": "cmd+alt+i",
"window:reload": "cmd+r",
"window:reloadFull": "cmd+shift+r",
"window:preferences": "cmd+,",
"zoom:reset": "cmd+0",
"zoom:in": "cmd+plus",
"zoom:out": "cmd+-",
"window:new": "cmd+n",
"window:minimize": "cmd+m",
"window:zoom": "ctrl+alt+cmd+m",
"window:toggleFullScreen": "cmd+ctrl+f",
"window:close": "cmd+shift+w",
"tab:new": "cmd+t",
"tab:next": "cmd+shift+]",
"tab:prev": "cmd+shift+[",
"pane:next": "cmd+]",
"pane:prev": "cmd+[",
"pane:splitVertical": "cmd+d",
"pane:splitHorizontal": "cmd+shift+d",
"pane:close": "cmd+w",
"editor:undo": "cmd+z",
"editor:redo": "cmd+y",
"editor:cut": "cmd+x",
"editor:copy": "cmd+c",
"editor:paste": "cmd+v",
"editor:selectAll": "cmd+a",
"editor:clearBuffer": "cmd+k",
"editor:emojis": "cmd+ctrl+space",
"plugins:update": "cmd+shift+u"
}

30
app/keymaps/linux.json Normal file
View file

@ -0,0 +1,30 @@
{
"window:devtools": "ctrl+shift+i",
"window:reload": "ctrl+shift+r",
"window:reloadFull": "ctrl+shift+f5",
"window:preferences": "ctrl+,",
"zoom:reset": "ctrl+0",
"zoom:in": "ctrl+plus",
"zoom:out": "ctrl+-",
"window:new": "ctrl+shift+n",
"window:minimize": "ctrl+shift+m",
"window:zoom": "ctrl+shift+alt+m",
"window:toggleFullScreen": "f11",
"window:close": "ctrl+shift+w",
"tab:new": "ctrl+shift+t",
"tab:next": "ctrl+tab",
"tab:prev": "ctrl+shift+tab",
"pane:next": "ctrl+pageup",
"pane:prev": "ctrl+pagedown",
"pane:splitVertical": "ctrl+shift+d",
"pane:splitHorizontal": "ctrl+shift+e",
"pane:close": "ctrl+shift+w",
"editor:undo": "ctrl+shift+z",
"editor:redo": "ctrl+shift+y",
"editor:cut": "ctrl+shift+x",
"editor:copy": "ctrl+shift+c",
"editor:paste": "ctrl+shift+v",
"editor:selectAll": "ctrl+shift+a",
"editor:clearBuffer": "ctrl+shift+k",
"plugins:update": "ctrl+shift+u"
}

30
app/keymaps/win32.json Normal file
View file

@ -0,0 +1,30 @@
{
"window:devtools": "ctrl+shift+i",
"window:reload": "ctrl+shift+r",
"window:reloadFull": "ctrl+shift+f5",
"window:preferences": "ctrl+,",
"zoom:reset": "ctrl+0",
"zoom:in": "ctrl+plus",
"zoom:out": "ctrl+-",
"window:new": "ctrl+shift+n",
"window:minimize": "ctrl+m",
"window:zoom": "ctrl+shift+alt+m",
"window:toggleFullScreen": "f11",
"window:close": "ctrl+shift+w",
"tab:new": "ctrl+shift+t",
"tab:next": "ctrl+tab",
"tab:prev": "ctrl+shift+tab",
"pane:next": "ctrl+pageup",
"pane:prev": "ctrl+pagedown",
"pane:splitVertical": "ctrl+shift+d",
"pane:splitHorizontal": "ctrl+shift+e",
"pane:close": "ctrl+shift+w",
"editor:undo": "ctrl+shift+z",
"editor:redo": "ctrl+shift+y",
"editor:cut": "ctrl+shift+x",
"editor:copy": "ctrl+shift+c",
"editor:paste": "ctrl+shift+v",
"editor:selectAll": "ctrl+shift+a",
"editor:clearBuffer": "ctrl+shift+k",
"plugins:update": "ctrl+shift+u"
}

View file

@ -1,3 +1,5 @@
const {getKeymaps} = require('../config');
// menus
const viewMenu = require('./menus/view');
const shellMenu = require('./menus/shell');
@ -8,20 +10,16 @@ const helpMenu = require('./menus/help');
const darwinMenu = require('./menus/darwin');
module.exports = (createWindow, updatePlugins) => {
const menu = [].concat(
shellMenu(createWindow),
editMenu(),
viewMenu(),
pluginsMenu(updatePlugins),
windowMenu(),
helpMenu()
);
if (process.platform === 'darwin') {
menu.unshift(
darwinMenu()
);
}
const commands = getKeymaps().commands;
const menu = [
(process.platform === 'darwin' ? darwinMenu(commands) : []),
shellMenu(commands, createWindow),
editMenu(commands),
viewMenu(commands),
pluginsMenu(commands, updatePlugins),
windowMenu(commands),
helpMenu(commands)
];
return menu;
};

View file

@ -1,10 +1,9 @@
// This menu label is overrided by OSX to be the appName
// The label is set to appName here so it matches actual behavior
const {app} = require('electron');
const {accelerators} = require('../../accelerators');
const {openConfig} = require('../../config');
module.exports = function () {
module.exports = function (commands) {
return {
label: `${app.getName()}`,
submenu: [
@ -16,7 +15,7 @@ module.exports = function () {
},
{
label: 'Preferences...',
accelerator: accelerators.preferences,
accelerator: commands['window:preferences'],
click() {
openConfig();
}

View file

@ -1,41 +1,40 @@
const {accelerators} = require('../../accelerators');
const {openConfig} = require('../../config');
module.exports = function () {
module.exports = function (commands) {
const submenu = [
{
role: 'undo',
accelerator: accelerators.undo
accelerator: commands['editor:undo']
},
{
role: 'redo',
accelerator: accelerators.redo
accelerator: commands['editor:redo']
},
{
type: 'separator'
},
{
role: 'cut',
accelerator: accelerators.cut
accelerator: commands['editor:cut']
},
{
role: 'copy',
accelerator: accelerators.copy
accelerator: commands['editor:copy']
},
{
role: 'paste',
accelerator: accelerators.paste
accelerator: commands['editor:paste']
},
{
role: 'selectall',
accelerator: accelerators.selectAll
accelerator: commands['editor:selectAll']
},
{
type: 'separator'
},
{
label: 'Clear Buffer',
accelerator: accelerators.clear,
accelerator: commands['editor:clearBuffer'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('session clear req');
@ -49,7 +48,7 @@ module.exports = function () {
{type: 'separator'},
{
label: 'Preferences...',
accelerator: accelerators.preferences,
accelerator: commands['window:preferences'],
click() {
openConfig();
}

View file

@ -1,12 +1,10 @@
const {accelerators} = require('../../accelerators');
module.exports = function (update) {
module.exports = function (commands, update) {
return {
label: 'Plugins',
submenu: [
{
label: 'Update',
accelerator: accelerators.updatePlugins,
accelerator: commands['plugins:update'],
click() {
update();
}

View file

@ -1,6 +1,4 @@
const {accelerators} = require('../../accelerators');
module.exports = function (createWindow) {
module.exports = function (commands, createWindow) {
const isMac = process.platform === 'darwin';
return {
@ -8,14 +6,14 @@ module.exports = function (createWindow) {
submenu: [
{
label: 'New Window',
accelerator: accelerators.newWindow,
accelerator: commands['window:new'],
click() {
createWindow();
}
},
{
label: 'New Tab',
accelerator: accelerators.newTab,
accelerator: commands['tab:new'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('termgroup add req');
@ -29,7 +27,7 @@ module.exports = function (createWindow) {
},
{
label: 'Split Vertically',
accelerator: accelerators.splitVertically,
accelerator: commands['pane:splitVertical'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('split request vertical');
@ -38,7 +36,7 @@ module.exports = function (createWindow) {
},
{
label: 'Split Horizontally',
accelerator: accelerators.splitHorizontally,
accelerator: commands['pane:splitHorizontal'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('split request horizontal');
@ -50,7 +48,7 @@ module.exports = function (createWindow) {
},
{
label: 'Close Session',
accelerator: accelerators.closeSession,
accelerator: commands['pane:close'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('termgroup close req');
@ -60,7 +58,7 @@ module.exports = function (createWindow) {
{
label: isMac ? 'Close Window' : 'Quit',
role: 'close',
accelerator: accelerators.closeWindow
accelerator: commands['window:close']
}
]
};

View file

@ -1,12 +1,10 @@
const {accelerators} = require('../../accelerators');
module.exports = function () {
module.exports = function (commands) {
return {
label: 'View',
submenu: [
{
label: 'Reload',
accelerator: accelerators.reload,
accelerator: commands['window:reload'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('reload');
@ -15,7 +13,7 @@ module.exports = function () {
},
{
label: 'Full Reload',
accelerator: accelerators.fullReload,
accelerator: commands['window:reloadFull'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.reload();
@ -24,7 +22,7 @@ module.exports = function () {
},
{
label: 'Developer Tools',
accelerator: accelerators.toggleDevTools,
accelerator: commands['window:devtools'],
click(item, focusedWindow) {
if (focusedWindow) {
const webContents = focusedWindow.webContents;
@ -41,7 +39,7 @@ module.exports = function () {
},
{
label: 'Reset Zoom Level',
accelerator: accelerators.resetZoom,
accelerator: commands['zoom:reset'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('reset fontSize req');
@ -50,7 +48,7 @@ module.exports = function () {
},
{
label: 'Zoom In',
accelerator: accelerators.zoomIn,
accelerator: commands['zoom:in'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('increase fontSize req');
@ -59,7 +57,7 @@ module.exports = function () {
},
{
label: 'Zoom Out',
accelerator: accelerators.zoomOut,
accelerator: commands['zoom:out'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('decrease fontSize req');

View file

@ -1,25 +1,24 @@
const {accelerators} = require('../../accelerators');
module.exports = function () {
module.exports = function (commands) {
return {
role: 'window',
submenu: [
{
role: 'minimize',
accelerator: accelerators.minimize
},
{
role: 'zoom'
accelerator: commands['window:minimize']
},
{
type: 'separator'
},
{ // It's the same thing as clicking the green traffc-light on macOS
role: 'zoom',
accelerator: commands['window:zoom']
},
{
label: 'Select Tab',
submenu: [
{
label: 'Previous',
accelerator: accelerators.showPreviousTab,
accelerator: commands['tab:prev'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('move left req');
@ -28,7 +27,7 @@ module.exports = function () {
},
{
label: 'Next',
accelerator: accelerators.showNextTab,
accelerator: commands['tab:next'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('move right req');
@ -45,7 +44,7 @@ module.exports = function () {
submenu: [
{
label: 'Previous',
accelerator: accelerators.selectNextPane,
accelerator: commands['pane:prev'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('prev pane req');
@ -54,7 +53,7 @@ module.exports = function () {
},
{
label: 'Next',
accelerator: accelerators.selectPreviousPane,
accelerator: commands['pane:next'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('next pane req');
@ -71,7 +70,7 @@ module.exports = function () {
},
{
role: 'togglefullscreen',
accelerators: accelerators.enterFullScreen
accelerators: commands['window:toggleFullScreen']
}
]
};

View file

@ -10,6 +10,7 @@ const shellEnv = require('shell-env');
const config = require('./config');
const notify = require('./notify');
const _keys = require('./config/keymaps');
// local storage
const cache = new Config();
@ -30,7 +31,8 @@ const availableExtensions = new Set([
'mapHyperTermState', 'mapTermsState',
'mapHeaderState', 'mapNotificationsState',
'mapHyperTermDispatch', 'mapTermsDispatch',
'mapHeaderDispatch', 'mapNotificationsDispatch'
'mapHeaderDispatch', 'mapNotificationsDispatch',
'extendKeymaps'
]);
// init plugin directories if not present
@ -354,6 +356,15 @@ function decorateObject(base, key) {
return decorated;
}
exports.extendKeymaps = function () {
modules.forEach(plugin => {
if (plugin.extendKeymaps) {
const keys = _keys.extend(plugin.extendKeymaps());
config.extendKeymaps(keys);
}
});
};
exports.decorateMenu = function (tpl) {
return decorateObject(tpl, 'decorateMenu');
};

23
lib/command-registry.js Normal file
View file

@ -0,0 +1,23 @@
const commands = {};
class CommandRegistry {
register(cmds) {
if (cmds) {
for (const command in cmds) {
if (command) {
commands[command] = cmds[command];
}
}
}
}
getCommand(cmd) {
return commands[cmd] !== undefined;
}
exec(cmd, e) {
commands[cmd](e);
}
}
export default new CommandRegistry();

View file

@ -1,6 +1,7 @@
import React from 'react';
import Component from '../component';
import {decorate, getTermGroupProps} from '../utils/plugins';
import CommandRegistry from '../command-registry';
import TermGroup_ from './term-group';
const TermGroup = decorate(TermGroup_, 'TermGroup');
@ -13,6 +14,7 @@ export default class Terms extends Component {
this.terms = {};
this.bound = new WeakMap();
this.onRef = this.onRef.bind(this);
this.registerCommands = CommandRegistry.register;
props.ref_(this);
}

View file

@ -1,11 +1,10 @@
import {clipboard} from 'electron';
import {hterm, lib} from 'hterm-umdjs';
import runes from 'runes';
import {isAccelerator} from '../app/accelerators';
import fromCharCode from './utils/key-code';
import selection from './utils/selection';
import returnKey from './utils/keymaps';
import CommandRegistry from './command-registry';
hterm.defaultStorage = new lib.Storage.Memory();
@ -232,8 +231,15 @@ hterm.Keyboard.prototype.onKeyDown_ = function (e) {
e.preventDefault();
}
// hterm shouldn't consume a hyper accelerator
if (e.altKey || e.metaKey || isAccelerator(e)) {
// test key from keymaps before moving forward with actions
const key = returnKey(e);
if (key) {
if (CommandRegistry.getCommand(key)) {
CommandRegistry.exec(key, e);
}
}
if (e.altKey || e.metaKey || key) {
// If the `hyperCaret` was removed on `selectAll`, we need to insert it back
if (e.key === 'v' && this.terminal.hyperCaret.parentNode !== this.terminal.cursorNode_) {
this.terminal.focusHyperCaret();

34
lib/utils/keymaps.js Normal file
View file

@ -0,0 +1,34 @@
import {remote} from 'electron';
const {getKeymaps} = remote.require('./config');
export default function returnKey(e) {
let keys = [];
if (e.metaKey && process.platform === 'darwin') {
keys.push('cmd');
} else if (e.metaKey) {
keys.push(e.key);
}
if (e.ctrlKey) {
keys.push('ctrl');
}
if (e.shiftKey) {
keys.push('shift');
}
if (e.altKey) {
keys.push('alt');
}
if (e.key === ' ') {
keys.push('space');
} else if (e.key !== 'Meta' && e.key !== 'Control' && e.key !== 'Shift' && e.key !== 'Alt') {
keys.push(e.key.replace('Arrow', ''));
}
keys = keys.join('+');
return getKeymaps().keys[keys.toLowerCase()];
}