├── styles.css ├── versions.json ├── .gitignore ├── manifest.json ├── tsconfig.json ├── README.md ├── package.json ├── rollup.config.js ├── LICENSE └── main.ts /styles.css: -------------------------------------------------------------------------------- 1 | input[type='text'].invalid-accelerator { 2 | background-color: #e24f4f; 3 | } 4 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.1.0": "0.11.13", 3 | "0.1.1": "0.11.13", 4 | "0.1.2": "0.11.13" 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | *.iml 3 | .idea 4 | 5 | # npm 6 | node_modules 7 | package-lock.json 8 | 9 | # build 10 | main.js 11 | *.js.map 12 | 13 | # obsidian 14 | data.json 15 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-global-hotkeys", 3 | "name": "Global Hotkeys", 4 | "version": "0.1.2", 5 | "minAppVersion": "0.11.13", 6 | "description": "Add support for global hotkeys", 7 | "author": "Marc Jessome", 8 | "authorUrl": "https://github.com/mjessome/obsidian-global-hotkeys/", 9 | "isDesktopOnly": true 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "es6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "lib": [ 13 | "dom", 14 | "es5", 15 | "scripthost", 16 | "es2015" 17 | ] 18 | }, 19 | "include": [ 20 | "**/*.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Global Hotkeys for Obsidian 2 | 3 | This is an [Obsidian](https://obsidian.md) plugin, adding support for system-wide global hotkeys. 4 | 5 | ## Features 6 | 7 | - Any Obsidian command can be mapped 8 | - Brings Obsidian to the front on every command 9 | - Extra commands added: 10 | - Bring Obsidian to front 11 | - Show/Hide Obsidian 12 | 13 | ## Settings 14 | 15 | In the `Global Hotkeys` settings, you can enter a system-wide key combination that 16 | will perform the specified command. The key combination is specified following 17 | [Electron's Accelerators](https://www.electronjs.org/docs/api/accelerator). 18 | 19 | ## To-Do 20 | 21 | - Improve hotkey settings 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-global-hotkeys", 3 | "version": "0.1.2", 4 | "description": "Register global hotkeys for obsidian", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "rollup --config rollup.config.js -w", 8 | "build": "rollup --config rollup.config.js --environment BUILD:production" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@rollup/plugin-commonjs": "^18.0.0", 15 | "@rollup/plugin-node-resolve": "^11.2.1", 16 | "@rollup/plugin-typescript": "^8.2.1", 17 | "@types/node": "^14.14.37", 18 | "obsidian": "^0.12.0", 19 | "rollup": "^2.32.1", 20 | "tslib": "^2.2.0", 21 | "typescript": "^4.2.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | 5 | const isProd = (process.env.BUILD === 'production'); 6 | 7 | const banner = 8 | `/* 9 | THIS IS A GENERATED/BUNDLED FILE BY ROLLUP 10 | if you want to view the source visit the plugins github repository 11 | */ 12 | `; 13 | 14 | export default { 15 | input: 'main.ts', 16 | output: { 17 | dir: '.', 18 | sourcemap: 'inline', 19 | sourcemapExcludeSources: isProd, 20 | format: 'cjs', 21 | exports: 'default', 22 | banner, 23 | }, 24 | external: ['obsidian'], 25 | plugins: [ 26 | typescript(), 27 | nodeResolve({browser: true}), 28 | commonjs(), 29 | ] 30 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Marc Jessome 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal, Notice, Platform, Plugin, PluginSettingTab, Setting } from 'obsidian'; 2 | const remote = require('electron').remote; 3 | const globalShortcut = remote.globalShortcut; 4 | 5 | interface GlobalHotkeysPluginSettings { 6 | accelerators: { [key: string]: string }; 7 | } 8 | 9 | const DEFAULT_SETTINGS: GlobalHotkeysPluginSettings = { 10 | accelerators: {}, 11 | } 12 | 13 | export default class GlobalHotkeysPlugin extends Plugin { 14 | settings: GlobalHotkeysPluginSettings; 15 | currentlyMapped: { [key: string]: string }; 16 | 17 | async registerGlobalShortcut(command_id:string, accelerator:string, 18 | oncomplete?:(success:boolean)=>void) { 19 | if (command_id in this.currentlyMapped) { 20 | this.unregisterGlobalShortcut(command_id); 21 | } 22 | 23 | let success = (() => { 24 | try { 25 | return globalShortcut.register(accelerator, () => { 26 | const command = app.commands.commands[command_id]; 27 | if (!command) return; 28 | this.app.setting.close(); // Ensure all modals are closed? 29 | const win = remote.getCurrentWindow(); 30 | const wasHidden = !win.isFocused() || !win.isVisible(); 31 | 32 | if (command.checkCallback) 33 | command.checkCallback(false); 34 | else if (command.callback) 35 | command.callback(); 36 | 37 | // only activate Obsidian if visibility hasn't changed 38 | const isHidden = !win.isFocused() || !win.isVisible(); 39 | if (wasHidden && isHidden) 40 | remote.getCurrentWindow().show(); // Activate obsidian 41 | }); 42 | } catch (error) { 43 | return false; 44 | } 45 | })(); 46 | 47 | if (success) { 48 | this.currentlyMapped[command_id] = accelerator; 49 | } 50 | 51 | if (oncomplete) { 52 | oncomplete(success); 53 | } 54 | } 55 | 56 | async unregisterGlobalShortcut(command_id:string) { 57 | const accelerator = this.currentlyMapped[command_id]; 58 | if (accelerator) { 59 | globalShortcut.unregister(accelerator); 60 | delete this.currentlyMapped[command_id]; 61 | } 62 | } 63 | 64 | isRegistered(command_id:string) { 65 | return (command_id in this.currentlyMapped); 66 | } 67 | 68 | async onload() { 69 | this.currentlyMapped = {}; 70 | 71 | this.addCommand({ 72 | id: 'bring-to-front', 73 | name: 'Bring Obsidian to front', 74 | checkCallback: (checking: boolean) => { 75 | if (!checking) 76 | remote.getCurrentWindow().show(); 77 | return true; 78 | } 79 | }); 80 | 81 | this.addCommand({ 82 | id: 'show-hide', 83 | name: 'Show/Hide Obsidian', 84 | checkCallback: (checking: boolean) => { 85 | if (!checking) { 86 | const win = remote.getCurrentWindow(); 87 | if (win.isVisible()) { 88 | if (Platform.isMacOS) { 89 | remote.Menu.sendActionToFirstResponder('hide:'); 90 | } else { 91 | win.blur(); 92 | } 93 | } else { 94 | win.show(); 95 | } 96 | } 97 | return true; 98 | } 99 | }); 100 | 101 | await this.loadSettings(); 102 | 103 | globalShortcut.unregisterAll(); 104 | for (const cmd in this.settings.accelerators) { 105 | const a = this.settings.accelerators[cmd]; 106 | if (a) { 107 | this.registerGlobalShortcut(cmd, a); 108 | } 109 | } 110 | 111 | this.addSettingTab(new GlobalShortcutSettingTab(this.app, this)); 112 | } 113 | 114 | onunload() { 115 | globalShortcut.unregisterAll(); 116 | } 117 | 118 | async loadSettings() { 119 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 120 | } 121 | 122 | async saveSettings() { 123 | await this.saveData(this.settings); 124 | } 125 | } 126 | 127 | class GlobalShortcutSettingTab extends PluginSettingTab { 128 | plugin: GlobalHotkeysPlugin; 129 | filterString: string; 130 | settingElems: Setting[]; 131 | 132 | constructor(app: App, plugin: GlobalHotkeysPlugin) { 133 | super(app, plugin); 134 | this.plugin = plugin; 135 | this.filterString = ""; 136 | } 137 | 138 | updateHotkeyVisibility() { 139 | this.settingElems.forEach((e) => { 140 | const elemName = e.nameEl.textContent.toLowerCase(); 141 | const visible = this.filterString.length == 0 || elemName.contains(this.filterString); 142 | e.settingEl.toggle(visible); 143 | }); 144 | } 145 | 146 | async removeSavedAccelerator(command_id:string) { 147 | this.plugin.unregisterGlobalShortcut(command_id); 148 | delete this.plugin.settings.accelerators[command_id]; 149 | await this.plugin.saveSettings(); 150 | } 151 | 152 | display(): void { 153 | let {containerEl} = this; 154 | this.settingElems = [] 155 | 156 | containerEl.empty(); 157 | 158 | containerEl.createDiv('', div => { 159 | const text = document.createElement('p'); 160 | text.appendText("For information on key bindings, see documentation "); 161 | 162 | const link = document.createElement('a'); 163 | link.setAttribute('href', "https://www.electronjs.org/docs/api/accelerator#available-modifiers"); 164 | link.textContent = "here"; 165 | text.appendChild(link); 166 | 167 | text.appendText("."); 168 | div.appendChild(text); 169 | 170 | const exampleText = document.createElement('p'); 171 | exampleText.appendChild(document.createElement('strong')).appendText('Example: '); 172 | exampleText.appendText('Cmd+Shift+Ctrl+Alt+N'); 173 | div.appendChild(exampleText); 174 | }); 175 | 176 | containerEl.createDiv('hotkey-search-container', div => { 177 | let filterEl = document.createElement('input'); 178 | div.appendChild(filterEl); 179 | filterEl.setAttribute('type', 'text'); 180 | filterEl.setAttribute('placeholder', 'Filter...'); 181 | filterEl.value = this.filterString; 182 | filterEl.addEventListener('input', e => { 183 | this.filterString = e.target.value.toLowerCase(); 184 | this.updateHotkeyVisibility(); 185 | }); 186 | }); 187 | 188 | let allCmds = this.app.commands.commands; 189 | 190 | const cmdKeys = Object.keys(allCmds); 191 | cmdKeys.sort((e1, e2) => (allCmds[e1].name < allCmds[e2].name) ? -1 : 1); 192 | cmdKeys.forEach(cmd => { 193 | const accelerator = this.plugin.settings.accelerators[cmd]; 194 | const name = allCmds[cmd].name; 195 | let setting = new Setting(containerEl) 196 | .setName(name) 197 | .addText(text => text 198 | .setPlaceholder('Hotkey') 199 | .setValue(accelerator) 200 | .onChange(async (value) => { 201 | const inputEl = setting.components[0].inputEl; 202 | if (value) { 203 | this.plugin.registerGlobalShortcut(cmd, value, async (success) => { 204 | if (success) { 205 | inputEl.classList.remove('invalid-accelerator'); 206 | this.plugin.settings.accelerators[cmd] = value; 207 | await this.plugin.saveSettings(); 208 | } else { 209 | this.removeSavedAccelerator(cmd); 210 | inputEl.classList.add('invalid-accelerator'); 211 | } 212 | }); 213 | } else { 214 | inputEl.classList.remove('invalid-accelerator'); 215 | this.removeSavedAccelerator(cmd); 216 | } 217 | })); 218 | this.settingElems.push(setting); 219 | }); 220 | 221 | this.updateHotkeyVisibility(); 222 | } 223 | } 224 | --------------------------------------------------------------------------------