├── .gitattributes ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── README.md ├── hotkey-helper.gif ├── jsconfig.json ├── manifest.json ├── ophidian.config.mjs ├── package.json ├── plugin-browser.png ├── pnpm-lock.yaml ├── rollup.config.js ├── src ├── hotkey-helper.ts └── obsidian-internals.ts ├── styles.css ├── tsconfig.json └── versions.json /.gitattributes: -------------------------------------------------------------------------------- 1 | main.js binary -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish plugin 2 | on: 3 | push: 4 | # Sequence of patterns matched against refs/tags 5 | tags: 6 | - "*" # Push events to matching any tag format, i.e. 1.0, 20.15.10 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ophidian-lib/build@v1 14 | with: 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | release-notes: ${{ github.event.commits[0].message }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Hotkey Helper: Easier Hotkey and Options Management for Obsidian 2 | 3 | > New in... 4 | > 5 | > * 0.3.13: Commands to let you open hotkeys or settings by typing the plugin name 6 | > * 0.3.12: Jump to edit a hotkey with Ctrl- or Cmd-Enter from the command palette! 7 | > * 0.3.8: Hit enter while typing in the community plugin search box to open the plugin catalog with that search 8 | > * 0.3.5: Plugin searches are saved and carry through when browsing the plugin catalog 9 | > * 0.3.2: Link to plugins with [Plugin URLs](#plugin-urls), and configuration buttons in the plugin browser 10 | > * 0.2.1: support for core plugins and non-plugin hotkeys as well as community plugins 11 | 12 | This plugin makes it easier to manage plugins' hotkeys and options in [Obsidian.md](https://obsidian.md), by adding icons next to each plugin (in the Core and Community plugin tabs) that you can use to open that plugin's options or hotkey assignments. 13 | 14 | ![](https://raw.githubusercontent.com/pjeby/hotkey-helper/master/hotkey-helper.gif) 15 | 16 | Better still: hovering over the hotkeys icon shows you how many commands the plugin has, how many of those commands have hotkeys assigned, and how many of the assignments are in conflict with any other hotkey assignments. (The icon is also highlighted with your theme's error background color if there are any conflicts.) 17 | 18 | The icons automatically come and go or change color as you enable or disable plugins, so you can immediately find out where a conflict is taking place, and easily review or set up a new plugin's hotkeys or settings. 19 | 20 | In addition, when you enable a plugin from the community plugins viewer, you can immediately access its configuration and hotkeys: 21 | 22 | ![Plugin browser view](https://raw.githubusercontent.com/pjeby/hotkey-helper/master/plugin-browser.png) 23 | 24 | ### Installation 25 | 26 | To install the plugin, search for "hotkey helper" in Obsidian's Community Plugins interface. Or, if it's not there yet, just visit the [Github releases page](https://github.com/pjeby/hotkey-helper/releases), download the plugin .zip from the latest release, and unzip it in your vault's `.obsidian/plugins/` directory. 27 | 28 | Either way, you can then enable it from the Obsidian "Community Plugins" tab for that vault. 29 | 30 | If you encounter any problems with the plugin, please file bug reports to this repository rather than using the Obsidian forums: I don't check the forums every day (or even every week!) but I do receive email notices from Github and will get back to you much faster than I will see forum comments. 31 | 32 | ### Plugin URLs 33 | 34 | When this plugin is enabled, you can open plugin information using URLs of the form `obsidian://goto-plugin?id=plugin-id`. This will open the Community Plugins browser of the current vault, displaying information for that plugin. So for example, the URL will open this page in Obsidian's plugin browser (if your current vault has Hotkey Helper enabled). 35 | 36 | This means that if you are a plugin author and want to make it easy for people to find and install your plugin (i.e., without needing to type in its name), you can just include a URL wherever you're promoting your plugin (or others are sharing it. (Note: Github strips `obsidian://` URLs from markdown, so if you want to include a link in your project's README, you can link to e.g. https://obsidian-plugins.peak-dev.org/goto/hotkey-helper/ to get a redirect to the actual Obsidian URL. Hopefully this can be replaced with a redirector at an official domain in the future.) 37 | 38 | In addition to the `id=` argument, you can also add `&show=config` or `&show=hotkeys` to the URL to make it go directly to the settings or hotkey configuration for that plugin (if it's installed, enabled, and has a settings tab or commands). This can make it easier to support your users, by being able to give a link rather than lengthy instructions to locate the specific items/areas needed. 39 | 40 | ### Known Issues/Current Limitations 41 | 42 | * If you search in the Hotkeys tab for a string without spaces, ending with `:`, it will only display commands provided by the named plugin; e.g. searching for `workspaces:` would list only commands from the built-in Workspaces plugin (if enabled), rather than all commands whose name *contains* the word workspaces followed by a `:`. 43 | -------------------------------------------------------------------------------- /hotkey-helper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pjeby/hotkey-helper/e5c9756cb255ecbe7ca62b0626a7a967dbba6112/hotkey-helper.gif -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es6", 4 | "moduleResolution": "node", 5 | }, 6 | "exclude": ["node_modules"] 7 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "hotkey-helper", 3 | "name": "Hotkey Helper", 4 | "version": "0.3.20", 5 | "minAppVersion": "1.5.8", 6 | "description": "Easily see and access any plugin's settings or hotkey assignments (and conflicts) from the Community Plugins tab", 7 | "author": "PJ Eby", 8 | "authorUrl": "https://github.com/pjeby" 9 | } -------------------------------------------------------------------------------- /ophidian.config.mjs: -------------------------------------------------------------------------------- 1 | import Builder from "@ophidian/build"; 2 | 3 | new Builder("src/hotkey-helper.ts") 4 | .withSass() 5 | .withInstall() 6 | .build(); 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hotkey-helper", 3 | "scripts": { 4 | "dev": "node ophidian.config.mjs dev", 5 | "build": "node ophidian.config.mjs production" 6 | }, 7 | "license": "ISC", 8 | "type": "module", 9 | "devDependencies": { 10 | "@ophidian/build": "^1.2", 11 | "@ophidian/core": "0.0.23", 12 | "i18next": "^20.3.2", 13 | "monkey-around": "^2.1", 14 | "obsidian": "1.3.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /plugin-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pjeby/hotkey-helper/e5c9756cb255ecbe7ca62b0626a7a967dbba6112/plugin-browser.png -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | specifiers: 4 | '@ophidian/build': ^1.2 5 | '@ophidian/core': 0.0.23 6 | i18next: ^20.3.2 7 | monkey-around: ^2.1 8 | obsidian: 1.3.5 9 | 10 | devDependencies: 11 | '@ophidian/build': 1.2.1 12 | '@ophidian/core': 0.0.23 13 | i18next: 20.6.1 14 | monkey-around: 2.3.0 15 | obsidian: 1.3.5 16 | 17 | packages: 18 | 19 | /@babel/runtime/7.18.9: 20 | resolution: {integrity: sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==} 21 | engines: {node: '>=6.9.0'} 22 | dependencies: 23 | regenerator-runtime: 0.13.9 24 | dev: true 25 | 26 | /@esbuild/android-arm/0.17.6: 27 | resolution: {integrity: sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g==} 28 | engines: {node: '>=12'} 29 | cpu: [arm] 30 | os: [android] 31 | requiresBuild: true 32 | dev: true 33 | optional: true 34 | 35 | /@esbuild/android-arm64/0.17.6: 36 | resolution: {integrity: sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg==} 37 | engines: {node: '>=12'} 38 | cpu: [arm64] 39 | os: [android] 40 | requiresBuild: true 41 | dev: true 42 | optional: true 43 | 44 | /@esbuild/android-x64/0.17.6: 45 | resolution: {integrity: sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ==} 46 | engines: {node: '>=12'} 47 | cpu: [x64] 48 | os: [android] 49 | requiresBuild: true 50 | dev: true 51 | optional: true 52 | 53 | /@esbuild/darwin-arm64/0.17.6: 54 | resolution: {integrity: sha512-bsDRvlbKMQMt6Wl08nHtFz++yoZHsyTOxnjfB2Q95gato+Yi4WnRl13oC2/PJJA9yLCoRv9gqT/EYX0/zDsyMA==} 55 | engines: {node: '>=12'} 56 | cpu: [arm64] 57 | os: [darwin] 58 | requiresBuild: true 59 | dev: true 60 | optional: true 61 | 62 | /@esbuild/darwin-x64/0.17.6: 63 | resolution: {integrity: sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg==} 64 | engines: {node: '>=12'} 65 | cpu: [x64] 66 | os: [darwin] 67 | requiresBuild: true 68 | dev: true 69 | optional: true 70 | 71 | /@esbuild/freebsd-arm64/0.17.6: 72 | resolution: {integrity: sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg==} 73 | engines: {node: '>=12'} 74 | cpu: [arm64] 75 | os: [freebsd] 76 | requiresBuild: true 77 | dev: true 78 | optional: true 79 | 80 | /@esbuild/freebsd-x64/0.17.6: 81 | resolution: {integrity: sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q==} 82 | engines: {node: '>=12'} 83 | cpu: [x64] 84 | os: [freebsd] 85 | requiresBuild: true 86 | dev: true 87 | optional: true 88 | 89 | /@esbuild/linux-arm/0.17.6: 90 | resolution: {integrity: sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw==} 91 | engines: {node: '>=12'} 92 | cpu: [arm] 93 | os: [linux] 94 | requiresBuild: true 95 | dev: true 96 | optional: true 97 | 98 | /@esbuild/linux-arm64/0.17.6: 99 | resolution: {integrity: sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w==} 100 | engines: {node: '>=12'} 101 | cpu: [arm64] 102 | os: [linux] 103 | requiresBuild: true 104 | dev: true 105 | optional: true 106 | 107 | /@esbuild/linux-ia32/0.17.6: 108 | resolution: {integrity: sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ==} 109 | engines: {node: '>=12'} 110 | cpu: [ia32] 111 | os: [linux] 112 | requiresBuild: true 113 | dev: true 114 | optional: true 115 | 116 | /@esbuild/linux-loong64/0.17.6: 117 | resolution: {integrity: sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ==} 118 | engines: {node: '>=12'} 119 | cpu: [loong64] 120 | os: [linux] 121 | requiresBuild: true 122 | dev: true 123 | optional: true 124 | 125 | /@esbuild/linux-mips64el/0.17.6: 126 | resolution: {integrity: sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA==} 127 | engines: {node: '>=12'} 128 | cpu: [mips64el] 129 | os: [linux] 130 | requiresBuild: true 131 | dev: true 132 | optional: true 133 | 134 | /@esbuild/linux-ppc64/0.17.6: 135 | resolution: {integrity: sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg==} 136 | engines: {node: '>=12'} 137 | cpu: [ppc64] 138 | os: [linux] 139 | requiresBuild: true 140 | dev: true 141 | optional: true 142 | 143 | /@esbuild/linux-riscv64/0.17.6: 144 | resolution: {integrity: sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ==} 145 | engines: {node: '>=12'} 146 | cpu: [riscv64] 147 | os: [linux] 148 | requiresBuild: true 149 | dev: true 150 | optional: true 151 | 152 | /@esbuild/linux-s390x/0.17.6: 153 | resolution: {integrity: sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q==} 154 | engines: {node: '>=12'} 155 | cpu: [s390x] 156 | os: [linux] 157 | requiresBuild: true 158 | dev: true 159 | optional: true 160 | 161 | /@esbuild/linux-x64/0.17.6: 162 | resolution: {integrity: sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw==} 163 | engines: {node: '>=12'} 164 | cpu: [x64] 165 | os: [linux] 166 | requiresBuild: true 167 | dev: true 168 | optional: true 169 | 170 | /@esbuild/netbsd-x64/0.17.6: 171 | resolution: {integrity: sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A==} 172 | engines: {node: '>=12'} 173 | cpu: [x64] 174 | os: [netbsd] 175 | requiresBuild: true 176 | dev: true 177 | optional: true 178 | 179 | /@esbuild/openbsd-x64/0.17.6: 180 | resolution: {integrity: sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw==} 181 | engines: {node: '>=12'} 182 | cpu: [x64] 183 | os: [openbsd] 184 | requiresBuild: true 185 | dev: true 186 | optional: true 187 | 188 | /@esbuild/sunos-x64/0.17.6: 189 | resolution: {integrity: sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw==} 190 | engines: {node: '>=12'} 191 | cpu: [x64] 192 | os: [sunos] 193 | requiresBuild: true 194 | dev: true 195 | optional: true 196 | 197 | /@esbuild/win32-arm64/0.17.6: 198 | resolution: {integrity: sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg==} 199 | engines: {node: '>=12'} 200 | cpu: [arm64] 201 | os: [win32] 202 | requiresBuild: true 203 | dev: true 204 | optional: true 205 | 206 | /@esbuild/win32-ia32/0.17.6: 207 | resolution: {integrity: sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg==} 208 | engines: {node: '>=12'} 209 | cpu: [ia32] 210 | os: [win32] 211 | requiresBuild: true 212 | dev: true 213 | optional: true 214 | 215 | /@esbuild/win32-x64/0.17.6: 216 | resolution: {integrity: sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA==} 217 | engines: {node: '>=12'} 218 | cpu: [x64] 219 | os: [win32] 220 | requiresBuild: true 221 | dev: true 222 | optional: true 223 | 224 | /@nodelib/fs.scandir/2.1.5: 225 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 226 | engines: {node: '>= 8'} 227 | dependencies: 228 | '@nodelib/fs.stat': 2.0.5 229 | run-parallel: 1.2.0 230 | dev: true 231 | 232 | /@nodelib/fs.stat/2.0.5: 233 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 234 | engines: {node: '>= 8'} 235 | dev: true 236 | 237 | /@nodelib/fs.walk/1.2.8: 238 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 239 | engines: {node: '>= 8'} 240 | dependencies: 241 | '@nodelib/fs.scandir': 2.1.5 242 | fastq: 1.17.1 243 | dev: true 244 | 245 | /@ophidian/build/1.2.1: 246 | resolution: {integrity: sha512-ulefYLGwlWmsP5WgkOunujhU57dB5slENQ7VvwFh0h0Yk6id4Osg3jN0yONtB9gF6awfjzfK4OxzfOhmuzWx7g==} 247 | dependencies: 248 | builtin-modules: 3.3.0 249 | copy-newer: 2.1.2 250 | esbuild: 0.17.6 251 | esbuild-plugin-copy: 2.1.1_esbuild@0.17.6 252 | esbuild-plugin-sass: 1.0.1_esbuild@0.17.6 253 | fs-extra: 10.1.0 254 | monkey-around: 2.3.0 255 | sass: 1.47.0 256 | dev: true 257 | 258 | /@ophidian/core/0.0.23: 259 | resolution: {integrity: sha512-R4DcgLsqNIzrfDsDTc6EcUoCkJGgQw+SDiEC4u/vPn3W0MizWnz4lWuU5OZv32dY7RbnXj3qMFBGXfg8ez4mFA==} 260 | dependencies: 261 | '@preact/signals-core': 1.8.0 262 | defaults: 2.0.2 263 | i18next: 20.6.1 264 | monkey-around: 2.3.0 265 | obsidian: 1.3.5 266 | to-use: 0.3.3 267 | wonka: 6.3.4 268 | transitivePeerDependencies: 269 | - '@codemirror/state' 270 | - '@codemirror/view' 271 | dev: true 272 | 273 | /@preact/signals-core/1.8.0: 274 | resolution: {integrity: sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==} 275 | dev: true 276 | 277 | /@types/codemirror/5.60.8: 278 | resolution: {integrity: sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==} 279 | dependencies: 280 | '@types/tern': 0.23.4 281 | dev: true 282 | 283 | /@types/estree/0.0.51: 284 | resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} 285 | dev: true 286 | 287 | /@types/tern/0.23.4: 288 | resolution: {integrity: sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==} 289 | dependencies: 290 | '@types/estree': 0.0.51 291 | dev: true 292 | 293 | /ansi-styles/4.3.0: 294 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 295 | engines: {node: '>=8'} 296 | dependencies: 297 | color-convert: 2.0.1 298 | dev: true 299 | 300 | /anymatch/3.1.3: 301 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 302 | engines: {node: '>= 8'} 303 | dependencies: 304 | normalize-path: 3.0.0 305 | picomatch: 2.3.1 306 | dev: true 307 | 308 | /array-union/1.0.2: 309 | resolution: {integrity: sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==} 310 | engines: {node: '>=0.10.0'} 311 | dependencies: 312 | array-uniq: 1.0.3 313 | dev: true 314 | 315 | /array-union/2.1.0: 316 | resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 317 | engines: {node: '>=8'} 318 | dev: true 319 | 320 | /array-uniq/1.0.3: 321 | resolution: {integrity: sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==} 322 | engines: {node: '>=0.10.0'} 323 | dev: true 324 | 325 | /arrify/1.0.1: 326 | resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} 327 | engines: {node: '>=0.10.0'} 328 | dev: true 329 | 330 | /balanced-match/1.0.2: 331 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 332 | dev: true 333 | 334 | /binary-extensions/2.3.0: 335 | resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 336 | engines: {node: '>=8'} 337 | dev: true 338 | 339 | /brace-expansion/1.1.11: 340 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 341 | dependencies: 342 | balanced-match: 1.0.2 343 | concat-map: 0.0.1 344 | dev: true 345 | 346 | /braces/3.0.2: 347 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 348 | engines: {node: '>=8'} 349 | dependencies: 350 | fill-range: 7.0.1 351 | dev: true 352 | 353 | /builtin-modules/3.3.0: 354 | resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} 355 | engines: {node: '>=6'} 356 | dev: true 357 | 358 | /chalk/4.1.2: 359 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 360 | engines: {node: '>=10'} 361 | dependencies: 362 | ansi-styles: 4.3.0 363 | supports-color: 7.2.0 364 | dev: true 365 | 366 | /chokidar/3.6.0: 367 | resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 368 | engines: {node: '>= 8.10.0'} 369 | dependencies: 370 | anymatch: 3.1.3 371 | braces: 3.0.2 372 | glob-parent: 5.1.2 373 | is-binary-path: 2.1.0 374 | is-glob: 4.0.3 375 | normalize-path: 3.0.0 376 | readdirp: 3.6.0 377 | optionalDependencies: 378 | fsevents: 2.3.3 379 | dev: true 380 | 381 | /color-convert/2.0.1: 382 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 383 | engines: {node: '>=7.0.0'} 384 | dependencies: 385 | color-name: 1.1.4 386 | dev: true 387 | 388 | /color-name/1.1.4: 389 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 390 | dev: true 391 | 392 | /concat-map/0.0.1: 393 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 394 | dev: true 395 | 396 | /copy-newer/2.1.2: 397 | resolution: {integrity: sha512-IDhyNGNvbSqwjQXjZ3tAzZNXRw0UmXa+TmTQmMJziikQ+sdsV9EkI6B2WZX1u9m3TKHayBCc2pGqXU/KlBqJdg==} 398 | engines: {node: '>=4'} 399 | hasBin: true 400 | dependencies: 401 | fs-write-stream-atomic: 1.0.10 402 | globby: 4.1.0 403 | graceful-fs: 4.2.11 404 | minimist: 1.2.8 405 | mkdirp: 0.5.6 406 | pify: 2.3.0 407 | dev: true 408 | 409 | /core-util-is/1.0.3: 410 | resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} 411 | dev: true 412 | 413 | /css-tree/1.1.3: 414 | resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} 415 | engines: {node: '>=8.0.0'} 416 | dependencies: 417 | mdn-data: 2.0.14 418 | source-map: 0.6.1 419 | dev: true 420 | 421 | /defaults/2.0.2: 422 | resolution: {integrity: sha512-cuIw0PImdp76AOfgkjbW4VhQODRmNNcKR73vdCH5cLd/ifj7aamfoXvYgfGkEAjNJZ3ozMIy9Gu2LutUkGEPbA==} 423 | engines: {node: '>=16'} 424 | dev: true 425 | 426 | /dir-glob/3.0.1: 427 | resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 428 | engines: {node: '>=8'} 429 | dependencies: 430 | path-type: 4.0.0 431 | dev: true 432 | 433 | /esbuild-plugin-copy/2.1.1_esbuild@0.17.6: 434 | resolution: {integrity: sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw==} 435 | peerDependencies: 436 | esbuild: '>= 0.14.0' 437 | dependencies: 438 | chalk: 4.1.2 439 | chokidar: 3.6.0 440 | esbuild: 0.17.6 441 | fs-extra: 10.1.0 442 | globby: 11.1.0 443 | dev: true 444 | 445 | /esbuild-plugin-sass/1.0.1_esbuild@0.17.6: 446 | resolution: {integrity: sha512-YFxjzD9Z1vz92QCJcAmCO15WVCUiOobw9ypdVeMsW+xa6S+zqryLUIh8d3fe/UkRHRO5PODZz/3xDAQuEXZwmQ==} 447 | peerDependencies: 448 | esbuild: '>=0.11.14' 449 | dependencies: 450 | css-tree: 1.1.3 451 | esbuild: 0.17.6 452 | fs-extra: 10.0.0 453 | sass: 1.47.0 454 | tmp: 0.2.1 455 | dev: true 456 | 457 | /esbuild/0.17.6: 458 | resolution: {integrity: sha512-TKFRp9TxrJDdRWfSsSERKEovm6v30iHnrjlcGhLBOtReE28Yp1VSBRfO3GTaOFMoxsNerx4TjrhzSuma9ha83Q==} 459 | engines: {node: '>=12'} 460 | hasBin: true 461 | requiresBuild: true 462 | optionalDependencies: 463 | '@esbuild/android-arm': 0.17.6 464 | '@esbuild/android-arm64': 0.17.6 465 | '@esbuild/android-x64': 0.17.6 466 | '@esbuild/darwin-arm64': 0.17.6 467 | '@esbuild/darwin-x64': 0.17.6 468 | '@esbuild/freebsd-arm64': 0.17.6 469 | '@esbuild/freebsd-x64': 0.17.6 470 | '@esbuild/linux-arm': 0.17.6 471 | '@esbuild/linux-arm64': 0.17.6 472 | '@esbuild/linux-ia32': 0.17.6 473 | '@esbuild/linux-loong64': 0.17.6 474 | '@esbuild/linux-mips64el': 0.17.6 475 | '@esbuild/linux-ppc64': 0.17.6 476 | '@esbuild/linux-riscv64': 0.17.6 477 | '@esbuild/linux-s390x': 0.17.6 478 | '@esbuild/linux-x64': 0.17.6 479 | '@esbuild/netbsd-x64': 0.17.6 480 | '@esbuild/openbsd-x64': 0.17.6 481 | '@esbuild/sunos-x64': 0.17.6 482 | '@esbuild/win32-arm64': 0.17.6 483 | '@esbuild/win32-ia32': 0.17.6 484 | '@esbuild/win32-x64': 0.17.6 485 | dev: true 486 | 487 | /fast-glob/3.3.2: 488 | resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 489 | engines: {node: '>=8.6.0'} 490 | dependencies: 491 | '@nodelib/fs.stat': 2.0.5 492 | '@nodelib/fs.walk': 1.2.8 493 | glob-parent: 5.1.2 494 | merge2: 1.4.1 495 | micromatch: 4.0.5 496 | dev: true 497 | 498 | /fastq/1.17.1: 499 | resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 500 | dependencies: 501 | reusify: 1.0.4 502 | dev: true 503 | 504 | /fill-range/7.0.1: 505 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 506 | engines: {node: '>=8'} 507 | dependencies: 508 | to-regex-range: 5.0.1 509 | dev: true 510 | 511 | /fs-extra/10.0.0: 512 | resolution: {integrity: sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==} 513 | engines: {node: '>=12'} 514 | dependencies: 515 | graceful-fs: 4.2.11 516 | jsonfile: 6.1.0 517 | universalify: 2.0.1 518 | dev: true 519 | 520 | /fs-extra/10.1.0: 521 | resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} 522 | engines: {node: '>=12'} 523 | dependencies: 524 | graceful-fs: 4.2.11 525 | jsonfile: 6.1.0 526 | universalify: 2.0.1 527 | dev: true 528 | 529 | /fs-write-stream-atomic/1.0.10: 530 | resolution: {integrity: sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==} 531 | dependencies: 532 | graceful-fs: 4.2.11 533 | iferr: 0.1.5 534 | imurmurhash: 0.1.4 535 | readable-stream: 2.3.8 536 | dev: true 537 | 538 | /fs.realpath/1.0.0: 539 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 540 | dev: true 541 | 542 | /fsevents/2.3.3: 543 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 544 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 545 | os: [darwin] 546 | requiresBuild: true 547 | dev: true 548 | optional: true 549 | 550 | /glob-parent/5.1.2: 551 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 552 | engines: {node: '>= 6'} 553 | dependencies: 554 | is-glob: 4.0.3 555 | dev: true 556 | 557 | /glob/6.0.4: 558 | resolution: {integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==} 559 | dependencies: 560 | inflight: 1.0.6 561 | inherits: 2.0.4 562 | minimatch: 3.1.2 563 | once: 1.4.0 564 | path-is-absolute: 1.0.1 565 | dev: true 566 | 567 | /glob/7.2.3: 568 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 569 | dependencies: 570 | fs.realpath: 1.0.0 571 | inflight: 1.0.6 572 | inherits: 2.0.4 573 | minimatch: 3.1.2 574 | once: 1.4.0 575 | path-is-absolute: 1.0.1 576 | dev: true 577 | 578 | /globby/11.1.0: 579 | resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} 580 | engines: {node: '>=10'} 581 | dependencies: 582 | array-union: 2.1.0 583 | dir-glob: 3.0.1 584 | fast-glob: 3.3.2 585 | ignore: 5.3.1 586 | merge2: 1.4.1 587 | slash: 3.0.0 588 | dev: true 589 | 590 | /globby/4.1.0: 591 | resolution: {integrity: sha512-JPDtMSr0bt25W64q792rvlrSwIaZwqUAhqdYKSr57Wh/xBcQ5JDWLM85ndn+Q1WdBQXLb9YGCl0QN/T0HpqU0A==} 592 | engines: {node: '>=0.10.0'} 593 | dependencies: 594 | array-union: 1.0.2 595 | arrify: 1.0.1 596 | glob: 6.0.4 597 | object-assign: 4.1.1 598 | pify: 2.3.0 599 | pinkie-promise: 2.0.1 600 | dev: true 601 | 602 | /graceful-fs/4.2.11: 603 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 604 | dev: true 605 | 606 | /has-flag/4.0.0: 607 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 608 | engines: {node: '>=8'} 609 | dev: true 610 | 611 | /i18next/20.6.1: 612 | resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==} 613 | dependencies: 614 | '@babel/runtime': 7.18.9 615 | dev: true 616 | 617 | /iferr/0.1.5: 618 | resolution: {integrity: sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==} 619 | dev: true 620 | 621 | /ignore/5.3.1: 622 | resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} 623 | engines: {node: '>= 4'} 624 | dev: true 625 | 626 | /immutable/4.3.5: 627 | resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==} 628 | dev: true 629 | 630 | /imurmurhash/0.1.4: 631 | resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 632 | engines: {node: '>=0.8.19'} 633 | dev: true 634 | 635 | /inflight/1.0.6: 636 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 637 | dependencies: 638 | once: 1.4.0 639 | wrappy: 1.0.2 640 | dev: true 641 | 642 | /inherits/2.0.4: 643 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 644 | dev: true 645 | 646 | /is-binary-path/2.1.0: 647 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 648 | engines: {node: '>=8'} 649 | dependencies: 650 | binary-extensions: 2.3.0 651 | dev: true 652 | 653 | /is-extglob/2.1.1: 654 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 655 | engines: {node: '>=0.10.0'} 656 | dev: true 657 | 658 | /is-glob/4.0.3: 659 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 660 | engines: {node: '>=0.10.0'} 661 | dependencies: 662 | is-extglob: 2.1.1 663 | dev: true 664 | 665 | /is-number/7.0.0: 666 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 667 | engines: {node: '>=0.12.0'} 668 | dev: true 669 | 670 | /isarray/1.0.0: 671 | resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} 672 | dev: true 673 | 674 | /jsonfile/6.1.0: 675 | resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} 676 | dependencies: 677 | universalify: 2.0.1 678 | optionalDependencies: 679 | graceful-fs: 4.2.11 680 | dev: true 681 | 682 | /mdn-data/2.0.14: 683 | resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} 684 | dev: true 685 | 686 | /merge2/1.4.1: 687 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 688 | engines: {node: '>= 8'} 689 | dev: true 690 | 691 | /micromatch/4.0.5: 692 | resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} 693 | engines: {node: '>=8.6'} 694 | dependencies: 695 | braces: 3.0.2 696 | picomatch: 2.3.1 697 | dev: true 698 | 699 | /minimatch/3.1.2: 700 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 701 | dependencies: 702 | brace-expansion: 1.1.11 703 | dev: true 704 | 705 | /minimist/1.2.8: 706 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 707 | dev: true 708 | 709 | /mkdirp/0.5.6: 710 | resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} 711 | hasBin: true 712 | dependencies: 713 | minimist: 1.2.8 714 | dev: true 715 | 716 | /moment/2.29.4: 717 | resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} 718 | dev: true 719 | 720 | /monkey-around/2.3.0: 721 | resolution: {integrity: sha512-QWcCUWjqE/MCk9cXlSKZ1Qc486LD439xw/Ak8Nt6l2PuL9+yrc9TJakt7OHDuOqPRYY4nTWBAEFKn32PE/SfXA==} 722 | dev: true 723 | 724 | /normalize-path/3.0.0: 725 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 726 | engines: {node: '>=0.10.0'} 727 | dev: true 728 | 729 | /object-assign/4.1.1: 730 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 731 | engines: {node: '>=0.10.0'} 732 | dev: true 733 | 734 | /obsidian/1.3.5: 735 | resolution: {integrity: sha512-2Zg9vlaEZw6fd2AohcdrC1kV+lZcb4a1Ju6GcIwdWaGOWj6l//7wbKD6vVhO2GlfoQRGARYu++eLo7FEc+f6Tw==} 736 | peerDependencies: 737 | '@codemirror/state': ^6.0.0 738 | '@codemirror/view': ^6.0.0 739 | dependencies: 740 | '@types/codemirror': 5.60.8 741 | moment: 2.29.4 742 | dev: true 743 | 744 | /once/1.4.0: 745 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 746 | dependencies: 747 | wrappy: 1.0.2 748 | dev: true 749 | 750 | /path-is-absolute/1.0.1: 751 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 752 | engines: {node: '>=0.10.0'} 753 | dev: true 754 | 755 | /path-type/4.0.0: 756 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 757 | engines: {node: '>=8'} 758 | dev: true 759 | 760 | /picomatch/2.3.1: 761 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 762 | engines: {node: '>=8.6'} 763 | dev: true 764 | 765 | /pify/2.3.0: 766 | resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} 767 | engines: {node: '>=0.10.0'} 768 | dev: true 769 | 770 | /pinkie-promise/2.0.1: 771 | resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} 772 | engines: {node: '>=0.10.0'} 773 | dependencies: 774 | pinkie: 2.0.4 775 | dev: true 776 | 777 | /pinkie/2.0.4: 778 | resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} 779 | engines: {node: '>=0.10.0'} 780 | dev: true 781 | 782 | /process-nextick-args/2.0.1: 783 | resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} 784 | dev: true 785 | 786 | /queue-microtask/1.2.3: 787 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 788 | dev: true 789 | 790 | /readable-stream/2.3.8: 791 | resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} 792 | dependencies: 793 | core-util-is: 1.0.3 794 | inherits: 2.0.4 795 | isarray: 1.0.0 796 | process-nextick-args: 2.0.1 797 | safe-buffer: 5.1.2 798 | string_decoder: 1.1.1 799 | util-deprecate: 1.0.2 800 | dev: true 801 | 802 | /readdirp/3.6.0: 803 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 804 | engines: {node: '>=8.10.0'} 805 | dependencies: 806 | picomatch: 2.3.1 807 | dev: true 808 | 809 | /regenerator-runtime/0.13.9: 810 | resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} 811 | dev: true 812 | 813 | /reusify/1.0.4: 814 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 815 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 816 | dev: true 817 | 818 | /rimraf/3.0.2: 819 | resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} 820 | hasBin: true 821 | dependencies: 822 | glob: 7.2.3 823 | dev: true 824 | 825 | /run-parallel/1.2.0: 826 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 827 | dependencies: 828 | queue-microtask: 1.2.3 829 | dev: true 830 | 831 | /safe-buffer/5.1.2: 832 | resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} 833 | dev: true 834 | 835 | /sass/1.47.0: 836 | resolution: {integrity: sha512-GtXwvwgD7/6MLUZPnlA5/8cdRgC9SzT5kAnnJMRmEZQFRE3J56Foswig4NyyyQGsnmNvg6EUM/FP0Pe9Y2zywQ==} 837 | engines: {node: '>=8.9.0'} 838 | hasBin: true 839 | dependencies: 840 | chokidar: 3.6.0 841 | immutable: 4.3.5 842 | source-map-js: 1.2.0 843 | dev: true 844 | 845 | /slash/3.0.0: 846 | resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} 847 | engines: {node: '>=8'} 848 | dev: true 849 | 850 | /source-map-js/1.2.0: 851 | resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} 852 | engines: {node: '>=0.10.0'} 853 | dev: true 854 | 855 | /source-map/0.6.1: 856 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 857 | engines: {node: '>=0.10.0'} 858 | dev: true 859 | 860 | /string_decoder/1.1.1: 861 | resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} 862 | dependencies: 863 | safe-buffer: 5.1.2 864 | dev: true 865 | 866 | /supports-color/7.2.0: 867 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 868 | engines: {node: '>=8'} 869 | dependencies: 870 | has-flag: 4.0.0 871 | dev: true 872 | 873 | /tmp/0.2.1: 874 | resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} 875 | engines: {node: '>=8.17.0'} 876 | dependencies: 877 | rimraf: 3.0.2 878 | dev: true 879 | 880 | /to-regex-range/5.0.1: 881 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 882 | engines: {node: '>=8.0'} 883 | dependencies: 884 | is-number: 7.0.0 885 | dev: true 886 | 887 | /to-use/0.3.3: 888 | resolution: {integrity: sha512-i5hrYhcDyrjl4tF2EvZnmL8VAMdMYnMFeX1bIF0ekn2gKgs76UhZcHt+hLIUKh0fIii+Ix6uUp5W6yYlU5+Z8A==} 889 | dev: true 890 | 891 | /universalify/2.0.1: 892 | resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} 893 | engines: {node: '>= 10.0.0'} 894 | dev: true 895 | 896 | /util-deprecate/1.0.2: 897 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 898 | dev: true 899 | 900 | /wonka/6.3.4: 901 | resolution: {integrity: sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg==} 902 | dev: true 903 | 904 | /wrappy/1.0.2: 905 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 906 | dev: true 907 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import builder from "obsidian-rollup-presets"; 2 | 3 | export default builder() 4 | .apply(c => c.output.sourcemap = "inline") 5 | .assign({input: "src/plugin.js"}) 6 | .withInstall(__dirname) 7 | .build(); 8 | -------------------------------------------------------------------------------- /src/hotkey-helper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Events, Plugin, Platform, Keymap, Setting, Modal, Notice, debounce, SettingTab, PluginManifest, 3 | ExtraButtonComponent, Hotkey, Command, SearchComponent 4 | } from "obsidian"; 5 | import {around, serialize} from "monkey-around"; 6 | import {defer, modalSelect, onElement, use, app} from "@ophidian/core"; 7 | import "./obsidian-internals"; 8 | import "../styles.css"; 9 | 10 | interface OldPluginViewer extends Modal { // pre 1.0 11 | autoopen?: string 12 | showPlugin(manifest: PluginManifest): Promise 13 | updateSearch(): any 14 | searchEl: HTMLInputElement 15 | pluginContentEl: HTMLDivElement 16 | } 17 | 18 | interface NewPluginViewer extends Modal { // 1.0+ 19 | setAutoOpen(pluginId: string): this 20 | showItem(manifest: PluginManifest): Promise 21 | updateItems(): void 22 | search: { inputEl: HTMLInputElement } 23 | detailsEl: HTMLDivElement 24 | } 25 | 26 | 27 | function hotkeyToString(hotkey: Hotkey) { 28 | return Keymap.compileModifiers(hotkey.modifiers)+"," + hotkey.key.toLowerCase() 29 | } 30 | 31 | function isPluginTab(id: string) { 32 | return id === "plugins" || id === "community-plugins"; 33 | } 34 | 35 | function pluginSettingsAreOpen() { 36 | return settingsAreOpen() && isPluginTab(app.setting.activeTab?.id) 37 | } 38 | 39 | function settingsAreOpen() { 40 | return app.setting.containerEl.parentElement !== null 41 | } 42 | 43 | function isPluginViewer(ob: any): ob is OldPluginViewer { 44 | return ( 45 | ob instanceof Modal && 46 | ob.hasOwnProperty("autoload") && 47 | typeof (ob as any).showPlugin === "function" && 48 | typeof (ob as any).updateSearch === "function" && 49 | typeof (ob as any).searchEl == "object" 50 | ); 51 | } 52 | 53 | function isNewPluginViewer(ob: any): ob is NewPluginViewer { 54 | return ( 55 | ob instanceof Modal && 56 | typeof (ob as any).setAutoOpen === "function" && 57 | typeof (ob as any).search?.inputEl === "object" 58 | ) 59 | } 60 | 61 | export default class HotkeyHelper extends Plugin { 62 | lastSearch = {} as Record 63 | hotkeyButtons = {} as Record>; 64 | globalsAdded = false; 65 | searchInput: SearchComponent = null; 66 | lastTabId: string; 67 | currentViewer: Modal; 68 | 69 | use = use.plugin(this); 70 | 71 | onload() { 72 | const workspace = this.app.workspace, plugin = this, events = workspace as Events; 73 | this.registerEvent(events.on("plugin-settings:before-display", (settingsTab, tabId) => { 74 | this.hotkeyButtons = {}; 75 | this.globalsAdded = false; 76 | this.searchInput = null; 77 | const remove = around(Setting.prototype, { 78 | addSearch(old) { return function(f) { 79 | remove(); 80 | return old.call(this, (i: SearchComponent) => { 81 | plugin.searchInput = i; f?.(i); 82 | }) 83 | }} 84 | }); 85 | defer(remove); 86 | }) ); 87 | this.registerEvent( events.on("plugin-settings:after-display", () => this.refreshButtons(true)) ); 88 | 89 | this.registerEvent( events.on("plugin-settings:plugin-control", (setting, manifest, enabled, tabId) => { 90 | this.globalsAdded || this.addGlobals(tabId, setting.settingEl); 91 | }) ); 92 | 93 | // Refresh the buttons when commands or setting tabs are added or removed 94 | const requestRefresh = debounce(this.refreshButtons.bind(this), 50, true); 95 | function refresher(old: (...args: any[]) => any ) { 96 | return function(...args: any[]){ requestRefresh(); return old.apply(this, args); }; 97 | } 98 | this.register(around(app.commands, {addCommand: refresher, removeCommand: refresher})); 99 | this.register(around(app.setting, {addSettingTab: refresher, removeSettingTab: refresher})); 100 | 101 | workspace.onLayoutReady(this.whenReady.bind(this)); 102 | this.registerObsidianProtocolHandler("goto-plugin", ({id, show}) => { 103 | workspace.onLayoutReady(() => { this.gotoPlugin(id, show); }); 104 | }); 105 | } 106 | 107 | whenReady() { 108 | const app = this.app, plugin = this; 109 | const cmdPalette = app.internalPlugins.plugins["command-palette"]?.instance?.modal; 110 | 111 | if (cmdPalette) { 112 | this.register(around(cmdPalette, { 113 | onChooseItem(old) { 114 | return function oci(cmd, e) { 115 | if (Keymap.isModEvent(e)) { 116 | defer(() => plugin.showHotkeysFor(cmd.name)); 117 | return false; 118 | } 119 | return old.call(this, cmd, e) 120 | }; 121 | } 122 | })); 123 | const first = cmdPalette.modalEl.find(".prompt-instructions .prompt-instruction"); 124 | if (first) { 125 | first.parentNode.insertBefore( 126 | createDiv("prompt-instruction", d => { 127 | d.createSpan({ 128 | cls: "prompt-instruction-command", text: Keymap.compileModifiers(["Mod"])+"+↵" 129 | }); 130 | d.appendText(" "); 131 | d.createSpan({text: "to configure hotkey(s)"}) 132 | this.register(() => d.detach()); 133 | }), 134 | null 135 | ) 136 | } 137 | } 138 | 139 | const corePlugins = this.getSettingsTab("plugins"); 140 | const community = this.getSettingsTab("community-plugins"); 141 | 142 | // Hook into the display() method of the plugin settings tabs 143 | if (corePlugins) this.register(around(corePlugins, {display: this.addPluginSettingEvents.bind(this, corePlugins.id)})); 144 | if (community) this.register(around(community, {display: this.addPluginSettingEvents.bind(this, community.id)})); 145 | 146 | const enhanceViewer = () => this.enhanceViewer(); 147 | 148 | if (community) this.register( 149 | // Trap opens of the community plugins viewer from the settings panel 150 | onElement( 151 | community.containerEl, "click", 152 | ".mod-cta, .installed-plugins-container .setting-item-info", 153 | enhanceViewer, 154 | true 155 | ) 156 | ); 157 | 158 | // Trap opens of the community plugins viewer via URL 159 | this.register( 160 | around(app.workspace.protocolHandlers, { 161 | get(old) { 162 | return function get(key: string) { 163 | if (key === "show-plugin") enhanceViewer(); 164 | return old.call(this, key); 165 | } 166 | } 167 | }) 168 | ) 169 | 170 | // Now force a refresh if either plugins tab is currently visible (to show our new buttons) 171 | function refreshTabIfOpen() { 172 | if (pluginSettingsAreOpen()) app.setting.openTabById(app.setting.activeTab.id); 173 | } 174 | refreshTabIfOpen(); 175 | 176 | // And do it again after we unload (to remove the old buttons) 177 | this.register(() => defer(refreshTabIfOpen)); 178 | 179 | // Tweak the hotkey settings tab to make filtering work on id prefixes as well as command names 180 | const hotkeysTab = this.getSettingsTab("hotkeys") as SettingTab & {updateHotkeyVisibility(): void }; 181 | if (hotkeysTab) { 182 | this.register(around(hotkeysTab, { 183 | display(old) { return function() { old.call(this); (this.searchInputEl ?? this.searchComponent.inputEl)?.focus(); }; }, 184 | updateHotkeyVisibility(old) { 185 | return function() { 186 | const searchInputEl = (this.searchInputEl ?? this.searchComponent.inputEl); 187 | if (!searchInputEl) return old.call(this); 188 | const oldSearch = searchInputEl.value, oldCommands = app.commands.commands; 189 | try { 190 | if (oldSearch.endsWith(":") && !oldSearch.contains(" ")) { 191 | // This is an incredibly ugly hack that relies on updateHotkeyVisibility() iterating app.commands.commands 192 | // looking for hotkey conflicts *before* anything else. 193 | let current = oldCommands; 194 | let filtered = Object.fromEntries(Object.entries(app.commands.commands).filter( 195 | ([id, cmd]) => (id+":").startsWith(oldSearch) 196 | )); 197 | searchInputEl.value = ""; 198 | app.commands.commands = new Proxy(oldCommands, {ownKeys(){ 199 | // The first time commands are iterated, return the whole thing; 200 | // after that, return the filtered list 201 | try { return Object.keys(current); } finally { current = filtered; } 202 | }}); 203 | } 204 | return old.call(this); 205 | } finally { 206 | searchInputEl.value = oldSearch; 207 | app.commands.commands = oldCommands; 208 | } 209 | } 210 | } 211 | })); 212 | } 213 | 214 | // Add commands 215 | this.addCommand({ 216 | id: "open-plugins", 217 | name: "Open the Community Plugins settings", 218 | callback: () => this.showSettings("community-plugins") || true 219 | }); 220 | this.addCommand({ 221 | id: "browse-plugins", 222 | name: "Browse or search the Community Plugins catalog", 223 | callback: () => this.gotoPlugin() 224 | }) 225 | const alphaSort = new Intl.Collator(undefined, {usage: "sort", sensitivity: "base", numeric: true}).compare; 226 | this.addCommand({ 227 | id: "open-settings", 228 | name: "Open settings for plugin...", 229 | callback: async () => { 230 | const {item} = await modalSelect( 231 | app.setting.pluginTabs.concat(app.setting.settingTabs).sort((a, b) => alphaSort(a.name, b.name)), 232 | t => t.name, 233 | "Select a plugin to open its settings...", 234 | ); 235 | if (item) { 236 | this.showSettings(item.id); 237 | } 238 | } 239 | }); 240 | this.addCommand({ 241 | id: "open-hotkeys", 242 | name: "Open hotkeys for plugin...", 243 | callback: async () => { 244 | const commandsByPlugin = this.refreshCommands(); 245 | const plugins = Object.values(app.plugins.plugins) 246 | .map(p => p.manifest as Partial) 247 | .concat( 248 | Object.entries(app.internalPlugins.plugins) 249 | .map( 250 | ([id, {instance: {name}, _loaded:enabled}]) => {return {id, name, enabled};} 251 | ) 252 | .filter(p => p.enabled) 253 | ) 254 | .concat([ 255 | {id: "app", name: "App"}, 256 | {id: "editor", name: this.getSettingsTab("editor")?.name || "Editor"}, 257 | {id: "workspace", name: this.getSettingsTab("file")?.name || "Files & Links"} 258 | ]) 259 | .filter(m => commandsByPlugin[m.id]?.length); 260 | ; 261 | const {item} = await modalSelect( 262 | plugins.sort((a, b) => alphaSort(a.name, b.name)), 263 | t => t.name, 264 | "Select a plugin to open its hotkeys..."); 265 | if (item) { 266 | this.showHotkeysFor(item.id+":"); 267 | } 268 | } 269 | }); 270 | } 271 | 272 | createExtraButtons(setting: Setting, manifest: {id: string, name: string}, enabled: boolean) { 273 | if (manifest.id !== "app") setting.addExtraButton(btn => { 274 | btn.setIcon("gear"); 275 | btn.onClick(() => this.showConfigFor(manifest.id.replace(/^workspace$/,"file"))); 276 | btn.setTooltip("Options"); 277 | btn.extraSettingsEl.toggle(enabled) 278 | }); 279 | setting.addExtraButton(btn => { 280 | btn.setIcon("any-key"); 281 | btn.onClick(() => this.showHotkeysFor(manifest.id+":")) 282 | btn.extraSettingsEl.toggle(enabled) 283 | this.hotkeyButtons[manifest.id] = btn; 284 | }); 285 | } 286 | 287 | // Add top-level items (search and pseudo-plugins) 288 | addGlobals(tabId: string, settingEl: HTMLDivElement) { 289 | this.globalsAdded = true; 290 | 291 | // Add a search filter to shrink plugin list 292 | const containerEl = settingEl.parentElement; 293 | let searchEl: SearchComponent; 294 | if (tabId !== "plugins" || this.searchInput) { 295 | // Replace the built-in search handler 296 | (searchEl = this.searchInput)?.onChange(changeHandler); 297 | } else { 298 | const tmp = new Setting(containerEl).addSearch(s => { 299 | searchEl = s; 300 | s.setPlaceholder("Filter plugins...").onChange(changeHandler); 301 | }); 302 | searchEl.containerEl.style.margin = "0"; 303 | containerEl.createDiv("hotkey-search-container").append(searchEl.containerEl); 304 | tmp.settingEl.detach(); 305 | } 306 | if (tabId === "community-plugins") { 307 | searchEl.inputEl.addEventListener("keydown", e => { 308 | if (e.keyCode === 13 && !Keymap.getModifiers(e)) { 309 | this.gotoPlugin(); 310 | return false; 311 | } 312 | }) 313 | } 314 | const plugin = this; 315 | function changeHandler(seek: string){ 316 | const find = (plugin.lastSearch[tabId] = seek).toLowerCase(); 317 | function matchAndHighlight(el: HTMLElement) { 318 | if (!el) return false; 319 | const text = el.textContent = el.textContent; // clear previous highlighting, if any 320 | const index = text.toLowerCase().indexOf(find); 321 | if (!~index) return false; 322 | el.textContent = text.substr(0, index); 323 | el.createSpan("suggestion-highlight").textContent = text.substr(index, find.length); 324 | el.insertAdjacentText("beforeend", text.substr(index+find.length)) 325 | return true; 326 | } 327 | containerEl.findAll(".setting-item").forEach(e => { 328 | const nameMatches = matchAndHighlight(e.find(".setting-item-name")); 329 | const descMatches = matchAndHighlight( 330 | e.find(".setting-item-description > div:last-child:not(.plugin-comment)") ?? 331 | e.find(".setting-item-description > div:nth-child(3):not(.plugin-comment)") 332 | ); 333 | const authorMatches = matchAndHighlight( 334 | e.find(".setting-item-description > div:nth-child(2)") 335 | ); 336 | e.toggle(nameMatches || descMatches || authorMatches); 337 | }); 338 | } 339 | defer(() => { 340 | if (!searchEl) return 341 | if (searchEl && typeof plugin.lastSearch[tabId] === "string") { 342 | searchEl.setValue(plugin.lastSearch[tabId]); 343 | searchEl.onChanged(); 344 | } 345 | if (!Platform.isMobile) searchEl.inputEl.select(); 346 | }); 347 | containerEl.append(settingEl); 348 | 349 | if (tabId === "plugins") { 350 | const editorName = this.getSettingsTab("editor")?.name || "Editor"; 351 | const workspaceName = this.getSettingsTab("file")?.name || "Files & Links"; 352 | this.createExtraButtons( 353 | new Setting(settingEl.parentElement) 354 | .setName("App").setDesc("Miscellaneous application commands (always enabled)"), 355 | {id: "app", name: "App"}, true 356 | ); 357 | this.createExtraButtons( 358 | new Setting(settingEl.parentElement) 359 | .setName(editorName).setDesc("Core editing commands (always enabled)"), 360 | {id: "editor", name: editorName}, true 361 | ); 362 | this.createExtraButtons( 363 | new Setting(settingEl.parentElement) 364 | .setName(workspaceName).setDesc("Core file and pane management commands (always enabled)"), 365 | {id: "workspace", name: workspaceName}, true 366 | ); 367 | settingEl.parentElement.append(settingEl); 368 | } 369 | } 370 | 371 | enhanceViewer() { 372 | const plugin = this; 373 | setTimeout(around(Modal.prototype, { 374 | open(old) { 375 | return function(...args) { 376 | if (isNewPluginViewer(this)) { 377 | defer(() => { 378 | if (plugin.lastSearch["community-plugins"]) { 379 | this.search.inputEl.value = plugin.lastSearch["community-plugins"]; 380 | this.search.inputEl.dispatchEvent(new Event('input')); 381 | } 382 | }); 383 | plugin.currentViewer = this; 384 | around(this, { 385 | close(old) { return function(...args: any[]) { 386 | plugin.currentViewer = null; 387 | return old.apply(this, args); 388 | }}, 389 | 390 | showItem(old) { return async function(manifest: PluginManifest){ 391 | const res = await old.call(this, manifest); 392 | if (plugin.app.plugins.plugins[manifest.id]) { 393 | const hotkeysName = i18next.t("setting.hotkeys.name"); 394 | const buttons = this.detailsEl.find("button").parentElement; 395 | for (const b of buttons.findAll("button")) { 396 | if (b.textContent === hotkeysName) { 397 | plugin.hotkeyButtons[manifest.id] = { 398 | setTooltip(tip) {b.title = tip; return this; }, extraSettingsEl: b 399 | } 400 | }; 401 | } 402 | plugin.refreshButtons(true); 403 | } 404 | return res; 405 | }} 406 | }) 407 | } 408 | // Pre 1.0 409 | if (isPluginViewer(this)) { 410 | defer(() => { 411 | if (plugin.lastSearch["community-plugins"]) { 412 | // Detach the old search area, in case the empty search is still running 413 | const newResults = this.searchResultEl.cloneNode(); 414 | this.searchContainerEl.replaceChild(newResults, this.searchResultEl); 415 | this.searchResultEl = newResults; 416 | // Force an update; use an event so that the "x" appears on search 417 | this.searchEl.value = plugin.lastSearch["community-plugins"]; 418 | this.searchEl.dispatchEvent(new Event('input')); 419 | } 420 | this.searchEl.select(); 421 | }); 422 | plugin.currentViewer = this; 423 | around(this, { 424 | updateSearch: serialize, // prevent race conditions 425 | 426 | close(old) { return function(...args: any[]) { 427 | plugin.currentViewer = null; 428 | return old.apply(this, args); 429 | }}, 430 | 431 | showPlugin(old) { return async function(manifest: PluginManifest){ 432 | const res = await old.call(this, manifest); 433 | if (plugin.app.plugins.plugins[manifest.id]) { 434 | const hotkeysName = i18next.t("setting.hotkeys.name"); 435 | const buttons = this.pluginContentEl.find("button").parentElement; 436 | for (const b of buttons.findAll("button")) { 437 | if (b.textContent === hotkeysName) { 438 | plugin.hotkeyButtons[manifest.id] = { 439 | setTooltip(tip) {b.title = tip; return this; }, extraSettingsEl: b 440 | } 441 | }; 442 | } 443 | plugin.refreshButtons(true); 444 | } 445 | return res; 446 | }} 447 | }) 448 | } 449 | return old.apply(this, args); 450 | } 451 | } 452 | }), 0); 453 | } 454 | 455 | getSettingsTab(id: string) { 456 | return app.setting.settingTabs.filter(t => t.id === id).shift() as SettingTab & {name: string}; 457 | } 458 | 459 | addPluginSettingEvents(tabId: string, old: SettingTab["display"]) { 460 | const app = this.app, plugin = this; 461 | let in_event = false; 462 | 463 | function trigger(name: string, ...args: any[]) { 464 | in_event = true; 465 | try { app.workspace.trigger(name, ...args); } catch(e) { console.error(e); } 466 | in_event = false; 467 | } 468 | 469 | // Wrapper to add plugin-settings events 470 | return function display(...args: any[]) { 471 | if (in_event) return; 472 | trigger("plugin-settings:before-display", this, tabId); 473 | 474 | // Track which plugin each setting is for 475 | let manifests: {id: string, name: string, enabled?: boolean}[]; 476 | if (tabId === "plugins") { 477 | manifests = Object.entries(app.internalPlugins.plugins).map( 478 | ([id, {instance: {name, hiddenFromList}, _loaded:enabled}]) => {return !hiddenFromList && {id, name, enabled};} 479 | ).filter(m => m); 480 | } else { 481 | manifests = Object.values(app.plugins.manifests); 482 | } 483 | manifests.sort((e, t) => e.name.localeCompare(t.name)); 484 | let which = 0, currentId = ""; 485 | 486 | // Trap the addition of the "uninstall" buttons next to each plugin 487 | const remove = around(Setting.prototype, { 488 | addExtraButton(old) { 489 | return function(cb) { 490 | // The only "extras" added to settings w/a description are on the plugins, currently, 491 | // so only try to match those to plugin names 492 | if (!in_event && (tabId === "plugins" || this.descEl.childElementCount) && (manifests[which]||{}).name === this.nameEl.textContent) { 493 | const manifest = manifests[which++]; 494 | currentId = manifest.id; 495 | trigger("plugin-settings:plugin-control", this, manifest, manifest.enabled, tabId); 496 | } 497 | return old.call(this, function(b: ExtraButtonComponent) { 498 | cb(b); 499 | // Add key count/conflict indicators to built-in key buttons 500 | if (!in_event && b.extraSettingsEl.find("svg.any-key, svg.lucide-plus-circle") && currentId) { 501 | plugin.hotkeyButtons[currentId] = b; 502 | b.onClick(plugin.showHotkeysFor.bind(plugin, currentId+":")); 503 | } 504 | }); 505 | } 506 | } 507 | }); 508 | 509 | try { 510 | return old.apply(this, args); 511 | } finally { 512 | remove(); 513 | trigger("plugin-settings:after-display", this); 514 | } 515 | } 516 | } 517 | 518 | gotoPlugin(id?: string, show="info") { 519 | if (id && show === "hotkeys") return this.showHotkeysFor(id+":"); 520 | if (id && show === "config") { 521 | if (!this.showConfigFor(id)) this.app.setting.close(); 522 | return; 523 | } 524 | 525 | if (!this.showSettings("community-plugins")) return; 526 | const remove = around(Modal.prototype, { 527 | open(old) { 528 | return function(...args) { 529 | remove(); 530 | if (id) { this.autoload = id; this.setAutoOpen?.(id); } 531 | return old.apply(this, args); 532 | } 533 | } 534 | }) 535 | this.app.setting.activeTab.containerEl.find(".mod-cta").click(); 536 | // XXX handle nav to not-cataloged plugin 537 | } 538 | 539 | showSettings(id: string) { 540 | this.currentViewer?.close(); // close the plugin browser if open 541 | settingsAreOpen() || app.setting.open(); 542 | if (id) { 543 | if (app.setting.activeTab?.id !== id) app.setting.openTabById(id); 544 | return app.setting.activeTab?.id === id ? app.setting.activeTab : false 545 | } 546 | } 547 | 548 | showHotkeysFor(search: string) { 549 | const tab = this.showSettings("hotkeys"); 550 | if (tab && (tab.searchInputEl ?? tab.searchComponent.inputEl) && tab.updateHotkeyVisibility) { 551 | (tab.searchInputEl ?? tab.searchComponent.inputEl).value = search; 552 | tab.updateHotkeyVisibility(); 553 | } 554 | } 555 | 556 | showConfigFor(id: string) { 557 | if (this.showSettings(id)) return true; 558 | new Notice( 559 | `No settings tab for "${id}": it may not be installed or might not have settings.` 560 | ); 561 | return false; 562 | } 563 | 564 | pluginEnabled(id: string) { 565 | return app.internalPlugins.plugins[id]?._loaded || app.plugins.plugins[id]; 566 | } 567 | 568 | commandsByPlugin = {} as Record; 569 | assignedKeyCount = {} as Record; 570 | 571 | refreshCommands() { 572 | const hkm = app.hotkeyManager; 573 | this.assignedKeyCount = {}; 574 | return this.commandsByPlugin = Object.values(app.commands.commands).reduce((cmds, cmd)=>{ 575 | const pid = cmd.id.split(":",2).shift(); 576 | const hotkeys = (hkm.getHotkeys(cmd.id) || hkm.getDefaultHotkeys(cmd.id) || []).map(hotkeyToString); 577 | hotkeys.forEach(k => this.assignedKeyCount[k] = 1 + (this.assignedKeyCount[k]||0)); 578 | (cmds[pid] || (cmds[pid]=[])).push({hotkeys, cmd}); 579 | return cmds; 580 | }, {} as Record); 581 | } 582 | 583 | refreshButtons(force=false) { 584 | // Don't refresh when not displaying, unless rendering is in progress 585 | if (!pluginSettingsAreOpen() && !force) return; 586 | 587 | // Get a list of commands by plugin 588 | this.refreshCommands(); 589 | 590 | // Plugin setting tabs by plugin 591 | const tabs = Object.values(app.setting.pluginTabs).reduce((tabs, tab)=> { 592 | tabs[tab.id] = tab; return tabs 593 | }, {} as Record); 594 | tabs["workspace"] = tabs["editor"] = true; 595 | 596 | for(const id of Object.keys(this.hotkeyButtons || {})) { 597 | const btn = this.hotkeyButtons[id]; 598 | if (!this.commandsByPlugin[id] || app.internalPlugins.plugins[id]?.enabled === false) { 599 | // Plugin is disabled or has no commands 600 | btn.extraSettingsEl.hide(); 601 | continue; 602 | } 603 | const assigned = this.commandsByPlugin[id].filter(info => info.hotkeys.length); 604 | const conflicts = assigned.filter(info => info.hotkeys.filter(k => this.assignedKeyCount[k]>1).length).length; 605 | 606 | btn.setTooltip( 607 | `Configure hotkeys${"\n"}(${assigned.length}/${this.commandsByPlugin[id].length} assigned${ 608 | conflicts ? "; "+conflicts+" conflicting" : "" 609 | })` 610 | ); 611 | btn.extraSettingsEl.toggleClass("mod-error", !!conflicts); 612 | btn.extraSettingsEl.show(); 613 | } 614 | } 615 | } 616 | -------------------------------------------------------------------------------- /src/obsidian-internals.ts: -------------------------------------------------------------------------------- 1 | import {i18n} from "i18next"; 2 | 3 | declare module "obsidian" { 4 | namespace Keymap { 5 | function compileModifiers(mods: string[]): string 6 | function getModifiers(event: MouseEvent|KeyboardEvent): string 7 | } 8 | interface App { 9 | plugins: Plugins 10 | commands: Commands 11 | internalPlugins: InternalPluginsManager 12 | setting: SettingsManager 13 | hotkeyManager: HotKeyManager 14 | } 15 | 16 | interface Workspace { 17 | protocolHandlers: Map 18 | } 19 | 20 | interface Commands { 21 | commands: Record; 22 | addCommand(cmd: Command): void; 23 | removeCommand(cmd: Command): void; 24 | } 25 | 26 | interface HotKeyManager { 27 | getHotkeys(id: string): Hotkey[]; 28 | getDefaultHotkeys(id: string): Hotkey[]; 29 | } 30 | 31 | interface SettingsManager { 32 | activeTab: SettingTab | null; 33 | openTabById(id: string): SettingTab | null; 34 | openTab(tab: SettingTab): void; 35 | open(): void; 36 | close(): void; 37 | onOpen(): void; 38 | onClose(): void; 39 | settingTabs: SettingTab[]; 40 | pluginTabs: SettingTab[]; 41 | addSettingTab(): void; 42 | removeSettingTab(): void; 43 | containerEl: HTMLDivElement; 44 | } 45 | 46 | interface SettingTab { 47 | id: string 48 | name: string 49 | searchInputEl?: HTMLInputElement; // XXX should be subtypes for hotkey and plugin tabs 50 | searchComponent?: { 51 | inputEl?: HTMLInputElement; 52 | } 53 | updateHotkeyVisibility?(): void; 54 | } 55 | 56 | interface SearchComponent { 57 | containerEl: HTMLDivElement; 58 | } 59 | 60 | interface Plugins { 61 | manifests: Record; 62 | plugins: Record; 63 | 64 | enablePlugin(pluginId: string): Promise; 65 | disblePlugin(pluginId: string): Promise; 66 | } 67 | 68 | interface InternalPluginsManager { 69 | getEnabledPlugins(): InternalPlugin[]; 70 | getPluginById(id: string & keyof InternalPlugins): InternalPlugin 71 | plugins: InternalPlugins & Record> 72 | } 73 | 74 | interface InternalPlugin extends Component { 75 | /** The actual internal plugin object (state and methods). */ 76 | instance: InternalPluginInstance; 77 | enabled: boolean; 78 | _loaded: boolean; 79 | } 80 | 81 | interface InternalPlugins { 82 | "command-palette": InternalPlugin<{modal: FuzzySuggestModal}> 83 | } 84 | 85 | type InternalPluginInstance = T & { 86 | name: string 87 | hiddenFromList: boolean 88 | } 89 | 90 | type ViewFactory = (leaf: WorkspaceLeaf) => View 91 | } 92 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .clickable-icon.mod-error, 2 | .modal .community-plugin-info button.mod-error, 3 | .modal-container .mod-community-plugin .community-modal-button-container button.mod-error { 4 | background-color: var(--background-modifier-error); 5 | } 6 | 7 | .community-plugin-info > p > button { margin-top: 6px; margin-bottom: 6px;} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "esModuleInterop": true, 7 | "module": "ESNext", 8 | "target": "ES2018", 9 | "allowJs": true, 10 | "noImplicitAny": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "jsx": "react", 14 | "jsxFactory": "el", 15 | "lib": [ 16 | "dom", 17 | "es2019", 18 | "scripthost" 19 | ] 20 | }, 21 | "exclude": ["dist"], 22 | "include": [ 23 | "src/*.ts", "src/*.tsx", 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.3.20": "1.5.8", 3 | "0.3.18": "1.2.8", 4 | "0.3.17": "1.1.16", 5 | "0.3.16": "0.15.9", 6 | "0.3.11": "0.13.19", 7 | "0.3.8": "0.12.3", 8 | "0.3.0": "0.12.1", 9 | "0.2.1": "0.11.13", 10 | "0.1.1": "0.11.3" 11 | } --------------------------------------------------------------------------------