for hacking reality itself
Go to file
ndwarshuis 7d0e2145fd added org toc 2018-12-13 22:13:28 -05:00
conf added org toc 2018-12-13 22:13:28 -05:00
.gitignore update gitignore to not track .el files 2018-12-11 20:20:45 -05:00
README.org relinked readme 2018-12-11 19:34:38 -05:00
init.el added org toc 2018-12-13 22:13:28 -05:00

README.org

overview

purpose and usage

This is my personal emacs config.

I use this for a variety of purposes:

  • maintaining my todo list/GTD workflow (org mode)
  • unified interface for system administration (dired, shell, git, ediff)
  • email (mu4e)
  • writing some of my favorite languages (R, Lisp, Haskell, Lua, Python)
  • document preparation (latex)

for new users

Feel free to take bits and pieces for your own configuration file. Like many things in emacs, the config file is quite self documenting; however, there are some useful ramblings that decribe why I made some design choices over others. As someone who learned from countless emacs configs of other experienced users, I thought it was extremely beneficial to see the thought process behind their workflow and code, and I hope my annotations pay that forward. Finally, please don't just blindly copy this config into your ~/.emacs.d. I don't care if you do, but you will learn more if you build from scratch.

config structure

The "config file" is actually several files.

The "root" is init.el which is the file explicitly loaded by emacs. The init.el is sometimes the only file people use, but my configuration became so huge I decided to split it in several files to maintain sanity. init.el currently has minimum functionality, including setting the repositories, configuring the package use-package (which installs all other packages and ensures they are available if I move this config elsewhere), and load paths for the other config files as explained below. Basically, it's a bootloader for bigger, better config files.

Once loaded, the init.el pulls in another file called main.el from the conf directory with the function org-babel-load-file. main.el is actually sourced from an org file, which allows markdown and annotations surrounding each block of lisp code. Since github is awesome and understands org files as a valid markdown format, all readme files (including this one) are actually org file.

After main.el is loaded, it may pull in a number of other files that I deemed big enough to separate (again using the org-babel-load-file function). Each .el/.org file that is included is stored in a separate directory under conf; navigate to these directories to see the file in readme format as you are reading this one.

includes

These is general code that is used throughout the config file. Stored in another file for brevity.

(org-babel-load-file (expand-file-name "lib/lib.org" nd/conf-dir))

ui

theme

This theme has good functionality for many different modes without being over-the-top or overly complex. It also comes with an easy way to set custom colors.

