Category Archives: emacs

Making a numpad-based hydra for categorizing Org list items

I 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.

Comparison of Helm and Hydra approaches

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!

2019-06-03 Emacs news

Links from reddit.com/r/emacs, /r/orgmode, /r/spacemacs, /r/planetemacs, Hacker News, planet.emacslife.com, YouTube, the changes to the Emacs NEWS file, and emacs-devel.

2019-05-27 Emacs news

Links from reddit.com/r/emacs, /r/orgmode, /r/spacemacs, /r/planetemacs, Hacker News, planet.emacslife.com, YouTube, the changes to the Emacs NEWS file, and emacs-devel.

2019-05-20 Emacs news

Links from reddit.com/r/emacs, /r/orgmode, /r/spacemacs, /r/planetemacs, Hacker News, planet.emacslife.com, YouTube, the changes to the Emacs NEWS file, and emacs-devel.

Adding :target option for the TOC keyword in Org Mode

Now that A- can be counted on to happily play with a babysitter for several hours once a week, I’ve decided to alternate consulting and personal projects. Two weeks ago, I used my personal time to make a script that renewed my library books automatically. This week, I set aside time to look at Org Mode. DC had asked me to update the patch I made to allow people to specify a target for the table of contents, and I was curious about whether I could hack something together.

Patch for adding :target to TOC keyword

Here’s a sample file that shows what I mean:

#+OPTIONS: toc:nil
* Not this section
** Heading X
** Heading Y
* Target
  :PROPERTIES:
  :CUSTOM_ID: TargetSection
  :END:
** Heading A
** Heading B
* Another section
#+TOC: headlines 1 :target "Target"

Here’s the core of how to make it work for HTML exports:

(defun org-html-keyword (keyword _contents info)
  "Transcode a KEYWORD element from Org to HTML.
CONTENTS is nil.  INFO is a plist holding contextual information."
  (let ((key (org-element-property :key keyword))
  (value (org-element-property :value keyword)))
    (cond
     ((string= key "HTML") value)
     ((string= key "TOC")
      (let ((case-fold-search t))
  (cond
   ((string-match "\\<headlines\\>" value)
    (let ((depth (and (string-match "\\<[0-9]+\\>" value)
          (string-to-number (match-string 0 value))))
    (scope
     (cond
      ;; link
      ((string-match ":target +\"\\([^\"]+\\)\"" value)
       (let ((link (with-temp-buffer
         (save-excursion
           (insert (org-make-link-string (match-string 1 value))))
         (org-element-link-parser))))
         (pcase (org-element-property :type link)
           ((or "custom-id" "id") (org-export-resolve-id-link link info))
           ("fuzzy" (org-export-resolve-fuzzy-link link info))
           (_ nil))))
      ;; local
      ((string-match-p "\\<local\\>" value) keyword))))
      (org-html-toc depth info scope)))
   ((string= "listings" value) (org-html-list-of-listings info))
   ((string= "tables" value) (org-html-list-of-tables info))))))))

It was a lot of fun Doing the Right Thing(s): writing documentation, adding tests, and making it work for more than just HTML export. I found out where to make the changes by using grep to search for TOC in the Org Mode source code. All the heavy lifting was already done by org-export-collect-headlines, so it was just a matter of passing the right scope. It took me a while to figure out that I needed to pass an Org link element. An easy way of making that element work for both fuzzy and ID-specific links was to insert the target text into a temporary buffer (remembering to use org-make-link-string) and then calling org-element-link-parser.

I tried figuring out how to make it work with a link to another file, but I didn’t get very far, so I figured I’d just wrap things up nicely there.

I wasn’t sure if my original post made it through because I sent it through Gmane and Cc:d DC, who got it with an empty To:, so I ended up submitting it twice. I just realized I forgot to add test-ox-ascii.el. I don’t want to spam the list, so I’ll send that along with other changes if people have feedback.

But look! Open source contributions! I’m so excited. I wonder what I’ll get to do in two weeks from now. =)

2019-05-13 Emacs news

Update: Added link to Paris meetup on May 21.

Links from reddit.com/r/emacs, /r/orgmode, /r/spacemacs, /r/planetemacs, Hacker News, planet.emacslife.com, YouTube, the changes to the Emacs NEWS file, and emacs-devel.