New link features in org 9

| categories: orgmode | tags:

Org 9.0 is finally out, and it comes with a totally overhauled link capability! This post documents some of those capabilities. These new capabilities include support for:

  1. Custom link faces
  2. Custom tooltips on links
  3. More consistent interface for completion
  4. Special keymaps on links
  5. Customized folding of bracketed links

This post will make more sense with this video: https://www.youtube.com/watch?v=5haX95nk02E

1 A colored link with a static tooltip.

All links in elisp are now defined with org-link-set-parameters.

(org-link-set-parameters
 "red"
 :follow (lambda (path) (message "You clicked me."))
 :export (lambda (path desc backend)
           (cond
            ((eq 'html backend)
             (format "<font color=\"red\">%s</font>"
                     (or desc path)))))
 :face '(:foreground "red")
 :help-echo "Click me for a message.")

A link that is colored red. In bracketed form: A link with a description.

You can change a parameter like this.

(org-link-set-parameters
 "red"
 :face '(:foreground "red" :underline t))

A link that is colored red and underlined.

You can use any face for a link. Here we define one and use it.

(defface org-link-green
  '((t (:inherit org-link :foreground "green")))
  "A green link.")

(org-link-set-parameters
   "green"
   :follow (lambda (path) (message "You clicked me."))
   :export (lambda (path desc backend)
     (cond
      ((eq 'html backend)
       (format "<font color=\"green\">%s</font>"
               (or desc path)))))
   :face 'org-link-green
   :help-echo "Click me for a message.")

A link works.

2 A colored link with a static tooltip and no folding.

This link will be shown in full unfolded form even when other links are folded in descriptive format.

(org-link-set-parameters
 "red-full"
 :follow (lambda (path) (message "You clicked me."))
 :export (lambda (path desc backend)
           (cond
            ((eq 'html backend)
             (format "<font color=\"red\">%s</font>"
                     (or desc path)))))
 :face '(:foreground "red")
 :display 'full
 :help-echo "Click me for a message.")

A link that is colored red. In bracketed form: A link with a description. This regular bracketed doi is still folded.

3 A dynamic tooltip

You can make tooltips dynamic. The function must take these arguments (window object position), and construct the tooltip from that information. Here we show what the cursor is on and the point that it is on. \(x^2\)

(defun redd-tip (window object position)
  (save-excursion
    (goto-char position)
    (goto-char (org-element-property :begin (org-element-context)))
    (cond ((looking-at org-plain-link-re)
           (format "Looking at %s with mouse at %s" (match-string 0) position))
          ((looking-at org-bracket-link-regexp)
           (format "Looking at %s in a bracketed link with mouse at %s"
                   (match-string 0) position))
          (t
           "No match"))))

(org-link-set-parameters
 "redd"
 :face '(:underline t)
 :help-echo 'redd-tip)

A link with a dynamic tooltip: link or this one another-link bracketed redd

4 A better file link

Say you want to get a menu of options for file links. For example to find the file, open it in dired, copy the link, etc… We use helm here to make that happen.

(org-link-set-parameters
 "file"
 :follow (lambda (path)
           (funcall
            (helm :sources
                  `((name . "Action")
                    (candidates . ,(append
                                    (loop for f in '(find-file
                                                     org-open-file)
                                          collect (cons (symbol-name f) f))
                                    '(("dired" . (lambda (path)
                                                   (dired (file-name-directory path))
                                                   (re-search-forward (file-name-nondirectory path))))
                                      ("copy org link" . (lambda (path)
                                                           (kill-new (format "[[file:%s]]" path)))))))
                    (action . identity)))
            path)))

5 A link with a new keymap.

To get a special keymap, we have to create a new keymap. We can make a copy of org-mouse-map and add new keys to it that are specific to this link. With this link, you can use arrow-keys with a modifier key to jump between links. We define C-left and C-right to go to the previous and next links, and for fun a C-up and super-mouse-1 bindings that are in effect only on the links.

(defun prev-link ()
  (interactive)
  (re-search-backward "keym:" nil t))

(defun next-link ()
  (interactive)
  (re-search-forward "keym:" nil t))

(org-link-set-parameters
 "keym"
 :follow (lambda (path)
           (interactive)
           (message "You followed me."))
 :keymap (let ((map (copy-keymap org-mouse-map)))
           (define-key map (kbd "C-<left>") 'prev-link)
           (define-key map (kbd "C-<right>") 'next-link)
           (define-key map (kbd "C-<up>")
             (lambda ()
               (interactive)(message-box "special C-up")))
           (define-key map [s-mouse-1]
             (lambda ()
               (interactive)
               (message-box "s-Followed")))
           map))

one then two and finally three

6 A completion example with a dynamic face for validation

This example shows how to add a completion function, and use a dynamic face to show when a bad link has been made (in this case there are 4 allowed fruits, and anything else should be red.

(defun my-comp (&optional arg)
  (format "fruit:%s"
          (completing-read "Choose a fruit: " '("apple" "orange" "grapes" "kiwi"))))

(defun fruit-link-face (path)
  (if (member path '("apple" "orange" "grapes" "kiwi"))
      'org-link
    '(:foreground "red")))

(defun fruit-tooltip (_win _obj position)
  (save-match-data
    (save-excursion
      (goto-char position)
      (let ((path (org-element-property :path (org-element-context))))
        (if (member path '("apple" "orange" "grapes" "kiwi"))
            "A fruit"
          (format "%s: Illegal value. Must be one of apple, orange, grapes or kiwi."
                  path))))))

(org-link-set-parameters "fruit"
                         :help-echo 'fruit-tooltip
                         :face 'fruit-link-face
                         :complete 'my-comp)

apple an orange in brackets

a bad grapefruit. kiwi

kiwi

7 A store link example

A store link example Put your cursor on a headline, and type C-c l. Then move it and type C-c C-l to insert the link.

(defun store-my-headline ()
  (when (and (eq major-mode 'org-mode)
             (org-at-heading-p))
    (org-store-link-props
     :type "head"
     :link (format "head:*%s" (nth 4 (org-heading-components)))
     :description (nth 4 (org-heading-components)))))

(defun follow-head (path)
  (org-open-link-from-string (format "[[%s]]" path)))

(org-link-set-parameters
 "head" :follow 'follow-head :store 'store-my-headline)

8 An activate-func example

You may want to do some additional things when a link is activated. For example, maybe it makes sense for different parts of the link to have different actions, or colors. Here is an example where we make an rgb link of three numbers, and color each number, and make the link color dynamic.

We make a keymap so C-up increments a color, and C-down decrements a color.

(require 'color)

(defun rgb-face (path)
  (let* ((f (split-string path ","))
         (red (/ (string-to-number (nth 0 f)) 255.0))
         (green (/ (string-to-number (nth 1 f)) 255.0))
         (blue (/ (string-to-number (nth 2 f)) 255.0))
         (hex (color-rgb-to-hex red green blue)))
    (list :foreground hex)))


(defun rgb-func (start end path bracketp)
  (save-excursion
    (goto-char start)
    (save-match-data
      (cl-loop for num in (split-string path ",")
               for face in (list '(:foreground "red")
                                 '(:foreground "green")
                                 '(:foreground "blue"))
               do
               (progn
                 (re-search-forward num end t)
                 (add-text-properties
                  (match-beginning 0)
                  (match-end 0)
                  (list 'face face)))))))

(defun ninc ()
  (interactive)
  (skip-chars-backward "0-9")
  (or (looking-at "[0-9]+")
      (error "No number at point"))
  (replace-match (number-to-string (1+ (string-to-number (match-string 0))))))


(defun NINC ()
  (interactive)
  (let* ((link (org-element-context))
         (path (org-element-property :path link))
         (beg (org-element-property :begin link))
         (end (org-element-property :end link))
         (rgb (mapcar 'string-to-number (split-string path ","))))
    (setq rgb (mapcar (lambda (x) (+ x 10)) rgb))
    (setf (buffer-substring beg end)
          (format "rgb:%s" (mapconcat 'identity (mapcar 'number-to-string rgb) ",")))))

(defun NDEC ()
  (interactive)
  (let* ((link (org-element-context))
         (path (org-element-property :path link))
         (beg (org-element-property :begin link))
         (end (org-element-property :end link))
         (rgb (mapcar 'string-to-number (split-string path ","))))
    (setq rgb (mapcar (lambda (x) (- x 10)) rgb))
    (setf (buffer-substring beg end)
          (format "rgb:%s" (mapconcat 'identity (mapcar 'number-to-string rgb) ",")))))


(defun ndec ()
  (interactive)
  (skip-chars-backward "0-9")
  (or (looking-at "[0-9]+")
      (error "No number at point"))
  (replace-match (number-to-string (1- (string-to-number (match-string 0))))))

(org-link-set-parameters "rgb" :face 'rgb-face
                         :activate-func 'rgb-func
                         :keymap (let ((map (copy-keymap org-mouse-map)))
                                   (define-key map (kbd "C-<up>") 'ninc)
                                   (define-key map (kbd "C-<down>") 'ndec)
                                   (define-key map (kbd "s-<up>") 'NINC)
                                   (define-key map (kbd "s-<down>") 'NDEC)
                                   map))

83,29,238 This is a violet color. 112,17,19

This is an rgb link with three comma separated numbers. We color each number accordingly, and set the rgb link to the color represented by the RGB pair.

225,225,225 This is a light gray.

A subtle point in this example is the need to save-match-data. Some functions modify the match-data, and this will mess up the whole font-lock system. I learned that by trial and error.

Copyright (C) 2016 by John Kitchin. See the License for information about copying.

org-mode source

Org-mode version = 9.0

Discuss on Twitter