1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

CLI: Resolve #63: Allow customizing shortcuts

This commit is contained in:
Laurent Cozic 2018-02-16 22:53:53 +00:00
parent 71319eee28
commit e4d477fb4c
9 changed files with 423 additions and 250 deletions

View File

@ -35,38 +35,55 @@ const ConsoleWidget = require('./gui/ConsoleWidget.js');
class AppGui {
constructor(app, store) {
this.app_ = app;
this.store_ = store;
constructor(app, store, keymap) {
try {
this.app_ = app;
this.store_ = store;
BaseWidget.setLogger(app.logger());
BaseWidget.setLogger(app.logger());
this.term_ = new TermWrapper(tk.terminal);
this.term_ = new TermWrapper(tk.terminal);
this.renderer_ = null;
this.logger_ = new Logger();
this.buildUi();
// Some keys are directly handled by the tkwidget framework
// so they need to be remapped in a different way.
this.tkWidgetKeys_ = {
'focus_next': 'TAB',
'focus_previous': 'SHIFT_TAB',
'move_up': 'UP',
'move_down': 'DOWN',
'page_down': 'PAGE_DOWN',
'page_up': 'PAGE_UP',
};
this.renderer_ = new Renderer(this.term(), this.rootWidget_);
this.renderer_ = null;
this.logger_ = new Logger();
this.buildUi();
this.app_.on('modelAction', async (event) => {
await this.handleModelAction(event.action);
});
this.renderer_ = new Renderer(this.term(), this.rootWidget_);
this.shortcuts_ = this.setupShortcuts();
this.app_.on('modelAction', async (event) => {
await this.handleModelAction(event.action);
});
this.inputMode_ = AppGui.INPUT_MODE_NORMAL;
this.keymap_ = this.setupKeymap(keymap);
this.commandCancelCalled_ = false;
this.inputMode_ = AppGui.INPUT_MODE_NORMAL;
this.currentShortcutKeys_ = [];
this.lastShortcutKeyTime_ = 0;
this.commandCancelCalled_ = false;
// Recurrent sync is setup only when the GUI is started. In
// a regular command it's not necessary since the process
// exits right away.
reg.setupRecurrentSync();
DecryptionWorker.instance().scheduleStart();
this.currentShortcutKeys_ = [];
this.lastShortcutKeyTime_ = 0;
// Recurrent sync is setup only when the GUI is started. In
// a regular command it's not necessary since the process
// exits right away.
reg.setupRecurrentSync();
DecryptionWorker.instance().scheduleStart();
} catch (error) {
this.fullScreen(false);
console.error(error);
process.exit(1);
}
}
store() {
@ -105,6 +122,7 @@ class AppGui {
buildUi() {
this.rootWidget_ = new ReduxRootWidget(this.store_);
this.rootWidget_.name = 'root';
this.rootWidget_.autoShortcutsEnabled = false;
const folderList = new FolderListWidget();
folderList.style = {
@ -272,152 +290,26 @@ class AppGui {
this.stdout(chalk.cyan.bold('> ' + cmd));
}
setupShortcuts() {
const shortcuts = {};
setupKeymap(keymap) {
const output = [];
shortcuts['TAB'] = {
friendlyName: 'Tab',
description: () => _('Give focus to next pane'),
isDocOnly: true,
}
for (let i = 0; i < keymap.length; i++) {
const item = Object.assign({}, keymap[i]);
shortcuts['SHIFT_TAB'] = {
friendlyName: 'Shift+Tab',
description: () => _('Give focus to previous pane'),
isDocOnly: true,
}
if (!item.command) throw new Error('Missing command for keymap item: ' + JSON.stringify(item));
shortcuts[':'] = {
description: () => _('Enter command line mode'),
action: async () => {
const cmd = await this.widget('statusBar').prompt();
if (!cmd) return;
this.addCommandToConsole(cmd);
await this.processCommand(cmd);
},
};
if (!('type' in item)) item.type = 'exec';
shortcuts['ESC'] = { // Built into terminal-kit inputField
description: () => _('Exit command line mode'),
isDocOnly: true,
};
shortcuts['ENTER'] = {
description: () => _('Edit the selected note'),
action: () => {
const w = this.widget('mainWindow').focusedWidget;
if (w.name === 'folderList') {
this.widget('noteList').focus();
} else if (w.name === 'noteList' || w.name === 'noteText') {
this.processCommand('edit $n');
}
},
}
shortcuts['CTRL_C'] = {
description: () => _('Cancel the current command.'),
friendlyName: 'Ctrl+C',
isDocOnly: true,
}
shortcuts['CTRL_D'] = {
description: () => _('Exit the application.'),
friendlyName: 'Ctrl+D',
isDocOnly: true,
}
shortcuts['DELETE'] = {
description: () => _('Delete the currently selected note or notebook.'),
action: async () => {
if (this.widget('folderList').hasFocus) {
const item = this.widget('folderList').selectedJoplinItem;
if (!item) return;
if (item.type_ === BaseModel.TYPE_FOLDER) {
await this.processCommand('rmbook ' + item.id);
} else if (item.type_ === BaseModel.TYPE_TAG) {
this.stdout(_('To delete a tag, untag the associated notes.'));
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
this.store().dispatch({
type: 'SEARCH_DELETE',
id: item.id,
});
}
} else if (this.widget('noteList').hasFocus) {
await this.processCommand('rmnote $n');
} else {
this.stdout(_('Please select the note or notebook to be deleted first.'));
}
if (item.command in this.tkWidgetKeys_) {
item.type = 'tkwidgets';
}
};
shortcuts['BACKSPACE'] = {
alias: 'DELETE',
};
item.canRunAlongOtherCommands = item.type === 'function' && ['toggle_metadata', 'toggle_console'].indexOf(item.command) >= 0;
shortcuts[' '] = {
friendlyName: 'SPACE',
description: () => _('Set a to-do as completed / not completed'),
action: 'todo toggle $n',
output.push(item);
}
shortcuts['tc'] = {
description: () => _('[t]oggle [c]onsole between maximized/minimized/hidden/visible.'),
action: () => {
if (!this.consoleIsShown()) {
this.showConsole();
this.minimizeConsole();
} else {
if (this.consoleIsMaximized()) {
this.hideConsole();
} else {
this.maximizeConsole();
}
}
},
canRunAlongOtherCommands: true,
}
shortcuts['/'] = {
description: () => _('Search'),
action: { type: 'prompt', initialText: 'search ""', cursorPosition: -2 },
};
shortcuts['tm'] = {
description: () => _('[t]oggle note [m]etadata.'),
action: () => {
this.toggleNoteMetadata();
},
canRunAlongOtherCommands: true,
}
shortcuts['mn'] = {
description: () => _('[M]ake a new [n]ote'),
action: { type: 'prompt', initialText: 'mknote ""', cursorPosition: -2 },
}
shortcuts['mt'] = {
description: () => _('[M]ake a new [t]odo'),
action: { type: 'prompt', initialText: 'mktodo ""', cursorPosition: -2 },
}
shortcuts['mb'] = {
description: () => _('[M]ake a new note[b]ook'),
action: { type: 'prompt', initialText: 'mkbook ""', cursorPosition: -2 },
}
shortcuts['yn'] = {
description: () => _('Copy ([Y]ank) the [n]ote to a notebook.'),
action: { type: 'prompt', initialText: 'cp $n ""', cursorPosition: -2 },
}
shortcuts['dn'] = {
description: () => _('Move the note to a notebook.'),
action: { type: 'prompt', initialText: 'mv $n ""', cursorPosition: -2 },
}
return shortcuts;
return output;
}
toggleConsole() {
@ -492,8 +384,16 @@ class AppGui {
return this.logger_;
}
shortcuts() {
return this.shortcuts_;
keymap() {
return this.keymap_;
}
keymapItemByKey(key) {
for (let i = 0; i < this.keymap_.length; i++) {
const item = this.keymap_[i];
if (item.keys.indexOf(key) >= 0) return item;
}
return null;
}
term() {
@ -524,18 +424,78 @@ class AppGui {
}
}
async processCommand(cmd) {
async processFunctionCommand(cmd) {
if (cmd === 'activate') {
const w = this.widget('mainWindow').focusedWidget;
if (w.name === 'folderList') {
this.widget('noteList').focus();
} else if (w.name === 'noteList' || w.name === 'noteText') {
this.processPromptCommand('edit $n');
}
} else if (cmd === 'delete') {
if (this.widget('folderList').hasFocus) {
const item = this.widget('folderList').selectedJoplinItem;
if (!item) return;
if (item.type_ === BaseModel.TYPE_FOLDER) {
await this.processPromptCommand('rmbook ' + item.id);
} else if (item.type_ === BaseModel.TYPE_TAG) {
this.stdout(_('To delete a tag, untag the associated notes.'));
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
this.store().dispatch({
type: 'SEARCH_DELETE',
id: item.id,
});
}
} else if (this.widget('noteList').hasFocus) {
await this.processPromptCommand('rmnote $n');
} else {
this.stdout(_('Please select the note or notebook to be deleted first.'));
}
} else if (cmd === 'toggle_console') {
if (!this.consoleIsShown()) {
this.showConsole();
this.minimizeConsole();
} else {
if (this.consoleIsMaximized()) {
this.hideConsole();
} else {
this.maximizeConsole();
}
}
} else if (cmd === 'toggle_metadata') {
this.toggleNoteMetadata();
} else if (cmd === 'enter_command_line_mode') {
const cmd = await this.widget('statusBar').prompt();
if (!cmd) return;
this.addCommandToConsole(cmd);
await this.processPromptCommand(cmd);
} else {
throw new Error('Unknown command: ' + cmd);
}
}
async processPromptCommand(cmd) {
if (!cmd) return;
cmd = cmd.trim();
if (!cmd.length) return;
this.logger().info('Got command: ' + cmd);
if (cmd === 'q' || cmd === 'wq' || cmd === 'qa') { // Vim bonus
await this.app().exit();
return;
}
try {
let note = this.widget('noteList').currentItem;
let folder = this.widget('folderList').currentItem;
@ -786,35 +746,34 @@ class AppGui {
// -------------------------------------------------------------------------
const shortcutKey = this.currentShortcutKeys_.join('');
let cmd = shortcutKey in this.shortcuts_ ? this.shortcuts_[shortcutKey] : null;
let keymapItem = this.keymapItemByKey(shortcutKey);
// If this command is an alias to another command, resolve to the actual command
if (cmd && cmd.alias) cmd = this.shortcuts_[cmd.alias];
let processShortcutKeys = !this.app().currentCommand() && cmd;
if (cmd && cmd.canRunAlongOtherCommands) processShortcutKeys = true;
let processShortcutKeys = !this.app().currentCommand() && keymapItem;
if (keymapItem && keymapItem.canRunAlongOtherCommands) processShortcutKeys = true;
if (statusBar.promptActive) processShortcutKeys = false;
if (cmd && cmd.isDocOnly) processShortcutKeys = false;
if (processShortcutKeys) {
this.logger().info('Shortcut:', shortcutKey, cmd.description());
this.logger().info('Shortcut:', shortcutKey, keymapItem);
this.currentShortcutKeys_ = [];
if (typeof cmd.action === 'function') {
await cmd.action();
} else if (typeof cmd.action === 'object') {
if (cmd.action.type === 'prompt') {
let promptOptions = {};
if ('cursorPosition' in cmd.action) promptOptions.cursorPosition = cmd.action.cursorPosition;
const commandString = await statusBar.prompt(cmd.action.initialText ? cmd.action.initialText : '', null, promptOptions);
this.addCommandToConsole(commandString);
await this.processCommand(commandString);
} else {
throw new Error('Unknown command: ' + JSON.stringify(cmd.action));
}
} else { // String
this.stdout(cmd.action);
await this.processCommand(cmd.action);
if (keymapItem.type === 'function') {
this.processFunctionCommand(keymapItem.command);
} else if (keymapItem.type === 'prompt') {
let promptOptions = {};
if ('cursorPosition' in keymapItem) promptOptions.cursorPosition = keymapItem.cursorPosition;
const commandString = await statusBar.prompt(keymapItem.command ? keymapItem.command : '', null, promptOptions);
this.addCommandToConsole(commandString);
await this.processPromptCommand(commandString);
} else if (keymapItem.type === 'exec') {
this.stdout(keymapItem.command);
await this.processPromptCommand(keymapItem.command);
} else if (keymapItem.type === 'tkwidgets') {
this.widget('root').handleKey(this.tkWidgetKeys_[keymapItem.command]);
} else {
throw new Error('Unknown command type: ' + JSON.stringify(keymapItem));
}
}

View File

@ -312,6 +312,63 @@ class Application extends BaseApplication {
return this.activeCommand_;
}
async loadKeymaps() {
const defaultKeyMap = [
{ "keys": [":"], "type": "function", "command": "enter_command_line_mode" },
{ "keys": ["TAB"], "type": "function", "command": "focus_next" },
{ "keys": ["SHIFT_TAB"], "type": "function", "command": "focus_previous" },
{ "keys": ["UP"], "type": "function", "command": "move_up" },
{ "keys": ["DOWN"], "type": "function", "command": "move_down" },
{ "keys": ["PAGE_UP"], "type": "function", "command": "page_up" },
{ "keys": ["PAGE_DOWN"], "type": "function", "command": "page_down" },
{ "keys": ["ENTER"], "type": "function", "command": "activate" },
{ "keys": ["DELETE", "BACKSPACE"], "type": "function", "command": "delete" },
{ "keys": [" "], "command": "todo toggle $n" },
{ "keys": ["tc"], "type": "function", "command": "toggle_console" },
{ "keys": ["tm"], "type": "function", "command": "toggle_metadata" },
{ "keys": ["/"], "type": "prompt", "command": "search \"\"", "cursorPosition": -2 },
{ "keys": ["mn"], "type": "prompt", "command": "mknote \"\"", "cursorPosition": -2 },
{ "keys": ["mt"], "type": "prompt", "command": "mktodo \"\"", "cursorPosition": -2 },
{ "keys": ["mb"], "type": "prompt", "command": "mkbook \"\"", "cursorPosition": -2 },
{ "keys": ["yn"], "type": "prompt", "command": "cp $n \"\"", "cursorPosition": -2 },
{ "keys": ["dn"], "type": "prompt", "command": "mv $n \"\"", "cursorPosition": -2 }
];
// Filter the keymap item by command so that items in keymap.json can override
// the default ones.
const itemsByCommand = {};
for (let i = 0; i < defaultKeyMap.length; i++) {
itemsByCommand[defaultKeyMap[i].command] = defaultKeyMap[i]
}
const filePath = Setting.value('profileDir') + '/keymap.json';
if (await fs.pathExists(filePath)) {
try {
let configString = await fs.readFile(filePath, 'utf-8');
configString = configString.replace(/^\s*\/\/.*/, ''); // Strip off comments
const keymap = JSON.parse(configString);
for (let keymapIndex = 0; keymapIndex < keymap.length; keymapIndex++) {
const item = keymap[keymapIndex];
itemsByCommand[item.command] = item;
}
} catch (error) {
let msg = error.message ? error.message : '';
msg = 'Could not load keymap ' + filePath + '\n' + msg;
error.message = msg;
throw error;
}
}
const output = [];
for (let n in itemsByCommand) {
if (!itemsByCommand.hasOwnProperty(n)) continue;
output.push(itemsByCommand[n]);
}
return output;
}
async start(argv) {
argv = await super.start(argv);
@ -338,8 +395,10 @@ class Application extends BaseApplication {
} else { // Otherwise open the GUI
this.initRedux();
const keymap = await this.loadKeymaps();
const AppGui = require('./app-gui.js');
this.gui_ = new AppGui(this, this.store());
this.gui_ = new AppGui(this, this.store(), keymap);
this.gui_.setLogger(this.logger_);
await this.gui_.start();

View File

@ -36,21 +36,19 @@ class Command extends BaseCommand {
async action(args) {
const stdoutWidth = app().commandStdoutMaxWidth();
if (args.command === 'shortcuts') {
if (args.command === 'shortcuts' || args.command === 'keymap') {
if (app().gui().isDummy()) {
throw new Error(_('Shortcuts are not available in CLI mode.'));
}
const shortcuts = app().gui().shortcuts();
const keymap = app().gui().keymap();
let rows = [];
for (let n in shortcuts) {
if (!shortcuts.hasOwnProperty(n)) continue;
const shortcut = shortcuts[n];
if (!shortcut.description) continue;
n = shortcut.friendlyName ? shortcut.friendlyName : n;
rows.push([n, shortcut.description()]);
for (let i = 0; i < keymap.length; i++) {
const item = keymap[i];
const keys = item.keys.map((k) => k === ' ' ? '(SPACE)' : k);
rows.push([keys.join(', '), item.command]);
}
cliUtils.printArray(this.stdout.bind(this), rows);
@ -78,7 +76,7 @@ class Command extends BaseCommand {
this.stdout(_('To maximise/minimise the console, press "TC".'));
this.stdout(_('To enter command line mode, press ":"'));
this.stdout(_('To exit command line mode, press ESCAPE'));
this.stdout(_('For the complete list of available keyboard shortcuts, type `help shortcuts`'));
this.stdout(_('For the complete list of available keyboard shortcuts, type `help keymap`'));
}
app().gui().showConsole();

View File

@ -2057,9 +2057,9 @@
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
},
"tkwidgets": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/tkwidgets/-/tkwidgets-0.5.21.tgz",
"integrity": "sha512-gJfpYq3UM6AZ23ZM+D9BZ1PhsJLLHgjCOf487/lS9pO0uDdnkMcVXkkKEfRl00EyjPnGc88QZhEkVOvrtKsuPA==",
"version": "0.5.25",
"resolved": "https://registry.npmjs.org/tkwidgets/-/tkwidgets-0.5.25.tgz",
"integrity": "sha512-f+12QbxNCLg9Jou5JoPJxATGLmzpDAQeM7QRTXvuqdEB/QvPD9+UlPUL7eYJP1QJv2zzT6EIWWbdpDkXPEtzCQ==",
"requires": {
"chalk": "2.3.0",
"emphasize": "1.5.0",

View File

@ -58,7 +58,7 @@
"string-to-stream": "^1.1.0",
"strip-ansi": "^4.0.0",
"tcp-port-used": "^0.1.2",
"tkwidgets": "^0.5.21",
"tkwidgets": "^0.5.25",
"url-parse": "^1.2.0",
"uuid": "^3.0.1",
"word-wrap": "^1.2.3",

View File

@ -217,13 +217,12 @@ Current translations:
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/br.png) | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po) | | 73%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/ru.png) | Русский | [ru_RU](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po) | Artyom Karlov <artyom.karlov@gmail.com> | 92%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | RCJacH <RCJacH@outlook.com> | 75%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/jp.png) | 日本語 | [ja_JP](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po) | | 73%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/masWter/png/16/country-4x3/jp.png) | 日本語 | [ja_JP](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po) | | 73%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# Known bugs
- Non-alphabetical characters such as Chinese or Arabic might create glitches in the terminal on Windows. This is a limitation of the current Windows console.
- Auto-update is not working in the Linux desktop application.
- While the mobile can sync and load tags, it is not currently possible to create new ones. The desktop and terminal apps can create, delete and edit tags.
# License

View File

@ -96,7 +96,7 @@ The complete usage information is available from command-line mode, by typing on
Command | Description
--------|-------------------
`help` | General help information
`help shortcuts` | Lists the available shortcuts
`help keymap` | Lists the available shortcuts
`help [command]` | Displays information about a particular command
If the help is not fully visible, press `Tab` multiple times till the console is in focus and use the arrow keys or page up/down to scroll the text.
@ -177,25 +177,78 @@ Give a new title to the note:
# Available shortcuts
There are two types of shortcuts: those that manipulate the user interface directly, such as `TAB` to move from one pane to another, and those that are simply shortcuts to actual commands. In a way similar to Vim, these shortcuts are generally a verb followed by an object. For example, typing `mn` ([m]ake [n]ote), is used to create a new note: it will switch the interface to command line mode and pre-fill it with `mknote ""` from where the title of the note can be entered. See below for the full list of shortcuts:
There are two types of shortcuts: those that manipulate the user interface directly, such as `TAB` to move from one pane to another, and those that are simply shortcuts to actual commands. In a way similar to Vim, these shortcuts are generally a verb followed by an object. For example, typing `mn` ([m]ake [n]ote), is used to create a new note: it will switch the interface to command line mode and pre-fill it with `mknote ""` from where the title of the note can be entered. See below for the full list of default shortcuts:
Tab Give focus to next pane
Shift+Tab Give focus to previous pane
: Enter command line mode
ESC Exit command line mode
ENTER Edit the selected note
Ctrl+C Cancel the current command.
Ctrl+D Exit the application.
DELETE Delete the currently selected note or notebook.
SPACE Set a to-do as completed / not completed
tc [t]oggle [c]onsole between maximized/minimized/hidden/visible.
/ Search
tm [t]oggle note [m]etadata.
mn [M]ake a new [n]ote
mt [M]ake a new [t]odo
mb [M]ake a new note[b]ook
yn Copy ([Y]ank) the [n]ote to a notebook.
dn Move the note to a notebook.
: enter_command_line_mode
TAB focus_next
SHIFT_TAB focus_previous
UP move_up
DOWN move_down
PAGE_UP page_up
PAGE_DOWN page_down
ENTER activate
DELETE, BACKSPACE delete
(SPACE) todo toggle $n
tc toggle_console
tm toggle_metadata
/ search ""
mn mknote ""
mt mktodo ""
mb mkbook ""
yn cp $n ""
dn mv $n ""
Shortcut can be configured by adding a file `keymap.json` to the profile directory. The content of this file is a JSON array with each entry defining a command and the keys associated with it.
Each entry can have the following properties:
Name | Description
-----|------------
`keys` | The array of keys that will trigger the action. Special keys such as page up, down arrow, etc. needs to be specified UPPERCASE. See the [list of available special keys](https://github.com/cronvel/terminal-kit/blob/3114206a9556f518cc63abbcb3d188fe1995100d/lib/termconfig/xterm.js#L531). For example, `['DELETE', 'BACKSPACE']` means the command will run if the user pressed either the delete or backspace key. Key combinations can also be provided - in that case specify them lowercase. For example "tc" means that the command will be executed when the user pressed "t" then "c". Special keys can also be used in this fashion - simply write them one after the other. For instance, `CTRL_WCTRL_W` means the action would be executed if the user pressed "ctrl-w ctrl-w".
`type` | The command type. It can have the value "exec", "function" or "prompt". **exec**: Simply execute the provided command. For example `edit $n` would edit the selected note. **function**: Run a special commands (see below for the list of function. **prompt**: A bit similar to "exec", except that the command is not going to be executed immediately - this allows the user to provide additional data. For example `mknote ""` would fill the command line with this command and allow the user to set the title. A prompt command can also take a `cursorPosition` parameter (see below)
`command` | The command that needs to be executed
`cusorPosition` | An integer. For prompt commands, tells where the cursor (caret) should start at. This is convenient for example to position the cursor between quotes. Use a negative value to set a position starting from the end.
This is the list of available functions:
Name | Description
-----|------------
enter_command_line_mode | Enter command line mode
focus_next | Focus next pane (or widget)
focus_previous | Focus previous pane (or widget)
move_up | Move up (in a list for example)
move_down | Move down (in a list for example)
page_up | Page up
page_down | Page down
activate | Activates the selected item. If the item is a note for example it will be open in the editor
delete | Deletes the selected item
toggle_console | Toggle the console
toggle_metadata | Toggle note metadata
As an example, this is the default keymap:
```json
[
{ "keys": [":"], "type": "function", "command": "enter_command_line_mode" },
{ "keys": ["TAB"], "type": "function", "command": "focus_next" },
{ "keys": ["SHIFT_TAB"], "type": "function", "command": "focus_previous" },
{ "keys": ["UP"], "type": "function", "command": "move_up" },
{ "keys": ["DOWN"], "type": "function", "command": "move_down" },
{ "keys": ["PAGE_UP"], "type": "function", "command": "page_up" },
{ "keys": ["PAGE_DOWN"], "type": "function", "command": "page_down" },
{ "keys": ["ENTER"], "type": "function", "command": "activate" },
{ "keys": ["DELETE", "BACKSPACE"], "type": "function", "command": "delete" },
{ "keys": [" "], "command": "todo toggle $n" },
{ "keys": ["tc"], "type": "function", "command": "toggle_console" },
{ "keys": ["tm"], "type": "function", "command": "toggle_metadata" },
{ "keys": ["/"], "type": "prompt", "command": "search \"\"", "cursorPosition": -2 },
{ "keys": ["mn"], "type": "prompt", "command": "mknote \"\"", "cursorPosition": -2 },
{ "keys": ["mt"], "type": "prompt", "command": "mktodo \"\"", "cursorPosition": -2 },
{ "keys": ["mb"], "type": "prompt", "command": "mkbook \"\"", "cursorPosition": -2 },
{ "keys": ["yn"], "type": "prompt", "command": "cp $n \"\"", "cursorPosition": -2 },
{ "keys": ["dn"], "type": "prompt", "command": "mv $n \"\"", "cursorPosition": -2 }
]
```
# Available commands

View File

@ -392,14 +392,14 @@ $$
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/hr.png" alt=""></td>
<td>Croatian</td>
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po">hr_HR</a></td>
<td>Hrvoje Mandić <a href="&#109;&#97;&#105;&#x6c;&#116;&#x6f;&#x3a;&#x74;&#114;&#98;&#x75;&#104;&#111;&#109;&#x40;&#x6e;&#x65;&#x74;&#46;&#104;&#x72;">&#x74;&#114;&#98;&#x75;&#104;&#111;&#109;&#x40;&#x6e;&#x65;&#x74;&#46;&#104;&#x72;</a></td>
<td>Hrvoje Mandić <a href="&#x6d;&#x61;&#x69;&#108;&#x74;&#111;&#58;&#116;&#114;&#98;&#x75;&#x68;&#111;&#109;&#x40;&#110;&#101;&#x74;&#x2e;&#104;&#114;">&#116;&#114;&#98;&#x75;&#x68;&#111;&#109;&#x40;&#110;&#101;&#x74;&#x2e;&#104;&#114;</a></td>
<td>71%</td>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/de.png" alt=""></td>
<td>Deutsch</td>
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po">de_DE</a></td>
<td>Tobias Strobel <a href="&#x6d;&#97;&#x69;&#x6c;&#x74;&#111;&#58;&#x67;&#x69;&#116;&#x40;&#115;&#116;&#x72;&#x6f;&#x62;&#x65;&#x6c;&#116;&#111;&#98;&#x69;&#x61;&#115;&#x2e;&#x64;&#x65;">&#x67;&#x69;&#116;&#x40;&#115;&#116;&#x72;&#x6f;&#x62;&#x65;&#x6c;&#116;&#111;&#98;&#x69;&#x61;&#115;&#x2e;&#x64;&#x65;</a></td>
<td>Tobias Strobel <a href="&#109;&#97;&#105;&#108;&#116;&#111;&#x3a;&#103;&#105;&#116;&#64;&#x73;&#x74;&#x72;&#111;&#x62;&#101;&#x6c;&#116;&#111;&#x62;&#105;&#97;&#115;&#46;&#x64;&#101;">&#103;&#105;&#116;&#64;&#x73;&#x74;&#x72;&#111;&#x62;&#101;&#x6c;&#116;&#111;&#x62;&#105;&#97;&#115;&#46;&#x64;&#101;</a></td>
<td>90%</td>
</tr>
<tr>
@ -413,7 +413,7 @@ $$
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/es.png" alt=""></td>
<td>Español</td>
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po">es_ES</a></td>
<td>Fernando Martín <a href="&#109;&#97;&#105;&#108;&#x74;&#x6f;&#x3a;&#x66;&#x40;&#109;&#x72;&#x74;&#x6e;&#x2e;&#101;&#115;">&#x66;&#x40;&#109;&#x72;&#x74;&#x6e;&#x2e;&#101;&#115;</a></td>
<td>Fernando Martín <a href="&#109;&#97;&#x69;&#x6c;&#x74;&#x6f;&#x3a;&#102;&#x40;&#109;&#x72;&#116;&#110;&#x2e;&#101;&#x73;">&#102;&#x40;&#109;&#x72;&#116;&#110;&#x2e;&#101;&#x73;</a></td>
<td>98%</td>
</tr>
<tr>
@ -455,18 +455,18 @@ $$
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/ru.png" alt=""></td>
<td>Русский</td>
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po">ru_RU</a></td>
<td>Artyom Karlov <a href="&#x6d;&#x61;&#105;&#108;&#x74;&#111;&#x3a;&#97;&#114;&#116;&#121;&#111;&#109;&#x2e;&#x6b;&#x61;&#114;&#108;&#111;&#x76;&#x40;&#103;&#x6d;&#x61;&#x69;&#108;&#46;&#99;&#x6f;&#109;">&#97;&#114;&#116;&#121;&#111;&#109;&#x2e;&#x6b;&#x61;&#114;&#108;&#111;&#x76;&#x40;&#103;&#x6d;&#x61;&#x69;&#108;&#46;&#99;&#x6f;&#109;</a></td>
<td>Artyom Karlov <a href="&#x6d;&#x61;&#x69;&#108;&#x74;&#x6f;&#58;&#x61;&#114;&#x74;&#121;&#111;&#x6d;&#x2e;&#x6b;&#97;&#x72;&#x6c;&#111;&#118;&#64;&#103;&#x6d;&#97;&#105;&#x6c;&#46;&#x63;&#x6f;&#109;">&#x61;&#114;&#x74;&#121;&#111;&#x6d;&#x2e;&#x6b;&#97;&#x72;&#x6c;&#111;&#118;&#64;&#103;&#x6d;&#97;&#105;&#x6c;&#46;&#x63;&#x6f;&#109;</a></td>
<td>92%</td>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cn.png" alt=""></td>
<td>中文 (简体)</td>
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po">zh_CN</a></td>
<td>RCJacH <a href="&#109;&#97;&#x69;&#108;&#x74;&#x6f;&#x3a;&#82;&#x43;&#x4a;&#x61;&#x63;&#x48;&#x40;&#x6f;&#117;&#x74;&#x6c;&#111;&#x6f;&#x6b;&#x2e;&#99;&#111;&#x6d;">&#82;&#x43;&#x4a;&#x61;&#x63;&#x48;&#x40;&#x6f;&#117;&#x74;&#x6c;&#111;&#x6f;&#x6b;&#x2e;&#99;&#111;&#x6d;</a></td>
<td>RCJacH <a href="&#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x52;&#67;&#74;&#x61;&#x63;&#72;&#x40;&#x6f;&#117;&#x74;&#108;&#111;&#x6f;&#x6b;&#46;&#x63;&#x6f;&#x6d;">&#x52;&#67;&#74;&#x61;&#x63;&#72;&#x40;&#x6f;&#117;&#x74;&#108;&#111;&#x6f;&#x6b;&#46;&#x63;&#x6f;&#x6d;</a></td>
<td>75%</td>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/jp.png" alt=""></td>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/masWter/png/16/country-4x3/jp.png" alt=""></td>
<td>日本語</td>
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po">ja_JP</a></td>
<td></td>
@ -478,7 +478,6 @@ $$
<h1 id="known-bugs">Known bugs</h1>
<ul>
<li>Non-alphabetical characters such as Chinese or Arabic might create glitches in the terminal on Windows. This is a limitation of the current Windows console.</li>
<li>Auto-update is not working in the Linux desktop application.</li>
<li>While the mobile can sync and load tags, it is not currently possible to create new ones. The desktop and terminal apps can create, delete and edit tags.</li>
</ul>
<h1 id="license">License</h1>

View File

@ -278,7 +278,7 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
<td>General help information</td>
</tr>
<tr>
<td><code>help shortcuts</code></td>
<td><code>help keymap</code></td>
<td>Lists the available shortcuts</td>
</tr>
<tr>
@ -332,25 +332,131 @@ fe889 07/12/2017 17:57 My note
</code></pre><p>Give a new title to the note:</p>
<pre><code>$ joplin set fe889 title &quot;New title&quot;
</code></pre><h1 id="available-shortcuts">Available shortcuts</h1>
<p>There are two types of shortcuts: those that manipulate the user interface directly, such as <code>TAB</code> to move from one pane to another, and those that are simply shortcuts to actual commands. In a way similar to Vim, these shortcuts are generally a verb followed by an object. For example, typing <code>mn</code> ([m]ake [n]ote), is used to create a new note: it will switch the interface to command line mode and pre-fill it with <code>mknote &quot;&quot;</code> from where the title of the note can be entered. See below for the full list of shortcuts:</p>
<pre><code>Tab Give focus to next pane
Shift+Tab Give focus to previous pane
: Enter command line mode
ESC Exit command line mode
ENTER Edit the selected note
Ctrl+C Cancel the current command.
Ctrl+D Exit the application.
DELETE Delete the currently selected note or notebook.
SPACE Set a to-do as completed / not completed
tc [t]oggle [c]onsole between maximized/minimized/hidden/visible.
/ Search
tm [t]oggle note [m]etadata.
mn [M]ake a new [n]ote
mt [M]ake a new [t]odo
mb [M]ake a new note[b]ook
yn Copy ([Y]ank) the [n]ote to a notebook.
dn Move the note to a notebook.
</code></pre><h1 id="available-commands">Available commands</h1>
<p>There are two types of shortcuts: those that manipulate the user interface directly, such as <code>TAB</code> to move from one pane to another, and those that are simply shortcuts to actual commands. In a way similar to Vim, these shortcuts are generally a verb followed by an object. For example, typing <code>mn</code> ([m]ake [n]ote), is used to create a new note: it will switch the interface to command line mode and pre-fill it with <code>mknote &quot;&quot;</code> from where the title of the note can be entered. See below for the full list of default shortcuts:</p>
<pre><code>: enter_command_line_mode
TAB focus_next
SHIFT_TAB focus_previous
UP move_up
DOWN move_down
PAGE_UP page_up
PAGE_DOWN page_down
ENTER activate
DELETE, BACKSPACE delete
(SPACE) todo toggle $n
tc toggle_console
tm toggle_metadata
/ search &quot;&quot;
mn mknote &quot;&quot;
mt mktodo &quot;&quot;
mb mkbook &quot;&quot;
yn cp $n &quot;&quot;
dn mv $n &quot;&quot;
</code></pre><p>Shortcut can be configured by adding a file <code>keymap.json</code> to the profile directory. The content of this file is a JSON array with each entry defining a command and the keys associated with it.</p>
<p>Each entry can have the following properties:</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>keys</code></td>
<td>The array of keys that will trigger the action. Special keys such as page up, down arrow, etc. needs to be specified UPPERCASE. See the <a href="https://github.com/cronvel/terminal-kit/blob/3114206a9556f518cc63abbcb3d188fe1995100d/lib/termconfig/xterm.js#L531">list of available special keys</a>. For example, <code>[&#39;DELETE&#39;, &#39;BACKSPACE&#39;]</code> means the command will run if the user pressed either the delete or backspace key. Key combinations can also be provided - in that case specify them lowercase. For example &quot;tc&quot; means that the command will be executed when the user pressed &quot;t&quot; then &quot;c&quot;. Special keys can also be used in this fashion - simply write them one after the other. For instance, <code>CTRL_WCTRL_W</code> means the action would be executed if the user pressed &quot;ctrl-w ctrl-w&quot;.</td>
</tr>
<tr>
<td><code>type</code></td>
<td>The command type. It can have the value &quot;exec&quot;, &quot;function&quot; or &quot;prompt&quot;. <strong>exec</strong>: Simply execute the provided command. For example <code>edit $n</code> would edit the selected note. <strong>function</strong>: Run a special commands (see below for the list of function. <strong>prompt</strong>: A bit similar to &quot;exec&quot;, except that the command is not going to be executed immediately - this allows the user to provide additional data. For example <code>mknote &quot;&quot;</code> would fill the command line with this command and allow the user to set the title. A prompt command can also take a <code>cursorPosition</code> parameter (see below)</td>
</tr>
<tr>
<td><code>command</code></td>
<td>The command that needs to be executed</td>
</tr>
<tr>
<td><code>cusorPosition</code></td>
<td>An integer. For prompt commands, tells where the cursor (caret) should start at. This is convenient for example to position the cursor between quotes. Use a negative value to set a position starting from the end.</td>
</tr>
</tbody>
</table>
<p>This is the list of available functions:</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>enter_command_line_mode</td>
<td>Enter command line mode</td>
</tr>
<tr>
<td>focus_next</td>
<td>Focus next pane (or widget)</td>
</tr>
<tr>
<td>focus_previous</td>
<td>Focus previous pane (or widget)</td>
</tr>
<tr>
<td>move_up</td>
<td>Move up (in a list for example)</td>
</tr>
<tr>
<td>move_down</td>
<td>Move down (in a list for example)</td>
</tr>
<tr>
<td>page_up</td>
<td>Page up</td>
</tr>
<tr>
<td>page_down</td>
<td>Page down</td>
</tr>
<tr>
<td>activate</td>
<td>Activates the selected item. If the item is a note for example it will be open in the editor</td>
</tr>
<tr>
<td>delete</td>
<td>Deletes the selected item</td>
</tr>
<tr>
<td>toggle_console</td>
<td>Toggle the console</td>
</tr>
<tr>
<td>toggle_metadata</td>
<td>Toggle note metadata</td>
</tr>
</tbody>
</table>
<p>As an example, this is the default keymap:</p>
<pre><code class="lang-json">[
{ &quot;keys&quot;: [&quot;:&quot;], &quot;type&quot;: &quot;function&quot;, &quot;command&quot;: &quot;enter_command_line_mode&quot; },
{ &quot;keys&quot;: [&quot;TAB&quot;], &quot;type&quot;: &quot;function&quot;, &quot;command&quot;: &quot;focus_next&quot; },
{ &quot;keys&quot;: [&quot;SHIFT_TAB&quot;], &quot;type&quot;: &quot;function&quot;, &quot;command&quot;: &quot;focus_previous&quot; },
{ &quot;keys&quot;: [&quot;UP&quot;], &quot;type&quot;: &quot;function&quot;, &quot;command&quot;: &quot;move_up&quot; },
{ &quot;keys&quot;: [&quot;DOWN&quot;], &quot;type&quot;: &quot;function&quot;, &quot;command&quot;: &quot;move_down&quot; },
{ &quot;keys&quot;: [&quot;PAGE_UP&quot;], &quot;type&quot;: &quot;function&quot;, &quot;command&quot;: &quot;page_up&quot; },
{ &quot;keys&quot;: [&quot;PAGE_DOWN&quot;], &quot;type&quot;: &quot;function&quot;, &quot;command&quot;: &quot;page_down&quot; },
{ &quot;keys&quot;: [&quot;ENTER&quot;], &quot;type&quot;: &quot;function&quot;, &quot;command&quot;: &quot;activate&quot; },
{ &quot;keys&quot;: [&quot;DELETE&quot;, &quot;BACKSPACE&quot;], &quot;type&quot;: &quot;function&quot;, &quot;command&quot;: &quot;delete&quot; },
{ &quot;keys&quot;: [&quot; &quot;], &quot;command&quot;: &quot;todo toggle $n&quot; },
{ &quot;keys&quot;: [&quot;tc&quot;], &quot;type&quot;: &quot;function&quot;, &quot;command&quot;: &quot;toggle_console&quot; },
{ &quot;keys&quot;: [&quot;tm&quot;], &quot;type&quot;: &quot;function&quot;, &quot;command&quot;: &quot;toggle_metadata&quot; },
{ &quot;keys&quot;: [&quot;/&quot;], &quot;type&quot;: &quot;prompt&quot;, &quot;command&quot;: &quot;search \&quot;\&quot;&quot;, &quot;cursorPosition&quot;: -2 },
{ &quot;keys&quot;: [&quot;mn&quot;], &quot;type&quot;: &quot;prompt&quot;, &quot;command&quot;: &quot;mknote \&quot;\&quot;&quot;, &quot;cursorPosition&quot;: -2 },
{ &quot;keys&quot;: [&quot;mt&quot;], &quot;type&quot;: &quot;prompt&quot;, &quot;command&quot;: &quot;mktodo \&quot;\&quot;&quot;, &quot;cursorPosition&quot;: -2 },
{ &quot;keys&quot;: [&quot;mb&quot;], &quot;type&quot;: &quot;prompt&quot;, &quot;command&quot;: &quot;mkbook \&quot;\&quot;&quot;, &quot;cursorPosition&quot;: -2 },
{ &quot;keys&quot;: [&quot;yn&quot;], &quot;type&quot;: &quot;prompt&quot;, &quot;command&quot;: &quot;cp $n \&quot;\&quot;&quot;, &quot;cursorPosition&quot;: -2 },
{ &quot;keys&quot;: [&quot;dn&quot;], &quot;type&quot;: &quot;prompt&quot;, &quot;command&quot;: &quot;mv $n \&quot;\&quot;&quot;, &quot;cursorPosition&quot;: -2 }
]
</code></pre>
<h1 id="available-commands">Available commands</h1>
<p>The following commands are available in <a href="#command-line-mode">command-line mode</a>:</p>
<pre><code>attach &lt;note&gt; &lt;file&gt;