diff --git a/app/commands.js b/app/commands.js new file mode 100644 index 00000000..af27997c --- /dev/null +++ b/app/commands.js @@ -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); + } +}; diff --git a/app/config.js b/app/config.js index 58cb18d1..50b726d8 100644 --- a/app/config.js +++ b/app/config.js @@ -73,12 +73,6 @@ exports.getKeymaps = () => { return cfg.keymaps; }; -exports.extendKeymaps = keymaps => { - if (keymaps) { - cfg.keymaps = keymaps; - } -}; - exports.setup = () => { cfg = _import(); _watch(); diff --git a/app/config/import.js b/app/config/import.js index fb3ebca8..ea633990 100644 --- a/app/config/import.js +++ b/app/config/import.js @@ -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 = () => { diff --git a/app/config/init.js b/app/config/init.js index 85b1cdb9..e741f760 100644 --- a/app/config/init.js +++ b/app/config/init.js @@ -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 = { diff --git a/app/config/keymaps.js b/app/config/keymaps.js deleted file mode 100644 index 915d5335..00000000 --- a/app/config/keymaps.js +++ /dev/null @@ -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 -}; diff --git a/app/index.js b/app/index.js index 141cc039..226cd366 100644 --- a/app/index.js +++ b/app/index.js @@ -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(); }; diff --git a/app/keymaps/darwin.json b/app/keymaps/darwin.json index 1b91163e..b254254d 100644 --- a/app/keymaps/darwin.json +++ b/app/keymaps/darwin.json @@ -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" } diff --git a/app/menus/menu.js b/app/menus/menu.js index d7d11afa..61ca2323 100644 --- a/app/menus/menu.js +++ b/app/menus/menu.js @@ -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_; +}; diff --git a/app/menus/menus/darwin.js b/app/menus/menus/darwin.js index 041b7b5c..c2694553 100644 --- a/app/menus/menus/darwin.js +++ b/app/menus/menus/darwin.js @@ -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' diff --git a/app/menus/menus/edit.js b/app/menus/menus/edit.js index 363e6fd7..c20e9380 100644 --- a/app/menus/menus/edit.js +++ b/app/menus/menus/edit.js @@ -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'] } ); } diff --git a/app/menus/menus/shell.js b/app/menus/menus/shell.js index 7e536a6d..e78f7752 100644 --- a/app/menus/menus/shell.js +++ b/app/menus/menus/shell.js @@ -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'] } ] }; diff --git a/app/menus/menus/view.js b/app/menus/menus/view.js index e6a4e8c6..ad168518 100644 --- a/app/menus/menus/view.js +++ b/app/menus/menus/view.js @@ -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); } } ] diff --git a/app/menus/menus/window.js b/app/menus/menus/window.js index 262dd5f3..ed955e58 100644 --- a/app/menus/menus/window.js +++ b/app/menus/menus/window.js @@ -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'] } ] }; diff --git a/app/plugins.js b/app/plugins.js index a5731e64..d6bafd0f 100644 --- a/app/plugins.js +++ b/app/plugins.js @@ -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'); }; diff --git a/app/plugins/extensions.js b/app/plugins/extensions.js index 69e7de84..102bb6c5 100644 --- a/app/plugins/extensions.js +++ b/app/plugins/extensions.js @@ -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' ]) }; diff --git a/app/ui/window.js b/app/ui/window.js index d52f5f7d..934ad331 100644 --- a/app/ui/window.js +++ b/app/ui/window.js @@ -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(); diff --git a/app/utils/keymaps/find-command-by-keys.js b/app/utils/keymaps/find-command-by-keys.js deleted file mode 100644 index 5cba0d05..00000000 --- a/app/utils/keymaps/find-command-by-keys.js +++ /dev/null @@ -1,5 +0,0 @@ -const normalize = require('./normalize'); - -module.exports = (keys, commands) => { - return commands[normalize(keys)]; -}; diff --git a/app/utils/keymaps/get-command.js b/app/utils/keymaps/get-command.js deleted file mode 100644 index a27dd3ed..00000000 --- a/app/utils/keymaps/get-command.js +++ /dev/null @@ -1,6 +0,0 @@ -const {getKeymaps} = require('../../config'); -const findCommandByKeys = require('./find-command-by-keys'); - -module.exports = keys => { - return findCommandByKeys(keys, getKeymaps().keys); -}; diff --git a/app/utils/keymaps/normalize.js b/app/utils/keymaps/normalize.js deleted file mode 100644 index 105fb478..00000000 --- a/app/utils/keymaps/normalize.js +++ /dev/null @@ -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('+'); -}; diff --git a/app/utils/map-keys.js b/app/utils/map-keys.js new file mode 100644 index 00000000..c5f99fb5 --- /dev/null +++ b/app/utils/map-keys.js @@ -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; + }, {}); +}; diff --git a/lib/actions/ui.js b/lib/actions/ui.js index bf450278..2b30308f 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -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); + } + } + }); +} diff --git a/lib/command-registry.js b/lib/command-registry.js index 4ea71139..ebf78517 100644 --- a/lib/command-registry.js +++ b/lib/command-registry.js @@ -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); diff --git a/lib/components/term.js b/lib/components/term.js index ff3baabb..3233ead3 100644 --- a/lib/components/term.js +++ b/lib/components/term.js @@ -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) { diff --git a/lib/components/terms.js b/lib/components/terms.js index 97cc562a..048497ce 100644 --- a/lib/components/terms.js +++ b/lib/components/terms.js @@ -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); } diff --git a/lib/constants/ui.js b/lib/constants/ui.js index 23e452cd..98c33c11 100644 --- a/lib/constants/ui.js +++ b/lib/constants/ui.js @@ -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'; diff --git a/lib/containers/hyper.js b/lib/containers/hyper.js index 491efe34..142aa837 100644 --- a/lib/containers/hyper.js +++ b/lib/containers/hyper.js @@ -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)); } }; }, diff --git a/lib/hterm.js b/lib/hterm.js deleted file mode 100644 index 5a3efc60..00000000 --- a/lib/hterm.js +++ /dev/null @@ -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 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}; diff --git a/lib/utils/key-code.js b/lib/utils/key-code.js deleted file mode 100644 index 41accc8b..00000000 --- a/lib/utils/key-code.js +++ /dev/null @@ -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(); -} diff --git a/lib/utils/keymaps.js b/lib/utils/keymaps.js deleted file mode 100644 index b7e29def..00000000 --- a/lib/utils/keymaps.js +++ /dev/null @@ -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('+')); -} diff --git a/package.json b/package.json index de646282..8e38b173 100644 --- a/package.json +++ b/package.json @@ -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,28 +67,32 @@ } ] }, - "overrides": [{ - "files": "app/config/config-default.js", - "rules": { - "prettier/prettier": [ - "error", - { - "printWidth": 120, - "tabWidth": 2, - "singleQuote": true, - "trailingComma": "es5", - "bracketSpacing": false, - "semi": true, - "useTabs": false, - "parser": "babylon", - "jsxBracketSameLine": false - } - ] + "overrides": [ + { + "files": "app/config/config-default.js", + "rules": { + "prettier/prettier": [ + "error", + { + "printWidth": 120, + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "es5", + "bracketSpacing": false, + "semi": true, + "useTabs": false, + "parser": "babylon", + "jsxBracketSameLine": false + } + ] + } } - }] + ] }, "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", diff --git a/yarn.lock b/yarn.lock index 092cfee6..00399147 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"