Sharing an Emacs configuration across machines

Once I started using Emacs more frequently, I wanted to use it on every machine I owned. But I found it a pain to keep the configuration synchronized between machines; there were always some little changes I'd need on my laptop and not my desktop (and vice versa). Preventing these changes from affecting the wrong machine became a headache, but thankfully there are ways around it with a little bit of elisp.

My current setup now uses the same configuration directory, but with custom setup files that only affect certain users and operating systems.

Here's how it all fits together.

no-littering

no-littering is a small package that keeps the ~/.emacs.d directory clean and organized. This makes it easier to exclude machine-specific files during synchronization.

It's available on melpa and doesn't require any configuration to get started; a single line is all that's required to use it:

;; Standard load.
(require 'no-littering)

;; or with use-package
(use-package no-littering)

Synchronization

I sync my ~/.emacs.d directory between machines using Dropbox. The following directories are excluded:

  • ~/.emacs.d/auto-save-list/
  • ~/.emacs.d/backup/
  • ~/.emacs.d/etc/
  • ~/.emacs.d/server/
  • ~/.emacs.d/var/

These directories often contain things that are tied to the current machine's directory layout, which can cause issues when they are synchronized. The backup directory in particular generates a lot of files and excluding that cut out a lot of headaches.

Per-user configuration

All of my per-user configuration lives in the ~/.emacs.d/users/{username}/ directory. All files in that directory are loaded last so that they override any default settings.

The following snippet is placed just before (provide 'init) in my ~/.emacs.d/init.el file.

;; Configure the user directory path.
(defvar user-settings-dir
  (concat user-emacs-directory "users/" user-login-name)
  "Settings directory for the current user.")

(defun pn/load-user-init-file (file)
  "Load an init file from FILE."
  (load (format "%s/%s" user-settings-dir file)))

;; Load all elisp files in the current user directory.
(when (file-exists-p user-settings-dir)
  (mapc #'pn/load-user-init-file
	(directory-files user-settings-dir nil "^[^#].*el$")))

Per-OS configuration

The final piece of the puzzle is setting up configuration that only runs on a specific operating system. I use this for setting fonts, paths, and themes.

I put this code in a user-specific init.el file, although it can also go at the end of the main init.el file.

(when (string= "windows-nt" system-type)
  ;; Windows only configuration.
  )

(when (string= "darwin" system-type)
  ;; Mac OS only configuraiton.
  )

(when (string= "gnu/linux" system-type)
  ;; Linux only configuration.
  )

Using all of these techniques together allows me to keep my Emacs configuration in sync with a minimum of fuss.


30 days of blogging kick off

One of my minor goals for 2020 is to try a 30 day trial of blogging every day. The first post on this site was published on June 8th, 2006 (5,254 days ago). Since then I've written 114 posts, which works out at one post every 46 days. I am not a prolific writer.

I've always wondered what would happen if I wrote more often. Would my writing improve? Would more people visit the site? Would I find inner peace? This is my way of finding out.

I am expecting this to be quite difficult, so I set up a goal on Beeminder to help keep me on track. Most of my beeminder goals have a week of mercy after failure, but this one does not, which means if I fail to publish a post I still need to write the next day. I've capped the charge at $5, but if I miss everything it would cost $150. Ouch.

My current list of post ideas has around twenty topics, but I'm expecting to come up with new ideas as I write. So if all goes well, in 30 days time this site will have 144 posts.


Groundhog Day Resolutions - October 2020

I didn't write my September post until the 21st, so it didn't give me much time to get things done. Here's how everything went.

September's Primary Goals

1. Complete at least one "other goal"

I marked "Contribute regularly to an online community" as completed. I've been doing this since the start of the year so I think it's fair to mark as completed.

It's a different experience from when I used to post on forums in the late 90's - a lot of communities are centralized on Reddit, Facebook, and Twitter - but it's been nice to talk to other people.

One thing I still want to do is integrate Webmention support into this site.

Out of all the goals I picked this year, this was probably the only one made easier by Covid restrictions.

2. Continue my bodyweight workouts

Not only did I not miss a single workout on September, but I also added some runs to the same days I did workouts. I was a little worried I wouldn't be able to continue the same workout schedule once marathon training starts, but so far things are looking good.

3. Create plans for my bodyweight skill goals, and follow them

I created a spreadsheet to track my progress through each set of progressions, and I added skill work to my scheduled workouts.

September's Secondary Goals

1. Prep for a 30 day blog marathon

I have a list of topics to write about, and I'm sure more ideas will surface once I start writing regularly.

2. Read a book!

I bought some books, but that doesn't count for much if I don't read them.

Primary Goals for October

1. Publish version 0.5 of Writing PHP with Emacs

I've been working on the latest chapter, "Using lsp-mode", for the last week or so. There's quite a lot of information to cover and it will probably be the largest chapter by quite a margin.

I'd like to have it published by the end of October.

2. Start blogging every day

Write every day for 30 days is one of my secondary goals for 2020. I want to have this started before November 1st rolls around.

3. Complete another secondary goal

With less than 3 months to go I need to make some serious progress here.

I have mixed feelings about September. I met nearly all of my targets, but I spent the first few weeks of the month focusing on work to the detriment of everything else. I don't like cramming goals in all at the end of the month - despite how frequently I do it - and it gave me a bit of an unpleasant feeling.

I'm deliberately keeping October's list of goals simple. I'm not a particularly good writer, so I want to make that my primary focus for the month.


Groundhog Day Resolutions - September 2020

I really wanted this entry to be a little more introspective, but it's already September 21st and there isn't much time for that. August was another busy month, and just like July it was so short that I'm not convinced it actually happened.

August's Primary Goals

1. Release version 0.3 of The Book

I published a new chapter of The Book. I'd like to be writing more, but I'm on pace to have everything I wanted written by the end of the year.

2. Bodyweight workouts, 3 times a week

This goal was a big, big success for me. I managed 3 workouts a week and saw some pretty nice improvements: I went from 15 pull ups to 32, increased my push ups to 18 per day, and added some basic dragon flag progressions near the end of the month. I also bought a set of gymnastic rings for incline rows and dips.

I set up a Beeminder goal which made all the difference. Previous attempts at this goal have failed because I would take days off to "rest", or because I didn't feel 100%. It turns out $5 is all it takes to encourage me to work past those problems.

3. Release version 1.2 of beeminder.el

Good grief, what will it take to get this thing released?

August's Secondary Goals

1. Publish 8 posts on this site

Not even close here.

2. Maintain a frog stand for 30 seconds

I made it up to 5 seconds, but it was tough to get much higher than that. I did not practice as deliberately as I should - I set a timer for 5 minutes and then tried to frog stand in that time.

Next time around I'll create a plan and set myself some weekly and daily targets. I've had success with that approach in other areas, and I think it will work here too.

Primary Goals for September

1. Complete at least one "other goal"

2020 is nearly 75% of the way through, but I've only completed 20% of my goals. I need to get a move on here.

2. Continue my bodyweight workouts

This is going so well and I want to keep it going.

3. Create plans for my bodyweight skill goals, and follow them

Frog standing turned out to be more of a challenge than I anticipated. Likewise, my early attempts at L-sits have not been too successful. It's likely that I'll need to integrate additional stretching into my routine before I make much progress here.

Secondary Goals for September

1. Prep for a 30 day blog marathon

There are two things that stop me from writing more: I don't always have a topic to write about, and when I do I tend to go overboard so it takes weeks to actually write.

The plan here is to create a big list of topics I can pick from, and to keep each topic small enough so that I can write a post in under an hour.

2. Read a book!

I haven't sat down and read anything for months. I'd like to change that.


Keeping notes with Zetteldeft

One of my goals for 2020 was to try zettelkasten for managing my notes. I've experimented with various approaches in the past: paper notes in folders (and all over my desk), self-hosted wikis, and custom software like OneNote or Notion.

A few years ago I started using LionWiki to manage a small personal knowledge base. It was somewhat successful, but it didn't quite fit in with my daily workflow. It also required a network connection and browser to be open, and re-organizing notes wasn't quite as quick as I would like. I really like it as a lightweight wiki solution, but for personal notes I wanted something more.

Eventually I switched to deft, an Emacs-based tool for organizing notes that is based on Notational Velocity. This summer I evaluated a number of other zettelkasten tools, but I settled on Zetteldeft as it integrated well with my existing set of notes.

zetteldeft works alongside deft and adds support for special linking syntax, as well as a slightly different naming scheme for notes (timestamp followed by the title).

Zetteldeft open and ready to search
Figure 1: Click to see deft's search in action

There are a few things I really like about deft/zetteldeft's approach:

  • Text based notes – All of my notes are stored in org-mode format on my home computer. deft also supports plain text and markdown formats.
  • Fast to find notes – With a few keystrokes (C-c z D) I can bring up a new search, and the results are narrowed as I type.
  • Fast to add notes – C-c z n brings up a prompt for a new note title. Once that is entered, I'm taken to the note file with the title already filled in.
  • Linking to other notes is easy – zetteldeft supports linking to notes via a special character and the note id, which looks like this: ยง2018-07-09-2115. With some modifications it can also use a special zdlink protocol in org-mode.
  • Fits in with my daily work – If I've found the solution to a problem, I can quickly create a note and paste the solution without having to switch to another application or open a browser tab.

My zetteldeft configuration

The following code lives in my Emacs init.el. It sets up deft and zetteldeft, along with a bunch of keyboard shortcuts.

One additional change I made is binding C-c z p to sodaware/deft-open-preview. This allows me to open notes in a preview window without the list losing keyboard focus.

I use file-truename when setting my notes path as zetteldeft had some problems locating my notes when using a relative path.

(use-package deft
  :bind
  (("C-c d" . deft))
  :custom
  ;; Set deft path to full path so that zetteldeft works.
  (deft-directory         (file-truename "~/Documents/notes"))
  (deft-extensions        '("md" "org"))
  (deft-default-extension "org")
  (deft-recursive         t))

(use-package zetteldeft
  :bind
  ("C-c z d" . deft)
  ("C-c z R" . deft-refresh)
  ("C-c z D" . zetteldeft-deft-new-search)
  ("C-c z s" . zetteldeft-search-at-point)
  ("C-c z c" . zetteldeft-search-current-id)
  ("C-c z f" . zetteldeft-follow-link)
  ("C-c z F" . zetteldeft-avy-file-search-ace-window)
  ("C-c z l" . zetteldeft-avy-link-search)
  ("C-c z t" . zetteldeft-avy-tag-search)
  ("C-c z T" . zetteldeft-tag-buffer)
  ("C-c z i" . zetteldeft-find-file-id-insert)
  ("C-c z I" . zetteldeft-find-file-full-title-insert)
  ("C-c z o" . zetteldeft-find-file)
  ("C-c z n" . zetteldeft-new-file)
  ("C-c z N" . zetteldeft-new-file-and-link)
  ("C-c z p" . sodaware/deft-open-preview)
  ("C-c z r" . zetteldeft-file-rename)
  ("C-c z x" . zetteldeft-count-words)
  :config
  (defun sodaware/deft-open-preview ()
    (interactive)
    (deft-open-file-other-window))

  (font-lock-add-keywords
   'org-mode
   `((,zetteldeft-id-regex  . font-lock-warning-face)
     (,zetteldeft-tag-regex . font-lock-warning-face))))

Additional changes: the zdlink protocol

This is one of my favourite changes - it adds a new org-mode protocol, zdlink, which allows me to insert an org-style link to any note via C-c C-l. These links can be opened like any other org-mode link, and because they only include the note name (not the full path) they work across machines.

This isn't very useful if you're using markdown, but for pure org it makes linking and navigating much easier.

;; Add custom `zdlink` to handle zettledeft links.
(eval-after-load 'org
  (lambda ()
    (require 'zetteldeft)
    (org-link-set-parameters
     "zdlink"
     :follow
     (lambda (str) (zetteldeft--search-filename
		    (zetteldeft--lift-id str)))
     :complete  #'sodaware/zd-complete-link
     :help-echo "Searches provided ID in Zetteldeft")))

(defun sodaware/zd-complete-link ()
  "Link completion for `zdlink' type links."
  (let* ((file (completing-read "File to link to: "
				(deft-find-all-files-no-prefix)))
	 (link (zetteldeft--lift-id file)))
    (unless link (user-error "No file selected"))
    (concat "zdlink:" link)))

Additional feature: zetteldeft homepage

Note: This behaviour is now part of zetteldeft core. A home note can be assigned via the zetteldeft-home-id variable, and then accessed with M-x zetteldeft-go-home.

This is a small change that allows me to open a specific note file with a keyboard shortcut. This is useful when keeping a single note that acts as a gateway to all others.

I prefer the main search buffer for finding notes, but I keep one note that lists my main projects and links to their notes.

(defun sodaware/zd-homepage ()
  "Open Zetteldeft home file."
  (interactive)
  (zetteldeft-find-file "2020-07-10-0959 Home.org"))
(global-set-key (kbd "C-c z h") #'sodaware/zd-homepage)

Additional feature: .dir-locals.el config

This is a really simple .dir-locals.el file that lives in my main notes directory. It adds 3 features:

  • emojify-mode support – Sometimes I see fancy notion setups and want to spice up my buffers with some emoji. I don't, but with emojify-mode I could.
  • whitespace-mode - This removes trailing whitespace when I save notes.
  • Expands notes on opening – I normally have org-mode headlines collapsed as I use it for organizing my todo lists, but for notes I like to see the entire document when I open it.
((org-mode . ((mode . emojify)
	      (mode . whitespace-cleanup)
	      (eval . (outline-show-all)))))