John's Emacs Config

Table of Contents


See init.el on how Org loads and interprets this file for initializing Emacs.

The latest raw version of this file can be found at

This file was last exported: 2018-10-21 18:21

System paths and files

Tell Emacs where to put packages installed from Melpa, where custom themes can be loaded and where customizations done with customize should be stored.

(push (expand-file-name "lib" "~/.emacs.d") load-path)
(push (expand-file-name "themes" "~/.emacs.d") custom-theme-load-path)

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


(defconst *is-a-mac* (eq system-type 'darwin))
(defconst *is-linux* (eq system-type 'gnu/linux))

Package Archives and Management


Add MELPA and MELPA Stable as package archives.

(require 'package)
(add-to-list 'package-archives
  '("melpa" . "") t)
(add-to-list 'package-archives
  '("melpa-stable" . "") t)

Pin these packages to use fetch from MELPA stable.

(setq package-pinned-packages '((cider . "melpa-stable")))

Initialize the package library.


Utility Functions and Macros

Define utility functions and macros used throughout this config file.


Define install-package for easier installing of packages.

;; copied from
(defun install-package (package &optional min-version no-refresh)
  "Install given PACKAGE, optionally requiring MIN-VERSION.
If NO-REFRESH is non-nil, the available package lists will not be
re-downloaded in order to locate PACKAGE."
  (message "%s" package)
  (if (package-installed-p package min-version)
    (if (or (assoc package package-archive-contents) no-refresh)
        (package-install package)
        (install-package package min-version t)))))


(defun add-auto-mode (mode &rest patterns)
  "Add entries to `auto-mode-alist' to use `MODE' for all given file `PATTERNS'."
  (dolist (pattern patterns)
    (add-to-list 'auto-mode-alist (cons pattern mode))))

Persistent scratch

Persist the *scratch* buffer every 5 minutes, so we don't lose any possibly important data if/when Emacs crashes.1

(defun save-persistent-scratch ()
  "Write the contents of *scratch* to the file name
  (with-current-buffer (get-buffer-create "*scratch*")
    (write-region (point-min) (point-max) "~/.emacs-persistent-scratch")))

(defun load-persistent-scratch ()
  "Load the contents of `persistent-scratch-file-name' into the
  scratch buffer, clearing its contents first."
  (if (file-exists-p "~/.emacs-persistent-scratch")
      (with-current-buffer (get-buffer "*scratch*")
        (delete-region (point-min) (point-max))
        (insert-file-contents "~/.emacs-persistent-scratch"))))

(push #'load-persistent-scratch after-init-hook)
(push #'save-persistent-scratch kill-emacs-hook)

(if (not (boundp 'save-persistent-scratch-timer))
    (setq save-persistent-scratch-timer
          (run-with-idle-timer 300 t 'save-persistent-scratch)))

OS Specific


Conditionally set the following when on OS X (see Constants):

  1. Reveal file in current buffer in Finder.
  2. Use Command ⌘ for Meta and don't use Option ⌥.
  3. Fix mouse wheel/trackpad scrolling to be less "jerky".
(when *is-a-mac*
  ;; 1.
  (install-package 'reveal-in-finder)
  (require 'reveal-in-finder)
  ;; 2.
  (setq mac-command-modifier 'meta)
  (setq mac-option-modifier 'none)
  ;; 3.
  (setq mouse-wheel-scroll-amount '(1
                                    ((shift) . 5)

The following adds packages installed by Homebrew to our load-path.

(if *is-a-mac*
  (let ((default-directory "/usr/local/share/emacs/site-lisp/"))


The following adds packages installed by Pacman to our load-path.

(if *is-linux*
  (let ((default-directory "/usr/share/emacs/site-lisp/"))

Load keychain environment variables, so we don't have to keep on typing our SSH passphrase.

(when *is-linux*

Make the kill ring work with X selections.

(setq select-enable-clipboard t
      select-enable-primary t)

TODO Windows

Try using this config on a Windows (7+) VM.

Install Packages

Install all packages here using install-package.

(defvar my-packages
  '(;;;; Misc

    ;;;; Mode-line

    ;;;; UI

    ;;;; ido, ~M-x~

    ;;;; Window and frame management

    ;;;; Interactive Search

    ;;;; Completion

    ;;;; Linting

    ;;;; Dired
    ;; dired+

    ;;;; Ack & Ag

    ;;;; Git

    ;;;; Projectile

    ;;;; frame-purpose
    ;; frame-workflow

    ;;;; Evil (Vim)

    ;;;; Ledger

    ;;;; Language specific
    ;;;;;; Python
    ;; ein

    ;;;;;; YAML

    ;;;;;; HTML, CSS

    ;;;;;; Markdown

    ;;;;;; Javascript

    ;;;;;; Lisp

    ;;;;;; Clojure

    ;;;;;; Misc

    ;;;;;; Org
    ;; ob-ipython
  "My packages!")

;; loop over my-packages and install them
(defun install-my-packages ()
  (mapc 'install-package my-packages))



Now that everything is installed and ready, we can begin configuring packages, modes, key bindings, etc.


For a majority of programming modes, we want to indent immediately after a newline.

(add-hook 'prog-mode-hook
          (lambda () (local-set-key (kbd "RET") 'newline-and-indent)))

For a majority of programming languages, an underscore is part of a word or symbol.

(modify-syntax-entry  ?_ "w" (standard-syntax-table))

Set some generic variables.

 tab-width 4
 make-backup-files nil
 indent-tabs-mode nil
 show-trailing-whitespace t
 visible-bell nil)

We don't want to have to type "yes" or "no" at prompts.

(fset 'yes-or-no-p 'y-or-n-p)

Remember where we were when we last visited a file.

(setq-default save-place t)
(setq save-place-file "~/.emacs.d/tmp/saved-places")

Automatically creating missing parent directories when visiting a new file.

(defun my-create-non-existent-directory ()
      (let ((parent-directory (file-name-directory buffer-file-name)))
        (when (and (not (file-exists-p parent-directory))
                   (y-or-n-p (format "Directory `%s' does not exist! Create it?" parent-directory)))
          (make-directory parent-directory t))))
(add-to-list 'find-file-not-found-functions #'my-create-non-existent-directory)

When visiting buffers with the same name, uniqify them instead of the default of appending a number.

(setq uniquify-buffer-name-style 'forward
      uniquify-separator " • "
      uniquify-after-kill-buffer-p t
  ;; don't uniquify internal buffers (those that start with '*')
      uniquify-ignore-buffers-re "^\\*")

Bind undo/redo to sane bindings.

(require 'undo-tree)
(global-set-key (kbd "M-z") 'undo)
(global-set-key (kbd "M-Z") 'undo-tree-redo)

Interactive functions to encode/decode region for URLs.

(defun url-encode-region (beg end)
  "URL encode the region between BEG and END."
  (interactive "r")
  (if (use-region-p)
      (let* ((selected-text (buffer-substring beg end))
             (encoded-text (url-hexify-string selected-text)))
        (kill-region beg end)
        (insert encoded-text))))

(defun url-decode-region (beg end)
  "URL decode the region between BEG and END."
  (interactive "r")
  (if (use-region-p)
      (let* ((selected-text (buffer-substring beg end))
             (decoded-text (url-unhex-string selected-text)))
        (kill-region beg end)
        (insert decoded-text))))


;; make these environment variables available in Emacs
(with-eval-after-load 'exec-path-from-shell
  (dolist (var '("SSH_AUTH_SOCK"
    (add-to-list 'exec-path-from-shell-variables var)))
(when (memq window-system '(mac ns))


Configure UI stuff like:

  • hide toolbars
  • hide GUI scrollbars, use in-buffer scrollbars instead with yascroll
  • show indentation guide (useful for Python and HTML)
(require 'yascroll)
(require 'indent-guide)

;; don't show toolbar
(tool-bar-mode -1)

;; don't show menubar
(menu-bar-mode -1)

;; highlight matching parentheses
(show-paren-mode 1)

;; show line numbers
(setq display-line-numbers-width 4)  ;; workaround for annoying issue of shifting line number width

;; we use yascroll for the scrollbar instead
(scroll-bar-mode -1)
(global-yascroll-bar-mode 1)
(setq yascroll:delay-to-hide nil)

;; show column number in mode-line

(setq inhibit-splash-screen nil)

(setq-default indicate-empty-lines t)

;; enable indent-guide for the following modes only
(setq indent-guide-recursive nil)
;; (add-hook 'python-mode-hook 'indent-guide-mode)
(add-hook 'web-mode-hook 'indent-guide-mode)

Enable highlight-symbol in select modes. Also patch how symbols are (not) highlighted when holding down movement keys.

(dolist (hook '(prog-mode-hook html-mode-hook))
  (add-hook hook 'highlight-symbol-mode)
  (add-hook hook 'highlight-symbol-nav-mode)
  (add-hook hook 'vimish-fold-mode))
  ;(add-hook hook 'hs-minor-mode))

(defun highlight-symbol-mode-post-command ()
  "After a command, change the temporary highlighting.
Remove the temporary symbol highlighting and, unless a timeout is specified,
create the new one."
  (if (eq this-command 'highlight-symbol-jump)
      (when highlight-symbol-on-navigation-p
    (highlight-symbol-update-timer highlight-symbol-idle-delay)))

(defun highlight-symbol-update-timer (value)
  (when highlight-symbol-timer
    (cancel-timer highlight-symbol-timer))
  (setq highlight-symbol-timer
        (run-with-timer value nil 'highlight-symbol-temp-highlight)))

(setq highlight-symbol-idle-delay .1)


(defvar PragmataPro-font '(:family "PragmataPro" :size 13))
(defvar Go-font '(:family "Go Mono" :size 12))
(defvar Terminus-font '(:family "Terminus" :size 14))

(set-frame-font (apply 'font-spec PragmataPro-font) nil t)

(when *is-a-mac*
     t 'symbol
     (font-spec :family "Apple Color Emoji") nil 'prepend))

Easily switch fonts.

(defun my-switch-font (font)
  (interactive "sSwitch font (1. PragmataPro 2. Go Mono 3. Terminus): ")
  (cond ((string= font "1") (set-frame-font (apply 'font-spec PragmataPro-font) nil t))
        ((string= font "2") (set-frame-font (apply 'font-spec Go-font) nil t))
        ((string= font "3") (set-frame-font (apply 'font-spec Terminus-font) nil t))
        (t (message "Invalid option. Please choose 1 - 3."))))


Theme of the month.

(load-theme 'plan9 t)

Mode line


Hide some minor modes from the mode line.


(setq rm-blacklist '(" hl-p" " hl-s" " $" " hs" " zf" " company"
                     " GG" " FlyC" " Undo-Tree" " FlyC-" " Isearch"
                     " Anaconda" " Anzu"))


Enable pixelwise resizing of frames, so they can be properly aligned by our window manager.

(setq frame-resize-pixelwise t)


Highlight search results in the ag buffer.

(setq ag-highlight-search t)

ido, M-x

(ido-mode t)
(ido-everywhere t)
(flx-ido-mode t)

(setq ido-enable-flex-matching t
      ido-use-filename-at-point nil
      ido-auto-merge-work-directories-length 0
;; Allow the same buffer to be open in different frames
      ido-default-buffer-method 'selected-window)

Render ido candidates vertically.

(ido-vertical-mode t)
(setq ido-vertical-define-keys 'C-n-and-C-p-only
      ido-vertical-show-count t)

Ignore dired buffers when using ido-switch-buffer, as we're only interested in actual file buffers (and some internal buffers).

(defun ido-ignore-dired-buffers (name)
  "Ignore dired buffers"
      (with-current-buffer name
        (derived-mode-p 'dired-mode)))
(add-to-list 'ido-ignore-buffers 'ido-ignore-dired-buffers)

Use ido in all interactions with M-x (i.e. provides ido-completion when doing M-x ledger-report, etc.)

(ido-ubiquitous-mode t)

Override M-x to use smex. Smex basically sorts commands by most-recently used.

(global-set-key (kbd "M-x") 'smex)
(global-set-key (kbd "M-X") 'smex-major-mode-commands)

Swiper, Ivy

(setq ivy-use-virtual-buffers t)
(global-set-key "\C-s" 'swiper)
(global-set-key (kbd "C-c C-r") 'ivy-resume)
(global-set-key (kbd "<f6>") 'ivy-resume)

Emulate Evil's * command with Swiper.

(global-set-key (kbd "C-M-s")
                (lambda ()
                  (swiper (word-at-point))))

Window and frame management

Use M-g [h|j|k|l] to swap buffers between windows. Also allow using numbers to switch window focus.

(require 'buffer-move)
(require 'window-number)

(dolist (fn '(buf-move-up buf-move-down buf-move-left buf-move-right))
  (let ((file "buffer-move"))
    (autoload fn file "Swap buffers between windows" t)))
(global-set-key (kbd "M-g h")       'buf-move-left)
(global-set-key (kbd "M-g l")       'buf-move-right)
(global-set-key (kbd "M-g k")       'buf-move-up)
(global-set-key (kbd "M-g j")       'buf-move-down)

(window-number-meta-mode 1)

Cycle through a window's buffer history using C-M-,~ (backward) and ~C-M-. (forward).

(global-set-key (kbd "C-M-,") 'switch-to-prev-buffer)
(global-set-key (kbd "C-M-.") 'switch-to-next-buffer)

Interactive searching

(global-anzu-mode t)

(global-set-key [remap query-replace-regexp] 'anzu-query-replace-regexp)
(global-set-key [remap query-replace] 'anzu-query-replace)

;; Activate occur easily inside isearch
(define-key isearch-mode-map (kbd "C-o") 'isearch-occur)



Enable company-mode globally.

(require 'company)
(add-hook 'after-init-hook #'global-company-mode)

Add backends.

(add-to-list 'company-backends 'company-restclient)


(setq flycheck-check-syntax-automatically '(save idle-change mode-enabled)
      flycheck-idle-change-delay 0.8)
(add-hook 'after-init-hook #'global-flycheck-mode)

Language Specific

Language Server Protocol

Define LSP client for Python. This defines a lsp-python-enable function which we use to hook into python-mode.

(require 'lsp-mode)

(lsp-define-stdio-client lsp-python "python"

(add-hook 'python-mode-hook 'lsp-python-enable)
(add-to-list 'company-backends 'company-lsp)

Enable lsp-imenu integration.

(require 'lsp-imenu)
(add-hook 'lsp-after-open-hook 'lsp-enable-imenu)

LSP extras.

(setq lsp-ui-sideline-ignore-duplicate t)
(add-hook 'lsp-mode-hook 'lsp-ui-mode)


Use Django style docstring format when filling docstrings.

(setq python-fill-docstring-style 'django)
  • Anaconda

    Use Anaconda with company for code completion.

    ; (require 'company-anaconda)
    ; (add-to-list 'company-backends 'company-anaconda)
    ; (add-hook 'python-mode-hook 'anaconda-mode)
  • Virtual Environments

    Tell virtualenvwrapper where $WORKON_HOME is.

    (if (getenv "WORKON_HOME")
      (setq venv-location (getenv "WORKON_HOME"))
      (message "WORKON_HOME env variable not set."))

    When opening a Python file in a project with directory local variables set for the project's virtualenv, activate that virtualenv.

    ;; e.g. in .dir-locals.el
    ;; ((python-mode . ((project-venv-name . "myproject-env"))))
    (add-hook 'python-mode-hook (lambda ()
                                  (when (boundp 'project-venv-name)
                                    (venv-workon project-venv-name))))

    When switching focus to another frame, re-activate the proper virtualenv for that frame's project.

    (add-hook 'focus-in-hook (lambda ()
                               (if (boundp 'project-venv-name)
                                   (venv-workon project-venv-name)

    Show active virtualenv in mode line.

    (setq-default mode-line-format (cons '(:exec (concat "venv:" venv-current-name)) mode-line-format))
  • EIN

    The Emacs IPython Notebook package.

    Interactive function to create a frame dedicated for EIN usage. Make sure to have a Jupyter server up and running first.

    ;;(defun my-ein-frame ()
    ;;  "Open a frame dedicated for EIN."
    ;;  (interactive)
    ;;  ;; TODO: check if jupyter server is up and running before launching the frame
    ;;  (nameframe-with-frame "EIN"
    ;;    (persp-switch "ein")
    ;;    (call-interactively 'ein:notebooklist-login)))


(add-auto-mode 'yaml-mode "\\.ya?ml\\'")

HTML/CSS (web-mode)

We use web-mode for working with templates and enable it for the following filetypes.

(add-to-list 'auto-mode-alist '("\\.jinja2?\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.html?\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.css?\\'" . web-mode))

(setq web-mode-markup-indent-offset 4
      web-mode-css-indent-offset 4
      web-mode-code-indent-offset 4
      web-mode-enable-auto-quoting nil
      web-mode-enable-block-face t
      web-mode-enable-current-element-highlight t)

Use the appropriate web-mode engine when visiting a particular filetype. At the moment we default to the django engine for .html files. If you are in a project that uses jinja2 for templates, and the file extensions are in .html (a safe bet), then you'll need to define a .dir-locals.el file for that project, telling it to use the appropriate engine.

(setq web-mode-engines-alist
      '(("jinja2"    . "\\.jinja2\\'")
        ("django"    . "\\.html\\'")))


(add-to-list 'auto-mode-alist '("\\.\\(md\\|markdown\\)\\'" . markdown-mode))


We use js2-mode instead of the built-in js-mode.

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

(setq js2-use-font-lock-faces t
      js2-mode-must-byte-compile nil
      js2-basic-offset 2
      js2-indent-on-enter-key t
      js2-auto-indent-p t
      js2-bounce-indent-p nil)

(with-eval-after-load 'js2-mode


Use pp-eval-expression. The same as eval-expression, but pretty-prints output.

(global-set-key (kbd "M-:") 'pp-eval-expression)

Define a list of "lispy" modes, so we can activate/deactivate stuff for all of them in a loop.

(require 'derived)

;; elisp only
(defconst elispy-modes
  '(emacs-lisp-mode ielm-mode))
;; all lisps
(defconst lispy-modes
  (append elispy-modes
          '(lisp-mode inferior-lisp-mode lisp-interaction-mode
  "All lispy major modes.")

(defun my-lisp-setup ()
  "Enable features useful in any Lisp mode."
  ;; (rainbow-delimiters-mode t)
  ;; (hl-sexp-mode)

(dolist (hook (mapcar #'derived-mode-hook-name lispy-modes))
  (add-hook hook 'my-lisp-setup))

Check parentheses on save.

(defun maybe-check-parens ()
  "Run `check-parens' if this is a lispy mode."
  (when (memq major-mode lispy-modes)

(add-hook 'after-save-hook 'maybe-check-parens)

Dim parentheses for Lisps.



Hide *nrepl-connection* and *nrepl-server* buffers.

(setq nrepl-hide-special-buffers t)

Set some variables in CIDER REPL and some hooks.

(setq cider-repl-use-clojure-font-lock t)
(add-hook 'cider-repl-mode-hook 'subword-mode)
(add-hook 'cider-repl-mode-hook 'paredit-mode)
(add-hook 'cider-repl-mode-hook
          (lambda () (setq show-trailing-whitespace nil)))

Use clojure-mode for Clojurescript.

(add-auto-mode 'clojure-mode "\\.cljs\\'")


(add-hook 'elm-mode-hook #'elm-oracle-setup-completion)


(add-hook 'haskell-mode-hook 'haskell-indentation-mode)
(eval-after-load 'flycheck
  '(add-hook 'flycheck-mode-hook #'flycheck-haskell-setup))

Code Folding (HideShow)

(setq origami-fold-replacement "...")

Show the contents of the first 40 characters of the folded text and the number of lines folded.

(setq hs-set-up-overlay
      (defun my-hs-overlay (ov)
        (when (eq 'code (overlay-get ov 'hs))
          (overlay-put ov 'display
                        (format " ... %s <%d> ... "
                                 "\n" ""
                                  "^[ \t]*" ""
                                   "[ \t]*$" ""
                                   (buffer-substring (overlay-start ov)
                                                     (+ (overlay-start ov) 40)))))
                                (count-lines (overlay-start ov)
                                             (overlay-end ov)))
                        'face 'diff-removed)))))


Don't hide details in dired.

(setq diredp-hide-details-initially-flag nil)

Define some keybindings for dired for quick navigation.

(defun bind-dired-utils-keys ()
  (bind-keys :map dired-mode-map
           ("." . dired-up-directory)
           ("M-o" . dired-subtree-insert)
           ("M-c" . dired-subtree-remove)
           ("M-u" . dired-subtree-up)
           ("M-d" . dired-subtree-down)
           ("M-p" . dired-subtree-previous-sibling)
           ("M-n" . dired-subtree-next-sibling)
           ("M->" . dired-subtree-end)
           ("M-<" . dired-subtree-beginning)
           ("C-c d" . dired-filter-by-directory)
           ("C-c f" . dired-filter-by-file)))

Setup dired+.

(with-eval-after-load 'dired
  (require 'dired+)
  (require 'dired-subtree)
  (require 'dired-filter)
  (when (fboundp 'global-dired-hide-details-mode)
    (global-dired-hide-details-mode -1))
  (setq dired-recursive-deletes 'top)
  (define-key dired-mode-map [mouse-2] 'dired-find-file))

Open dired for the current directory when pressing C-x C-d.

(global-set-key (kbd "C-x C-d") '(lambda () (interactive) (dired ".")))

Omit uninteresting files in dired.

(add-hook 'dired-mode-hook (lambda () (dired-omit-mode)))


Tell Org where our orgfiles are.

(setq org-directory "~/orgfiles")

Set custom TODO keywords.

(setq org-todo-keywords
      '((sequence "TODO" "DOING" "WAITING" "LATER" "|" "DONE" "DELEGATED" "CANCELED")))

Default notes file for org-capture.

(setq org-default-notes-file (concat org-directory "/"))

Set custom org-capture templates.

(setq org-capture-templates
      '(("t" "Todo" entry (file+headline (concat org-directory "/") "Other")
         "* TODO %?\n  %i\n  %a")
        ("n" "Note" entry (file+datetree (concat org-directory "/"))
         "* %?\nEntered on %U\n  %i\n  %a")))

(global-set-key (kbd "C-c o c") 'org-capture)

Add custom org-agenda command. We'd like to see at a glance:

  • Our agenda for the week
  • What we are currently working on
  • List of remaining TODO items
(setq org-agenda-custom-commands
      '(("z" "Agenda and Tasks"
         ((agenda "")
          (todo "DOING")
          (todo "TODO")))))

Enable font-locking for org source blocks.

(setq org-src-fontify-natively t)

Don't evaluate source blocks when exporting.

(setq org-export-babel-evaluate nil)

Allow quotes to be verbatim2, 3.

(add-hook 'org-mode-hook
          (lambda ()
            (setcar (nthcdr 2 org-emphasis-regexp-components) " \t\n,'")
            (org-set-emph-re 'org-emphasis-regexp-components org-emphasis-regexp-components)
            (custom-set-variables `(org-emphasis-alist ',org-emphasis-alist))))

Enable the toc-org package, for generating and inserting table of contents directly in the Org document itself (e.g. useful for Github files)

(add-hook 'org-mode-hook 'toc-org-enable)


Allow exporting and publishing to ODT.

(require 'ox-odt)

  (defun org-odt-publish-to-odt (plist filename pub-dir)
    "Publish an org file to ODT.

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

  Return output file name."
    (unless (or (not pub-dir) (file-exists-p pub-dir)) (make-directory pub-dir t))
    ;; Check if a buffer visiting FILENAME is already open.
    (let* ((org-inhibit-startup t)
           (visiting (find-buffer-visiting filename))
           (work-buffer (or visiting (find-file-noselect filename))))
      (with-current-buffer work-buffer
        (let ((outfile (org-export-output-file-name ".odt" nil pub-dir)))
           (let* ((org-odt-embedded-images-count 0)
                  (org-odt-embedded-formulas-count 0)
                  (org-odt-object-counters nil)
                  (hfy-user-sheet-assoc nil))
             (let ((output (org-export-as 'odt nil nil nil
                                               (expand-file-name filename) :crossrefs nil t)
                                              ,@(plist-get plist :filter-final-output))))))
                   (out-buf (progn (require 'nxml-mode)
                                   (let ((nxml-auto-insert-xml-declaration-flag nil))
                                      (concat org-odt-zip-dir "content.xml") t)))))
               (with-current-buffer out-buf (erase-buffer) (insert output))))))))
      (unless visiting (kill-buffer work-buffer))))

Fix the path to the soffice program on macOS.

(setq org-odt-convert-processes '(("LibreOffice"
                                   "/Applications/ --headless --convert-to %f%x --outdir %d %i")))

Configure publishing of our orgfiles.

(setq org-export-date-timestamp-format "%Y-%m-%d")

(defun my-website-sitemap-function (project &optional sitemap-filename)
  "Custom sitemap generator that inserts additional options."
  (let ((sitemap (org-publish-sitemap-default project sitemap-filename)))
    (concat sitemap
            "\n\n#+OPTIONS: html-preamble:nil"
            "\n#+SUBTITLE: a.k.a. john2x"
            "\n#+HTML_HEAD_EXTRA: <style>body { font-family: CMU Serif, serif; margin: auto; max-width:768px; };</style>"
            (format "\n#+DATE:%s" (format-time-string "%Y-%m-%d")))))

(defun my-website-html-postamble (options)
  (concat "<hr>"
          (if (and (plist-get options ':keywords) (not (string= (plist-get options ':keywords) "")))
              (format "<p>Keywords: %s</p>" (plist-get options ':keywords))
          (format "<p class=\"date\">Modified: %s</p>" (format-time-string "%Y-%m-%d %H:%M:%S %Z"))
          (format "<p>Copyright (c) %s %s</p>"
                  (format-time-string "%Y") ;; TODO: get from document options
                  (car (plist-get options ':author)))
          (format "<p>%s</p>" (plist-get options ':creator))))

(setq org-publish-project-alist
         :base-directory "~/Dropbox/orgfiles"
         :publishing-directory "~/Dropbox/orgfiles/published"
         :publishing-function org-html-publish-to-html
         :section-numbers nil
         :table-of-contents nil
         :recursive t
         :auto-sitemap t
         :sitemap-filename ""
         :sitemap-title "orgfiles")
         :base-directory "~/projects/misc/"
         :base-extension "png\\|jpg"
         :publishing-directory "~/projects/misc/"
         :publishing-function org-publish-attachment)
         :base-directory "~/projects/misc/"
         :base-extension "txt\\|xml\\|pdf\\|html"
         :publishing-directory "~/projects/misc/"
         :publishing-function org-publish-attachment)
         :base-directory "~/projects/misc/"
         :publishing-directory "~/projects/misc/"
         :recursive t
         :exclude "level-.*\\|.*\.draft\.org\\|README\.org"
         :publishing-function org-html-publish-to-html
         :auto-sitemap t
         :sitemap-title "John Louis Del Rosario"
         :sitemap-filename ""
         :sitemap-sort-files anti-chronologically
         :sitemap-function my-website-sitemap-function
         :html-link-up "/"
         :html-link-home "/"
         :html-preamble "<p class=\"date\">Published: %d</p>"
         :html-postamble my-website-html-postamble)
        ("website" :components ("website-content" "website-images" "website-others"))))


Experiment with org-journal for a personal diary of sorts.

(setq org-journal-dir (concat org-directory "/journal/"))

Org Babel

Load additional languages.

; (org-babel-do-load-languages
;  'org-babel-load-languages
;  '((ipython . t)))

Set additional templates.

(setq org-structure-template-alist (append org-structure-template-alist
                                           '(("sel" "#+BEGIN_SRC emacs-lisp?\n\n#+END_SRC")
                                             ("sip" "#+BEGIN_SRC ipython :session?\n\n#+END_SRC")
                                             ("ex" "#+BEGIN_EXAMPLE\n\n#+END_EXAMPLE"))))


Show git status indicators in the fringe.

(global-git-gutter-mode 1)
(setq git-gutter:modified-sign "* "
      git-gutter:added-sign "+ "
      git-gutter:deleted-sign "- "
      git-gutter:lighter " GG")

(global-set-key (kbd "M-g M-p") 'git-gutter:previous-hunk)
(global-set-key (kbd "M-g M-n") 'git-gutter:next-hunk)

Package for yanking/killing links to Git repository files.

(require 'browse-at-remote)


;; skip warning introduced by 1.4.0
(setq magit-last-seen-setup-instructions "1.4.0")

 magit-save-some-buffers nil
 magit-process-popup-time 10
 magit-diff-refine-hunk t
 magit-restore-window-configuration t
 magit-completing-read-function 'magit-ido-completing-read
 magit-revert-buffers nil)

(global-set-key (kbd "C-c m m") 'magit-status)

Make the Magit buffer take the entire frame.

(with-eval-after-load 'magit
  (fullframe magit-status magit-mode-quit-window))

Projectile and frame workflow

Configure Projectile for project management and navigation.

(diminish 'projectile-mode)

(setq projectile-completion-system 'ido
      projectile-enable-caching t)

(global-set-key (kbd "C-x p") 'projectile-find-file)

Configure frame-workflow and frame-purpose to have dedicated frames for each Projectile project.

(require 'frame-workflow)
(frame-purpose-mode 1)
(frame-workflow-mode 1)

(setq projectile-switch-project-action #'frame-workflow-switch-directory-frame)

(global-set-key (kbd "M-P") 'frame-workflow-switch-frame)

When creating a new Projectile frame, we want the frame's name to be set to the project name.

(defun frame-workflow-set-projectile-frame-name (frame-action)
  "Set the frame's name and call FRAME-ACTION."
  (set-frame-parameter nil 'name (projectile-project-name))
  (funcall frame-action))

(setq frame-workflow-directory-frame-action
      (lambda ()
        (frame-workflow-set-projectile-frame-name #'projectile-dired)))

Not Projectile, but still project management related.

(global-set-key (kbd "<f3>") 'project-explorer-toggle)


Set some default values. We don't want to auto-reconnect too much since it could flood the channel and get us temporarily banned.

(setq erc-nick "john2x"
      erc-server-auto-reconnect t
      erc-server-reconnect-timeout 15)

Change header-line face when disconnected.

(defface erc-header-line-disconnected
  '((t (:inherit magit-diff-removed)))
  "Face to use when ERC has been disconnected.")

(defun erc-update-header-line-show-disconnected ()
  "Use a different face in the header-line when disconnected."
    (cond ((erc-server-process-alive) 'erc-header-line)
          (t 'erc-header-line-disconnected))))

(setq erc-header-line-face-method 'erc-update-header-line-show-disconnected)

Interactive function to create a frame dedicated to ERC and automatically connect to preset servers. (We don't join channels automatically as it could take too long.)

(defun my-erc-frame ()
  "Switch or create to a frame called 'ERC' and connect to IRC"
  (frame-workflow-define-subject "erc"
    :make-frame '(frame-purpose-make-mode-frame 'erc-mode)
    :layout '(progn
               (erc :server "" :port "6667" :nick "john2x")

When reconnect attempts fail, have a convenient shortcut to reconnect manually.

(with-eval-after-load 'erc
  (define-key erc-mode-map (kbd "C-c C-r") (lambda () (interactive) (erc-server-reconnect))))

When reconnecting, don't bring any channels up into the current buffer.

(setq erc-join-buffer 'window-noselect)

When using a VPN, (and probably other servers as well) requires us to authenticate with SASL. Unfortunately, SASL support isn't implemented yet in the default ERC package bundled with Emacs.

There's an erc-sasl library4 but it requires patching the erc-login function so it sends the appropriate CAP request for SASL. Until erc-sasl gets merged into the main ERC package, we'll have to patch it here.

(require 'erc-sasl)
(add-to-list 'erc-sasl-server-regexp-list "irc\\.freenode\\.net")

(defun erc-login ()
  "Perform user authentication at the IRC server. (PATCHED)"
  (erc-log (format "login: nick: %s, user: %s %s %s :%s"
           (or erc-system-name (system-name))
  (if erc-session-password
      (erc-server-send (format "PASS %s" erc-session-password))
    (message "Logging in without password"))
  (when (and (featurep 'erc-sasl) (erc-sasl-use-sasl-p))
    (erc-server-send "CAP REQ :sasl"))
  (erc-server-send (format "NICK %s" (erc-current-nick)))
   (format "USER %s %s %s :%s"
       ;; hacked - S.B.
       (if erc-anonymous-login erc-email-userid (user-login-name))
       "0" "*"

.ircauthinfo is where we store our NickServ passwords, so we don't have to type it in all the time (and it breaks erc-login prompt when SASL is required).

(add-to-list 'auth-sources "~/.emacs.d/.ircauthinfo")

Set the prompt to use the channel name.

(setq erc-prompt  (lambda () (concat (buffer-name) " > ")))

Add a /FLUSH command to flush the ERC buffer of contents.

(defun erc-cmd-FLUSH (&rest ignore)
  "Erase the current buffer."
  (let ((inhibit-read-only t))
    (message "Flushed contents of channel")

Set the fill prefix to a constant value, instead of basing it off the username.

(setq erc-fill-prefix "        ↳ ")

Channel tracking is for keeping track of activity in channels which are currently not visible on some frame/window. Ignore tracking the following types of messages.

(setq erc-track-exclude-types '("JOIN" "NICK" "PART" "QUIT"))

Disable nlinum-mode for ERC buffers.

(add-hook 'erc-mode-hook (lambda () (nlinum-mode -1)))

Send messages with C-RET instead of just RET to avoid accidentally pasting text into an ERC buffer and pressing Enter.

(with-eval-after-load 'erc
  (define-key erc-mode-map (kbd "<C-return>") 'erc-send-current-line)
  (define-key erc-mode-map (kbd "RET") '(lambda () (interactive) (message "Send with C-return"))))


Highlight nicknames so they're easier to spot.

(require 'erc-highlight-nicknames)
(add-to-list 'erc-modules 'highlight-nicknames)

Use the services module to automatically attempt to identify with NickServ when connection to a server.

(add-to-list 'erc-modules 'services)

Save logs when leaving a channel.

(add-to-list 'erc-modules 'log)
(setq erc-save-buffer-on-part t)
(setq erc-log-channels-directory "~/.erc/logs")

Render smiley icons, because why not :-)?

(add-to-list 'erc-modules 'smiley)

Finally, reload ERC's modules.



mu4e is a mail client for Emacs. It works in conjunction with offlineimap to provide a nice interface to read and send mail. Note that mu4e is installed via OS package manager (i.e. Homebrew).

Set some default variables.

(require 'mu4e)
(require 'smtpmail)
(require 'starttls)

(setq mu4e-update-interval nil)

(setq user-mail-address ""
      user-full-name  "John Louis Del Rosario"
      mu4e-compose-signature "\nSent from GNU Emacs")

(setq mu4e-maildir "~/mail"
      mu4e-sent-folder "/personal/[Gmail]/Sent Mail"
      mu4e-drafts-folder "/personal/drafts"
      mu4e-trash-folder "/personal/[Gmail]/Trash"
      mu4e-refile-folder "/personal/archive")

(setq mu4e-headers-skip-duplicates t)

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

;; set mu4e as the default mail program
(setq mail-user-agent 'mu4e-user-agent)

;; kill compose buffer instead of just hiding it
(setq message-kill-buffer-on-exit t)

(setq mu4e-view-show-addresses t)
(setq mu4e-attachment-dir "~/Downloads/Attachments/")

;; disable trailing whitespace when in mu4e
(add-hook 'mu4e-headers-mode-hook (lambda () (setq-local show-trailing-whitespace nil)))
(add-hook 'mu4e-view-mode-hook (lambda () (setq-local show-trailing-whitespace nil)))

(setq message-send-mail-function 'smtpmail-send-it
      smtpmail-stream-type 'starttls
      smtpmail-default-smtp-server ""
      smtpmail-smtp-server ""
      smtpmail-smtp-user ""
      smtpmail-smtp-service 587
      starttls-extra-arguments '("--x509cafile" "/usr/local/etc/openssl/cert.pem"))

Define multiple email accounts so we can switch between them.

(setq mu4e-user-mail-address-list '(""

(defvar my-mu4e-account-alist
     (mu4e-sent-folder "/personal/[Gmail]/Sent Mail")
     (mu4e-drafts-folder "/personal/drafts")
     (mu4e-compose-signature "\nSent from GNU Emacs")
     (user-mail-address "")
     (smtpmail-smtp-user "")
     (smtpmail-default-smtp-server "")
     (smtpmail-smtp-server "")
     (smtpmail-stream-type starttls)
     (smtpmail-smtp-service 587))
     (mu4e-sent-folder "/collabspot/[Gmail]/Sent Mail")
     (mu4e-drafts-folder "/collabspot/drafts")
     (mu4e-compose-signature "\nSent from GNU Emacs")
     (user-mail-address "")
     (smtpmail-smtp-user "")
     (smtpmail-default-smtp-server "")
     (smtpmail-smtp-server "")
     (smtpmail-stream-type starttls)
     (smtpmail-smtp-service 587))
     (mu4e-sent-folder "/newlogic/[Gmail]/Sent Mail")
     (mu4e-drafts-folder "/newlogic/drafts")
     (mu4e-compose-signature "\nSent from GNU Emacs")
     (user-mail-address "")
     (smtpmail-smtp-user "")
     (smtpmail-default-smtp-server "")
     (smtpmail-smtp-server "")
     (smtpmail-stream-type starttls)
     (smtpmail-smtp-service 587))))

Function to select an account when composing an email.

(defun my-mu4e-set-account ()
  "Set the account for composing a message."
  (let* ((account
          (if mu4e-compose-parent-message
              (let ((maildir (mu4e-message-field mu4e-compose-parent-message :maildir)))
                (string-match "/\\(.*?\\)/" maildir)
                (match-string 1 maildir))
            (completing-read (format "Compose with account: (%s) "
                                     (mapconcat #'(lambda (var) (car var))
                                                my-mu4e-account-alist "/"))
                             (mapcar #'(lambda (var) (car var)) my-mu4e-account-alist)
                             nil t nil nil (caar my-mu4e-account-alist))))
         (account-vars (cdr (assoc account my-mu4e-account-alist))))
    (if account-vars
        (mapc #'(lambda (var)
                  (set (car var) (cadr var)))
      (error "No email account found"))))

(add-hook 'mu4e-compose-pre-hook 'my-mu4e-set-account)

(add-hook 'message-send-hook
  (lambda ()
    (unless (yes-or-no-p "Are you sure you want to send this?")
      (signal 'quit nil))))

Define some shortcuts to common folders.

(setq mu4e-maildir-shortcuts
    '(("/personal/INBOX"               . ?j)
      ("/collabspot/INBOX"             . ?c)
      ("/newlogic/INBOX"           . ?n)))

Define bookmarks.

(add-to-list 'mu4e-bookmarks
  '((concat "maildir:/personal/[Gmail]/\"Sent Mail\" AND"
            " OR maildir:/newlogic/[Gmail]/\"Sent Mail\" AND"
            " OR maildir:/collabspot/[Gmail]/\"Sent Mail\" AND") "Recent sent mail" ?s))

(add-to-list 'mu4e-bookmarks
  '((concat "maildir:/personal/[Gmail]/\"All Mail\" AND"
            " OR maildir:/newlogic/[Gmail]/\"All Mail\" AND"
            " OR maildir:/collabspot/[Gmail]/\"All Mail\" AND") "Recent all mail" ?a))

(add-to-list 'mu4e-bookmarks
  '((concat "maildir:/personal/INBOX AND"
            " OR maildir:/newlogic/INBOX AND"
            " OR maildir:/collabspot/INBOX AND") "Recent inbox" ?3))

(add-to-list 'mu4e-bookmarks
  '((concat "maildir:/personal/INBOX AND"
            " OR maildir:/newlogic/INBOX AND"
            " OR maildir:/collabspot/INBOX AND") "This week's inbox" ?7))

(add-to-list 'mu4e-bookmarks
  '("flag:flagged" "Flagged" ?f))

(add-to-list 'mu4e-bookmarks
  '((concat "maildir:/personal/INBOX AND"
            " OR maildir:/newlogic/INBOX AND"
            " OR maildir:/collabspot/INBOX AND") "Today's inbox" ?i))

Define custom headers.

(add-to-list 'mu4e-header-info-custom
  '(:important . (:name "Important"
                  :shortname "Impt"
                  :help "Tagged as important by Gmail"
                  (lambda (msg)
                    (if (member "\\Important" (mu4e-message-field msg :tags))
                      " ")))))

 '(:account . (:name "Account"
               :shortname "Acct"
               :help "Account name"
               (lambda (msg)
                 (let ((maildir (mu4e-message-field msg :maildir)))
                   (cl-flet ((starts-with (s begins)
                                          (cond ((>= (length s) (length begins))
                                             (string-equal (substring s 0 (length begins))
                                                (t nil))))
                     (cond ((starts-with maildir "/collabspot") "collabspot")
                           ((starts-with maildir "/newlogic") "newlogic")
                           (t "personal"))))))))

(setq mu4e-headers-fields '((:human-date . 12)
                            (:account . 10)
                            (:flags . 6)
                            (:important . 4)
                            (:mailing-list . 10)
                            (:from-or-to . 22)

Improve rendering of HTML only emails.

(require 'mu4e-contrib)
(setq mu4e-html2text-command 'mu4e-shr2text)

Some emails are unreadable, even with the html2text command. We resort to viewing the email in the browser instead.

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

Enable org-mode support when writing emails, so we can send pretty HTML emails.

(require 'org-mu4e)
(setq org-mu4e-convert-to-html t)

;; when composing an email, switch on the special mu4e/orgmode mode
(define-key mu4e-compose-mode-map (kbd "C-c o") 'org~mu4e-mime-switch-headers-or-body)

Use dired to select and attach files. To attach a file to a message, press C-x C-d when composing a message (this opens dired) then navigate to the file you want to attach. Then press C-c RET C-a y to attach the highlighted file. You can also mark multiple files to attach them.

(require 'gnus-dired)
;; make the `gnus-dired-mail-buffers' function also work on
;; message-mode derived modes, such as mu4e-compose-mode
(defun gnus-dired-mail-buffers ()
  "Return a list of active message buffers."
  (let (buffers)
      (dolist (buffer (buffer-list t))
        (set-buffer buffer)
        (when (and (derived-mode-p 'message-mode)
                (null message-sent-message-via))
          (push (buffer-name buffer) buffers))))
    (nreverse buffers)))

(setq gnus-dired-mail-mode 'mu4e-user-agent)
(add-hook 'dired-mode-hook 'turn-on-gnus-dired-mode)

Interactive function to switch to a frame dedicated for mu4e.

(defun my-mu4e-frame ()
  "Switch or create to a frame called 'mu4e' and connect to IRC"
  (let ((frame (frame-purpose-make-mode-frame 'mu4e)))

Show unread emails in mode-line (via mu4e-alert package).

(require 'mu4e-alert)
(setq mu4e-alert-interesting-mail-query
      (concat "(maildir:/personal/INBOX AND"
              " OR maildir:/newlogic/INBOX AND"
              " OR maildir:/collabspot/INBOX AND"
              " AND flag:unread"))
(add-hook 'after-init-hook #'mu4e-alert-enable-mode-line-display)

Disable line numbers in mu4e buffers.

(add-hook 'mu4e-main-mode-hook (lambda () (nlinum-mode -1)))
(add-hook 'mu4e-headers-mode-hook (lambda () (nlinum-mode -1)))
(add-hook 'mu4e-view-mode-hook (lambda () (nlinum-mode -1)))
(add-hook 'mu4e-compose-mode-hook (lambda () (nlinum-mode -1)))
(add-hook 'mu4e-org-mode-hook (lambda () (nlinum-mode -1)))


(defconst *ledger-journal-path* "~/Dropbox/ledger/john.ledger")
(defconst *ledger-docs-dir* "~/Dropbox/ledger/")

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

(add-hook 'ledger-mode-hook 'goto-address-prog-mode)

;; don't override the highlighting of each posted item
;; in a xact if it is cleared/pending
(setq ledger-fontify-xact-state-overrides nil)

;; (defun my-ledger-frame ()
;;   "Easy way to open my ledger journal"
;;   (interactive)
;;   (nameframe-with-frame "ledger"
;;     (persp-switch "ledger")
;;     (find-file *ledger-journal-path*)
;;     (split-window-right)
;;     (find-file-other-window (concat *ledger-docs-dir* "Accounts.ledger"))
;;     (split-window-below)
;;     (window-number-select 1)
;;     (ledger-report "bal" nil)
;;     (toggle-frame-maximized)))

(with-eval-after-load 'flycheck
  (require 'flycheck-ledger))


Evil is meant to be enabled globally.

(evil-mode 1)

But we only want Normal state for particular modes, and use Emacs state everywhere else.

So first, we set Emacs state as Evil's default state.

(setq-default evil-default-state 'emacs)

We then clear Evil's whitelists of modes that should start in a particular state, so they all start in Emacs state.

(setq-default evil-insert-state-modes '())

Then we specify which modes we want Normal state for.

(setq-default evil-normal-state-modes

Set the evil-leader.

(require 'evil-leader)
(evil-leader/set-leader ",")
(evil-leader/set-key "a g" 'ag)

Enable Evil plugins.

(global-evil-surround-mode 1)
(global-evil-matchit-mode 1)
(global-evil-search-highlight-persist t)
(with-eval-after-load 'evil
  (require 'evil-anzu)
  (require 'evil-vimish-fold))
(evil-vimish-fold-mode 1)

Use SPACE for scrolling.

(define-key evil-normal-state-map (kbd "SPC") 'evil-scroll-down)
(define-key evil-normal-state-map (kbd "S-SPC") 'evil-scroll-up)

Bind some keys on the leader.

(evil-leader/set-key "n" 'evil-search-highlight-persist-remove-all)
(evil-leader/set-key "w" 'evil-write)

(defun my-evil-reload-buffer ()
  (evil-edit nil t))
(evil-leader/set-key "e" 'my-evil-reload-buffer)

By default, C-u is bound to Emacs' universal-argument function, a rather important function used by various commands. But in Vim, C-u is supposed to scroll up half a page, and that has been burned into muscle memory by now. As a compromise, we bind universal-argument to M-u (which previously performs upcase-word, something we rarely, if ever, use), and use Vim's version of C-u to scroll up half a page.

(global-set-key (kbd "M-u") 'universal-argument)
(define-key universal-argument-map (kbd "M-u") 'universal-argument-more)
(with-eval-after-load 'evil-maps
  (define-key evil-motion-state-map (kbd "C-u") 'evil-scroll-up))

Fix visual select bug on macOS.

(fset 'evil-visual-update-x-selection 'ignore)

evil-nerd-commenter defines a global key binding for C-c p, which we do not use. Remove this binding and rebind the C-c p p binding for projectile-swith-project.

(global-unset-key (kbd "C-c p"))
(global-set-key (kbd "C-c p p") 'projectile-switch-project)


Author: John Louis Del Rosario

Created: 2018-10-21 Sun 18:21