04 Feb 2018

Emacs Configuration

My Emacs configuration is written as an org-mode document which is executed on start-up. I've found other people's config files invaluable when customising my Emacs experience, so I'm publishing mine in the hope it gives you some ideas.

Initialisation

Bootstrapping

This is the content used in init.el to get things started cleanly and load this org file. The content is mostly taken from Daniel Mai's Emacs configuration. After editing this src block, run 'org-babel-tangle' ("C-c C-v C-t" by default) to write out a new init.el. Changes to the other code blocks in this file do not require you to run this command.

The gc-cons-threshold settings from Daniel Mai's config has been removed. It was presumably there to speed up launch but isn't a major issue for me. Notes: Reddit thread on setting a high gc-cons-threshold during start-up, and Mailinglist post recommending not to touch it since gc-cons-percentage was introduced in Emacs 22.

;;; Begin initialization
;; Turn off mouse interface early in startup to avoid momentary display
(when window-system
  (menu-bar-mode -1)
  (tool-bar-mode -1)
  (scroll-bar-mode -1)
  (tooltip-mode -1))

(setq inhibit-startup-message t)
(setq initial-scratch-message "")

;;; Set up package
(require 'package)
(add-to-list 'package-archives
             '("melpa" . "http://melpa.org/packages/") t)
(package-initialize)

;;; Bootstrap use-package
;; Install use-package if it's not already installed.
;; use-package is used to configure the rest of the packages.
(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (package-install 'use-package))

;; From use-package README
(eval-when-compile
  (require 'use-package))

;(require 'diminish)                ;; if you use :diminish
(require 'bind-key)

;; For debugging
; (setq use-package-verbose t)

;; run server if using emacsclient as default EDITOR also useful for
;; org-protocol capture https://www.emacswiki.org/emacs/EmacsClient

(require 'server)
(unless (server-running-p)
  (server-start))

;;; Load the config
(org-babel-load-file "~/org/website/dotfiles/emacs.org")

Display org-mode agenda on startup

From How to display custom agenda-view on Emacs startup? on StackOverflow. The second string argument to org-agenda executes the view that would be triggered by that key in the agenda dispatcher.

(add-hook 'after-init-hook
          (lambda ()
            (org-agenda nil "a")
            (delete-other-windows)))

Personal Information

(setq user-full-name "Caolan McMahon"
      user-mail-address "caolan.mcmahon@gmail.com")

Customize settings

Set up the customize file to its own separate file, instead of saving customize settings in init.el.

(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(load custom-file)

Themes

E-Ink theme

The E-Ink theme is a Low distraction, minimalistic color theme for Emacs emulating reading on E Ink devices. Almost monochrome with no syntax highlighting. This uses my edited version, which tweaks a few small settings to my liking.

;; (use-package my-eink-theme
;;   :if (window-system)
;;   :load-path "~/.emacs.d/themes/"
;;   :init (lambda ()
;;           (load-theme 'my-eink t)))

Solarized

There are a few solarized themes on melpa, this one appears to be the most popular (at least, I've seen it in a few emacs dotfiles and it has the most stars on github as of <2016-10-10 Mon>).

(use-package color-theme :ensure t)
(use-package color-theme-solarized :ensure t)

(defun my/setup-color-theme ()
  (interactive)
  (load-theme 'solarized t))

(eval-after-load 'color-theme (my/setup-color-theme))

Switch between light/dark modes using 'C-c t'. This code taken from https://github.com/pyr/dot.emacs/blob/master/customizations/40-theme.el.

(custom-set-variables '(solarized-termcolors 256))
(setq solarized-default-background-mode 'light)

(defun set-background-mode (frame mode)
  (set-frame-parameter frame 'background-mode mode)
  (when (not (display-graphic-p frame))
    (set-terminal-parameter (frame-terminal frame) 'background-mode mode))
  (enable-theme 'solarized))

(defun switch-theme ()
  (interactive)
  (let ((mode  (if (eq (frame-parameter nil 'background-mode) 'dark)
                   'light 'dark)))
    (set-background-mode nil mode)))

(add-hook 'after-make-frame-functions
          (lambda (frame)
            (set-background-mode frame solarized-default-background-mode)))

(set-background-mode nil solarized-default-background-mode)
(global-set-key (kbd "C-c t") 'switch-theme)

My custom overrides for solarized theme.

(custom-theme-set-faces
 'solarized
 '(magit-section-highlight ((t (:background "#EEE8D5"))))
 '(magit-diff-context-highlight ((t (:background "#EEE8D5"))))
 '(web-mode-current-element-highlight-face ((t (:background "#EEE8D5")))))

Sublime Text themes

;; (use-package sublime-themes
;;   :ensure t)

A nice dark theme named after Rich Hickey:

;; (load-theme 'hickey t)

Fonts

Unset fixed-pitch face

I'd usually just like this to be my default font, instead it defaults to Monospace on my system, so I'm un-setting that. This issue is especially apparent in markdown-mode.

(custom-set-faces '(fixed-pitch ((t nil))))

Latin Modern

The Latin Modern family provides a nice typewriter-style monospace font designed for use with LaTeX.

(set-default-font "Latin Modern Mono 12")

TeX Gyre Cursor

TeX Gyre Cursor is based on URW Nimbus Mono, and can be used as a Courier replacement.

;(set-default-font "TeX Gyre Cursor 11")

Iosevka

Iosevka is a slender monospace sans-serif and slab-serif typeface designed for programming. Compact (thin/narrow) and includes nice ligatures for arrows etc. I've not had much luck with ligatures in Emacs, but the non-ligature version works well.

;(set-default-font "Iosevka Slab 10")
;(set-default-font "Iosevka 10")

DejaVu Sans Mono

DejaVu Sans Mono is an easy to read sans-serif font based on Bitstream Vera. Works well at smaller font-sizes and has a much smaller line height than Latin Modern meaning you can fit a lot of information on the page.

;(set-default-font "DejaVu Sans Mono 10")

FiraCode

Fira Code is an extension of the Fira Mono font containing a set of ligatures for common programming multi-character combinations. Unfortunately, I couldn't get the ligatures working in Emacs, even after following this wiki page, and installing Fira Code Symbol.

;(set-default-font "Fira Code 10")

General configuration

Display column number in modeline

(column-number-mode)

Display current time in modeline

Useful when I fullscreen Emacs.

(display-time-mode 1)

Smooth scrolling

Scroll one line at a time, less 'jumpy' than the default.

;; scroll one line at a time (less "jumpy" than defaults)
(setq mouse-wheel-scroll-amount '(1 ((shift) . 1))) ;; one line at a time
(setq mouse-wheel-progressive-speed nil) ;; don't accelerate scrolling
(setq mouse-wheel-follow-mouse 't) ;; scroll window under mouse
(setq scroll-step 1) ;; keyboard scroll one line at a time

Use spaces instead of tabs by default

(setq-default indent-tabs-mode nil)

dired

These options taken from Emacs Rocks Episode 16.

Hide file permissions and other details by default:

(use-package dired-details
  :ensure t
  :config (progn
            (setq dired-details-hidden-string "--- ")
            (dired-details-install)))

Easily copy/move files between split panes:

(setq dired-dwim-target t)

Movement and editing

Enable downcase and upcase region

These commands are disabled by default.

(put 'downcase-region 'disabled nil)
(put 'upcase-region 'disabled nil)

Cut/copy/comment current line if no region selected

In many programs, like SlickEdit, TextMate and VisualStudio, “cut” and “copy” act on the current line if no text is visually selected. For this, I originally used code from Tim Krones Emacs config, but now I'm using the whole-line-or-region package, which can be customised to use the same line-or-region style for comments too. See comments in whole-line-or-region.el for details.

(use-package whole-line-or-region
  :ensure t)

(add-to-list 'whole-line-or-region-extensions-alist
             '(comment-dwim whole-line-or-region-comment-dwim nil))
(whole-line-or-region-mode 1)

Place cursor at beginning of search matches

From: http://www.emacswiki.org/emacs/IncrementalSearch#toc4. With this hook, both ‘C-g’ and ‘RET’ exit the search at the begining of the search string. To get back to where you started the search, just use ‘C-x C-x’. This works because isearch sets the mark at the search start.

(add-hook 'isearch-mode-end-hook 'my-goto-match-beginning)

(defun my-goto-match-beginning ()
  (when (and isearch-forward isearch-other-end)
    (goto-char isearch-other-end)))

(defadvice isearch-exit (after my-goto-match-beginning activate)
  "Go to beginning of match."
  (when (and isearch-forward isearch-other-end)
    (goto-char isearch-other-end)))

Expand region

Increases the selected region by semantic units. Just keep pressing the key until it selects what you want.

(use-package expand-region
  :ensure t
  :bind (("C-=" . er/expand-region)
         ("C-+" . er/contract-region)))

Multiple cursors

Allows you to edit in multiple locations at once. If no region is selected I've configured it to call expand-region on the first key press, then select similar regions in subsequent key presses.

(defun caolan/select-word-or-mark-next ()
  "mark next if region selected, otherwise select word using expand-region"
  (interactive)
  (if mark-active
      (call-interactively #'mc/mark-next-like-this)
      (call-interactively #'er/expand-region)))

(use-package multiple-cursors
  :ensure t
  :bind (;("C-," . caolan/select-word-or-mark-next)
         ("C-," . mc/mark-next-like-this)
         ("C-<" . mc/mark-previous-like-this)
         ;; Adds a cursor to each line in active region
         ("C-c ," . mc/edit-lines)))

Ace jump mode

AceJump is a minor mode which provides fast/direct cursor movement to currently visible words/chars/lines. Inspired by the EasyMotion vim plugin.

(use-package ace-jump-mode
  :ensure t
  :bind (("M-s" . ace-jump-mode)))

Hippie-expand

HippieExpand looks at the word before point and tries to expand it in various ways.

(global-set-key "\M- " 'hippie-expand)

Highlighting

Use the highlight package to allow highlighting of text, especially useful when working with multiple docs or org-mode tables etc.

 (use-package highlight
   :ensure t)

(define-key ctl-x-map [(control ?y)]     'hlt-highlight)
(define-key ctl-x-map [(control ?Y)]     'hlt-unhighlight-region)
(define-key ctl-x-map [(down-mouse-2)]   'hlt-highlighter)
(define-key ctl-x-map [(S-down-mouse-2)] 'hlt-eraser)

Window management

Quickly switch between windows using M-o

Note that by default "M-o M-o" runs the command 'font-lock-fontify-block', which will no longer be accessible via that key sequence.

(global-unset-key (kbd "M-o"))
(bind-key "M-o" 'other-window)

Close other windows using M-1

By default this is bound to "C-x 1" but I use it often, making it a single keypress seems a good idea. This tip is taken from Endless Parentheses.

(bind-key "M-1" 'delete-other-windows)

Emacs shell

Use Eshell smart display, similar to the Plan 9 terminal.

(require 'eshell)
(require 'em-smart)
(setq eshell-where-to-jump 'begin)
(setq eshell-review-quick-commands nil)
(setq eshell-smart-space-goes-to-end t)

Set environment variable so running CHICKEN Scheme suites via the 'test' egg will display colour output.

(setenv "TEST_USE_ANSI" "1")

Add my home bin directory to PATH.

(setenv "PATH" (concat "/home/caolan/bin:/home/caolan/.nix-profile/bin:" (getenv "PATH")))

Setup Nix environment variables and add Nix bin directory to PATH.

(setenv "PATH" (concat "/home/caolan/.nix-profile/bin:" (getenv "PATH")))
(setenv "NIX_PATH" "nixpkgs=/home/caolan/.nix-defexpr/channels/nixpkgs")
(setenv "NIX_SSL_CERT_FILE" "/etc/ssl/certs/ca-certificates.crt")

Add keyboard shortcut to open eshell in other window:

(defun eshell-other-window ()
  "Opens `eshell' in a new window."
  (interactive)
  (let ((buf (eshell)))
    (switch-to-buffer (other-buffer buf))
    (switch-to-buffer-other-window buf)))

(define-key global-map "\C-ce" 'eshell-other-window)

Pop-up terminal

From http://pragmaticemacs.com/emacs/pop-up-a-quick-shell-with-shell-pop/. Opens a quick ANSI terminal in the directory of the currently open file.

(use-package shell-pop
  :ensure t
  :bind (("C-t" . shell-pop))
  :config
  (setq shell-pop-shell-type (quote ("ansi-term" "*ansi-term*" (lambda nil (ansi-term shell-pop-term-shell)))))
  (setq shell-pop-term-shell "/bin/bash")
  (setq shell-pop-universal-key "C-t")
  ;; need to do this manually or not picked up by `shell-pop'
  (shell-pop--set-shell-type 'shell-pop-shell-type shell-pop-shell-type))

Ediff

Some tips taken from the post Setting up Ediff.

Don't use the weird setup with a control panel in a separate frame, use a normal Emacs window instead.

(setq ediff-window-setup-function 'ediff-setup-windows-plain)

Split the windows horizontally instead of vertically as I find it easier to follow.

(setq ediff-split-window-function 'split-window-horizontally)

Restore the windows after Ediff quits. By default, when you quit the Ediff session with q, it just leaves the two diff windows around, instead of restoring the window configuration from when Ediff was started.

(winner-mode)
(add-hook 'ediff-after-quit-hook-internal 'winner-undo)

Don't wait 3 seconds then ask about closing the merge buffer, just close it!

;; write merge buffer.  If the optional argument save-and-continue is non-nil,
;; then don't kill the merge buffer
(defun caolan/ediff-write-merge-buffer-and-maybe-kill (buf file
                                                           &optional
                                                           show-file save-and-continue)
  (if (not (eq (find-buffer-visiting file) buf))
      (let ((warn-message
             (format "Another buffer is visiting file %s. Too dangerous to save the merge buffer"
                     file)))
        (beep)
        (message "%s" warn-message)
        (with-output-to-temp-buffer ediff-msg-buffer
          (princ "\n\n")
          (princ warn-message)
          (princ "\n\n")
          )
        (sit-for 2))
    (ediff-with-current-buffer buf
      (if (or (not (file-exists-p file))
              (y-or-n-p (format "File %s exists, overwrite? " file)))
          (progn
            ;;(write-region nil nil file)
            (ediff-with-current-buffer buf
              (set-visited-file-name file)
              (save-buffer))
            (if show-file
                (progn
                  (message "Merge buffer saved in: %s" file)
                  (set-buffer-modified-p nil)))
            (if (and (not save-and-continue))
                (ediff-kill-buffer-carefully buf)))))
    ))

(defun caolan/ediff-maybe-save-and-delete-merge (&optional save-and-continue)
  "Default hook to run on quitting a merge job.
This can also be used to save merge buffer in the middle of an Ediff session.

If the optional SAVE-AND-CONTINUE argument is non-nil, save merge buffer and
continue.  Otherwise:
If `ediff-autostore-merges' is nil, this does nothing.
If it is t, it saves the merge buffer in the file `ediff-merge-store-file'
or asks the user, if the latter is nil.  It then asks the user whether to
delete the merge buffer.
If `ediff-autostore-merges' is neither nil nor t, the merge buffer is saved
only if this merge job is part of a group, i.e., was invoked from within
`ediff-merge-directories', `ediff-merge-directory-revisions', and such."
  (let ((merge-store-file ediff-merge-store-file)
        (ediff-autostore-merges ; fake ediff-autostore-merges, if necessary
         (if save-and-continue t ediff-autostore-merges)))
    (if ediff-autostore-merges
        (cond ((stringp merge-store-file)
               ;; store, ask to delete
               (caolan/ediff-write-merge-buffer-and-maybe-kill
                ediff-buffer-C merge-store-file 'show-file save-and-continue))
              ((eq ediff-autostore-merges t)
               ;; ask for file name
               (setq merge-store-file
                     (read-file-name "Save the result of the merge in file: "))
               (caolan/ediff-write-merge-buffer-and-maybe-kill
                ediff-buffer-C merge-store-file nil save-and-continue))
              ((and (ediff-buffer-live-p ediff-meta-buffer)
                    (ediff-with-current-buffer ediff-meta-buffer
                                               (ediff-merge-metajob)))
               ;; The parent metajob passed nil as the autostore file.
               nil)))
    ))

(add-hook 'ediff-quit-merge-hook #'caolan/ediff-maybe-save-and-delete-merge)

Fully expand Org files in Ediff (otherwise it's hard to see the changes between files). This tip taken from the Emacs Stackexchange, which also has an interesting solution to unfold/fold Org elements as Ediff selects/deselects changes, but I found just showing everything to work more reliably.

Note, show-all is marked obsolete in Emacs 25.1 in favor of outline-show-all. But the latter symbol is not bound in 24.5, and since I use Debian stable (Jessie) on my desktop which provides Emacs 24.4, I need to use the older show-all for now.

(add-hook 'ediff-prepare-buffer-hook #'show-all)

Coding

Magit - git interface

Use shortcut for magit-status, since I open it a lot.

(use-package magit
  :ensure t
  :bind (("C-c m" . magit-status)))

Flycheck

Flycheck provides 'on the fly' syntax checking for many languages. I don't enable global mode, and instead enable it for specific language modes in their section of this config.

(use-package flycheck
  :ensure t)

Markdown mode

Emacs markdown-mode is a major mode for editing markdown text (including GitHub-flavoured markdown), it includes syntax highlighting, convenient editing, and browser preview commands.

Use GitHub-flavoured markdown for README.md files. Regular markdown for other files.

(use-package markdown-mode
  :ensure t
  :commands (markdown-mode gfm-mode)
  :mode (("README\\.md\\'" . gfm-mode)
         ("\\.md\\'" . markdown-mode)
         ("\\.markdown\\'" . markdown-mode)))

This also requires a markdown to HTML conversion binary to be available, by default it uses markdown, but this can be configured via the markdown-command variable.

sudo apt-get install markdown

HTML/CSS/Templates

Use web-mode for mixing markup, template syntax, JavaScript, etc.

(use-package web-mode
  :ensure t
  :mode "\\.html?\\'"
  :config
  (progn
    (setq web-mode-markup-indent-offset 4)
    (setq web-mode-code-indent-offset 4)
    (setq web-mode-enable-current-element-highlight t)
    (setq web-mode-enable-auto-expanding t)
    ))

Turn on Rainbow mode for CSS and HTML.

(add-hook 'web-mode-hook #'rainbow-mode)
(add-hook 'css-mode-hook #'rainbow-mode)

JavaScript

I like js2-mode, which includes a javascript interpreter implemented in elisp and will highlight syntax errors and undeclared variables as you type.

(use-package js2-mode
  :ensure t
  :commands js2-mode
  :bind (("C-c ! n" . js2-next-error))
  :init
  (progn
    (setq-default js2-basic-offset 4)
    (setq-default js2-strict-trailing-comma-warning t)
    (setq-default js2-global-externs
                  '("module"
                    "exports"
                    "require"
                    "process"
                    "setTimeout"
                    "clearTimeout"
                    "setInterval"
                    "clearInterval"
                    "window"
                    "location"
                    "__dirname"
                    "console"
                    "JSON"))
    (add-to-list 'interpreter-mode-alist (cons "node" 'js2-mode))))

(add-to-list 'auto-mode-alist '("\\.js\\'" . js2-mode))

Python

Enable flycheck.

(add-hook 'python-mode-hook #'flycheck-mode)
(setq flycheck-python-flake8-executable "/home/caolan/.local/bin/flake8")

Disable pylint, and force use of flake8 instead.

(add-to-list 'flycheck-disabled-checkers 'python-pylint)
(add-hook 'python-mode-hook
          (lambda () (flycheck-select-checker 'python-flake8)))

For flycheck to work, install flake8. To display warnings for using incorrect naming conventions, I also like to install pep8-naming.

pip install flake8 pep8-naming

Install Jedi for auto-completion (via company):

(use-package company-jedi
  :ensure t)

Jedi also requires virtualenv to be available:

sudo apt-get install virtualenv

Then you can run M-x jedi:install-server.

Python mode hook to turn on autocomplete etc.

(add-hook 'python-mode-hook
          (lambda ()
            (company-mode 1)))

Scheme

Geiser

Integration with a scheme interpreter via Geiser.

(use-package geiser
  :ensure t
  :config
  (progn
   (setq geiser-active-implementations '(chicken))
   (setq geiser-default-implementation 'chicken)))

Geiser also works well with Company mode for auto-complete.

Indentation

Turn off indentation for body of CHICKEN Scheme modules and a few other tweaks. From Tweaking stock scheme-mode indentation.

(add-hook 'scheme-mode-hook
          (lambda ()
            (defun scheme-module-indent (state indent-point normal-indent) 0)
            (put 'module 'scheme-indent-function 'scheme-module-indent)
            (put 'test-group 'scheme-indent-function 1)
            (put 'test-generative 'scheme-indent-function 1)
            (put 'and-let* 'scheme-indent-function 1)
            (put 'parameterize 'scheme-indent-function 1)
            (put 'handle-exceptions 'scheme-indent-function 1)
            (put 'when 'scheme-indent-function 1)
            (put 'unless 'scheme-indent-function 1)
            (put 'match 'scheme-indent-function 1)
            (put 'match-let 'scheme-indent-function 1)
            (put 'match-lambda 'scheme-indent-function 1)
            (put 'match-lambda* 'scheme-indent-function 1)))

Erlang

Enable Erlang mode.

(use-package erlang :ensure t)

ini files

There doesn't appear to be a bundled mode for windows-style .ini files, and as of <2016-10-15 Sat> no package in melpa. I'm using a downloaded copy of ini-mode from GitHub.

(add-to-list 'load-path "~/.emacs.d/lisp/")
(load "ini-mode.el")

;(use-package ini-mode
;  :load-path "~/.emacs.d/lisp/")

Nix

A mode for working with nix-expressions.

(use-package nix-mode
  :ensure t)

General

Using dumb-jump to jump to definitions

The dumb-jump package provides 'jump to definition' support for multiple languages by simply searching for possible definitions using The Silver Searcher ag, ripgrep rg, or grep. It does not require building stored indexes (TAGS) or background processes etc.

Enable dumb-jump-mode globally:

(use-package dumb-jump
  :ensure t
  :init (lambda ()
          (dumb-jump-mode)))

You also want to install The Silver Searcher, otherwise it will fall back to grep which will be slower.

sudo apt-get install silversearcher-ag

Company mode (auto-complete)

Provides auto-completion and pop-to-buffer documentation for candidates.

(use-package company
  :ensure t
  :commands company-mode)

EditorConfig plugin

Reads EditorConfig files to set coding style options according to current project.

(use-package editorconfig
  :ensure t
  :config
  (editorconfig-mode 1))

Rainbow mode

Rainbow-mode is a minor-mode which highlights hexadecimal colour codes (#RRGGBB), named HTML colours, rgb() colours etc. Sets the text background to the colour described.

(use-package rainbow-mode
  :ensure t)

This is not turned on by default, but is enabled via specific major-mode hooks (e.g. CSS).

Writing

Sentences end with a single space

This makes sentence navigation commands work for me.

(setq sentence-end-double-space nil)

Enable spellcheck in plain text mode

(add-hook 'text-mode-hook 'flyspell-mode)

Thesaurus

Many of the thesaurus packages use an online service, synosaurus gives you the option of using a local Wordnet install. It also has a nice 'replace word' interface.

NOTE: this requires you to install wordnet

(use-package synosaurus
  :ensure t
  :config (progn
            (setq synosaurus-backend 'synosaurus-backend-wordnet)
            (setq synosaurus-choose-method 'default)))

Dictionary

Since I already use a local Wordnet as a thesaurus, I'm using it for a dictionary as well via the wordnut package.

(use-package wordnut
  :ensure t)

Olivetti minor mode

A minor mode for a nice writing environment.

(use-package olivetti
  :ensure t
  :config (setq olivetti-body-width 90))

Finances

Ledger mode

I use Emacs ledger-mode with ledger to manage my finances.

(use-package ledger-mode
  :ensure t
  :commands ledger-mode)

And associate .ledger files with ledger-mode:

(add-to-list 'auto-mode-alist '("\\.ledger$" . ledger-mode))

I also have some custom elisp to import bank CSV files into ledger documents:

(use-package csv
  :ensure t)

;; (defun caolan/ledger-list-accounts (filename)
;;   (remove-if
;;     (lambda (line) (equal (length line) 0))
;;     (split-string
;;     (shell-command-to-string
;;     (concat "ledger accounts -f " (shell-quote-argument filename)))
;;     "\n")))

(defun caolan/ledger-list-accounts ()
  (let ((tmp-buffer (get-buffer-create (make-temp-name "ledger-accounts-"))))
    (shell-command-on-region (point-min)
                             (point-max)
                             "ledger accounts -f -"
                             tmp-buffer)
    (switch-to-buffer tmp-buffer)
    (let ((result (buffer-string)))
      (kill-buffer tmp-buffer)
      (remove-if #'string-empty-p (split-string result "\n")))))

(defun caolan/ledger-read-account (prompt desc skip-acct)
  (let* ((accounts (caolan/ledger-list-accounts))
         (last-account (caolan/last-used-account desc skip-acct))
         (default (or last-account "unknown")))
    (setq accounts (cons default (delete default accounts)))
    (ido-completing-read prompt accounts)))

(defun caolan/lloyds-convert-date (datestr)
  (string-join (reverse (split-string datestr "/")) "/"))

(defun caolan/format-money-string (amount)
  (concat "£" (format "%.2f" (string-to-number amount))))

;; (defun caolan/ledger-find-accounts ()
;;   (let ((p (point))
;;         (accounts '()))
;;     (goto-char (point-min))
;;     (search-forward-regexp "^[:graph:]")
;;     (next-line)
;;     (goto-char p)))

(defun caolan/read-line ()
  (let ((start (progn (beginning-of-line) (point)))
        (end (progn (end-of-line) (point))))
    (buffer-substring-no-properties start end)))

(defun caolan/read-line-account ()
  (let ((line (caolan/read-line)))
    (and (string-match "^[[:space:]]+\\([[:graph:]]+\\)" line)
         (substring line (match-beginning 1) (match-end 1)))))

(defun caolan/last-used-account (desc skip-acct)
  (let ((p (point)))
    (if (search-backward-regexp
         (concat "^[[:digit:]][[:digit:]][[:digit:]][[:digit:]]"
                 "/[[:digit:]][[:digit:]]"
                 "/[[:digit:]][[:digit:]] "
                 (regexp-quote desc)) nil t)
        (progn (next-line)
               (let ((acct (caolan/read-line-account)))
                 (while (and acct (string-equal acct skip-acct))
                   (next-line)
                   (setq acct (caolan/read-line-account)))
                 (goto-char p)
                 acct))
      (progn
        (goto-char p)
        nil))))

(defun caolan/lloyds-convert-rows (rows)
  (when rows
    (let* ((row (car rows))
           (debit (assoc-default "Debit Amount" row))
           (credit (assoc-default "Credit Amount" row))
           (type (if (equal (length credit) 0) "Debit" "Credit"))
           (description (assoc-default "Transaction Description" row))
           (amount (if (equal (length credit) 0) debit credit))
           (account (caolan/ledger-read-account
                     (concat type " by " description
                             " (" (caolan/format-money-string amount) "): ")
                     description
                     "assets:lloyds:joint")))
      (insert (caolan/lloyds-convert-date (assoc-default "Transaction Date" row)))
      (insert " ")
      (insert description)
      (insert "\n")
      (ledger-magic-tab)
      (if (equal (length credit) 0)
          (progn
            (insert account "  " (caolan/format-money-string debit))
            (back-to-indentation)
            (ledger-magic-tab)
            (move-end-of-line nil)
            (insert "\n")
            (ledger-magic-tab)
            (insert "assets:lloyds:joint\n"))
        (progn
          (insert "assets:lloyds:joint")
          (insert "  " (caolan/format-money-string credit))
          (back-to-indentation)
          (ledger-magic-tab)
          (move-end-of-line nil)
          (insert "\n")
          (ledger-magic-tab)
          (insert account "\n"))))
    (insert "\n")
    (caolan/lloyds-convert-rows (cdr rows))))

(defun caolan/ledger-import-lloyds-csv ()
  "Parse a Lloyds CSV file and insert its transactions into the
current buffer, converted to Ledger format."
  (interactive)
  (let* ((filename (read-file-name "Lloyds CSV file: "))
         (rows (reverse
                (with-temp-buffer
                  (insert-file-contents filename)
                  (csv-parse-buffer t)))))
    (goto-char (point-max))
    (insert "\n")
    (caolan/lloyds-convert-rows rows)))

Email

Installation

My mail setup uses mu (via mu4e) to index and search messages downloaded by offlineimap. These dependencies should be installed and configured first.

Load mu4e

Loads from a local directory as mu4e is not available in the package repositories.

(use-package mu4e
  :load-path "~/.emacs.d/lisp/mu4e/")

Account details

;; Personal details
(setq user-mail-address "caolan.mcmahon@gmail.com"
      user-full-name "Caolan McMahon")

;; offlineimap mail directory
(setq mu4e-maildir "/home/caolan/mail/caolan.mcmahon@gmail.com")
(setq mu4e-drafts-folder "/Drafts")
(setq mu4e-refile-folder "/Archived")
(setq mu4e-trash-folder "/Trash")
(setq mu4e-sent-folder "/Sent Mail")

;; SMTP configuration
(setq message-send-mail-function 'smtpmail-send-it
     smtpmail-stream-type 'starttls
     smtpmail-default-smtp-server "smtp.gmail.com"
     smtpmail-smtp-server "smtp.gmail.com"
     smtpmail-smtp-service 587)

;; don't save message to Sent Messages, Gmail/IMAP takes care of this
(setq mu4e-sent-messages-behavior 'delete)

Email Signature

(setq mu4e-compose-signature "Caolan")

Mu4e config

Update email

Fetch email every 20 minutes.

(setq mu4e-update-interval 1200)

Allow for manual updates using 'U' in the main mu4e view. This only requires setting mu4e-get-mail-command, but I want it to also display the buffer with update progress if there is an update already running in the background. The default behaviour just reports that there is an update already running, without bringing the process buffer into view.

(setq mu4e-get-mail-command "offlineimap")

(defun caolan/update-mail ()
  "Run offline imap and update mu4e indexes - if already running, display buffer"
  (interactive)
  (if (get-process "mu4e-update")
      (display-buffer-at-bottom
       (process-buffer (get-process "mu4e-update"))
       '())
    (mu4e-update-mail-and-index nil)))

(defun caolan/mu4e-main-mode-hook ()
  (local-set-key (kbd "U") 'caolan/update-mail))
(add-hook 'mu4e-main-mode-hook 'caolan/mu4e-main-mode-hook)

HTML emails

I find the included mu4e-shr2text command which uses the shr package (also used by eww) to render HTML too slow and have switched to using w3m with the display_link_number option.

;; (setq mu4e-html2text-command 'mu4e-shr2text)
;; (setq mu4e-html2text-command "lynx -stdin -dump")
;; (setq mu4e-html2text-command "pandoc -f html -t org")
(setq mu4e-html2text-command "w3m -dump -s -T text/html -o display_link_number=true")

Add option to view HTML emails in browser using 'aV' in message view.

(add-to-list 'mu4e-view-actions
  '("ViewInBrowser" . mu4e-action-view-in-browser) t)

Inline images

Display images inline in the message buffer (use imagemagick if available).

(add-hook 'mu4e-view-mode-hook
          (lambda ()
            (setq mu4e-view-show-images t)))

(when (fboundp 'imagemagick-register-types)
  (imagemagick-register-types))

Spell check

Enable flyspell mode during message composition.

(add-hook 'mu4e-compose-mode-hook flyspell-mode)

Avoid hard wrapping email content

Many email services/clients expect soft-wrapped emails, so I like to use visual-line-mode and the visual-fill-column package instead of auto-fill-mode. To show whether a paragraph is hard- or soft-wrapped I also turn on visual line indicators in the fringe.

(use-package visual-fill-column
  :ensure t)

(add-hook 'mu4e-compose-mode-hook
          (lambda ()
            (set-fill-column 72)
            (auto-fill-mode 0)
            (visual-fill-column-mode)
            (setq visual-line-fringe-indicators '(left-curly-arrow right-curly-arrow))
            (visual-line-mode)))

GPG encryption

Try to automatically decrypt emails.

(setq mu4e-decryption-policy t)

Display email addresses (not just names)

(setq mu4e-view-show-addresses t)

Kill message buffer after sending email

(setq message-kill-buffer-on-exit t)

Shortcuts

Jump to maildir shortcuts

(setq mu4e-maildir-shortcuts
      '(("/INBOX" . ?i)
        ("/Archived" . ?a)
        ("/Drafts" . ?d)
        ("/Trash" . ?t)
        ("/Sent Mail" . ?s)))

Bookmarks

Default views displayed in the mu4e main window

(setq mu4e-bookmarks
  '(("flag:unread AND NOT flag:trashed AND NOT maildir:\"/Archived\"" "Unread messages" ?u)
    ("date:today..now" "Today's messages" ?t)
    ("date:7d..now" "Last 7 days" ?w)
    ("mime:image/*" "Messages with images" ?p)))

Twitter

I don't really read Twitter from Emacs, but it can be useful to author + post tweets from emacs lisp.

(use-package twittering-mode
  :ensure t)

RSS feeds

I use elfeed as my feed reader and elfeed-org to configure my feeds in an org-mode file. This configuration is based on a Pragmatic Emacs post.

By storing feeds in an org document, I can also use the file as my website's links page, so it's kept up to date.

(use-package elfeed-org
  :ensure t
  :config
  (progn
    (elfeed-org)
    (setq rmh-elfeed-org-files (list "~/org/website/blogroll.org"))))

The Pragmatic Emacs post suggests making sure elfeed updates its database whenever it quits, and that it reads it again when resumed.

;;functions to support syncing .elfeed between machines
;;makes sure elfeed reads index from disk before launching
(defun caolan/elfeed-load-db-and-open ()
  "Wrapper to load the elfeed db from disk before opening"
  (interactive)
  (elfeed-db-load)
  (elfeed)
  (elfeed-search-update--force))

;;write to disk when quiting
(defun caolan/elfeed-save-db-and-bury ()
  "Wrapper to save the elfeed db to disk before burying buffer"
  (interactive)
  (elfeed-db-save)
  (quit-window))

Now install and configure elfeed.

(use-package elfeed
  :ensure t
  :bind (:map elfeed-search-mode-map
              ("q" . caolan/elfeed-save-db-and-bury)))

See also, my elfeed custom link definition which will store a link to the current posts URL via org-mode.

Org-mode

Babel

Enable more languages.

(org-babel-do-load-languages
  'org-babel-load-languages
  '((emacs-lisp . t)
    (ledger . t)
    (scheme . t)
    (python . t)
    (dot . t)
    (shell . t)))

General

Make the lines in the buffer wrap around the edges of the screen.

;; (add-hook 'org-mode-hook #'visual-line-mode)

Hide multiple asterisks in outline mode and indent content to match level.

(add-hook 'org-mode-hook #'org-indent-mode)

Enable spellcheck.

(add-hook 'org-mode-hook 'flyspell-mode)

Display inline images at startup. This can be customized per-file with corresponding #+STARTUP keywords 'inlineimages' and 'noinlineimages'.

(setq org-startup-with-inline-images t)

Install exporter backend for GitHub-flavoured markdown:

(use-package ox-gfm
  :ensure t)

Custom links

Mu4e

Store a link to a mu4e query or message, setting various properties for use in capture templates. Basic support is provided by 'org-mu4e, but this uses some code from Using org-capture-templates with mu4e to extend the properties available to templates.

(require 'org-mu4e)

(defun org-mu4e-store-link ()
  "Store a link to a mu4e query or message."
  (cond
    ;; storing links to queries
    ((eq major-mode 'mu4e-headers-mode)
     (let* ((query (mu4e-last-query))
             desc link)
       (org-store-link-props :type "mu4e" :query query)
       (setq link (concat "mu4e:query:" query))
       (org-add-link-props :link link :description link)
       link))
    ;; storing links to messages
    ((eq major-mode 'mu4e-view-mode)
     (let* ((msg (mu4e-message-at-point))
            (msgid (or (plist-get msg :message-id) "<none>"))
            (from (car (car (mu4e-message-field msg :from))))
            (to (car (car (mu4e-message-field msg :to))))
            (subject (mu4e-message-field msg :subject))
            link)
       (setq link (concat "mu4e:msgid:" msgid))
       (org-store-link-props 
          :type "mu4e" :from from :to to :subject subject
          :message-id msgid)
       (org-add-link-props
          :link link
          :description (funcall org-mu4e-link-desc-func msg))
   link))))

(org-add-link-type "mu4e" 'org-mu4e-open)
(add-hook 'org-store-link-functions 'org-mu4e-store-link)

Capture

Enable org-capture hotkey.

(define-key global-map "\C-cc" 'org-capture)

Start with empty capture templates list.

(setq org-capture-templates nil)

Blog post

Structure for a new draft blog post. Adds begin / end preview markers to remind me, and starts a new outline structure.

(defun read-blog-file-from-minibuffer ()
  (let ((filename (read-file-name "Draft: " "~/org/website/drafts/")))
    (set-buffer (org-capture-target-buffer filename))
    (org-capture-put-target-region-and-position)
    (widen)
    (setq target-entry-p nil)))

(push `("b" "Blog post" plain
        (function read-blog-file-from-minibuffer)
        ,(string-join
          '("#+TITLE: %^{Title}"
            "#+DATE: "
            ""
            "#+BEGIN_PREVIEW"
            "#+END_PREVIEW"
            ""
            "* Notes :noexport:"
            "** Main characters"
            "** Point of view"
            "** Concepts & themes"
            "** Point sentence"
            "** Issue"
            "** Discussion"
            "** References")
          "\n")
        :jump-to-captured t
        :immediate-finish t)
      org-capture-templates)

Link

A simple link capture template, useful when storing links to things from Firefox.

(push '("l" "Link" entry (file "~/org/notes.org") "* %a")
      org-capture-templates)

Respond later

The 'Respond later' template is a customised TODO which includes some extra email information. This relies on the extended email properties made available in the Org-mode -> Custom Links -> mu4e section of this config.

(push `("r" "Respond later" entry (file+headline "~/org/tasks.org" "Capture")
        ,(string-join
          '("* TODO Respond to %:from on %a"
            "  %?"
            "  :LOGBOOK:"
            "  - Captured on %U from %a"
            "  :END:")
          "\n"))
        org-capture-templates)

Todo

The Todo template is based on the blog post The power of orgmode capture templates.

(push `("t" "Todo" entry (file+headline "~/org/tasks.org" "Capture")
        ,(string-join
          '("* TODO %^{Description}"
            "  %?"
            "  %a"
            "  :LOGBOOK:"
            "  - Captured on %U"
            "  :END:")
          "\n"))
        org-capture-templates)

Note

During expansion of the template, %a has been replaced by a link to the location from where you called the capture command. This can be extremely useful for deriving tasks from emails, for example. This tip from the Org-mode manual. The %U will be replaced with the time of the capture, this is an 'inactive' timestamp meaning it won't show up in the agenda view.

(push '("n" "Note" entry (file "~/org/notes.org")
        "* %?\nCaptured on %U %a")
      org-capture-templates)

Weekly review

Add notes to this week's 'weekly review' blog post.

(defvar caolan/dow-time-days
  '(("Monday" . 1)
    ("Tuesday" . 2)
    ("Wednesday" . 3)
    ("Thursday" . 4)
    ("Friday" . 5)
    ("Saturday" . 6)
    ("Sunday" . 0))
  "Days of week alist used by `caolan/dow-time'")

(defun caolan/dow-time (weekday)
  "Returns time of next day matching weekday name, e.g.
'Friday' (or today if it matches)"
  (let* ((day (if (numberp weekday) weekday
                (cdr (assoc weekday caolan/dow-time-days))))
         (time (decode-time))
         (dow (nth 6 time))
         (day-of-month (nth 3 time)))
    (if (= day dow)
        (encode-time 0 0 0
                     (nth 3 time)
                     (nth 4 time)
                     (nth 5 time))
      (let ((new-dow (if (> day dow) ;; Check what's the new dow's index
                         (- day dow) ;; In the same week
                       (+ (- 7 dow) day)))) ;; In the next week
        (encode-time 0 0 0
                     (+ new-dow day-of-month)
                     (nth 4 time)
                     (nth 5 time))))))

(defun caolan/capture-weekly-review-file ()
  (let* ((end-of-week (caolan/dow-time "Friday"))
         (filename (expand-file-name
                   (format "~/org/website/drafts/journal-%s.org"
                           (format-time-string "%Y-%m-%d" end-of-week)))))
    (unless (file-exists-p filename)
      (message "creating new weekly review file")
      (with-temp-buffer
        (find-file filename)
        (insert (format "#+TITLE: Journal: %s\n"
                        (format-time-string "%B %d, %Y" end-of-week)))
        (insert "#+DATE:\n\n")
        (insert "#+BEGIN_PREVIEW\n#+END_PREVIEW\n\n")
        (insert "* Notes\n")
        (org-set-tags-to ":noexport:")))
    filename))

(push '("w" "Weekly review" entry
        (file+headline (caolan/capture-weekly-review-file) "Notes")
        "* %?")
      org-capture-templates)

I also have a 'weekly-review' command which opens up my current weekly review draft post:

(defun weekly-review ()
  (interactive)
  (find-file (caolan/capture-weekly-review-file)))

Refiling

Provide refile targets as paths, so a level 3 headline will be available as level1/level2/level3. Offer completions in hierarchical steps.

(setq org-refile-use-outline-path t)
(setq org-outline-path-complete-in-steps t)

Consider only headings in current buffer for refiling, up to a maximum depth.

(setq org-refile-targets '((nil . (:maxlevel . 3))))

Create any missing parent nodes during refile (after asking for confirmation).

(setq org-refile-allow-creating-parent-nodes 'confirm)

Org-protocol

Use org-protocol to trigger org-mode interactions from external programs. Useful for capturing links from Firefox using the org-mode-capture add-on.

(require 'org-protocol)

Agenda

Specifying only those files with agenda items and TODO's for faster scanning. The check to exclude paths for missing files is taken from Sacha Chua's config.

(setq org-agenda-files
      (delq nil
            (mapcar (lambda (x) (and (file-exists-p x) x))
                    '("~/org/tasks.org"))))

Custom agenda view which shows only TODO/STARTED/WAITING items that don't have a DEADLINE/SCHEDULED tag (or it is empty). This helps me ensure all important tasks are in my weekly agenda view - unscheduled tasks are easier to ignore.

(setq org-agenda-custom-commands
      `(("X" "Not scheduled" tags
         "-DEADLINE={.+}-SCHEDULED={.+}/!+TODO|+STARTED|+WAITING")))

Publishing

Remove section numbers, table of contents etc. from HTML output plus some other sensible defaults. These can be overridden in org-publish-project-alist.

(setq org-export-with-section-numbers nil)
(setq org-html-include-timestamps nil)
(setq org-export-with-sub-superscripts nil)
(setq org-export-with-toc nil)
(setq org-html-toplevel-hlevel 2)
(setq org-export-htmlize-output-type 'css)
(setq org-export-html-coding-system 'utf-8-unix)
(setq org-html-viewport nil)

We'll need the htmlize package for syntax highlighting of code blocks.

(use-package htmlize :ensure t)

And ox-rss.el for RSS 2.0 exports.

(use-package ox-rss
  :load-path "~/.emacs.d/lisp/")

Add my projects:

 ;; always rebuild all files
 ;; (setq org-publish-use-timestamps-flag nil)

 (setq org-publish-project-alist
       '(("website"
          :components ("website-static" "website-slides" "website-pages" "website-rss"))
         ("website-static"
          :base-directory "~/org/website/static"
          :base-extension ".*"
          :recursive t
          :publishing-function org-publish-attachment
          :publishing-directory "~/public_html/static"
          :exclude "~$"
          )
         ("website-slides"
          :base-directory "~/org/website/slides"
          :base-extension ".*"
          :recursive t
          :publishing-function org-publish-attachment
          :publishing-directory "~/public_html/slides"
          :exclude "~$"
          )
         ("website-pages"
          :base-directory "~/org/website/"
          :base-extension "org"
          :publishing-directory "~/public_html/"
          :publishing-function my-website-publish-to-html
          :preparation-function my-blog-preprocessor
          :htmlized-source t
          :my-site-name "Caolan McMahon"
          :my-stylesheet "static/css/style.css"
          :my-nav-file "nav.org"
          :language "en"
          :html-doctype "html5"
          :html-html5-fancy t
          :html-head nil
          :html-head-include-default-style nil
          :html-head-include-scripts nil
          :html-postamble my-website-postamble
          :html-preamble my-website-preamble
          :html-home/up-format ""
          :html-link-up ""
          :html-link-home ""
          :recursive t
          ;; NOTE: :exclude in org-mode seems to operate only on _base_ filename or directory path relative to :base-directory - so paths like posts/archive.org won't work! - TODO: split this into two publish steps, one for general pages another for the posts/* subdir - then I can exclude posts/recent.org etc?
;; "\\`static\\'\\|\\`slides\\'\\|\\`nav\\.org\\'\\|\\`posts/index\\.org\\'\\|\\`posts/rss\\.org\\'\\|\\`posts/archive\\.org\\'\\|\\`posts/recent\\.org\\'"

          :exclude  "\\`static\\'\\|\\`slides\\'\\|\\`nav\\.org\\'\\|\\`rss\\.org\\'"
          :rss (("posts/.*"
                 :rss-title "Caolan McMahon's blog"
                 :rss-file "posts/rss.xml"))
          )
         ("website-rss"
          :base-directory "~/org/website/posts"
          :base-extension "org"
          :publishing-directory "~/public_html/posts/"
          :publishing-function org-rss-publish-to-rss
          :rss-extension "xml"
          :html-link-home "https://caolan.org/posts/"
          :html-link-use-abs-url t
          :html-link-org-files-as-html t
          :title "Caolan McMahon"
          :description "Caolan McMahon's blog"
          :rss-image-url "https://caolan.org/static/img/me.jpg"
          :section-numbers nil
          :exclude ".*"
          :include ("rss.org")
          :table-of-contents nil
          )
         ))

I use rsync to upload the website to my server.

(defun rsync-my-website ()
  (interactive)
  (shell-command
   (string-join
    '("rsync -avh --delete --exclude \"private\" --exclude \"awstats\""
      "~/public_html/"
      "caolan@caolan.org:/var/www/htdocs/caolan.org/")
    " ")))

I wrap the normal org-html-publish-to-html function so I can set the stylesheet location relative to the current file. This means I can directly open the HTML output using the file: protocol and still have the browser find my CSS. This overrides the :html-head property set at the project level.

(defun my-website-rss-plist-match (rss-list filename)
  (and rss-list
       (let ((x (car rss-list)))
         (if (string-match-p (car x) filename)
             (cdr x)
           (my-website-rss-plist-match (cdr rss-list) filename)))))

(defun my-website-get-rss-plist (plist filename)
  (my-website-rss-plist-match
   (plist-get plist :rss)
   (file-relative-name filename (plist-get plist :base-directory))))

(defun my-website-publish-to-html (plist filename pub-dir)
  "HTML publish wrapper to set relative paths for stylesheets etc."
  (let* ((base (file-name-as-directory (plist-get plist :base-directory)))
         (my-stylesheet (plist-get plist :my-stylesheet))
         (rss-plist (my-website-get-rss-plist plist filename))
         (rss-title (plist-get rss-plist :rss-title))
         (rss-file (plist-get rss-plist :rss-file))
         (rss-link (and rss-file
                        (file-relative-name
                         (expand-file-name rss-file base)
                         (file-name-directory (expand-file-name filename)))))
         (stylesheet (and my-stylesheet
                          (file-relative-name
                           (expand-file-name my-stylesheet base)
                           (file-name-directory (expand-file-name filename))))))
    (org-html-publish-to-html
     (plist-put plist
                :html-head
                (concat
                 "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
                 (if stylesheet
                     (format "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\" />\n" stylesheet)
                   "")
                 (if rss-link
                     (format "<link rel=\"alternate\" type=\"application/rss+xml\" title=\"%s\" href=\"%s\" />\n"
                             rss-title rss-link)
                   "")))
     filename
     pub-dir)))

Custom preamble format - used to insert navigation.

(defun caolan/repeated-string (n str)
  (string-join (make-list (+ n 1) "") str))

(defun path-to-breadcrumbs (file basedir)
  (let ((dir (expand-file-name (file-name-directory file))))
    (if (and (string-equal dir (expand-file-name basedir))
             (string-equal (file-name-base file) "index"))
        ;; empty breadcrumbs to show we're at root index page
        nil
      (let* ((crumb-path (directory-file-name
                          (file-relative-name dir basedir)))
             (crumb-parts (remove-if
                           (lambda (x) (string-equal x "."))
                           (reverse (split-string crumb-path "/"))))
             (crumbs (and crumb-parts
                          (list (cons (car crumb-parts) "index.html")))))
        ;; parent directories
        (dotimes (i (length (cdr crumb-parts)))
          (setq crumbs
                (cons (cons (nth i (cdr crumb-parts))
                            (concat (caolan/repeated-string (+ i 1) "../")
                                    "index.html"))
                      crumbs)))
        (setq crumbs
              (cons `("caolan" . ,(if (> (length crumb-parts) 0)
                                      (concat (caolan/repeated-string
                                               (length crumb-parts)
                                               "../")
                                              "index.html")
                                    "index.html"))
                    crumbs))
        (if (string-equal (file-name-base file) "index")
            (butlast crumbs)
            crumbs)))))

(defun my-website-preamble (plist)
  (let* ((file (expand-file-name (plist-get plist :input-file)))
         (basedir (file-name-as-directory (plist-get plist :base-directory)))
         (crumbs (path-to-breadcrumbs file base))
         (date (org-publish-find-date file)))
    (concat
     "<ol class=\"breadcrumbs\">"
     (if crumbs
         (mapconcat (lambda (crumb)
                      (concat "<li>"
                              "<a href=\"" (cdr crumb) "\">"
                              (car crumb)
                              "</a>"
                              "</li>"))
                    crumbs
                    "")
       "<li class=\"current\"><a href=\"index.html\">caolan</a></li>")
     "</ol>"
     (if date
         (concat "<div class=\"date\">"
                 (format-time-string "%d %b %Y" date)
                 "</div>")
       "")
     )))

Custom postamble format.

(defun caolan/relative-tree (base dir filename plist)
  "Expand #+INCLUDE keywords and return org-mode tree with all
links made relative to dir."
  (with-temp-buffer
    (cd base)
    (insert-file-contents filename)
    (org-export-expand-include-keyword)
    (org-mode)
    (let ((tree (org-element-parse-buffer)))
      (org-element-map tree 'link
        (lambda (link)
          (let* ((path (org-element-property :path link))
                 (type (org-element-property :type link)))
            (when (string= type "file")
              (let* ((abs-path (expand-file-name path base))
                     (rel-path (file-relative-name abs-path dir)))
                (org-element-put-property link :path rel-path)
                (org-element-put-property
                 link :raw-link (concat type ":" rel-path))))))
        plist)
      tree)))

(defun caolan/htmlize-relative (base dir filename plist)
  "Convert org file to html and make links relative to dir. Returns html string"
  (let ((nav (caolan/relative-tree base dir filename plist)))
    (with-temp-buffer
      (insert (org-element-interpret-data nav))
      (mark-whole-buffer)
      (org-export-replace-region-by 'html)
      (buffer-string))))

(defun my-website-postamble (plist)
  (let* ((file (plist-get plist :input-file))
         (dir (file-name-directory file))
         (base (file-name-as-directory (plist-get plist :base-directory)))
         (path-to-root (if base
                           (file-relative-name base dir)
                           "/"))
         (rss-plist (my-website-get-rss-plist plist filename))
         (rss-title (plist-get rss-plist :rss-title))
         (rss-file (plist-get rss-plist :rss-file))
         (rss-link (and rss-file
                        (file-relative-name
                         (expand-file-name rss-file base)
                         (file-name-directory (expand-file-name filename)))))
         (author (org-publish-format-file-entry "%a" file plist))
         (date (org-publish-format-file-entry "%d" file plist))
         (nav-file (plist-get plist :my-nav-file)))
    ;; make sure we're reading the latest BIND values:
    ;;(org-mode-restart)
    (concat
     "<nav id=\"menu\">" (caolan/htmlize-relative base dir nav-file plist) "</nav>"
       ;; "<div id=\"page-footer\">"
       ;; ;"<p class=\"date\">Page generated: " (format-time-string "%Y-%m-%d")
       ;; (if rss-link
       ;;     (concat
       ;;      "<p class=\"rss-link\">"
       ;;      "<a href=\"" rss-link "\">"
       ;;      "<img src=\"" path-to-root "static/img/rss.png\" />"
       ;;      "</a>&nbsp;"
       ;;      "<a href=\"" rss-link "\">"
       ;;      "RSS"
       ;;      "</a>"
       ;;      "</p>")
       ;;   "")
       ;; "<p class=\"author\">"
       ;; "Author: <a href=\"" path-to-root "index.html\">Caolan McMahon</a>"
       ;; "</p>"
       ;; (if date (concat "<p class=\"date\">Date: " date "</p>") "")
       ;; "<p class=\"back-to-top\"><a href=\"#\">Back to top</a></p>"
       ;; "</div>"
       )))

Build blog posts index and archives pages.

(defun my-website-get-blog-post-files (dir)
  "Returns a list of posts in the specified directory, excluding
   index.org and rss.org files"
  (let* ((old-base (plist-get project-plist :base-directory))
         (p (cons "website-pages"
                  (plist-put project-plist :base-directory dir)))
         (files (org-publish-get-base-files p
                 (concat "index\\.org\\'\\|"
                         "rss\\.org\\'\\|"
                         "archive\\.org\\'\\|"
                         "recent\\.org\\'"))))
    ;; plist-put may mutate underlying plist, so make sure we reset
    ;; base-directory
    (setq project-plist
          (plist-put project-plist :base-directory old-base))
        files))

(defun my-website-blog-post-metadata (file)
  "Extracts title and date from blog post .org file and returns an alist
   with the filename and these properties."
  (let* ((org-inhibit-startup t)
         (visiting (find-buffer-visiting file))
         (buffer (or visiting (find-file-noselect file))))
    (with-current-buffer buffer
      (let* ((env
              ;; protect local variables in open buffers
              (if visiting
                  (org-export-with-buffer-copy (org-export-get-environment))
                (org-export-get-environment)))
             (title (plist-get env :title))
             (date (plist-get env :date))
             (filetags (plist-get env :filetags)))
        (unless visiting (kill-buffer buffer))
        (list
         :title (if title
                    (org-no-properties
                     (org-element-interpret-data title))
                  (file-name-nondirectory (file-name-sans-extension file)))
         :date
         ;; DATE is either a timestamp object or a secondary string.  If it
         ;; is a timestamp or if the secondary string contains a timestamp,
         ;; convert it to internal format.  Otherwise, use FILE
         ;; modification time.
         (cond ((eq (org-element-type date) 'timestamp)
                (org-time-string-to-time (org-element-interpret-data date)))
               ((let ((ts (and (consp date) (assq 'timestamp date))))
                  (and ts
                       (let ((value (org-element-interpret-data ts)))
                         (and (org-string-nw-p value)
                              (org-time-string-to-time value))))))
               ((file-exists-p file) (nth 5 (file-attributes file)))
               (t (error "No such file: \"%s\"" file)))
         :filetags filetags
         :file file)))))

(defun my-website-get-blog-posts (dir)
  "Find all blog post files and return a list of blog post metadata,
   sorted in ascending date order (oldest first)."
  (sort
   (mapcar 'my-website-blog-post-metadata
           (my-website-get-blog-post-files dir))
   (lambda (a b)
     (time-less-p (plist-get a :date)
                  (plist-get b :date)))))

(defun my-website-get-preview (file)
    "Extract preview text a from blog post (between #+BEGIN_PREVIEW and
     #+END_PREVIEW). The comments in FILE have to be on their own lines,
     prefereably before and after paragraphs."
    (with-temp-buffer
      (insert-file-contents file)
      (goto-char (point-min))
      (let ((beg (+ 1 (re-search-forward "^#\\+BEGIN_PREVIEW$")))
            (end (progn (re-search-forward "^#\\+END_PREVIEW$")
                        (match-beginning 0))))
        (buffer-substring beg end))))

(defun my-website-get-full-text (file)
    "Extract body text from a blog post (between #+BEGIN_PREVIEW
     and the end of the document)."
    (with-temp-buffer
      (insert-file-contents file)
      (goto-char (point-min))
      (let ((preview-start (+ 1 (re-search-forward "^#\\+BEGIN_PREVIEW$"))))
        (re-search-forward "^#\\+END_PREVIEW$")
        (let ((preview-end (match-beginning 0))
              (body-start (match-end 0))
              (body-end (point-max)))
        (concat (buffer-substring preview-start preview-end)
                (buffer-substring body-start body-end))))))

(defun my-website-blog-take-posts (posts n &optional newlist)
  (if (or (= n 0) (not posts))
      newlist
    (my-website-blog-take-posts
     (cdr posts)
     (- n 1)
     (cons (car posts) newlist))))

(defun my-website-blog-get-recent (posts n &optional recent)
  "Returns the N most recent posts from the list, in ascending
   date order (oldest first)."
  (nreverse (my-website-blog-take-posts (reverse posts) n)))

(defun my-website-add-previews (posts)
  (mapcar
   (lambda (post)
     (let ((file (plist-get post :file)))
       (plist-put post :preview (my-website-get-preview file))))
   posts))

(defun my-website-add-texts (posts)
  (mapcar
   (lambda (post)
     (let ((file (plist-get post :file)))
       (plist-put post :text (my-website-get-full-text file))))
   posts))

(defun my-website-blog-posts-tags (base posts-dir posts)
  "Updates blog posts in the index.org of the posts directory"
  (let ((tags nil))
    (mapc (lambda (post)
            (mapc (lambda (tag)
                    (setq tags
                          (plist-put tags
                                     (intern tag)
                                     (cons post (plist-get tags (intern tag))))))
                    (plist-get post :filetags)))
          posts)
;;    (with-temp-buffer
    ;; (find-file (concat posts-dir "archive.org"))
  ;;   (erase-buffer)
  ;;   (insert "#+TITLE: Posts archive\n\n")
  ;;   (mapc (lambda (year)
  ;;         (insert (format "* %s\n" (car year)))
  ;;         (mapc (lambda (post)
  ;;                 (let* ((date (plist-get post :date))
  ;;                        (title (plist-get post :title))
  ;;                        (file (plist-get post :file))
  ;;                        (relative-path (file-relative-name file posts-dir)))
  ;;                   (insert
  ;;                    (format "+ %s [[file:%s][%s]]\n"
  ;;                            (format-time-string "%Y-%m-%d" date)
  ;;                            relative-path
  ;;                            title))))
  ;;               (cdr year)))
  ;;       (my-website-group-posts-by-year posts))
  ;; (save-buffer)))
    (print tags)))

(defun my-website-blog-posts-rss (base posts-dir posts)
  "Updates blog posts in the index.org of the posts directory"
  (with-temp-buffer
    (find-file (concat posts-dir "rss.org"))
    (erase-buffer)
    ;(insert "#+TITLE: Posts\n\n")
    ;(insert "* Most recent\n")
    (let ((recent (my-website-add-previews
                   (my-website-blog-get-recent posts 10))))
      (mapc (lambda (post)
              (let* ((date (plist-get post :date))
                     (title (plist-get post :title))
                     (preview (plist-get post :preview))
                     (file (plist-get post :file))
                     (relative-path (file-relative-name file posts-dir))
                     (link (concat (file-name-sans-extension relative-path) ".html"))
                     (rss-pubdate
                      (format-time-string "<%Y-%m-%d %a %H:%M>" date)))
                (insert (format "* [[file:%s][%s]]\n" relative-path title))
                (org-set-property "RSS_PERMALINK" link)
                (org-set-property "PUBDATE" rss-pubdate)
                ;; to avoid second update to rss.org by org-icalendar-create-uid
                (org-id-get-create)
                (insert preview)
                (insert (format " [[file:%s][Read more…]]\n\n" relative-path))
                (insert (format-time-string
                         "#+BEGIN_POSTDATE\n%a, %d %B %Y\n#+END_POSTDATE\n\n"
                         date))))
            recent))
    ;(insert "* Archive\n")
    ;(insert "You can find more posts in the [[file:archive.org][archive]].\n")
    (save-buffer)))

(defun my-website-blog-posts-recent (base posts-dir posts)
  "Updates blog posts in the recent.org file in the posts directory"
  (with-temp-buffer
    (find-file (concat base "recent.org"))
    (erase-buffer)
    (let ((recent (my-website-blog-get-recent posts 4)))
      (mapc (lambda (post)
              (let* ((date (plist-get post :date))
                     (title (plist-get post :title))
                     (file (plist-get post :file))
                     (relative-path (file-relative-name file base))
                     (link (concat (file-name-sans-extension relative-path) ".html")))
                (insert (format "+ [[file:%s][%s]]\n" relative-path title))))
            recent))
    (save-buffer)))

(defun my-website-group-posts-by-year (posts &optional years)
  "Group posts into an alist keyed by the year it was published,
assumes the posts list provided is already sorted in ascending
date order. Returns the alist in _descending_ date order."
  (if (not posts)
      years
    (let* ((post (car posts))
           (date (plist-get post :date))
           (year (format-time-string "%Y" date))
           (current-year (and years (car years))))
      (my-website-group-posts-by-year
       (cdr posts)
       (if (and current-year (equal (car current-year) year))
           (cons (cons year (cons post (cdr current-year)))
                 (cdr years))
         (cons (cons year (list post))
               years))))))

(defun my-website-blog-posts-archive (base posts-dir posts)
  "Updates complete blog posts listing in archive.org"
  (with-temp-buffer
    (find-file (concat posts-dir "archive.org"))
    (erase-buffer)
    (insert "#+TITLE: Posts archive\n\n")
    (mapc (lambda (year)
            (insert (format "* %s\n" (car year)))
            (mapc (lambda (post)
                    (let* ((date (plist-get post :date))
                           (title (plist-get post :title))
                           (file (plist-get post :file))
                           (relative-path (file-relative-name file posts-dir)))
                      (insert
                       (format "+ %s [[file:%s][%s]]\n"
                               (format-time-string "%Y-%m-%d" date)
                               relative-path
                               title))))
                  (cdr year)))
          (my-website-group-posts-by-year posts))
    (save-buffer)))

(defun my-blog-preprocessor (project-plist)
  (let* ((base (file-name-as-directory
                (plist-get project-plist :base-directory)))
         (posts-dir (expand-file-name "posts/" base))
         (posts (my-website-get-blog-posts posts-dir)))
    (print posts)
    (my-website-blog-posts-rss base posts-dir posts)
    (my-website-blog-posts-tags base posts-dir posts)
    (my-website-blog-posts-recent base posts-dir posts)
    (my-website-blog-posts-archive base posts-dir posts)))

Global keyboard shortcuts

(bind-key "C-c a" 'org-agenda)
(bind-key "C-c l" 'org-store-link)
(bind-key "C-c L" 'org-insert-link-global)
(bind-key "C-c O" 'org-open-at-point-global)

Presentations

(use-package ox-reveal
  :ensure t)

Invoicing

This uses LaTeX to generate PDFs, and the xelatex command needs to be available:

sudo apt-get install texlive-xetex

I also use Free Serif typeface in my invoice LaTeX template:

sudo apt-get install fonts-freefont-ttf

Code to generate invoices:

(defun caolan/org-table-last-row ()
  (interactive)
  (goto-char (org-table-end))
  (backward-char)
  (org-table-goto-column 1))

(defun caolan/org-table-append-row ()
  (interactive)
  (caolan/org-table-last-row)
  (org-table-insert-row 1))

(defun caolan/org-table-append-row-data (&rest cols)
  (caolan/org-table-append-row)
  (while cols
    (insert (car cols))
    (org-cycle)
    (setq cols (cdr cols))))

(defun invoice-find-rate-info (client rows)
  (if (equal nil rows)
      nil
    (let ((row (car rows)))
      (if (string-equal client (car row))
          (cdr row)
        (invoice-find-rate-info client (cdr rows))))))

(defun invoice-get-table (path)
  (with-temp-buffer
    ;; TODO: find-file is pointless inside with-temp-buffer
    (find-file path)
    (goto-char (point-min))
    (unless (org-at-table-p)
      (user-error "Table not on first line"))
    (org-table-align)          ; Make sure we have everything we need.
    (let ((table (org-table-to-lisp
                  (buffer-substring-no-properties
                   (org-table-begin) (org-table-end)))))
      (kill-buffer)
      (cddr table))))

(defun invoice-get-clients ()
  (with-temp-buffer
    (insert-file-contents "~/org/business/accounts/clients.org")
    (org-mode)
    (org-map-entries
     (lambda ()
       (let ((name (nth 4 (org-heading-components)))
             (props (org-entry-properties nil)))
         (cons name
               (cons (cons "PROJECTS"
                           (org-map-entries
                            (lambda ()
                              (cons (nth 4 (org-heading-components))
                                    (org-entry-properties nil)))
                            ":project:"
                            'tree))
                     props))))
     "LEVEL=1")))

(defun invoice-get-sales ()
  (invoice-get-table "~/org/business/accounts/sales.org"))

(defun invoice-get-timesheet ()
  (invoice-get-table "~/org/business/accounts/timesheet.org"))

(defun invoice-get-last-invoice (sales client-name &optional project-name)
  (let* ((client-name (string-trim client-name))
         (invoices (remove-if-not
                    (lambda (x)
                      (and (string-equal (string-trim (nth 2 x)) client-name)
                           (or (not project-name)
                               (string-equal (string-trim (nth 3 x))
                                             project-name))))
                    (sort sales
                          (lambda (a b)
                            (string-lessp (car a) (car b)))))))
    (and invoices
         (car (last invoices)))))

(defun invoice-get-client-names (clients)
  (let ((sales (invoice-get-sales)))
    (mapcar #'cdr
            (sort (mapcar
                   (lambda (client)
                     (let ((last-invoice (invoice-get-last-invoice sales (car client))))
                       (cons (and last-invoice
                                  (org-parse-time-string (car last-invoice)))
                             (car client))))
                   clients)
                  ;; sort clients by most recently invoiced,
                  ;; or alphabetical if not invoiced
                  (lambda (a b)
                    (cond ((and (not (car a)) (not (car b)))
                           (string-lessp (cdr a) (cdr b)))
                          ((not (car b)) t)
                          ((not (car a)) nil)
                          (t (time-less-p (car b) (car a)))))))))

(defun invoice-get-project-names (client)
  (message "get-project-names")
  (print client)
  (let ((projects (assoc-default "PROJECTS" client))
        (sales (invoice-get-sales)))
    (print projects)
    (mapcar #'cdr
            (sort (mapcar
                   (lambda (project)
                     (print project)
                     (let ((last-invoice (invoice-get-last-invoice
                                          sales
                                          (assoc-default "ITEM" client)
                                          (car project))))
                       (cons (and last-invoice
                                  (org-parse-time-string (car last-invoice)))
                             (car project))))
                   projects)
                  ;; sort clients by most recently invoiced,
                  ;; or alphabetical if not invoiced
                  (lambda (a b)
                    (cond ((and (not (car a)) (not (car b)))
                           (string-lessp (cdr a) (cdr b)))
                          ((not (car b)) t)
                          ((not (car a)) nil)
                          (t (time-less-p (car b) (car a)))))))))

(defun invoice-format-date (date)
  (format-time-string "%Y-%m-%d" date))

(defun invoice-format-date-email (date)
  (format-time-string "%d %B %Y" 
    (apply #'encode-time (org-parse-time-string date))))

(defun invoice-format-org-date-string (date)
  (invoice-format-date (apply #'encode-time (org-parse-time-string date))))

(defun invoice-check-timesheet-up-to-date ()
  (let* ((rows (invoice-get-timesheet))
         (last-row (car (last rows)))
         (last-date (invoice-format-org-date-string (car last-row))))
    (if (not (string-equal last-date (invoice-format-date (org-current-time))))
        (progn
          (find-file "~/org/business/accounts/timesheet.org")
          (user-error "Timesheet not up to date, add missing entries then try again."))
      (message "Timesheet up to date"))))

(defun invoice-add-item (items name count)
  (if (equal nil items)
      (list (cons name count))
    (let ((item (car items)))
      (if (string-equal name (car item))
          (cons (cons name (+ count (cdr item)))
                (cdr items))
        (cons (car item)
              (invoice-add-item (cdr items) name count))))))

(defun invoice-items (client-name project-name start end)
  (let ((rows (remove-if
               (lambda (row)
                 (let ((date (invoice-format-org-date-string (car row))))
                   (or (not (string-equal (nth 1 row) client-name))
                       (not (string-equal (nth 2 row) project-name))
                       (string-lessp date start)
                       (string-lessp end date))))
               (invoice-get-timesheet))))
    (reduce (lambda (items row)
              (invoice-add-item items
                                (nth 3 row)
                                (string-to-number (nth 4 row))))
            rows
            :initial-value '())))

(defun invoice-format (id date client project-name rate items)
  (concat "\\documentclass{../gc_invoice}

\\date{" (format-time-string "%d. %B %Y" date) "}
\\client{" (assoc-default "ITEM" client) "}
\\clientaddress{" (assoc-default "ADDRESS" client #'equal "") "}
\\invoiceno{" (number-to-string id) "}

\\begin{document}
  \\begin{gc_invoice}
    \\begin{invoice}{GBP}{0}
      \\ProjectTitle{" project-name "}
      " (string-join
         (mapcar (lambda (item)
                   (concat "\\Fee{" (car item) "} {" (number-to-string rate) "} {"
                           (number-to-string (cdr item)) "}")) 
                 items)
         "\n") "
    \\end{invoice}
  \\end{gc_invoice}
\\end{document}"))
(defun write-file-p (filename)
  (or (not (file-exists-p tex-filename))
      (y-or-n-p (concat "File " tex-filename " already exists, overwrite?"))))

(defun invoice (&optional client-name)
  (interactive)
  (invoice-check-timesheet-up-to-date)
  (let* ((clients (invoice-get-clients))
         (client-name (or client-name
                          (ido-completing-read "Client: "
                                               (invoice-get-client-names clients))))
         (client (assoc-default client-name clients))
         (projects (assoc-default "PROJECTS" client))
         (project-name (ido-completing-read "Project: "
                                            (invoice-get-project-names client)))
         ;; (project (assoc-default project-name projects))
         (last-invoice (invoice-get-last-invoice (invoice-get-sales)
                                                 client-name
                                                 project-name))
         (last-invoice-id (string-to-number (cadr last-invoice)))
         (invoice-id (+ 1 last-invoice-id))
         ;; (last-invoice-time-str (car last-invoice))
         (last-invoice-time (apply #'encode-time
                                   (org-parse-time-string (car last-invoice))))
          ;; by default invoice invoice from day after last invoice
         ;; (default-start
         ;;   (apply #'encode-time
         ;;          (org-parse-time-string
         ;;           (org-read-date nil nil "++1" nil
         ;;                          (org-time-string-to-time last-invoice-time-str)))))
         (start (org-read-date nil nil nil "Invoice from" last-invoice-time "++1"))
         (end (org-read-date nil nil nil "Invoice to"
                             ;; (apply #'encode-time (org-parse-time-string start))
                             last-invoice-time
                             (concat "++" (assoc-default "INVOICE_INTERVAL" client))))
         (invoice-date (apply #'encode-time (org-parse-time-string end)))
         (items (invoice-items client-name project-name start end))
         (output-directory
          (expand-file-name "~/org/business/invoices/sales/unpaid/"))
         (base-filename (concat output-directory
                               (number-to-string invoice-id)
                               "." (assoc-default "CUSTOM_ID" client) "." end))
         (tex-filename (concat base-filename ".tex"))
         (pdf-filename (concat base-filename ".pdf"))
         (rate (string-to-number (assoc-default "RATE" client))))
  (when (write-file-p tex-filename)
    (with-temp-buffer
      (insert
       (invoice-format invoice-id invoice-date client project-name rate items))
      (write-file tex-filename)
      (set (make-local-variable 'default-directory) output-directory)
      (call-process-shell-command
       (concat "xelatex "
               (shell-quote-argument
                (file-relative-name tex-filename output-directory)))
       nil "*invoice xelatex*" t))
    ;; update sales table
    (find-file "~/org/business/accounts/sales.org")
    (caolan/org-table-append-row-data
     end
     (number-to-string invoice-id)
     client-name
     project-name
     (number-to-string (* rate (reduce #'+ (mapcar #'cdr items)))))
    (save-buffer)
    ;; display PDF and create new email ready to send
    (let ((pdf-buffer (find-file pdf-filename)))
      (message-mail-other-window
       (assoc-default "INVOICE_RECIPIENT" client)
       (concat "Invoice #" (number-to-string invoice-id)))
      (insert "Please find my invoice attached. "
              "This covers the period from " (invoice-format-date-email start)
              " to " (invoice-format-date-email end) ".\n"
              "\n"
              "Many thanks,\n"
              "\n"
              "Caolan McMahon\n"
              "\n"
              "---\n"
              "\n"
              "Ground Computing Ltd. is a company registered in England and Wales. Registered number: 08051370. Registered office: 4 Cross Street, Beeston, Nottingham, NG9 2NX\n"
              "\n")
      (mml-attach-file pdf-filename))
    )))