feature(cloudcmd) add ability to toggle vim hotkes using Esc

This commit is contained in:
coderaiser 2021-01-15 21:12:11 +02:00
parent 595805eeaf
commit cd7bf0fe01
6 changed files with 541 additions and 467 deletions

View file

@ -201,7 +201,7 @@ Then, start the server again with `cloudcmd` and reload the page.
| `Insert` | select current file (and move to next)
| `F9` | context menu
| `~` | console
| `Ctrl + Click` | open file on new tab
| `Esc` | toggle vim hot keys
### Vim

View file

@ -8,15 +8,15 @@ const rendy = require('rendy');
const load = require('load.js');
const tryToCatch = require('try-to-catch');
const {addSlashToEnd} = require('format-io');
const pascalCase = require('just-pascal-case');
const currify = require('currify');
const isDev = process.env.NODE_ENV === 'development';
const Images = require('./dom/images');
const {unregisterSW} = require('./sw/register');
const getJsonFromFileTable = require('./get-json-from-file-table');
const currify = require('currify');
const Key = require('./key');
const noJS = (a) => a.replace(/.js$/, '');
@ -206,7 +206,7 @@ function CloudCmdProto(DOM) {
};
const initModules = async () => {
CloudCmd.Key = new CloudCmd.Key();
CloudCmd.Key = Key;
CloudCmd.Key.bind();
const [, modules] = await tryToCatch(Files.get, 'modules');

18
client/key/binder.js Normal file
View file

@ -0,0 +1,18 @@
'use strict';
module.exports.createBinder = () => {
let binded = false;
return {
isBind() {
return binded;
},
setBind() {
binded = true;
},
unsetBind() {
binded = false;
},
};
};

View file

@ -7,495 +7,485 @@ const Info = DOM.CurrentInfo;
const exec = require('execon');
const clipboard = require('@cloudcmd/clipboard');
const Events = require('../dom/events');
const Buffer = require('../dom/buffer');
const Events = require('../dom/events');
const KEY = require('./key');
const vim = require('./vim');
const setCurrentByChar = require('./set-current-by-char');
const {createBinder} = require('./binder');
const fullstore = require('fullstore');
const Chars = fullstore();
const isVimEnabled = fullstore(false);
const toggleVim = (keyCode) => keyCode === KEY.ESC && isVimEnabled(!isVimEnabled());
Chars([]);
KeyProto.prototype = KEY;
CloudCmd.Key = KeyProto;
const {loadDir} = CloudCmd;
const {assign} = Object;
function KeyProto() {
let Binded;
const binder = createBinder();
module.exports = assign(binder, KEY);
module.exports.bind = () => {
Events.addKey(listener, true);
binder.setBind();
};
module.exports._listener = listener;
function getChar(event) {
/*
* event.keyIdentifier deprecated in chrome v51
* but event.key is absent in chrome <= v51
*/
const Key = this;
const {
key,
shift,
keyCode,
keyIdentifier,
} = event;
const char = key || fromCharCode(keyIdentifier);
const symbol = getSymbol(shift, keyCode);
this.isBind = () => {
return Binded;
};
return [symbol, char];
}
async function listener(event) {
const {keyCode} = event;
this.setBind = () => {
Binded = true;
};
// strange chrome bug calles listener twice
// in second time event misses a lot fields
if (typeof event.altKey === 'undefined')
return;
this.unsetBind = () => {
Binded = false;
};
const alt = event.altKey;
const ctrl = event.ctrlKey;
const meta = event.metaKey;
const isBetween = keyCode >= KEY.ZERO && keyCode <= KEY.Z;
const isNumpad = /Numpad/.test(event.code);
this.bind = () => {
Events.addKey(listener, true);
Binded = true;
};
const [symbol, char] = getChar(event);
function getChar(event) {
/*
* event.keyIdentifier deprecated in chrome v51
* but event.key is absent in chrome <= v51
*/
if (event.key)
return event.key;
return fromCharCode(event.keyIdentifier);
if (!binder.isBind())
return;
toggleVim(keyCode);
const isVim = isVimEnabled() || CloudCmd.config('vim');
if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || symbol))
return setCurrentByChar(char, Chars);
Chars([]);
await switchKey(event);
if (keyCode >= KEY.F1 && keyCode <= KEY.F10)
return;
if (isVim)
vim(char, event);
}
function getSymbol(shift, keyCode) {
switch(keyCode) {
case KEY.DOT:
return '.';
case KEY.HYPHEN:
return shift ? '_' : '-';
case KEY.EQUAL:
return shift ? '+' : '=';
}
async function listener(event) {
const {keyCode} = event;
// strange chrome bug calles listener twice
// in second time event misses a lot fields
if (typeof event.altKey === 'undefined')
return;
const alt = event.altKey;
const ctrl = event.ctrlKey;
const shift = event.shiftKey;
const meta = event.metaKey;
const isBetween = keyCode >= KEY.ZERO && keyCode <= KEY.Z;
const isNumpad = /Numpad/.test(event.code);
let char = getChar(event);
let isSymbol = ['.', '_', '-', '+', '='].includes(char);
if (!isSymbol) {
isSymbol = getSymbol(shift, keyCode);
if (isSymbol)
char = isSymbol;
}
if (!Key.isBind())
return;
const isVim = CloudCmd.config('vim');
if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || isSymbol))
return setCurrentByChar(char, Chars);
Chars([]);
await switchKey(event);
if (keyCode >= KEY.F1 && keyCode <= KEY.F10)
return;
if (isVim)
vim(char, event);
return '';
}
function fromCharCode(keyIdentifier) {
const code = keyIdentifier.substring(2);
const hex = parseInt(code, 16);
const char = String.fromCharCode(hex);
return char;
}
async function switchKey(event) {
let i;
let isSelected;
let prev;
let next;
let current = Info.element;
let dataName;
const {
name,
panel,
path,
isDir,
} = Info;
const {Operation, loadDir} = CloudCmd;
const {keyCode} = event;
const alt = event.altKey;
const shift = event.shiftKey;
const ctrl = event.ctrlKey;
const meta = event.metaKey;
const ctrlMeta = ctrl || meta;
if (current) {
prev = current.previousSibling;
next = current.nextSibling;
}
function getSymbol(shift, keyCode) {
switch(keyCode) {
case KEY.DOT:
return '.';
case KEY.HYPHEN:
return shift ? '_' : '-';
case KEY.EQUAL:
return shift ? '+' : '=';
}
}
switch(keyCode) {
case KEY.TAB:
DOM.changePanel();
event.preventDefault();
break;
function fromCharCode(keyIdentifier) {
const code = keyIdentifier.substring(2);
const hex = parseInt(code, 16);
const char = String.fromCharCode(hex);
return char;
}
case KEY.INSERT:
DOM .toggleSelectedFile(current)
.setCurrentFile(next);
break;
async function switchKey(event) {
let i;
let isSelected;
let prev;
let next;
let current = Info.element;
let dataName;
const {
name,
panel,
path,
isDir,
} = Info;
const {Operation} = CloudCmd;
const {keyCode} = event;
const alt = event.altKey;
const shift = event.shiftKey;
const ctrl = event.ctrlKey;
const meta = event.metaKey;
const ctrlMeta = ctrl || meta;
if (current) {
prev = current.previousSibling;
next = current.nextSibling;
}
switch(keyCode) {
case Key.TAB:
DOM.changePanel();
event.preventDefault();
break;
case Key.INSERT:
DOM .toggleSelectedFile(current)
.setCurrentFile(next);
break;
case Key.INSERT_MAC:
DOM .toggleSelectedFile(current)
.setCurrentFile(next);
break;
case Key.DELETE:
if (shift)
Operation.show('delete:silent');
else
Operation.show('delete');
break;
case Key.ASTERISK:
DOM.toggleAllSelectedFiles(current);
break;
case Key.PLUS:
DOM.expandSelection();
event.preventDefault();
break;
case Key.MINUS:
DOM.shrinkSelection();
event.preventDefault();
break;
case Key.F1:
CloudCmd.Help.show();
event.preventDefault();
break;
case Key.F2:
CloudCmd.UserMenu.show();
break;
case Key.F3:
event.preventDefault();
if (Info.isDir)
await loadDir({
path,
});
else if (shift)
CloudCmd.Markdown.show(path);
else if (ctrlMeta)
CloudCmd.sortPanel('name');
else
CloudCmd.View.show();
break;
case Key.F4:
if (shift)
CloudCmd.EditFileVim.show();
else
CloudCmd.EditFile.show();
event.preventDefault();
break;
case Key.F5:
if (ctrlMeta)
CloudCmd.sortPanel('date');
else if (alt)
Operation.show('pack');
else
Operation.show('copy');
event.preventDefault();
break;
case Key.F6:
if (ctrlMeta)
CloudCmd.sortPanel('size');
else if (shift)
DOM.renameCurrent(current);
else
Operation.show('move');
event.preventDefault();
break;
case Key.F7:
if (shift)
DOM.promptNewFile();
else
DOM.promptNewDir();
event.preventDefault();
break;
case Key.F8:
case KEY.INSERT_MAC:
DOM .toggleSelectedFile(current)
.setCurrentFile(next);
break;
case KEY.DELETE:
if (shift)
Operation.show('delete:silent');
else
Operation.show('delete');
event.preventDefault();
break;
break;
case KEY.ASTERISK:
DOM.toggleAllSelectedFiles(current);
break;
case KEY.PLUS:
DOM.expandSelection();
event.preventDefault();
break;
case KEY.MINUS:
DOM.shrinkSelection();
event.preventDefault();
break;
case KEY.F1:
CloudCmd.Help.show();
event.preventDefault();
break;
case KEY.F2:
CloudCmd.UserMenu.show();
break;
case KEY.F3:
event.preventDefault();
case Key.F9:
if (alt)
Operation.show('extract');
else
CloudCmd.Menu.show();
event.preventDefault();
break;
case Key.F10:
CloudCmd.Config.show();
event.preventDefault();
break;
case Key.TRA:
event.preventDefault();
if (shift)
return CloudCmd.Terminal.show();
CloudCmd.Konsole.show();
break;
case KEY.BRACKET_CLOSE:
CloudCmd.Konsole.show();
event.preventDefault();
break;
case Key.SPACE:
if (!isDir || name === '..')
isSelected = true;
else
isSelected = DOM.isSelected(current);
exec.if(isSelected, () => {
DOM.toggleSelectedFile(current);
}, (callback) => {
DOM.loadCurrentSize(current, callback);
if (Info.isDir)
await loadDir({
path,
});
else if (shift)
CloudCmd.Markdown.show(path);
else if (ctrlMeta)
CloudCmd.sortPanel('name');
else
CloudCmd.View.show();
break;
case KEY.F4:
if (shift)
CloudCmd.EditFileVim.show();
else
CloudCmd.EditFile.show();
event.preventDefault();
break;
case KEY.F5:
if (ctrlMeta)
CloudCmd.sortPanel('date');
else if (alt)
Operation.show('pack');
else
Operation.show('copy');
event.preventDefault();
break;
case KEY.F6:
if (ctrlMeta)
CloudCmd.sortPanel('size');
else if (shift)
DOM.renameCurrent(current);
else
Operation.show('move');
event.preventDefault();
break;
case KEY.F7:
if (shift)
DOM.promptNewFile();
else
DOM.promptNewDir();
event.preventDefault();
break;
case KEY.F8:
Operation.show('delete');
event.preventDefault();
break;
case KEY.F9:
if (alt)
Operation.show('extract');
else
CloudCmd.Menu.show();
event.preventDefault();
break;
case KEY.F10:
CloudCmd.Config.show();
event.preventDefault();
break;
case KEY.TRA:
event.preventDefault();
if (shift)
return CloudCmd.Terminal.show();
CloudCmd.Konsole.show();
break;
case KEY.BRACKET_CLOSE:
CloudCmd.Konsole.show();
event.preventDefault();
break;
case KEY.SPACE:
if (!isDir || name === '..')
isSelected = true;
else
isSelected = DOM.isSelected(current);
exec.if(isSelected, () => {
DOM.toggleSelectedFile(current);
}, (callback) => {
DOM.loadCurrentSize(current, callback);
});
event.preventDefault();
break;
case KEY.U:
if (ctrlMeta) {
DOM.swapPanels();
event.preventDefault();
break;
case Key.U:
if (ctrlMeta) {
DOM.swapPanels();
event.preventDefault();
}
break;
/* navigation on file table: *
* in case of pressing button 'up', *
* select previous row */
case Key.UP:
if (shift)
DOM.toggleSelectedFile(current);
DOM.setCurrentFile(prev);
event.preventDefault();
break;
/* in case of pressing button 'down', *
* select next row */
case Key.DOWN:
if (shift)
DOM.toggleSelectedFile(current);
DOM.setCurrentFile(next);
event.preventDefault();
break;
case Key.LEFT:
if (!alt)
return;
event.preventDefault();
dataName = Info.panel.getAttribute('data-name');
if (dataName === 'js-right')
DOM.duplicatePanel();
break;
case Key.RIGHT:
if (!alt)
return;
event.preventDefault();
dataName = Info.panel.getAttribute('data-name');
if (dataName === 'js-left')
DOM.duplicatePanel();
break;
/* in case of pressing button 'Home', *
* go to top element */
case Key.HOME:
DOM.setCurrentFile(Info.first);
event.preventDefault();
break;
/* in case of pressing button 'End', select last element */
case Key.END:
DOM.setCurrentFile(Info.last);
event.preventDefault();
break;
/* если нажали клавишу page down проматываем экран */
case Key.PAGE_DOWN:
DOM.scrollByPages(panel, 1);
for (i = 0; i < 30; i++) {
if (!current.nextSibling)
break;
current = current.nextSibling;
}
DOM.setCurrentFile(current);
event.preventDefault();
break;
/* если нажали клавишу page up проматываем экран */
case Key.PAGE_UP:
DOM.scrollByPages(panel, -1);
for (i = 0; i < 30; i++) {
if (!current.previousSibling)
break;
current = current.previousSibling;
}
DOM.setCurrentFile(current);
event.preventDefault();
break;
case Key.ENTER:
if (Info.isDir)
await loadDir({path});
else
CloudCmd.View.show();
break;
case Key.BACKSPACE:
CloudCmd.goToParentDir();
event.preventDefault();
break;
case Key.BACKSLASH:
if (ctrlMeta)
await loadDir({
path: '/',
});
break;
case Key.A:
if (ctrlMeta) {
DOM.selectAllFiles();
event.preventDefault();
}
break;
case Key.G:
if (alt) {
DOM.goToDirectory();
event.preventDefault();
}
break;
case Key.M:
if (ctrlMeta) {
if (shift)
CloudCmd.EditNamesVim.show();
else
CloudCmd.EditNames.show();
event.preventDefault();
}
break;
case Key.P:
if (!ctrlMeta)
return;
event.preventDefault();
clipboard
.writeText(Info.dirPath)
.catch(CloudCmd.log);
break;
/**
* обновляем страницу,
* загружаем содержимое каталога
* при этом данные берём всегда с
* сервера, а не из кэша
* (обновляем кэш)
*/
case Key.R:
if (ctrlMeta) {
CloudCmd.log('reloading page...\n');
CloudCmd.refresh();
event.preventDefault();
}
break;
case Key.C:
if (ctrlMeta)
Buffer.copy();
break;
case Key.X:
if (ctrlMeta)
Buffer.cut();
break;
case Key.V:
if (ctrlMeta)
Buffer.paste();
break;
case Key.Z:
if (ctrlMeta)
Buffer.clear();
break;
/* чистим хранилище */
case Key.D:
if (ctrlMeta) {
CloudCmd.log('clearing storage...');
await DOM.Storage.clear();
CloudCmd.log('storage cleared');
event.preventDefault();
}
break;
}
break;
/* navigation on file table: *
* in case of pressing button 'up', *
* select previous row */
case KEY.UP:
if (shift)
DOM.toggleSelectedFile(current);
DOM.setCurrentFile(prev);
event.preventDefault();
break;
/* in case of pressing button 'down', *
* select next row */
case KEY.DOWN:
if (shift)
DOM.toggleSelectedFile(current);
DOM.setCurrentFile(next);
event.preventDefault();
break;
case KEY.LEFT:
if (!alt)
return;
event.preventDefault();
dataName = Info.panel.getAttribute('data-name');
if (dataName === 'js-right')
DOM.duplicatePanel();
break;
case KEY.RIGHT:
if (!alt)
return;
event.preventDefault();
dataName = Info.panel.getAttribute('data-name');
if (dataName === 'js-left')
DOM.duplicatePanel();
break;
/* in case of pressing button 'Home', *
* go to top element */
case KEY.HOME:
DOM.setCurrentFile(Info.first);
event.preventDefault();
break;
/* in case of pressing button 'End', select last element */
case KEY.END:
DOM.setCurrentFile(Info.last);
event.preventDefault();
break;
/* если нажали клавишу page down проматываем экран */
case KEY.PAGE_DOWN:
DOM.scrollByPages(panel, 1);
for (i = 0; i < 30; i++) {
if (!current.nextSibling)
break;
current = current.nextSibling;
}
DOM.setCurrentFile(current);
event.preventDefault();
break;
/* если нажали клавишу page up проматываем экран */
case KEY.PAGE_UP:
DOM.scrollByPages(panel, -1);
for (i = 0; i < 30; i++) {
if (!current.previousSibling)
break;
current = current.previousSibling;
}
DOM.setCurrentFile(current);
event.preventDefault();
break;
case KEY.ENTER:
if (Info.isDir)
await loadDir({path});
else
CloudCmd.View.show();
break;
case KEY.BACKSPACE:
CloudCmd.goToParentDir();
event.preventDefault();
break;
case KEY.BACKSLASH:
if (ctrlMeta)
await loadDir({
path: '/',
});
break;
case KEY.A:
if (ctrlMeta) {
DOM.selectAllFiles();
event.preventDefault();
}
break;
case KEY.G:
if (alt) {
DOM.goToDirectory();
event.preventDefault();
}
break;
case KEY.M:
if (ctrlMeta) {
if (shift)
CloudCmd.EditNamesVim.show();
else
CloudCmd.EditNames.show();
event.preventDefault();
}
break;
case KEY.P:
if (!ctrlMeta)
return;
event.preventDefault();
clipboard
.writeText(Info.dirPath)
.catch(CloudCmd.log);
break;
/**
* обновляем страницу,
* загружаем содержимое каталога
* при этом данные берём всегда с
* сервера, а не из кэша
* (обновляем кэш)
*/
case KEY.R:
if (ctrlMeta) {
CloudCmd.log('reloading page...\n');
CloudCmd.refresh();
event.preventDefault();
}
break;
case KEY.C:
if (ctrlMeta)
Buffer.copy();
break;
case KEY.X:
if (ctrlMeta)
Buffer.cut();
break;
case KEY.V:
if (ctrlMeta)
Buffer.paste();
break;
case KEY.Z:
if (ctrlMeta)
Buffer.clear();
break;
/* чистим хранилище */
case KEY.D:
if (ctrlMeta) {
CloudCmd.log('clearing storage...');
await DOM.Storage.clear();
CloudCmd.log('storage cleared');
event.preventDefault();
}
break;
}
}

65
client/key/index.spec.js Normal file
View file

@ -0,0 +1,65 @@
'use strict';
const autoGlobals = require('auto-globals');
const test = autoGlobals(require('supertape'));
const stub = require('@cloudcmd/stub');
const mockRequire = require('mock-require');
const {reRequire, stopAll} = mockRequire;
const {ESC} = require('./key');
const {
getDOM,
getCloudCmd,
} = require('./vim/globals.fixture');
global.DOM = getDOM();
global.CloudCmd = getCloudCmd();
test('cloudcmd: client: key: enable vim', async (t) => {
const vim = stub();
mockRequire('./vim', vim);
const {_listener, setBind} = reRequire('.');
const event = {
keyCode: ESC,
key: 'Escape',
altKey: false,
};
setBind();
await _listener(event);
stopAll();
t.calledWith(vim, ['Escape', event]);
t.end();
});
test('cloudcmd: client: key: disable vim', async (t) => {
const vim = stub();
const _config = stub();
const {_listener, setBind} = reRequire('.');
const event = {
keyCode: ESC,
key: 'Escape',
altKey: false,
};
const {CloudCmd} = global;
const {config} = CloudCmd;
CloudCmd.config = _config;
setBind();
await _listener(event);
await _listener(event);
CloudCmd.config = config;
t.calledWith(_config, ['vim']);
t.end();
});

View file

@ -4,6 +4,7 @@ module.exports = {
BACKSPACE : 8,
TAB : 9,
ENTER : 13,
CAPSLOCK : 20,
ESC : 27,
SPACE : 32,