Monday, July 18, 2022

Emacs for Project Notes: Quick Capture, Automatic Organization

I noticed a pattern during work video calls: I found myself regularly sharing my screen and opening Notepad to capture notes and for use as a team whiteboard. While useful during the call, the result was a jumble of files with notes.txt somewhere in the name.

It occurred to me that I needed a better system. I wanted an effortless way to capture notes that had archival organization baked in. With a bit of elisp I was able to get emacs to tackle this job.

Let's walk through my solution.

I set up a new keyboard mapping for Control-x t that invokes the function project-notes-find-file-today. This function visits a file that's relevant to what I'm currently working on within emacs. Here's the code to set up the keyboard shortcut:

(defun my-minimal-dev-keys-hook ()
  (local-set-key (kbd "C-x t p") 'browse-project-resource-url)
  (local-set-key (kbd "C-x t n") 'project-notes-find-file-today)
  (local-set-key (kbd "C-x t s") 'ag-project-regexp)
  (local-set-key (kbd "C-x t D") 'define-word-at-point))

Here's the definition of project-notes-find-file-today. It starts by figuring out a few key facts:

  • What's the base note taking directory?
  • What's today's date in YYYY-MM-DDD format?
  • What's the name of the source code branch that I'm currently looking at?
  • What's the name of the customer associated with these files?

Once I know this information, it's straightforward to call find-file on a path that looks like so:

  [Notes Directory]/[Customer]/[Branch]/[Today's Date].md

This arrangements organizes my notes by customer, project and date and seems like a reasonable balance between creating too many and too few files.

(defun project-notes-find-file-today ()
  "Quickly open up a notes file or create one for today."
  (let* ((base "~/dt/i2x/project-notes/src/main")
         (timestamp (format-time-string "%Y-%m-%d"))
         (branch (current-vc-branch))
         (customer (current-customer))
         (dir (cond
               ((and branch customer)
                (format "%s/%s/%s" base customer branch))
                (format "%s/%s/misc" base customer))
               (t (format "%s/internal/misc" base)))))
    (unless (file-accessible-directory-p dir)
      (mkdir dir t))
    (find-file (format "%s/" dir timestamp))))

All that remains is to define current-customer and current-vc-branch. current-customer is pretty straightforward because I use the following convention:

  dt/i2x/[Customer Name]/src/...

If I can find dt/i2x then I know the next directory name is the customer.

(defun current-customer ()
  "Guess the customer we are looking at by looking at our path"
  (let ((d default-directory))
    (if (string-match "dt/i2x/\\(.*?\\)/" d)
        (match-string 1 d)

current-vc-branch is a tiny bit more sophisticated. For one thing, some of my projects use Git while others use Subversion. I was surprised that there wasn't an existing elisp function to tell me which version control system managed an existing directory. Fortunately, both Git and Subversion use a well know directory I can search for to figure out which tool is active.

From there, I was able to use the built in vc-git function vc-git--symbolic-ref to find out the current git branch name. I derive the Subversion branch name by using dsvn's svn-current-url function.

(defun current-vc-branch ()
  "Guess the current branch or return nil if aren't on one"
  (require 'dsvn)
  (let ((d default-directory))
     ((locate-dominating-file d ".git")
      (vc-git--symbolic-ref default-directory))
     ((locate-dominating-file d ".svn")
      (let ((url (svn-current-url)))
        (cond ((string-match "/trunk\\(/\\|$\\)" url) "trunk")
              ((string-match "/branches/\\(.*?\\)\\(/\\|$\\)" url)
               (match-string 1 url))
              (t nil))))
     (t nil))))

When I run Control-x t a file is ultimately visited under dt/i2x/project-notes/main. I've arranged for that directory to be a Git repository. After my meeting or other session that inspired me to capture notes, I commit any new or modified files and do a push a remote repository. The result is that my project notes are saved in the cloud and accessible from all the devices I develop on.

And just like that, tracking on-demand project notes went from a chore to a joy.

No comments:

Post a Comment