import fs from 'fs-plus' import path from 'path' import mousetrap from 'mousetrap' import {ipcRenderer} from 'electron' import {Emitter, Disposable} from 'event-kit' let suspended = false const templateConfigKey = 'core.keymapTemplate' /* By default, Mousetrap stops all hotkeys within text inputs. Override this to more specifically block only hotkeys that have no modifier keys (things like Gmail's "x", while allowing standard hotkeys.) */ mousetrap.prototype.stopCallback = (e, element, combo) => { if (suspended) { return true; } const withinTextInput = element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.isContentEditable const withinWebview = element.tagName === 'WEBVIEW'; if (withinWebview) { return true; } if (withinTextInput) { const isPlainKey = !/(mod|command|ctrl)/.test(combo); const isReservedTextEditingShortcut = /(mod|command|ctrl)\+(a|x|c|v)/.test(combo); return isPlainKey || isReservedTextEditingShortcut; } return false; } class KeymapFile { constructor(manager, filePath) { this._manager = manager; this._path = filePath; this._bindings = {}; this._disposable = null; } load = () => { let keymaps = null; try { keymaps = JSON.parse(fs.readFileSync(this._path)) } catch (e) { if (e.code === 'ENOENT') { return; } console.error(e); return; } this._bindings = {}; Object.keys(keymaps).forEach((command) => { let keystrokesArray = keymaps[command]; if (!(keystrokesArray instanceof Array)) { keystrokesArray = [keystrokesArray]; } for (const keystrokes of keystrokesArray) { this._manager.ensureKeystrokesRegistered(keystrokes); this._bindings[command] = this._bindings[command] || []; this._bindings[command].push(keystrokes); } }); this._manager.keymapCacheInvalidated(); } watch() { fs.watch(this._path, this.load); } bindings() { return this._bindings; } } export default class KeymapManager { constructor({configDirPath, resourcePath}) { this.configDirPath = configDirPath; this.resourcePath = resourcePath; this._emitter = new Emitter(); this._registered = {}; this._files = []; } getUserKeymapPath() { return path.join(this.configDirPath, 'keymap.json'); } suspendAllKeymaps() { NylasEnv.menu.sendToBrowserProcess(NylasEnv.menu.template, {}); suspended = true; } resumeAllKeymaps() { NylasEnv.menu.update(); suspended = false; } loadKeymaps = () => { // Load the base keymap and the base.platform keymap this.loadKeymap(path.join(this.resourcePath, 'keymaps', 'base.json')) this.loadKeymap(path.join(this.resourcePath, 'keymaps', `base-${process.platform}.json`)) // Load the template keymap (Gmail, Mail.app, etc.) the user has chosen if (this._unobserveTemplate) { this._unobserveTemplate.dispose(); } this._unobserveTemplate = NylasEnv.config.observe(templateConfigKey, this.loadTemplateKeymap); const userKeymapPath = this.getUserKeymapPath() if (!fs.existsSync(userKeymapPath)) { fs.writeFileSync(userKeymapPath, "{}"); } this.userKeymap = new KeymapFile(this, userKeymapPath) this.userKeymap.load() this.userKeymap.watch() } loadTemplateKeymap = () => { if (this._removeTemplate) { this._removeTemplate.dispose(); } let templateFile = NylasEnv.config.get(templateConfigKey); if (templateFile) { templateFile = templateFile.replace("GoogleInbox", "Inbox by Gmail"); const templateKeymapPath = path.join(this.resourcePath, 'keymaps', 'templates', `${templateFile}.json`); this._removeTemplate = this.loadKeymap(templateKeymapPath); } } loadKeymap(filePath) { const file = new KeymapFile(this, filePath); this._files.push(file); file.load(); return new Disposable(() => { this._files = this._files.filter(f => f !== file); this.keymapCacheInvalidated(); }); } ensureKeystrokesRegistered(keystrokes) { if (this._registered[keystrokes]) { return; } this._registered[keystrokes] = true; mousetrap.bind(keystrokes, () => { for (const command of (this._commandsCache[keystrokes] || [])) { if (command.startsWith('application:')) { ipcRenderer.send('command', command); } else { NylasEnv.commands.dispatch(command); } } return false }); } keymapCacheInvalidated() { this._bindingsCache = {}; for (const file of this._files) { const fileBindings = file.bindings(); for (const command of Object.keys(fileBindings)) { const keystrokesArray = fileBindings[command]; this._bindingsCache[command] = (this._bindingsCache[command] || []).concat(keystrokesArray); } } if (this.userKeymap) { const userBindings = this.userKeymap.bindings(); for (const command of Object.keys(userBindings)) { this._bindingsCache[command] = userBindings[command]; } } this._commandsCache = {}; for (const command of Object.keys(this._bindingsCache)) { for (const keystrokes of this._bindingsCache[command]) { if (!this._commandsCache[keystrokes]) { this._commandsCache[keystrokes] = []; } if (!this._commandsCache[keystrokes].includes(command)) { this._commandsCache[keystrokes].push(command); } } } this._emitter.emit('on-did-reload-keymap'); } onDidReloadKeymap = (callback) => { return this._emitter.on('on-did-reload-keymap', callback); } getBindingsForAllCommands() { return this._bindingsCache; } getBindingsForCommand(command) { return this._bindingsCache[command] || []; } }