(use-package spacemacs-theme
  :defer t
  :config
  (setq spacemacs-theme-custom-colors '((lnum . "#64707c"))))

Since I run emacs in client/server mode, the loaded theme can change depending on if the client is a terminal or server (terminals have far fewer colors). This makes the theme reset when terminal is loaded before gui or vice versa.

(defvar nd/theme 'spacemacs-dark)
(defvar nd/theme-window-loaded nil)
(defvar nd/theme-terminal-loaded nil)

;; required for emacsclient/daemon setup
(if (daemonp)
    (add-hook 'after-make-frame-functions
              (lambda (frame)
                (select-frame frame)
                (if (window-system frame)
                    (unless nd/theme-window-loaded
                      (if nd/theme-terminal-loaded
                          (enable-theme nd/theme)
                        (load-theme nd/theme t))
                      (setq nd/theme-window-loaded t))
                  (unless nd/theme-terminal-loaded
                    (if nd/theme-window-loaded
                        (enable-theme nd/theme)
                      (load-theme nd/theme t))
                    (setq nd/theme-terminal-loaded t)))))
  (progn
    (load-theme nd/theme t)
    (if (display-graphic-p)
        (setq nd/theme-window-loaded t)
      (setq nd/theme-terminal-loaded t))))

modeline

This modeline goes along with the spacemacs-theme. It also has nice integration with evil-mode (see keybindings below).

(use-package spaceline
  :ensure t
  :config
  (require 'spaceline-config)
  (setq powerline-default-separator 'arrow
        spaceline-buffer-size-p nil)
  (spaceline-spacemacs-theme))

(line-number-mode 1)
(column-number-mode 1)

delight

I like to keep the modeline clean and uncluttered. This package prevents certain mode names from showing in the modeline (it also has support for use-package through the :delight keyword)

(use-package delight
  :ensure t)

clean the interface

Emacs comes with some useless garbage by default. IMHO (in my haughty opinion), text editors should be boxes with text in them. No menu bars, scroll bars, or toolbars (and certainly no ribbons). Also, I don't need the startup screen; scratch buffer is fine.

(setq inhibit-startup-screen t)
(tool-bar-mode -1)
(menu-bar-mode -1)
(scroll-bar-mode -1)

other enhancements

popup windows

Some modes like to make popup windows (eg ediff). This prevents that.

(setq pop-up-windows nil)

line wrap

I don't like line wrap

(set-default 'truncate-lines t)

smooth scrolling

This makes scrolling smoother

(setq scroll-conservatively 100)

imagemagick

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

yes-no prompt

Some prompts require literal "yes" or "no" to decide action. Life is short and I would rather not waste keystrokes typing whole words. This makes all "yes/no" prompts only require "y" or "n."

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

autosave

Saving files continuously is actually really annoying and clutters my disk. Turn it off.

(setq make-backup-files nil)
(setq auto-save-default nil)

packages

beacon

This makes a nice glowy effect on the cursor when switching window focus. Very elegant way of saving time in finding where you left off.

(use-package beacon
  :ensure t
  :delight
  :init
  (beacon-mode 1))

whichkey

Everyone forgets keybindings. When typing a key chord, this will display a window with all possible completions and their commands.

(use-package which-key
  :ensure t
  :delight
  :init
  (which-key-mode))

helm

One of the best packages for emacs. Helm is basically a search and completion engine (other exanples being ido-mode and ivy-mode) which is mainly used for finding files and selecting commands (which are obviously used often). It also integrates well with many other modes such as evil-mode and org-mode.

(use-package helm
  :ensure t
  :delight
  :init
  (helm-mode 1)
  :config
  (setq helm-autoresize-max-height 0
        helm-autoresize-max-height 40
        helm-M-x-fuzzy-match t
        helm-buffers-fuzzy-matching t
        helm-recentf-fuzzy-match t
        helm-semantic-fuzzy-match t
        helm-imenu-fuzzy-match t
        helm-scroll-amount 8)
  (add-to-list 'display-buffer-alist
               `(,(rx bos "*helm" (* not-newline) "*" eos)
                 (display-buffer-in-side-window)
                 (inhibit-same-window . t)
                 (window-height . 0.4)))
  (helm-autoresize-mode 1)
  (require 'helm-config))

helm-swoop

(use-package helm-swoop
  :ensure t)

rainbow-delimiters

This color-codes matching parenthesis. Enable pretty much everywhere.

(use-package rainbow-delimiters
  :ensure t
  :delight
  :hook
  ((prog-mode . rainbow-delimiters-mode)
   (inferior-ess-mode . rainbow-delimiters-mode)
   (ess-mode . rainbow-delimiters-mode)
   (LaTeX-mode . rainbow-delimiters-mode)
   (Tex-latex-mode . rainbow-delimiters-mode)))

ace-window

This is an elegant window selector. It displays a number in the corner when activated, and windows may be chosen by pressing the corresponding number. Note that spacemacs fails to make the numbers look nice so the theme code is a workaround to make them smaller and prettier.

(use-package ace-window
  :ensure t
  :config
  (setq aw-background t)
  (custom-set-faces '(aw-leading-char-face 
                      ((t (:foreground "#292b2e"
                           :background "#bc6ec5"
                           :height 1.0
                           :box nil))))))

avy

Allows jumping to any character in any window with a few keystrokes. Goodbye mouse :)

  (use-package avy
    :ensure t
    :config
    (setq avy-background t))

sudo edit

Allows opening a file with sudo elevation.

  (use-package sudo-edit
    :ensure t)

undo tree

Displays undo history in a nice tree. Also dislays diff information.

(use-package undo-tree
  :ensure t
  :delight
  :config
  (setq undo-tree-visualizer-diff t)
  (global-undo-tree-mode))

fill-column-indicator

Displays a line at 80 characters as a guide for column width.

(use-package fill-column-indicator
  :ensure t
  :config
  (setq fci-rule-use-dashes t)
  :hook
  (prog-mode . fci-mode))

rainbow

Overlays hex color codes with matching colors in certain modes like css and html.

(use-package rainbow-mode
  :ensure t)

async

Allows certain processes to run in multithreaded manner. For things like IO this makes sense.

(use-package async
  :ensure t
  :delight dired-async-mode
  :init
  (dired-async-mode 1))

csv-mode

This adds support for csv files. Almost makes them editable like a spreadsheet. The lambda function enables alignment by default.

(use-package csv-mode
  :ensure t
  :hook (csv-mode . (lambda () (csv-align-fields nil (point-min) (point-max)))))

markdown-mode

Added support for standard markdown files.

(use-package markdown-mode
  :ensure t)

polymode

This allows multiple modes in one buffer. This may sound totally crazy…but it actually is. Despite it's hackiness, it actually makes alot of sense for some situations such as R markdown which requires yaml, markdown, and R code in one buffer.

(use-package polymode
  :ensure t
  :after markdown-mode
  :mode
  (("\\.Rmd\\'" . poly-markdown+r-mode)
   ("\\.rmd\\'" . poly-markdown+r-mode))
  :config
  (require 'poly-R)
  (require 'poly-markdown))

editing

tabs and alignment

First things first, don't use spaces and force tabs to 4 chars by default (because…let's compromise on things that don't matter since I am using spaces anyways)

(setq-default indent-tabs-mode nil
              tab-width 4)

completion

company

(use-package company
  :ensure t
  :delight " ©"
  :config
  (setq company-idle-delay 0
        company-minimum-prefix-length 3))

flycheck

(use-package flycheck
  :ensure t
  :hook
  (prog-mode . flycheck-mode)
  :config
  (setq flycheck-check-syntax-automatically '(save
                                              idle-change
                                              mode-enabled)
        flycheck-idle-change-delay 2
        flycheck-error-list-minimum-level 'warning
        flycheck-navigation-minimum-level 'warning))

yasnippet

(use-package yasnippet
  :ensure t)

(use-package yasnippet-snippets
  :ensure t
  :after yasnippet
  :hook
  ((prog-mode . yas-minor-mode))
  :config
  (yas-reload-all))

electric pairs

Complete pairs globally. Maybe will add more mode-specific options in addition to defaults (eg html-mode)

;; (electric-pair-mode t)

flyspell

Obviously I am going to use helm when I spellcheck something.

(use-package flyspell-correct-helm
  :ensure t
  :after (helm flyspell))

Additionally, I want to automatically highlight errors whenever flyspell-mode is enabled.

;; (add-hook 'flyspell-mode-hook 'flyspell-buffer)

progmode

(add-hook 'prog-mode-hook #'prettify-symbols-mode)
(add-hook 'prog-mode-hook #'flyspell-prog-mode)
(setq flyspell-issue-message-flag nil)

languages

elisp

(add-hook 'emacs-lisp-mode-hook 'company-mode)

ess

NOTES:

  • ess is not considered part of prog-mode for some reason
  • ess-mode requires a running R process for company to work
  • flycheck requries r-lintr
(defun nd/init-ess-company ()
  "Set the company backends for ess modes."
  (setq-local company-backends '((company-R-objects company-R-args))))

(use-package ess
  :ensure t
  :init
  (load "ess-site")
  :hook
  ((ess-mode . flycheck-mode)
   (ess-mode . company-mode)
   (ess-mode . nd/init-ess-company)
   (ess-mode . prettify-symbols-mode)
   (ess-mode . fci-mode)

   (inferior-ess-mode . company-mode)
   (inferior-ess-mode . nd/init-ess-company)
   (inferior-ess-mode . prettify-symbols-mode))
  :config
  (setq inferior-R-args "--quiet --no-save"
        ess-history-file "session.Rhistory"
        ess-history-directory (substitute-in-file-name "${XDG_CONFIG_HOME}/r/")))

python

(elpy-enable)

;; make python tabs 4 chars
(add-hook 'python-mode-hook
      (lambda ()
        (setq indent-tabs-mode t)
        (setq tab-width 4)
        (setq python-offset 4)))
        
(setq python-shell-interpreter "ipython"
      python-shell-interpreter-args "--colors=Linux --profile=default")

haskell

major mode and intero

Haskell is covered just with the basic major mode and intero (provides company and flycheck) which integrates well with stack.

(use-package haskell-mode
  :ensure t
  :config
  (setq haskell-interactive-popup-errors nil))
  
(use-package intero
  :ensure t
  :after haskell-mode
  :hook
  (haskell-mode . intero-mode))
camelCase

The defacto style for haskell mandates camelcase, so use subword mode.

(add-hook 'haskell-mode-hook #'subword-mode)

latex

flycheck

Flycheck should work out of the box.

(add-hook 'LaTeX-mode-hook #'flycheck-mode)
(add-hook 'Tex-latex-mode-hook #'flycheck-mode)
company

There are two backends which (kinda) complement each other. The company-math package should privide completion for math symbols and the company-auctex package should cover pretty much everything else.

(defun nd/init-company-auctex ()
  "Set the company backends for auctex modes."
  (setq-local company-backends '((company-auctex-labels
                                  company-auctex-bibs
                                  company-auctex-macros
                                  company-auctex-symbols
                                  company-auctex-environments
                                  ;; company-latex-commands
                                  company-math-symbols-latex
                                  company-math-symbols-unicode))))

(use-package company-math
  :ensure t
  :after company
  :config
  (setq company-math-allow-unicode-symbols-in-faces '(font-latex-math-face)
        company-math-disallow-latex-symbols-in-faces nil))

(use-package company-auctex
  :ensure t
  :after (company company-math)
  :hook
  ((LaTeX-mode . company-mode)
   (LaTeX-mode . nd/init-company-auctex)
   (Tex-latex-mode . company-mode)
   (Tex-latex-mode . nd/init-company-auctex)))
auto-fill-mode

I like having my lines short and readable (also easier to git). Turn on autofill here and also make a nice vertical line at 80 chars (visual-line-mode).

(defun nd/turn-on-auto-fill-maybe ()
  "Prompts user to turn on `auto-fill-mode'."
  (when (y-or-n-p "Activate Auto Fill Mode? ")
    (turn-on-auto-fill)))
  
(add-hook 'LaTeX-mode-hook #'nd/turn-on-auto-fill-maybe)
(add-hook 'LaTeX-mode-hook #'fci-mode)
flyspell

Spell checking is important for prose

(add-hook 'LaTeX-mode-hook (lambda () (flyspell-mode 1)))

org-mode

My org config is massive and therefore stored in another file.

(org-babel-load-file (expand-file-name "org/org.org" nd/conf-dir))

tools

printing

For some reason there is no default way to get a "print prompt." Instead one needs to either install some third-party helper or make a function like this.

(defun nd/helm-set-printer-name ()
  "Set the printer name using helm-completion to select printer."
  (interactive)
  (let ((pl (or helm-ff-printer-list (helm-ff-find-printers))))
    (if pl (setq printer-name (helm-comp-read "Printer: " pl)))))

magit

(use-package magit
  :ensure t
  :config
  :delight auto-revert-mode
  (setq magit-push-always-verify nil
        git-commit-summary-max-length 50))

dired

no confirm

Keeping confirmation enabled does weird stuff with helm. Not ideal at the moment but we shall see if I find something better.

(setq dired-no-confirm '(move copy))

compression

Only supports tar.gz, tar.bz2, tar.xz, and .zip by default. Add support for more fun algos such as lzo and zpaq

(if (file-exists-p "/usr/bin/7z")
    (add-to-list 'dired-compress-files-alist
                    '("\\.7z\\'" . "7z a %o %i")))

(if (file-exists-p "/usr/bin/lrzip")
    (progn
      (add-to-list 'dired-compress-files-alist
                   '("\\.lrz\\'" . "lrzip -L 9 -o %o %i &"))
      (add-to-list 'dired-compress-files-alist
                   '("\\.lzo\\'" . "lrzip -l -L 9 -o %o %i &"))
      (add-to-list 'dired-compress-files-alist
                   '("\\.zpaq\\'" . "lrzip -z -L 9 -o %o %i &"))))

;; NOTE: this must be after the shorter lrz algos otherwise it will
;; always default to .lrz and not .tar.lrz
(if (file-exists-p "/usr/bin/lrztar")
    (progn
      (add-to-list 'dired-compress-files-alist
                   '("\\.tar\\.lrz\\'" . "lrztar -L 9 -o %o %i &"))
      (add-to-list 'dired-compress-files-alist
                   '("\\.tar\\.lzo\\'" . "lrztar -l -L 9 -o %o %i &"))
      (add-to-list 'dired-compress-files-alist
                   '("\\.tar\\.zpaq\\'" . "lrztar -z -L 9 -o %o %i &"))))

formatting for humans

make sizes human readable

(setq dired-listing-switches "-Alh")

mu4e attachments

By default the included gnus-dired package does not understan mu4e, so override the existing gnus-dired-mail-buffers function to fix. This allows going to a dired buffer, marking files, and attaching them interactively to mu4e draft buffers.

;; from here:
;; https://www.djcbsoftware.nl/code/mu/mu4e/Dired.html#Dired
(require 'gnus-dired)

(eval-after-load 'gnus-dired
  '(defun gnus-dired-mail-buffers ()
     "Return a list of active mu4e 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)

directory sized

By default dired uses ls -whatever to get its output. This does not have recursive directory contents by default. This nitfy package solves this. This is not on default because navigation is much slower and the du output adds very little in many situations (toggle when needed).

(use-package dired-du
  :ensure t
  :config
  (setq dired-du-size-format t))

mounted devices

If dired is to replace all other file managers it must handle devices. This function assumes all my devices are mounted on /media/$USER and that udevil is installed. It provides mount and mount/follow ops for all usb removable media and follow/unmount for all mounted devices (note the latter includes things that are not mounted here such as samba drives, which I normally hotkey to my window manager). This almost replicates the functionality of gvfs that I actually use without the bloat; the only missing piece is MPT for android (which will come later).

(defun nd/helm-devices ()
  "Mount, unmount, and navigate to removable media using helm."
  (interactive)
  (let* ((mounted (mapcar
                   (lambda (d)
                     `(,(file-name-base d) . ,d))
                   (nd/get-mounted-directories)))
         (mountable (seq-filter
                     (lambda (d) (not (member (car d) (mapcar #'car mounted))))
                     (nd/get-mountable-devices))))
    (helm
     :sources
     (list
      (helm-build-sync-source "Mounted Devices"
        :candidates mounted
        :action
        '(("Open" . (lambda (s) (find-file s)))
          ("Unmount" . (lambda (s) (start-process "unmount" nil "udevil" "unmount" s)))))
      (helm-build-sync-source "Mountable Devices"
        :candidates mountable
        :action
        '(("Mount and Follow" . (lambda (s)
                                  (nd/mount-device s)
                                  (find-file (nd/get-mountpoint s))))
          ("Mount" . (lambda (s) (nd/mount-device s))))))
     :buffer "*helm device buffer*"
     :prompt "Device: ")))

mu4e

basic

(require 'mu4e)

(setq mail-user-agent 'mu4e-user-agent
      mu4e-maildir "/mnt/data/Mail"

      mu4e-attachment-dir "~/Downloads"
      
      mu4e-view-show-images t
      mu4e-headers-show-target nil
      
      mu4e-view-show-addresses t

      message-kill-buffer-on-exit t
      
      mu4e-change-filenames-when-moving t

      mu4e-confirm-quit nil

      mu4e-view-prefer-html t

      mu4e-compose-dont-reply-to-self t
      
      mu4e-get-mail-command "systemctl --user start mbsync"

      user-full-name "Dwarshuis, Nathan J")

headers view

(setq mu4e-headers-fields '((:human-date . 11)
                            (:flags . 5)
                            (:from . 22)
                            (:thread-subject))
      mu4e-headers-date-format "%F"
      mu4e-headers-time-format "%R"
      mu4e-use-fancy-chars nil)

citing

The citation line should enable history folding in outlook. This is enabled by using 32 underscores followed by the addressing info of the previous message(s).

;; necessary for the header macros below
(require 'nnheader)

(defun nd/message-insert-citation-header ()
  "Insert the header of the reply message."
  (let* ((h message-reply-headers)
         (sep "________________________________")
         (from (concat "From: " (mail-header-from h)))
         (date (concat "Sent: " (mail-header-date h)))
         (to (concat "To: " user-full-name))
         (subj (concat "Subject: " (message-strip-subject-re (mail-header-subject h)))))
    (insert (string-join `("" ,sep ,from ,date ,to ,subj "") "\n"))))
    
(setq message-citation-line-function 'nd/message-insert-citation-header)

The default "> " things are annoying when citing old messages.

(setq message-yank-prefix "")
(setq message-yank-cited-prefix "")
(setq message-yank-empty-prefix "")

By default the citation is destroyed (as in totally textified) if it is HTML. I want the links to be preserved, so use html2text and set arguments accordingly. Note that --body-width=0 is necessary to prevent line breaks from being inserted in the middle of links.

(setq
 mu4e-compose-pre-hook
 (lambda ()
   (let* ((msg mu4e-compose-parent-message)
          (html (and msg (plist-get msg :body-html)))
          ;; oops, mu4e screwed up
          (mu4e-html2text-command
           (if (file-exists-p "/usr/bin/html2text")
               "html2text --ignore-emphasis --images-to-alt --body-width=0"
             'mu4e-shr2text)))
     (when (and html mu4e-view-prefer-html (member compose-type '(reply forward)))
       ;; hackity hack, since the normal mu4e-message-body-text function
       ;; does not render the desired html, do it here and force the
       ;; aforementioned function to only look at text by removing
       ;; the html
       (plist-put msg :body-txt (mu4e~html2text-shell msg mu4e-html2text-command))
       (plist-put msg :body-html nil)))))

smtp

(require 'smtpmail)
;; (require 'smtpmail-async)
;; (require 'secrets)
;; (setq secrets-enabled t)
(setq send-mail-function 'smtpmail-send-it
      message-send-mail-function 'smtpmail-send-it)
(add-to-list 'auth-sources (expand-file-name "~/.emacs.d/.authinfo_mu4e.gpg"))
;; (add-to-list 'auth-sources "secrets:default")

contexts

I have current have three contexts, personal and two work accounts. The first is a gmail account and the second/third are office365 accounts.

(setq mu4e-context-policy 'pick-first
      mu4e-compose-context-policy 'ask-if-none
      mu4e-user-mail-address-list '("natedwarshuis@gmail.com" "ndwarshuis3@gatech.edu" "ndwarsh@emory.edu")
      
      mu4e-contexts
      `( ,(make-mu4e-context
           :name "personal"
           :match-func
           (lambda (msg)
             (when msg
               (let ((pfx (mu4e-message-field msg :maildir)))
                 (string-prefix-p "/gmail" pfx))))
           :vars '((mu4e-trash-folder . "/gmail/trash")
                   (mu4e-drafts-folder . "/gmail/drafts")
                   (mu4e-sent-folder . "/gmail/sent")
                   (mu4e-refile-folder . "/gmail/archive")
                   (mu4e-sent-messages-behavior . delete)
                   (smtpmail-stream-type . starttls)
                   (smtpmail-smtp-server . "smtp.gmail.com")
                   (smtpmail-smtp-service . 587)
                   (smtpmail-smtp-user . "natedwarshuis@gmail.com")
                   (user-mail-address . "natedwarshuis@gmail.com")
                   (mu4e-maildir-shortcuts .
                                           (("/gmail/inbox" . ?i)
                                            ("/gmail/sent" . ?s)
                                            ("/gmail/trash" . ?t)
                                            ("/gmail/drafts" . ?d)
                                            ("/gmail/archive" . ?a)))))
         ,(make-mu4e-context
           :name "gatech"
           :match-func
           (lambda (msg)
             (when msg
               (let ((pfx (mu4e-message-field msg :maildir)))
                 (string-prefix-p "/gatech" pfx))))
           :vars '((mu4e-trash-folder . "/gatech/trash")
                   (mu4e-drafts-folder . "/gatech/drafts")
                   (mu4e-sent-folder . "/gatech/sent")
                   (mu4e-refile-folder . "/gatech/archive")
                   (mu4e-sent-messages-behavior . sent)
                   (smtpmail-stream-type . starttls)
                   (smtpmail-smtp-server . "smtp.office365.com")
                   (smtpmail-smtp-service . 587)
                   (smtpmail-smtp-user . "ndwarshuis3@gatech.edu")
                   (user-mail-address . "ndwarshuis3@gatech.edu")
                   (mu4e-maildir-shortcuts .
                                           (("/gatech/inbox" . ?i)
                                            ("/gatech/sent" . ?s)
                                            ("/gatech/trash" . ?t)
                                            ("/gatech/drafts" . ?d)
                                            ("/gatech/archive" . ?a)))))
         ,(make-mu4e-context
           :name "emory"
           :match-func
           (lambda (msg)
             (when msg
               (let ((pfx (mu4e-message-field msg :maildir)))
                 (string-prefix-p "/emory" pfx))))
           :vars '((mu4e-trash-folder . "/emory/trash")
                   (mu4e-drafts-folder . "/emory/drafts")
                   (mu4e-sent-folder . "/emory/sent")
                   (mu4e-refile-folder . "/emory/archive")
                   (mu4e-sent-messages-behavior . sent)
                   (smtpmail-stream-type . starttls)
                   (smtpmail-smtp-server . "smtp.office365.com")
                   (smtpmail-smtp-service . 587)
                   (smtpmail-smtp-user . "ndwarsh@emory.edu")
                   (user-mail-address . "ndwarsh@emory.edu")
                   (mu4e-maildir-shortcuts .
                                           (("/emory/inbox" . ?i)
                                            ("/emory/sent" . ?s)
                                            ("/emory/trash" . ?t)
                                            ("/emory/drafts" . ?d)
                                            ("/emory/archive" . ?a)))))))

org-mu4e

(use-package org-mu4e
  :after (org mu4e)
  :config
  (setq
   ;; for using mu4e in org-capture templates
   org-mu4e-link-query-in-headers-mode nil
   ;; for composing rich-text emails using org mode
   org-mu4e-convert-to-html t))

signature

Signatures take lots of space and make short messages look needlessly clunky, so keep off by default.

(setq mu4e-compose-signature-auto-include nil

      mu4e-compose-signature
      (string-join
       '("Nathan Dwarshuis"
         ""
         "PhD Student - Biomedical Engineering - Krish Roy Lab"
         "Georgia Institute of Technology and Emory University"
         "ndwarshuis3@gatech.edu")
       "\n"))

visual-line-mode

By default mu4e adds breaks after 80-ish chars using auto-fill-mode which makes messages look weird when opened. Visual-line-mode avoids this problem.

(add-hook 'mu4e-compose-mode-hook 'turn-off-auto-fill)
(add-hook 'mu4e-compose-mode-hook 'visual-line-mode)
(add-hook 'mu4e-view-mode-hook 'turn-off-auto-fill)
(add-hook 'mu4e-view-mode-hook 'visual-line-mode)

flyspell

Spell checking is generally a good idea when writing to pointy-haired bosses.

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

auctex

(load "auctex.el" nil t t)
(require 'tex-mik)

(setq TeX-view-program-selection '(((output-dvi has-no-display-manager)
                                    "dvi2tty")
                                   ((output-dvi style-pstricks)
                                    "dvips and gv")
                                   (output-dvi "xdvi")
                                   (output-pdf "Okular")
                                   (output-html "xdg-open")))

;; remove ugly section size
(setq font-latex-fontify-sectioning 'color)

(add-hook 'LaTeX-mode-hook (lambda () (outline-minor-mode 1)))
(add-hook 'Tex-latex-mode-hook (lambda () (outline-minor-mode 1)))

(use-package outline-magic
  :ensure t
  :after outline)

bibtex

(use-package org-ref
  :ensure t
  :after org
  :config
  (setq reftex-default-bibliography (expand-file-name "~/BibTeX/master.bib")
        org-ref-bibliography-notes (expand-file-name "~/BibTeX/notes.org")
        org-ref-default-bibliography (expand-file-name "~/BibTeX/master.bib")))

(use-package helm-bibtex
  :ensure t
  :after helm
  :config
  (setq bibtex-completion-bibliography (expand-file-name "~/BibTeX/master.bib")
        bibtex-completion-library-path (expand-file-name "~/BibTeX/pdf")
        bibtex-completion-pdf-field "File"))

ebib

(use-package ebib
  :ensure t)

shell

(defadvice ansi-term (before force-bash)
  (interactive (list "/bin/zsh")))
(ad-activate 'ansi-term)

(defun nd/term-send-raw-escape ()
  "Send a raw escape character to the running terminal."
  (interactive)
  (term-send-raw-string "\e"))

ediff

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

keybindings

For the sake of my sanity, all bindings go here. Note this means I don't use :bind in use-package forms.

setup

Most of my modifiers are reloacted using xkb and xcape. Below is a summary where each item is in the form <original key> -> <new key action> (<key release action if used>)

  • tab -> l_super (tab)
  • backslash -> r_super (backslash)
  • caps -> l_ctrl (escape)
  • return -> r_ctrl (return)
  • l_ctrl -> l_hyper
  • l_super -> iso_l3_shift (xf86search)
  • space -> r_alt (space)
  • r_alt -> r_hyper
  • r_ctrl -> caps

evil

I like being evil. All package and custom bindings go here.

base

(use-package evil
  :ensure t
  :init
  ;; this is required to make evil collection work
  (setq evil-want-integration nil)
  :config
  (evil-mode 1))

motion

By default, emacs counts a sentence as having at least 2 spaces after punctuation. Make this behave more like vim.

(setq sentence-end-double-space nil)

evil state defaults

Some modes use primitive emacs bindings by default. Educate them.

(add-to-list 'evil-motion-state-modes 'ess-help-mode)
(add-to-list 'evil-insert-state-modes 'inferior-ess-mode)

enhancements

delightfully ripped off from vim plugins

surround
(use-package evil-surround
  :ensure t
  :after evil
  :config
  (global-evil-surround-mode 1))
commentary
(use-package evil-commentary
  :ensure t
  :after evil
  :delight
  :config
  (evil-commentary-mode))
replace with register
(use-package evil-replace-with-register
  :ensure t
  :after evil
  :config
  (evil-replace-with-register-install))

unbind emacs keys

Some of these commands just get in the way of being evil (which really means that I keep pressing them on accident). Rather than nullifying them completely, tuck them away in the emacs state map in case I actually want them.

(mapc (lambda (k) (nd/move-key global-map evil-emacs-state-map (eval k)))
      '((kbd "C-s")
        (kbd "C-p")
        (kbd "C-n")
        (kbd "C-f")
        (kbd "C-b")
        (kbd "C-a")
        (kbd "C-e")
        (kbd "C-<SPC>")
        
        (kbd "C-x C-;")
        (kbd "C-x C-l")
        (kbd "C-x C-u")
        (kbd "C-x C-z")
        (kbd "C-x C-c")

        (kbd "M-c")
        (kbd "M-d")
        (kbd "M-e")
        (kbd "M-r")
        (kbd "M-f")
        (kbd "M-h")
        (kbd "M-j")
        (kbd "C-M-j")
        (kbd "M-k")
        (kbd "M-l")
        (kbd "M-m")
        (kbd "M-q")
        (kbd "M-w")
        (kbd "M-t")
        (kbd "M-u")
        (kbd "M-i")
        (kbd "M-z")
        (kbd "M-v")
        (kbd "M-/")
        (kbd "M-;")
        (kbd "M-DEL")))

evil-org

(use-package evil-org
  :ensure t
  :after (evil org)
  :delight
  :config
  (add-hook 'org-mode-hook 'evil-org-mode)
  (add-hook 'evil-org-mode-hook 'evil-org-set-key-theme)

  (require 'evil-org-agenda)
  (evil-org-agenda-set-keys)
  ;; some of the defaults bug me...
  (evil-define-key 'motion org-agenda-mode-map
    "t" 'nd/toggle-project-toplevel-display
    "D" 'org-agenda-day-view
    "W" 'org-agenda-week-view
    "M" 'org-agenda-month-view
    "Y" 'org-agenda-year-view
    "ct" nil
    "sC" 'nd/org-agenda-filter-non-context
    "sE" 'nd/org-agenda-filter-non-effort
    "sD" 'nd/org-agenda-filter-delegate
    "sP" 'nd/org-agenda-filter-non-peripheral
    "e" 'org-agenda-set-effort
    "ce" nil))

evil-magit

(use-package evil-magit
  :ensure t
  :after (evil magit))

visual line mode

This is somewhat strange because all I really care about is moving between lines and to the beginning and end as normal. However, I like the idea of thinking of paragraphs as one line (eg df. deletes a sentence even if on multiple lines). Opinion subject to change.

(evil-define-key '(normal visual) 'visual-line-mode
  "j" 'evil-next-visual-line
  "k" 'evil-previous-visual-line
  "0" 'beginning-of-visual-line
  "$" 'end-of-visual-line)

comint

Comint-based inferior modes often are not evil (eg intero and ESS). Configure this similarly to term (see below) where C-j/k navigate cmd history and insert mode goes to cmd input line.

interactive functions

Some common interactive functions for comint-based modes

(defun nd/comint-char-mode-evil-insert ()
  "If not at the last line, go to the end of the buffer and enter insert mode.  Else just enter insert mode."
  (interactive)
  (if (/= (line-number-at-pos (point)) (line-number-at-pos (point-max)))
        (goto-char (point-max))))
        
(defun nd/comint-send-input-evil-insert (&optional send-input-cmd)
  "Go into insert mode after calling SEND-INPUT-CMD which is usually
the function that send the command to the interactive process in the
REPL. If no SEND-INPUT-CMD then `comint-send-input' is used."
  (interactive)
  (if send-input-cmd (funcall send-input-cmd) (comint-send-input))
  (evil-insert 1))
        
(evil-define-key '(normal insert) comint-mode-map
  (kbd "C-k") 'comint-previous-input
  (kbd "C-j") 'comint-next-input)
ess
(evil-define-key 'normal inferior-ess-mode-map
  (kbd "RET") (lambda () nd/comint-send-input-evil-insert
                'inferior-ess-send-input))

(add-hook 'inferior-ess-mode-hook
          (lambda ()
            (add-hook 'evil-insert-state-entry-hook
                      'nd/comint-char-mode-evil-insert nil t)))
intero
(evil-define-key 'normal intero-repl-mode-map
  (kbd "RET") 'nd/comint-send-input-evil-insert)
  
(add-hook 'intero-repl-mode-hook
          (lambda ()
            (add-hook 'evil-insert-state-entry-hook
                      'nd/comint-char-mode-evil-insert nil t)))

collection

Most packages that don't have an evil version are in this one. I don't like surprises so I set evil-collection-modes-list with the modes I actually want. Some of these are further configured below.

(use-package evil-collection
  :ensure t
  :after evil
  :init
  (setq evil-collection-mode-list
        '(company dired ediff flycheck helm minibuffer mu4e term which-key))
  (setq evil-collection-setup-minibuffer t)
  :config
  (evil-collection-init))
dired

Dired makes new buffers by default. Use find-alternate-file to avoid this.

(defun nd/dired-move-to-parent-directory ()
  "Move buffer to parent directory (like 'cd ..')."
  (interactive)
  (find-alternate-file ".."))

(defun nd/dired-xdg-open ()
  "Open all non-text files in external app using xdg-open.
Only regular files are considered."
  (interactive)
  (let* ((file-list (seq-filter #'file-regular-p (dired-get-marked-files)))
         (do-it (if (<= (length file-list) 5)
                    t
                  (y-or-n-p "Open more then 5 files? "))))
    (when do-it
      (mapc
       (lambda (f) (let ((process-connection-type nil))
                (start-process "" nil "xdg-open" f)))
       file-list))))

(defun nd/dired-open-with ()
  "Open marked non-text files in external app via open-with dialog
according to mime types as listed in all available desktop files."
  (interactive)
  (let* ((mf (seq-filter #'file-regular-p (dired-get-marked-files)))
         (qmf (mapcar #'shell-quote-argument mf))
         (file-mime-list (mapcar (lambda (f) (list f (nd/get-mime-type f))) qmf)))

    (if (= (length file-mime-list) 0)
        (message "No files selected")
      
      (let* ((first-pair (car file-mime-list))
             (last-pairs (cdr file-mime-list))
             mime-alist file-list)
        (setq file-list (nth 0 first-pair)
              mime-alist (nd/get-apps-from-mime (nth 1 first-pair)))
        ;; if multiple files selected, add to the selection list
        (if last-pairs
            (progn
              (setq file-list (string-join (mapcar #'car file-mime-list) " "))
              (dolist (mime (mapcar (lambda (f) (nth 1 f)) last-pairs))
                (setq mime-alist (intersection mime-alist
                                               (nd/get-apps-from-mime mime)
                                               :test #'equal)))))
        (if (= (length mime-alist) 0)
            (let* ((ml (delete-dups (mapcan #'cdr file-mime-list)))
                   (mls (string-join ml ", ")))
              (if (= (length ml) 1)
                  (message (concat "No apps found for mime type: "  mls))
                (message (concat "No common apps found for mime types: " mls))))
          (helm
           :sources (helm-build-sync-source "Apps"
                      :candidates mime-alist
                      :action '(("Open" . (lambda (f) (nd/execute-desktop-command f file-list)))))
           :buffer "*helm open with*"))))))

(defun nd/dired-sort-by ()
  "Sort current dired buffer by a list of choices presented in helm menu.
Note this assumes there are no sorting switches on `dired-ls'"
  (interactive)
  (let ((sort-alist '(("Name" . "")
                      ("Date" . "-t")
                      ("Size" . "-S")
                      ("Extension" . "-X")
                      ("Dirs First" . "--group-directories-first"))))
    (helm
     :sources
     (helm-build-sync-source "Switches"
       :candidates sort-alist
       :action
       '(("Sort" . (lambda (s) (dired-sort-other (concat dired-listing-switches " " s))))))
     :buffer "*helm sort buffer*")))

(put 'dired-find-alternate-file 'disabled nil)

(evil-define-key 'normal dired-mode-map
  "a" 'dired-find-file
  "za" 'gnus-dired-attach
  "gs" 'nd/dired-sort-by
  "^" 'nd/dired-move-to-parent-directory
  "q" 'nd/kill-current-buffer
  (kbd "<return>") 'dired-find-alternate-file
  (kbd "C-<return>") 'nd/dired-xdg-open
  (kbd "M-<return>") 'nd/dired-open-with)
helm

I like tab completion…regardless of what the helm zealots say. This is actually easier and faster because I can just scroll through the source list with j/k and mash TAB when I find the right directory.

(evil-define-key '(normal insert) helm-map
  (kbd "<tab>") 'helm-execute-persistent-action
  (kbd "C-<tab>") 'helm-select-action)
term

Since I use vi mode in my terminal emulator, need to preserve the escape key's raw behavior

(evil-define-key 'insert term-raw-map
  (kbd "<escape>") 'nd/term-send-raw-escape
  (kbd "C-<escape>") 'evil-normal-state)

local

These are for mode-specific bindings that can/should be outside of the evil maps above (there are not many, and these may be merged with their evil bretheren in the future).

org-mode

(add-hook 'org-mode-hook
          (lambda ()
            ;; override default TODO timestamp creation to insert the creation date
            (local-set-key (kbd "M-S-<return>") 'nd/org-insert-todo-heading-inactive-timestamp)

            ;; use the hyper keys/vim arrows with the shifters instead of shift/arrows
            (local-set-key (kbd "H-k") 'org-shiftup)
            (local-set-key (kbd "H-l") 'org-shiftright)
            (local-set-key (kbd "H-j") 'org-shiftdown)
            (local-set-key (kbd "H-h") 'org-shiftleft)

            ;; this is just a useful function I made (actually I think I stole)
            (local-set-key (kbd "C-c C-x x") 'nd/mark-subtree-done)

            ;; override default org subtree cloning with something that clones and resets
            (local-set-key (kbd "C-c C-x c") 'nd/org-clone-subtree-with-time-shift)))
            
(add-hook 'org-agenda-mode-hook
          (lambda ()
            (local-set-key (kbd "C-c C-c") 'org-agenda-set-tags)
            (local-set-key (kbd "C-c C-x c") 'nd/org-agenda-clone-subtree-with-time-shift)
            (local-set-key (kbd "C-c C-x C-b") 'nd/org-agenda-toggle-checkbox)))

mu4e

(define-key mu4e-headers-mode-map (kbd "C-c C-l") 'org-store-link)
(define-key mu4e-view-mode-map (kbd "C-c C-l") 'org-store-link)

dired

(define-key dired-mode-map (kbd "C-x g") 'magit)

helm-prefix

Some of these are useful enough that I make give them a direct binding without requiring a prefix. For now this is fine.

(define-key helm-command-prefix (kbd "b") 'helm-bibtex)
(define-key helm-command-prefix (kbd "S") 'helm-swoop)
(define-key helm-command-prefix (kbd "<f8>") 'helm-resume)

Give f to nd/helm-flyspell-correct instead of helm-multi-files and give the latter F (used much less).

(define-key helm-command-prefix (kbd "f") 'helm-flyspell-correct)
(define-key helm-command-prefix (kbd "F") 'helm-multi-files)

outline-magic

(define-key outline-minor-mode-map (kbd "<tab>") 'outline-cycle)

global

function

The function keys are nice because they are almost (not always) free in every mode. Therefore I use these for functions that I need to access anywhere, but not necessary extremely often (because they are out of the way and harder to reach).

(global-set-key (kbd "<f1>") 'org-agenda)
(global-set-key (kbd "<f2>") 'org-capture)
(global-set-key (kbd "<f3>") 'cfw:open-org-calendar)
(global-set-key (kbd "<f4>") 'org-clock-goto)
(global-set-key (kbd "<f5>") 'ansi-term)
(global-set-key (kbd "<f8>") 'helm-command-prefix)
(global-set-key (kbd "C-<f5>") 'nd/open-urxvt)
(global-set-key (kbd "<f12>") 'mu4e)
(global-set-key (kbd "C-<f12>") 'global-hl-line-mode)
(global-set-key (kbd "S-<f12>") 'display-line-numbers-mode)

control/meta

(global-set-key (kbd "C-<SPC>") 'company-complete)

(global-set-key (kbd "C-c e") 'nd/config-visit)
(global-set-key (kbd "C-c r") 'nd/config-reload)
(global-set-key (kbd "C-c s") 'sudo-edit)

(global-set-key (kbd "C-x 2") 'nd/split-and-follow-horizontally)
(global-set-key (kbd "C-x 3") 'nd/split-and-follow-vertically)
(global-unset-key (kbd "C-x c"))
(global-set-key (kbd "C-x k") 'nd/kill-current-buffer)
(global-set-key (kbd "C-x C-d") 'helm-bookmarks)
(global-set-key (kbd "C-x C-c C-d") 'nd/helm-devices)
(global-set-key (kbd "C-x C-f") 'helm-find-files)
(global-set-key (kbd "C-x C-b") 'helm-buffers-list)

(global-set-key (kbd "C-M-S-k") 'nd/close-all-buffers)
(global-set-key (kbd "C-M-S-o") 'nd/org-close-all-buffers)
(global-set-key (kbd "C-M-S-a") 'org-agenda-kill-all-agenda-buffers)

(global-set-key (kbd "M-b") 'nd/switch-to-previous-buffer)
(global-set-key (kbd "M-o") 'ace-window)
(global-set-key (kbd "M-s") 'avy-goto-char)
(global-set-key (kbd "M-x") 'helm-M-x)