WhichKey / Hydra makeshift in Obsidian

Published on:2024/12/16
Banner for WhichKey / Hydra makeshift in Obsidian

Introduction

What are we talking about

Whichkey or Hydra is a dynamic, interactive key binding and command discovery system that displays available keystrokes and commands in real-time based on the current context or partially entered key sequence.

That's a mouthful, what does it mean?

It provides a hierarchical view of complex keybindings allowing:

  1. To have key "sequences", where shorcuts are triggered with more than one modifer + key.
    For instance: something like: space + a + b without having to hold space
  2. "Teaching yourself" the commands by interactively providing the available sequences.
    For instance: you can remember that date related commands are in space + d and Hydra will display all the related commands. Pressing t could insert today's date while pressing m could insert tomorrow's date. Next time, you can just press space + d + t.

Let's look at the most popular implementations from 2 massive editor ecosystems

Which-key for Neovim

Which-key in neovim

Hydra for Emacs

Hydra in emacs

in Obsidian

First let's take a look at what my attempt, HydraShard, looks like:
(This takes after a random theme I installed for testing purposes)

Hydrashard in obsidian Pretty convincing for a quick hack.

We will go over how I met these few requirements:

  • allow for keychords/key sequences
  • display whichkey/hydra style context during the key sequence
  • programmaticaly assign keystrokes to commands

The last point is an issue I have with Obsidian coming from emacs/neovim where each template or command has to be registered as a command and then bound to a key manually in GUI's.

Honorable mentions

Obsidian-sequence-hotkeys

  • Very powerful, but no programmatic assignment as far as I can tell
  • The functions have to be "palette commands" which is not a requirement for HydraShard

Obsidian vimrc

I'm sure there are a lot of interesting idea there but I am avoid emulating vim these days.

Usage

As per the last article, we'll make heavy use of templater user scripts but anything should be possible without it. First add the HydraShard script to the script folder.

Get it from here.

HydraShard relies on being called using templater as an "insert", not a new file. Then this the only binding necessary to set up in GUI.
If you bind your hydra-powered template to ctrl+n all the subsequent keys defined in HydraShard will be ctrl+n+a, ctrl+n+b etc.

HydraShard, as any Templater user script can be called with

tp.user.hydra(menuitems)

Where menuitems is expected to be an object of the following shape:

  let menuitems = [
    {
      key: "f",
      name: "Name to be displayed here",
      action: async () => {
          let var = await tp.user.someModule();
          return var;
      },
    },
{...}
  ]

Each element has a key to trigger, name to be displayed and function to be called upon being selected.
You can capture the returned value from the tp.user.hydra() call such as:

Output

let result = tp.user.hydra(menuitems)
console.log(result.result) // to get the captured output
console.log(result.item) // to get the selected option

Structure and trigger

Then there's a few ways to go about it but here is my favourite: by defining as much as possible in JS and using the shortest template possible.

(pass around the tp object as a context to be able to us the templater API and other user scripts)

function myNestedShortcuts(tp) {
  let menuitems = [
    {
      key: "t",
      name: "Add Tag",
      action: async () => {
        const tag = await tp.system.prompt("Enter tag:");
        // Do something with it
        console.log(`Tag added: ${tag}`);
        return tag;
      },
    },
  ];
  let result = tp.user.hydra(menuitems);
  return result;
}
module.exports = myNestedShortcuts;

then in the template, this is the only content needed:

<%*
let result = tp.user.myNestedShortcuts(tp)
%>

Note that nothing stops you from defining the menuitems object in templater instead.
You can also implement more logic using the returned values from Hydra.

If your setup is not too long to define you can also define your function and your menuitems in the same template.

Conclusion

I am very happy with the results for such a hacky solutions.
I can define a lot of key-sequence based shortcuts in JS without having to register them as commands and it has been pretty performant too.

Be aware of certain limitations since it's Templater based such as the need to be in a note, HydraShard does not work in an empty tab or even in Excalidraw.

Going further

Adding an optional timeout: currently pressing any non-binded key cancels it out so pressing 'escape' works pretty well.

Better html: I imagine there are more proper ways to add GUI elements to Obsidian and use native components.

Mobile version: The command palette can be horrible to use on mobile the little I tried it. Maybe there's a smart way to trigger it from within the app or even through URI to get a touch-based command menu of sort.

Register as an API instead of a templater script: it could simplify installation by registering in some app.plugins.plugins['hydra'].hydra() instead of Templater's space but that probably wouldn't change much the functionality at this point.