Customizing Pasted Strings in Emacs

(A version of this post was posted earlier, but disappeared. Sorry for any inconvenience.)

The Problem

At my day job, we use the Jira ticketing system, so it’s very useful to link to tickets in Org-mode files, to the point where I’ve defined a shortcut, [[jira:ABC-12345]], and an org capture template for tickets. But sometimes, I’ll copy a link from an email message or something, and C-y to paste it in Emacs, and then I get an ugly

 

https://jira.example.com/ticket/ABC-12345

That got me to wondering if I could rewrite the text before pasting it, and turn that URL into

The Transform Function

We’re going to need a function to do the transformation, so let’s get that out of the way. Here’s a function that takes a string to be yanked from the kill buffer, and returns the string that we actually want. The only thing to note is that I used cond instead of if because I’ll probably want to expand it later, as I find new patterns I want to transform.

(defun my-org-convert-links (str)
  "Given a string to yank, try to convert it to org format."
  ;; Use `cond' rather than `if' to make it easier to add other
  ;; patterns.
  (cond
   ;; Example Jira URL. Extract the ticket number.
   ((string-match
     "^https://jira.example.com/ticket/\\([A-Z]+-[0-9]+\\)$"
     str)
    (let ((ticket (substring str
                             (match-beginning 1)
                             (match-end 1))))
      (format "[[jira:%s][%s]]" ticket ticket)
      ))

   ;; Insert other transformations here. Whatever's useful for you.

   ;; Nothing special. Just return the string.
   (t str))
  )

Hooking into yank

In an ordinary buffer, the Emacs function for pasting is yank, as you can confirm with M-x describe-key C-y, or whatever you’ve bound it to. And if you look up its documentation, you’ll see that it can be customized using yank-transform-functions. This is a list of functions that are called with the string to be yanked. We’ve already written a transform function above, so all we need to do is add it to yank-transform-functions:

(add-to-list 'yank-transform-functions
             #'my-org-convert-links)

In this case, the syntax [[jira:ABC-123456][ABC-123456]] doesn’t make sense outside of Org-Mode buffers, so let’s make it specific to org-mode buffers: we’ll make yank-transform-functions buffer-local, and only add our function to it in Org-Mode buffers:

(add-hook 'org-mode-hook
  (lambda nil
    (make-variable-buffer-local 'yank-transform-functions)
    (add-to-list 'yank-transform-functions
                 #'my-org-convert-links)
    ))

Now, the more astute among you will have noticed that in Org-Mode, C-y is bound to org-yank, which performs other org-specific magic before pasting. Thankfully, it winds up calling the built-in yank, so the customization above still works.

And that’s about it! I can now have Jira links look pretty when I paste them into my Org-Mode notes, and a structure where I can easily add more transformations if and when I want them.

I’ll note that yanking is different from drag-and-drop, and is handled differently from Emacs. I want to set up something similar for that, but that’ll be a story for a different day.

On Emacs Theme Precedence

I’ve written before about themes in Emacs, and in particular about using them for something other than just colors. And naturally, you can have more than one theme active at the same time. So what happens when you have a variable set in two active themes?

The list of enabled themes is the custom-enabled-themes variable. And rather intuitively, earlier ones override later ones. Normally, (load-theme 'mytheme) prepends mytheme to custom-enabled-themes, so the settings you just loaded take effect, and last until you load some other settings on top of them, which makes sense.

There’s one twist, though: the settings you change through the “Customize Emacs” system also count as a theme, at least in some ways. If you look at the property list for a symbol defined in two themes, you might see something like

(symbol-plist 'my-var)

(theme-value
 ((user 'customized-value)
  (second-theme 'second-value)
  (first-theme 'first-value))
 saved-value 'customized-value
 ...)

This tells us that two themes, first-theme and second-theme, have both defined my-var, and set it to first-value and second-value respectively. The symbol’s value is resolved in the order in which it appears here, so second-value overrides first-value because it was loaded later.

But there’s that third user entry, which comes first, and thus overrides the other values: that comes from (customize-variable 'my-var). I find this counterintuitive, since I think of the customization variables as the defaults, to be overridden by themes.

The solution I came up with this was to define my own theme, arensb, to hold all of the settings that might be overridden by other themes, and to load it first. Actually, I already had this theme lying around to store my favorite colors and faces, but themes can hold other values, so it made sense to reuse it.

Monthly Reports with Org-Mode

Like a lot of people, I have to submit a monthly “bullet” report, listing the things I’ve done in the previous month.

Since I use Org-Mode for planning, scheduling, and organizing tool (or rather: I tend to throw a bunch of notes into a file and tell this love child of a day planner and a wiki to tell me what I should do next), I figured I should use that.

I could use the timeline feature (C-c a L), but that only works for the current buffer, and I want a report that covers all buffers, just like the agenda.

What I’ve done in the past is to use C-c a a to get the agenda view, go back a month, toggle displaying completed/archived/whatever items, and go through that to make my bullet list.

But I finally got around to encapsulating that into a single M-x bullet command:

; Make it easier to generate bullets for $BOSS
(defvar bullet-entry-types
  '(:closed)
  "Org-mode agenda types that we want to see in the monthly bullet report
See `org-agenda-entry-types'."
  )

(defun bullets ()
  "Show a list of achievements for the past month, for monthly reports.
Uses `org-agenda'.
"
  (interactive)
  (require 'org-agenda)
  ; All we're doing here, really, is calling `org-agenda' with
  ; arguments giving a start date and a number of days. But to do
  ; that, we need to figure out
  ; - the date of the first of last month
  ; - the number of days in last month
  (let* ((now (current-time))
	 ; Figure out when last month was. Assuming that I run this
	 ; close to the beginning of a month, then `now' minus two
	 ; weeks was some time in the previous month. We can use that
	 ; to extract the year and month that we're interested in.
	 (2weeks-ago
	  (time-subtract now
			 (days-to-time 14)))
	 ; We'll also need to know when the first of this month was,
	 ; to find out how long last month was. If today is the 12th
	 ; of the month, then the first of the month was `now' minus
	 ; 11 days.
	 (1st-of-this-month
	  (time-subtract now
			 (days-to-time
			  (- (nth 3 (decode-time now))
			     1))))
	 ; Ditto to find the first of last month.
	 (1st-of-last-month
	  (time-subtract 2weeks-ago
			 (days-to-time
			  (- (nth 3 (decode-time 2weeks-ago))
			     1))))
	 ; The length of last month is the difference (in days)
	 ; between the first of last month, and the first of this
	 ; month.
	 (len-last-month
	  (time-to-number-of-days
	   (time-subtract 1st-of-this-month
			  1st-of-last-month)))
	 (start-date (decode-time 1st-of-last-month))
	 (start-year (nth 5 start-date))	; Year number
	 (start-mon (nth 4 start-date))		; Month number
	 ; Restrict the agenda to only those types of entries we're
	 ; interested in. I think this takes advantage of dynamic
	 ; scoping, which is normally an abomination unto the lord,
	 ; but is useful here.
	 (org-agenda-entry-types bullet-entry-types)
	 )
    ; Create an agenda with the stuff we've prepared above
    (org-agenda-list nil
		     (format "%04d-%02d-01"
			     start-year
			     start-mon)
		     len-last-month)
    ))

I hope this proves useful to someone.