mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-03 08:35:29 +02:00
241 lines
8.3 KiB
TypeScript
241 lines
8.3 KiB
TypeScript
|
const fs = require('fs-extra');
|
||
|
const BaseService = require('lib/services/BaseService');
|
||
|
const { shim } = require('lib/shim.js');
|
||
|
|
||
|
const keysRegExp = /^([0-9A-Z)!@#$%^&*(:+<_>?~{|}";=,\-./`[\\\]']|F1*[1-9]|F10|F2[0-4]|Plus|Space|Tab|Backspace|Delete|Insert|Return|Enter|Up|Down|Left|Right|Home|End|PageUp|PageDown|Escape|Esc|VolumeUp|VolumeDown|VolumeMute|MediaNextTrack|MediaPreviousTrack|MediaStop|MediaPlayPause|PrintScreen)$/;
|
||
|
const modifiersRegExp = {
|
||
|
darwin: /^(Ctrl|Option|Shift|Cmd)$/,
|
||
|
default: /^(Ctrl|Alt|AltGr|Shift|Super)$/,
|
||
|
};
|
||
|
|
||
|
const defaultKeymap = {
|
||
|
darwin: [
|
||
|
{ accelerator: 'Cmd+N', command: 'newNote' },
|
||
|
{ accelerator: 'Cmd+T', command: 'newTodo' },
|
||
|
{ accelerator: 'Cmd+S', command: 'synchronize' },
|
||
|
{ accelerator: 'Cmd+P', command: 'print' },
|
||
|
{ accelerator: 'Cmd+H', command: 'hideApp' },
|
||
|
{ accelerator: 'Cmd+Q', command: 'quit' },
|
||
|
{ accelerator: 'Cmd+,', command: 'config' },
|
||
|
{ accelerator: 'Cmd+W', command: 'closeWindow' },
|
||
|
{ accelerator: 'Option+Cmd+I', command: 'insertTemplate' },
|
||
|
{ accelerator: 'Cmd+C', command: 'textCopy' },
|
||
|
{ accelerator: 'Cmd+X', command: 'textCut' },
|
||
|
{ accelerator: 'Cmd+V', command: 'textPaste' },
|
||
|
{ accelerator: 'Cmd+A', command: 'textSelectAll' },
|
||
|
{ accelerator: 'Cmd+B', command: 'textBold' },
|
||
|
{ accelerator: 'Cmd+I', command: 'textItalic' },
|
||
|
{ accelerator: 'Cmd+K', command: 'textLink' },
|
||
|
{ accelerator: 'Cmd+`', command: 'textCode' },
|
||
|
{ accelerator: 'Shift+Cmd+T', command: 'insertDateTime' },
|
||
|
{ accelerator: 'Shift+Cmd+F', command: 'focusSearch' },
|
||
|
{ accelerator: 'Cmd+F', command: 'showLocalSearch' },
|
||
|
{ accelerator: 'Shift+Cmd+S', command: 'focusElementSideBar' },
|
||
|
{ accelerator: 'Shift+Cmd+L', command: 'focusElementNoteList' },
|
||
|
{ accelerator: 'Shift+Cmd+N', command: 'focusElementNoteTitle' },
|
||
|
{ accelerator: 'Shift+Cmd+B', command: 'focusElementNoteBody' },
|
||
|
{ accelerator: 'Option+Cmd+S', command: 'toggleSidebar' },
|
||
|
{ accelerator: 'Cmd+L', command: 'toggleVisiblePanes' },
|
||
|
{ accelerator: 'Cmd+0', command: 'zoomActualSize' },
|
||
|
{ accelerator: 'Cmd+E', command: 'startExternalEditing' },
|
||
|
{ accelerator: 'Option+Cmd+T', command: 'setTags' },
|
||
|
{ accelerator: 'Cmd+G', command: 'gotoAnything' },
|
||
|
{ accelerator: 'F1', command: 'help' },
|
||
|
],
|
||
|
default: [
|
||
|
{ accelerator: 'Ctrl+N', command: 'newNote' },
|
||
|
{ accelerator: 'Ctrl+T', command: 'newTodo' },
|
||
|
{ accelerator: 'Ctrl+S', command: 'synchronize' },
|
||
|
{ accelerator: 'Ctrl+P', command: 'print' },
|
||
|
{ accelerator: 'Ctrl+Q', command: 'quit' },
|
||
|
{ accelerator: 'Ctrl+Alt+I', command: 'insertTemplate' },
|
||
|
{ accelerator: 'Ctrl+C', command: 'textCopy' },
|
||
|
{ accelerator: 'Ctrl+X', command: 'textCut' },
|
||
|
{ accelerator: 'Ctrl+V', command: 'textPaste' },
|
||
|
{ accelerator: 'Ctrl+A', command: 'textSelectAll' },
|
||
|
{ accelerator: 'Ctrl+B', command: 'textBold' },
|
||
|
{ accelerator: 'Ctrl+I', command: 'textItalic' },
|
||
|
{ accelerator: 'Ctrl+K', command: 'textLink' },
|
||
|
{ accelerator: 'Ctrl+`', command: 'textCode' },
|
||
|
{ accelerator: 'Ctrl+Shift+T', command: 'insertDateTime' },
|
||
|
{ accelerator: 'F6', command: 'focusSearch' },
|
||
|
{ accelerator: 'Ctrl+F', command: 'showLocalSearch' },
|
||
|
{ accelerator: 'Ctrl+Shift+S', command: 'focusElementSideBar' },
|
||
|
{ accelerator: 'Ctrl+Shift+L', command: 'focusElementNoteList' },
|
||
|
{ accelerator: 'Ctrl+Shift+N', command: 'focusElementNoteTitle' },
|
||
|
{ accelerator: 'Ctrl+Shift+B', command: 'focusElementNoteBody' },
|
||
|
{ accelerator: 'F10', command: 'toggleSidebar' },
|
||
|
{ accelerator: 'Ctrl+L', command: 'toggleVisiblePanes' },
|
||
|
{ accelerator: 'Ctrl+0', command: 'zoomActualSize' },
|
||
|
{ accelerator: 'Ctrl+E', command: 'startExternalEditing' },
|
||
|
{ accelerator: 'Ctrl+Alt+T', command: 'setTags' },
|
||
|
{ accelerator: 'Ctrl+,', command: 'config' },
|
||
|
{ accelerator: 'Ctrl+G', command: 'gotoAnything' },
|
||
|
{ accelerator: 'F1', command: 'help' },
|
||
|
],
|
||
|
};
|
||
|
|
||
|
export interface KeymapItem {
|
||
|
accelerator: string;
|
||
|
command: string;
|
||
|
}
|
||
|
|
||
|
interface Keymap {
|
||
|
[command: string]: KeymapItem;
|
||
|
}
|
||
|
|
||
|
export default class KeymapService extends BaseService {
|
||
|
private keymap: Keymap;
|
||
|
private defaultKeymap: KeymapItem[];
|
||
|
|
||
|
constructor() {
|
||
|
super();
|
||
|
|
||
|
// Automatically initialized for the current platform
|
||
|
this.initialize();
|
||
|
}
|
||
|
|
||
|
initialize(platform: string = shim.platformName()) {
|
||
|
this.keysRegExp = keysRegExp;
|
||
|
switch (platform) {
|
||
|
case 'darwin':
|
||
|
this.defaultKeymap = defaultKeymap.darwin;
|
||
|
this.modifiersRegExp = modifiersRegExp.darwin;
|
||
|
break;
|
||
|
default:
|
||
|
this.defaultKeymap = defaultKeymap.default;
|
||
|
this.modifiersRegExp = modifiersRegExp.default;
|
||
|
}
|
||
|
|
||
|
this.keymap = {};
|
||
|
for (let i = 0; i < this.defaultKeymap.length; i++) {
|
||
|
// Make a copy of the KeymapItem before assigning it
|
||
|
// Otherwise we're going to mess up the defaultKeymap array
|
||
|
this.keymap[this.defaultKeymap[i].command] = { ...this.defaultKeymap[i] };
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async loadKeymap(keymapPath: string) {
|
||
|
this.keymapPath = keymapPath; // Used for saving changes later..
|
||
|
|
||
|
if (await fs.exists(keymapPath)) {
|
||
|
this.logger().info(`Loading keymap: ${keymapPath}`);
|
||
|
|
||
|
try {
|
||
|
const keymapFile = await fs.readFile(keymapPath, 'utf-8');
|
||
|
this.setKeymap(JSON.parse(keymapFile));
|
||
|
} catch (err) {
|
||
|
const msg = err.message ? err.message : '';
|
||
|
throw new Error(`Failed to load keymap: ${keymapPath}\n${msg}`);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
hasAccelerator(command: string) {
|
||
|
return !!this.keymap[command];
|
||
|
}
|
||
|
|
||
|
getAccelerator(command: string) {
|
||
|
const item = this.keymap[command];
|
||
|
|
||
|
if (!item) throw new Error(`KeymapService: "${command}" command does not exist!`);
|
||
|
else return item.accelerator;
|
||
|
}
|
||
|
|
||
|
setAccelerator(command: string, accelerator: string) {
|
||
|
this.keymap[command].accelerator = accelerator;
|
||
|
}
|
||
|
|
||
|
resetAccelerator(command: string) {
|
||
|
const defaultItem = this.defaultKeymap.find((item => item.command === command));
|
||
|
|
||
|
if (!defaultItem) throw new Error(`KeymapService: "${command}" command does not exist!`);
|
||
|
else this.setAccelerator(command, defaultItem.accelerator);
|
||
|
}
|
||
|
|
||
|
setKeymap(customKeymap: KeymapItem[]) {
|
||
|
for (let i = 0; i < customKeymap.length; i++) {
|
||
|
const item = customKeymap[i];
|
||
|
|
||
|
try {
|
||
|
this.validateKeymapItem(item); // Throws if there are any issues in the keymap item
|
||
|
this.setAccelerator(item.command, item.accelerator);
|
||
|
} catch (err) {
|
||
|
throw new Error(`Keymap item ${JSON.stringify(item)} is invalid: ${err.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
this.validateKeymap(); // Throws whenever there are duplicate Accelerators used in the keymap
|
||
|
} catch (err) {
|
||
|
this.initialize();
|
||
|
throw new Error(`Keymap configuration contains duplicates\n${err.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private validateKeymapItem(item: KeymapItem) {
|
||
|
if (!item.hasOwnProperty('command')) {
|
||
|
throw new Error('"command" property is missing');
|
||
|
} else if (!this.keymap.hasOwnProperty(item.command)) {
|
||
|
throw new Error(`"${item.command}" is not a valid command`);
|
||
|
}
|
||
|
|
||
|
if (!item.hasOwnProperty('accelerator')) {
|
||
|
throw new Error('"accelerator" property is missing');
|
||
|
} else if (item.accelerator !== null) {
|
||
|
try {
|
||
|
this.validateAccelerator(item.accelerator);
|
||
|
} catch (err) {
|
||
|
throw new Error(`"${item.accelerator}" is not a valid accelerator`);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private validateKeymap() {
|
||
|
const usedAccelerators = new Set();
|
||
|
|
||
|
for (const item of Object.values(this.keymap)) {
|
||
|
const itemAccelerator = item.accelerator;
|
||
|
|
||
|
if (usedAccelerators.has(itemAccelerator)) {
|
||
|
const originalItem = Object.values(this.keymap).find(_item => _item.accelerator == item.accelerator);
|
||
|
throw new Error(
|
||
|
`Accelerator "${itemAccelerator}" can't be used for both "${item.command}" and "${originalItem.command}" commands\n` +
|
||
|
'You have to change the accelerator for any of above commands'
|
||
|
);
|
||
|
} else if (itemAccelerator !== null) {
|
||
|
usedAccelerators.add(itemAccelerator);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private validateAccelerator(accelerator: string) {
|
||
|
let keyFound = false;
|
||
|
|
||
|
const parts = accelerator.split('+');
|
||
|
const isValid = parts.every((part, index) => {
|
||
|
const isKey = this.keysRegExp.test(part);
|
||
|
const isModifier = this.modifiersRegExp.test(part);
|
||
|
|
||
|
if (isKey) {
|
||
|
// Key must be unique
|
||
|
if (keyFound) return false;
|
||
|
keyFound = true;
|
||
|
}
|
||
|
|
||
|
// Key is required
|
||
|
if (index === (parts.length - 1) && !keyFound) return false;
|
||
|
return isKey || isModifier;
|
||
|
});
|
||
|
|
||
|
if (!isValid) throw new Error(`Accelerator invalid: ${accelerator}`);
|
||
|
}
|
||
|
|
||
|
static instance() {
|
||
|
if (this.instance_) return this.instance_;
|
||
|
|
||
|
this.instance_ = new KeymapService();
|
||
|
return this.instance_;
|
||
|
}
|
||
|
}
|