Fix keyboard shortcuts on Linux and Windows (#1058)

* `command` => `mod`

* `Option` => `Alt`

* Allow hterm to consume a keyboard event only if it's not a Hyper accelerator

* Remove `console.log`s

* Say no to bikeshedding

* We already clear the selection on `onKeyDown`

* Add comments

* Remove meaningless comment

* Add fullscreen shortcut for Windows and Linux

* Use the accelerators defined in `accelerators.js` for the app menu
This commit is contained in:
Matheus Fernandes 2016-11-30 18:19:45 -02:00 committed by Guillermo Rauch
parent 810e0a9806
commit be286c0d5a
4 changed files with 197 additions and 56 deletions

138
app/accelerators.js Normal file
View file

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

View file

@ -2,6 +2,8 @@ const os = require('os');
const path = require('path'); const path = require('path');
const {app, shell, dialog} = require('electron'); const {app, shell, dialog} = require('electron');
const {accelerators} = require('./accelerators');
const isMac = process.platform === 'darwin'; const isMac = process.platform === 'darwin';
const appName = app.getName(); const appName = app.getName();
@ -22,7 +24,7 @@ module.exports = ({createWindow, updatePlugins}) => {
}, },
{ {
label: 'Preferences...', label: 'Preferences...',
accelerator: 'Cmd+,', accelerator: accelerators.preferences,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('preferences'); focusedWindow.rpc.emit('preferences');
@ -64,14 +66,14 @@ module.exports = ({createWindow, updatePlugins}) => {
submenu: [ submenu: [
{ {
label: 'New Window', label: 'New Window',
accelerator: 'CmdOrCtrl+N', accelerator: accelerators.newWindow,
click() { click() {
createWindow(); createWindow();
} }
}, },
{ {
label: 'New Tab', label: 'New Tab',
accelerator: 'CmdOrCtrl+T', accelerator: accelerators.newTab,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('termgroup add req'); focusedWindow.rpc.emit('termgroup add req');
@ -85,7 +87,7 @@ module.exports = ({createWindow, updatePlugins}) => {
}, },
{ {
label: 'Split Vertically', label: 'Split Vertically',
accelerator: isMac ? 'Cmd+D' : 'Ctrl+Shift+E', accelerator: accelerators.splitVertically,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('split request vertical'); focusedWindow.rpc.emit('split request vertical');
@ -94,7 +96,7 @@ module.exports = ({createWindow, updatePlugins}) => {
}, },
{ {
label: 'Split Horizontally', label: 'Split Horizontally',
accelerator: isMac ? 'Cmd+Shift+D' : 'Ctrl+Shift+O', accelerator: accelerators.splitHorizontally,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('split request horizontal'); focusedWindow.rpc.emit('split request horizontal');
@ -106,7 +108,7 @@ module.exports = ({createWindow, updatePlugins}) => {
}, },
{ {
label: 'Close Session', label: 'Close Session',
accelerator: 'CmdOrCtrl+W', accelerator: accelerators.closeSession,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('termgroup close req'); focusedWindow.rpc.emit('termgroup close req');
@ -116,7 +118,7 @@ module.exports = ({createWindow, updatePlugins}) => {
{ {
label: isMac ? 'Close Window' : 'Quit', label: isMac ? 'Close Window' : 'Quit',
role: 'close', role: 'close',
accelerator: 'CmdOrCtrl+Shift+W' accelerator: accelerators.closeWindow
} }
] ]
}; };
@ -125,32 +127,38 @@ module.exports = ({createWindow, updatePlugins}) => {
label: 'Edit', label: 'Edit',
submenu: [ submenu: [
{ {
role: 'undo' role: 'undo',
accelerator: accelerators.undo
}, },
{ {
role: 'redo' role: 'redo',
accelerator: accelerators.redo
}, },
{ {
type: 'separator' type: 'separator'
}, },
{ {
role: 'cut' role: 'cut',
accelerator: accelerators.cut
}, },
{ {
role: 'copy' role: 'copy',
accelerator: accelerators.copy
}, },
{ {
role: 'paste' role: 'paste',
accelerator: accelerators.paste
}, },
{ {
role: 'selectall' role: 'selectall',
accelerator: accelerators.selectAll
}, },
{ {
type: 'separator' type: 'separator'
}, },
{ {
label: 'Clear', label: 'Clear',
accelerator: 'CmdOrCtrl+K', accelerator: accelerators.clear,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('session clear req'); focusedWindow.rpc.emit('session clear req');
@ -165,7 +173,7 @@ module.exports = ({createWindow, updatePlugins}) => {
{type: 'separator'}, {type: 'separator'},
{ {
label: 'Preferences...', label: 'Preferences...',
accelerator: 'Cmd+,', accelerator: accelerators.preferences,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('preferences'); focusedWindow.rpc.emit('preferences');
@ -182,7 +190,7 @@ module.exports = ({createWindow, updatePlugins}) => {
submenu: [ submenu: [
{ {
label: 'Reload', label: 'Reload',
accelerator: 'CmdOrCtrl+R', accelerator: accelerators.reload,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('reload'); focusedWindow.rpc.emit('reload');
@ -191,7 +199,7 @@ module.exports = ({createWindow, updatePlugins}) => {
}, },
{ {
label: 'Full Reload', label: 'Full Reload',
accelerator: 'CmdOrCtrl+Shift+R', accelerator: accelerators.fullReload,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.reload(); focusedWindow.reload();
@ -200,7 +208,7 @@ module.exports = ({createWindow, updatePlugins}) => {
}, },
{ {
label: 'Toggle Developer Tools', label: 'Toggle Developer Tools',
accelerator: isMac ? 'Alt+Command+I' : 'Ctrl+Shift+I', accelerator: accelerators.toggleDevTools,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.webContents.toggleDevTools(); focusedWindow.webContents.toggleDevTools();
@ -212,7 +220,7 @@ module.exports = ({createWindow, updatePlugins}) => {
}, },
{ {
label: 'Reset Zoom Level', label: 'Reset Zoom Level',
accelerator: 'CmdOrCtrl+0', accelerator: accelerators.resetZoom,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('reset fontSize req'); focusedWindow.rpc.emit('reset fontSize req');
@ -221,7 +229,7 @@ module.exports = ({createWindow, updatePlugins}) => {
}, },
{ {
label: 'Zoom In', label: 'Zoom In',
accelerator: 'CmdOrCtrl+plus', accelerator: accelerators.zoomIn,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('increase fontSize req'); focusedWindow.rpc.emit('increase fontSize req');
@ -230,7 +238,7 @@ module.exports = ({createWindow, updatePlugins}) => {
}, },
{ {
label: 'Zoom Out', label: 'Zoom Out',
accelerator: 'CmdOrCtrl+-', accelerator: accelerators.zoomOut,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('decrease fontSize req'); focusedWindow.rpc.emit('decrease fontSize req');
@ -245,7 +253,7 @@ module.exports = ({createWindow, updatePlugins}) => {
submenu: [ submenu: [
{ {
label: 'Update All Now', label: 'Update All Now',
accelerator: 'CmdOrCtrl+Shift+U', accelerator: accelerators.updatePlugins,
click() { click() {
updatePlugins(); updatePlugins();
} }
@ -257,7 +265,8 @@ module.exports = ({createWindow, updatePlugins}) => {
role: 'window', role: 'window',
submenu: [ submenu: [
{ {
role: 'minimize' role: 'minimize',
accelerator: accelerators.minimize
}, },
{ {
role: 'zoom' role: 'zoom'
@ -267,7 +276,7 @@ module.exports = ({createWindow, updatePlugins}) => {
}, },
{ {
label: 'Show Previous Tab', label: 'Show Previous Tab',
accelerator: 'CmdOrCtrl+Option+Left', accelerator: accelerators.showPreviousTab,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('move left req'); focusedWindow.rpc.emit('move left req');
@ -276,7 +285,7 @@ module.exports = ({createWindow, updatePlugins}) => {
}, },
{ {
label: 'Show Next Tab', label: 'Show Next Tab',
accelerator: 'CmdOrCtrl+Option+Right', accelerator: accelerators.showNextTab,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('move right req'); focusedWindow.rpc.emit('move right req');
@ -288,7 +297,7 @@ module.exports = ({createWindow, updatePlugins}) => {
}, },
{ {
label: 'Select Next Pane', label: 'Select Next Pane',
accelerator: 'Ctrl+Alt+Tab', accelerator: accelerators.selectNextPane,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('next pane req'); focusedWindow.rpc.emit('next pane req');
@ -297,7 +306,7 @@ module.exports = ({createWindow, updatePlugins}) => {
}, },
{ {
label: 'Select Previous Pane', label: 'Select Previous Pane',
accelerator: 'Ctrl+Shift+Alt+Tab', accelerator: accelerators.selectPreviousPane,
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.rpc.emit('prev pane req'); focusedWindow.rpc.emit('prev pane req');

View file

@ -44,22 +44,22 @@ class Hyper extends Component {
const lastIndex = this.terms.getLastTermIndex(); const lastIndex = this.terms.getLastTermIndex();
const document = term.getTermDocument(); const document = term.getTermDocument();
const keys = new Mousetrap(document); const keys = new Mousetrap(document);
keys.bind('command+1', moveTo.bind(this, 0)); keys.bind('mod+1', moveTo.bind(this, 0));
keys.bind('command+2', moveTo.bind(this, 1)); keys.bind('mod+2', moveTo.bind(this, 1));
keys.bind('command+3', moveTo.bind(this, 2)); keys.bind('mod+3', moveTo.bind(this, 2));
keys.bind('command+4', moveTo.bind(this, 3)); keys.bind('mod+4', moveTo.bind(this, 3));
keys.bind('command+5', moveTo.bind(this, 4)); keys.bind('mod+5', moveTo.bind(this, 4));
keys.bind('command+6', moveTo.bind(this, 5)); keys.bind('mod+6', moveTo.bind(this, 5));
keys.bind('command+7', moveTo.bind(this, 6)); keys.bind('mod+7', moveTo.bind(this, 6));
keys.bind('command+8', moveTo.bind(this, 7)); keys.bind('mod+8', moveTo.bind(this, 7));
keys.bind('command+9', moveTo.bind(this, lastIndex)); keys.bind('mod+9', moveTo.bind(this, lastIndex));
keys.bind('command+shift+left', moveLeft); keys.bind('mod+shift+left', moveLeft);
keys.bind('command+shift+right', moveRight); keys.bind('mod+shift+right', moveRight);
keys.bind('command+shift+[', moveLeft); keys.bind('mod+shift+[', moveLeft);
keys.bind('command+shift+]', moveRight); keys.bind('mod+shift+]', moveRight);
keys.bind('command+alt+left', moveLeft); keys.bind('mod+alt+left', moveLeft);
keys.bind('command+alt+right', moveRight); keys.bind('mod+alt+right', moveRight);
keys.bind('ctrl+shift+tab', moveLeft); keys.bind('ctrl+shift+tab', moveLeft);
keys.bind('ctrl+tab', moveRight); keys.bind('ctrl+tab', moveRight);
@ -68,10 +68,10 @@ class Hyper extends Component {
keys.bind('alt+right', bound('moveWordRight')); keys.bind('alt+right', bound('moveWordRight'));
keys.bind('alt+backspace', bound('deleteWordLeft')); keys.bind('alt+backspace', bound('deleteWordLeft'));
keys.bind('alt+del', bound('deleteWordRight')); keys.bind('alt+del', bound('deleteWordRight'));
keys.bind('command+backspace', bound('deleteLine')); keys.bind('mod+backspace', bound('deleteLine'));
keys.bind('command+left', bound('moveToStart')); keys.bind('mod+left', bound('moveToStart'));
keys.bind('command+right', bound('moveToEnd')); keys.bind('mod+right', bound('moveToEnd'));
keys.bind('command+a', bound('selectAll')); keys.bind('mod+a', bound('selectAll'));
this.keys = keys; this.keys = keys;
} }

View file

@ -1,6 +1,8 @@
import {hterm, lib} from 'hterm-umdjs'; import {hterm, lib} from 'hterm-umdjs';
import runes from 'runes'; import runes from 'runes';
import {isAccelerator} from '../app/accelerators';
import fromCharCode from './utils/key-code'; import fromCharCode from './utils/key-code';
import selection from './utils/selection'; import selection from './utils/selection';
@ -164,7 +166,8 @@ hterm.Keyboard.prototype.onKeyDown_ = function (e) {
e.preventDefault(); e.preventDefault();
} }
if (e.metaKey || e.altKey || (e.ctrlKey && e.code === 'Tab')) { if (isAccelerator(e)) {
// hterm shouldn't consume a hyper accelerator
return; return;
} }
if ((!e.ctrlKey || e.code !== 'ControlLeft') && if ((!e.ctrlKey || e.code !== 'ControlLeft') &&
@ -176,15 +179,6 @@ hterm.Keyboard.prototype.onKeyDown_ = function (e) {
return oldKeyDown.call(this, e); return oldKeyDown.call(this, e);
}; };
const oldKeyPress = hterm.Keyboard.prototype.onKeyPress_;
hterm.Keyboard.prototype.onKeyPress_ = function (e) {
if (e.metaKey) {
return;
}
selection.clear(this.terminal);
return oldKeyPress.call(this, e);
};
// we re-implement `wipeContents` to preserve the line // we re-implement `wipeContents` to preserve the line
// and cursor position that the client is in. // and cursor position that the client is in.
// otherwise the user ends up with a completely clear // otherwise the user ends up with a completely clear