Making a numpad-based hydra for categorizing Org list items
| emacs, orgI like to categorize links for Emacs News so that it’s not an overwhelmingly long wall of text. After I’ve deleted duplicate links, there are around a hundred links left to categorize. I used to use Helm and some custom code to simplify moving Org list items into different categories in the same list. Then I can type “org” to move something to the Org Mode category and “dev” to move something to the Emacs development category.
When I don’t have a lot of computer time, I usually do this categorization by SSHing into my server from my phone. It’s hard to type on my phone, though. I thought a numpad-based Hydra might be better for quick entry, like a phone system. I wanted to be able to use the numeric keypad to sort items into the most common categories, with a few shortcuts for making it easier to organize. Here’s a list of shortcuts:
| 0-9 | Select options from the list, with 0 for other. |
| \/ | Opens the URL in a web browser |
| , | Selects a category by name, creating as needed |
| \* | Shows the URL in the messages buffer, and toggles it |
| – | Deletes the item |
| . | Quits |
Here’s what the Helm version looked like on the left, and here’s the new numpad-powered one on the right. I liked how fewer buttons made it easier to hit the right one when I’m sorting on my phone. I can add new categories with completion. Because I assigned numbers to specific categories instead of having them automatically calculated based on the headings in the list, it was easy to get into the rhythm of tapping 6 for Org, 7 for coding, and so on.

Here’s the code that makes it happen. I experimented with dynamically defining a hydra using eval and defmacro so that I could more easily define the menu in a variable. It seems to work fine so far.
(defvar my/org-categorize-emacs-news-menu
'(("0" . "Other")
("1" . "Emacs Lisp")
("2" . "Emacs development")
("3" . "Emacs configuration")
("4" . "Appearance")
("5" . "Navigation")
("6" . "Org Mode")
("7" . "Coding")
("8" . "Community")
("9" . "Spacemacs")))
(defun my/org-move-current-item-to-category (category)
"Move current list item under CATEGORY earlier in the list.
CATEGORY can be a string or a list of the form (text indent regexp).
Point should be on the next line to process, even if a new category
has been inserted."
(interactive "MCategory: ")
(when category
(let* ((beg (line-beginning-position))
(end (line-end-position))
(string (org-trim (buffer-substring-no-properties beg end)))
(category-text (if (stringp category) category (elt category 0)))
(category-indent (if (stringp category) 2 (+ 2 (elt category 1))))
(category-regexp (if (stringp category) category (elt category 2)))
(pos (point))
s)
(delete-region beg (min (1+ end) (point-max)))
(unless (string= category-text "x")
(if (re-search-backward category-regexp nil t)
(forward-line 1)
(setq s (concat "- " category-text "\n"))
(insert s)
(setq pos (+ (length s) pos)))
(insert (make-string category-indent ?\ )
string "\n")
(goto-char (+ pos (length string) category-indent 1))
(recenter)))))
(eval
`(defhydra my/org-categorize-emacs-news (global-map "C-c e")
,@(mapcar
(lambda (x)
`(,(car x)
(lambda () (interactive) (my/org-move-current-item-to-category ,(concat (cdr x) ":")))
,(cdr x)))
my/org-categorize-emacs-news-menu)
(","
(lambda () (interactive)
(my/org-move-current-item-to-category
(completing-read (match-string 2) (my/org-get-list-categories))))
"By string")
("/" (lambda () (interactive)
(save-excursion
(re-search-forward org-link-bracket-re)
(backward-char)
(org-open-at-point)))
"Open")
("*"
(lambda () (interactive)
(if (string= (buffer-name) "*Messages*")
(bury-buffer)
(save-excursion
(re-search-forward org-link-bracket-re)
(message (match-string 1)))
(switch-to-buffer "*Messages*")))
"Show URL")
("-" kill-whole-line "Kill")
("." nil "Done")))
Let’s see how it holds up next week!