16 Aug 2019

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)

Darktooth

(use-package darktooth-theme
  :ensure t)

(load-theme 'darktooth 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 12")

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.

;; ;; (when (window-system)
;; ;;   (set-frame-font "Fira Code 10"))
;; ;; (let ((alist '((33 . ".\\(?:\\(?:==\\|!!\\)\\|[!=]\\)")
;; ;;                (35 . ".\\(?:###\\|##\\|_(\\|[#(?[_{]\\)")
;; ;;                (36 . ".\\(?:>\\)")
;; ;;                (37 . ".\\(?:\\(?:%%\\)\\|%\\)")
;; ;;                (38 . ".\\(?:\\(?:&&\\)\\|&\\)")
;; ;;                (42 . ".\\(?:\\(?:\\*\\*/\\)\\|\\(?:\\*[*/]\\)\\|[*/>]\\)")
;; ;;                (43 . ".\\(?:\\(?:\\+\\+\\)\\|[+>]\\)")
;; ;;                (45 . ".\\(?:\\(?:-[>-]\\|<<\\|>>\\)\\|[<>}~-]\\)")
;; ;;                (46 . ".\\(?:\\(?:\\.[.<]\\)\\|[.=-]\\)")
;; ;;                (47 . ".\\(?:\\(?:\\*\\*\\|//\\|==\\)\\|[*/=>]\\)")
;; ;;                (48 . ".\\(?:x[a-zA-Z]\\)")
;; ;;                (58 . ".\\(?:::\\|[:=]\\)")
;; ;;                (59 . ".\\(?:;;\\|;\\)")
;; ;;                (60 . ".\\(?:\\(?:!--\\)\\|\\(?:~~\\|->\\|\\$>\\|\\*>\\|\\+>\\|--\\|<[<=-]\\|=[<=>]\\||>\\)\\|[*$+~/<=>|-]\\)")
;; ;;                (61 . ".\\(?:\\(?:/=\\|:=\\|<<\\|=[=>]\\|>>\\)\\|[<=>~]\\)")
;; ;;                (62 . ".\\(?:\\(?:=>\\|>[=>-]\\)\\|[=>-]\\)")
;; ;;                (63 . ".\\(?:\\(\\?\\?\\)\\|[:=?]\\)")
;; ;;                (91 . ".\\(?:]\\)")
;; ;;                (92 . ".\\(?:\\(?:\\\\\\\\\\)\\|\\\\\\)")
;; ;;                (94 . ".\\(?:=\\)")
;; ;;                (119 . ".\\(?:ww\\)")
;; ;;                (123 . ".\\(?:-\\)")
;; ;;                (124 . ".\\(?:\\(?:|[=|]\\)\\|[=>|]\\)")
;; ;;                (126 . ".\\(?:~>\\|~~\\|[>=@~-]\\)")
;; ;;                )
;; ;;              ))
;; ;;   (dolist (char-regexp alist)
;; ;;     (set-char-table-range composition-function-table (car char-regexp)
;; ;;                           `([,(cdr char-regexp) 0 font-shape-gstring]))))

(set-default-font "Fira Code:spacing=110:size=16")
;(set-default-font "Fira Code 12")
;(set-default-font "Fira Code 10")

;; Fira code
;; ;; This works when using emacs --daemon + emacsclient
;; (add-hook 'after-make-frame-functions (lambda (frame) (set-fontset-font t '(#Xe100 . #Xe16f) "Fira Code Symbol")))
;; ;; This works when using emacs without server/client
;; (set-fontset-font t '(#Xe100 . #Xe16f) "Fira Code Symbol")
;; ;; I haven't found one statement that makes both of the above situations work, so I use both for now

;; (defconst fira-code-font-lock-keywords-alist
;;   (mapcar (lambda (regex-char-pair)
;;             `(,(car regex-char-pair)
;;               (0 (prog1 ()
;;                    (compose-region (match-beginning 1)
;;                                    (match-end 1)
;;                                    ;; The first argument to concat is a string containing a literal tab
;;                                    ,(concat "        " (list (decode-char 'ucs (cadr regex-char-pair)))))))))
;;           '(("\\(www\\)"                   #Xe100)
;;             ("[^/]\\(\\*\\*\\)[^/]"        #Xe101)
;;             ("\\(\\*\\*\\*\\)"             #Xe102)
;;             ("\\(\\*\\*/\\)"               #Xe103)
;;             ("\\(\\*>\\)"                  #Xe104)
;;             ("[^*]\\(\\*/\\)"              #Xe105)
;;             ("\\(\\\\\\\\\\)"              #Xe106)
;;             ("\\(\\\\\\\\\\\\\\)"          #Xe107)
;;             ("\\({-\\)"                    #Xe108)
;;             ("\\(\\[\\]\\)"                #Xe109)
;;             ("\\(::\\)"                    #Xe10a)
;;             ("\\(:::\\)"                   #Xe10b)
;;             ("[^=]\\(:=\\)"                #Xe10c)
;;             ("\\(!!\\)"                    #Xe10d)
;;             ("\\(!=\\)"                    #Xe10e)
;;             ("\\(!==\\)"                   #Xe10f)
;;             ("\\(-}\\)"                    #Xe110)
;;             ("\\(--\\)"                    #Xe111)
;;             ("\\(---\\)"                   #Xe112)
;;             ("\\(-->\\)"                   #Xe113)
;;             ("[^-]\\(->\\)"                #Xe114)
;;             ("\\(->>\\)"                   #Xe115)
;;             ("\\(-<\\)"                    #Xe116)
;;             ("\\(-<<\\)"                   #Xe117)
;;             ("\\(-~\\)"                    #Xe118)
;;             ("\\(#{\\)"                    #Xe119)
;;             ("\\(#\\[\\)"                  #Xe11a)
;;             ("\\(##\\)"                    #Xe11b)
;;             ("\\(###\\)"                   #Xe11c)
;;             ("\\(####\\)"                  #Xe11d)
;;             ("\\(#(\\)"                    #Xe11e)
;;             ("\\(#\\?\\)"                  #Xe11f)
;;             ("\\(#_\\)"                    #Xe120)
;;             ("\\(#_(\\)"                   #Xe121)
;;             ("\\(\\.-\\)"                  #Xe122)
;;             ("\\(\\.=\\)"                  #Xe123)
;;             ("\\(\\.\\.\\)"                #Xe124)
;;             ("\\(\\.\\.<\\)"               #Xe125)
;;             ("\\(\\.\\.\\.\\)"             #Xe126)
;;             ("\\(\\?=\\)"                  #Xe127)
;;             ("\\(\\?\\?\\)"                #Xe128)
;;             ("\\(;;\\)"                    #Xe129)
;;             ("\\(/\\*\\)"                  #Xe12a)
;;             ("\\(/\\*\\*\\)"               #Xe12b)
;;             ("\\(/=\\)"                    #Xe12c)
;;             ("\\(/==\\)"                   #Xe12d)
;;             ("\\(/>\\)"                    #Xe12e)
;;             ("\\(//\\)"                    #Xe12f)
;;             ("\\(///\\)"                   #Xe130)
;;             ("\\(&&\\)"                    #Xe131)
;;             ("\\(||\\)"                    #Xe132)
;;             ("\\(||=\\)"                   #Xe133)
;;             ("[^|]\\(|=\\)"                #Xe134)
;;             ("\\(|>\\)"                    #Xe135)
;;             ("\\(\\^=\\)"                  #Xe136)
;;             ("\\(\\$>\\)"                  #Xe137)
;;             ("\\(\\+\\+\\)"                #Xe138)
;;             ("\\(\\+\\+\\+\\)"             #Xe139)
;;             ("\\(\\+>\\)"                  #Xe13a)
;;             ("\\(=:=\\)"                   #Xe13b)
;;             ("[^!/]\\(==\\)[^>]"           #Xe13c)
;;             ("\\(===\\)"                   #Xe13d)
;;             ("\\(==>\\)"                   #Xe13e)
;;             ("[^=]\\(=>\\)"                #Xe13f)
;;             ("\\(=>>\\)"                   #Xe140)
;;             ("\\(<=\\)"                    #Xe141)
;;             ("\\(=<<\\)"                   #Xe142)
;;             ("\\(=/=\\)"                   #Xe143)
;;             ("\\(>-\\)"                    #Xe144)
;;             ("\\(>=\\)"                    #Xe145)
;;             ("\\(>=>\\)"                   #Xe146)
;;             ("[^-=]\\(>>\\)"               #Xe147)
;;             ("\\(>>-\\)"                   #Xe148)
;;             ("\\(>>=\\)"                   #Xe149)
;;             ("\\(>>>\\)"                   #Xe14a)
;;             ("\\(<\\*\\)"                  #Xe14b)
;;             ("\\(<\\*>\\)"                 #Xe14c)
;;             ("\\(<|\\)"                    #Xe14d)
;;             ("\\(<|>\\)"                   #Xe14e)
;;             ("\\(<\\$\\)"                  #Xe14f)
;;             ("\\(<\\$>\\)"                 #Xe150)
;;             ("\\(<!--\\)"                  #Xe151)
;;             ("\\(<-\\)"                    #Xe152)
;;             ("\\(<--\\)"                   #Xe153)
;;             ("\\(<->\\)"                   #Xe154)
;;             ("\\(<\\+\\)"                  #Xe155)
;;             ("\\(<\\+>\\)"                 #Xe156)
;;             ("\\(<=\\)"                    #Xe157)
;;             ("\\(<==\\)"                   #Xe158)
;;             ("\\(<=>\\)"                   #Xe159)
;;             ("\\(<=<\\)"                   #Xe15a)
;;             ("\\(<>\\)"                    #Xe15b)
;;             ("[^-=]\\(<<\\)"               #Xe15c)
;;             ("\\(<<-\\)"                   #Xe15d)
;;             ("\\(<<=\\)"                   #Xe15e)
;;             ("\\(<<<\\)"                   #Xe15f)
;;             ("\\(<~\\)"                    #Xe160)
;;             ("\\(<~~\\)"                   #Xe161)
;;             ("\\(</\\)"                    #Xe162)
;;             ("\\(</>\\)"                   #Xe163)
;;             ("\\(~@\\)"                    #Xe164)
;;             ("\\(~-\\)"                    #Xe165)
;;             ("\\(~=\\)"                    #Xe166)
;;             ("\\(~>\\)"                    #Xe167)
;;             ("[^<]\\(~~\\)"                #Xe168)
;;             ("\\(~~>\\)"                   #Xe169)
;;             ("\\(%%\\)"                    #Xe16a)
;;            ;; ("\\(x\\)"                   #Xe16b) This ended up being hard to do properly so i'm leaving it out.
;;             ("[^:=]\\(:\\)[^:=]"           #Xe16c)
;;             ("[^\\+<>]\\(\\+\\)[^\\+<>]"   #Xe16d)
;;             ("[^\\*/<>]\\(\\*\\)[^\\*/<>]" #Xe16f))))

;; (defun add-fira-code-symbol-keywords ()
;;   (font-lock-add-keywords nil fira-code-font-lock-keywords-alist))

;; (add-hook 'prog-mode-hook
;;           #'add-fira-code-symbol-keywords)

Courier Prime Code

From https://quoteunquoteapps.com/courierprime/

;; (set-default-font "Courier Prime Code 14")

PT Mono

From https://company.paratype.com/pt-sans-pt-serif

;; (set-default-font "PT Mono 13")

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.

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:" (getenv "PATH")))

Add cargo bin directory to PATH.

(setenv "PATH" (concat "/home/caolan/.cargo/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?\\'" . web-mode)
         ("\\.tsx\\'" . web-mode)
         ("\\.jsx\\'" . web-mode))
  :config
  (setq web-mode-markup-indent-offset 4
        web-mode-css-indent-offset 4
        web-mode-code-indent-offset 4
        web-mode-block-padding 4
        web-mode-comment-style 4

        web-mode-enable-css-colorization t
        web-mode-enable-auto-pairing t
        web-mode-enable-comment-keywords t
        web-mode-enable-current-element-highlight t
        web-mode-enable-auto-expanding t
        web-mode-enable-auto-indentation nil
        )
  (add-hook 'web-mode-hook
            (lambda ()
              (when (and buffer-file-name
                         (equal "tsx" (file-name-extension buffer-file-name)))
                (setup-tide-mode))))
  ;; enable typescript-tslint checker
  (flycheck-add-mode 'typescript-tslint 'web-mode))

Turn on Rainbow mode for CSS and HTML.

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

JavaScript (and Typescript)

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))

Typescript mode.

(use-package typescript-mode
  :ensure t)

Add convenient commands for running karma tests.

(use-package karma
  :ensure t)

;; mode hooks are added for plain js modes files by default
(add-hook 'typescript-mode-hook 'karma-mode-hook)

TypeScript interactive development environment.

(use-package tide
  :ensure t)

(defun setup-tide-mode ()
  (interactive)
  (tide-setup)
  (flycheck-mode +1)
  (setq flycheck-check-syntax-automatically '(save mode-enabled))
  (eldoc-mode +1)
  (tide-hl-identifier-mode +1)
  ;; company is an optional dependency. You have to
  ;; install it separately via package-install
  ;; `M-x package-install [ret] company`
  (company-mode +1))

;; aligns annotation to the right hand side
(setq company-tooltip-align-annotations t)

;; formats the buffer before saving
(add-hook 'before-save-hook 'tide-format-before-save)

(add-hook 'typescript-mode-hook #'setup-tide-mode)

Python

Enable flycheck.

(add-hook 'python-mode-hook #'flycheck-mode)
(setq flycheck-python-flake8-executable "/usr/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)

Rust

For racer:

$ rustup component add rust-src
$ cargo install racer

Config based on http://www.jasonamyers.com/2015/rust-emacs/

(use-package company :ensure t)
(use-package company-racer :ensure t)
(use-package racer :ensure t)
;; (use-package flycheck :ensure t)
;; (use-package flycheck-rust :ensure t)
(use-package rust-mode :ensure t)

(setq company-racer-executable "/home/caolan/.cargo/bin/racer")
(setq racer-rust-src-path "/home/caolan/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src")

;; Reduce the time after which the company auto completion popup opens
(setq company-idle-delay 0.2)

;; Reduce the number of characters before company kicks in
(setq company-minimum-prefix-length 1)

;; ;; ;; Set path to racer binary
;; ;; (setq racer-cmd "/usr/local/bin/racer")

;; ;; ;; Set path to rust src directory
;; ;; (setq racer-rust-src-path "/Users/jasomyer/.rust/src/")

;; Load rust-mode when you open `.rs` files
(add-to-list 'auto-mode-alist '("\\.rs\\'" . rust-mode))

;; ;; Setting up configurations when you load rust-mode
(add-hook 'racer-mode-hook #'company-mode)
(add-hook 'rust-mode-hook #'racer-mode)
(add-hook 'racer-mode-hook #'eldoc-mode)
(add-hook 'racer-mode-hook #'company-mode)

(define-key rust-mode-map (kbd "TAB") #'company-indent-or-complete-common)
(setq company-tooltip-align-annotations t)

(with-eval-after-load 'company
  (add-to-list 'company-backends 'company-racer))

;; (with-eval-after-load 'rust-mode
;;   (add-hook 'flycheck-mode-hook #'flycheck-rust-setup))

ini files

(use-package ini-mode
  :ensure t)

TOML files

(use-package toml-mode
  :ensure t)

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).

Remove trailing whitespace

(add-hook 'before-save-hook 'delete-trailing-whitespace)

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)))

Send confirmation

When sending mail to *@lists.sr.ht, check to see if the message is hard-wrapped at 72 chars. As per the mailing list etiquette.

(defun caolan/line-length ()
  (save-excursion (end-of-line) (current-column)))

(defun caolan/max-line-length-p (max)
  (goto-char (point-min))
  (while (and (not (eobp)) (<= (caolan/line-length) max))
    (forward-line 1))
  (eobp))

(add-hook 'message-send-hook
          (lambda ()
            ;; ask for confirmation line-length > 72 when sending to
            ;; lists.sr.ht, see https://man.sr.ht/lists.sr.ht/etiquette.md
            (if (string-match-p
                 (regexp-quote "@lists.sr.ht")
                 (message-field-value "to"))
                (unless (or (caolan/max-line-length-p 72)
                            (yes-or-no-p "Email to lists.sr.ht not hard wrapped at 72 chars, send anyway? "))
                  (signal 'quit nil)))))

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)

Org-mode

ox-rss from org-mode contrib

;;; ox-rss.el --- RSS 2.0 Back-End for Org Export Engine

;; Copyright (C) 2013-2015  Bastien Guerry

;; Author: Bastien Guerry <bzg@gnu.org>
;; Keywords: org, wp, blog, feed, rss

;; This file is not yet part of GNU Emacs.

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; This library implements a RSS 2.0 back-end for Org exporter, based on
;; the `html' back-end.
;;
;; It requires Emacs 24.1 at least.
;;
;; It provides two commands for export, depending on the desired output:
;; `org-rss-export-as-rss' (temporary buffer) and `org-rss-export-to-rss'
;; (as a ".xml" file).
;;
;; This backend understands three new option keywords:
;;
;; #+RSS_EXTENSION: xml
;; #+RSS_IMAGE_URL: http://myblog.org/mypicture.jpg
;; #+RSS_FEED_URL: http://myblog.org/feeds/blog.xml
;;
;; It uses #+HTML_LINK_HOME: to set the base url of the feed.
;;
;; Exporting an Org file to RSS modifies each top-level entry by adding a
;; PUBDATE property.  If `org-rss-use-entry-url-as-guid', it will also add
;; an ID property, later used as the guid for the feed's item.
;;
;; The top-level headline is used as the title of each RSS item unless
;; an RSS_TITLE property is set on the headline.
;;
;; You typically want to use it within a publishing project like this:
;;
;; (add-to-list
;;  'org-publish-project-alist
;;  '("homepage_rss"
;;    :base-directory "~/myhomepage/"
;;    :base-extension "org"
;;    :rss-image-url "http://lumiere.ens.fr/~guerry/images/faces/15.png"
;;    :html-link-home "http://lumiere.ens.fr/~guerry/"
;;    :html-link-use-abs-url t
;;    :rss-extension "xml"
;;    :publishing-directory "/home/guerry/public_html/"
;;    :publishing-function (org-rss-publish-to-rss)
;;    :section-numbers nil
;;    :exclude ".*"            ;; To exclude all files...
;;    :include ("index.org")   ;; ... except index.org.
;;    :table-of-contents nil))
;;
;; ... then rsync /home/guerry/public_html/ with your server.
;;
;; By default, the permalink for a blog entry points to the headline.
;; You can specify a different one by using the :RSS_PERMALINK:
;; property within an entry.

;;; Code:

(require 'ox-html)
(declare-function url-encode-url "url-util" (url))

;;; Variables and options

(defgroup org-export-rss nil
  "Options specific to RSS export back-end."
  :tag "Org RSS"
  :group 'org-export
  :version "24.4"
  :package-version '(Org . "8.0"))

(defcustom org-rss-image-url "https://orgmode.org/img/org-mode-unicorn-logo.png"
  "The URL of the an image for the RSS feed."
  :group 'org-export-rss
  :type 'string)

(defcustom org-rss-extension "xml"
  "File extension for the RSS 2.0 feed."
  :group 'org-export-rss
  :type 'string)

(defcustom org-rss-categories 'from-tags
  "Where to extract items category information from.
The default is to extract categories from the tags of the
headlines.  When set to another value, extract the category
from the :CATEGORY: property of the entry."
  :group 'org-export-rss
  :type '(choice
          (const :tag "From tags" from-tags)
          (const :tag "From the category property" from-category)))

(defcustom org-rss-use-entry-url-as-guid t
  "Use the URL for the <guid> metatag?
When nil, Org will create ids using `org-icalendar-create-uid'."
  :group 'org-export-rss
  :type 'boolean)

;;; Define backend

(org-export-define-derived-backend 'rss 'html
  :menu-entry
  '(?r "Export to RSS"
       ((?R "As RSS buffer"
            (lambda (a s v b) (org-rss-export-as-rss a s v)))
        (?r "As RSS file" (lambda (a s v b) (org-rss-export-to-rss a s v)))
        (?o "As RSS file and open"
            (lambda (a s v b)
              (if a (org-rss-export-to-rss t s v)
                (org-open-file (org-rss-export-to-rss nil s v)))))))
  :options-alist
  '((:description "DESCRIPTION" nil nil newline)
    (:keywords "KEYWORDS" nil nil space)
    (:with-toc nil nil nil) ;; Never include HTML's toc
    (:rss-extension "RSS_EXTENSION" nil org-rss-extension)
    (:rss-image-url "RSS_IMAGE_URL" nil org-rss-image-url)
    (:rss-feed-url "RSS_FEED_URL" nil nil t)
    (:rss-categories nil nil org-rss-categories))
  :filters-alist '((:filter-final-output . org-rss-final-function))
  :translate-alist '((headline . org-rss-headline)
                     (comment . (lambda (&rest args) ""))
                     (comment-block . (lambda (&rest args) ""))
                     (timestamp . (lambda (&rest args) ""))
                     (plain-text . org-rss-plain-text)
                     (section . org-rss-section)
                     (template . org-rss-template)))

;;; Export functions

;;;###autoload
(defun org-rss-export-as-rss (&optional async subtreep visible-only)
  "Export current buffer to a RSS buffer.

If narrowing is active in the current buffer, only export its
narrowed part.

If a region is active, export that region.

A non-nil optional argument ASYNC means the process should happen
asynchronously.  The resulting buffer should be accessible
through the `org-export-stack' interface.

When optional argument SUBTREEP is non-nil, export the sub-tree
at point, extracting information from the headline properties
first.

When optional argument VISIBLE-ONLY is non-nil, don't export
contents of hidden elements.

Export is done in a buffer named \"*Org RSS Export*\", which will
be displayed when `org-export-show-temporary-export-buffer' is
non-nil."
  (interactive)
  (let ((file (buffer-file-name (buffer-base-buffer))))
    (org-icalendar-create-uid file 'warn-user)
    (org-rss-add-pubdate-property))
  (org-export-to-buffer 'rss "*Org RSS Export*"
    async subtreep visible-only nil nil (lambda () (text-mode))))

;;;###autoload
(defun org-rss-export-to-rss (&optional async subtreep visible-only)
  "Export current buffer to a RSS file.

If narrowing is active in the current buffer, only export its
narrowed part.

If a region is active, export that region.

A non-nil optional argument ASYNC means the process should happen
asynchronously.  The resulting file should be accessible through
the `org-export-stack' interface.

When optional argument SUBTREEP is non-nil, export the sub-tree
at point, extracting information from the headline properties
first.

When optional argument VISIBLE-ONLY is non-nil, don't export
contents of hidden elements.

Return output file's name."
  (interactive)
  (let ((file (buffer-file-name (buffer-base-buffer))))
    (org-icalendar-create-uid file 'warn-user)
    (org-rss-add-pubdate-property))
  (let ((outfile (org-export-output-file-name
                  (concat "." org-rss-extension) subtreep)))
    (org-export-to-file 'rss outfile async subtreep visible-only)))

;;;###autoload
(defun org-rss-publish-to-rss (plist filename pub-dir)
  "Publish an org file to RSS.

FILENAME is the filename of the Org file to be published.  PLIST
is the property list for the given project.  PUB-DIR is the
publishing directory.

Return output file name."
  (let ((bf (get-file-buffer filename)))
    (if bf
          (with-current-buffer bf
            (org-icalendar-create-uid filename 'warn-user)
            (org-rss-add-pubdate-property)
            (write-file filename))
      (find-file filename)
      (org-icalendar-create-uid filename 'warn-user)
      (org-rss-add-pubdate-property)
      (write-file filename) (kill-buffer)))
  (org-publish-org-to
   'rss filename (concat "." org-rss-extension) plist pub-dir))

;;; Main transcoding functions

(defun org-rss-headline (headline contents info)
  "Transcode HEADLINE element into RSS format.
CONTENTS is the headline contents.  INFO is a plist used as a
communication channel."
  (if (> (org-export-get-relative-level headline info) 1)
      (org-export-data-with-backend headline 'html info)
    (unless (org-element-property :footnote-section-p headline)
      (let* ((email (org-export-data (plist-get info :email) info))
             (author (and (plist-get info :with-author)
                          (let ((auth (plist-get info :author)))
                            (and auth (org-export-data auth info)))))
             (htmlext (plist-get info :html-extension))
             (hl-number (org-export-get-headline-number headline info))
             (hl-home (file-name-as-directory (plist-get info :html-link-home)))
             (hl-pdir (plist-get info :publishing-directory))
             (hl-perm (org-element-property :RSS_PERMALINK headline))
             (anchor (org-export-get-reference headline info))
             (category (org-rss-plain-text
                        (or (org-element-property :CATEGORY headline) "") info))
             (pubdate0 (org-element-property :PUBDATE headline))
             (pubdate (let ((system-time-locale "C"))
                        (if pubdate0
                            (format-time-string
                             "%a, %d %b %Y %H:%M:%S %z"
                             (org-time-string-to-time pubdate0)))))
             (title (org-rss-plain-text
                     (or (org-element-property :RSS_TITLE headline)
                         (replace-regexp-in-string
                          org-bracket-link-regexp
                          (lambda (m) (or (match-string 3 m)
                                          (match-string 1 m)))
                          (org-element-property :raw-value headline))) info))
             (publink
              (or (and hl-perm (concat (or hl-home hl-pdir) hl-perm))
                  (concat
                   (or hl-home hl-pdir)
                   (file-name-nondirectory
                    (file-name-sans-extension
                     (plist-get info :input-file))) "." htmlext "#" anchor)))
             (guid (if org-rss-use-entry-url-as-guid
                       publink
                     (org-rss-plain-text
                      (or (org-element-property :ID headline)
                          (org-element-property :CUSTOM_ID headline)
                          publink)
                      info))))
        (if (not pubdate0) "" ;; Skip entries with no PUBDATE prop
          (format
           (concat
            "<item>\n"
            "<title>%s</title>\n"
            "<link>%s</link>\n"
            "<author>%s (%s)</author>\n"
            "<guid isPermaLink=\"false\">%s</guid>\n"
            "<pubDate>%s</pubDate>\n"
            (org-rss-build-categories headline info) "\n"
            "<description><![CDATA[%s]]></description>\n"
            "</item>\n")
           title publink email author guid pubdate contents))))))

(defun org-rss-build-categories (headline info)
  "Build categories for the RSS item."
  (if (eq (plist-get info :rss-categories) 'from-tags)
      (mapconcat
       (lambda (c) (format "<category><![CDATA[%s]]></category>" c))
       (org-element-property :tags headline)
       "\n")
    (let ((c (org-element-property :CATEGORY headline)))
      (format "<category><![CDATA[%s]]></category>" c))))

(defun org-rss-template (contents info)
  "Return complete document string after RSS conversion.
CONTENTS is the transcoded contents string.  INFO is a plist used
as a communication channel."
  (concat
   (format "<?xml version=\"1.0\" encoding=\"%s\"?>"
           (symbol-name org-html-coding-system))
   "\n<rss version=\"2.0\"
        xmlns:content=\"http://purl.org/rss/1.0/modules/content/\"
        xmlns:wfw=\"http://wellformedweb.org/CommentAPI/\"
        xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
        xmlns:atom=\"http://www.w3.org/2005/Atom\"
        xmlns:sy=\"http://purl.org/rss/1.0/modules/syndication/\"
        xmlns:slash=\"http://purl.org/rss/1.0/modules/slash/\"
        xmlns:georss=\"http://www.georss.org/georss\"
        xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\"
        xmlns:media=\"http://search.yahoo.com/mrss/\">"
   "<channel>"
   (org-rss-build-channel-info info) "\n"
   contents
   "</channel>\n"
   "</rss>"))

(defun org-rss-build-channel-info (info)
  "Build the RSS channel information."
  (let* ((system-time-locale "C")
         (title (org-export-data (plist-get info :title) info))
         (email (org-export-data (plist-get info :email) info))
         (author (and (plist-get info :with-author)
                      (let ((auth (plist-get info :author)))
                        (and auth (org-export-data auth info)))))
         (date (format-time-string "%a, %d %b %Y %H:%M:%S %z")) ;; RFC 882
         (description (org-export-data (plist-get info :description) info))
         (lang (plist-get info :language))
         (keywords (plist-get info :keywords))
         (rssext (plist-get info :rss-extension))
         (blogurl (or (plist-get info :html-link-home)
                      (plist-get info :publishing-directory)))
         (image (url-encode-url (plist-get info :rss-image-url)))
         (ifile (plist-get info :input-file))
         (publink
          (or (plist-get info :rss-feed-url)
              (concat (file-name-as-directory blogurl)
                      (file-name-nondirectory
                       (file-name-sans-extension ifile))
                      "." rssext))))
    (format
     "\n<title>%s</title>
<atom:link href=\"%s\" rel=\"self\" type=\"application/rss+xml\" />
<link>%s</link>
<description><![CDATA[%s]]></description>
<language>%s</language>
<pubDate>%s</pubDate>
<lastBuildDate>%s</lastBuildDate>
<generator>%s</generator>
<webMaster>%s (%s)</webMaster>
<image>
<url>%s</url>
<title>%s</title>
<link>%s</link>
</image>
"
     title publink blogurl description lang date date
     (concat (format "Emacs %d.%d"
                     emacs-major-version
                     emacs-minor-version)
             " Org-mode " (org-version))
     email author image title blogurl)))

(defun org-rss-section (section contents info)
  "Transcode SECTION element into RSS format.
CONTENTS is the section contents.  INFO is a plist used as
a communication channel."
  contents)

(defun org-rss-timestamp (timestamp contents info)
  "Transcode a TIMESTAMP object from Org to RSS.
CONTENTS is nil.  INFO is a plist holding contextual
information."
  (org-html-encode-plain-text
   (org-timestamp-translate timestamp)))

(defun org-rss-plain-text (contents info)
  "Convert plain text into RSS encoded text."
  (let (output)
    (setq output (org-html-encode-plain-text contents)
          output (org-export-activate-smart-quotes
                  output :html info))))

;;; Filters

(defun org-rss-final-function (contents backend info)
  "Prettify the RSS output."
  (with-temp-buffer
    (xml-mode)
    (insert contents)
    (indent-region (point-min) (point-max))
    (buffer-substring-no-properties (point-min) (point-max))))

;;; Miscellaneous

(defun org-rss-add-pubdate-property ()
  "Set the PUBDATE property for top-level headlines."
  (let (msg)
    (org-map-entries
     (lambda ()
       (let* ((entry (org-element-at-point))
              (level (org-element-property :level entry)))
         (when (= level 1)
           (unless (org-entry-get (point) "PUBDATE")
             (setq msg t)
             (org-set-property
              "PUBDATE" (format-time-string
                         (cdr org-time-stamp-formats)))))))
     nil nil 'comment 'archive)
    (when msg
      (message "Property PUBDATE added to top-level entries in %s"
               (buffer-file-name))
      (sit-for 2))))

(provide 'ox-rss)

;;; ox-rss.el ends here

Babel

Enable more languages.

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

Don't ask for confirmation when evaluating dot code blocks (graphviz).

(defun my-org-confirm-babel-evaluate (lang body)
  (not (string= lang "dot")))  ; don't ask for ditaa
(setq org-confirm-babel-evaluate 'my-org-confirm-babel-evaluate)

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. (NOTE replaced with inline above).

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

NOTE: custom publishing code seems to work with org-mode 9.0.9 but not 9.1.9 currently.

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)

Pomodoro

(use-package pomidor
  :ensure t)

;; turn off annoying constant tick-tock noise
(setq pomidor-sound-tick nil
      pomidor-sound-tack nil)

;; I like to keep the ding when the pomodoro has ended though
;; pomidor-sound-overwork nil

Japanese input

This allows switching between Japanese and English input using Ctrl-c + j

(use-package mozc
  :ensure t
  :bind (("C-c j" . mozc-mode)))

For this to work, you also need to install the mozc server and emacs helper program:

sudo apt-get install mozc-server emacs-mozc-bin