Multiple keymaps and mousetrap (#2412)

* WIP

* WIP

* Wip

* Wip

* wip

* Refactor without normalize and plugin

* Replace extendKeymaps by decorateKeymaps

* WIP

* Add mousetrap

* Add first command over rpc

* More commands

* Add all commands

* Begin to hook commands

* Working multiple keymaps

* Use redux action to trigger command

* Use forked version of Mousetrap to capture key events

* Fix lint

* Add command in redux action to debug purpose

* ExecCommand from menu click

* Remove unused files

* Fix xterm should ignore catched events

* Re-enable IntelliSense checking

* Remove unused runes dep
This commit is contained in:
CHaBou 2017-11-03 03:51:18 +01:00 committed by Guillermo Rauch
parent 95f52e20e1
commit 2af575c3c0
31 changed files with 441 additions and 1068 deletions

81
app/commands.js Normal file
View file

@ -0,0 +1,81 @@
const {app} = require('electron');
const {openConfig} = require('./config');
const {updatePlugins} = require('./plugins');
const commands = {
'window:new': () => {
// If window is created on the same tick, it will consume event too
setTimeout(app.createWindow, 0);
},
'tab:new': focusedWindow => {
focusedWindow.rpc.emit('termgroup add req');
},
'pane:splitVertical': focusedWindow => {
focusedWindow.rpc.emit('split request vertical');
},
'pane:splitHorizontal': focusedWindow => {
focusedWindow.rpc.emit('split request horizontal');
},
'pane:close': focusedWindow => {
focusedWindow.rpc.emit('termgroup close req');
},
'window:preferences': () => {
openConfig();
},
'editor:clearBuffer': focusedWindow => {
focusedWindow.rpc.emit('session clear req');
},
'plugins:update': () => {
updatePlugins();
},
'window:reload': focusedWindow => {
focusedWindow.rpc.emit('reload');
},
'window:reloadFull': focusedWindow => {
focusedWindow.reload();
},
'window:devtools': focusedWindow => {
const webContents = focusedWindow.webContents;
if (webContents.isDevToolsOpened()) {
webContents.closeDevTools();
} else {
webContents.openDevTools({mode: 'detach'});
}
},
'zoom:reset': focusedWindow => {
focusedWindow.rpc.emit('reset fontSize req');
},
'zoom:in': focusedWindow => {
focusedWindow.rpc.emit('increase fontSize req');
},
'zoom:out': focusedWindow => {
focusedWindow.rpc.emit('decrease fontSize req');
},
'tab:prev': focusedWindow => {
focusedWindow.rpc.emit('move left req');
},
'tab:next': focusedWindow => {
focusedWindow.rpc.emit('move right req');
},
'pane:prev': focusedWindow => {
focusedWindow.rpc.emit('prev pane req');
},
'pane:next': focusedWindow => {
focusedWindow.rpc.emit('next pane req');
}
};
//Special numeric command
[1, 2, 3, 4, 5, 6, 7, 8, 'last'].forEach(cmdIndex => {
const index = cmdIndex === 'last' ? cmdIndex : cmdIndex - 1;
commands[`tab:jump:${cmdIndex}`] = focusedWindow => {
focusedWindow.rpc.emit('move jump req', index);
};
});
exports.execCommand = (command, focusedWindow) => {
const fn = commands[command];
if (fn) {
fn(focusedWindow);
}
};

View file

@ -73,12 +73,6 @@ exports.getKeymaps = () => {
return cfg.keymaps;
};
exports.extendKeymaps = keymaps => {
if (keymaps) {
cfg.keymaps = keymaps;
}
};
exports.setup = () => {
cfg = _import();
_watch();

View file

@ -1,8 +1,7 @@
const {writeFileSync, readFileSync} = require('fs');
const {sync: mkdirpSync} = require('mkdirp');
const {defaultCfg, cfgPath, plugs} = require('./paths');
const {defaultCfg, cfgPath, plugs, defaultPlatformKeyPath} = require('./paths');
const {_init, _extractDefault} = require('./init');
const _keymaps = require('./keymaps');
let defaultConfig;
@ -23,7 +22,17 @@ const _importConf = function() {
mkdirpSync(plugs.local);
try {
const _defaultCfg = readFileSync(defaultCfg, 'utf8');
const _defaultCfg = _extractDefault(readFileSync(defaultCfg, 'utf8'));
// Importing platform specific keymap
try {
const content = readFileSync(defaultPlatformKeyPath(), 'utf8');
const mapping = JSON.parse(content);
_defaultCfg.keymaps = mapping;
} catch (err) {
//eslint-disable-next-line no-console
console.error(err);
}
// Importing user config
try {
const _cfgPath = readFileSync(cfgPath, 'utf8');
return {userCfg: _cfgPath, defaultCfg: _defaultCfg};
@ -39,13 +48,8 @@ const _importConf = function() {
exports._import = () => {
const imported = _importConf();
defaultConfig = _extractDefault(imported.defaultCfg);
const cfg = _init(imported);
if (cfg) {
cfg.keymaps = _keymaps.import(cfg.keymaps);
}
return cfg;
defaultConfig = imported.defaultCfg;
return _init(imported);
};
exports.getDefaultConfig = () => {

View file

@ -1,5 +1,6 @@
const vm = require('vm');
const notify = require('../notify');
const mapKeys = require('../utils/map-keys');
const _extract = function(script) {
const module = {};
@ -30,19 +31,17 @@ const _init = function(cfg) {
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.defaultCfg;
}
// Merging platform specific keymaps with user defined keymaps
_cfg.keymaps = mapKeys(Object.assign({}, cfg.defaultCfg.keymaps, _cfg.keymaps));
// Ignore undefined values in plugin and localPlugins array Issue #1862
_cfg.plugins = (_cfg.plugins && _cfg.plugins.filter(Boolean)) || [];
_cfg.localPlugins = (_cfg.localPlugins && _cfg.localPlugins.filter(Boolean)) || [];
return _cfg;
}
return _extractDefault(cfg.defaultCfg);
return cfg.defaultCfg;
};
module.exports = {

View file

@ -1,68 +0,0 @@
const {readFileSync} = require('fs');
const normalize = require('../utils/keymaps/normalize');
const {defaultPlatformKeyPath} = require('./paths');
const commands = {};
const keys = {};
const generatePrefixedCommand = function(command, key) {
const baseCmd = command.replace(/:prefix$/, '');
for (let i = 1; i <= 9; i++) {
// 9 is a special number because it means 'last'
const index = i === 9 ? 'last' : i;
commands[`${baseCmd}:${index}`] = normalize(`${key}+${i}`);
}
};
const _setKeysForCommands = function(keymap) {
for (const command in keymap) {
if (command) {
// In case of a command finishing by :prefix
// we need to generate commands and keys
if (command.endsWith(':prefix')) {
generatePrefixedCommand(command, keymap[command]);
} else {
commands[command] = normalize(keymap[command]);
}
}
}
};
const _setCommandsForKeys = function(commands_) {
for (const command in commands_) {
if (command) {
keys[commands_[command]] = command;
}
}
};
const _import = function(customKeys) {
try {
const mapping = JSON.parse(readFileSync(defaultPlatformKeyPath()));
_setKeysForCommands(mapping);
_setKeysForCommands(customKeys);
_setCommandsForKeys(commands);
return {commands, keys};
} catch (err) {
//eslint-disable-next-line no-console
console.error(err);
}
};
const _extend = function(customKeys) {
if (customKeys) {
for (const command in customKeys) {
if (command) {
commands[command] = normalize(customKeys[command]);
keys[normalize(customKeys[command])] = command;
}
}
}
return {commands, keys};
};
module.exports = {
import: _import,
extend: _extend
};

View file

@ -56,7 +56,6 @@ const {app, BrowserWindow, Menu} = require('electron');
const {gitDescribe} = require('git-describe');
const isDev = require('electron-is-dev');
const AppMenu = require('./menus/menu');
const config = require('./config');
// set up config
@ -64,6 +63,8 @@ config.setup();
const plugins = require('./plugins');
const AppMenu = require('./menus/menu');
const Window = require('./ui/window');
const windowSet = new Set([]);
@ -181,15 +182,7 @@ app.on('ready', () =>
});
const makeMenu = () => {
const menu = plugins.decorateMenu(
AppMenu(
createWindow,
() => {
plugins.updatePlugins({force: true});
},
plugins.getLoadedPluginVersions
)
);
const menu = plugins.decorateMenu(AppMenu.createMenu(createWindow, plugins.getLoadedPluginVersions));
// If we're on Mac make a Dock Menu
if (process.platform === 'darwin') {
@ -204,12 +197,12 @@ app.on('ready', () =>
app.dock.setMenu(dockMenu);
}
Menu.setApplicationMenu(Menu.buildFromTemplate(menu));
Menu.setApplicationMenu(AppMenu.buildMenu(menu));
};
const load = () => {
plugins.onApp(app);
plugins.extendKeymaps();
plugins.checkDeprecatedExtendKeymaps();
makeMenu();
};

View file

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

View file

@ -1,8 +1,8 @@
// Packages
const {app, dialog} = require('electron');
const {app, dialog, Menu} = require('electron');
// Utilities
const {getKeymaps, getConfig} = require('../config');
const {getConfig} = require('../config');
const {icon} = require('../config/paths');
const viewMenu = require('./menus/view');
const shellMenu = require('./menus/shell');
@ -11,13 +11,22 @@ const pluginsMenu = require('./menus/plugins');
const windowMenu = require('./menus/window');
const helpMenu = require('./menus/help');
const darwinMenu = require('./menus/darwin');
const {getDecoratedKeymaps} = require('../plugins');
const {execCommand} = require('../commands');
const appName = app.getName();
const appVersion = app.getVersion();
module.exports = (createWindow, updatePlugins, getLoadedPluginVersions) => {
let menu_ = [];
exports.createMenu = (createWindow, getLoadedPluginVersions) => {
const config = getConfig();
const {commands} = getKeymaps();
// We take only first shortcut in array for each command
const allCommandKeys = getDecoratedKeymaps();
const commandKeys = Object.keys(allCommandKeys).reduce((result, command) => {
result[command] = allCommandKeys[command][0];
return result;
}, {});
let updateChannel = 'stable';
@ -39,14 +48,19 @@ module.exports = (createWindow, updatePlugins, getLoadedPluginVersions) => {
});
};
const menu = [
...(process.platform === 'darwin' ? [darwinMenu(commands, showAbout)] : []),
shellMenu(commands, createWindow),
editMenu(commands),
viewMenu(commands),
pluginsMenu(commands, updatePlugins),
windowMenu(commands),
helpMenu(commands, showAbout)
...(process.platform === 'darwin' ? [darwinMenu(commandKeys, showAbout)] : []),
shellMenu(commandKeys, execCommand),
editMenu(commandKeys, execCommand),
viewMenu(commandKeys, execCommand),
pluginsMenu(commandKeys, execCommand),
windowMenu(commandKeys, execCommand),
helpMenu(commandKeys, showAbout)
];
return menu;
};
exports.buildMenu = template => {
menu_ = Menu.buildFromTemplate(template);
return menu_;
};

View file

@ -1,9 +1,8 @@
// 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 {openConfig} = require('../../config');
module.exports = (commands, showAbout) => {
module.exports = (commandKeys, showAbout) => {
return {
label: `${app.getName()}`,
submenu: [
@ -18,10 +17,7 @@ module.exports = (commands, showAbout) => {
},
{
label: 'Preferences...',
accelerator: commands['window:preferences'],
click() {
openConfig();
}
accelerator: commandKeys['window:preferences']
},
{
type: 'separator'

View file

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

View file

@ -1,4 +1,4 @@
module.exports = (commands, createWindow) => {
module.exports = (commandKeys, execCommand) => {
const isMac = process.platform === 'darwin';
return {
@ -6,20 +6,16 @@ module.exports = (commands, createWindow) => {
submenu: [
{
label: 'New Window',
accelerator: commands['window:new'],
click() {
createWindow();
accelerator: commandKeys['window:new'],
click(item, focusedWindow) {
execCommand('window:new', focusedWindow);
}
},
{
label: 'New Tab',
accelerator: commands['tab:new'],
accelerator: commandKeys['tab:new'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('termgroup add req');
} else {
createWindow();
}
execCommand('tab:new', focusedWindow);
}
},
{
@ -27,20 +23,16 @@ module.exports = (commands, createWindow) => {
},
{
label: 'Split Vertically',
accelerator: commands['pane:splitVertical'],
accelerator: commandKeys['pane:splitVertical'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('split request vertical');
}
execCommand('pane:splitVertical', focusedWindow);
}
},
{
label: 'Split Horizontally',
accelerator: commands['pane:splitHorizontal'],
accelerator: commandKeys['pane:splitHorizontal'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('split request horizontal');
}
execCommand('pane:splitHorizontal', focusedWindow);
}
},
{
@ -48,17 +40,15 @@ module.exports = (commands, createWindow) => {
},
{
label: 'Close Session',
accelerator: commands['pane:close'],
accelerator: commandKeys['pane:close'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('termgroup close req');
}
execCommand('pane:close', focusedWindow);
}
},
{
label: isMac ? 'Close Window' : 'Quit',
role: 'close',
accelerator: commands['window:close']
accelerator: commandKeys['window:close']
}
]
};

View file

@ -1,37 +1,26 @@
module.exports = commands => {
module.exports = (commandKeys, execCommand) => {
return {
label: 'View',
submenu: [
{
label: 'Reload',
accelerator: commands['window:reload'],
accelerator: commandKeys['window:reload'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('reload');
}
execCommand('window:reload', focusedWindow);
}
},
{
label: 'Full Reload',
accelerator: commands['window:reloadFull'],
accelerator: commandKeys['window:reloadFull'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.reload();
}
execCommand('window:reloadFull', focusedWindow);
}
},
{
label: 'Developer Tools',
accelerator: commands['window:devtools'],
click(item, focusedWindow) {
if (focusedWindow) {
const webContents = focusedWindow.webContents;
if (webContents.isDevToolsOpened()) {
webContents.closeDevTools();
} else {
webContents.openDevTools({mode: 'detach'});
}
}
accelerator: commandKeys['window:devtools'],
click: (item, focusedWindow) => {
execCommand('window:reloadFull', focusedWindow);
}
},
{
@ -39,29 +28,23 @@ module.exports = commands => {
},
{
label: 'Reset Zoom Level',
accelerator: commands['zoom:reset'],
accelerator: commandKeys['zoom:reset'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('reset fontSize req');
}
execCommand('zoom:reset', focusedWindow);
}
},
{
label: 'Zoom In',
accelerator: commands['zoom:in'],
accelerator: commandKeys['zoom:in'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('increase fontSize req');
}
execCommand('zoom:in', focusedWindow);
}
},
{
label: 'Zoom Out',
accelerator: commands['zoom:out'],
accelerator: commandKeys['zoom:out'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('decrease fontSize req');
}
execCommand('zoom:out', focusedWindow);
}
}
]

View file

@ -1,18 +1,12 @@
module.exports = commands => {
module.exports = (commandKeys, execCommand) => {
// Generating tab:jump array
const tabJump = [];
for (let i = 1; i <= 9; i++) {
// 9 is a special number because it means 'last'
const label = i === 9 ? 'Last' : `${i}`;
const tabIndex = i === 9 ? 'last' : i - 1;
tabJump.push({
label: label,
accelerator: commands[`tab:jump:${label.toLowerCase()}`],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('move jump req', tabIndex);
}
}
accelerator: commandKeys[`tab:jump:${label.toLowerCase()}`]
});
}
@ -21,7 +15,7 @@ module.exports = commands => {
submenu: [
{
role: 'minimize',
accelerator: commands['window:minimize']
accelerator: commandKeys['window:minimize']
},
{
type: 'separator'
@ -29,27 +23,23 @@ module.exports = commands => {
{
// It's the same thing as clicking the green traffc-light on macOS
role: 'zoom',
accelerator: commands['window:zoom']
accelerator: commandKeys['window:zoom']
},
{
label: 'Select Tab',
submenu: [
{
label: 'Previous',
accelerator: commands['tab:prev'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('move left req');
}
accelerator: commandKeys['tab:prev'],
click: (item, focusedWindow) => {
execCommand('tab:prev', focusedWindow);
}
},
{
label: 'Next',
accelerator: commands['tab:next'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('move right req');
}
accelerator: commandKeys['tab:next'],
click: (item, focusedWindow) => {
execCommand('tab:next', focusedWindow);
}
},
{
@ -66,20 +56,16 @@ module.exports = commands => {
submenu: [
{
label: 'Previous',
accelerator: commands['pane:prev'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('prev pane req');
}
accelerator: commandKeys['pane:prev'],
click: (item, focusedWindow) => {
execCommand('pane:prev', focusedWindow);
}
},
{
label: 'Next',
accelerator: commands['pane:next'],
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.rpc.emit('next pane req');
}
accelerator: commandKeys['pane:next'],
click: (item, focusedWindow) => {
execCommand('pane:next', focusedWindow);
}
}
]
@ -92,7 +78,7 @@ module.exports = commands => {
},
{
role: 'togglefullscreen',
accelerators: commands['window:toggleFullScreen']
accelerators: commandKeys['window:toggleFullScreen']
}
]
};

View file

@ -6,10 +6,10 @@ const ms = require('ms');
const config = require('./config');
const notify = require('./notify');
const _keys = require('./config/keymaps');
const {availableExtensions} = require('./plugins/extensions');
const {install} = require('./plugins/install');
const {plugs} = require('./config/paths');
const mapKeys = require('./utils/map-keys');
// local storage
const cache = new Config();
@ -19,7 +19,7 @@ const localPath = plugs.local;
// caches
let plugins = config.getPlugins();
let paths = getPaths(plugins);
let paths = getPaths();
let id = getId(plugins);
let modules = requirePlugins();
@ -64,7 +64,7 @@ function updatePlugins({force = false} = {}) {
cache.set('hyper.plugins', id_);
// cache paths
paths = getPaths(plugins);
paths = getPaths();
// clear require cache
clearCache();
@ -289,7 +289,7 @@ function decorateObject(base, key) {
try {
res = plugin[key](decorated);
} catch (e) {
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`);
notify('Plugin error!', `"${plugin._name}" when decorating ${key}`);
return;
}
if (res && typeof res === 'object') {
@ -303,22 +303,6 @@ function decorateObject(base, key) {
return decorated;
}
exports.extendKeymaps = () => {
modules.forEach(plugin => {
if (plugin.extendKeymaps) {
let pluginKeymap;
try {
pluginKeymap = plugin.extendKeymaps();
} catch (e) {
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`);
return;
}
const keys = _keys.extend(pluginKeymap);
config.extendKeymaps(keys);
}
});
};
exports.getDeprecatedConfig = () => {
const deprecated = {};
const baseConfig = config.getConfig();
@ -359,6 +343,22 @@ exports.getDecoratedConfig = () => {
return translatedConfig;
};
exports.checkDeprecatedExtendKeymaps = () => {
modules.forEach(plugin => {
if (plugin.extendKeymaps) {
notify('Plugin warning!', `"${plugin._name}" use deprecated "extendKeymaps" handler`);
return;
}
});
};
exports.getDecoratedKeymaps = () => {
const baseKeymaps = config.getKeymaps();
// Ensure that all keys are in an array and don't use deprecated key combination`
const decoratedKeymaps = mapKeys(decorateObject(baseKeymaps, 'decorateKeymaps'));
return decoratedKeymaps;
};
exports.getDecoratedBrowserOptions = defaults => {
return decorateObject(defaults, 'decorateBrowserOptions');
};

View file

@ -20,6 +20,7 @@ module.exports = {
'decorateNotifications',
'decorateTabs',
'decorateConfig',
'decorateKeymaps',
'decorateEnv',
'decorateTermGroup',
'decorateSplitPane',
@ -34,7 +35,6 @@ module.exports = {
'mapHyperTermDispatch',
'mapTermsDispatch',
'mapHeaderDispatch',
'mapNotificationsDispatch',
'extendKeymaps'
'mapNotificationsDispatch'
])
};

View file

@ -11,6 +11,7 @@ const createRPC = require('../rpc');
const notify = require('../notify');
const fetchNotifications = require('../notifications');
const Session = require('../session');
const {execCommand} = require('../commands');
module.exports = class Window {
constructor(options_, cfg, fn) {
@ -167,6 +168,10 @@ module.exports = class Window {
rpc.on('close', () => {
window.close();
});
rpc.on('command', command => {
const focusedWindow = BrowserWindow.getFocusedWindow();
execCommand(command, focusedWindow);
});
const deleteSessions = () => {
sessions.forEach((session, key) => {
session.removeAllListeners();

View file

@ -1,5 +0,0 @@
const normalize = require('./normalize');
module.exports = (keys, commands) => {
return commands[normalize(keys)];
};

View file

@ -1,6 +0,0 @@
const {getKeymaps} = require('../../config');
const findCommandByKeys = require('./find-command-by-keys');
module.exports = keys => {
return findCommandByKeys(keys, getKeymaps().keys);
};

View file

@ -1,17 +0,0 @@
// This function receives a keymap in any key order and returns
// the same keymap alphatetically sorted by the clients locale.
// eg.: cmd+alt+o -> alt+cmd+o
// We do this in order to normalize what the user defined to what we
// internally parse. By doing this, you can set your keymaps in any given order
// eg.: alt+cmd+o, cmd+alt+o, o+alt+cmd, etc. #2195
module.exports = keybinding => {
function sortAlphabetically(a, b) {
return a.localeCompare(b);
}
return keybinding
.toLowerCase()
.split('+')
.sort(sortAlphabetically)
.join('+');
};

41
app/utils/map-keys.js Normal file
View file

@ -0,0 +1,41 @@
const generatePrefixedCommand = (command, shortcuts) => {
const result = {};
const baseCmd = command.replace(/:prefix$/, '');
for (let i = 1; i <= 9; i++) {
// 9 is a special number because it means 'last'
const index = i === 9 ? 'last' : i;
const prefixedShortcuts = shortcuts.map(shortcut => `${shortcut}+${i}`);
result[`${baseCmd}:${index}`] = prefixedShortcuts;
}
return result;
};
module.exports = config => {
return Object.keys(config).reduce((keymap, command) => {
if (!command) {
return;
}
// We can have different keys for a same command.
const shortcuts = Array.isArray(config[command]) ? config[command] : [config[command]];
const fixedShortcuts = [];
shortcuts.forEach(shortcut => {
let newShortcut = shortcut;
if (newShortcut.indexOf('cmd') !== -1) {
// Mousetrap use `command` and not `cmd`
//eslint-disable-next-line no-console
console.warn('Your config use deprecated `cmd` in key combination. Please use `command` instead.');
newShortcut = newShortcut.replace('cmd', 'command');
}
fixedShortcuts.push(newShortcut);
});
if (command.endsWith(':prefix')) {
return Object.assign(keymap, generatePrefixedCommand(command, fixedShortcuts));
}
keymap[command] = fixedShortcuts;
return keymap;
}, {});
};

View file

@ -19,7 +19,8 @@ import {
UI_MOVE_PREV_PANE,
UI_WINDOW_GEOMETRY_CHANGED,
UI_WINDOW_MOVE,
UI_OPEN_FILE
UI_OPEN_FILE,
UI_COMMAND_EXEC
} from '../constants/ui';
import {setActiveGroup} from './term-groups';
@ -260,3 +261,18 @@ export function openFile(path) {
});
};
}
export function execCommand(command, fn, e) {
return dispatch =>
dispatch({
type: UI_COMMAND_EXEC,
command,
effect() {
if (fn) {
fn(e);
} else {
rpc.emit('command', command);
}
}
});
}

View file

@ -1,23 +1,46 @@
const commands = {};
import {remote} from 'electron';
class CommandRegistry {
register(cmds) {
if (cmds) {
for (const command in cmds) {
if (command) {
commands[command] = cmds[command];
}
}
}
const {getDecoratedKeymaps} = remote.require('./plugins');
let commands = {};
export const getRegisteredKeys = () => {
const keymaps = getDecoratedKeymaps();
return Object.keys(keymaps).reduce((result, actionName) => {
const commandKeys = keymaps[actionName];
commandKeys.forEach(shortcut => {
result[shortcut] = actionName;
});
return result;
}, {});
};
export const registerCommandHandlers = cmds => {
if (!cmds) {
return;
}
getCommand(cmd) {
return commands[cmd] !== undefined;
}
commands = Object.assign(commands, cmds);
};
exec(cmd, e) {
commands[cmd](e);
}
}
export const getCommandHandler = command => {
return commands[command];
};
export default new CommandRegistry();
// Some commands are firectly excuted by Electron menuItem role.
// They should not be prevented to reach Electron.
const roleCommands = [
'window:close',
'editor:undo',
'editor:redo',
'editor:cut',
'editor:copy',
'editor:paste',
'editor:selectAll',
'window:minimize',
'window:zoom',
'window:toggleFullScreen'
];
export const shouldPreventDefault = command => !roleCommands.includes(command);

View file

@ -4,8 +4,6 @@ import Terminal from 'xterm';
import {clipboard} from 'electron';
import {PureComponent} from '../base-components';
import terms from '../terms';
import returnKey from '../utils/keymaps';
import CommandRegistry from '../command-registry';
import processClipboard from '../utils/paste';
// map old hterm constants to xterm.js
@ -166,17 +164,8 @@ export default class Term extends PureComponent {
}
keyboardHandler(e) {
if (e.type !== 'keydown') {
return true;
}
// test key from keymaps before moving forward with actions
const key = returnKey(e);
if (key) {
if (CommandRegistry.getCommand(key)) {
CommandRegistry.exec(key, e);
}
return false;
}
// Has Mousetrap flagged this event as a command?
return !e.catched;
}
componentWillReceiveProps(nextProps) {

View file

@ -1,7 +1,7 @@
import React from 'react';
import {Component} from '../base-components';
import {decorate, getTermGroupProps} from '../utils/plugins';
import CommandRegistry from '../command-registry';
import {registerCommandHandlers} from '../command-registry';
import TermGroup_ from './term-group';
import StyleSheet_ from './style-sheet';
@ -16,7 +16,7 @@ export default class Terms extends Component {
this.terms = {};
this.bound = new WeakMap();
this.onRef = this.onRef.bind(this);
this.registerCommands = CommandRegistry.register;
this.registerCommands = registerCommandHandlers;
props.ref_(this);
}

View file

@ -17,3 +17,4 @@ export const UI_OPEN_FILE = 'UI_OPEN_FILE';
export const UI_OPEN_HAMBURGER_MENU = 'UI_OPEN_HAMBURGER_MENU';
export const UI_WINDOW_MINIMIZE = 'UI_WINDOW_MINIMIZE';
export const UI_WINDOW_CLOSE = 'UI_WINDOW_CLOSE';
export const UI_COMMAND_EXEC = 'UI_COMMAND_EXEC';

View file

@ -1,10 +1,12 @@
/* eslint-disable react/no-danger */
import React from 'react';
import Mousetrap from 'mousetrap';
import {PureComponent} from '../base-components';
import {connect} from '../utils/plugins';
import * as uiActions from '../actions/ui';
import {getRegisteredKeys, getCommandHandler, shouldPreventDefault} from '../command-registry';
import HeaderContainer from './header';
import TermsContainer from './terms';
@ -17,6 +19,7 @@ class Hyper extends PureComponent {
super(props);
this.handleFocusActive = this.handleFocusActive.bind(this);
this.onTermsRef = this.onTermsRef.bind(this);
this.mousetrap = null;
}
componentWillReceiveProps(next) {
@ -35,8 +38,34 @@ class Hyper extends PureComponent {
}
attachKeyListeners() {
// eslint-disable-next-line no-console
console.error('unimplemented');
if (!this.mousetrap) {
this.mousetrap = new Mousetrap(window, true);
this.mousetrap.stopCallback = () => {
// All events should be intercepted even if focus is in an input/textarea
return false;
};
} else {
this.mousetrap.reset();
}
const keys = getRegisteredKeys();
Object.keys(keys).forEach(commandKeys => {
this.mousetrap.bind(
commandKeys,
e => {
const command = keys[commandKeys];
// We should tell to xterm that it should ignore this event.
e.catched = true;
this.props.execCommand(command, getCommandHandler(command), e);
shouldPreventDefault(command) && e.preventDefault();
},
'keydown'
);
});
}
componentDidMount() {
this.attachKeyListeners();
}
onTermsRef(terms) {
@ -108,16 +137,8 @@ const HyperContainer = connect(
},
dispatch => {
return {
moveTo: i => {
dispatch(uiActions.moveTo(i));
},
moveLeft: () => {
dispatch(uiActions.moveLeft());
},
moveRight: () => {
dispatch(uiActions.moveRight());
execCommand: (command, fn, e) => {
dispatch(uiActions.execCommand(command, fn, e));
}
};
},

View file

@ -1,485 +0,0 @@
import {clipboard} from 'electron';
import {hterm, lib} from 'hterm-umdjs';
import runes from 'runes';
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();
// Provide selectAll to terminal viewport
hterm.Terminal.prototype.selectAll = () => {
// If the cursorNode_ having hyperCaret we need to remove it
if (this.cursorNode_.contains(this.hyperCaret)) {
this.cursorNode_.removeChild(this.hyperCaret);
// We need to clear the DOM range to reset anchorNode
selection.clear(this);
selection.all(this);
}
};
// override double click behavior to copy
const oldMouse = hterm.Terminal.prototype.onMouse_;
hterm.Terminal.prototype.onMouse_ = e => {
if (e.type === 'dblclick') {
selection.extend(this);
//eslint-disable-next-line no-console
console.log('[hyper+hterm] ignore double click');
return;
}
return oldMouse.call(this, e);
};
function containsNonLatinCodepoints(s) {
return /[^\u0000-\u00ff]/.test(s);
}
// hterm Unicode patch
hterm.TextAttributes.splitWidecharString = str => {
const context = runes(str).reduce(
(ctx, rune) => {
const code = rune.codePointAt(0);
if (code < 128 || lib.wc.charWidth(code) === 1) {
ctx.acc += rune;
return ctx;
}
if (ctx.acc) {
ctx.items.push({str: ctx.acc});
ctx.acc = '';
}
ctx.items.push({str: rune, wcNode: true});
return ctx;
},
{items: [], acc: ''}
);
if (context.acc) {
context.items.push({str: context.acc});
}
return context.items;
};
// hterm Unicode patch
const cache = [];
lib.wc.strWidth = str => {
const shouldCache = str.length === 1;
if (shouldCache && cache[str] !== undefined) {
return cache[str];
}
const chars = runes(str);
let width = 0;
let rv = 0;
for (let i = 0; i < chars.length; i++) {
const codePoint = chars[i].codePointAt(0);
width = lib.wc.charWidth(codePoint);
if (width < 0) {
return -1;
}
rv += width * (codePoint <= 0xffff ? 1 : 2);
}
if (shouldCache) {
cache[str] = rv;
}
return rv;
};
// hterm Unicode patch
lib.wc.substr = (str, start, optWidth) => {
const chars = runes(str);
let startIndex;
let endIndex;
let width = 0;
for (let i = 0; i < chars.length; i++) {
const codePoint = chars[i].codePointAt(0);
const charWidth = lib.wc.charWidth(codePoint);
if (width + charWidth > start) {
startIndex = i;
break;
}
width += charWidth;
}
if (optWidth) {
width = 0;
for (endIndex = startIndex; endIndex < chars.length && width < optWidth; endIndex++) {
width += lib.wc.charWidth(chars[endIndex].charCodeAt(0));
}
if (width > optWidth) {
endIndex--;
}
return chars.slice(startIndex, endIndex).join('');
}
return chars.slice(startIndex).join('');
};
// MacOS emoji bar support
hterm.Keyboard.prototype.onTextInput_ = e => {
if (!e.data) {
return;
}
runes(e.data).forEach(this.terminal.onVTKeystroke.bind(this.terminal));
};
hterm.Terminal.IO.prototype.writeUTF8 = string => {
if (this.terminal_.io !== this) {
throw new Error('Attempt to print from inactive IO object.');
}
if (!containsNonLatinCodepoints(string)) {
this.terminal_.interpret(string);
return;
}
runes(string).forEach(rune => {
this.terminal_.getTextAttributes().unicodeNode = containsNonLatinCodepoints(rune);
this.terminal_.interpret(rune);
this.terminal_.getTextAttributes().unicodeNode = false;
});
};
const oldIsDefault = hterm.TextAttributes.prototype.isDefault;
hterm.TextAttributes.prototype.isDefault = () => {
return !this.unicodeNode && oldIsDefault.call(this);
};
const oldSetFontSize = hterm.Terminal.prototype.setFontSize;
hterm.Terminal.prototype.setFontSize = px => {
oldSetFontSize.call(this, px);
const doc = this.getDocument();
let unicodeNodeStyle = doc.getElementById('hyper-unicode-styles');
if (!unicodeNodeStyle) {
unicodeNodeStyle = doc.createElement('style');
unicodeNodeStyle.setAttribute('id', 'hyper-unicode-styles');
doc.head.appendChild(unicodeNodeStyle);
}
unicodeNodeStyle.innerHTML = `
.unicode-node {
display: inline-block;
vertical-align: top;
width: ${this.scrollPort_.characterSize.width}px;
}
`;
};
const oldCreateContainer = hterm.TextAttributes.prototype.createContainer;
hterm.TextAttributes.prototype.createContainer = text => {
const container = oldCreateContainer.call(this, text);
if (container.style && runes(text).length === 1 && containsNonLatinCodepoints(text)) {
container.className += ' unicode-node';
}
return container;
};
// Do not match containers when one of them has unicode text (unicode chars need to be alone in their containers)
const oldMatchesContainer = hterm.TextAttributes.prototype.matchesContainer;
hterm.TextAttributes.prototype.matchesContainer = obj => {
return oldMatchesContainer.call(this, obj) && !this.unicodeNode && !containsNonLatinCodepoints(obj.textContent);
};
// there's no option to turn off the size overlay
hterm.Terminal.prototype.overlaySize = () => {};
// fixing a bug in hterm where a double click triggers
// a non-collapsed selection whose text is '', and results
// in an infinite copy loop
hterm.Terminal.prototype.copySelectionToClipboard = () => {
const text = this.getSelectionText();
if (text) {
this.copyStringToClipboard(text);
}
};
let lastEventTimeStamp;
let lastEventKey;
// passthrough all the commands that are meant to control
// hyper and not the terminal itself
const oldKeyDown = hterm.Keyboard.prototype.onKeyDown_;
hterm.Keyboard.prototype.onKeyDown_ = e => {
const modifierKeysConf = this.terminal.modifierKeys;
if (e.timeStamp === lastEventTimeStamp && e.key === lastEventKey) {
// Event was already processed.
// It seems to occur after a char composition ended by Tab and cause a blur.
// See https://github.com/zeit/hyper/issues/1341
e.preventDefault();
return;
}
lastEventTimeStamp = e.timeStamp;
lastEventKey = e.key;
if (
e.altKey &&
e.which !== 16 && // Ignore other modifer keys
e.which !== 17 &&
e.which !== 18 &&
e.which !== 91 &&
modifierKeysConf.altIsMeta
) {
const char = fromCharCode(e);
this.terminal.onVTKeystroke('\x1b' + char);
e.preventDefault();
}
if (
e.metaKey &&
e.code !== 'MetaLeft' &&
e.code !== 'MetaRight' &&
e.which !== 16 &&
e.which !== 17 &&
e.which !== 18 &&
e.which !== 91 &&
modifierKeysConf.cmdIsMeta
) {
const char = fromCharCode(e);
this.terminal.onVTKeystroke('\x1b' + char);
e.preventDefault();
}
// 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();
}
return;
}
// Test for valid keys in order to accept clear status
const clearBlacklist = ['control', 'shift', 'capslock', 'dead'];
if (!clearBlacklist.includes(e.code.toLowerCase()) && !clearBlacklist.includes(e.key.toLowerCase())) {
// Since Electron 1.6.X, there is a race condition with character composition
// if this selection clearing is made synchronously. See #2140.
setTimeout(() => selection.clear(this.terminal), 0);
}
// If the `hyperCaret` was removed on `selectAll`, we need to insert it back
if (this.terminal.hyperCaret.parentNode !== this.terminal.cursorNode_) {
this.terminal.focusHyperCaret();
}
return oldKeyDown.call(this, e);
};
const oldOnMouse = hterm.Terminal.prototype.onMouse_;
hterm.Terminal.prototype.onMouse_ = e => {
// override `preventDefault` to not actually
// prevent default when the type of event is
// mousedown, so that we can still trigger
// focus on the terminal when the underlying
// VT is interested in mouse events, as is the
// case of programs like `vtop` that allow for
// the user to click on rows
if (e.type === 'mousedown') {
e.preventDefault = () => {};
return;
}
return oldOnMouse.call(this, e);
};
hterm.Terminal.prototype.onMouseDown_ = e => {
// copy/paste on right click
if (e.button === 2) {
const text = this.getSelectionText();
if (text) {
this.copyStringToClipboard(text);
} else {
this.onVTKeystroke(clipboard.readText());
}
}
};
// override `ScrollPort.resize` to avoid an expensive calculation
// just to get the size of the scrollbar, which for Hyper is always
// set to overlay (hence with `0`)
hterm.ScrollPort.prototype.resize = () => {
this.currentScrollbarWidthPx = 0;
this.syncScrollHeight();
this.syncRowNodesDimensions_();
this.publish('resize', {scrollPort: this}, () => {
this.scrollRowToBottom(this.rowProvider_.getRowCount());
this.scheduleRedraw();
});
};
// make background transparent to avoid transparency issues
hterm.ScrollPort.prototype.setBackgroundColor = () => {
this.screen_.style.backgroundColor = 'transparent';
};
// will be called by the <Term/> right after the `hterm.Terminal` is instantiated
hterm.Terminal.prototype.onHyperCaret = caret => {
this.hyperCaret = caret;
let ongoingComposition = false;
caret.addEventListener('compositionstart', () => {
ongoingComposition = true;
});
// we can ignore `compositionstart` since chromium always fire it with ''
caret.addEventListener('compositionupdate', () => {
this.cursorNode_.style.backgroundColor = 'yellow';
this.cursorNode_.style.borderColor = 'yellow';
});
// at this point the char(s) is ready
caret.addEventListener('compositionend', () => {
ongoingComposition = false;
this.cursorNode_.style.backgroundColor = '';
this.setCursorShape(this.getCursorShape());
this.cursorNode_.style.borderColor = this.getCursorColor();
caret.innerText = '';
});
// if you open the `Emoji & Symbols` (ctrl+cmd+space)
// and select an emoji, it'll be inserted into our caret
// and stay there until you star a compositon event.
// to avoid that, we'll just check if there's an ongoing
// compostion event. if there's one, we do nothing.
// otherwise, we just remove the emoji and stop the event
// propagation.
// PS: this event will *not* be fired when a standard char
// (a, b, c, 1, 2, 3, etc) is typed only for composed
// ones and `Emoji & Symbols`
caret.addEventListener('input', e => {
if (!ongoingComposition) {
caret.innerText = '';
e.stopPropagation();
e.preventDefault();
}
});
// we need to capture pastes, prevent them and send its contents to the terminal
caret.addEventListener('paste', e => {
e.stopPropagation();
e.preventDefault();
const text = e.clipboardData.getData('text');
this.onVTKeystroke(text);
});
// here we replicate the focus/blur state of our caret on the `hterm` caret
caret.addEventListener('focus', () => {
this.cursorNode_.setAttribute('focus', true);
this.restyleCursor_();
});
caret.addEventListener('blur', () => {
this.cursorNode_.setAttribute('focus', false);
this.restyleCursor_();
});
// this is necessary because we need to access the `document_` and the hyperCaret
// on `hterm.Screen.prototype.syncSelectionCaret`
this.primaryScreen_.terminal = this;
this.alternateScreen_.terminal = this;
};
// ensure that our contenteditable caret is injected
// inside the term's cursor node and that it's focused
hterm.Terminal.prototype.focusHyperCaret = () => {
if (!this.hyperCaret.parentNode !== this.cursorNode_) {
this.cursorNode_.appendChild(this.hyperCaret);
}
this.hyperCaret.focus();
};
hterm.Screen.prototype.syncSelectionCaret = () => {
const p = this.terminal.hyperCaret;
const doc = this.terminal.document_;
const win = doc.defaultView;
const s = win.getSelection();
const r = doc.createRange();
r.selectNodeContents(p);
s.removeAllRanges();
s.addRange(r);
};
// For some reason, when the original version of this function was called right
// after a new tab was created, it was breaking the focus of the other tab.
// After some investigation, I (matheuss) found that `this.iframe_.focus();` (from
// the original function) was causing the issue. So right now we're overriding
// the function to prevent the `iframe_` from being focused.
// This shouldn't create any side effects we're _stealing_ the focus from `htem` anyways.
hterm.ScrollPort.prototype.focus = () => {
this.screen_.focus();
};
// fixes a bug in hterm, where the cursor goes back to `BLOCK`
// after the bell rings
const oldRingBell = hterm.Terminal.prototype.ringBell;
hterm.Terminal.prototype.ringBell = () => {
oldRingBell.call(this);
setTimeout(() => {
this.restyleCursor_();
}, 200);
};
// fixes a bug in hterm, where the shorthand hex
// is not properly converted to rgb
lib.colors.hexToRGB = arg => {
const hex16 = lib.colors.re_.hex16;
const hex24 = lib.colors.re_.hex24;
function convert(hex) {
if (hex.length === 4) {
hex = hex.replace(hex16, (h, r, g, b) => {
return '#' + r + r + g + g + b + b;
});
}
const ary = hex.match(hex24);
if (!ary) {
return null;
}
return 'rgb(' + parseInt(ary[1], 16) + ', ' + parseInt(ary[2], 16) + ', ' + parseInt(ary[3], 16) + ')';
}
if (Array.isArray(arg)) {
for (let i = 0; i < arg.length; i++) {
arg[i] = convert(arg[i]);
}
} else {
arg = convert(arg);
}
return arg;
};
// add support for cursor styles 5 and 6, fixes #270
hterm.VT.CSI[' q'] = parseState => {
const arg = parseState.args[0];
if (arg === '0' || arg === '1') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.BLOCK);
this.terminal.setCursorBlink(true);
} else if (arg === '2') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.BLOCK);
this.terminal.setCursorBlink(false);
} else if (arg === '3') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.UNDERLINE);
this.terminal.setCursorBlink(true);
} else if (arg === '4') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.UNDERLINE);
this.terminal.setCursorBlink(false);
} else if (arg === '5') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.BEAM);
this.terminal.setCursorBlink(true);
} else if (arg === '6') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.BEAM);
this.terminal.setCursorBlink(false);
} else {
//eslint-disable-next-line no-console
console.warn('Unknown cursor style: ' + arg);
}
};
export default hterm;
export {lib};

View file

@ -1,79 +0,0 @@
/**
* Keyboard event keyCodes have proven to be really unreliable.
* This util function will cover most of the edge cases where
* String.fromCharCode() doesn't work.
*/
const _toAscii = {
188: '44',
109: '45',
190: '46',
191: '47',
192: '96',
220: '92',
222: '39',
221: '93',
219: '91',
173: '45',
187: '61', // IE Key codes
186: '59', // IE Key codes
189: '45' // IE Key codes
};
const _shiftUps = {
96: '~',
49: '!',
50: '@',
51: '#',
52: '$',
53: '%',
54: '^',
55: '&',
56: '*',
57: '(',
48: ')',
45: '_',
61: '+',
91: '{',
93: '}',
92: '|',
59: ':',
39: "'",
44: '<',
46: '>',
47: '?'
};
const _arrowKeys = {
38: '',
40: '',
39: '',
37: ''
};
/**
* This fn takes a keyboard event and returns
* the character that was pressed. This fn
* purposely doesn't take into account if the alt/meta
* key was pressed.
*/
export default function fromCharCode(e) {
let code = String(e.which);
if ({}.hasOwnProperty.call(_arrowKeys, code)) {
return _arrowKeys[code];
}
if ({}.hasOwnProperty.call(_toAscii, code)) {
code = _toAscii[code];
}
const char = String.fromCharCode(code);
if (e.shiftKey) {
if ({}.hasOwnProperty.call(_shiftUps, code)) {
return _shiftUps[code];
}
return char.toUpperCase();
}
return char.toLowerCase();
}

View file

@ -1,106 +0,0 @@
import {remote} from 'electron';
const getCommand = remote.require('./utils/keymaps/get-command');
// Key handling is deeply inspired by Mousetrap
// https://github.com/ccampbell/mousetrap
const _EXCLUDE = {
16: 'shift',
17: 'ctrl',
18: 'alt',
91: 'meta',
93: 'meta',
224: 'meta'
};
const _MAP = {
8: 'backspace',
9: 'tab',
13: 'enter',
20: 'capslock',
27: 'esc',
32: 'space',
33: 'pageup',
34: 'pagedown',
35: 'end',
36: 'home',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
45: 'ins',
46: 'del'
};
const _KEYCODE_MAP = {
106: '*',
107: '+',
109: '-',
110: '.',
111: '/',
186: ';',
187: '=',
188: ',',
189: '-',
190: '.',
191: '/',
192: '`',
219: '[',
220: '\\',
221: ']',
222: "'"
};
const characterFromEvent = e => {
if (_EXCLUDE[e.which]) {
return;
}
if (_MAP[e.which]) {
return _MAP[e.which];
}
if (_KEYCODE_MAP[e.which]) {
return _KEYCODE_MAP[e.which];
}
// if it is not in the special map
// with keydown and keyup events the character seems to always
// come in as an uppercase character whether you are pressing shift
// or not. we should make sure it is always lowercase for comparisons
return String.fromCharCode(e.which).toLowerCase();
};
export default function returnKey(e) {
const character = characterFromEvent(e);
if (!character) {
return false;
}
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');
}
keys.push(character);
return getCommand(keys.join('+'));
}

View file

@ -1,8 +1,7 @@
{
"repository": "zeit/hyper",
"scripts": {
"start":
"echo 'please run `yarn run dev` in one tab and then `yarn run app` in another one'",
"start": "echo 'please run `yarn run dev` in one tab and then `yarn run app` in another one'",
"app": "electron app",
"dev": "webpack -w",
"build": "cross-env NODE_ENV=production webpack",
@ -11,18 +10,21 @@
"test:unit": "ava test/unit",
"test:unit:watch": "yarn run test:unit -- --watch",
"prepush": "yarn test",
"postinstall":
"electron-builder install-app-deps && yarn run rebuild-node-pty",
"rebuild-node-pty":
"electron-rebuild -f -w app/node_modules/node-pty -m app",
"dist":
"yarn run build && cross-env BABEL_ENV=production babel --out-file app/renderer/bundle.js --no-comments --minified app/renderer/bundle.js && build",
"clean":
"node ./bin/rimraf-standalone.js node_modules && node ./bin/rimraf-standalone.js ./app/node_modules && node ./bin/rimraf-standalone.js ./app/renderer"
"postinstall": "electron-builder install-app-deps && yarn run rebuild-node-pty",
"rebuild-node-pty": "electron-rebuild -f -w app/node_modules/node-pty -m app",
"dist": "yarn run build && cross-env BABEL_ENV=production babel --out-file app/renderer/bundle.js --no-comments --minified app/renderer/bundle.js && build",
"clean": "node ./bin/rimraf-standalone.js node_modules && node ./bin/rimraf-standalone.js ./app/node_modules && node ./bin/rimraf-standalone.js ./app/renderer"
},
"eslintConfig": {
"plugins": ["react", "prettier"],
"extends": ["eslint:recommended", "plugin:react/recommended", "prettier"],
"plugins": [
"react",
"prettier"
],
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"prettier"
],
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "module",
@ -39,7 +41,10 @@
"node": true
},
"rules": {
"func-names": ["error", "as-needed"],
"func-names": [
"error",
"as-needed"
],
"no-shadow": "error",
"no-extra-semi": 0,
"react/prop-types": 0,
@ -62,7 +67,8 @@
}
]
},
"overrides": [{
"overrides": [
{
"files": "app/config/config-default.js",
"rules": {
"prettier/prettier": [
@ -80,10 +86,13 @@
}
]
}
}]
}
]
},
"babel": {
"presets": ["react"],
"presets": [
"react"
],
"env": {
"production": {
"plugins": [
@ -120,20 +129,28 @@
"target": [
{
"target": "deb",
"arch": ["x64"]
"arch": [
"x64"
]
},
{
"target": "AppImage",
"arch": ["x64"]
"arch": [
"x64"
]
},
{
"target": "rpm",
"arch": ["x64"]
"arch": [
"x64"
]
}
]
},
"win": {
"target": ["squirrel"]
"target": [
"squirrel"
]
},
"mac": {
"category": "public.app-category.developer-tools",
@ -150,6 +167,7 @@
"color": "2.0.0",
"css-loader": "0.28.7",
"json-loader": "0.5.7",
"mousetrap": "chabou/mousetrap#useCapture",
"ms": "2.0.0",
"php-escape-shell": "1.0.0",
"react": "16.0.0",
@ -159,7 +177,6 @@
"redux": "3.7.2",
"redux-thunk": "2.2.0",
"reselect": "3.0.1",
"runes": "0.4.3",
"seamless-immutable": "7.1.2",
"semver": "5.4.1",
"uuid": "3.1.0",

View file

@ -1061,14 +1061,10 @@ balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
base64-js@1.2.0:
base64-js@1.2.0, base64-js@^1.0.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1"
base64-js@^1.0.2:
version "1.2.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886"
bcrypt-pbkdf@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
@ -3992,7 +3988,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
dependencies:
brace-expansion "^1.1.7"
minimist@0.0.8:
minimist@0.0.8, minimist@~0.0.1:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@ -4000,10 +3996,6 @@ minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
minimist@~0.0.1:
version "0.0.10"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
mkdirp@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12"
@ -4016,6 +4008,10 @@ mkdirp@0.5.0:
dependencies:
minimist "0.0.8"
mousetrap@chabou/mousetrap#useCapture:
version "1.6.1"
resolved "https://codeload.github.com/chabou/mousetrap/tar.gz/c95eeeaafba1131dd8d35bc130d4a79b2ff9261a"
ms@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
@ -5314,10 +5310,6 @@ run-async@^2.2.0:
dependencies:
is-promise "^2.1.0"
runes@0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/runes/-/runes-0.4.3.tgz#32f7738844bc767b65cc68171528e3373c7bb355"
rx-lite-aggregates@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"