I saw a few posts on r/emacs about Hyperbole, including some videos, and I -put it in a tab- save the URL to look at it later. When I finally got down to it and watched some of the videos and read some of the documentation... well I was intrigued but still confused on what it was and how I could use it. So I installed it and have used it for a while. I have to say I like it and it has improved my workflow, but I don't know if I can describe Hyperbole succinctly. So I'm writing this up, in case someone else is confused and perhaps I can get some clarity as to what the Hyperbole package is.
Let me make a quick disclaimer -- there is a lot of functionality in Hyperbole and I'm only covering the things I have found useful in the few weeks I've been experimenting with. There are things that I keep uncovering (even in the middle of writing this post).
The Magic Key Combination
Hyperbole introduces the key combo of M-return
that becomes magic. The easiest way to describe it is "M-return
does what you want in the context that you are currently in`". Hyperbole calls this the Smart Key and it's assigned both the "Action Key" and the "Assist Key". I'm assuming they did this because you can configure the "Magic Key" to be what you want, and the Smart and Assist Keys could be different. But what do I mean about "does what you want"? Here are examples from the Hyperbole documentation.:
Smart Key - Magit Mode
If pressed within a Magit buffer and not on a button:
ACTION KEY
(1) on the last line, quit from the magit mode ({q} key binding);
(2) at the end of a line, scroll up a windowful;
(3) on an initial read-only header line, cycle visibility of diff sections;
(4) anywhere else, hide/show the thing at point ({TAB} key binding)
unless that does nothing in the mode, then jump to the thing at point
({RET} key binding) but display based on the value of
hpath:display-where.
ASSIST KEY
(1) on the last line, quit from the magit mode ({q} key binding);
(2) at the end of a line, scroll down a windowful;
(3) on an initial read-only header line, cycle visibility of all sections;
(4) anywhere else, jump to the thing at point ({RET} key binding)
but display based on the value of hpath:display-where."
q
Smart Key - Org Mode
When in an Org mode context and hsys-org-enable-smart-keys is non-nil:
ACTION KEY
(1) If on an Org todo keyword, cycle through the keywords in
that set or if final done keyword, remove it.
(2) If on an Org agenda view item, jump to the item for editing.
(3) Within a radio or internal target or a link to it, jump between
the target and the first link to it, allowing two-way navigation.
(4) Follow other internal links and ID references in Org mode files.
(5) Follow Org mode external links.
(6) When on a Hyperbole button, activate the button.
(7) With point on the :dir path of a code block definition, display the
directory given by the path.
(8) With point on any #+BEGIN_SRC, #+END_SRC, #+RESULTS, #+begin_example
or #+end_example header, execute the code block via the Org mode
standard binding of {C-c C-c}, org-ctrl-c-ctrl-c.
(9) When point is on an Org mode heading, cycle the view of the subtree
at point.
(10) In any other context besides the end of a line, invoke the Org mode
standard binding of {M-RET}, org-meta-return.
So these are just examples from the docs but it goes with what I am saying:
- Are you in a Magit buffer online that can cycle visibility? M-return
will do that.
- Are you at the end of Magit buffer? Then M-return
will quit the buffer
- Are you on a OrgMode Todo item? Then M-return
will change status
- Want to execute code in a org-src section? Get on the begin_src
and hit M-return
But probably the most important things that M-return
executes are buttons -- both Implicit and Explicit.
Button Pushing
I think Buttons are the most important concept in Hyperbole and yet they took me a while to get my brain around. They aren't really that confusing but they are abstract concepts because a button can be literally anything. I will present some ways I have utilized them that have helped my workflow more in a short time than I would have thought. It's easier to start with Implicit Buttons and then talk about Explicit ones.
Implicit Buttons
These are by far the easiest ones to understand... you have some in your codebase now! It's pretty basic - this, for example, is an Implicit Button:
~/Projects/my-project/ReadMe.md
Just put your cursor on that file path, hit M-return
and if the file exists, it will open it in Emacs. "Wait," you say, "that's just a text filepath?" And you are right! This is what makes it implicit because it's just text link! Hyperbole tries to open up or follow any string that looks like a filepath. But what if just used a directory?
~/Projects/my-project
If you hit M-return
on that, it will open up Dired in that folder, if the path exists! So, as I said above, M-return
does what you expect it to do.
So "Well great, hyperlinks! So sorta like a wiki." and that is true! But wait... there is more! Let say you have this:
"!poetry run pytest"
So that seems like a shell command... and you are 100% correct! If you put your cursor between the quotes (which are important), hit M-return
, Hyperbole will start a terminal in Emacs (ansi-term, in my case), cd
to the folder that file is in, and run the command. But here is another one, with <
>
.
<describe-function 'describe-function>
"Wait, " you think, "is that Elisp?" Yes, yes it is. Hit M-return
and watch the magic happen. Yes you can run arbitrary Elisp functions with Hyperbole anywhere in your file. This opens up a lot of possibilities. I quickly came up with a solution to something that always causes me some frustration: in Django, a View can return an HTML template, but that template file could be under the current directory or in the main template folder. So there can be a journey to figure out where it is. So I used Direnv to set TEMPLATE_PATH
to a list of locations where my templates could be:
export TEMPLATE_PATH=$HOME/Projects/my-project/project/templates:$HOME/Projects/my-project/apps/app1/templates
and then I wrote this little Elisp function
(defun mh/dj-open-template-file (filename)
"Use Emacs to find and open a file named FILENAME under TEMPLATE_PATH."
(interactive "sEnter filename to search for: ")
(let ((template-path (getenv "TEMPLATE_PATH")))
(if template-path
(let* ((files (directory-files-recursively template-path (regexp-quote filename)))
(file-to-open (car files))) ; Just take the first match
(if file-to-open
(find-file-other-window file-to-open)
(message "File not found under %s" template-path)))
(message "TEMPLATE_PATH environment variable is not set"))))
Now in my Django views, I can put in this implicit button:
# <mh/dj-open-template-file 'home.html'>
return render(request, "home.html")
Then I can hit M-return
in that comment, and another window opens in the frame with the home.html
open. Similarly, you can do keystrokes:
{C-x 3 C-x o }
So that will just simply run those keystrokes to split vertically and switch to that other window.
Explicit Buttons
The Explicit Buttons are similar only they are formal and look like buttons. They are really made to run Elisp function easily (or, at least that is what I think). To make a button, you have to use the Hyperbole menu, which I haven't had to mention yet. C-h h e
will bring up the Explicit Button Menu. From there, c
will be created. I will use my mh/dj-open-template-file
as an example.
So I would create a button with a label called home
because that is the file that I want to load. Next, I would choose my elisp function (mh/dj-open-template-file
) and then it will ask me for a parameter (because the function requires one) and I put in home.html
. Then where I was in the file says to write <(home)>
. There! That is the Explicit Button! Now, anywhere in that file, you can type <(home)>
that button will be a way to run mh/dj-open-template-file
to find home.html
. Note that I said anywhere in that file... from what I have found the explicit buttons only exist in the current file. So if you want to use the same explicit button in another file, you have to re-create it with the same process. It's because of this I find explicit buttons to be less useful than implicit ones (why remake the same buttons over and over again?).
From a utility standpoint, the information about the explicit button is written to $PWD/.hypb
in the same directory. It's not editable (and not meant to be) but that enables the buttons to persist after you restart Emacs.
Window/Frame control
This is something that I didn't think I would use much, but I found that I really like it. C-h h s w
will bring up a menu to tweak all kinds of things about the current window -- size, position in the frame, change frame, and more! The similar C-h h s f
then you have a similar thing for the frame. I find the Frame controls a lot more useful than the Window one. You can also create a grid of windows in the frame with the @
A lot is going on in this so check out the docs. But what is nice about this is that it makes small adjustments very quick and easy without taking your hands off the keyboard. Also makes it easy to load the file in a new frame, which is a command I can never remember.
Window Config
This name is similar to the above but it's very different. In essence you can save the opened buffers and the window formation to use later. So, if you are like me and get 3 different windows just so and then mess that all up when debugging, this is the fix for you! You can save it into a "window-ring" (that is my term) where you can just pop off the configs. Or you can save it by name and then just restore it by name later. You get to this by C-h h w
.
There is a lot to be said when you have a bunch of windows saved in a circular ring and you just pop them off until you find the right one. Or save one by name so you can get at it again later. The big caveat to this, however, is that it doesn't persist. So you can set up a bunch of saved windows but lose them when you restart Emacs. Generally not a huge deal but I was excited to save some window configs in my Emacs configuration but no cigar.
My Config
There is a lot that I liked here and some that I didn't. And then there are some that I tweaked. :)
One of the things that didn't work is M-return
didn't follow the definition in my Python code. I figured out that it had to do with Hyperbole assuming that I was using Etags and my Prelude config is using anaconda-mode
. I did some digging and figured out what the function in Hyperbole is smart-python-tag
and then I put some elisp advice around it:
(defun mh/override-smart-python-tag (original-function &rest args)
"Conditionally override `smart-python-tag` to use `anaconda-mode-find-definitions` in Python mode."
;; Check if we're in python-mode and anaconda-mode is active
(if (and (eq major-mode 'python-mode)
(bound-and-true-p anaconda-mode))
(call-interactively 'anaconda-mode-find-definitions) ;; Call anaconda-mode-find-definitions interactively
(apply original-function args))) ;; Otherwise, call the original function
(advice-add 'smart-python-tag :around #'mh/override-smart-python-tag)
Now M-return
works in Python like expected.
The next things are less drastic but also important to me. I liked the functionality with Windows and Frames but I didn't want to have to dig into the Hyperbole menu each time (C-h h w s
to save a window? No thanks!). Of course, this is Emacs so I can configure things how I want. I do like Magit's Transient interface (and Hyperbole's menus essentially work the same way) I decided to dip my toes into making my own Tranient menu. And it was a lot easier than I thought:
(transient-define-prefix mh/hyperbole-transient ()
["Hyperbole commands"
("s" "save window" hywconfig-ring-save)
("y" "yank window" hywconfig-yank-pop)
("w" "window control" hycontrol-enable-windows-mode)
("f" "frame control" hycontrol-enable-frames-mode)
])
(global-set-key (kbd "C-h j") 'mh/hyperbole-transient)
Yes, I assigned it to C-h j
, because it's close to Hyperbole. And it was an open keystroke on my system. Now when I use that, I have a list of Hyperbole options that I use the most.
Related, I wanted to be able to save a lot of windows and just move through them. Both my Transient and the Hyperbole menus quit after you get a window off the ring. What I needed was a Hydra -- something I had tried before but couldn't figure out a use. But I did now!
(defhydra mh/window-config-hydra (:color blue :hint nil)
"
^Window Configurations^
-----------------------
_s_: Save Window Config _y_: Yank Window Config
"
("s" hywconfig-ring-save "save" )
("y" hywconfig-yank-pop "yank" :color red))
(global-set-key (kbd "C-c m") 'mh/window-config-hydra/body)
I have a hard time finding open key sequences but C-c m
is fast and was unused. Now I can simply type that and hit y
until I find the window that I want.
You can see my Hyperbole specific things (including my two Django-based functions I use for buttons) on my Gitlab page. You can look at the rest of my configuration as well, if that is useful.
Epilogue
When I started this post, I didn't plan on a 2,000+ words about Hyperbole but there is so much to talk about and there are things that I didn't even touch on like Koutliner, which is an outliner system sorta like OrgMode, and Hyrolo is a record management system (think Rolodex but apparently for anything). Most of these are because I haven't needed to dive into it.
If nothing else, I think the Hyperbole is a big package but you can use whatever piece you want. I use Hyperbole as a way to reduce some cognitive load while working, like making it easy to link filenames to their locations, quickly jump between windows, etc.