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!