John's Emacs Config

Table of Contents

Introduction

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 https://github.com/john2x/emacs.d.

This file was last exported: 2016-05-07 16:26

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.

(add-to-list 'load-path (expand-file-name "lib" "~/.emacs.d/"))
(add-to-list 'custom-theme-load-path (expand-file-name "themes" "~/.emacs.d/"))

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

Constants

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

Package Archives and Management

MELPA

Using bleeding edge? I, too, like to live dangerously.

(require 'package)
(add-to-list 'package-archives
  '("melpa" . "https://melpa.org/packages/") t)
(package-initialize)

Utility Functions and Macros

Define utility functions and macros used throughout this config file.

install-package

Define install-package for easier installing of packages.

;; copied from github.com/purcell/emacs.d
(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)
      t
    (if (or (assoc package package-archive-contents) no-refresh)
        (package-install package)
      (progn
        (package-refresh-contents)
        (install-package package min-version t)))))

add-auto-mode

(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
`persistent-scratch-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

OS X

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

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

(let ((default-directory "/usr/local/share/emacs/site-lisp/"))
  (normal-top-level-add-subdirs-to-load-path))

TODO Linux

Try using this config on an Ubuntu or ArchLinux VM.

TODO Windows

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

Install Packages

Install all packages here using install-package.

(defvar my-packages
  '(;;;; Misc
    exec-path-from-shell
    undo-tree
    bind-key
    nameframe
    avy
    link-hint
    swiper

    ;;;; Mode-line
    diminish
    smart-mode-line

    ;;;; UI
    indent-guide
    yascroll
    highlight-symbol
    smooth-scroll
    nlinum

    ;;;; ido, ~M-x~
    flx-ido
    ido-ubiquitous
    smex
    idomenu
    ido-vertical-mode

    ;;;; Window and frame management
    buffer-move
    window-number
    fullframe

    ;;;; Interactive Search
    anzu

    ;;;; Completion
    company
    company-emoji

    ;;;; Linting
    flycheck

    ;;;; Dired
    dired+

    ;;;; Ack & Ag
    ag

    ;;;; Git
    magit
    git-blame
    gitignore-mode
    gitconfig-mode
    git-messenger
    git-gutter

    ;;;; Projectile
    projectile
    flx
    project-explorer
    nameframe-projectile

    ;;;; Perspective
    perspective
    nameframe-perspective

    ;;;; Evil (Vim)
    evil
    evil-anzu
    evil-surround
    evil-leader
    evil-matchit
    evil-nerd-commenter
    evil-search-highlight-persist
    evil-vimish-fold

    ;;;; Ledger
    ledger-mode
    flycheck-ledger

    ;;;; Language specific
    ;;;;;; Python
    virtualenvwrapper
    anaconda-mode
    company-anaconda
    nose

    ;;;;;; YAML
    yaml-mode

    ;;;;;; HTML, CSS
    web-mode

    ;;;;;; Markdown
    markdown-mode

    ;;;;;; Javascript
    json-mode
    js2-mode

    ;;;;;; Lisp
    paredit
    rainbow-delimiters
    highlight-parentheses
    paren-face

    ;;;;;; Clojure
    cider

    ;;;;;; Misc
    haskell-mode
    ghc
    flycheck-haskell
    purescript-mode
    elm-mode
    mu4e-alert

    ;;;;;; Org
    htmlize
    org-journal)
  "My packages!")

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

(install-my-packages)

Configure

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

Misc

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.

(setq-default
 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)

Configure smooth scrolling.

(require 'smooth-scroll)
(smooth-scroll-mode 1)
(setq smooth-scroll/vscroll-step-size 4)

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)

Shell

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

UI

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)

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

;; show line numbers (use nlinum-mode; linum-mode is slow)
(global-nlinum-mode)

;; 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
(column-number-mode)

(setq inhibit-splash-screen t)

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

(with-eval-after-load 'highlight-symbol
  (diminish 'highlight-symbol-mode))

;; http://emacs.stackexchange.com/questions/931
(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-temp-highlight))
    (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)

Font

(defvar Input-font '(:family "Input" :size 12))
(defvar PragmataPro-font '(:family "Essential PragmataPro" :size 12))

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

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

Easily switch fonts.

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

Theme

Theme of the month.

(load-theme 'plan9 t)

Mode line

(which-function-mode)

Ag

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

(diminish 'anzu-mode)

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

Completion

company

Enable company-mode globally.

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

Flycheck

(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

Python

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

    (venv-initialize-interactive-shells)
    (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 ()
                                  (hack-local-variables)
                                  (when (boundp 'project-venv-name)
                                    (venv-workon project-venv-name))))
    

    Show active virtualenv in mode line.

    (setq-default mode-line-format (cons '(:exec venv-current-name) mode-line-format))
    

YAML

(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\\'")))

Markdown

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

Javascript

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
  (js2-imenu-extras-setup)
  (toggle-truncate-lines))

Lisp

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
            clojure-mode))
  "All lispy major modes.")

(defun my-lisp-setup ()
  "Enable features useful in any Lisp mode."
  ;; (rainbow-delimiters-mode t)
  ;; (hl-sexp-mode)
  (enable-paredit-mode)
  (turn-on-eldoc-mode)
  (highlight-parentheses-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)
    (check-parens)))

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

Dim parentheses for Lisps.

(global-paren-face-mode)

Clojure

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

Show eldoc for Clojure.

(add-hook 'cider-mode-hook 'cider-turn-on-eldoc-mode)

Use clojure-mode for Clojurescript.

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

Elm

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

Haskell

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

Code Folding (HideShow)

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
                       (propertize
                        (format " ... %s <%d> ... "
                                (replace-regexp-in-string
                                 "\n" ""
                                 (replace-regexp-in-string
                                  "^[ \t]*" ""
                                  (replace-regexp-in-string
                                   "[ \t]*$" ""
                                   (buffer-substring (overlay-start ov)
                                                     (+ (overlay-start ov) 40)))))
                                (count-lines (overlay-start ov)
                                             (overlay-end ov)))
                        'face 'diff-removed)))))

Dired

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)
  (bind-dired-utils-keys)
  (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)))

Org

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 "/notes.org"))

Set custom org-capture templates.

(setq org-capture-templates
      '(("t" "Todo" entry (file+headline (concat org-directory "/todo.org") "Other")
         "* TODO %?\n  %i\n  %a")
        ("n" "Note" entry (file+datetree (concat org-directory "/notes.org"))
         "* %?\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)
            (org-element--set-regexps)
            (custom-set-variables `(org-emphasis-alist ',org-emphasis-alist))))

Publishing

Configure publishing of our orgfiles.

(defun my-website-sitemap-function (project &optional sitemap-filename)
  "Custom sitemap generator that inserts additional options."
  (let ((buffer (org-publish-org-sitemap project sitemap-filename)))
    (with-current-buffer buffer
      (insert "\n#+OPTIONS: html-preamble:nil")
      (insert "\n#+SUBTITLE: a.k.a. john2x")
      (insert (format "\n#+DATE:%s" (format-time-string "%Y-%m-%d")))
      (save-buffer))))

(defun my-website-html-postamble (options)
  (message "%s" (plist-get options ':date))
  (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"))
          (format "<p>Copyright (c) %s %s</p>"
                  (car (split-string (car (plist-get options ':date)) "-")) ;; TODO: get from document options
                  (car (plist-get options ':author)))
          (format "<p>%s</p>" (plist-get options ':creator))))

(setq org-publish-project-alist
      `(("orgfiles"
         :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.org"
         :sitemap-title "orgfiles")
        ("website-main"
         :base-directory "~/Projects/misc/john2x.github.com/"
         :publishing-directory "~/Dropbox/Apps/updog/john2x/"
         :recursive t
         :exclude "level-.*\\|.*\.draft\.org"
         :publishing-function org-html-publish-to-html
         :auto-sitemap t
         :sitemap-filename "index.org"
         :sitemap-title "John Louis Del Rosario"
         :sitemap-sort-files "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-main"))))

Journal

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

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

Git

Show git status indicators in the fringe.

(global-git-gutter-mode 1)
(git-gutter:linum-setup)
(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)

Magit

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

(setq-default
 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))

Misc

Define functions to get/open the url of the current line/region in Github/Bitbucket.

(defun my-git-get-line-url ()
  (interactive))

Projectile

(projectile-global-mode)
(persp-mode)

(setq projectile-switch-project-action 'projectile-dired
      projectile-completion-system 'ido
      projectile-enable-caching t)

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

(nameframe-projectile-mode t)
(nameframe-perspective-mode t)
(global-set-key (kbd "M-P") 'nameframe-switch-frame)

Not actually projectile, but still project management related.

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

ERC

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

(setq erc-nick "john2x"
      erc-server-auto-reconnect nil)

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."
  (erc-with-server-buffer
    (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"
  (interactive)
  (nameframe-with-frame "ERC"
    (persp-switch "freenode.net")
    (erc :server "irc.freenode.net" :port "6667" :nick "john2x")))

When using a VPN, freenode.net (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"
           (erc-current-nick)
           (user-login-name)
           (or erc-system-name (system-name))
           erc-session-server
           erc-session-user-full-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)))
  (erc-server-send
   (format "USER %s %s %s :%s"
       ;; hacked - S.B.
       (if erc-anonymous-login erc-email-userid (user-login-name))
       "0" "*"
       erc-session-user-full-name))
  (erc-update-mode-line))

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

Modules

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)
(erc-update-modules)

Mail

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

(setq user-mail-address "john2x@gmail.com"
      user-full-name  "John Del Rosario"
      mu4e-compose-signature "john2x.com\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 "smtp.gmail.com"
      smtpmail-smtp-server "smtp.gmail.com"
      smtpmail-smtp-user "john2x@gmail.com"
      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 '("john2x@gmail.com"
                                    "john@collabspot.com"
                                    "john@prospecthive.com"))

(defvar my-mu4e-account-alist
  '(("personal"
     (mu4e-sent-folder "/personal/[Gmail]/Sent Mail")
     (mu4e-drafts-folder "/personal/drafts")
     (mu4e-compose-signature "john2x.com\nSent from GNU Emacs")
     (user-mail-address "john2x@gmail.com")
     (smtpmail-smtp-user "john2x@gmail.com")
     (smtpmail-default-smtp-server "smtp.gmail.com")
     (smtpmail-smtp-server "smtp.gmail.com")
     (smtpmail-stream-type starttls)
     (smtpmail-smtp-service 587))
    ("collabspot"
     (mu4e-sent-folder "/collabspot/[Gmail]/Sent Mail")
     (mu4e-drafts-folder "/collabspot/drafts")
     (mu4e-compose-signature "collabspot.com\nSent from GNU Emacs")
     (user-mail-address "john@collabspot.com")
     (smtpmail-smtp-user "john@collabspot.com")
     (smtpmail-default-smtp-server "smtp.gmail.com")
     (smtpmail-smtp-server "smtp.gmail.com")
     (smtpmail-stream-type starttls)
     (smtpmail-smtp-service 587))
    ("prospecthive"
     (mu4e-sent-folder "/prospecthive/[Gmail]/Sent Mail")
     (mu4e-drafts-folder "/prospecthive/drafts")
     (mu4e-compose-signature "prospecthive.com\nSent from GNU Emacs")
     (user-mail-address "john@prospecthive.com")
     (smtpmail-smtp-user "john@prospecthive.com")
     (smtpmail-default-smtp-server "smtp.gmail.com")
     (smtpmail-smtp-server "smtp.gmail.com")
     (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)))
              account-vars)
      (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)
      ("/prospecthive/INBOX"           . ?p)))

Define bookmarks.

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

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

(add-to-list 'mu4e-bookmarks
  '((concat "maildir:/personal/INBOX AND date:3d..now"
            " OR maildir:/prospecthive/INBOX AND date:3d..now"
            " OR maildir:/collabspot/INBOX AND date:3d..now") "Recent inbox" ?3))

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

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

(add-to-list 'mu4e-bookmarks
  '((concat "maildir:/personal/INBOX AND date:today..now"
            " OR maildir:/prospecthive/INBOX AND date:today..now"
            " OR maildir:/collabspot/INBOX AND date:today..now") "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"
                  :function
                  (lambda (msg)
                    (if (member "\\Important" (mu4e-message-field msg :tags))
                        "*"
                      " ")))))

(add-to-list
 'mu4e-header-info-custom
 '(:account . (:name "Account"
               :shortname "Acct"
               :help "Account name"
               :function
               (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))
                                                           begins))
                                                (t nil))))
                     (cond ((starts-with maildir "/collabspot") "collabspot")
                           ((starts-with maildir "/prospecthive") "prospecthive")
                           (t "personal"))))))))

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

Improve rendering of HTML only emails.

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

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)
    (save-current-buffer
      (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"
  (interactive)
  (nameframe-with-frame "mu4e"
    (persp-switch "mu4e")
    (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 date:today..now"
              " OR maildir:/prospecthive/INBOX AND date:today..now"
              " OR maildir:/collabspot/INBOX AND date:today..now)"
              " AND flag:unread"))
(add-hook 'after-init-hook #'mu4e-alert-enable-mode-line-display)

Ledger

(defconst *ledger-journal-path* "~/Dropbox/ledger/john.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*)))

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

Evil

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
  '(clojure-mode
    python-mode
    ruby-mode
    erlang-mode
    emacs-lisp-mode
    web-mode
    css-mode
    js2-mode
    js-mode
    json-mode
    html-mode
    ledger-mode
    yaml-mode
    elixir-mode
    org-mode
    sh-mode
    haskell-mode
    elm-mode
    purescript-mode
    markdown-mode))

Set the evil-leader.

(require 'evil-leader)
(evil-leader/set-leader ",")
(global-evil-leader-mode)

Enable Evil plugins.

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

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

Footnotes:

Author: John Del Rosario

Created: 2016-05-07 Sat 16:27

Validate