Teaching Emacs to recognize Jira tickets and show them in a browser using Hyperbole implicit buttons
It's difficult to describe the Emacs package Hyperbole succinctly, so I'm not even going to try 😅. I was looking for a quick way of opening Jira tickets from an Org mode file, and Hyperbole was the way. There are YouTube videos and articles on Hyperbole, but I found very little describing in one place what I wanted to do. Hopefully others might find this useful.
As well as developing my own apps, such as beorg, I work as a freelancer for a number of clients; three of my clients use Jira. The way I work is that each month I create a new file for that client and list tickets I'm to work on, record time against said tickets for the purposes of billing and make notes. This reduces the amount of time I need to spend in Jira itself.
A Jira ticket has the format PROJECT-ID. For example, CORE-1234 is a ticket in the project CORE.
I wanted to be able to put the cursor on a ticket reference, and open the ticket in my browser. I could have added a URL for the ticket, but then I wouldn't have had all this Hyperbole fun 🎢.
Hyperbole allows you to define implicit buttons. These turn any text into a button. Emacs does some pattern matching to extract the needed information and then does something. In my case I wanted Emacs to identify that the cursor is on a Jira ticket ID and then construct a URL for that ticket and open in my web browser. To support Jira tickets I need to define a new type of implicit button. When I started doing this I got confused and added the Emacs Lisp to my personal button file - this is not the place however if you want to define a new implicit button type!
The Emacs Lisp to define my new implicit button type is as follows:
(defvar my/jira-cs-browse-url "https://example.atlassian.net/browse/")
(defun my/jira-cs-reference (jira-id)
"Open ticket in CS Jira"
(let ((url (concat my/jira-cs-browse-url jira-id)))
(browse-url-default-browser url)))
(defib my/jira-cs ()
"Get the Jira ticket identifier at point and load ticket in browser"
(let ((case-fold-search t)
(jira-id nil)
(jira-regex "\\(CORE-[0-9]+\\)"))
(if (or (looking-at jira-regex)
(save-excursion
(skip-chars-backward "0-9")
(skip-chars-backward "-")
(skip-chars-backward "CORE")
(looking-at jira-regex)))
(progn (setq jira-id (match-string-no-properties 1))
(ibut:label-set jira-id
(match-beginning 1)
(match-end 1))
(hact 'my/jira-cs-reference jira-id)))))
I'm not going to go over exactly what the above code does, there are lots of introductions to Emacs Lisp available, however here are the important points to note:
- The function defining the implicit button is created using
defib, and notdefun. - This function,
my/jira-cs:- Looks to see if there is text matching a regular expression directly after the cursor or by moving backwards some specific characters.
- If there is, then the captured text is assigned to the variable jira-id (otherwise we don't continue).
- Then the text region which matched the regular expression is flashed, to help the user see that the Jira ticket ID was matched.
- Followed by running the function
my/jira-cs-reference. When defining an implicit button it is important that the action taken is executed viahact- otherwise you'll get an error in the mini-buffer.
- An action function is defined,
my/jira-cs-reference, to load the URL in the default web browser.
To test the button make sure you are in a buffer with the mode emacs-lisp-mode enabled, and then do M-x eval-buffer. This will run the code and create the implicit button. Pressing M-RET on text such as CORE-1234, in any buffer (not just an Org mode file), will open the Jira ticket in a web browser. Being able to do this from any buffer is a glimpse as to why Hyperbole can be so powerful.
You'll now need to permanently install your new button type. I'm using the Doom Emacs framework, so I'm going to add my implicit button type to ~/.doom.d/config.el. If you are using vanilla Emacs or another framework you'll know where to add this for your setup. Here is how the relevant section of my config.el file looks:
(use-package! hyperbole
:init
(hyperbole-mode)
:config
(defvar my/jira-cs-browse-url "https://example.atlassian.net/browse/")
(defun my/jira-cs-reference (jira-id)
"Open ticket in CS Jira"
(let ((url (concat my/jira-cs-browse-url jira-id)))
(browse-url-default-browser url)))
(defib my/jira-cs ()
"Get the Jira ticket identifier at point and load ticket in browser"
(let ((case-fold-search t)
(jira-id nil)
(jira-regex "\\(CORE-[0-9]+\\)"))
(if (or (looking-at jira-regex)
(save-excursion
(skip-chars-backward "0-9")
(skip-chars-backward "-")
(skip-chars-backward "CORE")
(looking-at jira-regex)))
(progn (setq jira-id (match-string-no-properties 1))
(ibut:label-set jira-id
(match-beginning 1)
(match-end 1))
(hact 'my/jira-cs-reference jira-id)))))
)
I enjoy writing Emacs Lisp, but do it very rarely - so the above took me some time to get right. The main sticking points were ensuring that hact was used to run the button action - and my mistake in adding the Emacs Lisp to the personal button file. Once I'd worked this out the rest was easy.
The Hyperbole documentation recommends looking at the file hibtypes.el. I managed to load this by bringing up the Emacs help for Hyperbole (SPC h f hyperbole), activating the link hui-mini.el and then using find-file (SPC . in Doom) to get to hibtypes.el (I'm sure there is an easier way!) There is lots to look at here, but for me I would have found a shorter example, such as I've detailed in this article, a great help in defining my first button!
Installing Hyperbole
If, like me, you are using the Doom framework then just add the following to your ~/.doom.d/packages.el file:
(package! hyperbole)
and this to your ~/.doom.d/config.el file:
(use-package! hyperbole
:init
(hyperbole-mode))
If you are using vanilla Emacs then check out the installation guide in the Hyperbole documentation itself.